目录
C语言的最后这些知识考得都少,相比于文件,该部分校招考点多一些。(文件是倒数第一这个是倒数第二吧)该部分为C语言的扩展学习,旨在完善C语法的同时,了解C语法之外的其他周边特性。学习完成该阶段,会了解到程序从“文本”到“二进制程序”的过程,接触到其他C标准头文件。
与标准库函数,甚至摸到C语言和操作系统/体系结构等学科的某些关联,整体达到完成C语言的学习,最后对于C语言的任何知识和代码,能读,能写,会查。
1.程序环境
程序环境是什么?我们都 "经历" 过,但不曾感知到 "他" 的存在。我们其实在不知不觉中早就已经接触到了程序环境…… 第一次创建了一个文件(test.c),敲下那句 "hello world" 随后保存后点击运行后编译出可执行文件(test.exe)时,其实就已经接触到了 "他" 了。
我们只是按下了运行,然后好像所有东西都像变魔术一样直接就产生了,这一切都似乎是理所当然的事。但是你是否思考过他是如何变成 "可执行程序" 的呢?在这一章,我们将简单地探讨一个 "源程序"是如何变成 "可执行程序" 的,作一个大概了解。
1.1 ANSI C 标准
ANSI C是由美国国家标准协会(ANSI)及国际化标准组织(ISO)推出的关于C语言的标准。
ANSI C 主要标准化了现存的实现, 同时增加了一些来自 C++ 的内容 (主要是函数原型)
并支持多国字符集 (包括备受争议的三字符序列)。
1.2程序的翻译环境和执行环境
ANSI C 的任何一种实现中,存在两种不同的环境:
① 翻译环境:在该环境中,源代码被转换为可执行的机器指令。
② 执行环境:用于实际执行代码。
1.2.1 翻译环境
组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
连接器同时也会引入标准C库函数中任何被该程序所用到的函数,且可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
举个例子:test.c、add.c、minu.c
再举个例子:
① sum.c
int global_val = 2023;
void print(const char* string)
{
printf("%s\n", string);
}
② test.c
#include <stdio.h>
int main()
{
extern void print(char* string);
extern int global_val;
printf("%d\n", global_val);
printf("Hello,World!\n");
return 0;
}
挺重要的图:
main.c
extern int sum(int, int);
int main(void)
{
sum(1, 2);
return 0;
}
sum.c
int sum(int num1, int num2)
{
return( num1 + num2);
}
解析图(VS2019):
1.2.2运行环境
程序执行过程:
① 程序必须载入内存中。在有操作系统的环境中:程序的载入一般由操作系统完成。在独立环境中:程序的载入必须手工安排,也可能是通过可执行代码置入只读内存来完成。
② 程序的执行便开始。接着便调用 main 函数。
③ 开始执行程序代码,这个时候程序将使用一个运行时堆栈(stack),
内存函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,
存储与静态内存中的变量在整个执行过程中一直保留他们的值。
④ 终止程序。正常终止 main 函数(也有可能是意外终止)。
举个例子:这段代码的执行过程
int Add(int x, int y) {
return( x + y);
}
int main(void) {
int a = 10;
int b = 20;
int ret = Add(a, b);
return 0;
}
【百度百科】C语言中,每个栈帧对应着一个未运行完的函数。
栈帧中保存了该函数的返回地址和局部变量。
2. 预处理详解
什么是预处理:
【百度百科】程序设计领域中,预处理一般是指在 程序源代码被翻译为目标代码的过程中,
生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,
但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)
预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。
2.1 预定义符号
下面的__ 都为两个下划线。
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义(VS2019,VS2022都未定义)
介绍:在预处理阶段被处理的已经定义好的符号为预定义符号。这些符号是可以直接使用的,
是在C语言中已经内置好的。代码演示:
#include <stdio.h>
int main()
{
printf("%s\n", __FILE__); // 返回使用行代码所在的源文件名,包括路径
printf("%d\n", __LINE__); // 返回行号
printf("%s\n", __DATE__); // 返回程序被编译的日期
printf("%s\n", __TIME__); // 返回程序被编译的时间
printf("%s\n", __FUNCTION__); // 返回所在函数的函数名
return 0;
}
这些预定义符号有什么用?
如果一个工程特别复杂,这时去调试时可能会无从下手。所以需要代码在运行的过程中记录一些日志信息,通过日志信息分析程序哪里出了问题,再进行排查就很简单了。
举个例子:
#include <stdio.h>
int main()
{
FILE* pf = fopen("log.txt", "a+"); //追加的形式,每运行一次就追加
if (pf == NULL)
{
perror("fopen");
return 1;
}
for (int i = 0; i < 5; i++)
{
printf("* 错误日志 ");
printf("%d *\n", i + 1);
printf("发生时间:%s %s\n", __DATE__, __TIME__);
printf("具体位置:%s,函数名为%s,第%d行。\n", __FILE__, __FUNCTION__, __LINE__);
printf("\n");
}
for (int i = 0; i < 5; i++)//写到文件记录下来
{
fprintf(pf,"* 错误日志 ");
fprintf(pf, "%d *\n", i + 1);
fprintf(pf, "发生时间:%s %s\n", __DATE__, __TIME__);
fprintf(pf, "具体位置:%s,函数名为%s,第%d行。\n", __FILE__, __FUNCTION__, __LINE__);
fprintf(pf, "\n");
}
fclose(pf);
pf = NULL;
return 0;
}
2.2 #define
(#define定义的标识符和宏和枚举一样,习惯用大写)(程序员的约定俗成)
2.2.1#define 定义标识符
代码演示:#define 定义标识符的方法
#include <stdio.h>
#define TIMES 100
int main()
{
int t = TIMES;
printf("%d\n", t);//100
return 0;
}
解析:在预处理阶段就会把 TIMES 替换为 100。
预处理结束后 int t = TIMES 这里就没有TIMES 了,会变为 int t = 100;
当然了, #define 定义的符号可不仅仅只有数字,还可以用来做很多事,比如:
#define REG register //给关键字register,创建一个简短的名字
#define DEAD_LOOP for(;;) //用更形象的符号来替换一种实现(死循环的实现)
这里假设一个程序里 switch 语句后面都需要加上break,但是某人原来不是写C语言的,
他以前用的语言 case 后面是不需要加 break 的,因为他不适应每个 case 后面都要加上 break,
所以总是会忘。这时可以妙用 #define 来解决:
#define CASE break;case // 在写case语句的时候自动字上break
int main()
{
int n = 0;
//switch (n)
//{
// case 1:
// break;
// case 2:
// break;
// case 3:
// break;
//}
switch (n)
{
case 1: // 第一个case不能替换
CASE 2: // 相当于 break; case 2:
CASE 3: // 相当于 break; case 3:
}
return 0;
}
如果定义的 stuff 过长,可以分行来写,除了最后一行外,每行的后面都加一个续行符即可 \ :
#include <stdio.h>
#define DEBUG_PRINT printf("file:%s\nline:%d\n \
date:%s\ntime:%s\n" , \
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
int main()
{
DEBUG_PRINT;
return 0;
}
#define 定义标识符时,为什么末尾没有加上分号?
举个例子:加上分号后,预处理替换的内容也会带分号 100;
#include <stdio.h>
#define TIMES 100;
int main(void)
{
int t = TIMES; // int t = 100;;
// 等于两个语句
// int t = 100;
// ;
return 0;
}
举个例子:加上分号,代码会出错的情况
#include <stdio.h>
#define TIMES 100;
int main(void)
{
int a, b;
if (a > 10)
b = TIMES; // b = 100;;
else // else不知道如何匹配了
b = -TIMES; // b = 100;;
return 0;
}
所以:在 #define 定义标识符时,尽量不要在末尾加分号(必须加的情况除外)
2.2.2 #define 定义宏
介绍:#define 机制允许把参数替换到文本中,这种实现通常被称为宏(macro)或
定义宏(define macro),parament-list 是一个由逗号隔开的符号表,他们可能出现在 stuff 中。
注意事项:
① 参数列表的左括号必须与 name 紧邻。
② 如果两者之间由任何空白存在,参数列表就会将其解释为 stuff 的一部分。
代码演示:3×3=9
#include <stdio.h>
#define SQUARE(X) X*X
int main()
{
printf("%d\n", SQUARE(3)); //替换成printf("%d\n", 3 * 3);
return 0;
}
那么SQUARE (3+1) 的结果是什么?
答案:7 。这里将 3+1 替换成 X,那么 X 就是3+1, 3+1 * 3+1, 根据优先级结果为 7。
要看作为一个整体,完全替换。宏的参数是完成替换的,他不会提前完成计算,
而是替换进去后再计算。替换是在预处理阶段时替换,表达式真正计算出结果是在运行时计算。
如果想获得 3+1 相乘(也就是得到 4×4 = 16) 的结果,我们需要给他们添加括号:
#include <stdio.h>
#define SQUARE(X) ((X)*(X))
// 整体再括一个括号,严谨
int main()
{
printf("%d\n", SQUARE(3 + 1)); //替换成printf("%d\n", ((3+1)* (3+1)));
return 0;
}
另外,整体再套一个括号。让代码更加严谨,防止产生不必要的错误。举个例子,我们DOUBLE实现两数相加,我希望得到 10* DOUBLE,也就是 "10*表达式相加" 的情况:
#include <stdio.h>
#define DOUBLE(X) (X)+(X)
int main()
{
printf("%d\n", 10 * DOUBLE(3+1));
// printf("%d\n", 10 * (4) + (4));
// 我们本意是想得到80,但是结果为44,因为整体没带括号
return 0;
}
解决方案:整体再加上一个括号
#include <stdio.h>
#define DOUBLE(X) ((X)+(X))
int main()
{
printf("%d\n", 10 * DOUBLE(3+1));
return 0;
}
结论:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,可以有效避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料地相互作用。
#define 替换规则
在程序中扩展 #define 定义符号或宏时,需要涉及的步骤如下:
① 检查:在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。
如果包含,它们首先被替换。
② 替换:替换文本随后被插入到程序中原来的文本位置。对于宏,函数名被它们的值替换。
③ 再次扫描:最后,再次对结果文件进行扫描,看看是否包含任何由 #define 定义的符号。
如果包含,就重复上述处理过程。
举个例子:
#define M 100
#define MAX(X, Y) ((X)>(Y) ? (X):(Y));
int main(void) {
int max = MAX(101, M);
return 0;
}
注意事项:
① 宏参数 和 #define 定义中可以出现 #define 定义的变量。但是对于宏绝对不能出现递归。
② 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。比如:
#include <stdio.h>
#define M 100
int main()
{
printf("M = %d", M);
return 0;
}
2.2.3 # 和 ##
我们知道,宏是把参数替换到文本中。那么如何把参数插入到字符串中呢?
比如这种情况,如果只规定传一个参数,使用函数是根本做不到的:
函数传两个参数:
#include <stdio.h>
void print(char x,int y)
{
printf("变量%c的值是%d\n", x,y);
}
int main()
{
int a = 10;
// 打印内容:变量a的值是10
print('a',a);
int b = 20;
// 打印内容:变量b的值是20
print('b',b);
int c = 30;
// 打印内容:变量c的值是30
print('c',c);
return 0;
}
这种情况,就可以用 宏 传一个参数来实现。
介绍:# 把一个宏参数变成对应的字符串。
使用 # 解决上面的问题:
#include <stdio.h>
#define PRINT(X) printf("变量"#X"的值是%d\n", X);
// #X 就会变成 X内容所定义的字符串
int main()
{
// 打印内容:变量a的值是10
int a = 10;
PRINT(a); // printf("变量""a""的值是%d\n", a);
// 打印内容:变量b的值是20
int b = 20;
PRINT(b); // printf("变量""b"的值是%d\n", b);
// 打印内容:变量c的值是30
int c = 30;
PRINT(c); // printf("变量""c""的值是%d\n", c);
return 0;
}
改进:让程序不仅仅支持打印整数,还可以打印其他类型的数(比如浮点数):
#include <stdio.h>
#define PRINT(X, FORMAT) printf("变量"#X"的值是 "FORMAT"\n", X);//format格式(格式化参数)
int main()
{
// 打印内容:变量a的值是10
int a = 10;
PRINT(a, "%d");
// 打印内容:变量f的值是5.5
float f = 5.5f;
PRINT(f, "%.1f"); //替换成printf("变量""f""的值是 ""%.1f""\n", f);
return 0;
}
这操作是不是很奇葩?还有更奇葩的呢:
介绍:## 可以把位于它两边的符号融合成一个符号。它允许宏定义从分离的文本片段创建标识符。
使用 ## 将两边的符号缝合成一个符号:
#include <stdio.h>
#define CAT(X,Y) X##Y //发现连3个也行
int main()
{
int vs2022 = 100; //下面会打印出100
printf("%d\n", CAT(vs, 2022)); //替换成printf("%d\n", vs2022);
return 0;
}
2.2.4带 "副作用" 的宏参数
什么是副作用?副作用就是表达式求值的时候出现的永久性效果,例如:
// 不带有副作用
x + 1;
// 带有副作用
x++;
int a = 1;
// 不带有副作用
int b = a + 1; // b=2, a=1
// 带有副作用
int b = ++a; // b=2, a=2
当宏参数在宏的定义中出现超过一次的情况下,如果参数带有副作用(后遗症),
那么在使用这个宏的时候就可能出现危险,导致不可预料的后果。
这种带有副作用的宏参数如果传到宏体内,这种副作用一直会延续到宏体内。
举个例子:
#include <stdio.h>
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main(void){
int a = 5;
int b = 8;
int m = MAX(a++, b++);
printf("m = %d\n", m);//9
printf("a=%d, b=%d\n", a, b);//6 10
return 0;
}
所以写宏的时候要尽量避免使用这种带副作用的参数。
2.2.5 宏和函数对比
举个例子:在两数中找较大值
① 用宏:
#include <stdio.h>
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int a = 10;
int b = 20;
int m = MAX(a, b); // int m = ((a)>(b) ? (a):(b))
printf("%d\n", m);
return 0;
}
② 用函数:
#include <stdio.h>
int Max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
int a = 10;
int b = 20;
int m = Max(a, b);
printf("%d\n", m);
return 0;
}
那么问题来了,宏和函数那种更好呢?
这题用宏更好,宏的优势:
用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多,
所以宏比函数在程序的规模和速度方面更胜一筹。 更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之,宏可以适用于整型、长整型、浮点型等可以用于
比较的类型。因为宏是类型无关的。
当然,宏也有劣势的地方:
① 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,
否则可能大幅度增加程序的长度。
② 宏不能调试。
③ 宏由于类型无关,因为没有类型检查,所以不够严谨。
④ 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到:
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
int main()
{
// 原本的写法:malloc(10*sizeof(int));
// 但我想这么写:malloc(10, int);
int* p = MALLOC(10, int); // (int*)malloc(10*sizeof(int))
return 0;
}
宏和函数的对比:
内联函数inline(C99)结合了宏和函数两者的优点(后面C++专栏会讲)
2.2.6宏和函数命名约定
命名约定,一般来讲函数的宏的使用语法很相似,所以语言本身没法区分二者。
约定俗成的一个习惯是: 宏名全部大写,函数名不要全部大写。
不过这也不是绝对的,比如我有时候就是想把一个宏伪装成函数来使用,那么我就全小写给宏取名。并不强制,但是这个约定是每个C/C++程序员大家的一种 "约定俗成" 。
2.3 #undef移除宏定义
#undef NAME 用于移除一个宏定义。(也不用在后面加分号)
代码演示:用完 M 之后移除该定义
#include <stdio.h>
#define M 100
int main(void)
{
int a = M;
printf("%d\n", M);
#undef M// 移除宏定义
return 0;
}
2.4命令行编译
什么是命令行编译?
在编译的时候通过命令行的方式对其进行相关的定义,叫做命令行编译。
介绍:许多C的编译器提供的一种能力,允许在命令行中定义符号。用于启动编译过程。
当我们根据同一个源文件要编译出不同的一个程序的不同版本的时,可以用到这种特性,增加灵活性。
例子:假如某个程序中声明了一个某个长度的数组,假如机器甲内存有限,我们需要一个很小的数据,但是机器丙的内存较大,我们需要一个大点的数组。
#include <stdio.h>
int main()
{
int arr[ARR_SIZE];
int i = 0;
for (i = 0; i < ARR_SIZE; i++)
{
arr[i] = i;
}
for (i = 0; i < ARR_SIZE; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
gcc 环境下测试:(VS 里面不太好演示)
gcc test.c -D ARR_SIZE=5
ls
a.out test.c
./a.out
0 1 2 3 4 5
gcc test.c -D ARR_SIZE=20
./a.out
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
2.5条件编译
条件编译介绍
在编译一个程序时,通过条件编译指令将一条语句(一组语句)编译或者放弃是很方便的。
调试用的代码删除了可惜,保留了又碍事。我们就可以使用条件编译来选择性地编译:
#include <stdio.h>
#define __DEBUG__ // 就像一个开关一样
int main()
{
int arr[10] = { 0 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__ // 因为__DEBUG__被定义了,所以为真
printf("%d ", arr[i]); // 就打印数组
#endif // 包尾
}
return 0;
}
如果不想用了,就把 #define __DEBUG__ 注释掉:代码运行后就不打印数组了。
2.5.1条件编译之常量表达式
如果常量表达式为真,参加编译。反之如果为假,则不参加编译。
代码演示:常量表达式为真
#include <stdio.h>
int main()
{
#if 1//非0都是真,算式也行
printf("Hello,World!\n");
#endif
return 0;
}
代码演示:常量表达式为假
#include <stdio.h>
int main()
{
#if 0
printf("Hello,World!\n");
#endif
return 0;
}
当然也可以用宏替换,可以表示地更清楚:
#include <stdio.h>
#define PRINT 1
#define DONT_PINRT 0
int main()
{
#if PRINT
printf("Hello,World!\n");
#endif
return 0;
}
2.5.2多分支的条件编译
介绍:多分支的条件编译,直到常量表达式为真时才执行。
代码演示:
#include <stdio.h>
int main()
{
#if 1 == 2 // 假
printf("aaaa\n");
#elif 2 == 2 // 真
printf("bbbb\n");
#else
printf("cccc\n")
#endif
return 0;
}
2.5.3条件编译判断是否被定义
定义:ifdef 和 if defined() ,ifndef 和 if !defined() 效果是一样的,用来判断是否被定义。
代码演示:
#include <stdio.h>
#define TEST 0
// #define TEST2 // 不定义
int main()
{
/* 如果TEST定义了,下面参与编译 */
// 1
#ifdef TEST
printf("1\n");
#endif
// 2
#if defined(TEST)
printf("2\n");
#endif
/* 如果TEST2不定义,下面参与编译 */
// 1
#ifndef TEST2
printf("3\n");
#endif
// 2
#if !defined(TEST2)
printf("4\n");
#endif
return 0;
}
2.5.4条件编译的嵌套
和 if 语句一样,是可以嵌套的:
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
2.6文件包含
我们已经知道,#include 指令可以使另外一个文件被编译。
就像它实际出现于 #include 指令的地方一样。
替换方式为,预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
2.6.0头文件被包含的方式
< > 和 " " 包含头文件的本质区别:查找的策略的区别
① " " 的查找策略:先在源文件所在的目录下查找。如果该头文件未找到,
则在库函数的头文件目录下查找。(如果仍然找不到,就提示编译错误)
Linux环境 标准头文件的路径:
/usr/include
VS环境 标准头文件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
< > 的查找策略:直接去标准路径下去查找。(如果仍然找不到,就提示编译错误)
既然如此,那么对于库文件是否也可以使用 " " 包含?
当然可以。但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
为了效率不建议这么做。
2.6.1嵌套文件的包含
头文件重复引入的情况:
comm.h 和 comm.c 是公共模块。
test1.h 和 test1.c 使用了公共模块。
test2.h 和 test2.c 使用了公共模块。
test.h 和 test.c 使用了 test1 模块和 test2 模块。
这样最终程序中就会出现两份 comm.h 的内容,这样就造成了文件内容的重复。
那么如何避免头文件的重复引入呢?
使用条件编译指令,每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
// 头文件的内容
#endif
如果嫌麻烦,还有一种非常简单的方法:
#pragma once // 让头文件即使被包含多次,也只包含一份
笔试题:选自《高质量C/C++编程指南》
① 头文件中的 ifnde / define / endif 是干什么用的?
答:防止头文件被重复多次包含。
② #include <filename.h> 和 #include "filename.h" 有什么区别?
答:尖括号是包含库里面的头文件的,双引号是包含自定义头文件的。
它们在查找策略上不同,尖括号直接去库目录下查找。
而双引号是现去自定义的代码路径下查找,如果找不到头文件,则在库函数的头文件目录下查找。
注:还有其他预处理指令
#error
#pragma
#line
...
不做介绍,可以自己去了解。
#pragma pack()//在结构体部分介绍。
3. 笔试选择题
3.1环境
以下什么内容的作用是将源程序文件进行处理,生成一个中间文件,
编译系统将对此中间文件进行编译并生成目标代码。
A.编译预处理
B.汇编
C.生成安装文件
D.编译
解析:
题干中提到了“编译”,说明是编译的上一步,那自然是编译预处理。
3.2可执行程序的生成
由多个源文件组成的C程序,经过编辑、预处理、编译、链接等阶段会生成最终的可执行程序。
下面哪个阶段可以发现被调用的函数未定义?( )
A.预处理
B.编译
C.链接
D.执行
解析:
预处理只会处理#开头的语句,编译阶段只校验语法,链接时才会去找实体,
所以是链接时出错的,故选C。这里附上每个步骤的具体操作方式:
预处理: 相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有头文件
(都已经被展开了)、宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了),
没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
编译: 将预处理完的文件逐一进行一系列词法分析、语法分析、语义分析及优化后,
产生相应的汇编代码文件。编译是针对单个文件编译的,只校验本文件的语法是否有问题,
不负责寻找实体。
链接: 通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。 链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。在此过程中会发现被调用的函数未被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的。
3.3定义变量
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。
3.4预处理指令
下面哪个不是预处理指令:( )
A.#define
B.#if
C.#undef
D.#end
解析:
#define执行查找替换,#if可以区分是否编译,#undef可以反定义,
也就是取消#define宏定义的东西,#end并没有这个东西,只有#endif。
3.5预定义符号
下面哪个不是预定义符号?( )
A.__FILE__
B.__TIME__
C.__DATE__
D.__MAIN__
解析:
前三个是常用宏,分别是:打印所在文件、打印编译时间、打印编译日期。
除此之外,还有__LINE__(行号)、__FUNCTION__(函数名)等宏,而__MAIN__并不存在。
3.6宏定义1
以下代码输出什么? ( )
#include<stdio.h>
#define N 4
#define Y(n) ((N+2)*n) /*这种定义在编程规范中是严格禁止的*/
int main()
{
int z = 2 * (N + Y(5 + 1));
printf("%d", z);
return 0;
}
A.出错
B.60
C.48
D.70
解析:
#include<stdio.h>
#define N 4
#define Y(n) ((N+2)*n) /*这种定义在编程规范中是严格禁止的*/
int main()
{
//N=4 所以Y(n)=((4+2)*n)
//所以2 * (N + Y(5 + 1)) = 2 * (4 + (4 + 2) * 5 + 1 ) = 2 * 35 = 70
int z = 2 * (N + Y(5 + 1));
printf("%d", z);
return 0;
}
3.7宏定义2
下面代码执行的结果是:( )
#include<stdio.h>
#define A 2+2
#define B 3+3
#define C A*B
int main()
{
printf("%d\n", C);
return 0;
}
解析:
宏C预处理后的代码是:2+2*3+3,即2+6+3,等于11
3.8条件编译指令
下面哪个是条件编译指令( )
A.#define
B.#ifdef
C.#pragma
D.#error
解析:
A是宏定义,C是一个比较复杂的预编译语句,但跟条件肯定扯不上关系,
D是报错用的,条件编译指令包括#if、#ifdef,#ifndef,#else,#elif、#endif等。
除此之外还有#if defined(xxx)的用法。选B
3.9头文件
以下关于头文件,说法正确的是( )
A.#include,编译器寻找头文件时,会从当前编译的源文件所在的目录去找
B.#include“filename.h”,编译器寻找头文件时,会从通过编译选项指定的库目录去找
C.多个源文件同时用到的全局整数变量,它的声明和定义都放在头文件中,是好的编程习惯
D.在大型项目开发中,把所有自定义的数据类型、函数声明都放在一个头文件中,各个源文件都
只需包含这个头文件即可,省去了要写很多#include语句的麻烦,是好的编程习惯。
解析:
AB说反了,尖括号是直接去库找,双引号是先从当前目录找,再去库里找。
C选项头文件不能定义全局变量,否则如果有多个文件,那链接时会冲突。故选D。
D也不是十全十美,在大型项目的开发中,这也并不是一个很好的编程习惯,
分类放在不同的头文件并根据特点命名是更好的选择,因为这样更加方便代码的管理和维护,
就目前而言,算是一个好习惯吧。
本篇完。
C语言知识点算结束了。
后面更一篇剩下的课后编程作业这个专栏就结束了,穿越回来贴个链接:
C语言进阶21收尾(编程作业)(atoi,strncpy,strncat,offsetof模拟实现+找单身狗+宏交换二进制奇偶位)_GR C的博客-CSDN博客
然后就更数据结构了,再穿越回来贴个链接:
③数据结构与算法(初阶)C语言描述_GR C的博客-CSDN博客