在这个环境中,源代码被转化为可执行的机器指令(二进制指令)
----预处理gcc 文件.c -E -o 文件.i,但直接放在终端了,故需要-o(output)重定向到文件内部
----编译gcc -S 文件名,生成.s文件
----汇编gcc -c 文件名,生成.o文件
----直接gcc 文件名字,直接经过整个翻译环境(预处理、编译、汇编、链接)生成可执行程序a.exe
(1)符号表会把全局变量和具有外部链接的函数等标识符和地址记录下来(局部变量是程序在运行的时候才会创建,而创建符号表还在编译阶段),汇总成表(函数声明部分会给予一个无效的地址,以便后续和函数定义整合在一起)
(2)链接库是属于库函数的
(3)目标文件是二进制文件,这个文件是有格式的,在linux环境下,gcc产生的目标文件和可执行程序的格式都是elf,而这种文件格式将.o文件分为一个一个段,因此在有多个.o文件的情况下,链接器将这些.o文件对应的、相同数据的段合并在一起
(4)再将之前有多个源文件生成的多个符号表合并,重定位指的是:链接的时候整合出一份新的符号表的同时查看标识符和地址是否正确(比如,这个时候如果找不到函数定义,则根据之前给予函数声明的无效地址就会提示编译器“不存在该函数(即链接错误)”),因此,实际上调用函数的时候,就是在链接的过程后才能实现的
在这个环境中,主要是实际执行代码的过程
__FILE__ //进行编译的源文件名称
__LINE__ //显示文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器严格遵循ANSI C,其值为1,否则未定义
__FUNCTION__ //程序预编译时预编译器将用所在的函数名,返回值是字符串
__FUNCDNAME__ //和上面的宏类似,都是显示重命名后的函数
//演示代码
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
printf("%s\n", __FILE__); //进行编译的源文件名称
printf("%d\n", __LINE__); //显示文件当前的行号
printf("%s\n", __DATE__); //文件被编译的日期
printf("%s\n", __TIME__); //文件被编译的时间
//printf("%d\n", __STDC__); //当编译器严格遵循ANSI C时,则其值为1,否则未定义
return 0;
}
//实际上,编译器在代码编译的时候,会对函数和变量名重命名
//在C语言中重命名的规则是加上下划线
//在C++中重命名的规则 会更加复杂
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
printf("%s\n", __FUNCDNAME__);
printf("%s\n", __FUNCTION__);
return 0;
}
一般来讲把宏名全部大写
#include
#define NUM 10
int main()
{
printf("%d", NUM);
return 0;
}
#include
#define ADD(X, Y) ((X) + (Y))//注意定义宏名和参数列表之间不能有空格,否则就会将()一起作为替换内容
int main()
{
printf("%d", ADD(2, 3));
return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include
#define NUMBER 10
#define ADD(X,Y) ((X) + (Y))
int main()
{
printf("%d", NUMBER);
#undef NUMBER
printf("%d", NUMBER);//这个语句是没办法识别NUMBER这个宏的
printf("%d", ADD(1, 2));
#undef ADD
printf("%d", ADD(1, 2));//这个语句是没办法识别ADD这个宏的
return 0;
}
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
注意:
实际上在后来的C/C++里面引入了关键字内联(inline)具有函数的优点又具有宏的优点,可以把函数像宏一样直接替换在后续的代码中,可以理解为“加强版的宏”,不过这个以后再学…
//举例一个传标识符参数的宏
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#define MALLOC(num, type) ((type*)malloc(num * sizeof(type)))//传递了int过来,这是函数不能做到的
int main()
{
int* p = MALLOC(10, int);
if (!NULL)
{
return 0;
}
p[0] = 2;
p[1] = 0;
p[2] = 2;
p[3] = 3;
for (int i = 0; i < 4; i++)
{
printf("%d ", p[i]);
}
free(p);
return 0;
}
//示范一
#include
#define ADD(X, Y) \
((X) + (Y))
int main()
{
printf("%d", ADD(2, 3));
return 0;
}
//示范二
#include
#define PRINTF(num, format)\ //这里也用了换行符
printf("The value of "#num" is "format, num)
int main()
{
int a = 100;
char b = 'c';
double c = 3.14;
PRINTF(a, "%d\n");
PRINTF(b, "%c\n");
PRINTF(c, "%f\n");
return 0;
}
//示例三
#include
#define FUN(X, Y) X##Y
int main()
{
int XY = 10;
printf("%d\n", FUN(X, Y));//依旧可以打印出10,X和Y被FUN连接为一串字符串了,并且整体作为变量名
return 0;
}
#include
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
int main()
{
int a = 3;
int b = 5;
int c = MAX(a++, b++);
//((a++)>(b++)?(a++):(b++)),出现加加两次的情况,使用者容易忽略
printf("%d\n", c);//6
printf("%d\n", a);//4
printf("%d\n", b);//7
return 0;
}
//但是函数在使用的时候就不会出现这种错误,两者之间最大的区别就在于“替换”
有很多C编译器都提供了一种能力,允许直接在命令行中定义符号用于启动编译过程,例如下面这一串代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
int arr[NUM];//NUM是未定义的,因为不确定运行该代码的机器是否内存足够…
for (int i = 0; i < NUM; i++)
{
arr[i] = i;
}
for (int i = 0; i < NUM; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
本次采用的是VS2022自带的编译器cl.exe,其输入命令行的输入命令是:
cl 需要编译的源文件名 -D NUM=100
gcc 需要编译的源文件名 -D NUM=100
这里就不再演示; ,可以在VScode里面进行演示
//单分支条件编译指令
#define _CRT_SECURE_NO_WARNINGS 1
#include
//#define NUM1 200
int main()
{
#ifdef NUM1/*或者写成#if也是可以的*/
printf("定义了NUM1");
#endif
return 0;
}
//多分支条件编译指令
#define _CRT_SECURE_NO_WARNINGS 1
#include
//#define NUM1 200
#define NUM2 100
int main()
{
#ifdef NUM1/*或者写成#if也是可以的*/
printf("定义了NUM1");
#elif NUM2
printf("定义了NUM2");
#else
printf("NUM1和NUM2均未定义\n");
#endif
return 0;
}
//正确的写法
#define _CRT_SECURE_NO_WARNINGS 1
#include
#define NUM1 200
int main()
{
#ifdef NUM1 == 200
printf("定义了NUM1");
#endif
return 0;
}
//错误的写法
#define _CRT_SECURE_NO_WARNINGS 1
#include
//#define NUM1 200
int main()
{
int NUM1 = 200;//这在预编译的时候是不可见的!NUM1还未被创建
#ifdef NUM1 == 200
printf("定义了NUM1");
#endif
return 0;
}
1.单分支的条件编译
#if 常量表达式,由预处理器求值
//...
#endif
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)//等价于#ifdef symbol
#if !defined(symbol)//等价于#ifndef symbol
4.嵌套编译指令
#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
#include
#include
…
//在别的地方自定义了一个function_1.h
//在别的地方自定义了一个function_2.h
#include "function.1"
#include "function.2"
//尽管对于头文件也可以使用引号来包含但是查找效率会变低,并且无法区分是库文件还是自定义头文件
//在VS2022里,如果没有在当前源文件路径下找到function_1或function_2头文件的话,就会到标准头文件路径下去找:
//(1)VS是在安装路径中, 一个叫做“include”的文件夹中
//(2)linux是在路径“/usr/include”中
//写法一:
#ifndef __FUNCTION_H__
#define __FUNCTION_H__
//其他代码
#endif
//写法二:
#pragme once
//其他代码
可以到《C语言深度解剖》的第三章里学习
#define offsetof(StructType, MemberName) (size_t)&(((StructType *)0)->MemberName)
#define SwapIntBit(n) (((n) & 0x55555555) << 1 | ((n) & 0xaaaaaaaa) >> 1)