C语言学习笔记(第二部份)

说明:由于所有内容放在一个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是变量,它可以在程序执行期间,指向其他字符串。
  • 所以,如果想修改字符串,就要用字符数组而不是字符指针来存储字符串。
  • 在效率上,用指针来跟踪字符串中的当前位置更快速

字符串的输出

  • 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>中的库函数

  1. 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总是能以空字符结束
    
  2. size_t strlen(const char* s)

    其实 size_t 类型就是 typedef 的无符号整型,返回的是字符串 s 的长度:s 中第一个空字符之前的字符个数,不包含空字符。

    当用数组作为实际参数时,返回的不是数组本身的长度,而是存储在数组中的字符串的长度。

    char str[] = "abcde";
    printf("%d\n", strlen(str));	// 5
    
  3. char* strcat(char* s1, const char* s2)

    把字符串 s2 指向的字符串追加到 s1 指向的字符串的末尾(包括s2中的空字符),并返回指向 s1 的指针,把s1中原来的空字符丢掉

    使用更安全的strncat函数:

    strncat(str1, str2, sizeof(str1) - sizeof(str2) - 1)

    第三个空字符计算出 str1 中剩余的空间,减 1 是确保为空字符留下一个空间

  4. 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
    • 所有的大写字母都小于小写字母
    • 所有的数字都小于字母
    • 空格符小于所有的打印字符

    也可以对字符串用关系运算符(<, <=, >, >=)或判等运算符(==, !=)进行比较

  5. 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) {...}

const修饰的指针

  1. const int* p

    • 表示,p是指向“常整数”的指针,const保护p指向的对象,因此,*p不能被改变。*p=20是错误的
    void func(const int* p){
    	int j;
    	*p = 20;  // 错误
    	p = &j;	  // 正确,但不会对函数外部有任何影响
    }
    
  2. int* const p

    • 表示p是一个常指针,const可以保护 p 本身, const离p更近就保护p
    void func(int* const p){
    	int j;
        *p = 20;  // 正确
    	p = &j;	  // 错误
    }
    
  3. 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++ 标准的版本号(例如 199711L201103L)。
__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++ 标准的版本号(例如 199711L201103L)。
__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)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亲爱的老吉先森

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值