C语言期末习题汇总(超级硬核系列)
结构体
题目1: 结构体的基本概念
如有以下代码:
struct student
{
int num;
char name[32];
float score;
}stu;
则下面的叙述不正确的是:( )
A.struct 是结构体类型的关键字
B.struct student 是用户定义的结构体类型
C.num, score 都是结构体成员名
D.stu 是用户定义的结构体类型名
解析
struct student
{
int num;
char name[32];
float score;
}stu;//stu是变量
答案 :D
题目2:结构体成员的访问
下面程序的输出结果是:( )
struct stu
{
int num;
char name[10];
int age;
};
void fun(struct stu *p)
{
printf("%s\n",(*p).name);
return;
}
int main()
{
struct stu students[3] = {{9801,"zhang",20},
{9802,"wang",19},
{9803,"zhao",18} };
fun(students + 1);
return 0;
}
A.zhang
B.zhao
C.wang
D.18
解析
(*p).name 是访问结构体,fun(students+1)意思是访问结构体的第二个成员,不管结构体的实例是什么——访问其成员其实就是加成员的偏移量。
答案 :C
题目3:结构体成员的访问
下面程序要求输出结构体中成员a的数据,以下不能填入横线处的内容是( )
#include < stdio.h >
struct S
{
int a;
int b;
};
int main( )
{
struct S a, *p=&a;
a.a = 99;
printf( "%d\n", __________);
return 0;
}
A.a.a
B.*p.a
C.p->a
D.(*p).a
解析
指针->成员
结构体变量. 成员
答案 :B
题目4:结构体对齐原则(超级重要)
在VS2013下,默认对齐数为8字节,这个结构体所占的空间大小是( )字节
typedef struct{
int a;
char b;
short c;
short d;
}AA_t;
A.16
B.9
C.12
D.8
解析
这个题目需要我们去画出内存图
图例1:
步骤一:首先标出所有类型的数的字节大小
步骤二:与默认对齐数进行对比,判断所占空间大小
步骤三:从0开始,开始计算,根据对齐原则,得到上述结果
题目5:结构体对齐原则
在32位系统环境,编译选项为4字节对齐,那么sizeof(A)和sizeof(B)是( )
struct A
{
int a;
short b;
int c;
char d;
};
struct B
{
int a;
short b;
char c;
int d;
};
A.16,16
B.13,12
C.16,12
D.11,16
解析
题目6:结构体对齐原则
下面代码的结果是:( )
#pragma pack(4)/*编译选项,表示4字节对齐 平台:VS2013。语言:C语言*/
//假设long 是4个字节
int main(int argc, char* argv[])
{
struct tagTest1
{
short a;
char d;
long b;
long c;
};
struct tagTest2
{
long b;
short c;
char d;
long a;
};
struct tagTest3
{
short c;
long b;
char d;
long a;
};
struct tagTest1 stT1;
struct tagTest2 stT2;
struct tagTest3 stT3;
printf("%d %d %d", sizeof(stT1), sizeof(stT2), sizeof(stT3));
return 0;
}
#pragma pack()
A.12 12 16
B.11 11 11
C.12 11 16
D.11 11 16
题目7:位段大小的计算
有如下宏定义和结构定义
#define MAX_SIZE A+B
struct _Record_Struct
{
unsigned char Env_Alarm_ID : 4;
unsigned char Para1 : 2;
unsigned char state;
unsigned char avail : 1;
}*Env_Alarm_Record;
struct _Record_Struct *pointer = (struct _Record_Struct*)malloc(sizeof(struct _Record_Struct) * MAX_SIZE);
当A=2, B=3时,pointer分配( )个字节的空间。
A.20
B.15
C.11
D.9
解析
#define MAX_SIZE 2+3
struct _Record_Struct
{
unsigned char Env_Alarm_ID : 4;//1个字节是八个比特位,这个位段是占4个比特位
unsigned char Para1 : 2;//这个是占2个比特位
//上面两个一共占了6个比特位,占了1个字节
unsigned char state;//没有进行表识,说明了它独占1个字节
unsigned char avail : 1;//这也占了一个字节
}*Env_Alarm_Record;
struct _Record_Struct *pointer = (struct _Record_Struct*)malloc(sizeof(struct _Record_Struct) * 2+3);
答案:D
题目8:位段大小的计算+大小端字节序存储+结构体访问
下面代码的结果是( )
int main()
{
unsigned char puc[4];
struct tagPIM
{
unsigned char ucPim1;
unsigned char ucData0 : 1;
unsigned char ucData1 : 2;
unsigned char ucData2 : 3;
}*pstPimData;
pstPimData = (struct tagPIM*)puc;
memset(puc,0,4);
pstPimData->ucPim1 = 2;
pstPimData->ucData0 = 3;
pstPimData->ucData1 = 4;
pstPimData->ucData2 = 5;
printf("%02x %02x %02x %02x\n",puc[0], puc[1], puc[2], puc[3]);
return 0;
}
A.02 03 04 05
B.02 29 00 00
C.02 25 00 00
D.02 29 04 00
解析
枚举
题目1:枚举类型的存储类型
以下关于枚举的说法,哪个是正确的?()
A.枚举类型可以具有浮点数作为枚举值。
B.枚举类型在内存中存储为整数。
C.枚举类型可以包含字符串作为枚举值。
D.枚举类型不能作为函数的参数传递。
解析:
枚举类型在内存中存储为整数,浮点数和整数都不可以,正确答案为B。
D枚举类型能作为函数的参数传递
例子如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
enum COLOR
{
RED,
BLACK
}co;
void Func(enum COLOR x)
{
}
int main()
{
Func(RED);
return 0;
}
题目2:枚举的计数方式
下面代码的结果是:( )
enum ENUM_A
{
X1,
Y1,
Z1 = 255,
A1,
B1,
};
enum ENUM_A enumA = Y1;
enum ENUM_A enumB = B1;
printf("%d %d\n", enumA, enumB);
A.1, 4
B.1, 257
C.2, 257
D.2, 5
解析:
因为枚举没有特意说明,默认是从零开始,所以X1是0,Y1是1,但是Z1是已经定义好255,所以默认是从255开始计数,所以B1=257
答案为B
题目3:枚举常量和#define定义常量的区别
关于枚举常量和#define 定义常量,下面哪个表述是不严谨的?()
A.可读性更好:枚举符号提供了有意义的标识符,可以更清晰地表达代码的意图。相比之下,#define定义的符号只是简单的文本替换,不具备语义上的含义。
B.和#define定义的标识符比较枚举有类型检查,更加严谨
C.枚举常量是遵循作用域规则的,枚举声明在函数内,只能在函数内使用,#define定义的符号没有作用域概念,会在整个代码中进行文本替换
D.#define没有类型检查,使用更加方便,更加推荐使用
解析:
答案为D,没有谁好谁坏之分,相对来说
联合体
题目1:联合体大小计算
下面代码的结果是:( )
#include <stdio.h>
union Un
{
short s[7];
int n;
};
int main()
{
printf("%d\n", sizeof(union Un));
return 0;
}
A.14
B.4
C.16
D.18
解析:
联合的⼤⼩⾄少是最⼤成员的⼤⼩。
当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍。
这个题目中最大成员的大小是short s[7],14个字节,然后可以排除B,接着还要满足条件2,int 类型是4个字节大小,VS默认的对齐数是8,会跟VS进行大小的比较,取较小数作为最大对齐数,所以4是最大对齐数,但是14不是4的整数倍,所以答案是16,选择C
题目2:结构体大小计算+大小端字节序存储
在X86下,小端字节序存储,有下列程序
#include<stdio.h>
int main()
{
union
{
short k;
char i[2];
}*s, a;
s = &a;
s->i[0] = 0x39;
s->i[1] = 0x38;
printf("%x\n", a.k);
return 0;
}
输出结果是( )
A.3839
B.3938
C.380039
D.不确定
题干中涉及到大小端字节序存储,要画出内存图来进行判断
如下:
对联合体进行计算,根据计算规则,大小至少是最大成员的大小,且满足是最大对齐数的整数倍,所以结果是2,按照从零开始画出内存图,根据上面的代码可以清楚的看出0里面存放的是0x39,1里面存放的是0x38,也要注意规定低地址和高地址,小段字节序存储会和我们的常识发生相反的认识,所以最后的答案是3839,答案是A
⼤⼩端字节序和字节序判断
#include <stdio.h>
int main()
{
int a = 0x11223344;
return 0;
}
看调试结果:
在a中的 0x11223344 这个数字是按照字节为单位,倒着存储的。
这就是一个小段字节序存储
题目3:使用联合体判断大小段
使用联合体的知识,写一个函数判断当前机器是大端还是小端,如果是小端返回1,如果是大端返回0.
引入:
int main()
{
int a=1;
//0x 00 00 00 01
//在内存中的存储方式
return 0;
}
此图为大小端字节序存储的图
如果说我们要判断哪个是大端,哪个是小端,我们只需要看其第一位就好了,00就是大端,01就是小端
根据我们之前所学的知识:
可以写出以下编程:
int main()
{
int a=1;
if(*(char*)&a==1)//我们只需要知道第一个字节大小,强制类型转化为char*,然后对其解引用判断值是否为1,进而判断大小端
{
printf("小端\n");
}
else
{
printf("大端\n");
}
}
接下来是考虑联合体方面了
01开头的是小端,00开头的是大端
union Un
{
char c;
int i;
};
int main()
{
union Un un={0};
un.i=1;//根据结构体计算原则来看的
if(un,c==1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
}
int check_sys()
{
union
{
int i;
char c;
}un;
un.i = 1;
return un.c;//返回1是⼩端,返回0是⼤端
}
动态内存管理
题目1:对malloc函数的理解
关于动态内存函数的说法错误的是:( )
A.malloc函数向内存申请一块连续的空间,并返回起始地址
B.malloc申请空间失败,返回NULL指针
C.malloc可以向内存申请0字节的空间
D.malloc申请的内存空间,不进行释放也不会对程序有影响
解析:
答案为D,那么free函数的意义在哪里呢,而且释放完成之后还要对其置空
题目2:对malloc,calloc,realloc函数的理解
关于动态内存相关函数说法错误的是:( )
A.malloc函数和calloc函数的功能是相似的,都是申请一块连续的空间。
B.malloc函数申请的空间不初始化,calloc函数申请的空间会被初始化为0
C.realloc函数可以调整动态申请内存的大小,可大可小
D.free函数不可以释放realloc调整后的空间
解析:
A &B.calloc函数的参数有两个,并且它的初始值为0,而malloc函数的为随机值
D:只要是动态内存相关函数申请的空间,都要用free函数释放空间,所以答案为D
题目3:空间分布
动态申请的内存在内存的那个区域?( )
A.栈区
B.堆区
C.静态区
D.文字常量区
解析,答案为B
非常重要
题目4:动态内存管理的应用(校招常考,重点内容)
关于下面代码描述不正确的是:()
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
A.上面代码没问题
B.上面代码存在内存泄露
C.上面代码可能会崩溃,即使GetMemory函数返回,str依然为NULL
D.GetMemory函数无法把malloc开辟的100个字节带回来
解析:
答案为A,这是显而易见的,最简单的B选项,没有看到free函数释放空间,所以会存在内存泄露
它没有给str分配内存空间,所以CD都是对的,怎样进行修改呢?
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void GetMemory(char** p)//二级指针,接受的是一级指针的地址
{
*p = (char*)malloc(100);//对p进行解引用,开辟对应的空间
}
void Test()
{
char* str = NULL;
GetMemory(&str);//进行传址调用
strcpy(str, "hello world");
printf("%s\n",str);
free(str);
str == NULL;//对于动态内存相关函数都要free释放空间,并且置空
}
int main() {
Test();
return 0;
}
运行结果:
题目5:局部变量和动态内存管理函数的区别
关于下面代码描述正确的是( )
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
A.printf函数使用有问题
B.程序正常打印hello world
C.GetMemory函数返回的地址无法正常使用
D.程序存在内存泄露
解析:
局部变量有一个特点,出了作用域它开辟的内存就要被系统回收掉,所以呢虽然str能接收到return p返回的值,但是呢,此时可能打印出hello world 也可能不可能,因为它已经被操作系统回收了,也不存在内存泄露的问题,因为不是用的动态内存的相关的函数,所以答案是C
题目6:对动态内存管理错误的认识
以下哪个不是动态内存的错误( )
A.free参数为NULL
B.对非动态内存的free释放
C.对动态内存的多次释放
D.对动态内存的越界访问
解析:
答案为A
常见的动态内存管理的错误
1.对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
解决方法:
int main()
{
int* p = (int*)malloc(10*sizeof(int));
if (p == NULL)//先判断是不是空指针
{
perror("malloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i;//*(p+i) }
free(p);
p = NULL;
return 0;
}
2.对动态开辟空间的越界访问
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 40; i++)//只开辟了10个int 类型大小的空间,但是却要遍历40次,这就是越界访问了
{
p[i] = i;//*(p+i)
}
free(p);
p = NULL;
return 0;
}
3.对⾮动态开辟内存使⽤free释放
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
肯定不OK,百分之百会报错
4.使⽤free释放⼀块动态开辟内存的⼀部分
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i <5; i++)
{
*p = i;
p++;//p不再指向动态内存的起始位置
}
free(p);
p = NULL;
return 0;
}
如下图:
5.对同⼀块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
6.动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放。
题目7:数组串联
给你一个长度为 n
的整数数组 nums
。请你构建一个长度为 2n
的答案数组 ans
,数组下标 从 0 开始计数 ,对于所有 0 <= i < n
的 i
,满足下述所有要求:
ans[i] == nums[i]
ans[i + n] == nums[i]
具体而言,ans
由两个 nums
数组 串联 形成。
返回数组 ans
。
示例 1:
输入:nums = [1,2,1]
输出:[1,2,1,1,2,1]
解释:数组 ans 按下述方式形成:
- ans = [nums[0],nums[1],nums[2],nums[0],nums[1],nums[2]]
- ans = [1,2,1,1,2,1]
示例 2:
输入:nums = [1,3,2,1]
输出:[1,3,2,1,1,3,2,1]
解释:数组 ans 按下述方式形成:
- ans = [nums[0],nums[1],nums[2],nums[3],nums[0],nums[1],nums[2],nums[3]]
- ans = [1,3,2,1,1,3,2,1]
提示:
n == nums.length
1 <= n <= 1000
1 <= nums[i] <= 1000
对题目进行分析:
以示例1为例子,就相当于nums要做两次事情,一次是放i到ans中,另一次是放i+n到ans中
上图是以后遇到oj代码编程题会遇到的一些经验包
int getConcatenation(int*nums,int numSize,int*returnSize)
{
int* ans = (int*)malloc(2 * sizeof(int) * numSize);
for (int i = 0; i < numSize; i++)
{
ans[i] = nums[i];
ans[i+numSize] = nums[i];
}
*returnSize = 2 * numSize;
return ans;
}
returnSize是一个输出型参数,谁调用getConcatenation就给谁
ans是在getConcatenation函数内部申请的空间,返回给调用者,谁调用就返回给谁,由谁去释放内存(这个点题目在Note中有解释)
题目8:用malloc函数创建一个二维数组
使用malloc函数模拟开辟一个3*5的整型二维数组,开辟好后,使用二维数组的下标访问形式,访问空间。
分析:
二维数组是特殊的一维数组,也就是说二维数组的每个元素存放的是每个一维数组的地址,也可以存数组首元素的地址,首元素的地址存放的整型地址。
接下来再做一个分析:
看下面这行代码:
int *p=(int*)malloc(10*sizeof(int))//这个就相当于是开辟了一个一维数组的空间
再看下面这行代码:
int**p=(int**)malloc(3*sizeof(int*))//这个就是我们从上面这幅图中看到的开辟二维数组的方式
最后呈现的结果如下:
int main()
{
//1.开辟空间(核心部分)
int **p=(int**)malloc(3*sizeof(int*));
for(int i=0;i<3;i++)
{
p[i]=(int*)malloc(5*sizeof(int));
}
//2.初始化
for(int i=0;i<3;i++)
{
for(int j=0;j<5;j++)
{
p[i][j]=1;
}
}
//3.打印数组
for(int i=0;i<3;i++)
{
for(int j=0;j<5;j++)
{
printf("%d",p[i][j]);
}
printf("\n");
}
return 0;
}
打印结果:
题目9:练习使用动态内存相关的函数
练习使用动态内存相关的4个函数,并调试观察。
malloc、calloc、realloc、free
二进制文件和文本文件
题目1:文本文件和二进制文件的区别
关于文本文件和二进制文件描述错误的是?
A.文本文件是可以读懂的,二进制文件没办法直接读懂
B.数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件
C.将内存中的数据转化成ASCII码值的形式,并以ASCII码值的形式存储的文件就是文本文件。
D.文本文件和二进制文件没啥区别,机器都能识别
解析:
文本文件和二进制文件机器确实都能识别,但是存储的形式还是有所差异的。答案为D
题目2:打开文件
C语言以二进制方式打开一个文件的方法是?( )
A.FILE *f = fwrite( “test.bin”, “b” );
B.FILE *f = fopenb( “test.bin”, “w” );
C.FILE *f = fopen( “test.bin”, “wb” );
D.FILE *f = fwriteb( “test.bin” );
解析:
首先,因为要打开文件,AD直接拖出去,由于不存在一个“fopenb”函数,所以直接选C。二进制描述中的b要放在权限后,也就是“wb”才是合法的。
题目3:fopen函数
关于fopen函数说法不正确的是:( )
A.fopen打开文件的方式是"r",如果文件不存在,则打开文件失败
B.fopen打开文件的方式是"w",如果文件不存在,则创建该文件,打开成功
C.fopen函数的返回值无需判断
D.fopen打开的文件需要fclose来关闭
解析:
C选项中fopen的返回值可以检验文件是否打开成功,打开方式为"r"时尤其重要。ABD为文件操作的基本概念和原则
题目4:文件名及路径
下列关于文件名及路径的说法中错误的是:( )
A.文件名中有一些禁止使用的字符
B.文件名中一定包含后缀名
C.文件的后缀名决定了一个文件的默认打开方式
D.文件路径指的是从盘符到该文件所经历的路径中各符号名的集合
解析:
B选项中,文件名可以不包含后缀名。A的话,文件名中不能包含这些字符:/😗?"<>|,C表述了后缀名的作用,D是路径的基本概念。故选B。
有图有真相哦😎😎😎
题目5:文件读写函数
C语言中关于文件读写函数说法不正确的是:( )
A.fgetc是适用于所有输入流字符输入函数
B.getchar也是适用于所有流的字符输入函数
C.fputs是适用于所有输出流的文本行输出函数
D.fread是适用于文件输入流的二进制输入函数
解析:
B选项中,getchar只针对标准输入流stdin。即使对stdin重定向,getchar针对的也只是stdin。f系列的输入输出函数都是作用于所有流的的,所以AC没问题,D的表述也没问题,fread做的就是二进制的活。
题目6:fgetc
下面程序的功能是什么? ( )
int main()
{
long num=0;
FILE *fp = NULL;
if((fp=fopen("fname.dat", "r"))==NULL)
{
printf("Can’t open the file!");
exit(0);
}
while(fgetc(fp) != EOF)
{
num++;
}
printf("num=%d\n",num);
fclose(fp);
return 0;
}
A.拷贝文件
B.统计文件的字符数
C.统计文件的单词数
D.统计文件的行数
解析:
程序只通过只读方式打开了一个文件,所以A排除,文中使用的fgetc,且没有’ ‘和’\n’相关的统计,所以排除CD,直接选B。
题目7:三组函数的区分
下面说法不正确的是:( )
A.scanf和printf是针对标准输入、输出流的格式化输入、输出语句
B.fscanf和fprintf是针对所有输入、输出流的格式化输入、输出语句
C.sscanf是从字符串中读取格式化的数据
D.sprintf是把格式化的数据写到输出流中
解析:
D选项中,sprintf是把格式化的数据写到字符串中,与输出流无关。其他三句都准确描述了函数功能。
解析:
• stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
• stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出 流中。
• stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。
对sscanf的详细介绍:
int main()
{
char buffer[256] = { 0 };
sscanf("nayeon 614", "%s",buffer);
printf("%s", buffer);
return 0;
}
注意来看调试结果:
所以我们要注意sscanf遇到空格就停止
对sprintf的详细介绍:
int main()
{
char buffer[256] = { 0 };
char* p ="nayeon";
sprintf(buffer, "%s",p);
printf("%s", buffer);
return 0;
}
结果与上面类似,所以sprintf和sscanf它们使用方式差不多,但是sprintf绝不是把格式化的内容读到输出流里面,应该是上面的buffer这种
题目8:feof函数的理解(很容易理解错误)
关于feof函数描述不正确的是:( )
A.feof函数是用来判断文件是否读取结束的
B.feof函数是在文件读取结束的时候,检测是否是因为遇到了文件结束标志EOF,而读取结束
C.读取文本判断是否结束时,fgetc看返回值是否为EOF, fgets看返回值是否为NULL
D.二进制文件判断读取结束,看实际读取个数是否小于要求读取个数
解析:
说明:feof函数是在文件读取结束后,判断文件读取结束的原因的,是读取失败结束,还是遇到文件尾结束。所以A是错误的,其他选项均正确。
题目9: 写一个程序拷贝文件
使用所学文件操作,在当前目录下放一个文件data.txt,写一个程序,将data.txt文件拷贝一份,生成data_copy.txt文件。
基本思路:
- 打开文件data.txt,读取数据
- 打开文件data_copy.txt,写数据
- 从data.txt中读取数据存放到data_copy.txt文件中,直到文件结束。
- 关闭两个文件
int main()
{
//1.打开文件
FILE*pr=fopen("data.txt","r");
if(pr==NULL)
{
perror("open file 失败 by read");
}
FILE*pw=fopen("data_copy.txt","w");
if(pw==NULL)
{
perror("open file 失败 by write");
}
//2.写文件(核心代码部分)
int ch=0;
while(ch=fgetc(pr)!=EOF)
{
fputc(pw,ch);
}
//3.关闭文件
fclose(pr);
fclose(pw);
pr==NULL;
pw==NULL;
return 0;
}
翻译环境
题目1:编译和链接
由多个源文件组成的C程序,经过编辑、预处理、编译、链接等阶段会生成最终的可执行程序。下面哪个阶段可以发现被调用的函数未定义?( )
A.预处理
B.编译
C.链接
D.执行
解析:
预处理只会处理#开头的语句,编译阶段只校验语法,链接时才会去找实体,所以是链接时出错的,故选C。这里附上每个步骤的具体操作方式:
预处理:相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有头文件(都已经被展开了)、宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了),没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
编译:将预处理完的文件逐一进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。编译是针对单个文件编译的,只校验本文件的语法是否有问题,不负责寻找实体。
链接:通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。 链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。在此过程中会发现被调用的函数未被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的。
题目2:编译和链接
( ) 的作用是将源程序文件进行处理,生成一个中间文件,编译系统将对此中间文件进行编译并生成目标代码。
A.预处理
B.汇编
C.生成安装文件
D.编译
解析:
题干中提到了“编译”,说明是编译的上一步,那自然是预处理。故选A
预处理
题目1:预处理指令#define和typedef定义的区别
test.c文件中包括如下语句:
#define INT_PTR int*
typedef int*int_ptr;
INT_PTR a,b;
int_ptr c,d;
文件中定义的四个变量,哪个变量不是指针类型?( )
A.a
B.b
C.c
D.d
解析:
预处理的#define是查找替换,所以替换过后的语句是“int*a,b;”,其中b只是一个int变量,如果要让b也是指针,必须写成“int *a, *b;”。而typedef没有这个问题,c、d都是指针。故选B。
详细解释如下:
#define INT_PTR int*//替换
typedef int*int_ptr;//重新定义了一个类型,如给别人起绰号,叫张三李四之类的
INT_PTR a,b;//int *a,b
int_ptr c,d;//int *a,*b
题目2:C语言运行环境
关于C语言运行环境说法错误的是?
A.程序运行之前需要先载入内存中
B.载入内存后,便找到main函数,从main函数的开始位置开始执行
C.程序运行起来后,会为每一次函数调用创建运行时运行时堆栈,程序运行的上下文信息就放在运行堆栈中维护
D.程序运行起来就无法终止
解析:
D程序运行结束就停止了,还可以通过ctr+c等方式终止程序。
题目3:预定义符号
下面哪个不是预定义符号?( )
A.FILE
B.TIME
C.DATE
D.MAIN
解析:
前三个是常用宏,分别是:打印所在文件、打印编译时间、打印编译日期。除此之外,还有__LINE__(行号)、FUNCTION(函数名)等宏,而__MAIN__并不存在,故选D。
题目4:预处理指令
下面哪个不是预处理指令:( )
A.#define
B.#if
C.#undef
D.#end
解析:
#define执行查找替换,#if可以区分是否编译,#undef可以反定义,也就是取消#define宏定义的东西,#end并没有这玩意,只有#endif,故选D。
宏的实现
题目1:offsetof宏
写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
考察:
offsetof
宏的实现
如果我们不确定我们可以在msdn上查找offsetof相关的内容:
实现offset:
#include <stdio.h>
#include <stddef.h>
struct A
{
int a;
char b;
};
int main()
{
int a=offsetof(struct A,b);
printf("%d\n",a);
return 0;
}
运行结果:
接下来是模拟实现offsetof
#define OFFSETOF(StrcutType,MemberName) ((size_t)&(((StrcutType*)0)->MemberName))
#define offsetof(s,m) ((size_t)&(((s*)0)->m))
struct A
{
char a;
int b;
};
int main()
{
// //struct A* ptr = (struct A*)0;//ptr = NULL;
// //printf("%p\n", &(ptr->a));
// //printf("%p\n", &(ptr->b));
//
//
// //int a = offsetof(struct A,b);
int a = OFFSETOF(struct A,b);
printf("%d\n",a);
return 0;
}
题目2:交换奇偶位
写一个宏,可以将一个整数的二进制位的奇数位和偶数位交换。
分析思路:
拿一个数来举例:
0000 0000 0000 0000 0000 0000 0000 1101//十进制位的13
核心思路:先拿到奇数,再拿到偶数
第一步:拿到一个数的奇数位
0000 0000 0000 0000 0000 0000 0000 1101
0101 0101 0101 0101 0101 0101 0101 0101 &//按位与,两边同时为1才为1,一边为0或者两边都是0则为0
0000 0000 0000 0000 0000 0000 0000 0101//计算后得到的结果
第二步:拿到偶数位
0000 0000 0000 0000 0000 0000 0000 1101
1010 1010 1010 1010 1010 1010 1010 1010 &
0000 0000 0000 0000 0000 0000 0000 1000//偶数位的数字没有发生变化,拿到了这个数的偶数位
第三步:将拿到的奇数位和偶数位进行按位或,奇数位左移1位,偶数位右移1位
0000 0000 0000 0000 0000 0000 0000 1010
0000 0000 0000 0000 0000 0000 0000 0100 |
0000 0000 0000 0000 0000 0000 0000 1110// 14
最后呈现结果:
#define SWAP_BIT(n) ( (((n) & 0x55555555 ) << 1) | (((n) & 0xaaaaaaaa) >> 1))
int main()
{
int n = 13;
int a = SWAP_BIT(n);
printf("%d\n", a);
return 0;
}
运行结果:
题目3:宏定义
设有以下宏定义:
#define N 4
#define Y(n) ((N+2)*n) /这种定义在编程规范中是严格禁止的/
则执行语句:z = 2 * (N + Y(5+1));后,z的值为( )
A.出错
B.60
C.48
D.70
答案:D
题目4:宏定义
下面代码执行的结果是:( )
#define A 2+2
#define B 3+3
#define C A*B
int main()
{
printf("%d\n", C);
return 0;
}
A.24
B.11
C.10
D.23
答案:
因为#define只是起到了一个替换的作用,并没有实际改变类型,所以答案是B
题目5: 宏和函数的区别
下面哪个不是宏和函数的区别?( )
A.函数可以递归,宏不能递归
B.函数参数有类型检查,宏参数无类型检查
C.函数的执行速度更快,宏的执行速度慢
D.由于宏是通过替换完成的,所以操作符的优先级会影响宏的求值,应该尽量使用括号明确优先级
解析:
答案:C
题目6:条件编译指令
下面哪个是条件编译指令( )
A.#define
B.#ifdef
C.#pragma
D.#error
#define SIZE 10
#ifndef SIZE
#error 没有定义SIZE
#endif
//#error 确保在编译之前 满足某些条件
A是宏定义,C是一个比较复杂的预编译语句,但跟条件肯定扯不上关系,D是报错用的,条件编译指令包括#if、#ifdef,#ifndef,#else,#elif、#endif等。除此之外还有#if defined(xxx)的用法。故选B。
题目7:头文件
以下关于头文件,说法正确的是( )
A.#include<filename.h>,编译器寻找头文件时,会从当前编译的源文件所在的目录去找
B.#include“filename.h”,编译器寻找头文件时,会从通过编译选项指定的库目录去找
C.多个源文件同时用到的全局整数变量,它的声明和定义都放在头文件中,是好的编程习惯
D.在大型项目开发中,把所有自定义的数据类型、函数声明都放在一个头文件中,各个源文件都只需要包含这个头文件即可,省去了要写很多#include语句的麻烦,是好的编程习惯。
解析:
单纯地使用源文件,组织项目结构的时候,项目越大越复杂的时候,维护成本会变得越来越高==
这样也就诞生了头文件 .h
.h:头文件,组织项目结构的时候,减少大型项目的维护成本
有关头文件的补充说明:
.h基本上都是要被多个源文件包含的,可能有一个问题,头文件被重复包含的问题,会导致运行效率降低
解决方案:
在头文件开头写上#pragma once(**PS:**在VS2022中会默认加上#pragma once)
头文件包含以下内容:
-
C头文件
-
所有的变量的声明
-
所有的函数的声明
-
#define ,类型typedef, struct
在C语言中,包含头文件的两种形式:
#include <stdio.h>这种是C语言库里面要包含的头文件
#include"test.h"是包含自定义的头文件
C.多个源文件同时用到的全局整数变量,它的声明和定义都放在头文件中,是好的编程习惯
这是不可以的,会产生访问冲突
题目8:条件编译
C语言头文件中的 ifndef/define/endif 的作用?( )
A.防止头文件重复引用
B.规范化代码
C.标志被引用文件内容中可以被共享的代码
D.以上都不正确
答案:
A
#define A 2+2
#define B 3+3
#define C A*B
int main()
{
printf("%d\n", C);
return 0;
}
A.24
B.11
C.10
D.23
答案:
因为#define只是起到了一个替换的作用,并没有实际改变类型,所以答案是B