Featured image of post Learn c the hard way

Learn c the hard way

栈和堆的内存分配

网站的代码:(注释版本和原版还是分开做吧)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#define MAX_DATA 512
#define MAX_ROWS 100

struct Address {
    int id;
    int set;
    char name[MAX_DATA];
    char email[MAX_DATA];
};

struct Database {
    struct Address rows[MAX_ROWS];
};

struct Connection {
    FILE *file;
    struct Database *db;
};

void die(const char *message)
{
    if(errno) {
        perror(message);
    } else {
        printf("ERROR: %s\n", message);
    }

    exit(1);
}

void Address_print(struct Address *addr)
{
    printf("%d %s %s\n",
            addr->id, addr->name, addr->email);
}

void Database_load(struct Connection *conn)
{
    int rc = fread(conn->db, sizeof(struct Database), 1, conn->file);
    if(rc != 1) die("Failed to load database.");
}

struct Connection *Database_open(const char *filename, char mode)
{
    struct Connection *conn = malloc(sizeof(struct Connection));
    if(!conn) die("Memory error");

    conn->db = malloc(sizeof(struct Database));
    if(!conn->db) die("Memory error");

    if(mode == 'c') {
        conn->file = fopen(filename, "w");
    } else {
        conn->file = fopen(filename, "r+");

        if(conn->file) {
            Database_load(conn);
        }
    }

    if(!conn->file) die("Failed to open the file");

    return conn;
}

void Database_close(struct Connection *conn)
{
    if(conn) {
        if(conn->file) fclose(conn->file);
        if(conn->db) free(conn->db);
        free(conn);
    }
}

void Database_write(struct Connection *conn)
{
    rewind(conn->file);

    int rc = fwrite(conn->db, sizeof(struct Database), 1, conn->file);
    if(rc != 1) die("Failed to write database.");

    rc = fflush(conn->file);
    if(rc == -1) die("Cannot flush database.");
}

void Database_create(struct Connection *conn)
{
    int i = 0;

    for(i = 0; i < MAX_ROWS; i++) {
        // make a prototype to initialize it
        struct Address addr = {.id = i, .set = 0};
        // then just assign it
        conn->db->rows[i] = addr;
    }
}

void Database_set(struct Connection *conn, int id, const char *name, const char *email)
{
    struct Address *addr = &conn->db->rows[id];
    if(addr->set) die("Already set, delete it first");

    addr->set = 1;
    // WARNING: bug, read the "How To Break It" and fix this
    char *res = strncpy(addr->name, name, MAX_DATA);
    // demonstrate the strncpy bug
    if(!res) die("Name copy failed");

    res = strncpy(addr->email, email, MAX_DATA);
    if(!res) die("Email copy failed");
}

void Database_get(struct Connection *conn, int id)
{
    struct Address *addr = &conn->db->rows[id];

    if(addr->set) {
        Address_print(addr);
    } else {
        die("ID is not set");
    }
}

void Database_delete(struct Connection *conn, int id)
{
    struct Address addr = {.id = id, .set = 0};
    conn->db->rows[id] = addr;
}

void Database_list(struct Connection *conn)
{
    int i = 0;
    struct Database *db = conn->db;

    for(i = 0; i < MAX_ROWS; i++) {
        struct Address *cur = &db->rows[i];

        if(cur->set) {
            Address_print(cur);
        }
    }
}

int main(int argc, char *argv[])
{
    if(argc < 3) die("USAGE: ex17 <dbfile> <action> [action params]");

    char *filename = argv[1];
    char action = argv[2][0];
    struct Connection *conn = Database_open(filename, action);
    int id = 0;

    if(argc > 3) id = atoi(argv[3]);
    if(id >= MAX_ROWS) die("There's not that many records.");

    switch(action) {
        case 'c':
            Database_create(conn);
            Database_write(conn);
            break;

        case 'g':
            if(argc != 4) die("Need an id to get");

            Database_get(conn, id);
            break;

        case 's':
            if(argc != 6) die("Need id, name, email to set");

            Database_set(conn, id, argv[4], argv[5]);
            Database_write(conn);
            break;

        case 'd':
            if(argc != 4) die("Need id to delete");

            Database_delete(conn, id);
            Database_write(conn);
            break;

        case 'l':
            Database_list(conn);
            break;
        default:
            die("Invalid action, only: c=create, g=get, s=set, d=del, l=list");
    }

    Database_close(conn);

    return 0;
}

个人对代码的解释

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#define MAX_DATA 512
#define MAX_ROWS 100

//这是最基础的数据的结构体
struct Address {
    int id;
    //set的值变为1,说明已经创建数据,0表示没有初始化
    int set;
    char name[MAX_DATA];
    char email[MAX_DATA];
};

//结构体数组
struct Database {
    struct Address rows[MAX_ROWS];
};

//另一个结构体,用来方便访问内容,只需要函数传递一个指针就可以。
//突然意识到不只这个作用,还有释放内存。
//而且函数返回值只能有一个,FILE和结构体的数据这能选择一个,但必须同时传递两个数据,所以用结构体数组来解决
struct Connection {
    FILE *file;
    struct Database *db;
};

//这个函数就是为了在程序无法正常继续向下执行的时候进行退出,但目前没有释放内存,不完善
void die(const char *message)
{
    if(errno) {
        perror(message);
    } else {
        printf("ERROR: %s\n", message);
    }

    exit(1);
}

//打印数据
void Address_print(struct Address *addr)
{
    printf("%d %s %s\n",
            addr->id, addr->name, addr->email);
}

//从文件中加载数据
void Database_load(struct Connection *conn)
{
    int rc = fread(conn->db, sizeof(struct Database), 1, conn->file);
    //判断是否成功加载数据
    if(rc != 1) die("Failed to load database.");
}

//这个函数的作用就是打开文件,只不过要保证安全访问文件
struct Connection *Database_open(const char *filename, char mode)
{
    struct Connection *conn = malloc(sizeof(struct Connection));
    if(!conn) die("Memory error");

    conn->db = malloc(sizeof(struct Database));
    if(!conn->db) die("Memory error");

    if(mode == 'c') {
        conn->file = fopen(filename, "w");
    } else {
        conn->file = fopen(filename, "r+");

        if(conn->file) {
            Database_load(conn);
        }
    }

    //文件指针安全判断
    if(!conn->file) die("Failed to open the file");

    return conn;
}

void Database_close(struct Connection *conn)
{
    if(conn) {
        //关闭指针,释放内存
        if(conn->file) fclose(conn->file);
        if(conn->db) free(conn->db);
        free(conn);
    }
}

void Database_write(struct Connection *conn)
{
    //重置指针
    rewind(conn->file);

    int rc = fwrite(conn->db, sizeof(struct Database), 1, conn->file);
    if(rc != 1) die("Failed to write database.");

    //这里就把缓冲区的数据写入文件
    rc = fflush(conn->file);
    //判断是否正常写入数据
    if(rc == -1) die("Cannot flush database.");
}

void Database_create(struct Connection *conn)
{
    int i = 0;

    for(i = 0; i < MAX_ROWS; i++) {
        // make a prototype to initialize it
        struct Address addr = {.id = i, .set = 0};
        //这种方式可以保证出来id和set都初始化为0
        // then just assign it
        conn->db->rows[i] = addr;
    }
}

void Database_set(struct Connection *conn, int id, const char *name, const char *email)
{
    //方便后面操作,conn->db->rows[id]太长了
    struct Address *addr = &conn->db->rows[id];
    //判断是否已经创建email
    if(addr->set) die("Already set, delete it first");
    //改为1,说明已经注册了
    addr->set = 1;
    //了解这里的bug,name可能大于MAX_DATA
    // WARNING: bug, read the "How To Break It" and fix this
    char *res = strncpy(addr->name, name, MAX_DATA);
    if(res[MAX_DATA - 1] != '\0')   die("名字过长");
    // demonstrate the strncpy bug
    if(!res) die("Name copy failed");

    res = strncpy(addr->email, email, MAX_DATA);
    if(res[MAX_DATA - 1] != '\0')   die("email过长");
    if(!res) die("Email copy failed");
}

//访问某个特定ID的数据
void Database_get(struct Connection *conn, int id)
{
    struct Address *addr = &conn->db->rows[id];

    if(addr->set) {
        Address_print(addr);
    } else {
        die("ID is not set");
    }
}

void Database_delete(struct Connection *conn, int id)
{
    struct Address addr = {.id = id, .set = 0};
    //使用这种操作可以将除了id和set的数据都重置为0
    conn->db->rows[id] = addr;
}

void Database_list(struct Connection *conn)
{
    int i = 0;
    struct Database *db = conn->db;

    for(i = 0; i < MAX_ROWS; i++) {
        struct Address *cur = &db->rows[i];

        //通过set来判断数据是否存在
        if(cur->set) {
            Address_print(cur);
        }
    }
}

int main(int argc, char *argv[])
{   
    /*params是参数的意思。这里没什么,判断传递参数的数量,最少是3个,
    后面的[]里的内容是不同action会传递更多数量的参数*/
    if(argc < 3) die("USAGE: ex17 <dbfile> <action> [action params]");

    //文件名字
    char *filename = argv[1];
    //程序需要做出的行为
    char action = argv[2][0];
    //打开文件
    struct Connection *conn = Database_open(filename, action);
    int id = 0;

    //防止越界访问
    //atoi是将字符串类型的数字转化为整数类型,方便后续比较
    if(argc > 3) id = atoi(argv[3]);
    if(id >= MAX_ROWS) die("There's not that many records.");

    switch(action) {
        case 'c':
            //创建一个基础文件,说白了就是初始化一个没有任何email的文件
            Database_create(conn);
            Database_write(conn);
            break;

        case 'g':
            if(argc != 4) die("Need an id to get");

            Database_get(conn, id);
            break;

        case 's':
            if(argc != 6) die("Need id, name, email to set");

            Database_set(conn, id, argv[4], argv[5]);
            Database_write(conn);
            break;

        case 'd':
            if(argc != 4) die("Need id to delete");

            Database_delete(conn, id);
            Database_write(conn);
            break;

        case 'l':
            Database_list(conn);
            break;
        default:
            die("Invalid action, only: c=create, g=get, s=set, d=del, l=list");
    }

    Database_close(conn);

    return 0;
}

个人的问题和答案

  • sizeof(struct Database)的大小到底有多大?

    struct Address 的大小:

    idset 各 4 字节 → 8 字节

    nameemail 各 512 字节 → 1024 字节

    合计8 + 1024 = 1032 字节(通常没有填充,因为 char 数组不需要对齐到更大边界)

    struct Database 的大小:

    rows 数组包含 100 个 Address100 × 1032 = 103200 字节

  • rc = fflush(conn->file);为什么要立即刷新文件?

    fwrite 不保证立即落盘 fwrite 将数据写入 C 标准库的 用户态 I/O 缓冲区,此时数据还在内存中,并未真正到达磁盘。如果程序随后崩溃或异常退出,这些数据就会丢失。

  • 1
    2
    3
    4
    
    struct Connection {
        FILE *file;
        struct Database *db;
    };
    

    这段结构体的作用是什么,仅仅是为了函数传递参数的时候的方便吗?

    1. 资源管理:一个 Connection 对象同时持有文件句柄和数据库数据,打开时一起分配,关闭时一起释放,避免遗漏。
    2. 避免参数过多:多个函数需要同时操作文件和数据库,如果每个函数都传 (FILE*, struct Database*) 会很冗长,而传一个 Connection* 就够了。
    3. 封装与抽象:调用者不需要知道内部具体有哪两个资源,只需知道这是一个“连接”,符合模块化设计。
    4. 函数返回多个值Database_open 需要返回两个东西(文件指针和数据库结构),C 函数只能返回一个值,所以返回 Connection* 自然就解决了。
  • atoi(argv[3])的作用

    atoi(argv[3]) 的作用是将命令行传入的 字符串形式的 ID 转换成整数,并赋值给变量 id

函数指针

简单介绍

其实在C语言中,函数名本身就是一个指向函数的指针。当我们在代码中使用函数名时(不带括号),实际上我们使用的是函数的地址。那拿到地址可以干什么呢?

函数的地址相当于是函数的入口,函数指针指向它,则说明我们可以通过不断改变入口来切换函数指针的所指向的函数,简单说:函数指针就像一个“遥控器”,可以随时切换控制不同的函数。它能让我们**把“不同的行为”当作参数传给别人。**类比一下就是:

  • 实现排序的函数中可以传不同的“比较规则”。同一个 sort 函数,你可以让它按从小到大排数字,也可以按从大到小排,甚至按字符串长度排……只要你把不同的“比较方式”(函数)作为参数传进去就行。这也正是本章Lcthw所实现的。

直接举一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int add(int a, int b) {return a + b;}
int subtract(int a, int b) {return a - b;}

int main() {
    int (*operation)(int, int);// 声明一个函数指针

    operation = add;// 将函数指针指向 add 函数
    printf("加法结果:%d\n", operation(5, 3));
    
    operation = subtract;// 将函数指针指向 subtract 函数
    printf("减法结果:%d\n", operation(5, 3));
    return 0;
}

int (*operation)(int, int);简单介绍一下这个,前面的int其实就是函数返回的类型,(int ,int)是传递的参数的类型。

1
返回值类型 (*指针变量名)(参数类型列表);

之后的赋值比较好理解,由于函数名字本身就是地址,将地址赋值给指针,来完成对应的操作

如果定义的指针的类型和函数返回值的类型不匹配会出现什么情况?

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    // 声明一个返回 double 的函数指针
    double (*wrong_ptr)(int, int) = add;   // 类型不匹配!
    double result = wrong_ptr(3, 4);       // 未定义行为
    printf("%f\n", result);                // 可能输出垃圾值或崩溃
    return 0;
}

后果

  1. 返回值被错误解释 例如,实际函数返回 int,但指针被声明为返回 double。调用时,编译器会按照 double 的规则去取返回值(比如从浮点寄存器或栈中读取),但实际函数只写入了 int 所在的位置。结果你会得到一个“垃圾”值,或者程序崩溃。
  2. 栈/寄存器状态损坏 不同返回类型可能使用不同的调用约定(例如某些架构用寄存器 EAX 返回 int,用 ST(0) 返回 float)。不匹配可能导致调用者从错误的寄存器读取数据,或者破坏了后续操作。
  3. 程序崩溃(Segmentation Fault) 在更复杂的场景(如返回结构体)下,调用约定差异更大,很可能导致栈指针错乱,立即崩溃。
  4. 编译器警告 大多数编译器(如 GCC、Clang)会在你赋值或转换时发出警告,例如: warning: assignment from incompatible pointer type 绝不要忽略这个警告——它在提醒你类型不匹配。

另一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void terrible_err(void){
    printf(format:"Error:Meet a severe Error\n");
}

void bad_err(void){
    printf(format:"Error: Meet a bad ErrorIn");
}

typedef void (*err)(void);

void print_err(err e){
    e();
}

int main(){
    print_err(terrible_err);
    print_err(bad_err);
}

这个应该也容易理解,由于每次写void (*fun_ptr)(void)不太方便,所以就用了typedef来简化,fun_ptr就是函数指针

之后print_err函数需要一个函数指针,而这个函数的作用是调用这个函数的功能

回调函数

回调函数(callback),一个很生动形象的理解就是“你先告诉我等会要做什么,我到时候再叫你”。

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

也就是说上文的``terrible_errbad_err` 就是回调函数

也就是说,把一段可执行的代码像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,这就叫做回调。

接下来以我的理解解释一下:

main函数中,print_err传递了terrible_errbad_err函数的指针,之后在print_err执行了一段时间(这里没有,但实际写的代码中可能会出现)后才执行terrible_errbad_err函数的内容,这里的terrible_errbad_err就称为回调函数

调试宏

简单的宏定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 以下是两种简单的宏定义
#define PI_1 3.14        // 对象宏
#define S(a,b) a*b       // 函数宏

#include <stdio.h>

int main()
{
    printf("%d", PI_1);
    printf("%d * %d = %d", S(1,2) , 3 , S(1,2) * 3 );
    return 0;
}

这就是简单的宏定义,没什么好说的

用linux查看C语言标准库

Linux的C语言标准库放在/usr/include目录下,想要寻找一个文件的话可以使用locate指令

errno宏介绍

C 库宏 extern int errno 是通过系统调用设置的,在错误事件中的某些库函数表明了什么发生了错误。

errno 是 C 标准库中的一个宏,定义在 <errno.h> 头文件中。它用于指示在程序运行过程中发生的错误。errno 实际上是一个整数变量,用于存储错误代码。库函数在发生错误时,会设置 errno 为适当的错误代码,以便程序可以检查和处理这些错误。

使用 errno 的步骤

  1. 包含头文件:在使用 errno 之前,需要包含 <errno.h> 头文件。
  2. 调用函数:调用可能会设置 errno 的函数。
  3. 检查 errno:在函数返回表示错误时,检查 errno 以获取错误类型。
  4. 处理错误:根据 errno 的值,采取适当的错误处理措施。

具体例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <errno.h>
#include <string.h>
 
extern int errno ;
 
int main ()
{
   FILE *fp;
 
   fp = fopen("file.txt", "r");
   if( fp == NULL ) 
   {
      fprintf(stderr, "Value of errno: %d\n", errno);
      fprintf(stderr, "Error opening file: %s\n", strerror(errno));
   }
   else
   {
      fclose(fp);
   }
   
   return(0);
}

stderr stdout的区别:

stderr(标准错误流)和 stdout(标准输出流)都是输出目的地,但在设计和实践中,错误信息应该输出到 stderr,原因如下:

  1. 区分正常输出和错误信息

    • 程序正常执行的结果(如数据、计算结果)通常输出到 stdout
    • 错误、警告、调试信息应该输出到 stderr,这样两者不会混在一起。
  2. 缓冲行为不同

    • stdout 通常是行缓冲(或全缓冲),只有遇到换行符或缓冲区满才会真正输出。
    • stderr无缓冲的,错误信息会立即显示在终端上,不会因为缓冲延迟而错过关键信息。
  3. 便于重定向

    • 在命令行中,你可以将 stdout 重定向到文件(>),而 stderr 仍然显示在屏幕上,或者单独重定向(2>)。

    • 例如:

      1
      
      ./program > output.txt 2> error.txt
      

      这样正常输出和错误信息被分别保存,互不干扰。

      每个进程启动时,默认打开三个文件描述符:

      描述符 名称 默认目标
      0 stdin 键盘(终端输入)
      1 stdout 终端屏幕
      2 stderr 终端屏幕

      默认是1,所以第一个没有写数字

strerror(errno)是用来将erron的错误转化成字符串的,结果如下:

1
2
Value of errno: 2
Error opening file: No such file or directory

错误处理建议

检查返回值:在调用可能失败的函数时,始终检查其返回值。 设置 errno 为 0:在调用函数前,可以将 errno 设定为 0,以便在函数调用后检查 errno 是否被修改。 使用 strerror 和 perror:strerror 函数将 errno 转换为可读的错误消息字符串;perror 函数打印出带有描述性错误消息的 errno 值。

更详细的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main() {
    FILE *file = fopen("nonexistent_file.txt", "r");
    if (file == NULL) {
        switch (errno) {
            case EACCES:
                printf("Error: Permission denied\n");
                break;
            case ENOENT:
                printf("Error: No such file or directory\n");
                break;
            default:
                printf("Error opening file: %s\n", strerror(errno));
        }
        return 1;
    }

    // 文件处理代码
    fclose(file);
    return 0;
}

assert宏介绍

C 标准库的 assert.h头文件提供了一个名为 assert 的宏,它可用于验证程序做出的假设,并在假设为假时输出诊断消息。

assert.h 标准库主要用于在程序运行时进行断言断言是一种用于测试假设的手段,通常用于调试阶段,以便在程序出现不符合预期的状态时立即发现问题。

assert(expression)

assert 宏用于测试表达式 expression 是否为真。如果 expression 为假(即结果为 0),assert 会输出一条错误信息并终止程序的执行。这个错误信息包括以下内容:

  • 触发断言失败的表达式
  • 源文件名
  • 行号

在发布版本中,可以通过定义 NDEBUG 来禁用所有的 assert 断言。例如:

1
2
#define NDEBUG
#include <assert.h>

一旦定义了 NDEBUG,assert 宏将被预处理为一个空语句,不会有任何运行时开销。

以下是一个简单的示例,演示了 assert 的基本用法:

使用方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <assert.h>

void test_positive(int x) {
    assert(x > 0);
}

int main() {
    int a = 5;
    int b = -3;

    test_positive(a); // 这个断言通过
    test_positive(b); // 这个断言失败,程序终止

    printf("This line will not be executed if an assertion fails.\n");

    return 0;
}

断言的作用

  • 调试:在开发阶段,通过断言可以快速发现程序中的逻辑错误或假设不成立的情况。
  • 文档:断言可以作为文档的一部分,描述函数的前置条件和后置条件。
  • 防御性编程:虽然不建议在生产代码中使用断言来进行参数检查,但在某些情况下,断言可以作为最后的防线。

注意事项

  • 性能:在性能敏感的代码中,断言可能会增加额外的开销,尤其是在大量调用的情况下。因此,发布版本中通常会禁用断言。
  • 错误处理:断言不应该用于处理可以预期并且可以恢复的错误情况。断言更多地用于捕获程序员的错误。

dbg.h文件内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#ifndef __dbg_h__
#define __dbg_h__

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

#ifdef NDEBUG
#define debug(M, ...)
#else
#define debug(M, ...) fprintf(stderr, "DEBUG %s:%d: " M "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#endif

#define clean_errno() (errno == 0 ? "None" : strerror(errno))

#define log_err(M, ...) fprintf(stderr, "[ERROR] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)

#define log_warn(M, ...) fprintf(stderr, "[WARN] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)

#define log_info(M, ...) fprintf(stderr, "[INFO] (%s:%d) " M "\n", __FILE__, __LINE__, ##__VA_ARGS__)

#define check(A, M, ...) if(!(A)) { log_err(M, ##__VA_ARGS__); errno=0; goto error; }

#define sentinel(M, ...)  { log_err(M, ##__VA_ARGS__); errno=0; goto error; }

#define check_mem(A) check((A), "Out of memory.")

#define check_debug(A, M, ...) if(!(A)) { debug(M, ##__VA_ARGS__); errno=0; goto error; }

#endif

面对对象编程

object.h文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#ifndef _object_h
#define _object_h

//ifndef和endif联合使用,意思是只要_object_h没有被定义过,就执行这之间的内容,进行结构体的定义和函数申明

//这是一个枚举变量
typedef enum {
    NORTH, SOUTH, EAST, WEST
} Direction;

//object结构体,面向对象编程,这个程序的所有对象的行为在这里
typedef struct {
    char *description;
    //解释一下为什么都是void类型的指针,因为有不同的结构体指针会传过来,在函数内部将指针进行类型转换
    int (*init)(void *self);
    void (*describe)(void *self);
    void (*destroy)(void *self);
    void *(*move)(void *self, Direction direction);
    int (*attack)(void *self, int damage);
} Object;

//object.c中的函数的申明
int Object_init(void *self); //对象建立函数
void Object_destroy(void *self); //对象释放函数
void Object_describe(void *self); //对象描述函数,打印结构体中的char *description
void *Object_move(void *self, Direction direction); //对象移动函数
int Object_attack(void *self, int damage); //伤害函数
void *Object_new(size_t size, Object proto, char *description); //创建新对象的函数

//宏定义 NEW(T,N),会被转化为后面的内容,T和N相应替换
#define NEW(T, N) Object_new(sizeof(T), T##Proto, N)
#define _(N) proto.N

#endif
适合放在 .h 文件 禁止放在 .h 文件
函数声明 函数定义(非 inline
宏定义 变量定义(非 extern
类型定义(struct, union, enum) 不必要的 #include(会拖慢编译)
extern 变量声明 具体的业务逻辑代码
inline 函数定义
1
2
3
#ifndef _object_h
...
#endif

是为了防止在同一个.c源文件出现重复定义的行为:

假设你有三个头文件:

1
2
// common.h
#define BUFFER_SIZE 1024
1
2
3
// network.h
#include "common.h"
void send_data(void);
1
2
3
// file_io.h
#include "common.h"
void read_file(void);

然后你的 main.c 包含了后两个头文件:

1
2
3
4
5
// main.c
#include "network.h"
#include "file_io.h"

int main() { ... }

object.c文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "object.h"
#include <assert.h>

//相应函数的定义
void Object_destroy(void *self)
{
    //由于传递的是指向结构体的空指针,无法访问结构体内部元素,这里进行转化
    Object *obj = self;
    //释放对应对象的内存
    if(obj) {
        if(obj->description) free(obj->description);
        free(obj);
    }
}

//打印对应对象内部的描述
void Object_describe(void *self)
{
    Object *obj = self;
    printf("%s.\n", obj->description);
}

int Object_init(void *self)
{
    // do nothing really
    return 1;
}

void *Object_move(void *self, Direction direction)
{
    printf("You can't go that direction.\n");
    return NULL;
}

int Object_attack(void *self, int damage)
{
    printf("You can't attack that.\n");
    return 0;
}

void *Object_new(size_t size, Object proto, char *description)
{
    // setup the default functions in case they aren't set
    //保证所生成的对象结构体内部的函数都接上对应函数,即使这个结构体不需要,要接上这个函数内部的函数
    if(!proto.init) proto.init = Object_init;
    if(!proto.describe) proto.describe = Object_describe;
    if(!proto.destroy) proto.destroy = Object_destroy;
    if(!proto.attack) proto.attack = Object_attack;
    if(!proto.move) proto.move = Object_move;

    // this seems weird, but we can make a struct of one size,
    // then point a different pointer at it to "cast" it
    //为对应结构体申明内存
    Object *el = calloc(1, size);
    *el = proto;

    // copy the description over
    // 将这个对象的描述创建
    el->description = strdup(description);

    // initialize it with whatever init we were given
    if(!el->init(el)) {
        // looks like it didn't initialize properly
        //这里的判断条件其实是保证成功创建出对象,这里的init并不是这个文件的,而是传递过来的proto里的
        el->destroy(el);
        return NULL;
    } else {
        // all done, we made an object of any type
        return el;
    }
}

总体介绍一下这个文件:

void *Object_new(size_t size, Object proto, char *description)是关键代码,后续创建对象运行的就是这里的代码,

1
2
    Object *el = calloc(1, size);
    *el = proto;

这里有值得注意的,我们先把一段后续的创建对象的代码拿过来

1
Map *game = NEW(Map, "The Hall of the Minotaur.");

通过宏定义展开后调用了上述函数,但会发现这里传递的是Map结构体,我们把 Map结构体提前拿过来

1
2
3
4
5
struct Map {
    Object proto;
    Room *start;
    Room *location;
};

可以发现这里不是Object结构体,这里就是面对对象编程最应该学习的地方了,不难发现Map结构体中的第一个结构体就是Object结构体,它也只能放在第一个位置。

在 C 语言中,如果 Map 结构体的第一个成员是 Object,那么:

  • 指向 Map 的指针和指向 Object 的指针指向同一块内存地址
  • 标准 C 保证:指向结构体的指针可以安全地转换为指向其第一个成员的指针,反之亦然

所以说这是一个小巧思

(突然发现是自己那天弄错了,但由于存在学习的点就没删除,这里不是Map结构体,还是object结构体具体看ex19.c的代码,宏定义给Map加上了proto

其实发现Map还有一些拓展项,这里的calloc申请了内存,也进行初始化了,大小是Map 结构体的大小,但需要注意,el指针访问不了这里的拓展项

ex19.h文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#ifndef _ex19_h
#define _ex19_h

#include "object.h"


struct Monster {
    Object proto;
    int hit_points;
};

typedef struct Monster Monster;

int Monster_attack(void *self, int damage);
int Monster_init(void *self);

struct Room {
    Object proto;

    Monster *bad_guy;

    struct Room *north;
    struct Room *south;
    struct Room *east;
    struct Room *west;
};

typedef struct Room Room;

void *Room_move(void *self, Direction direction);
int Room_attack(void *self, int damage);
//int Room_init(void *self);


struct Map {
    Object proto;
    Room *start;
    Room *location;
};

typedef struct Map Map;

void *Map_move(void *self, Direction direction);
int Map_attack(void *self, int damage);
int Map_init(void *self);

#endif

ex19.c文件

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "ex19.h"


int Monster_attack(void *self, int damage)
{
    Monster *monster = self;

    printf("You attack %s!\n", monster->_(description));

    monster->hit_points -= damage;

    if(monster->hit_points > 0) {
        printf("It is still alive.\n");
        return 0;
    } else {
        printf("It is dead!\n");
        return 1;
    }
}

int Monster_init(void *self)
{
    Monster *monster = self;
    monster->hit_points = 10;
    return 1;
}

Object MonsterProto = {
    .init = Monster_init,
    .attack = Monster_attack
};


void *Room_move(void *self, Direction direction)
{
    Room *room = self;
    Room *next = NULL;

    if(direction == NORTH && room->north) {
        printf("You go north, into:\n");
        next = room->north;
    } else if(direction == SOUTH && room->south) {
        printf("You go south, into:\n");
        next = room->south;
    } else if(direction == EAST && room->east) {
        printf("You go east, into:\n");
        next = room->east;
    } else if(direction == WEST && room->west) {
        printf("You go west, into:\n");
        next = room->west;
    } else {
        printf("You can't go that direction.");
        next = NULL;
    }

    if(next) {
        next->_(describe)(next);
    }

    return next;
}


int Room_attack(void *self, int damage)
{
    Room *room = self;
    Monster *monster = room->bad_guy;

    if(monster) {
        monster->_(attack)(monster, damage);
        return 1;
    } else {
        printf("You flail in the air at nothing. Idiot.\n");
        return 0;
    }
}


Object RoomProto = {
    .move = Room_move,
    .attack = Room_attack
};


void *Map_move(void *self, Direction direction)
{
    Map *map = self;
    Room *location = map->location;
    Room *next = NULL;

    next = location->_(move)(location, direction);

    if(next) {
        map->location = next;
    }

    return next;
}

int Map_attack(void *self, int damage)
{
    Map* map = self;
    Room *location = map->location;

    return location->_(attack)(location, damage);
}


int Map_init(void *self)
{
    Map *map = self;

    // make some rooms for a small map
    //先创建房间
    Room *hall = NEW(Room, "The great Hall");
    Room *throne = NEW(Room, "The throne room");
    Room *arena = NEW(Room, "The arena, with the minotaur");
    Room *kitchen = NEW(Room, "Kitchen, you have the knife now");

    // put the bad guy in the arena
    //创建怪物
    arena->bad_guy = NEW(Monster, "The evil minotaur");

    // setup the map rooms
    //再将房间连接
    hall->north = throne;

    throne->west = arena;
    throne->east = kitchen;
    throne->south = hall;

    arena->east = throne;
    kitchen->west = throne;

    // start the map and the character off in the hall
    //设立开始房间
    map->start = hall;
    map->location = hall;

    return 1;
}

Object MapProto = {
    .init = Map_init,
    .move = Map_move,
    .attack = Map_attack
};

int process_input(Map *game)
{
    printf("\n> ");

    char ch = getchar();
    getchar(); // eat ENTER
    //避免缓冲区出现问题

    int damage = rand() % 4;

    switch(ch) {
        case -1:
            //退出游戏
            printf("Giving up? You suck.\n");
            return 0;
            break;

        case 'n':
            //移动
            game->_(move)(game, NORTH);
            break;

        case 's':
            game->_(move)(game, SOUTH);
            break;

        case 'e':
            game->_(move)(game, EAST);
            break;

        case 'w':
            game->_(move)(game, WEST);
            break;

        case 'a':
            //攻击
            game->_(attack)(game, damage);
            break;
        case 'l':
            printf("You can go:\n");
            if(game->location->north) printf("NORTH\n");
            if(game->location->south) printf("SOUTH\n");
            if(game->location->east) printf("EAST\n");
            if(game->location->west) printf("WEST\n");
            break;

        default:
            //保证程序不会出现问题
            printf("What?: %d\n", ch);
    }
    //继续循环
    return 1;
}

int main(int argc, char *argv[])
{
    // simple way to setup the randomness
    srand(time(NULL));

    // make our map to work with
    
    Map *game = NEW(Map, "The Hall of the Minotaur.");
    //这里成功创建了地图对象,内部调用了Map_init函数

    printf("You enter the ");
    game->location->_(describe)(game->location);

    //创建游戏之后运行
    while(process_input(game)) {
    }

    return 0;
}

将细节内容进行了注释,这里就大致讲一下思路:

这个游戏的对象之间是有联系的,最大的是地图,里面包含房间,房间之间又有联系,房间中有怪物

所以我们这里想要调用下一级的函数,必须也创建上一级的函数。

举一个例子:大怪物这个函数实现就必须创建地图和房间对应的函数。

个人感觉小游戏可以,但大型项目就不合适了,或许可以再多一个结构体保存游戏运行过程中的对象的结构体指针,只通过访问这个结构体就可以了

高级数据类型和控制结构

这里只记录我目前不了解和熟悉的

volatile

表示会做最坏的打算,编译器不会对它做任何优化。通常仅在对变量做一些奇怪的事情时,才会用到它。

在嵌入式系统、驱动开发或与硬件交互(如读取内存映射的寄存器、全局变量被中断服务程序修改)时,变量的值可以由外部事件改变(例如硬件信号、另一个执行线程)。如果编译器执行优化,它可能会假设变量的值在两次使用之间不会变化,从而导致程序行为错误。

使用场景

  • 读取硬件状态寄存器(如 UART 状态位)。
  • 在中断服务程序中修改的全局变量。
  • 多线程共享的变量(无锁编程中)。

例子:

在普通程序中,编译器会对变量进行激进的优化:

1
2
int flag = 0;
while (flag == 0) { }  // 等待 flag 变为非零

如果 flag 没有被声明为 volatile,且编译器没有发现当前函数会修改 flag,它可能会将 flag 的值读到寄存器中,然后无限循环,因为循环内一直比较的是寄存器的旧值,永远不会感知内存中 flag 的变化。

正确的做法是:

1
volatile int flag = 0;

这样每次循环都会从内存中读取 flag 的当前值。

变参函数

代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/** WARNING: This code is fresh and potentially isn't correct yet. */

#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include "dbg.h"

#define MAX_DATA 100

int read_string(char **out_string, int max_buffer)
{
    *out_string = calloc(1, max_buffer + 1);
    check_mem(*out_string);

    char *result = fgets(*out_string, max_buffer, stdin);
    check(result != NULL, "Input error.");

    return 0;

error:
    if(*out_string) free(*out_string);
    *out_string = NULL;
    return -1;
}

int read_int(int *out_int)
{
    char *input = NULL;
    
    int rc = read_string(&input, MAX_DATA);
    //利用另一个函数读取数据 
    check(rc == 0, "Failed to read number.");

    //由于是字符类型的数据,将其转化为int类型
    *out_int = atoi(input);

    //不要忘了释放内存,毕竟这里已经用完了
    free(input);
    return 0;

error:
    if(input) free(input);
    return -1;
}

int read_scan(const char *fmt, ...)
{
    int i = 0;
    int rc = 0;
    int *out_int = NULL;
    char *out_char = NULL;
    char **out_string = NULL;
    int max_buffer = 0;

    //变参函数部分,申明va_list变量
    va_list argp;
    //第一个参数是va_list变量,第二个是这个函数的最后一个固定参数,通过它来计算第一个可变参数
    va_start(argp, fmt);

    for(i = 0; fmt[i] != '\0'; i++) {
        if(fmt[i] == '%') {
            i++;
            switch(fmt[i]) {
                case '\0':
                    sentinel("Invalid format, you ended with %%.");
                    break;

                case 'd':
                    out_int = va_arg(argp, int *);
                    //将第一个大小为 int * 的参数传递给out_int,并将argp移动到下一个参数
                    rc = read_int(out_int);
                    check(rc == 0, "Failed to read int.");
                    break;

                case 'c':
                    out_char = va_arg(argp, char *);
                    *out_char = fgetc(stdin);
                    break;

                case 's':
                    max_buffer = va_arg(argp, int);
                    out_string = va_arg(argp, char **);
                    rc = read_string(out_string, max_buffer);
                    check(rc == 0, "Failed to read string.");
                    break;

                default:
                    sentinel("Invalid format.");
            }
        } else {
            fgetc(stdin);
        }

        check(!feof(stdin) && !ferror(stdin), "Input error.");
    }

    va_end(argp);
    return 0;

error:
    va_end(argp);
    return -1;
}



int main(int argc, char *argv[])
{
    char *first_name = NULL;
    char initial = ' ';
    char *last_name = NULL;
    int age = 0;

    printf("What's your first name? ");
    int rc = read_scan("%s", MAX_DATA, &first_name);
    //将fmt指针指向了%s,由于要改变一个一级指针,所以将二级指针传递过去
    check(rc == 0, "Failed first name.");

    printf("What's your initial? ");
    rc = read_scan("%c\n", &initial);
    check(rc == 0, "Failed initial.");

    printf("What's your last name? ");
    rc = read_scan("%s", MAX_DATA, &last_name);
    check(rc == 0, "Failed last name.");

    printf("How old are you? ");
    rc = read_scan("%d", &age);

    printf("---- RESULTS ----\n");
    printf("First Name: %s", first_name);
    printf("Initial: '%c'\n", initial);
    printf("Last Name: %s", last_name);
    printf("Age: %d\n", age);

    free(first_name);
    free(last_name);
    return 0;
error:
    return -1;
}

可变参数的介绍

需要头文件stdarg.h

类型/宏 作用
va_list 声明一个变量,用于存储参数信息。
va_start(ap, last_fixed) 初始化 va_list 变量 ap,使其指向第一个可变参数。last_fixed 是最后一个固定参数的名称。
va_arg(ap, type) 返回当前参数(假设类型为 type),并将内部指针移动到下一个参数。
va_end(ap) 清理 va_list 变量,必须与 va_start 配对。

八个防御性编程策略

一旦你接受了这一思维,你可以重新编写你的原型,并且遵循下面的八个策略,它们被我用于尽可能把代码变得可靠。当我编写代码的“实际”版本,我会严格按照下面的策略,并且尝试消除尽可能多的错误,以一些会破坏我软件的人的方式思考。

永远不要信任输入

永远不要提供的输入,并总是校验它。

避免错误

如果错误可能发生,不管可能性多低都要避免它。

过早暴露错误

过早暴露错误,并且评估发生了什么、在哪里发生以及如何修复。

记录假设

清楚地记录所有先决条件,后置条件以及不变量。

防止过多的文档

不要在实现阶段就编写文档,它们可以在代码完成时编写。

使一切自动化

使一切自动化,尤其是测试。

简单化和清晰化

永远简化你的代码,在没有牺牲安全性的同时变得最小和最整洁。

质疑权威

不要盲目遵循或拒绝规则。

这些并不是全部,仅仅是一些核心的东西,我认为程序员应该在编程可靠的代码时专注于它们。要注意我并没有真正说明如何具体做到这些,我接下来会更细致地讲解每一条,并且会布置一些覆盖它们的练习。

clang介绍

clang是和gcc一样的编译器,包含了预处理器、编译器,而且会自己调用汇编器、连接器或加载器等多种工具,而不是单单的一个编译器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
源文件(.c / .cpp)
【预处理】 -E
预处理后的文件(.i 或 .ii)  ← 可保留(-save-temps)
【编译】(前端 → IR)
      ├── 词法/语法/语义分析
      ├── 生成 LLVM IR(.ll 文本 或 .bc 二进制)
LLVM 中间表示(.ll 或 .bc)
【优化 & 代码生成】
汇编代码(.s)
【汇编】(as)
目标文件(.o 或 .obj)
【链接】(ld)
可执行文件(无扩展名 或 .exe)

这里主要了解一下编译的过程:

Licensed under CC BY-NC-SA 4.0
七弦绕明宫,知音且闻音,写作智音读作知音
使用 Hugo 构建
主题 StackJimmy 设计