这篇文章上次修改于 656 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

C Primer Plus 笔记2

11.5 字符串函数

略,见表

11.9 把字符串转换为数字

int atoi(const char *str);
double atof(const char *str);
long atol(const char *str);
// 等等

它们会先忽略前面的空格字符(例如空格、制表符、换行符等)。然后会一直解析到第一个非空白字符,把之前的整数部分转换返回。如果字符串为空,或者不包含可解析的整数,atoi() 返回0。

C语言对这种行为未定义,是不安全的。

更安全的一系列函数

long strtol(const char *restrict nptr, char **restrict endptr, int base);
float strtof(const char *restrict nptr, char **restrict endptr);
double strtod(const char *restrict nptr, char **restrict endptr);
  • nptr:要转换的字符串。
  • endptr:一个指向字符指针的指针,用于指示转换结束的位置。如果不需要这个信息,可以将该参数设置为 NULL
  • base:指定转换的进制,可以是 0 或者介于 2 到 36 之间的任意值。如果设置为 0,则会根据前缀来判断进制(0x 表示十六进制,0 表示八进制,无前缀表示十进制)。

其中endptr是作为类似于输出值的参数,以下处理用例

const char *str = "2147483648"; // 超过 int 范围的值
char *endptr;
errno = 0; // 设置错误码为 0
long result = strtol(str, &endptr, 10);
if ((result == LONG_MAX || result == LONG_MIN) && errno == ERANGE) {
    // 转换溢出
    printf("Overflow or underflow occurred.\n");
} else if (str == endptr) {
    // 没有有效的转换,出现错误
    printf("Invalid input.\n");
} else {
    // 转换成功,result 包含转换后的值
    printf("Value: %ld\n", result);
}
  • strtol() 函数在转换时会忽略前导空格字符。
  • 在使用 strtol() 前,应确保包含 <stdlib.h> 头文件。

12.1.2 链接

链接(Linkage)指的是标识符(如变量、函数、常量等)在不同文件之间的可见性和访问性。

有三种类型的链接:外部链接、内部链接和无链接。

外部链接(External Linkage): 可以被其他文件引用。在不同编译单元中声明并定义相同名称的外部链接标识符会引起链接错误。

int global_var = 10; // 具有外部链接

// 文件2.c
extern int global_var; // 引用外部链接的标识符

内部链接(Internal Linkage): 无法被其他文件引用,在不同编译单元中可以有相同名称的内部链接标识符。

static int local_var = 5; // 具有内部链接

// 文件2.c
static int local_var = 15; // 在不同编译单元中也可以有同名的内部链接标识符

无链接(No Linkage): 无链接的标识符只在当前作用域内可见,不能被其他编译单元引用。通常,局部变量和函数参数都具有无链接。

void func() {
    int local = 20; // 无链接的局部变量
}

12.1.4 自动变量

默认情况下,声明在块或者函数头中的额任何变量都属于自动储存类别。

int main(void){
	auto int plox;
    // 显式使用关键词auto 有意覆盖一个外部变量定义,强调不要改为其它储存类别
    ...
}

12.1.5 寄存器变量

register 关键字: 在C语言中,可以使用 register 关键字来建议编译器将一个变量存储在寄存器中。这个关键字并不是强制性的,编译器可以选择忽略它。编译器会根据实际情况决定是否将变量存储在寄存器中,所以 register 关键字在现代编译器中的作用有限。

register int x; // 提示编译器将 x 存储在寄存器中

访问可以更快,减少内存占用,但是寄存器数量有限,不适用于所有变量(不能储存地址)。减低可移植性。

12.1.7 外部链接的静态变量

// file1.c
int traveler = 1;

// file2.c
int stayhome = 1;
// main.c
extern int traveler;   // 引用 file1.c 中的 traveler
extern int stayhome;   // 引用 file2.c 中的 stayhome

int main(void) {
    // 使用 traveler 和 stayhome
    return 0;
}

12.1.11 储存类别和函数

用static定义的函数相当于文件内的私有函数,不能被引用,而不用static定义的函数则相当于公有函数,可以被引用。

12.2 随机数函数和静态变量

随机数函数文件有以下内容

static unsigned long int next = 1;

int rand0(void) {
    next = next * 1103515245 + 12345;
    return (unsigned int)(next / 65536) % 32768;
}

void srand0(unsigned int seed) {
    next = seed;
}

这个next很有类中的私有成员变量的味道,srand0就很像setter

12.4 分配内存:malloc() 和 free()

double * ptd;
ptd = (double *) malloc(30 * sizeof(double));
// 定义一个30长度的double数组
free(ptd) // 配套使用free返还内存
    
int (* p3) [m];
p3 = (int (*) [m]) malloc (n * m * sizeof(int)) 
// 创建一个m x n 的二维数组

如果分配失败,会返回一个NULL指针。

// 分配一个包含 5 个 int 元素的数组,并初始化为零
int *array = (int *)calloc(5, sizeof(int));

if (array != NULL) {
    for (int i = 0; i < num_elements; i++) {
        printf("%d ", array[i]); // 应该输出全部为 0
    }
    free(array); // 释放内存
} else {
    printf("Memory allocation failed.\n");
}

calloc的特点是会初始化元素为0.

12.5 ANSI C 类型限定符

当把全局变量放在头文件时,应当注意使用static声明全局变量,如果没有static,那么在两个不同的C文件引用这个头文件将会导致每个文件都有一个相同标识符的定义式声明,C不允许这个情况。

volatile 类型限定符

volatile告诉编译器不要对被声明为volatile的变量进行优化,因为这些变量的值可能会在程序的意料之外的情况下发生改变。

例如:

val1 = x;
// 中间略(x未修改)
val2 = x;

编译器会发现使用了两次x但没有修改x的值,于是编译器可能把x优化在寄存器里,在val2读取时采用寄存器上的x,这个过程称为高速缓存,指定volatile即显式说明不要使用该优化。

volatile主要用于以下两种情况:

  1. 硬件寄存器和外部设备访问: 当你在程序中访问硬件寄存器或外部设备的状态时,这些状态可能会在任何时候发生变化,但是编译器在优化代码时可能会认为这些变量不会变化,从而导致错误的优化。通过使用volatile关键字,你告诉编译器不要对这些变量进行优化,以确保每次访问时都会读取最新的值。
  2. 多线程或中断环境: 在多线程或中断处理程序中,一个变量的值可能会被其他线程或中断修改,而编译器可能会进行优化,认为变量的值在自身线程中不会改变。使用volatile可以确保每次访问都从内存中读取最新的值,而不是从寄存器或缓存中读取。

restrict 类型限定符

restrict 只能声明为指针,意味着在指针的生命周期内,该指针是访问对象的唯一途径,没有其他指针可以同时指向同一个对象。这样的声明允许编译器在优化代码时进行更激进的优化,以提高性能。可以使用在获取的动态内存指针中。

  1. 不能在同一个函数中使用restrict来声明多个指向相同类型对象的指针
  2. 在指针的生命周期内要求指针不会被别名。

优化示例

int ar[10];
int * restrict restar = (int *) malloc(10 * sizeof(int));
int * par = ar;
for (n = 0; n < 10; n++){
    par[n] += 5;
    restar[n] += 5;
    ar[n] *= 2;
    par[n] += 3;
    restar[n] += 3;
}

编译器会优化为restar[n] += 8; 但是par不能这么做,因为ar也修改了par的内存。

_Atomic类型限定符

在C11中,你可以使用 <stdatomic.h> 中的库函数和宏来执行原子操作,通常用于多线程

12.5.5 新旧关键词的位置

“c99允许把类型限定符放在方框当中”

关于这一点在书本P406,我在网上没有找到相应的使用,可能不常用。

13.2 标准I/O

fopen()函数

第二个参数是文件打开的模式,类似于python

C11新增了带x的写模式

wx写模式打开文件,但只有在文件不存在的情况下才成功。如果文件已经存在,则打开操作将失败,返回 NULL

x模式具有独占特性:

  • 原子性创建:在多线程或多进程环境中,如果多个实体同时尝试打开同一个文件以进行写入,使用 ‘x’ 模式可以确保只有一个实体能够成功,其他将会失败。

  • 避免竞态条件: 竞态条件是指多个进程或线程在没有适当同步的情况下访问共享资源,导致未定义的行为或数据损坏。使用 ‘x’ 模式可以防止多个实体同时尝试创建同一个文件。

getc() 和 putc() 函数

与getchar()和putchar类似,多一个参数用于指定文件流。

fclose() 函数

fclose(fp) 用于关闭fp指定的文件,如果成功关闭将返回0否则是EOF。

指向标准文件的指针

标准文件 文件指针 通常使用的设备
标准输入 stdin 键盘
标准输出 stdout 显示器
标准错误 stderr 显示器

fprintf()与printf()类似,多了第一个参数为文件指针

fscanf 同理

rewind(fp) 函数用于将文件流的位置指示器(文件指针)重新设置为文件的开头,以便进行后续的读取操作。

例如:

fprintf(stderr, "Failed to open error log file!\n");
fscanf(stderr, "Failed to open error log file!\n");
#include <stdio.h>

int main() {
    FILE *file = fopen("example.txt", "r"); // 以只读模式打开文件
    
    if (file != NULL) {
        char buffer[100]; // 用于存储读取的内容
        fscanf(file, "%s", buffer); // 读取文件内容
        printf("Read: %s\n", buffer);
        
        rewind(file); // 将文件指针重新设置为文件开头
        
        fscanf(file, "%s", buffer); // 重新读取文件内容
        printf("Read again: %s\n", buffer);
        
        fclose(file); // 关闭文件
    } else {
        printf("Failed to open the file.\n");
    }
    return 0;
}

随机访问:fseek() 和 ftell()

fseek()ftell() 是C标准库中用于文件定位的函数,它们允许您在文件中移动文件指针,以便进行特定位置的读取和写入操作。

  1. fseek() 函数

    int fseek(FILE *stream, long offset, int origin);
    • stream: 指向已打开文件的文件指针。

    • offset: 要移动的偏移量,单位是字节。

    • origin
      
      
           : 偏移量的参考点,可以是以下值之一:
      
           - `SEEK_SET`:从文件开头开始计算偏移。
           - `SEEK_CUR`:从当前文件指针位置开始计算偏移。
           - `SEEK_END`:从文件末尾开始计算偏移。
      
         `fseek()` 函数用于将文件指针定位到指定位置,以便进行读取或写入操作。它返回非零值表示失败,零表示成功。
      
      2. **`ftell()` 函数**:
      
         ```C
         long ftell(FILE *stream);
    • stream: 指向已打开文件的文件指针。

    ftell() 函数返回当前文件指针的偏移位置,即当前文件指针距离文件开头的字节数。如果出现错误,它返回 -1L。

13.5.4 fgetpos() 和 fsetpos() 函数

fgetpos()fsetpos() 是C标准库中用于文件位置的函数,它们允许您以更高级别的方式保存和恢复文件流的位置。这些函数对于处理二进制文件或在不同平台上移植代码时特别有用,因为它们可以保持文件位置的精确性。同时还可以避免long类型不够读取位置的问题。

  1. fgetpos() 函数

    int fgetpos(FILE *stream, fpos_t *pos);
    • pos: 一个指向 fpos_t 类型对象的指针,用于保存当前文件指针的位置。

    fgetpos() 函数用于获取当前文件指针的位置,并将其保存到指定的 fpos_t 对象中。它返回非零值表示失败,零表示成功。

  2. fsetpos() 函数

    int fsetpos(FILE *stream, const fpos_t *pos);
    • pos: 一个指向 fpos_t 类型对象的指针,用于指定要设置的文件指针的位置。

    fsetpos() 函数用于将文件指针恢复到之前使用 fgetpos() 保存的位置。它返回非零值表示失败,零表示成功。

fpos_t 是一个类型,用于存储文件指针的位置信息。它的实际定义可能因平台而异。

13.7 其他I/O函数

见p427

14.3.3 结构的初始化器

C99 和 C11 为结构提供了指定初始化器

struct book book {
    		char title[MAXTITLE];
    		char author[MAXAUTL];
    		float value;
}
struct book gift = { .value = 25.99,
                     .author = "James",
                     .title = "Rue for the Toad"}; // 可任意顺序
struct book gift = { .value = 18.90,
                     .author = "Philionna Pestle",
                     0.25}; // 此处value被0.25覆盖

14.4 结构数组

如果在栈上分配大型的数据结构或数组,可能会导致栈空间不足。这是因为栈是一个相对较小的内存区域,适合存储小而局部的数据。应当注意栈的大小。如果不足,可以设置栈的大小或者动态内存分配。

14.7.5 结构和结构指针的选择

写一个和结构相关的函数,使用结构指针作为参数返回结果,还是用结构作为参数和返回值

struct vector sum_vector(struct vector, struct vector);
ans = sum_vector(a, b);

把结构作为参数传递的优点:函数处理副本,保护原数据,代码风格更清楚

void sum_vector(const struct vector *, const struct vector *, struct vector *)
sum_vector(a, b, ans);

把指针作为参数和返回值的优点:兼容老版本的代码,减少传递结构的时间空间浪费(创建副本)还可以通过const来保证数据不会被修改

通常,为了追求效率会使用指针,按值传递是小型结构常用的方法。

14.7.6 结构中的字符数组和字符指针

在结构中使用字符指针容易造成错误

struct names{char last[100]};
struct pnames{char * last};

struct names accountant;
struct pnames attorney;
scanf("%s", accountant.last);
scanf("%s", attorney.last);

我们可以发现如果是accountant,我们的数据储存在了last数组内,但是对于attorney,我们无法确定我们输入的字符内存在哪里,只能确定我们把一个指针赋给了last,关于这段字符串的储存就变成了未定义的行为。

14.7.6 结构中的字符数组和字符指针

但是使用字符数组会让结构变得很大,我们可以使用malloc()来分配动态内存来储存它们

char buffer[100];
printf("Enter last name for attorney: ");
scanf("%s", buffer);

attorney.last = (char *)malloc(strlen(buffer) + 1); // Allocate memory
if (attorney.last != NULL) {
    strcpy(attorney.last, buffer); // Copy string
} else {
    // Handle memory allocation failure
    printf("Memory allocation failed for attorney.last\n");
}

// ...

// Don't forget to free allocated memory
free(attorney.last);

14.7.9 伸缩型数组成员

伸缩型数组成员用于动态分配结构体内部数组的技术。它允许你在结构体的末尾定义一个数组成员,但是这个数组的大小在运行时可以根据需要进行分配。

伸缩型数组成员的定义形式为:

struct MyStruct {
    // 其他成员
    size_t length;  // 记录数组的长度
    char data[];    // 伸缩型数组成员
};

这里,data 是一个伸缩型数组成员。注意以下几点:

  1. 伸缩型数组成员必须是最后一个成员。因为在内存中,该数组的大小是在结构体的末尾决定的,从而允许动态分配。
  2. 结构体中必须包含一个用于存储数组长度的变量,通常命名为 length 或类似的名字。这样,你在运行时可以知道数组的实际大小。

使用伸缩型数组成员时,通常需要进行手动的内存管理。你可以通过使用 malloc 分配结构体和数组的内存,然后根据需要更新 length 字段来记录数组的大小。当你不再需要这个结构体时,需要确保正确释放分配的内存。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct DynamicString {
    size_t length;
    char data[];
};

struct DynamicString *createDynamicString(const char *text) {
    size_t textLength = strlen(text);
    struct DynamicString *str = (struct DynamicString *)malloc(sizeof(struct DynamicString) + textLength + 1);
    
    if (str != NULL) {
        str->length = textLength;
        strcpy(str->data, text);
    }
    
    return str;
}

int main() {
    const char *text = "Hello, World!";
    struct DynamicString *dynStr = createDynamicString(text);
    
    if (dynStr != NULL) {
        printf("Dynamic String: %s\n", dynStr->data);
        free(dynStr);
    }
    
    return 0;
}

伸缩型数组成员的特殊处理要求:

  • 不能用结构进行赋值或者拷贝
  • 不要按值方式把这种结构传递给结构
  • 不要使用带伸缩型树组成员的结构作为数字成员或者另一个结构的成员

struct hack

“Struct Hack” 是一种在C语言中用于动态分配结构体内数组的技巧,类似于伸缩型数组成员。它的名字可能听起来有些奇怪,但它是一种早期的、有些非正式的技术,用于实现变长数组在结构体内的存储。这个技巧的原理是,通过在结构体内部定义一个长度为1的数组,然后动态地分配实际所需的大小,从而实现在结构体中存储变长的数组。

这个技巧的一般形式如下:

struct MyStruct {
    // 其他成员
    char data[1]; // 虚拟数组,长度为1,实际长度会动态分配
};

这里的 data 是一个长度为1的数组,但它实际上在运行时会根据需要被分配更多的内存,以适应实际的数据大小。

使用 “Struct Hack” 需要特殊的内存分配和释放技巧。通常的做法是,在分配内存时分配额外的空间来容纳整个结构体以及数组部分的数据,然后在实际使用时,通过偏移量访问数组部分的数据。

以下是一个使用 “Struct Hack” 的简单示例,用于存储可变长度的字符串:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct StringStruct {
    size_t length;
    char data[1]; // 虚拟数组
};

struct StringStruct *createStringStruct(const char *text) {
    size_t textLength = strlen(text);
    struct StringStruct *str = (struct StringStruct *)malloc(sizeof(struct StringStruct) + textLength);
    
    if (str != NULL) {
        str->length = textLength;
        strcpy(str->data, text);
    }
    
    return str;
}

void destroyStringStruct(struct StringStruct *str) {
    free(str);
}

int main() {
    const char *text = "Hello, Struct Hack!";
    struct StringStruct *stringStruct = createStringStruct(text);
    
    if (stringStruct != NULL) {
        printf("Length: %lu\n", stringStruct->length);
        printf("Data: %s\n", stringStruct->data);
        
        destroyStringStruct(stringStruct);
    }
    
    return 0;
}

尽管 “Struct Hack” 技巧在过去可能有用,但现代C标准中已经不推荐使用,因为它可能会违反内存对齐规则,并且可能与某些编译器优化冲突。在现代C语言中,更推荐使用伸缩型数组成员或更加安全的动态内存分配和管理方法。

14.8 保存结构为文件

struct book {
    char title[40];
    char author[40];
    float value;
}
book primer = {"qwq", "qaq", 12.34};

fprintf(file, "%s %s %.2f\n", primer.title, primer.author, value);

fwrite(&primer, sizeof(book), 1, file);

14.11 枚举类型

假设有以下声明:

enum feline {cat, lynx = 10, puma, tiger};

对于默认值 cat 为 0,puma 为 11, tiger 为 12.

14.11.5 共享名称空间

在C语言中,结构名和变量名并不在一个命名空间,所以以下是合法的

struct rect {double x; double y;};
int rect;

但在C++中,这种写法不合法,因为它把标记名和变量名放在相同的命名空间。

14.12 typedef 简介

define预处理器不同的功能

#define STRING char *
typedef char * STRING;

STRING name, sign; // 根据这个用例就可以发现了

typedef 对于结构的使用

typedef struct {double x; double y;} rect;

typedef并没有创建新的类型,而是对存在的匿名类型赋予了一个名称。

typedef 的复杂使用

typedef char (* FRPTC()) [5];

char (*FRPTC())[5]: 这是一个函数声明,表示函数名为FRPTC,该函数没有参数,返回一个指向长度为5的字符数组的指针。

用例如下

#include <stdio.h>

typedef char (*FRPTC())[5];

// 这是一个函数,返回一个指向长度为5的字符数组的指针
FRPTC createArray() {
    static char array[5] = "Hello";
    return &array;
}

int main() {
    // 使用别名声明一个函数指针
    FRPTC funcPtr = createArray;

    // 调用函数获取指向字符数组的指针,并输出其中的内容
    char (*ptrToCharArray)[5] = funcPtr();
    printf("Array content: %s\n", *ptrToCharArray);

    return 0;
}

4.13 复杂声明

以下是一些比较复杂的声明:

int board[8][8];
int ** ptr;
int * risks[10];
int (* rusks)[10];
int *oof[3][4];
int (* uuf)[3][4];
int (* uof[3])[4];

对于复杂的声明,关键是理解优先级:

数组名后面的[]和函数名后面的()拥有相同的优先级,优先级高于*

int * risks[10];代表一个指针数组

[]()优先级相同于是从左往右

int (* rusks)[10];代表一个指向数组的指针

同理

int oof[3][4];代表一个3个内涵4个int的数组的数组

对于int *oof[3][4];

由于[]的优先级较高,意思为代表一个3个内涵4个int *的数组的数组

对于int (* uuf)[3][4];

*先和uuf结合,代表我们定义的是一个指针所以uuf是一个指向3x4int的指针

对于int (* uof[3])[4];表示一个名为uof的数组,包含3个元素,每个元素都是指向长度为4的整型数组的指针。

15.1.2 有符号整数

以一个一字节的数做例子(共8个位)

符号量表示法:用第一位来表示符号,剩下的7位表示数字,例如10000001表示-1,00000001表示1,因此表示范围为-127~127。但是缺点在于有两个0,+0和-0容易混淆,并且用两个位表示一个数浪费。并且不利于加法计算。

二进制反码:正数的表示与符号量表示相同(0~127),负数的补码是通过以下步骤得到的:

  1. 取负数的绝对值,转换成二进制。
  2. 对二进制表示按位取反

不同于补码+1,更像是二进制补码的前身,在使用二进制反码表示负数时,减法操作可以通过将减法转化为加法来实现,但是还是存在两个0的问题。因此它的表示范围在-127~127

二进制补码:正数的表示与符号量表示相同(0~127)。负数的补码是通过以下步骤得到的:

  1. 取负数的绝对值,转换成二进制。
  2. 对二进制表示按位取反
  3. 对取反后的二进制表示加1

使用补码表示可以使加法和减法运算在硬件中变得更加简单,因为减法可以转化为加法操作。此外,补码还解决了在普通的二进制表示中存在的“两个零”的问题,即+0和-0。在补码中,只有一个表示零的方式,即全零的二进制。因此它的表示范围在-128~127

15.3 C按位运算符

掩码按位与运算符常用于掩码,将掩码与目标值进行与运算,掩码的0将会覆盖原来的值而1就代表原来的值,这体现了掩码的用法。

flags &MASK

打开位(设置位):打开其中的一个特定位,不改动其它位,用到按位或运算符,MASK中1的位置表示打开的位置, 0表示不动的位置

flags | MASK

关闭位(清空位):不影响其它位的情况下,关闭指定的位,MASK中1表示要关闭的位,0表示不变的位

flag & ~MASK

切换位:不影响其他位的情况下,切换指定的位1->0,0->1,用到按位异或运算符,MASK中1表示要切换的位,0表示不变的位

flags ^ MASK

检查位:检查指定的位是否为1,MASK中的1表示检查该位是否为1,0表示不检查

(flags & MASK) == MASK

&的优先级低于==

15.4 位字段

位字段(Bit Field)是在计算机编程中用来存储和操作数据的一种技术。它允许你将一个或多个连续的二进制位组合在一起,表示某种特定的信息或属性。位字段通常用于节省内存空间,以及在数据结构中紧凑地存储和访问多个布尔值或小范围整数。

在位字段中,每个二进制位都被分配给一个特定的含义或标志。通过位运算,你可以在一个位字段中设置、清除、读取和修改特定的位,以实现对数据的精细控制。

struct Flags {
    unsigned int flag1 : 1;
    unsigned int flag2 : 1;
    unsigned int flag3 : 1;
    unsigned int flag4 : 1;
};

但是声明的总位数不能超过unsigned int数据类型的位数

struct Flags {
    unsigned int flag1 : 1;
    unsigned int : 0; // unnamed bit-field
    unsigned int flag2 : 1;
};

未命名的位域,它的位宽为0。这实际上是一种典型的技巧,用于在结构体中引入对齐和填充。然而,这种用法在不同的编译器上可能会有不同的结果。

15.4.2 位字段和按位运算符

对于这两者,我们只需要选择其中之一就可以了,但也有联结两种方法的写法(不推荐这样做)

union Views{
	struct Flags st_flag;
    unsigned int us_flag;
};

15.5 对齐特性(c11)

在内存布局中,填充字节(padding bytes)用于对齐数据元素以满足对齐要求。填充字节的大小取决于数据元素的对齐要求和相邻数据元素之间的距离。通常情况下,填充字节的大小是使数据元素的地址满足对齐要求的最小值。

struct Example {
    char a;
    int b;
};
// 储存在内存中可能为    | a | padding | b     |

意思是,b身为一个int,假设它的对齐要求是4字节,而a作为char只要求一个字节,为了保证b的对齐padding作为一个用于对齐的填充字节可能占3字节。

_Alignas 关键字: _Alignas 关键字用于指定一个变量、结构体成员或数组的对齐要求。你可以将它放置在声明的前面,以指定所需的对齐方式。例如:(要求整个结构的对齐)

_Alignas(16) struct Example {
    char a;
    int b;
};

在这个例子中,struct Example 要求按照16字节对齐。

_Alignof 运算符: _Alignof 运算符返回给定类型的对齐要求。例如:

size_t alignment = _Alignof(struct Example);
size_t alignment2 = _Alignof(float);

alignas 编译器指令: 除了 _Alignas 关键字外,C11还引入了 _Alignas 编译器指令,用于在声明的后面指定对齐要求。例如:(要求其中的变量的对齐)

struct Example {
    char a;
    int b;
} _Alignas(16);

16.1 C语言编译

  1. 将字符映射到字符集,处理多字节字符和三字符序列以支持字符扩展
  2. 定位每个反斜杠后面跟着换行符的实例,并删除它们。将物理行转换为逻辑行。
  3. 编译器把文本划分成预处理记号序列,空白序列和注释序列。用空白字符替换注释。

16.2.1 记号

从技术角度,宏的替换体可以看做是记号型字符串,而不是字符型字符串。

16.3.1 用宏参数创建字符串

双引号内的字符串不会被变量替代。例如:

#define PSQR(x) printf("The square of X is %d.\n", ((X)*(X)));

PSQR(8);
// 输出为 "The square of X is 64.\n" 

16.3.2 预处理器粘合剂: ##运算符

与#运算符类似,##运算符可用于类函数宏的替换部分,可以把两个记号变为一个记号

#define XNAME(n) x ## n
int XNAME(1) = 10; // 会被替换为 int x1 = 10;

16.3.3 变参宏:…和_VA_ARG_

#include <stdio.h>

#define SUM(...) sum(__VA_ARGS__)

int sum(int first, ...) {
    int result = first;
    va_list args;
    va_start(args, first);
    while (1) {
        int num = va_arg(args, int);
        if (num == 0) break;
        result += num;
    }
    va_end(args);
    return result;
}

int main() {
    int total = SUM(1, 2, 3, 4, 0); // 0 表示参数结束
    printf("Total: %d\n", total); // 输出: Total: 10
    return 0;
}

16.6.3 条件编译

  1. #ifdef、#else、#endif指令
  2. #ifndef指令
  3. #if和#elif指令

16.6.4 预定义宏

  1. __LINE__:这个宏会在编译时展开为包含它的源文件中的当前行号。
  2. __FILE__:这个宏会在编译时展开为包含它的源文件的文件名。
  3. __DATE__:这个宏会在编译时展开为一个字符串,表示编译日期。
  4. __TIME__:这个宏会在编译时展开为一个字符串,表示编译时间。
  5. __STDC__:这个宏通常被定义为1,表示编译器符合C标准。

16.6.5 #line和#error

  1. #line 指令:

    • #line 指令用于更改编译器生成的行号和文件名信息,这对于调试和错误报告很有用。
    • 语法:#line linenumber "filename"
    • 不使用 #line 指令,编译器会自动处理行号和文件名信息。
    #line 100 "mycode.c"
    printf("This line will be treated as if it is on line 100 of 'mycode.c'\n");
  2. #error 指令:

    • #error 指令用于在编译时生成错误消息。当编译器遇到 #error 指令时,它将停止编译,并将指定的错误消息输出到编译器的错误消息中。
    • 语法:#error error_message
    • error_message 是您希望输出的自定义错误消息。

    例如:

    #ifdef DEBUG
    #error Debugging is not allowed in this build.
    #endif

    在上述示例中,如果在编译时定义了 DEBUG 宏,那么编译将会停止,并显示错误消息 “Debugging is not allowed in this build.”。

16.6.6 #pragma

#pragma 是一个预处理器指令,它用于向编译器发出特定于编译器或平台的指令。例如:

控制编译选项#pragma 可以用于设置特定的编译选项或优化,这些选项通常是编译器特定的。例如,您可以使用 #pragma 指令来设置编译器优化级别。

cCopy code
#pragma GCC optimize("O2")

C99还提供了_Pragma预处理运算符

16.6.7 泛型选择(C11)

C11添加了"泛型选择表达式" 在C11标准中,引入了 _Generic 关键字,用于实现泛型选择表达式。它允许根据表达式的类型来选择不同的操作。例如:

#include <stdio.h>

#define print_type(x) _Generic((x), \
    int: printf("Integer\n"), \
    double: printf("Double\n"), \
    char *: printf("String\n") \
)

int main() {
    int i = 5;
    double d = 3.14;
    char *s = "Hello";
    
    print_type(i); // 输出: Integer
    print_type(d); // 输出: Double
    print_type(s); // 输出: String
    
    return 0;
}

16.8 _Noreturn

noreturn 是一个函数属性,用于告诉编译器该函数不会返回到调用它的地方。这通常用于标识那些执行诸如终止程序、引发异常或永远不会正常返回的函数。(通常类似exit函数)

#include <stdio.h>
#include <stdlib.h>

_Noreturn void die(const char *message) {
    fprintf(stderr, "Error: %s\n", message);
    exit(EXIT_FAILURE);
}

int main() {
    die("Something went wrong");
    // 此处的代码永远不会执行,因为 die() 函数不会返回
    return 0;
}

16.10.3 tgmath.h 库(C99)

定义了泛型类型宏,会自动把sqrt转为对应的例如sqrtl等

如果不想使用宏函数,可以用括号把函数名括起来

y = sqrt(9);
y = (sqrt)(9);

16.11.1 exit() 和 atexit() 函数

exit()一般以0值返回作为正常结束,非零值表示终止失败,但是不同的系统可能有不同的值为了保证可移植性,设置了EXIT_SUCCESS的宏变量表示成功终止

atexit() 函数用于注册一个或多个函数在程序正常退出时调用(如清理内存),函数以一个函数指针作为参数,多个函数会按照注册的顺序逆序执行。

void cleanup1() {
    // 执行清理操作1
}

void cleanup2() {
    // 执行清理操作2
}

int main() {
    // 做一些工作
    // ...

    // 注册退出处理函数
    atexit(cleanup1);
    atexit(cleanup2);

    // 正常退出程序
    return 0;
}

书本p555 - 注意C和C++中的void*

两者对待void指针有不同,C++要求在void指针赋值给其他类型指针需要强制转换,C没有这样的要求。

16.12.1 assert的用法

使用assert()的好处不仅能表示文件和出问题的行号,在头文件中加入

#define NDEBUG

还能禁用assert()

16.12.2 _Static_assert (C11)

它可以导致程序无法通过编译,接受两个参数,第一个是整型常量表达式,第二个参数是一个字符串。如果第一个表达式的值为0(或_False),编译器会显示字符串,而且不编译该程序。

16.14 可变参数:stdarg.h

  1. va_listva_list 是一个类型,用于声明一个参数列表。它是一个指向参数列表的指针,通常在函数内部使用。
  2. va_startva_start 宏用于初始化 va_list,以便在函数中访问参数列表的参数。它接受两个参数,第一个参数是 va_list,第二个参数是最后一个已知的固定参数的名称。
  3. va_argva_arg 宏用于从参数列表中获取下一个参数的值。它接受两个参数,第一个参数是 va_list,第二个参数是要获取的参数的类型。
  4. va_endva_end 宏用于清理 va_list,以便在函数退出时不再访问参数列表。它接受一个参数,即 va_list
#include <stdio.h>
#include <stdarg.h>

// 一个可变参数的示例函数,计算可变数量整数的平均值
double average(int count, ...) {
    va_list args;
    va_start(args, count);

    double sum = 0.0;
    for (int i = 0; i < count; i++) {
        int num = va_arg(args, int);
        sum += num;
    }

    va_end(args);

    return sum / count;
}

int main() {
    double result = average(5, 10, 20, 30, 40, 50);
    printf("Average: %.2f\n", result);

    return 0;
}

在这个示例中,average 函数接受一个整数 count 和一系列整数参数,并计算它们的平均值。va_list 用于处理参数列表,va_start 初始化 va_listva_arg 用于逐个获取参数值,va_end 清理 va_list

对于可变参数函数,需要将省略号作为最后一个参数,并且至少要求有一个形参。