说明:由于所有内容放在一个md文件中会非常卡顿,本文件将接续C.md文件的第二部分
结构体
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
-
结构体的成员可以是标量,数组,指针,甚至是其他结构体。
-
声明一个结构体类型:
struct Stu{ char name[20]; char sex[5]; int age; };
-
在什么结构体类型时就创建两个变量:
struct Stu{ char name[20]; char sex[5]; int age; }S1,S2;
-
创建一个结构体变量:
struct Stu s1 = {"张三","男",22}; // 创建时就初始化
-
输出结构体变量的内容:
printf("%s %s %s\n", s1.name, s1.sex, s1.age);
结构体传参
-
传结构体变量(传结构体本身,需要在内存复制一份同样大小的空间)
void PrintStu(struct Stu st){ // 调用 PrintStu(s1); printf("%s %s %s\n", st.name, st.sex, st.age); // 结构体变量.成员变量 }
因此函数传参的时候,参数是需要压入栈中的,当实参比较大的时候,这种传递结构体本身的方式,会导致形参复制的空间比较大,内存消耗就很大。
-
传结构体指针(传的是结构体地址)
void PrintStu(struct Stu* st){ // 调用 PrintStu(&s1); printf("%s %s %s\n", st->name, st->sex, st->age); // 结构体指针->成员变量 }
这种方式传的是地址,压入栈中也才4个或8个字节。
所以,结构体传参时,尽量传结构体的地址。
void SetStu(struct Stu s){
s.name = "张三"; // 错误写法,因为name是数组名,数组名是一个地址,不能把字符串给一个地址,应该是把字符串放在地址所指的空间里
strcpy(s.name, "张三"); // 正确写法
s.age = 20;
s.score = 120.0;
}
类型转换
隐式类型转换
-
C语言的整型算术运算符总是至少以缺省整型类型的精度来进行的,为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转化为普通整型,这种转化称为整型提升。
-
整型提升
是按照变量的数据类型的符号位来提升的负数的整型提升: char c = -1; // -1 是整数,占32bit,在内存中的补码形式为:1 111 1111 1111 1111 1111 1111 1111 1111 // 而char c,只有8个bit,把低八位放进c中,因此,c中存放的为 1111 1111 // 对c进行整型提升,对于1 111 1111 ,最高位(符号位)为1,表示是一个负数 // 按照符号位提升,1 111 1111 1111 1111 1111 1111 1111 1111 正数的整型提升: char c = 1; 高位补0 无符号数的整型提升,高位补0
-
例题
char a = 5; // 0 000 0101 char b = 126; // 0 111 1110 char c = a + b; // 1 000 0011 // 补码 // 1 111 1100 // 按位取反 // 1 111 1101 // 末尾加一 = -125 所以 c = -125
-
例题
char a = 0xb6; // b6 =00000000 00000000 00000000 10110110 截断给 a 后是一个负数,a = 10110110 = -74的补码 short b = 0xb600; int c = 0xb6000000; if(a == 0xb6) printf("a"); // 比较时发生整型提升,不输出 if(b == 0xb600) printf("a"); // 比较时发生整型提升,不输出 if(c == 0xb6000000) printf("a"); // 比较时不发生整型提升,输出
-
例题
char c = 1; printf("%u\n",sizeof(c)); // 1 printf("%u\n",sizeof(+c)); // +c,发生运算,所以发生整型提升,输出4 printf("%u\n",sizeof(-c)); // -c,发生运算,所以发生整型提升,输出4
算术转换
-
当一个类型小于整型时,会发生整型提升;当一个类型大于整型时,会发生算术转换
1. long double 2. double 3. float 4. unsigned long int 5. long int 6. unsigned int 7. int
-
上述类型中,编号越小,等级越高,如果某个操作数的类型在上面中的等级越低,就要转换成另一个操作数的类型后再执行运算
字符串
字符串字面量
- 字符串字面量据就是字符串常量,是用一对双引号括起来的字符序列,eg:“hello”,在C语言中,它被当作是字符数组来处理(char*类型的指针)。
- C编译器遇到长度为n的字符串字面量时,会为其分配长度为n+1的内存空间,在其末尾添上标志字符串末尾的空字符 \0
- 字符串字面量可以为空。字符串
""
作为单独的一个空字符来存储,在内存中,仅有 \0 这一个字符。 - 通常情况下,C语言允许使用char*类型指针的地方使用字符串字面量
char* p = “abc”
- C语言允许对指针取下标,因此,也可以对字符串字面量取下标
char ch; ch = “abc”[1]
此时 ch = b
字符串变量
-
对于字符串变量,只要保证字符串是以空字符结尾的,任何一维的字符数组都可以用来存储字符串
-
确定字符串长度的最快方法:逐个字符的搜索空字符(字符串的长度取决于空字符的位置)
-
当要声明一个用于存储字符串的字符数组时,要始终保证数组的长度比字符串的长度多一个字符,一个常用的写法是:
#define STR_LEN 80 ... char str[STR_LEN + 1]; // 可以存放的字符串的最大长度是80
-
在声明时初始化字符串变量
char data[7] = "ni hao"; // 编译器把字符串"ni hao"复制到字符数组data中,并在其末尾追加一个空字符,从而使其可以作为字符串使用 上式也可以等价为: char data[7] = {'n', 'i', ' ', 'h', 'a', 'o', '\0'}; // 这种写法需要手动加上'\0'
-
当字符数组的初始化式比数组本身的长度短时,余下的位置会被初始化为 ’\0’
-
如果初始化时,没有给空字符预留空间,那么编译器不会试图存储空字符,这是数组就不作为字符串来使用,因为字符串必须以空字符来结尾。
char data[6] = "ni hao"; // 这样也是合法的,不过就不会存储空字符了
-
字符串变量的声明中可以省略它的长度,编译器在编译时会自动计算其长度,且以后不能再改变,这个长度是预留了空字符的位置的
char data[] ="ni hao"; // data[]数组的长度为7
字符数组和字符指针
1. char data[] = "ni hao"; // 数组
2. char* data = "ni hao"; // 指针
- 相同点:上述两种声明都可以用作字符串,任何需要传递字符数组或字符指针的函数都可以接受这两种声明的data作为参数,这两种声明没有任何差异
- 不同点:
- 在声明为数组时,就像任意数组元素一样,可以修改存储在data中的任意字符。但是,声明为指针时,data指向的是字符串字面量,它存储在
只读内存区
,所以无法修改其内容。data[2] = ‘N’
这样修改是违法的。 - 在声明为数组时,data是数组名。在声明为指针时,date是变量,它可以在程序执行期间,指向其他字符串。
- 在声明为数组时,就像任意数组元素一样,可以修改存储在data中的任意字符。但是,声明为指针时,data指向的是字符串字面量,它存储在
- 所以,如果想修改字符串,就要用字符数组而不是字符指针来存储字符串。
- 在效率上,用指针来跟踪字符串中的当前位置更快速
字符串的输出
-
printf(“%s”, str)
输出字符串:逐个输出字符串中的字符,直到遇到空字符停止。如果空字符丢失,printf函数会越过字符串的末尾继续输出,直到在内存的某个地方遇到空字符为止,如:char str[3] = { '1','2','3' }; printf("%s", str); // 输出 123烫烫烫烫烫烫烫烫虗 qQ楮?
-
puts(str)
函数只有一个参数,它输出字符串,会自动添加一个额外的换行符。
字符串的输入
scanf(“%s”, str)
把字符串读入字符数组时,如果首先遇到空白字符(包括:空格符,水平和垂直制表符,换行符和换页符),然后读取字符并存储到str中,直到再次遇到一个空白字符为止。scanf函数始终会自动在字符串末尾存储一个空字符。因此,用scanf函数读取字符串时,永远不可能包含空白字符gets(str)
函数可以用于一次读取一整行的输入,它也会自动在字符串的末尾存储一个空字符。- 两者的不同之处:
- gets函数不会在开始读取字符串之前跳过空白字符(scanf函数会跳过)
- gets函数不会持续读取直到找到换行符才停止(scanf函数会在任意空白字符处停止)
- gets函数会忽略掉换行符,不会把它存储到数组中,在数组中用空字符代替换行符
gets(str)
和scanf(“%s”, str)
都无法检测数组何时被填满,因此,它们在存储字符串时,都可能越过数组的边界。对于scanf函数,可以用%ns来控制可以存储的最多的字符数,对于gets,可以用fgets函数替代。
<string.h>中的库函数
-
char* strcpy(char* s1, const char* s2);
把 s2 指向的字符串复制到 s1 指向的数组中,返回 s1 。把 s2 中的字符复制到 s1 中时,一直遇到 s2 中的第一个空字符为止(空字符也复制)
但是strcpy函数无法检查 s1 的大小是否能够存下 s2 指向的字符串
char str[10]; str = "abcd"; // 这样写是错误的,因为str是数组名,数组名不能被修改(不能作为左值) char str[10] = "abcd"; // 但是这样初始化字符数组是可以的,因为这里的 = 不是赋值运算符 要实现把字符串“abcd”存储到str中,可以利用库函数 strcpy(str, "abcd");
使用更安全的strncpy函数:
strcnpy(str1, str2, sizeof(str1))
将字符串 str2 复制到 str1中,复制的最大长度是 sizeof(str1),也就是 str1 的长度,但是如果str2长度大于str1,就会导致复制到str1中过后,缺少空字符。更安全的用法:
strcnpy(str1, str2, sizeof(str1) - 1) str[sizeof(str1)-1] = ‘\0’; // 确保str1总是能以空字符结束
-
size_t strlen(const char* s)
其实 size_t 类型就是 typedef 的无符号整型,返回的是字符串 s 的长度:s 中第一个空字符之前的字符个数,不包含空字符。
当用数组作为实际参数时,返回的不是数组本身的长度,而是存储在数组中的字符串的长度。
char str[] = "abcde"; printf("%d\n", strlen(str)); // 5
-
char* strcat(char* s1, const char* s2)
把字符串 s2 指向的字符串追加到 s1 指向的字符串的末尾(包括s2中的空字符),并返回指向 s1 的指针,把s1中原来的空字符丢掉
使用更安全的strncat函数:
strncat(str1, str2, sizeof(str1) - sizeof(str2) - 1)
第三个空字符计算出 str1 中剩余的空间,减 1 是确保为空字符留下一个空间
-
int strcmp(const char* s1, const char* s2)
比较字符串 s1 和字符串 s2 ,然后根据 s1 是否小于,等于或者大于 s2,函数返回一个小于,等于或者大于 0 的值
比较的规则是按照ASCII表的顺序来的:
- s1 和 s2 前 n 个字符一样,但是 s1 的 n+1 个字符小于 s2 的 n+1 个字符,所以 s1<s2
- s1 的所有字符和 s2 一致,但是 s1 比 s2 更短,所以 s1<s2
- 所有的大写字母都小于小写字母
- 所有的数字都小于字母
- 空格符小于所有的打印字符
也可以对字符串用关系运算符(<, <=, >, >=)或判等运算符(==, !=)进行比较
-
int sprintf(char *str, const char *format, ...);
str
:目标字符串(字符数组),用于存储格式化后的结果。format
:格式化字符串,指定如何格式化后续参数。sprintf
的格式化字符串与printf
的格式化字符串完全相同。...
:可变参数列表,根据format
的格式说明符提供数据。- 成功时返回写入的字符数(不包括字符串结尾的
\0
)。 - 失败时返回负值。
char buffer[100]; int num = 123; double pi = 3.14159; // 将整数和浮点数格式化到 buffer 中 sprintf(buffer, "Number: %d, Pi: %.2f", num, pi); // 输出结果 printf("Formatted string: %s\n", buffer); // Formatted string: Number: 123, Pi: 3.14 return 0;
sprintf
会自动在格式化后的字符串末尾添加\0
,因此目标字符串必须有足够的空间存储\0
。sprintf
不会检查目标字符串的长度,如果格式化后的字符串长度超过了目标字符串的大小,会导致缓冲区溢出,引发未定义行为。
字符串惯用写法
-
搜索字符串结尾的空字符
while(*s) s++; // s 最终指向空字符 while(*s++); // s 最终指向空字符的下一个位置(后缀++优先级比*更高)
-
使用更快的方法求字符串的长度
size_t my_strlen(const char* s){ const char* p = s; while(*s) s++; return s - p; // 用空字符地址减去第一个字符的地址得到字符串的长度 }
-
字符串复制的惯用写法
while(*p++ = *s++); // 由于空字符'\0'的ASCII码值为0,所以当复制到空字符时,就会终止循环 // 而且由于循环是在赋值之后终止,所以也是复制了空字符过去的,就不用再手动添加空字符了 char* my_strcat(char* s1, const char* s2){ char*p = s1; while(*p) p++; while(*p++ = *s2++); return s1; }
字符串数组
-
二维的字符数组
char planets[][8] = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"};
这种写法非常浪费空间,因为大部分字符串集都是长字符串和短字符串的集合,其在内存中如下所示:
-
可以用字符串的指针数组来存储上述内容:
char *planets[] = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"};
在内存中:
这样在内存中,就不会有任何浪费。
指针
本质上,指针就是地址,口语中的指针通常指的是指针变量,指针变量就是一个存放地址的变量,通过这个地址可以找到内存单元
指针变量的大小对于固定类型的机器来说是固定的,不会因为指针变量的类型而发生改变。如:32(X86)位机是4个字节,64(X64)位机器是8个字节。
int* p = a
,说p指向a,此时*p就是a的别名,他们俩是等价的 *p,*&a,a 三者是等价的- * 称为解引用操作符,又叫间接寻址运算符
- & 和 * 运算符相遇时,可以抵消
指针类型的意义
-
指针变量的类型,决定了指针在被解引用时,到底应该被访问几个字节。
- 如果是 int* 类型的指针,解引用时访问 4 个字节
- 如果是 char* 类型的指针,解引用时访问 1 个字节
- 如果是 double* 类型的指针,解引用时访问 8 个字节
- 可以依次推广
int a = 0x11223344; int * pa = &a; *pa = 0; // 此时 pa = 0x00000000; int b = 0x11223344; char * pb = (char *)&b; // &a的返回值类型使int*,通过强制类型转换,转换成char* *pb = 0; // 此时 pb = 0x00332211; 只修改到了最高位字节的8bit
-
指针变量的类型,决定了作 指针加1 或 指针减1 运算的时候,应该跳过几个字节
- 如果是 int* 类型的指针,加1时,应该跳过 4 个字节
- 如果时 float* 类型的指针,加1时,应该跳过4 个字节
- 如果时 char* 类型的指针,加1时,应该跳过 1 个字节
int a = 0x11223344; int* pa = &a; char* pb = &a; printf("pa = %p\n", pa); // 输出 pa = 010FFC54 printf("pb = %p\n", pb); // 输出 pb = 010FFC54 printf("pa+1 = %p\n", pa + 1); // 输出 pa+1 = 010FFC58, 加了4个字节 printf("pb+1 = %p\n", pb + 1); // 输出 pb+1 = 010FFC55, 加了1个字节
- 虽然,int* 和 float* 都是跳过 4 个字节,但是他们也不能通用
int a = 0; int* pa = &a; *pa = 100; // 内存里放的是 0x 64 00 00 00 0x64 = 100d float* pb = &a; *pb = 100.0; // 内存里放的是 0x 00 00 c8 42 因为*pb是按照浮点数类型来存放100的
避免野指针
-
局部变量指针未初始化
int* p; // p没有初始化,意味着没有明确的指向,一个局部变量如果不初始化,放的是随机值 *p = 10; // 非法访问内存,这里的 p 就是野指针
-
指针越界访问
int arr[10] = { 0 }; int* p = arr; int i; for (i = 0; i <= 10; i++) { *p = i; // 当i=10时,指针指向的范围超过数组arr的范围,p成为野指针 }
-
指针指向的空间释放
int* test(){ int a = 10; return &a; } int main(){ int *p = test(); ... 此后再次使用 p,就是野指针了,因为 a 是局部变量,函数调用结束后,它的空间就被销毁了 }
-
常见的习惯:
- 当一个指针不知道初始化成什么的时候,应先赋于NULL:
int* p = NULL;
- 如果要使用一个指针,可以加一个 if 条件判断:
if(p != NULL) {...}
- 当一个指针不知道初始化成什么的时候,应先赋于NULL:
const修饰的指针
-
const int* p
- 表示,p是指向“常整数”的指针,const保护p指向的对象,因此,*p不能被改变。*p=20是错误的
void func(const int* p){ int j; *p = 20; // 错误 p = &j; // 正确,但不会对函数外部有任何影响 }
-
int* const p
- 表示p是一个常指针,const可以保护 p 本身, const离p更近就保护p
void func(int* const p){ int j; *p = 20; // 正确 p = &j; // 错误 }
-
const int* const p
- 表示,同时保护p和p指向的对象,此时都不能修改
void func(int* const p){ int j; *p = 20; // 错误 p = &j; // 错误 }
指针算术运算(地址算术运算)
-
C语言支持三种格式的指针算术运算
-
指针加上整数(如果p指向a[i],那么 p + j 指向a[i + j])
-
指针减少整数(如果p指向a[i],那么 p - j 指向a[i - j])
-
两个指针相减(如果p指向a[i],q指向a[j],那么 p - q 就等于 i - j),只有两个指针指向同一个数组时,他们相减才有意义。指针相减的绝对值是这两个指针之间的元素个数。
int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p, *q, i; p = &a[2]; q = p + 3; // 指针加上一个整数,q = &a[5] P += 6; // 指针加上一个整数,p = &a[8] q = p - 3; // 指针减少一个整数,q = &a[5] p -= 6; // 指针减少一个整数,p = &a[2] p = &a[5]; q = &a[3]; i = p - q; // i = 5 - 3 = 2 i = q - p; // i = 3 - 5 = -2
-
-
可以对指针用关系运算符(<, <=, >, >=)和判等运算符(==, !=)进行比较,但,当且仅当,两个指针指向同一个数组时,指针的比较才有意义,其比较的结果依赖于数组中两个元素的相对位置。
p = &a[5]; q = &a[1]; p <= q; // 结果是0 p >= q; // 结果是1
用数组名作指针
可以使用数组的名字作为指向数组第一个元素的指针
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
*a = 6; // 相当于a[0] = 6
*(a+1) = 7; // 相当于a[1] = 7
-
a + i 就等于 &a[i] , *(a+i) 就等于 a[i]
-
把数组名当作指针时,不能给数组名赋新的值
int a[10]; a++; // 这样写是错误的 可以把a赋值给一个指针变量,然后修改该指针变量 p = a; p++; // 这是可以的
*运算符和++运算符
后缀++的优先级比*更高,
*p++ 和 *(p++) 等价
(*p)++
*++p 和 *(++p) 等价
++*p 和 ++(*p) 等价
二级指针
-
一级指针
int a = 10; int* pa = &a; // pa 是一级指针
-
二级指针
int a = 10; int* pa = &a; // pa 是一级指针变量, pa 指向 a int** pb = &pa; // pb 是二级指针变量, pb 指向 pa, pb 是指向指针的指针 pa 是 *pb a 是 **pb **pb = 20; // 就等价于 *pa = 20; 也等价于 a = 20;
-
二级指针变量是用来存放一级指针变量的地址
指针数组
-
指针数组就是存放指针的数组
int a = 10, b = 20, c = 30; int* pa = &a, * pb = &b, * pc = &c; int* arr[] = {pa, pb, pc}; for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) { // 输出 10 20 30 printf("%d ", *(arr[i])); }
-
用指针数组模拟二维数组
int arr1[] = { 1, 2, 3 }; int arr2[] = { 4, 5, 6 }; int arr3[] = { 7, 8, 9 }; int* parr[3] = { arr1, arr2, arr3 }; int i, j; for (i = 0; i < 3; i++) { for (j = 0; j < 3; j++) { printf("%d ", parr[i][j]); } printf("\n"); }
预处理器
预处理器是一个小软件,它可以在编译前处理C程序。这是C/C++语言不同于其他编程语言的地方
指令
-
把预处理器执行的命令称为指令,在C语言中,所有的指令都是以 # 开头的。 指令默认只占一行,每条指令的结尾没有分号或者其他特殊的标记。
-
# 符号不需要在一行的行首,但必须保证它之前只有空白字符
-
在指令的符号之间可以插入任意数量的空格或水平制表符:
# define N 100 // 这样写是可以的,因为注释也可以和指令放在同一行
-
指令可以出现在程序中的任何地方,虽然习惯把它放在程序的开头,但是它也可以放在程序的中间。
工作原理
预处理器的输入是一个 C 程序,程序可能包含指令,预处理器会首先执行这些指令,然后把这些指令删除(并没有删除包含指令的行,而是把他们替换为空行),还要将每一处的注释都替换为一个空格字符
简单的宏
#define 标识符 替换列表
-
宏的替换列表可以包括:标识符,关键字,数值常量,字符常量,字符串字面量,操作符和排列
-
当预处理器遇到一个宏定义时,会将程序中所有遇到的标识符替换为替换列表的内容
-
如:对类型重命名
#define BOOL int
-
替换列表可以为空,如:
#define DEBUG
-
在宏定义中,习惯于用大写字母
带参数的宏(函数式宏)
#define 标识符(x1,x2,...,xn) 替换列表
标识符和左括号之间必须没有空格
#define MAX(x,y) ((x)>(y)?(x):(y))
-
带参数的宏可以包含空的参数列表
#define getchar() getc(stdin)
这样可以使 getchar()更像一个函数
-
使用带参数的宏替换真正的函数,可能会使程序更快;且,宏的参数没有类型,也就是说宏可以接受任何类型的参数。
-
预处理器不含检查宏参数的类型,也不会进行类型转换
-
无法用一个指针来指向一个宏
-
函数只会计算一次它的参数,但是宏可能会不止一次地计算它的参数:
#define MAX(x,y) ((x)>(y)?(x):(y)) n = MAX(i++,j); 也就是 n = ((i++)>(j)?(i++):(j)); // 这样 i 可能会被错误的加两次
-
宏定义中的圆括号是必须的,因为不知道传入的参数是什么,无法确保编译器的顺序,所以参数出现的每一次都要添加
圆括号
。 -
宏可以简化需要重复书写的代码段模式,例如,如果需要重复的写:
printf("%d ", n);
,就可以用宏替换#define PRINT_INT(n) printf("%d ", n) 此时 printf("%d ", n); 就 等价于 PRINT_INT(n);
-
一般,宏定义不要加分号,在调用的时候再加上,这样可以避免很多错误
#define
#define 指令定义了一个宏,用来给其他东西起别名,例如常量或常用的表达式。预处理器会通过将宏的名字和其定义存储在一起来响应#define指令,直到这个宏在程序中被使用时,预处理器会扩展宏,将宏替换为其定义值。
#undef 指令删除一个宏定义
#include
#include指令告诉预处理器打开一个特定的文件,将这个文件的内容作为正在编译的文件的一部分“包含”进来。
#error
-
#error指令预示着程序中出现了严重的错误,有些编译器会立即终止编译,不再检查其他错误
-
#error指令通常和其条件编译指令结合使用:
#if INT_MAX < 100000 #error int type is too small #endif
上上述代码用于测试一台机器的int类型的最大值是否能放下100000。
#运算符
#运算符所执行从操作可以理解为“字符串化”,如:
#define PRINT_INT(n) printf(#n " = %d\n", n)
n 之前的 # 运算符就是通知预处理器根据PRINT_INT的参数创建一个字符串字面量,所以 PRINT_N(i/j) 就会变为 printf("i/j" "= %d\n", n);
由于相邻的字符串字面量会被合并,因此,上边的语句又可以等价为:printf("i/j = %d\n", n);
##运算符
##运算符又被称为“记号粘合”,其作用是将两个记号(如标识符)粘合在一起,如:
#define MK_ID(n) i##n
当MK_ID被调用时,如:
int MK_ID(1),MK_ID(2),MK_ID(3);
就等价于 int i1,i2,i3;
条件编译
#if #ifdef #ifndef #elif #else #endif
-
#if 和 #endif 指令
在测试代码的时候,可能有的时候某段代码需要,而有的时候,这段代码又不需要,这时,可以通过这两个预处理器指令来实现:
#define DEBUG 1 ... #if DEBUG ... 测试代码 ... #endif
这样,当我们需要测试代码执行时,就把DUBUG改为1。在预处理时,预处理器遇到 #if 指令时,会计算 if 后边常量表达式的值,如果表达式的值为0,就会删除 #if 和 #endif 之间的行; 如果表达式的值非0,就会保留之间的行。如果 #if 后边的表达式是没有定义过的标识符,预处理器会把它当作 0 看待。
-
defined运算符
这个运算符是专门判断一个标识符是否为已经被定义过的宏(无论这个宏是否被赋值,只要定义了),是就返回1,否则返回0。defined运算符和 #if 指令结合使用:
#if defined(DEBUG) ... #endif
当DEBUG被定义成宏时,就会执行之间的代码。
-
#ifdef 和 #ifndef 指令
#ifdef指令用于测试一个标识符是否已经被定义为宏:
#ifdef 标识符 ... 测试代码 ... #endif --->等价于<--- #if defined(标识符) ... 测试代码 ... #endif
#ifndef指令用于测试标识符有没有没被定义为宏:
#ifndef 标识符 ... 测试代码 ... #endif --->等价于<--- #if !defined(标识符) ... 测试代码 ... #endif
-
#elif 和 #else 指令
#if ,#ifdef,#ifndef 指令可以像普通的 if 语句那样嵌套使用,嵌套时,添加合适的缩进:
#if 表达式1 表达式1成立时需要测试的代码 #elif 表达式2 表达式1为假,但表达式2为真时需要测试的代码 #else 其他情况下需要测试的代码 #endif
在 #if 和 #endif 指令之间可以有任意多个 #elif 指令,但只能有一个 #else 指令。
-
例子:临时屏蔽掉不想用的代码而不用注释的方式(条件屏蔽):
#if 0 代码段 #endif
宏的通用属性
-
宏的替换列表可以包含对其他宏的调用,如:
#define PI 3.14159 #define TWO_PI 2*PI
遇到TWO_PI时,替换成 2*PI,然后重新检查替换列表,再次替换成 2*3.14159
-
宏定义的作用范围通常到出现这个宏的文件末尾。由于宏是由预处理器处理的,他们并不遵从通常的作用域规则,因此,在函数内部定义的宏,不仅在函数内部起作用,而且作用到文件的末尾。
-
宏可以使用#undef取消宏定义,如:
#undef N
会删除 N 当前的定义,如果 N 没有被定义为一个宏,那么这个指令不会有任何作用。
预定义宏
宏名称 | 含义 |
---|---|
__DATE__ | 当前源文件的编译日期,格式为 "MMM DD YYYY" (例如 "Oct 10 2023" )。 |
__TIME__ | 当前源文件的编译时间,格式为 "HH:MM:SS" (例如 "14:30:00" )。 |
__FILE__ | 当前源文件的文件名(包括路径)。 |
__LINE__ | 当前代码行的行号(整数)。 |
__func__ | 当前函数的名称(C99 标准引入)。 |
__STDC__ | 如果编译器符合 ANSI C 标准,则定义为 1 ,否则未定义。 |
__STDC_VERSION__ | 表示编译器支持的 C 标准版本(C95 为 199409L ,C99 为 199901L ,C11 为 201112L )。 |
__cplusplus | 如果编译器是 C++ 编译器,则定义其值为 C++ 标准的版本号(例如 199711L 或 201103L )。 |
__GNUC__ | 如果使用 GCC 编译器,则定义其值为 GCC 的主版本号(例如 11 )。 |
__GNUC_MINOR__ | 如果使用 GCC 编译器,则定义其值为 GCC 的次版本号(例如 3 )。 |
__GNUC_PATCHLEVEL__ | 如果使用 GCC 编译器,则定义其值为 GCC 的修订版本号(例如 0 )。 |
__WIN32__ | 如果在 Windows 平台上编译,则定义。 |
__linux__ | 如果在 Linux 平台上编译,则定义。 |
__APPLE__ | 如果在 macOS 或 iOS 平台上编译,则定义。 |
__x86_64__ | 如果在 64 位 x86 架构上编译,则定义。 |
__i386__ | 如果在 32 位 x86 架构上编译,则定义。 |
__ARM_ARCH | 如果在 ARM 架构上编译,则定义其值为 ARM 架构版本号(例如 7 )。 |
_MSC_VER | 如果使用 Microsoft Visual C++ 编译器,则定义其值为编译器版本号(例如 1920 表示 VS 2019)。 |
STDC_VERSION__ | 表示编译器支持的 C 标准版本(C95 为 199409L ,C99 为 199901L ,C11 为 201112L )。 |
__cplusplus | 如果编译器是 C++ 编译器,则定义其值为 C++ 标准的版本号(例如 199711L 或 201103L )。 |
__GNUC__ | 如果使用 GCC 编译器,则定义其值为 GCC 的主版本号(例如 11 )。 |
__GNUC_MINOR__ | 如果使用 GCC 编译器,则定义其值为 GCC 的次版本号(例如 3 )。 |
__GNUC_PATCHLEVEL__ | 如果使用 GCC 编译器,则定义其值为 GCC 的修订版本号(例如 0 )。 |
__WIN32__ | 如果在 Windows 平台上编译,则定义。 |
__linux__ | 如果在 Linux 平台上编译,则定义。 |
__APPLE__ | 如果在 macOS 或 iOS 平台上编译,则定义。 |
__x86_64__ | 如果在 64 位 x86 架构上编译,则定义。 |
__i386__ | 如果在 32 位 x86 架构上编译,则定义。 |
__ARM_ARCH | 如果在 ARM 架构上编译,则定义其值为 ARM 架构版本号(例如 7 )。 |
_MSC_VER | 如果使用 Microsoft Visual C++ 编译器,则定义其值为编译器版本号(例如 1920 表示 VS 2019)。 |