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
      

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

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

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

      默认是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 配对。

va_list 是一个抽象类型,用来保存遍历变参所需的内部状态。在底层,它通常就是一个指针——指向当前正在处理的变参在栈上的位置。不同的平台/ABI 可能有不同的实现,但对使用者来说就是一个不透明的"迭代器"。

声明 va_list argp 就是在栈上分配了一个保存这个状态的小变量(通常就是 4 或 8 字节)。

八个防御性编程策略

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

永远不要信任输入

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

避免错误

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

过早暴露错误

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

记录假设

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

防止过多的文档

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

使一切自动化

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

简单化和清晰化

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

质疑权威

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

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

动态库

ctype.h 简介

#include <ctype.h> 是 C 标准库的头文件,提供字符分类和转换函数。libex29.c 中用到了其中两个:

函数作用
toupper(ch)将小写字母转为大写,非字母原样返回
tolower(ch)将大写字母转为小写,非字母原样返回

还有一些常用的:

函数作用
isalpha(ch)判断是否为字母
isdigit(ch)判断是否为数字
isalnum(ch)判断是否为字母或数字
isspace(ch)判断是否为空白字符(空格、换行、制表符等)
isupper(ch)判断是否为大写字母
islower(ch)判断是否为小写字母

libex29.c — 被加载的共享库

这是要编译成 .so 的库文件,没有 main 函数,只提供函数供别人调用。

1
2
3
#include <stdio.h>
#include <ctype.h>
#include "dbg.h"
1
2
3
4
5
int print_a_message(const char *msg)
{
    printf("A STRING: %s\n", msg);
    return 0;
}

打印字符串原样输出,返回 0 表示成功。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int uppercase(const char *msg)
{
    int i = 0;
    // BUG: \0 termination problems
    for(i = 0; msg[i] != '\0'; i++) {
        printf("%c", toupper(msg[i]));
    }
    printf("\n");
    return 0;
}

遍历字符串每个字符,用 toupper 转大写后打印。

Bug:用 msg[i] != '\0' 判断结尾,如果传入的字符串不以 \0 结尾(如二进制数据),或中间嵌入了 \0,循环行为会出错。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int lowercase(const char *msg)
{
    int i = 0;
    // BUG: \0 termination problems
    for(i = 0; msg[i] != '\0'; i++) {
        printf("%c", tolower(msg[i]));
    }
    printf("\n");
    return 0;
}

uppercase 对称,用 tolower 转小写,存在同样的 bug。

1
2
3
4
int fail_on_purpose(const char *msg)
{
    return 1;
}

故意返回 1(非零表示失败),用于演示加载器中的错误检查路径。


ex29.c — 动态加载器

这个程序有 main 函数,编译成可执行文件,运行时动态打开 .so 并调用其中的函数。

1
2
3
#include <stdio.h>
#include "dbg.h"
#include <dlfcn.h>

<dlfcn.h> 提供了 dlopendlsymdlclosedlerror 四个函数。编译时必须加 -ldl 链接 libdl 库。

1
typedef int (*lib_function)(const char *data);

定义函数指针类型 lib_function,指向接收 const char *、返回 int 的函数。libex29.c 中的四个函数都匹配这个签名。

1
2
3
4
int main(int argc, char *argv[])
{
    int rc = 0;
    check(argc == 4, "USAGE: ex29 libex29.so function data");

需要 3 个命令行参数:

  • argv[1] = 库文件路径
  • argv[2] = 要调用的函数名
  • argv[3] = 传给函数的数据
1
2
3
4
5
6
    char *lib_file = argv[1];
    char *func_to_run = argv[2];
    char *data = argv[3];

    void *lib = dlopen(lib_file, RTLD_NOW);
    check(lib != NULL, "Failed to open the library %s: %s",lib_file, dlerror());

dlopen 打开 .so 文件,RTLD_NOW 表示立即解析所有符号。失败时返回 NULL,用 dlerror() 查看错误描述。

1
2
3
    lib_function func = dlsym(lib, func_to_run);
    check(func != NULL, "Did not find %s function in the library %s: %s",
          func_to_run, lib_file, dlerror());

dlsym 从已打开的库中按名字字符串查找函数,返回函数地址。找不到返回 NULL

1
2
3
    rc = func(data);
    check(rc == 0, "Function %s return %d for data: %s",
          func_to_run, rc, data);

通过函数指针调用库中的函数。返回非 0 触发错误。

1
2
3
4
5
6
7
8
    rc = dlclose(lib);
    check(rc == 0, "Failed to close %s", lib_file);

    return 0;

error:
    return 1;
}

dlclose 关闭库句柄,释放资源。正常路径返回 0,出错路径跳到 error: 返回 1。


构建过程

编译共享库(.so):

1
2
3
4
5
6
7
8
# 第一步:编译成目标文件,需要 -fPIC 生成位置无关代码
gcc -c -fPIC libex29.c -o libex29.o

# 第二步:链接成共享库
gcc -shared -o libex29.so libex29.o

# 或一步完成:
gcc -shared -fPIC -o libex29.so libex29.c

编译加载器程序:

1
2
# 需要 -ldl 链接动态加载库
gcc -Wall -g -DNDEBUG ex29.c -ldl -o ex29

注意:-ldl 的位置在某些平台上会影响链接结果,如果报找不到符号,把 -ldl 放到源文件后面试试。

关键点:

  • -fPIC — Position Independent Code,位置无关代码。共享库加载时地址不确定,所以里面的代码必须能在任意内存地址运行。不加在某些平台上链接时直接报错。
  • -shared — 告诉 gcc 生成共享库而不是可执行文件。
  • -o libex29.so — 输出的文件名。Linux 上惯例叫 libXXX.so,但不是强制。

动态加载 API 总结

函数作用参数返回值
dlopen加载 .so(路径, 标志位)void * 句柄 / NULL
dlsym按名查符号(句柄, 名字字符串)void * 地址 / NULL
dlclose关闭库(句柄)0 成功 / 非0 失败
dlerror取错误信息(无)char * 描述 / NULL

典型使用模式

1
dlopen() -> dlerror() 检查失败 -> dlsym() -> dlerror() 检查失败 -> 调用函数 -> dlclose()

这个模式就是一个最小的 C 插件系统:.so 是插件,加载器在运行时按名字加载并调用,不需要重新编译就能换功能。

从源码到可执行文件的完整链条

四个阶段总览

1
hello.c    [预处理]    [编译]    [汇编]    [链接]    hello(可执行文件)
阶段命令输入输出文件性质
预处理gcc -E.c.i文本(头文件已展开)
编译gcc -S.i.s文本(汇编代码)
汇编gcc -c.s.o二进制(机器码,有空洞)
链接gcc.o + 库可执行文件二进制(地址全填满)

gcc 一条命令默认把四个阶段全做完,也可以用 -E-S-c 停在任意中间阶段观察产物。


第一阶段:预处理

命令:gcc -E hello.c -o hello.i

操作具体内容
头文件展开#include <stdio.h> 的内容全部复制进来
宏替换#define MAX 100 把代码里所有 MAX 替换成 100
条件编译#ifdef DEBUG ... #endif 按条件保留或删除代码段
删除注释///* */ 全部去掉
处理 #pragma编译器指令

输入 .c,输出 .i(仍然是文本文件,但比原来大很多,因为头文件被展开了)。


第二阶段:编译

命令:gcc -S hello.s -o hello.s(或 gcc -S hello.c -o hello.s 一步到汇编)

把 C 代码翻译成汇编代码。内部包含多个子步骤:

  • 词法分析 — 把源码拆成 token(关键字、标识符、运算符等)
  • 语法分析 — 检查语法结构是否合法,构建语法树
  • 语义分析 — 检查类型是否匹配、变量是否已声明等
  • 中间代码生成 — 转成与具体 CPU 无关的中间表示
  • 优化 — 常量折叠(2+3 直接合并成 5)、死代码删除、循环展开等
  • 目标代码生成 — 生成针对具体 CPU 架构(x86、ARM 等)的汇编指令

输入 .i,输出 .s(汇编文本文件,人类还能读)。

例如:

1
int x = a + b;

编译后可能变成:

1
2
3
movl    -8(%rbp), %eax
addl    -4(%rbp), %eax
movl    %eax, -12(%rbp)

第三阶段:汇编

命令:gcc -c hello.s -o hello.o(或 gcc -c hello.c -o hello.o

把汇编代码转成机器码(二进制),生成 .o 目标文件。

.o 文件(ELF 格式)的内部结构:

内容
.text机器指令
.data已初始化的全局/静态变量
.bss未初始化的全局/静态变量的占位符
.symtab符号表,记录所有函数名和全局变量名
.rel.text重定位信息,标记哪些地址"待填"

.o 文件里还有很多空洞——比如调用了 printf,但 printf 的实现不在这个 .o 里,编译器知道"这里要跳转到某个地址",但具体地址还不知道,先留空。


第四阶段:链接

命令:gcc hello.o -o hello

把多个 .o 文件和库拼成一个完整的可执行文件。链接器做的事:

  • 地址重定位 — 把所有 .o 的地址统一到一个地址空间
  • 符号解析 — 确认每个函数调用都能找到对应的定义
  • 段合并 — 把所有 .o.text 合并成一个大 .text.data 同理
  • 生成入口点 — 设置 _start 入口,让程序知道从哪里开始执行

静态链接

1
hello.o + libc.a  →  hello(自包含,约 800KB)
  • 把库函数的机器码从 .a 里复制到最终的可执行文件中
  • 填上所有未定的地址,所有函数的位置终于确定
  • 结果文件大,但移植简单,不依赖运行环境的库文件

动态链接

1
hello.o + libc.so  →  hello(依赖 .so,约 16KB)
  • 不复制代码,只记录"我需要 libc.so.6 里第 X 个位置的 printf"
  • 真正地址在程序启动时由动态链接器(/lib/ld-linux.so.2)填入
  • 结果文件极小,但运行时必须能找到对应的 .so

对应练习 29 的构建

1
2
3
4
5
6
7
8
# 汇编阶段:源码 → .o
gcc -c -fPIC libex29.c -o libex29.o

# 链接阶段:.o → .so(特殊形式的链接)
gcc -shared -o libex29.so libex29.o

# 四个阶段一步到位
gcc -Wall -g -DNDEBUG ex29.c -ldl -o ex29

最后一条命令中,gcc 内部自动执行了预处理 → 编译 → 汇编 → 链接全部四个阶段,使用者看不到中间产物,只拿到最终的可执行文件 ex29

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