C语言是建立在适当的关键字,表达式,语句以及使用它的规则上。然而 ,C标准不仅描述C语言,还描述如何执行 C 预处理器,C标准库有那些函数,以及这些函数的工作原理。
C 预处理器在程序执行之前检查程序(故称为预处理器),根据程序中的预处理器指令,预处理器把符号缩写替换成其表示的内容。预处理器可以包含程序所需要的其他文件,可以选择让编译器查看那些代码。预处理器并不知道 C 。基本上它的工作是把一些文本转换成另一些文本。看下面示例:
#include //包含头文件
#define HW "hello word!" //宏定义
#define 开始执行 main //宏定义
//#就是预处理,编译器编译连接之前的处理
int 开始执行()
{
printf(HW);
getchar();
}
运行效果:
这样简单的示例程序,都要经过编辑、预处理、编译、链接4个步骤,才能变成可执行程序,鼠标双击就弹出命令窗口,显示“Hello,World”。这也是一般C语言程序的编译流程。如下图:
编辑可能就是通常所说的“写代码”,用集成开发工具也好,用记事本也好,按C语言的语法规则组织一系列的源文件,主要有两种形式,一种是.c文件,另一种是.h文件,也称头文件。
“#include”和“#define”都属于编译预处理,C语言允许在程序中用预处理指令写一些命令行。预处理器在编译器之前根据指令更改程序文本。编译器看到的是预处理器修改过的代码文本,C语言的编译预处理功能主要包括宏定义、文件包含和条件编译3种。预处理器对宏进行替换,并将所包含的头文件整体插入源文件中,为后面要进行的编译做好准备。
示例:
// test.txt
printf("你好,世界!\n");
printf("你好,C 语言!\n");
#include //包含头文件 ,存放函数声明,让编译器直到有这个函数声明。
// 连接,就是找到函数实体,让调用的函数能够成功执行。在本例中 printf 函数在 stdio.h
// 头文件中声明,所以要提前引入头文件,否则,无法执行打印输出。
int main()
{
#include"test.txt"; //包含文件
getchar();
}
编译器处理的对象是由单个c文件和其中递归包含的头文件组成的编译单元,一般来说,头文件是不直接参加编译的。编译器会将每个编译单元翻译成同名的二进制代码文件,在DOS和Windows环境下,二进制代码文件的后缀名为.obj,在Unix环境下,其后缀名为.o,此时,二进制代码文件是零散的,还不是可执行二进制文件。错误检查大多是在编译阶段进行的,编译器主要进行语法分析,词法分析,产生目标代码并进行代码优化等处理。为全局变量和静态变量等分配内存,并检查函数是否已定义,如没有定义,是否有函数声明。函数声明通知编译器:该函数在本文件晚些时候定义,或者是在其他文件中定义。
在 VS 中查看编译器:
选中项目 右键【属性】 -->
链接器将编译得到的零散的二进制代码文件组合成二进制可执行文件,主要完成下述两个工作,一是解析其他文件中函数引用或其他引用,二是解析库函数。
程序错误:
编译链接,一大堆的错误提示,没有完美的程序,不存在没有缺陷的程序,如果一个程序运行很完美,那是因为它的缺陷到现在还没有被发现。同样,软件测试是为了发现程序中可能存在的问题,而不是证明程序没有错误。
错误分类:
编译错误主要有两类:
除了错误外,编译器还会对程序中一些不合理的用法进行警告(warning),尽管警告不耽误程序编译链接,但对警告信息不能掉以轻心,警告常常预示着隐藏很深的错误,特别是逻辑错误,应当仔细排查。
有定义 | 无声明 | 调用函数 | 可以编译,可以链接 |
有定义 | 有声明 | 调用函数 | 可编译,可链接 |
无定义 | 有声明 | 调用函数 | 可编译,无法链接 |
无定义 | 无声明 | 无法编译 |
1 用法和示例
在编写代码时,我们总是会做一些假设,断言(assert)就是用于在代码中捕捉这些假设,可以看作是异常处理的高级形式。经常用于代码调试。
可以在任何时候启用和禁用断言验证,因此可以在测试时启动断言,而在部署时禁用断言。
示例:
#define _CRT_SECURE_NO_WARNINGS //宏定义,去掉安全检查
#include
#include
#include //静态断言头文件
void main()
{
int n1, n2;
scanf("%d%d", &n1, &n2);
printf("n1=%d , n1=%d \n", n1, n2);
// 断言除数不能为0 ,n2 =0 不成立
assert(n2 != 0);
double num = (double)n1 / n2;
printf("num=%f \n", num);
system("pause");
}
首选正常输入两个值进行计算:
输入 0 引发错误,会弹出错误对话框。
添加 宏定义:可以关闭静态断言
#define NDEBUG //关闭静态断言, 宏定义必须放在 引用头文件之前才有效
示例2:
#define _CRT_SECURE_NO_WARNINGS //宏定义,去掉安全检查
//#define NDEBUG //关闭静态断言
#include
#include
#include //静态断言头文件
/* 静态断言,检测指针是否为空*/
void main()
{
char* p = (char*)malloc(sizeof(char) * 1024 * 1024 * 1024 * 10); //分配内存 10GB大小 ,验证报错
assert(p != NULL); //malloc 内存分配失败返回 NULL
*p = 'A'; //指针不为空时才能赋值
printf(*p);
system("pause");
}
如果不用 assert 静态断言,虽然程序错误,但是错误的产生原因和 错误发生的位置无法清晰定位。
2:自定义 静态断言宏指令
实现自定义的 assert 宏定义:
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#include
#define DEBUG //定义静态断言开启
// x :是条件符号 \ :是链接符 #define一行写不下用 \ 连接 #x :会为 x 条件自动加上 "" 称为字符串
#ifndef DEBUG //如果有定义 ,使用 预编译指令,添加断言开关
#define myassert(x) //没有代码提示
#else// DEBUG
#define myassert(x)\
if(!(x))\
{\
printf("myassert(%s) 宏检测开始 ... ...\n", #x); \
printf("当前函数名:%s , 文件名和路径:%s ,代码行号:%d \n", __FUNCTION__, __FILE__, __LINE__);\
char errmsg[50];\
sprintf(errmsg,"当前函数名:%s , 文件名:%s ,代码行号:%d \n", __FUNCTION__, __FILE__, __LINE__);\
MessageBoxA(0,errmsg,"ERROR INFO:",0);\
}
#endif
void main()
{
int num = 10;
myassert(num < 10);
printf("num=%d \n", num);
system("pause");
}
输出:
文件包含是 C 语言预处理的一个重要功能,用 "include" 来实现,将一个源文件的内容包含到另一个源文件中,称为它的一部分,文件包含的一般格式为:
#include<文件名> 或者 #include"文件名"
两种形式的区别在于:用尖括号表示在系统头文件目录中查找(由用户在编程环境中设置),而不在源文件目录中查找。使用双引号表示首先在当前源文件中查找,找不到再到系统头文件中查找。
在 include"文件名" 格式下,用户可以显示地指名文件的位置:
件包含语句中被包含的文件通常是以.h 结尾的头文件,这种头文件中的内容多为变量的定义、类型定义、宏定义、函数的定义或说明,但被包含的文件也可以是以.c为扩展名的C语言源文件。
通过某些条件,控制源程序中的某段源代码是否参加编译,这就是条件编译的功能,一般来说,所有源文件中的代码都应参加编译,但有时候希望某部分代码不参加编译,应用条件编译可达到这以目的。
条件编译的基本形式:
NO .1:
#if 判断表达式
语句段1
#else
语句段2
#endif
示例:
#include
#include
#include
#define FLAG 1
void main()
{
#if FLAG==2 //判断条件是否成立
MessageBoxA(0, "你好,世界!", "中文", 0);
#else
MessageBoxA(0, "hello word!", "English", 0);
#endif
}
效果:
NO.2:
#ifndef 标识符
语句段1
#else
语句段2
#endif
#include
#include
#include
#define FLAG 1
void main()
{
#ifndef B //如果 B 每没有定义
MessageBoxA(0, "你好,世界!", "中文", 0);
#else
MessageBoxA(0, "hello word!", "English", 0);
#endif // !B
}
效果:
C程序的编译分编辑、预处理、编译和链接几个步骤,预处理指令是由预处理器负责执行的,主要有头文件包含、宏定义、条件编译等,经过预处理后,编译器才开始工作,将每个编译单元编译成二进制代码文件,但此时分散的二进制代码文件中的变量和函数没有分配到具体内存地址,因而不能执行,需要链接器将这些二进制代码文件、用到的库文件中相关代码,系统相关的信息组合起来,形成二进制可执行文件。
5.1 扩展宏:
除了常用的宏定义 #define 外,ANSI 标准说明了5个 常用于代码调试的 预定义宏指令:
_DATA_ | 进行预处理的日期("Mmm dd yy" 形式的字符串) |
_FILE_ | 代表当前源文件名的字符串 |
_LINE_ | 代表当前源代码中行号的整数常量 |
_TIME_ | 源文件编译的时间 ,格式:"hh:mm:ss" |
_FUNCTION_ | 当前所在函数名 |
常用方法在 上面 assert 断言 中已经使用过。这里再次举例:
#include
#include
#include
void show()
{
printf("当前文件名称:%s\n", __FILE__);
printf("当前语句的行号: %d\n", __LINE__);
printf("当前函数名:%s\n", __FUNCTION__);
printf("当前编译日期:%s , 编译时间:%s\n", __DATE__, __TIME__);
}
void main()
{
printf("当前文件名称:%s\n", __FILE__);
printf("当前语句的行号: %d\n", __LINE__);
printf("当前函数名:%s\n", __FUNCTION__);
printf("当前编译日期:%s , 编译时间:%s\n", __DATE__, __TIME__);
printf("\n------------------------------\n");
show();
system("pause");
}
效果:
5.2 const常量与宏的差别:
示例:
#include
#include
#include
#define X 12.5
const int N = 12.5; //const 定义 存在 = 号赋值动作,会自动进行数据类型转换
void main()
{
printf("define 类型= %d\n", sizeof(X));
printf("const 类型= %d\n", sizeof(N));
system("pause");
}
输出:
const 是有数据类型的,可以根据数据类型进行安全检查,发现类型不匹配时,会发出警告或转换,可以预防数据类型不匹配的错误。const 是伪常量,通过指针可以修改其值。#define 就是替换,没有数据类型,无法进行安全检查。#define 定义的常量是无法修改的,是真正意义的常量。
5.3 宏的高级用法:
# 与 ## 的用法
示例:
#include
#include
#define S(x) system(#x)
#define PrintfNum(x) printf("%s=%d \n",#x,x);
void main()
{
int n1 = 10, n2 = 20, n3 = 30;
PrintfNum(n1);
PrintfNum(n2);
PrintfNum(n3);
S(calc); //参数名称没有加 ""
S(notepad);//参数名称没有加 ""
}
执行后打开了计算器和记事本 并且 输出了内容: 在 C 语言中 变量名实际上是对内存空间的抽象,不用 # 是无法把变量当字符串输出的。
示例:
#include
#include
#define PrintfNum(x) printf("%s=%d \n",#x,x);
#define N(x) N##x //N 不要求是 宏的变量 任意值都可以
#define P(x) show##x
void show1()
{
printf("NO 1\n");
}
void show2()
{
printf("NO 2\n");
}
void main()
{
int N(1) = 10, N(2) = 20, N(3) = 30;
N1 = 100; //## 是连接的作用 N(1) 等价于 N1
N2 = 200;
N3 = 300;
PrintfNum(N(1));
PrintfNum(N(2));
PrintfNum(N(3));
printf("\n----------------------------\n");
P(1)(); //当传入 1 时, show##x 等价于 将 show 和 1 连接在一起 组成函数名称
P(2)();
system("pause");
}
输出:
1:error: 在编译时输出编译错误信息,从而方便查看错误,进行调试
#include
#include
int main(void)
{
#define NAME1 "NAME1"
printf("%s \n", NAME1);
#undef NAME1
#ifndef NAME1
#error No define NAME1
#endif // !NAME1
return 0;
}
2: pragma message : 能够在编译窗口中输出相应的信息,对源代码的控制有用。
#include
#include
#pragma message("Copyright ©1995-2004 XXXXXX, Inc. All rights reserved.")//版权声明
#define X64
#ifdef X64
#pragma message("不支持X64平台")
#endif // X64
int main(void)
{
return 0;
}