24 / 07 / 08

「一生一芯」Learn C the hard way

建议使用查找来跳转到你好奇的部分。

练习 1:启用编译器

新建 helloworld.c 并写入以下代码。

int main(int argc, char *argv[]) { puts("Hello world."); return 0; }

$ make helloworld.c 报错 make: Nothing to be done for 'helloworld.c'.

解决方式: make helloworld

编译后提示:(与教程下述 $ CFLASS='-Wall' make helloworld 结果一致)

cc helloworld.c -o helloworld helloworld.c: In function ‘main’: helloworld.c:3:5: warning: implicit declaration of function ‘puts’ [-Wimplicit-function-declaration] 3 | puts("Hello world."); | ^~~~ helloworld.c:1:1: note: include ‘<stdio.h>’ or provide a declaration of ‘puts’ +++ |+#include <stdio.h> 1 | int main(int argc, char *argv[])

解决方式:添加 #include <stdio.h> 即可。

附加题 1:在你的文本编辑器中打开ex1文件,随机修改或删除一部分,之后运行它看看发生了什么。

提示 Segmentation fault

STFW:Segmentation faults in C or C++ is an error that occurs when a program attempts to access a memory location it does not have permission to access.

推测可能删除的部分与程序访问的内存地址相关,删除后程序访问了错误的地址。

附加题 2:再多打印5行文本或者其它比"Hello world."更复杂的东西。

#include <stdio.h> int main(int argc, char *argv[]) { puts("Hello world."); for(int i = 1; i < 6; i++) { for(int j = 0; j < i; j++) { printf("%%"); } printf("\n"); } return 0; }

踩坑 1:puts 自带换行。

踩坑 2:% 是转义字符,要输出需要 %%

附加题 3:执行 man 3 puts来阅读这个函数和其它函数的文档。

提供了定义的头文件、不同的 puts 类型作用、返回值、相关函数等信息。

练习 2:用Make来代替Python

创建 Makefile 并写入以下内容。

CFLAGS=-Wall -g clean: rm -f ex1

$ make clean 不能移除生成的文件

废话孩子……你写的是 helloworld 不是 ex1

make 报错 *** missing separator. Stop.

复制粘贴一时爽,空格 TAB 火葬场……让我去看看怎么把 nano 改成四空格(万能的 man 啊)。

附加题 1:创建目标all:ex1,可以以单个命令make构建ex1

CFLAGS=-Wall -g all:helloworld clean: rm -f helloworld

附加题 2:阅读man make来了解关于如何执行它的更多信息。

make 是一个读取Makefile 文件控制编译和构建程序的程序。

  1. **目标 (Target)**:要生成的文件或执行的动作。

  2. **依赖 (Dependency)**:目标所依赖的文件。

  3. **规则 (Rule)**:定义如何根据依赖生成目标。

  4. **命令 (Command)**:实现规则的具体命令。

基本结构:

target: dependencies
    command

执行 make 时,工具会根据规则检查目标和依赖的时间戳,决定是否需要重新构建目标。

附加题 3:阅读man cc来了解关于-Wall-g行为的更多信息。

  • -Wall:启用所有常见的警告,帮助发现潜在的问题和错误。它是 “Warning all”的缩写,非常有用,尤其是在开发阶段。

  • -g:生成调试信息,使生成的可执行文件包含调试符号。

附加题 4:在互联网上搜索Makefile文件,看看你是否能改进你的文件。

CFLAGS = -Wall -g #定义编译器标志。-Wall 启用所有常见的警告,-g 生成调试信息。 LDFLAGS = #定义链接器标志。此处为空,但可以用来指定库路径和库文件。 TARGET = helloworld #定义要生成的目标可执行文件名称。 SRCS = helloworld.c #定义源文件。 OBJS = $(SRCS:.c=.o) #将源文件扩展名从 .c 转换为 .o 以得到目标文件。 all: $(TARGET) #默认目标,生成目标可执行文件。 $(TARGET): $(OBJS) #定义如何链接目标文件生成可执行文件。 $(CC) $(LDFLAGS) -o $(TARGET) $(OBJS) %.o: %.c #定义通用规则,编译 .c 文件生成 .o 文件。 $(CC) $(CFLAGS) -c $< -o $@ clean: #定义清理规则,删除目标文件和可执行文件。 $(RM) $(OBJS) $(TARGET) .PHONY: all clean #声明伪目标,避免与文件名冲突。
  1. 变量使用:将源文件和目标文件定义为变量,便于管理和修改。

  2. 通用规则:使用通配符规则编译 .c 文件到 .o 文件。

  3. 依赖文件:明确定义目标和依赖关系。

  4. $(RM) 变量:使用 $(RM) 变量,更加通用和可移植。

  5. .PHONY 目标:声明伪目标,避免与文件名冲突。

附加题 5:在另一个C语言项目中找到Makefile文件,并且尝试理解它做了什么。

verilator 的 Makefile 为例(我为什么要折磨自己……):

变量部分

  • 项目版本和路径

    • VERSION, VLVERNUM:定义了 Verilator 的版本信息。

    • TOPDIR:定义了项目的顶级目录。

  • 构建工具

    • CXX, CC:定义了 C++ 和 C 编译器。

    • CXXFLAGS, CFLAGS:定义了编译器选项。

  • 依赖库和路径

    • LDFLAGS, LDLIBS:定义了链接器选项和库。

目标部分

  • 默认目标

    • all:默认目标,构建所有需要的文件。
  • 构建目标

    • bin/verilator, bin/verilator_bin_dbg 等:定义了具体可执行文件的构建规则。
  • 安装目标

    • install:用于安装 Verilator 到指定目录。
  • 清理目标

    • clean:用于清理构建过程中生成的中间文件。

    • distclean:用于清理所有生成的文件,恢复到最初状态。

练习 3:格式化输出

#include <stdio.h> int main() { int age = 10; int height = 72; printf("I am %d years old.\n", age); printf("I am %d inches tall.\n", height); return 0; }

附加题 1:找到尽可能多的方法使ex3崩溃。

为了使你的C程序 ex3 崩溃,有几个方法可以考虑,涉及改变或引入错误。以下是几种可能的方式:

  1. 使用未初始化的变量

    int main() { int age; int height; printf("I am %d years old.\n", age); printf("I am %d inches tall.\n", height); return 0; }

    这样做会使用未初始化的变量 ageheight,可能会导致程序输出垃圾值,甚至崩溃。

  2. 除以零

    int main() { int age = 10; int height = 72; int zero = 0; int crash = age / zero; // 除以零 printf("I am %d years old.\n", crash); printf("I am %d inches tall.\n", height); return 0; }
  3. 访问无效内存

    int main() { int age = 10; int height = 72; int *ptr = NULL; *ptr = age; // 访问NULL指针 printf("I am %d years old.\n", age); printf("I am %d inches tall.\n", height); return 0; }

附加题 2:执行man 3 printf来阅读其它可用的'%'格式化占位符。如果你在其它语言中使用过它们,应该看着非常熟悉(它们来源于printf)。

常见的 printf 格式化占位符:

  1. %d: 以十进制形式输出一个整数。

  2. %i: 以十进制形式输出一个整数(与 %d 类似)。

  3. %u: 以十进制形式输出一个无符号整数。

  4. %o: 以八进制形式输出一个无符号整数。

  5. %x: 以小写十六进制形式输出一个无符号整数。

  6. %X: 以大写十六进制形式输出一个无符号整数。

  7. %f: 以小数点形式输出一个浮点数。

  8. %e: 以科学计数法(小写e)形式输出一个浮点数。

  9. %E: 以科学计数法(大写E)形式输出一个浮点数。

  10. %g: 自动选择使用 %f%e,以更紧凑的形式输出浮点数。

  11. %G: 自动选择使用 %f%E,以更紧凑的形式输出浮点数。

  12. %c: 输出一个字符。

  13. %s: 输出一个字符串。

  14. %p: 输出一个指针的值。

  15. %n: 将到目前为止输出的字符数存储到对应的参数中。

  16. %%: 输出一个 % 字符。

附加题 3:将ex3添加到你的Makefileall列表中。到目前为止,可以使用make clean all来构建你所有的练习。

附加题 4:将ex3添加到你的Makefileclean列表中。当你需要的时候使用make clean可以删除它。

CFLAGS = -Wall -g #定义编译器标志。-Wall 启用所有常见的警告,-g 生成调试信息。 LDFLAGS = #定义链接器标志。此处为空,但可以用来指定库路径和库文件。 TARGETS = ex1 ex3 #定义要生成的目标可执行文件名称。 SRCS = ex1.c ex3.c #定义源文件。 OBJS = $(SRCS:.c=.o) #将源文件扩展名从 .c 转换为 .o 以得到目标文件。 all: $(TARGETS) #默认目标,生成目标可执行文件。 $(TARGETS): %: %.o #定义如何链接目标文件生成可执行文件。 $(CC) $(LDFLAGS) -o $@ $< %.o: %.c #定义通用规则,编译 .c 文件生成 .o 文件。 $(CC) $(CFLAGS) -c $< -o $@ clean: #定义清理规则,删除目标文件和可执行文件。 $(RM) $(OBJS) $(TARGETS) .PHONY: all clean #声明伪目标,避免与文件名冲突。

练习 4:Valgrind 介绍

#include <stdio.h> /* Warning: This program is wrong on purpose. */ int main() { int age = 10; int height; printf("I am %d years old.\n"); printf("I am %d inches tall.\n", height); return 0; }
$ valgrind ./ex4

附加题 1:按照上面的指导,使用Valgrind和编译器修复这个程序。

分析输出

编译器警告和 Valgrind 的输出会提示我们以下问题:

  • 使用未初始化的变量 height

  • printf 缺少格式化占位符对应的参数。

修复代码

我们需要:

  1. 初始化变量 height

  2. 修正 printf 调用中的格式化占位符。

附加题 2:在互联网上查询Valgrind相关的资料。

Valgrind 是一个强大的工具,用于调试和分析 Linux 程序,广泛用于检测内存管理和线程错误,以及进行性能分析。以下是 Valgrind 提供的一些关键功能及其优势:

主要功能和工具

  1. Memcheck:检测内存错误,例如非法内存访问、使用未初始化的内存和内存泄漏,使程序更加健壮。

  2. Cachegrind:分析缓存使用和分支预测,帮助优化程序性能。

  3. Callgrind:生成调用图,提供函数调用和执行路径的详细信息。

  4. Helgrind:识别线程错误,如数据竞争和锁顺序违规,确保代码线程安全。

  5. DRD:另一种线程错误检测工具,采用不同的分析方法,发现不同类型的线程问题。

  6. Massif:分析堆内存使用情况,帮助优化内存分配和使用。

  7. DHAT:分析堆块生命周期和利用率,提供内存效率的见解。

优势

  • 节省时间:Valgrind 可以自动检测许多手动难以发现的错误,节省开发人员的调试时间。

  • 性能分析:详细的分析有助于识别性能瓶颈,优化代码以提高速度和效率。

  • 免费开源:Valgrind 以 GNU 通用公共许可证 (GPL) 发布,用户可以自由修改和共享该工具。

  • 跨语言支持:虽然主要针对 C 和 C++ 程序,Valgrind 也适用于各种语言编写的程序,包括 Java、Perl、Python 等。

  • 易于使用:Valgrind 的工具可以在不修改或重新编译目标应用程序的情况下使用,使其易于集成到开发工作流程中。

使用场景

  • 持续测试:在自动化测试过程中运行 Valgrind 工具可以确保代码更改不会引入新错误。

  • 开发后期:在发布软件之前运行 Valgrind 可以帮助捕获潜在问题,从而发布更稳定的版本。

  • 按需调试:在怀疑存在错误或需要详细性能分析时,使用 Valgrind。

附加题 3:下载另一个程序并手动构建它。尝试一些你已经使用,但从来没有手动构建的程序。

感谢 Verilator。

附加题 4:看看Valgrind的源码是如何在目录下组织的,并且阅读它的Makefile文件。不要担心,这对我来说没有任何意义。

顶层目录结构

  • AUTHORS: 列出项目的主要贡献者。

  • COPYINGCOPYING.DOCS: 包含项目的许可协议和文档的许可协议。

  • FAQ.txt: 常见问题解答。

  • Makefile: 顶层 Makefile,定义了编译和链接整个项目的规则。

  • NEWSNEWS.old: 更新日志,记录项目的更新历史。

  • README 和其他 README 文件: 提供项目的总体介绍和特定平台的说明。

主要子目录

  • coregrind/: 包含 Valgrind 核心部分的代码。

  • VEX/: 包含 VEX(Valgrind 的翻译引擎)的代码。

  • include/: 包含公共头文件。

  • docs/: 文档目录,包含用户手册、开发者文档等。

  • tests/: 测试目录,包含各种测试用例和测试脚本。

工具和模块子目录

  • cachegrind/: Cachegrind 工具的代码,用于缓存分析。

  • callgrind/: Callgrind 工具的代码,用于调用图生成。

  • dhat/: DHAT 工具的代码,用于堆分析。

  • drd/: DRD 工具的代码,用于检测线程错误。

  • helgrind/: Helgrind 工具的代码,用于检测线程错误。

  • massif/: Massif 工具的代码,用于堆内存分析。

  • memcheck/: Memcheck 工具的代码,用于内存错误检测。

  • exp-bbv/: 实验性工具 BBV 的代码,用于生成基本块向量。

构建系统文件

  • Makefile.amMakefile.in : Automake 和 Autoconf 使用的文件,用于生成 Makefile

  • configureconfigure.ac: 配置脚本和配置脚本的输入文件,用于检测系统环境和生成 Makefile

  • config.hconfig.h.in: 配置头文件,用于定义配置选项。

  • compile, install-sh, depcomp 等: 辅助脚本,用于构建系统的各种任务。

平台支持文件

  • darwin.supp*, freebsd.supp*, solaris.supp* 等: 各平台的支持文件,定义了特定平台的补充规则和配置。

其他工具和模块

  • auxprogs/: 辅助程序目录,包含各种辅助工具。

  • gdbserver_tests/: GDB 服务器测试目录,包含与 GDB 相关的测试用例。

  • mpi/: MPI 支持文件,包含与消息传递接口相关的代码。

练习5:一个C程序的结构

#include <stdio.h> /* This is a comment. */ int main(int argc, char *argv[]) { int distance = 100; // this is also a comment printf("You are %d miles away.\n", distance); return 0; }

附加题 1:对于每一行,写出你不理解的符号,并且看看是否能猜出它们的意思。在纸上写下你的猜测,你可以在以后检查它,看看是否正确。

int argc, char *argv[]

在 C 和 C++ 编程中,int argc, char *argv[] 是标准的命令行参数,它们作为 main 函数的参数传递,用于获取从命令行传递给程序的参数。下面是它们的具体作用:

argc

  • 类型int

  • 含义:参数个数,表示命令行传递给程序的参数的数量。

  • 详情argc(argument count)包含传递给程序的参数总数,包括程序名本身。例如,如果命令行输入为 ./program arg1 arg2 arg3,则 argc 为 4。

argv

  • 类型char *argv[]char **argv

  • 含义:参数值,指向包含命令行参数的字符串数组。

  • 详情argv(argument vector)是一个指向字符指针的数组,每个元素指向一个命令行参数的字符串。argv[0] 通常是程序的名称,argv[1]argv[argc-1] 是命令行传递的其他参数。例如,如果命令行输入为 ./program arg1 arg2 arg3,则:

    • argv[0]"./program"

    • argv[1]"arg1"

    • argv[2]"arg2"

    • argv[3]"arg3"

示例

以下是一个示例程序,演示如何使用 argcargv 打印传递给程序的命令行参数:

#include <stdio.h> int main(int argc, char *argv[]) { printf("Number of arguments: %d\n", argc); for (int i = 0; i < argc; i++) { printf("Argument %d: %s\n", i, argv[i]); } return 0; }

运行此程序,如果命令行输入为 ./program arg1 arg2 arg3,输出将是:

Number of arguments: 4 Argument 0: ./program Argument 1: arg1 Argument 2: arg2 Argument 3: arg3

应用场景

  • 参数解析:通过 argcargv 解析命令行参数,根据输入的参数执行不同的操作。

  • 动态输入:允许程序接受动态输入,使得程序更灵活和通用。

  • 脚本化:在脚本和批处理操作中,使用命令行参数传递输入数据或配置选项。

附加题 2:回头去看之前几个练习的源代码,并且像这样分解代码,来看看你是否了解它们。写下你不了解和不能自己解释的东西。

谢天谢地前四个不算太难。

练习 6:变量类型

include <stdio.h> int main(int argc, char *argv[]) { int distance = 100; float power = 2.345f; double super_power = 56789.4532; char initial = 'A'; char first_name[] = "Zed"; char last_name[] = "Shaw"; printf("You are %d miles away.\n", distance); printf("You have %f levels of power.\n", power); printf("You have %f awesome super powers.\n", super_power); printf("I have an initial %c.\n", initial); printf("I have a first name %s.\n", first_name); printf("I have a last name %s.\n", last_name); printf("My whole name is %s %c. %s.\n", first_name, initial, last_name); return 0; }

附加题 1:寻找其他通过修改printf使这段C代码崩溃的方法。

#include <stdio.h> int main(int argc, char *argv[]) { int distance = 100; float power = 2.345f; double super_power = 56789.4532; char initial = 'A'; char first_name[] = "Zed"; char last_name[] = "Shaw"; // Incorrect format specifiers: printf("You are %f miles away.\n", distance); // should be %d printf("You have %d levels of power.\n", power); // should be %f printf("You have %s awesome super powers.\n", super_power); // should be %f printf("I have an initial %s.\n", initial); // should be %c printf("I have a first name %d.\n", first_name); // should be %s printf("I have a last name %f.\n", last_name); // should be %s printf("My whole name is %s %d. %c.\n", first_name, initial, last_name); // %d and %c should be swapped return 0; }

附加题 2:搜索“printf格式化”,试着使用一些高级的占位符。

看看练习 3 呢。

附加题 3:研究可以用几种方法打印数字。尝试以八进制或十六进制打印,或者其它你找到的方法。

#include <stdio.h> int main() { int number = 255; float power = 123.456f; // 打印整数 printf("Decimal: %d\n", number); printf("Octal: %o\n", number); printf("Hexadecimal (lowercase): %x\n", number); printf("Hexadecimal (uppercase): %X\n", number); printf("Unsigned Decimal: %u\n", number); // 打印浮点数 printf("Default format: %f\n", power); printf("Scientific notation: %e\n", power); printf("Shortest representation: %g\n", power); // 打印带有精度控制的浮点数 printf("Two decimal places: %.2f\n", power); printf("Four decimal places: %.4f\n", power); printf("Scientific notation with two significant figures: %.2e\n", power); printf("Shortest representation with four significant figures: %.4g\n", power); return 0; }

附加题 4:试着打印空字符串,即""。

#include <stdio.h> int main() { // 打印空字符串 printf("This is an empty string: \"%s\"\n", ""); // 打印空字符串在其他字符串的上下文中 char empty_string[] = ""; printf("Here is another empty string: \"%s\"\n", empty_string); return 0; }

练习 7:更多的变量和一些算术

int main(int argc, char *argv[]) { int bugs = 100; double bug_rate = 1.2; printf("You have %d bugs at the imaginary rate of %f.\n", bugs, bug_rate); long universe_of_defects = 1L * 1024L * 1024L * 1024L; printf("The entire universe has %ld bugs.\n", universe_of_defects); double expected_bugs = bugs * bug_rate; printf("You are expected to have %f bugs.\n", expected_bugs); double part_of_universe = expected_bugs / universe_of_defects; printf("That is only a %e portion of the universe.\n", part_of_universe); // this makes no sense, just a demo of something weird char nul_byte = '\0'; int care_percentage = bugs * nul_byte; printf("Which means you should care %d%%.\n", care_percentage); return 0; }

附加题 1:把为universe_of_defects赋值的数改为不同的大小,观察编译器的警告。

ex7.c: In function ‘main’: ex7.c:10:78: warning: integer overflow in expression of type ‘long int’ results in ‘0’ [-Woverflow] 10 | = 1024L * 1024L * 1024L * 1024L * 1024L * 1024L * 1024L * 1024L * 1024L; | ^ cc -o ex7 ex7.o

在 C/C++ 中,long 的大小和范围依赖于编译器和操作系统。通常情况下:

  • 在 32 位系统中:long 通常是 32 位,范围为 -2,147,483,648 到 2,147,483,647。

  • 在 64 位系统中:long 通常是 64 位,范围为 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。

附加题 2:这些巨大的数字实际上打印成了什么?

The entire universe has 0 bugs.

打印成了 0,BTW,使用 Valgrind 并没有错误提示。

附加题 3:将long改为unsigned long,并试着找到对它来说太大的数字。

ex7.c: In function ‘main’: ex7.c:10:41: warning: integer constant is so large that it is unsigned 10 | unsigned long universe_of_defects = 18446744073709551615; | ^~~~~~~~~~~~~~~~~~~~ cc -o ex7 ex7.o

在 C/C++ 中,unsigned long 的大小和范围也依赖于编译器和操作系统。通常情况下:

  • 在 32 位系统中:unsigned long 通常是 32 位,范围为 0 到 4,294,967,295。

  • 在 64 位系统中:unsigned long 通常是 64 位,范围为 0 到 18,446,744,073,709,551,615。

哎呀,去掉符号位自然就是两倍啦。

附加题 4:上网搜索unsigned做了什么。

诶呀,表明了是无符号数,可以多一位用来储存数的大小啦。

附加题 5:试着自己解释(在下个练习之前)为什么char可以和int相乘。

char 类型被存储为整型,编译器可以自动处理隐式转换。

练习 8:大小和数组

#include <stdio.h> int main(int argc, char *argv[]) { int areas[] = {10, 12, 13, 14, 20}; char name[] = "Zed"; char full_name[] = { 'Z', 'e', 'd', ' ', 'A', '.', ' ', 'S', 'h', 'a', 'w', '\0' }; // WARNING: On some systems you may have to change the // %ld in this code to a %u since it will use unsigned ints printf("The size of an int: %ld\n", sizeof(int)); printf("The size of areas (int[]): %ld\n", sizeof(areas)); printf("The number of ints in areas: %ld\n", sizeof(areas) / sizeof(int)); printf("The first area is %d, the 2nd %d.\n", areas[0], areas[1]); printf("The size of a char: %ld\n", sizeof(char)); printf("The size of name (char[]): %ld\n", sizeof(name)); printf("The number of chars: %ld\n", sizeof(name) / sizeof(char)); printf("The size of full_name (char[]): %ld\n", sizeof(full_name)); printf("The number of chars: %ld\n", sizeof(full_name) / sizeof(char)); printf("name=\"%s\" and full_name=\"%s\"\n", name, full_name); return 0; }

full_name最后的'\0'去掉,并重新运行它,在valgrind下再运行一遍。现在将full_name的定义从main函数中移到它的上面,尝试在Valgrind下运行它来看看是否能得到一些新的错误。有些情况下,你会足够幸运,不会得到任何错误。

去掉 '\0':无报错;移动至 main 上:无报错。

areas[0]改为areas[10]并打印,来看看Valgrind会输出什么。

The first area is 76098960, the 2nd 12. Valgrind 无报错。

尝试上述操作的不同变式,也对namefull_name执行一遍。

同上。

附加题 1:尝试使用areas[0] = 100;以及相似的操作对areas的元素赋值。

可以正常赋值。

附加题 2:尝试对namefull_name的元素赋值。

make ex8 cc -Wall -g -c ex8.c -o ex8.o ex8.c: In function ‘main’: ex8.c:16:14: error: assignment to expression with array type 16 | name = "A"; | ^ make: *** [Makefile:13: ex8.o] Error 1

尝试使用 strcpy 函数,需补充string.h并避免越界。

ex8.c: In function ‘main’: ex8.c:16:9: warning: implicit declaration of function ‘strcpy’ [-Wimplicit-function-declaration] 16 | strcpy(name, "Qidi"); | ^~~~~~ ex8.c:2:1: note: include ‘<string.h>’ or provide a declaration of ‘strcpy’ 1 | #include <stdio.h> +++ |+#include <string.h> 2 | ex8.c:16:9: warning: incompatible implicit declaration of built-in function ‘strcpy’ [-Wbuiltin-declaration-mismatch] 16 | strcpy(name, "Qidi"); | ^~~~~~ ex8.c:16:9: note: include ‘<string.h>’ or provide a declaration of ‘strcpy’ ex8.c:16:9: warning: ‘__builtin_memcpy’ writing 5 bytes into a region of size 4 overflows the destination [-Wstringop-overflow=] 16 | strcpy(name, "Qidi"); | ^~~~~~~~~~~~~~~~~~~~ ex8.c:12:10: note: destination object ‘name’ of size 4 12 | char name[] = "Zed"; | ^~~~ cc -o ex8 ex8.o

附加题 3:尝试将areas的一个元素赋值为name中的字符。

areas[0] = name[0]The first area is 90, the 2nd 12. 以 ASCII 码赋值。

附加题 4:上网搜索在不同的CPU上整数所占的不同大小。

  • x86 架构:通常采用 ILP32 数据模型,即 int、long 和指针都是 32 位。

  • x86-64 或 AMD64 架构:通常采用 LP64 数据模型,即 int 是 32 位,而 long 和指针是 64 位。

练习 9:数组和字符串

#include <stdio.h> int main(int argc, char *argv[]) { int numbers[4] = {0}; char name[4] = {'a'}; // first, print them out raw printf("numbers: %d %d %d %d\n", numbers[0], numbers[1], numbers[2], numbers[3]); printf("name each: %c %c %c %c\n", name[0], name[1], name[2], name[3]); printf("name: %s\n", name); // setup the numbers numbers[0] = 1; numbers[1] = 2; numbers[2] = 3; numbers[3] = 4; // setup the name name[0] = 'Z'; name[1] = 'e'; name[2] = 'd'; name[3] = '\0'; // then print them out initialized printf("numbers: %d %d %d %d\n", numbers[0], numbers[1], numbers[2], numbers[3]); printf("name each: %c %c %c %c\n", name[0], name[1], name[2], name[3]); // print the name like a string printf("name: %s\n", name); // another way to use name char *another = "Zed"; printf("another: %s\n", another); printf("another each: %c %c %c %c\n", another[0], another[1], another[2], another[3]); return 0; }

将一些字符赋给numbers的元素,之后用printf一次打印一个字符,你会得到什么编译器警告?

#include <stdio.h> int main(int argc, char *argv[]){ int n[4] = {0}; printf("%d\n", n[0]); printf("%c\n", n[0]); n[0] = "a"; printf("%d\n", n[0]); printf("%c\n", n[0]); return 0; }
make ex9_1 cc -Wall -g -c ex9_1.c -o ex9_1.o ex9_1.c: In function ‘main’: ex9_1.c:10:14: warning: assignment to ‘int’ from ‘char *’ makes integer from pointer without a cast [-Wint-conversion] 10 | n[0] = "a"; | ^ cc -o ex9_1 ex9_1.o ⮞ ./ex9_1 0 444567564

names执行上述的相反操作,把names当成int数组,并一次打印一个intValgrind会提示什么?

⮞ valgrind ./ex9_2 ==201320== Memcheck, a memory error detector ==201320== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward e t al. ==201320== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyri ght info ==201320== Command: ./ex9_2 ==201320== 97 a 5 ==201320== ==201320== HEAP SUMMARY: ==201320== in use at exit: 0 bytes in 0 blocks ==201320== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated ==201320== ==201320== All heap blocks were freed -- no leaks are possible ==201320== ==201320== For lists of detected and suppressed errors, rerun with: -s ==201320== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

无事发生……我不理解。

GPT:可能是因为 char 类型的元素虽然只有 1 字节,但在 printf 函数中,它们被自动提升为 int 类型。这种类型提升(integer promotion)在 C 标准中是合法的,并不会导致未定义行为。因此,Valgrind 并没有检测到内存访问错误。

有多少种其它的方式可以用来打印它?

  • 逐个字符打印

    • 使用 printf 打印各个元素

    • 使用 putchar 函数逐个打印字符

    • 使用指针遍历数组并打印

  • 作为字符串打印

    • 使用 printf%s 格式说明符

    • 使用 puts 函数

如果一个字符数组占四个字节,一个整数也占4个字节,你可以像整数一样使用整个name吗?你如何用黑魔法实现它?

  1. 使用联合体(union)

  2. 使用类型转换和指针

  3. 使用 memcpy 函数

拿出一张纸,将每个数组画成一排方框,之后在纸上画出代码中的操作,看看是否正确。

name转换成another的形式,看看代码是否能正常工作。

没看懂题目要求……将数组形式转化为指针吗?数组名即是地址。

练习 10:字符串数组和循环

#include <stdio.h> int main(int argc, char *argv[]) { int i = 0; // go through each string in argv // why am I skipping argv[0]? for(i = 1; i < argc; i++) { printf("arg %d: %s\n", i, argv[i]); } // let's make our own array of strings char *states[] = { "California", "Oregon", "Washington", "Texas" }; int num_states = 4; for(i = 0; i < num_states; i++) { printf("state %d: %s\n", i, states[i]); } return 0; }
make ex10 cc -Wall -g -c ex10.c -o ex10.o cc -o ex10 ex10.o ⮞ ./ex10 aaaa bbbb cccc dddd arg 1: aaaa arg 2: bbbb arg 3: cccc arg 4: dddd state 0: California state 1: Oregon state 2: Washington state 3: Texas

i初始化为0看看会发生什么。是否也需要改动argc,不改动的话它能正常工作吗?为什么下标从0开始可以正常工作?

⮞ ./ex10 a b c d arg 0: ./ex10 arg 1: a arg 2: b arg 3: c arg 4: d state 0: California state 1: Oregon state 2: Washington state 3: Texas

argc 的第一项是 ./ex10 本身哈哈哈哈哈。

num_states改为错误的值使它变大,来看看会发生什么。

⮞ ./ex10 a b c d arg 0: ./ex10 arg 1: a arg 2: b arg 3: c arg 4: d state 0: California state 1: Oregon state 2: Washington state 3: Texas Segmentation fault state 4: (null) ==5228== Invalid read of size 1 ==5228== at 0x4851E56: strlen (vg_replace_strmem.c:505) ==5228== by 0x48DFD30: __vfprintf_internal (vfprintf-internal.c:1517) ==5228== by 0x48C979E: printf (printf.c:33) ==5228== by 0x109235: main (ex10.c:21) ==5228== Address 0x9579af4c8feb2d00 is not stack'd, malloc'd or (recently) free'd ==5228== ==5228== ==5228== Process terminating with default action of signal 11 (SIGSEGV) ==5228== General Protection Fault ==5228== at 0x4851E56: strlen (vg_replace_strmem.c:505) ==5228== by 0x48DFD30: __vfprintf_internal (vfprintf-internal.c:1517) ==5228== by 0x48C979E: printf (printf.c:33) ==5228== by 0x109235: main (ex10.c:21) ==5228== ==5228== HEAP SUMMARY: ==5228== in use at exit: 1,024 bytes in 1 blocks ==5228== total heap usage: 1 allocs, 0 frees, 1,024 bytes allocated

不要越界喔。

附加题 1:弄清楚在for循环的每一部分你都可以放置什么样的代码。

附加题 2:查询如何使用','(逗号)字符来在for循环的每一部分中,';'(分号)之间分隔多条语句。

在 C 语言中,for 循环具有以下语法结构:

for (initialization; condition1, condition2; increment) { // Loop body }

在这个结构中:

  1. **初始化部分 (initialization)**:在循环开始前执行一次,通常用于声明和初始化控制变量。

  2. **条件部分 (condition)**:在每次迭代前进行检查,如果条件为真,继续执行循环体;如果条件为假,退出循环。

  3. **增量部分 (increment)**:在每次循环体执行后执行一次,用于更新控制变量。

  4. **循环体 (loop body)**:在每次迭代中执行的代码块。

查询NULL是什么东西,尝试将它用做states的一个元素,看看它会打印出什么。

在 C 语言中,NULL 是一个宏,它通常定义为一个指向地址为 0 的指针。它表示一个空指针,即不指向任何对象或函数的指针。使用 NULL 可以避免指针指向未初始化或无效的地址,减少了程序出错的风险。

char *states[] = { "California", "Oregon", "Washington", NULL, "Texas" }; int num_states = 5; for(i = 0; i < num_states; i++) { printf("state %d: %s\n", i, states[i]); }
⮞ ./ex10 arg 0: ./ex10 state 0: California state 1: Oregon state 2: Washington state 3: (null) state 4: Texas

看看你是否能在打印之前将states的一个元素赋值给argv中的元素,再试试相反的操作。

有点不理解题目的表述,如果直接在打印 argv 前将 states 赋值给它,那么必然会报错,因为此时 states 还没有被申明。如果将申明移动到打印前,尝试如下:

⮞ ./ex10 state 0: California state 1: Oregon state 2: Washington state 3: Texas ⮞ ./ex10 a b c d arg 1: California arg 2: b arg 3: c arg 4: d state 0: California state 1: Oregon state 2: Washington state 3: Texas

相反操作如下:

⮞ make ex10 cc -Wall -g -c ex10.c -o ex10.o cc -o ex10 ex10.o ./ex10 state 0: California state 1: (null) state 2: Washington state 3: Texas ./ex10 aa bb cc arg 1: aa arg 2: bb arg 3: cc state 0: California state 1: aa state 2: Washington state 3: Texas

练习 11:While 循环和布尔表达式

#include <stdio.h> int main(int argc, char *argv[]) { // go through each string in argv int i = 0; while(i < argc) { printf("arg %d: %s\n", i, argv[i]); i++; } // let's make our own array of strings char *states[] = { "California", "Oregon", "Washington", "Texas" }; int num_states = 4; i = 0; // watch for this while(i < num_states) { printf("state %d: %s\n", i, states[i]); i++; } return 0; }

让这些循环倒序执行,通过使用i--argc开始递减直到0。你可能需要做一些算数操作让数组的下标正常工作。

int i = argc - 1; while(i >= 0) { printf("arg %d: %s\n", i, argv[i]); i--; } // let's make our own array of strings char *states[] = { "California", "Oregon", "Washington", "Texas" }; int num_states = 4; i = num_states - 1; // watch for this while(i >= 0) { printf("state %d: %s\n", i, states[i]); i--;

使用while循环将argv中的值复制到states

i = 0; while(i < num_states) { states[i] = argv[i]; i++; }

让这个复制循环不会执行失败,即使argv之中有很多元素也不会全部放进states

num_states 为界可以有效避免越界访问 states,但是需要添加对 argv 的限制来避免不属于输入的内容被写入 states (例如你的 PATH 变量)。以下是不加限制的结果:

⮞ ./ex11_2 aa arg 0: ./ex11_2 arg 1: aa state 0: California state 1: Oregon state 2: Washington state 3: Texas state 0: ./ex11_2 state 1: aa state 2: (null) state 3: SHELL=/bin/bash

添加限制的复制

i = 0; while(i < num_states) { if(i > argc) states[i]=NULL; else states[i] = argv[i]; i++; }

诶呀 用 if 好像超纲了,但无所谓了……

⮞ ./ex11_2 aa arg 0: ./ex11_2 arg 1: aa state 0: California state 1: Oregon state 2: Washington state 3: Texas state 0: ./ex11_2 state 1: aa state 2: (null) state 3: (null

研究你是否真正复制了这些字符串。答案可能会让你感到意外和困惑。

states[i] = argv[i]; 只是将 argv[i] 中的字符串指针赋值给 states[i],两者指向的是同一个内存地址,并没有真正复制字符串。

练习 11:If,Else If,Else

#include <stdio.h> int main(int argc, char *argv[]) { int i = 0; if(argc == 1) { printf("You only have one argument. You suck.\n"); } else if(argc > 1 && argc < 4) { printf("Here's your arguments:\n"); for(i = 0; i < argc; i++) { printf("%s ", argv[i]); } printf("\n"); } else { printf("You have too many arguments. You suck.\n"); } return 0; }
  • &&改为||,于是你会把“与”操作变成“或”操作,并且看看会发生什么。

无论输入多少个参数都会被骂……

我已经向你简短地介绍了&&,它执行“与”操作。上网搜索与之不同的“布尔运算符”。

  • 逻辑与(&&):如果两个操作数都为真,则结果为真。

  • 逻辑或(||):如果两个操作数中至少有一个为真,则结果为真。

  • 逻辑非(!):将操作数的布尔值取反,即真变假,假变真。

为这个程序编写更多的测试用例,看看你会写出什么。

⮞ ./ex12 You only have one argument. You suck. ⮞ ./ex12 Hello Here's your arguments: ./ex12 Hello ⮞ ./ex12 Hello World Here's your arguments: ./ex12 Hello World ⮞ ./ex12 Hello Linux World You have too many arguments. You suck.

回到练习10和11,使用if语句使循环提前退出。你需要break语句来实现它,搜索它的有关资料。

修复了练习 11 中输入参数不足 state 会写入 (null) 的问题,现在没有输入就保持原样了。

i = 0; while(i < num_states) { if(i + 1 >= argc) break; else states[i] = argv[i+1]; i++; }
⮞ ./ex11_2 ! arg 0: ./ex11_2 arg 1: ! state 0: California state 1: Oregon state 2: Washington state 3: Texas state 0: ! state 1: Oregon state 2: Washington state 3: Texas

第一个判断所输出的话真的正确吗?由于你的“第一个参数”不是用户输入的第一个参数,把它改正。

#include <stdio.h> int main(int argc, char *argv[]) { int i = 0; if(argc == 1) { printf("You did not have one argument. You suck.\n"); } else if(argc > 1 && argc < 4) { printf("Here's your arguments:\n"); for(i = 1; i < argc; i++) { printf("%s ", argv[i]); } printf("\n"); } else { printf("You have too many arguments. You suck.\n"); } return 0; }
⮞ ./ex12 You did not have one argument. You suck. ⮞ ./ex12 1 Here's your arguments: 1 ⮞ ./ex12 1 2 Here's your arguments: 1 2 ⮞ ./ex12 1 2 3 You have too many arguments. You suck.

练习 13:Switch语句

#include <stdio.h> int main(int argc, char *argv[]) { if(argc != 2) { printf("ERROR: You need one argument.\n"); // this is how you abort a program return 1; } int i = 0; for(i = 0; argv[1][i] != '\0'; i++) { char letter = argv[1][i]; switch(letter) { case 'a': case 'A': printf("%d: 'A'\n", i); break; case 'e': case 'E': printf("%d: 'E'\n", i); break; case 'i': case 'I': printf("%d: 'I'\n", i); break; case 'o': case 'O': printf("%d: 'O'\n", i); break; case 'u': case 'U': printf("%d: 'U'\n", i); break; case 'y': case 'Y': if(i > 2) { // it's only sometimes Y printf("%d: 'Y'\n", i); } break; default: printf("%d: %c is not a vowel\n", i, letter); } } return 0; }

Bug:在前三个字母的 Y 不会被输出。(与附加题 3 相关)

编写另一个程序,在字母上做算术运算将它们转换为小写,并且在switch中移除所有额外的大写字母。

#include <stdio.h> int main(int argc, char *argv[]) { if(argc != 2) { printf("ERROR: You need one argument.\n"); return 1; } int i = 0; for(i = 0; argv[1][i] != '\0'; i++) { char letter = argv[1][i]; if(letter >= 'A' && letter <= 'Z') letter = letter - ('A' - 'a'); printf("%c",letter); } printf("\n"); return 0; }

为什么需要用到 switch 呢……

使用','(逗号)在for循环中初始化letter

int i = 0; char letter; for(i = 0, letter = argv[1][i]; argv[1][i] != '\0'; i++, letter = argv[1][i]) { ... }

使用另一个for循环来让它处理你传入的所有命令行参数。

int i = 0, j = 0; char letter; for(j = 1; j < argc; j++) { printf("\n"); for(i = 0, letter = argv[j][i]; argv[j][i] != '\0'; i++, letter = argv[j][i]) { ... } }
⮞ ./ex13_3 Hello World. YYYYYYY 0: H is not a vowel 1: 'E' 2: l is not a vowel 3: l is not a vowel 4: 'O' 0: W is not a vowel 1: 'O' 2: r is not a vowel 3: l is not a vowel 4: d is not a vowel 5: . is not a vowel 0: Y is not a vowel 1: Y is not a vowel 2: Y is not a vowel 3: 'Y' 4: 'Y' 5: 'Y' 6: 'Y'

将这个switch语句转为if语句,你更喜欢哪个呢?

#include <stdio.h> int main(int argc, char *argv[]) { if(argc != 2) { printf("ERROR: You need one argument.\n"); // this is how you abort a program return 1; } int i = 0; for(i = 0; argv[1][i] != '\0'; i++) { char letter = argv[1][i]; if (letter == 'a' || letter == 'A') { printf("%d: 'A'\n", i); } else if (letter == 'e' || letter == 'E') { printf("%d: 'E'\n", i); } else if (letter == 'i' || letter == 'I') { printf("%d: 'I'\n", i); } else if (letter == 'o' || letter == 'O') { printf("%d: 'O'\n", i); } else if (letter == 'u' || letter == 'U') { printf("%d: 'U'\n", i); } else if (letter == 'y' || letter == 'Y') { if(i > 2) { // it's only sometimes Y printf("%d: 'Y'\n", i); } } else { printf("%d: %c is not a vowel\n", i, letter); } } return 0; }

在“Y”的例子中,我在if代码块外面写了个break。这样会产生什么效果?如果把它移进if代码块,会发生什么?自己试着解答它,并证明你是正确的。

break 在外会导致 y 如果出现在前三个字母则不会被输出,如果移入代码块,才能正确输出位于前三个字母的 y。

练习 14:编写并使用函数

#include <stdio.h> #include <ctype.h> // forward declarations int can_print_it(char ch); void print_letters(char arg[]); void print_arguments(int argc, char *argv[]) { int i = 0; for(i = 0; i < argc; i++) { print_letters(argv[i]); } } void print_letters(char arg[]) { int i = 0; for(i = 0; arg[i] != '\0'; i++) { char ch = arg[i]; if(can_print_it(ch)) { printf("'%c' == %d ", ch, ch); } } printf("\n"); } int can_print_it(char ch) { return isalpha(ch) || isblank(ch); } int main(int argc, char *argv[]) { print_arguments(argc, argv); return 0; }

重新编写这些函数,使它们的数量减少。比如,你真的需要can_print_it吗?

#include <stdio.h> #include <ctype.h> void print_arguments(int argc, char *argv[]) { int i = 0; int j = 0; for(i = 1; i < argc; i++) { for(j = 0; argv[i][j] != '\0'; j++) { char ch = argv[i][j]; if(isalpha(ch) || isblank(ch)) { printf("'%c' == %d; ", ch, ch); } } printf("\n"); } } int main(int argc, char *argv[]) { print_arguments(argc, argv); return 0; }

使用strlen函数,让print_arguments知道每个字符串参数都有多长,之后将长度传入print_letters。然后重写print_letters,让它只处理固定的长度,不按照'\0'终止符。你需要#include <string.h>来实现它。

void print_arguments(int argc, char *argv[]) { int i = 0; int j = 0; for(i = 1; i < argc; i++) { printf("%lu",strlen(argv[i])); for(j = 0; j < strlen(argv[i]); j++) { char ch = argv[i][j]; if(isalpha(ch) || isblank(ch)) { printf("'%c' == %d; ", ch, ch); } } printf("\n"); } } int main(int argc, char *argv[]) { print_arguments(argc, argv); return 0; }

使用man来查询isalphaisblank的信息。使用其它相似的函数来只打印出数字或者其它字符。

C 标准库中还有其他一些类似的函数,用来判断字符类型。以下是一些相关的函数:

  • isdigit:检查字符是否为数字。

  • isalnum:检查字符是否为字母或数字。

  • isspace:检查字符是否为空白字符(空格、换行、制表符等)。

  • ispunct:检查字符是否为标点符号。

#include <stdio.h> #include <ctype.h> #include <string.h> void print_arguments(int argc, char *argv[]) { int i = 0; int j = 0; for(i = 1; i < argc; i++) { printf("%lu: ",strlen(argv[i])); for(j = 0; j < strlen(argv[i]); j++) { char ch = argv[i][j]; if(isalnum(ch) || isspace(ch) || ispunct(ch)) { printf("'%c' == %d;\t", ch, ch); } } printf("\n"); } } int main(int argc, char *argv[]) { print_arguments(argc, argv); return 0; }

上网浏览不同的人喜欢什么样的函数格式。永远不要使用“K&R”语法,因为它过时了,而且容易使人混乱,但是当你碰到一些人使用这种格式时,要理解代码做了什么。

Google:

  1. 文件组织

    • 每个文件只包含一个独立的概念。

    • 使用头文件保护(#ifndef, #define, #endif)防止重复包含。

  2. 命名规范

    • 变量和函数名使用小驼峰命名法(如 myVariable)。

    • 全局变量和常量使用全大写字母和下划线(如 GLOBAL_CONSTANT)。

  3. 缩进和空白

    • 使用2个空格缩进,不使用制表符。

    • 每个函数定义和实现之间留一个空行。

  4. 注释

    • 使用双斜杠(//)注释单行,使用斜杠星号(/* ... */)注释多行。

    • 注释应简明扼要,解释代码逻辑而不是代码本身。

  5. 函数和变量

    • 函数应尽量短小,每个函数只做一件事。

    • 避免使用全局变量,使用局部变量和函数参数。

  6. 常量

    • 使用const关键字定义常量,避免使用宏定义常量。
  7. 条件语句和循环

    • 在条件语句和循环中总是使用大括号,即使只有一行代码。

Microsoft:

  1. 命名约定

    • 变量名、函数名、类型名和常量名应使用驼峰命名法(CamelCase),首字母小写。

    • 宏定义使用全大写字母,并且单词之间用下划线分隔。

  2. 代码布局

    • 使用一致的缩进风格,推荐使用四个空格进行缩进。

    • 函数和控制结构的大括号应该独占一行,且大括号应该与相关语句对齐。

  3. 注释规范

    • 函数头应包含描述函数用途、参数和返回值的详细注释。

    • 代码行注释应解释代码的意图、复杂的算法或者可能引起误解的地方。

  4. 函数和参数

    • 函数应该有明确的声明和定义,包括返回类型、函数名和参数列表。

    • 参数应有描述其用途和类型的清晰命名,推荐参数顺序应该符合逻辑顺序。

  5. 错误处理

    • 建议在可能发生错误的地方进行错误检查,并采取适当的错误处理措施。

    • 错误处理应该清晰、一致,并能够提供有用的错误信息和恢复机制。

Recommended C Style and Coding Standards

练习 15:指针,可怕的指针

#include <stdio.h> int main(int argc, char *argv[]) { // create two arrays we care about int ages[] = {23, 43, 12, 89, 2}; char *names[] = { "Alan", "Frank", "Mary", "John", "Lisa" }; // safely get the size of ages int count = sizeof(ages) / sizeof(int); int i = 0; // first way using indexing for(i = 0; i < count; i++) { printf("%s has %d years alive.\n", names[i], ages[i]); } printf("---\n"); // setup the pointers to the start of the arrays int *cur_age = ages; char **cur_name = names; // second way using pointers for(i = 0; i < count; i++) { printf("%s is %d years old.\n", *(cur_name+i), *(cur_age+i)); } printf("---\n"); // third way, pointers are just arrays for(i = 0; i < count; i++) { printf("%s is %d years old again.\n", cur_name[i], cur_age[i]); } printf("---\n"); // fourth way with pointers in a stupid complex way for(cur_name = names, cur_age = ages; (cur_age - ages) < count; cur_name++, cur_age++) { printf("%s lived %d years so far.\n", *cur_name, *cur_age); } return 0; }

assert 宏用于在程序中添加断言。它检查表达式 expression 是否为真。如果表达式为假,程序将打印错误信息并终止执行。

试着将cur_age指向names。可以需要C风格转换来强制执行,试着查阅相关资料把它弄明白。

#include <stdio.h> int main(int argc, char *argv[]) { // create two arrays we care about int ages[] = {23, 43, 12, 89, 2}; char *names[] = { "Alan", "Frank", "Mary", "John", "Lisa" }; // safely get the size of ages int count = sizeof(ages) / sizeof(int); // setup the pointers to the start of the arrays int *cur_age = ages; char **cur_name = names; // forcing cur_age to point to names (note: this is just for demonstration) cur_age = (int *)names; // output using the modified cur_age pointer for(int i = 0; i < count; i++) { printf("%s has %d years alive.\n", *(cur_name + i), *(cur_age + i)); } return 0; }
⮞ ./ex15_0 Alan has -1933959164 years alive. Frank has 22046 years alive. Mary has -1933959159 years alive. John has 22046 years alive. Lisa has -1933959153 years alive.

使用访问指针的方式重写所有使用数组的地方。

#include <stdio.h> int main(int argc, char *argv[]) { // create two arrays we care about int ages[] = {23, 43, 12, 89, 2}; char *names[] = { "Alan", "Frank", "Mary", "John", "Lisa" }; // safely get the size of ages int count = sizeof(ages) / sizeof(int); // setup the pointers to the start of the arrays int *cur_age = ages; char **cur_name = names; // first way using pointers for(int i = 0; i < count; i++) { printf("%s has %d years alive.\n", *(cur_name + i), *(cur_age + i)); } printf("---\n"); // second way using pointers for(int i = 0; i < count; i++) { printf("%s is %d years old.\n", *(cur_name + i), *(cur_age + i)); } printf("---\n"); // third way using pointers for(int i = 0; i < count; i++) { printf("%s is %d years old again.\n", *(cur_name + i), *(cur_age + i)); } printf("---\n"); // fourth way with pointers in a slightly complex way for(; cur_age < ages + count; cur_name++, cur_age++) { printf("%s lived %d years so far.\n", *cur_name, *cur_age); } return 0; }

使用访问数组的方式重写所有使用指针的地方。

#include <stdio.h> int main(int argc, char *argv[]) { // create two arrays we care about int ages[] = {23, 43, 12, 89, 2}; char *names[] = { "Alan", "Frank", "Mary", "John", "Lisa" }; // safely get the size of ages int count = sizeof(ages) / sizeof(int); int i = 0; // first way using indexing for(i = 0; i < count; i++) { printf("%s has %d years alive.\n", names[i], ages[i]); } printf("---\n"); // second way using pointers (converted to array indexing) for(i = 0; i < count; i++) { printf("%s is %d years old.\n", names[i], ages[i]); } printf("---\n"); // third way, pointers are just arrays (already using array indexing) for(i = 0; i < count; i++) { printf("%s is %d years old again.\n", names[i], ages[i]); } printf("---\n"); // fourth way with pointers in a stupid complex way (converted to array indexing) for(i = 0; i < count; i++) { printf("%s lived %d years so far.\n", names[i], ages[i]); } return 0; }

在其它程序中使用指针来代替数组访问。

使用指针来处理命令行参数,就像处理names那样。

将获取值和获取地址组合到一起。

#include <stdio.h> int main(int argc, char *argv[]) { int i = 0; // Output addresses and values of argv for(i = 0; i < argc; i++) { printf("argv[%d]: %p: %s\n", i, (void *)&argv[i], argv[i]); } printf("---\n"); // let's make our own array of strings char *states[] = { "California", "Oregon", "Washington", "Texas" }; int num_states = 4; // Output addresses and values of states for(i = 0; i < num_states; i++) { printf("states[%d]: %p: %s\n", i, (void *)&states[i], states[i]); } return 0; }
⮞ ./ex15_5 aaaa bbbb cccc argv[0]: 0x7ffddf1cce58: ./ex15_5 argv[1]: 0x7ffddf1cce60: aaaa argv[2]: 0x7ffddf1cce68: bbbb argv[3]: 0x7ffddf1cce70: cccc --- states[0]: 0x7ffddf1ccd10: California states[1]: 0x7ffddf1ccd18: Oregon states[2]: 0x7ffddf1ccd20: Washington states[3]: 0x7ffddf1ccd28: Texas

在程序末尾添加一个for循环,打印出这些指针所指向的地址。你需要在printf中使用%p

// print the addresses of the pointers for(i = 0; i < count; i++) { printf("Address of names[%d]: %p, Address of ages[%d]: %p\n", i, (void*)&names[i], i, (void*)&ages[i]); }
Address of names[0]: 0x7fff7758f590, Address of ages[0]: 0x7fff7758f570 Address of names[1]: 0x7fff7758f598, Address of ages[1]: 0x7fff7758f574 Address of names[2]: 0x7fff7758f5a0, Address of ages[2]: 0x7fff7758f578 Address of names[3]: 0x7fff7758f5a8, Address of ages[3]: 0x7fff7758f57c Address of names[4]: 0x7fff7758f5b0, Address of ages[4]: 0x7fff7758f580

对于每一种打印数组的方法,使用函数来重写程序。试着向函数传递指针来处理数据。记住你可以声明接受指针的函数,但是可以像数组那样用它。

#include <stdio.h> // Function declarations void print_by_indexing(char **names, int *ages, int count); void print_by_pointer_arithmetic(char **names, int *ages, int count); void print_by_pointer_as_array(char **names, int *ages, int count); void print_by_complex_pointer_arithmetic(char **names, int *ages, int count); void print_addresses(char **names, int *ages, int count); int main(int argc, char *argv[]) { // create two arrays we care about int ages[] = {23, 43, 12, 89, 2}; char *names[] = { "Alan", "Frank", "Mary", "John", "Lisa" }; // safely get the size of ages int count = sizeof(ages) / sizeof(int); // Using different methods to print arrays print_by_indexing(names, ages, count); printf("---\n"); print_by_pointer_arithmetic(names, ages, count); printf("---\n"); print_by_pointer_as_array(names, ages, count); printf("---\n"); print_by_complex_pointer_arithmetic(names, ages, count); printf("---\n"); // Print the addresses of the pointers print_addresses(names, ages, count); return 0; } // Function definitions void print_by_indexing(char **names, int *ages, int count) { for(int i = 0; i < count; i++) { printf("%s has %d years alive.\n", names[i], ages[i]); } } void print_by_pointer_arithmetic(char **names, int *ages, int count) { for(int i = 0; i < count; i++) { printf("%s is %d years old.\n", *(names + i), *(ages + i)); } } void print_by_pointer_as_array(char **names, int *ages, int count) { for(int i = 0; i < count; i++) { printf("%s is %d years old again.\n", names[i], ages[i]); } } void print_by_complex_pointer_arithmetic(char **names, int *ages, int count) { char **cur_name = names; int *cur_age = ages; for(int i = 0; i < count; i++, cur_name++, cur_age++) { printf("%s lived %d years so far.\n", *cur_name, *cur_age); } } void print_addresses(char **names, int *ages, int count) { for(int i = 0; i < count; i++) { printf("Address of names[%d]: %p, Address of ages[%d]: %p\n", i, (void*)&names[i], i, (void*)&ages[i]); } }

for循环改为while循环,并且观察对于每种指针用法哪种循环更方便。

#include <stdio.h> int main(int argc, char *argv[]) { // create two arrays we care about int ages[] = {23, 43, 12, 89, 2}; char *names[] = { "Alan", "Frank", "Mary", "John", "Lisa" }; // safely get the size of ages int count = sizeof(ages) / sizeof(int); int i = 0; // first way using indexing i = 0; while(i < count) { printf("%s has %d years alive.\n", names[i], ages[i]); i++; } printf("---\n"); // setup the pointers to the start of the arrays int *cur_age = ages; char **cur_name = names; // second way using pointers i = 0; while(i < count) { printf("%s is %d years old.\n", *(cur_name+i), *(cur_age+i)); i++; } printf("---\n"); // third way, pointers are just arrays i = 0; while(i < count) { printf("%s is %d years old again.\n", cur_name[i], cur_age[i]); i++; } printf("---\n"); // fourth way with pointers in a stupid complex way cur_name = names; cur_age = ages; i = 0; while((cur_age - ages) < count) { printf("%s lived %d years so far.\n", *cur_name, *cur_age); cur_name++; cur_age++; i++; } return 0; }

看起来还是 for 更加简单呢。

练习 16:结构体和指向它们的指针

#include <stdio.h> #include <assert.h> #include <stdlib.h> #include <string.h> struct Person { char *name; int age; int height; int weight; }; struct Person *Person_create(char *name, int age, int height, int weight) { struct Person *who = malloc(sizeof(struct Person)); assert(who != NULL); who->name = strdup(name); who->age = age; who->height = height; who->weight = weight; return who; } void Person_destroy(struct Person *who) { assert(who != NULL); free(who->name); free(who); } void Person_print(struct Person *who) { printf("Name: %s\n", who->name); printf("\tAge: %d\n", who->age); printf("\tHeight: %d\n", who->height); printf("\tWeight: %d\n", who->weight); } int main(int argc, char *argv[]) { // make two people structures struct Person *joe = Person_create( "Joe Alex", 32, 64, 140); struct Person *frank = Person_create( "Frank Blank", 20, 72, 180); // print them out and where they are in memory printf("Joe is at memory location %p:\n", joe); Person_print(joe); printf("Frank is at memory location %p:\n", frank); Person_print(frank); // make everyone age 20 years and print them again joe->age += 20; joe->height -= 2; joe->weight += 40; Person_print(joe); frank->age += 20; frank->weight += 20; Person_print(frank); // destroy them both so we clean up Person_destroy(joe); Person_destroy(frank); return 0; }

试着传递NULLPerson_destroy来看看会发生什么。如果它没有崩溃,你必须移除Makefile的CFLAGS中的-g选项。

在当前的代码中,如果传递 NULLPerson_destroy 函数,在 Person_destroy 函数中使用了 assert(who != NULL); 进行检查。如果 whoNULLassert 会触发断言失败,程序会终止。

ex16: ex16.c:28: Person_destroy: Assertion `who != NULL' failed. Aborted

在结尾处忘记调用Person_destroy,在Valgrind下运行程序,你会看到它报告出你忘记释放内存。弄清楚你应该向valgrind传递什么参数来让它向你报告内存如何泄露。

⮞ valgrind --leak-check=full ./ex16 ==234164== Memcheck, a memory error detector ==234164== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al. ==234164== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info ==234164== Command: ./ex16 ==234164== Joe is at memory location 0x4a95040: Name: Joe Alex Age: 32 Height: 64 Weight: 140 Frank is at memory location 0x4a950f0: Name: Frank Blank Age: 20 Height: 72 Weight: 180 Name: Joe Alex Age: 52 Height: 62 Weight: 180 Name: Frank Blank Age: 40 Height: 72 Weight: 200 ==234164== ==234164== HEAP SUMMARY: ==234164== in use at exit: 69 bytes in 4 blocks ==234164== total heap usage: 5 allocs, 1 frees, 1,093 bytes allocated ==234164== ==234164== 33 (24 direct, 9 indirect) bytes in 1 blocks are definitely lost in loss record 3 of 4 ==234164== at 0x484880F: malloc (vg_replace_malloc.c:446) ==234164== by 0x1091EB: Person_create (ex16.c:15) ==234164== by 0x10936E: main (ex16.c:45) ==234164== ==234164== 36 (24 direct, 12 indirect) bytes in 1 blocks are definitely lost in loss record 4 of 4 ==234164== at 0x484880F: malloc (vg_replace_malloc.c:446) ==234164== by 0x1091EB: Person_create (ex16.c:15) ==234164== by 0x109390: main (ex16.c:48) ==234164== ==234164== LEAK SUMMARY: ==234164== definitely lost: 48 bytes in 2 blocks ==234164== indirectly lost: 21 bytes in 2 blocks ==234164== possibly lost: 0 bytes in 0 blocks ==234164== still reachable: 0 bytes in 0 blocks ==234164== suppressed: 0 bytes in 0 blocks ==234164== ==234164== For lists of detected and suppressed errors, rerun with: -s ==234164== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

忘记在Person_destroy中释放who->name,并且对比两次的输出。同时,使用正确的选项来让Valgrind告诉你哪里错了。

⮞ valgrind --leak-check=full -s ./ex16 ==236601== Memcheck, a memory error detector ==236601== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al. ==236601== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info ==236601== Command: ./ex16 ==236601== Joe is at memory location 0x4a95040: Name: Joe Alex Age: 32 Height: 64 Weight: 140 Frank is at memory location 0x4a950f0: Name: Frank Blank Age: 20 Height: 72 Weight: 180 Name: Joe Alex Age: 52 Height: 62 Weight: 180 Name: Frank Blank Age: 40 Height: 72 Weight: 200 ==236601== ==236601== HEAP SUMMARY: ==236601== in use at exit: 21 bytes in 2 blocks ==236601== total heap usage: 5 allocs, 3 frees, 1,093 bytes allocated ==236601== ==236601== 9 bytes in 1 blocks are definitely lost in loss record 1 of 2 ==236601== at 0x484880F: malloc (vg_replace_malloc.c:446) ==236601== by 0x491158E: strdup (strdup.c:42) ==236601== by 0x10922A: Person_create (ex16.c:18) ==236601== by 0x10935F: main (ex16.c:45) ==236601== ==236601== 12 bytes in 1 blocks are definitely lost in loss record 2 of 2 ==236601== at 0x484880F: malloc (vg_replace_malloc.c:446) ==236601== by 0x491158E: strdup (strdup.c:42) ==236601== by 0x10922A: Person_create (ex16.c:18) ==236601== by 0x109381: main (ex16.c:48) ==236601== ==236601== LEAK SUMMARY: ==236601== definitely lost: 21 bytes in 2 blocks ==236601== indirectly lost: 0 bytes in 0 blocks ==236601== possibly lost: 0 bytes in 0 blocks ==236601== still reachable: 0 bytes in 0 blocks ==236601== suppressed: 0 bytes in 0 blocks ==236601== ==236601== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

这一次,向Person_print传递NULL,并且观察Valgrind会输出什么。

⮞ ./ex16 Joe is at memory location 0x55e046cba2a0: Name: Joe Alex Age: 32 Height: 64 Weight: 140 Frank is at memory location 0x55e046cba2e0: Name: Frank Blank Age: 20 Height: 72 Weight: 180 Name: Joe Alex Age: 52 Height: 62 Weight: 180 Name: Frank Blank Age: 40 Height: 72 Weight: 200 Segmentation fault
==237517== Invalid read of size 8 ==237517== at 0x1092CA: Person_print (ex16.c:36) ==237517== by 0x109459: main (ex16.c:68) ==237517== Address 0x0 is not stack'd, malloc'd or (recently) free'd ==237517== ==237517== ==237517== Process terminating with default action of signal 11 (SIGSEGV) ==237517== Access not within mapped region at address 0x0 ==237517== at 0x1092CA: Person_print (ex16.c:36) ==237517== by 0x109459: main (ex16.c:68) ==237517== If you believe this happened as a result of a stack ==237517== overflow in your program's main thread (unlikely but ==237517== possible), you can try to increase the size of the ==237517== main thread stack using the --main-stacksize= flag. ==237517== The main thread stack size used in this run was 8388608. ==237517== ==237517== HEAP SUMMARY: ==237517== in use at exit: 1,093 bytes in 5 blocks ==237517== total heap usage: 5 allocs, 0 frees, 1,093 bytes allocated ==237517== ==237517== LEAK SUMMARY: ==237517== definitely lost: 0 bytes in 0 blocks ==237517== indirectly lost: 0 bytes in 0 blocks ==237517== possibly lost: 0 bytes in 0 blocks ==237517== still reachable: 1,093 bytes in 5 blocks ==237517== suppressed: 0 bytes in 0 blocks ==237517== Rerun with --leak-check=full to see details of leaked memory ==237517== ==237517== For lists of detected and suppressed errors, rerun with: -s ==237517== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0) Segmentation fault

附加题:将这个程序改为不用指针和malloc的版本

在这个练习的附加题中我想让你尝试一些有难度的东西:将这个程序改为不用指针和malloc的版本。这可能很困难,所以你需要研究下面这些东西:

  • 如何在栈上创建结构体,就像你创建任何其它变量那样。

  • 如何使用x.y而不是x->y来初始化结构体。

  • 如何不使用指针来将结构体传给其它函数。

#include <stdio.h> #include <string.h> struct Person { char name[50]; int age; int height; int weight; }; void Person_print(struct Person who) { printf("Name: %s\n", who.name); printf("\tAge: %d\n", who.age); printf("\tHeight: %d\n", who.height); printf("\tWeight: %d\n", who.weight); } int main(int argc, char *argv[]) { // create two people structures directly on the stack struct Person joe; strcpy(joe.name, "Joe Alex"); joe.age = 32; joe.height = 64; joe.weight = 140; struct Person frank; strcpy(frank.name, "Frank Blank"); frank.age = 20; frank.height = 72; frank.weight = 180; // print them out and where they are in memory printf("Joe is at memory location %p:\n", (void*)&joe); Person_print(joe); printf("Frank is at memory location %p:\n", (void*)&frank); Person_print(frank); // make everyone age 20 years and print them again joe.age += 20; joe.height -= 2; joe.weight += 40; Person_print(joe); frank.age += 20; frank.weight += 20; Person_print(frank); return 0; }

在这个版本中:

- 直接在栈上创建 struct Person 实例 joefrank

- 使用 strcpy 来复制字符串到 name 字段,因为不能再使用 strdup

- Person_createPerson_destroy 函数被移除,因为不再需要动态分配和释放内存。

练习 17:堆和栈的内存分配

#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; }

结构体之间的关系

+-----------------+ +--------------------+ +--------------------+ | struct Address |<--->| struct Database |<--->| struct Connection | +-----------------+ +--------------------+ +--------------------+ | id | | rows[MAX_ROWS] | | file | | set | | - Address[0] | | db | | name[MAX_DATA] | | - Address[1] | | | | email[MAX_DATA] | | - ... | | | +-----------------+ | - Address[99] | +--------------------+ +--------------------+
  • Address 结构体表示数据库中的一条记录。

    • id 是记录的唯一标识符。

    • set 表示该记录是否已经设置(即是否有效)。

    • nameemail 是记录的具体数据,分别存储名字和邮箱。

  • Database 结构体表示整个数据库。

    • 它包含一个 Address 数组 rows,每个元素代表一条记录。

    • MAX_ROWS 定义了数据库可以包含的最大记录数(100条)。

  • Connection 结构体表示与数据库文件的连接。

    • file 是一个文件指针,指向数据库文件。

    • db 是一个指向 Database 结构体的指针,表示内存中的数据库内容。

一些不太熟悉的函数

  • errno 是一个全局变量,表示最近一次函数调用失败时的错误码。

  • perror 用于打印描述错误码的错误信息。

  • malloc:分配指定大小的内存块,并返回指向该内存块的指针。

  • free:释放由 malloc 分配的内存块。

  • exit:终止程序执行,并返回一个状态码给操作系统。

  • atoi:将字符串转换为整数。

    • 它会跳过字符串前面的所有空白字符(如空格、制表符等)。

    • 然后它会尽可能多地转换数字字符,直到遇到非数字字符或字符串末尾为止。

    • 如果字符串不包含任何可转换的数字字符,那么 atoi 会返回 0

    • Example:123abc456 → 123.

  • fopen:用于打开文件,并根据不同的模式进行读写操作。

  • fclose:用于关闭打开的文件。

  • fread:用于从文件中读取数据到内存。

  • fwrite:用于将内存中的数据写入文件。

  • rewind:用于将文件指针移到文件开头。

  • fflush:用于刷新文件流的输出缓冲区,确保数据写入文件。

strncpy设计缺陷修复:

addr->name[MAX_DATA - 1] = '\0';

die函数需要接收conn变量作为参数,以便执行清理并关闭它。

修改代码,使其接收参数作为MAX_DATA和MAX_ROWS,将它们储存在Database结构体中,并且将它们写到文件。这样就可以创建任意大小的数据库。

#include <stdio.h> #include <assert.h> #include <stdlib.h> #include <errno.h> #include <string.h> struct Address { int id; int set; char *name; char *email; }; struct Database { int max_data; int max_rows; struct Address *rows; }; struct Connection { FILE *file; struct Database *db; }; void Database_close(struct Connection *conn) { if(conn) { if(conn->file) fclose(conn->file); if(conn->db) free(conn->db); free(conn); } } void die(struct Connection *conn, const char *message) { if (errno) { perror(message); } else { printf("ERROR: %s\n", message); } Database_close(conn); 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(conn, "Failed to load database header."); } struct Connection *Database_open(const char *filename, char mode, int max_data, int max_rows) { struct Connection *conn = malloc(sizeof(struct Connection)); if (!conn) die(NULL, "Memory error"); conn->db = malloc(sizeof(struct Database)); if (!conn->db) die(conn, "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(conn, "Failed to open the file"); return 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(conn, "Failed to write database header."); rc = fflush(conn->file); if (rc == -1) die(conn, "Cannot flush database."); } void Database_create(struct Connection *conn) { for (int i = 0; i < conn->db->max_rows; i++) { struct Address addr = {.id = i, .set = 0}; conn->db->rows[i] = addr; } printf("SUCCESS!\n"); } 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(conn, "Already set, delete it first"); addr->set = 1; char *res_name = strncpy(addr->name, name, conn->db->max_data); if (!res_name) die(conn, "Name copy failed"); char *res_email = strncpy(addr->email, email, conn->db->max_data); if (!res_email) die(conn, "Email copy failed"); if (res_name && res_email) printf("SUCCESS!\n"); } void Database_get(struct Connection *conn, int id) { struct Address *addr = &conn->db->rows[id]; if (addr->set) { Address_print(addr); } else { die(conn, "ID is not set"); } } void Database_delete(struct Connection *conn, int id) { struct Address addr = {.id = id, .set = 0}; conn->db->rows[id] = addr; printf("SUCCESS!\n"); } void Database_list(struct Connection *conn) { struct Database *db = conn->db; for (int i = 0; i < db->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(NULL, "USAGE: ex17 <dbfile> <action> [action params]"); char *filename = argv[1]; char action = argv[2][0]; struct Connection *conn = NULL; int id = 0; int max_data = 0; int max_rows = 0; if (action == 'c') { if (argc != 5) die(NULL, "Need max_data and max_rows to create"); max_data = atoi(argv[3]); max_rows = atoi(argv[4]); conn = Database_open(filename, action, max_data, max_rows); Database_create(conn); Database_write(conn); } else { conn = Database_open(filename, action, max_data, max_rows); if (argc > 3) id = atoi(argv[3]); //if (id >= conn->db->max_rows) die(conn, "There's not that many records."); switch (action) { case 'g': if (argc != 4) die(conn, "Need an id to get"); Database_get(conn, id); break; case 's': if (argc != 6) die(conn, "Need id, name, email to set"); Database_set(conn, id, argv[4], argv[5]); Database_write(conn); break; case 'd': if (argc != 4) die(conn, "Need id to delete"); Database_delete(conn, id); Database_write(conn); break; case 'l': Database_list(conn); break; default: die(conn, "Invalid action, only: c=create, g=get, s=set, d=del, l=list"); } } Database_close(conn); return 0; }

内存溢出原因:struct database 的字节数不能被正常获取。

解决方法:不一次性 load 整个结构体,先读取参数,再读取数组。

#include <stdio.h> #include <assert.h> #include <stdlib.h> #include <errno.h> #include <string.h> struct Address { int id; int set; char *name; char *email; }; struct Database { int max_data; int max_rows; struct Address **rows; }; struct Connection { FILE *file; struct Database *db; }; void Database_close(struct Connection *conn) { if (conn) { if (conn->db && conn->db->rows) { for (int i = 0; i < conn->db->max_rows; i++) { struct Address *cur = conn->db->rows[i]; free(cur); } } if (conn->file) fclose(conn->file); if (conn->db) free(conn->db); free(conn); printf("Database close : success\n"); } } void die(struct Connection *conn, const char *message) { if (errno) { perror(message); } else { printf("ERROR: %s\n", message); } Database_close(conn); 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) { assert(conn->db && conn->file); if (!(conn->db && conn->file)) die(conn, "Database load : Invalid Connection info"); if (fread(&conn->db->max_data, sizeof(conn->db->max_data), 1, conn->file) != 1) die(conn, "Database load : Couldn't read MAX_DATA"); if (fread(&conn->db->max_rows, sizeof(conn->db->max_rows), 1, conn->file) != 1) die(conn, "Database load : Couldn't read MAX_ROWS"); conn->db->rows = (struct Address **)malloc(sizeof(struct Address *) * conn->db->max_rows); assert(conn->db->rows); if (!(conn->db->rows)) die(conn, "Database_load : Could not read MAX_ROWS Address structures"); for (int i = 0; i < conn->db->max_rows; i++) { conn->db->rows[i] = (struct Address *)malloc(sizeof(struct Address)); struct Address *row = conn->db->rows[i]; if (fread(&row->id, sizeof(row->id), 1, conn->file) != 1) die(conn, "Database load : Could not read Address::id"); if (fread(&row->set, sizeof(row->set), 1, conn->file) != 1) die(conn, "Database load : Couldn't read Address::set"); row->name = malloc(conn->db->max_data * sizeof(*row->name)); row->email = malloc(conn->db->max_data * sizeof(*row->email)); assert(row->email && row->name); if (!(row->name && row->email)) die(conn, "Database load : Failed to Allocate Address strings"); if (fread(row->name, conn->db->max_data * sizeof(*row->name), 1, conn->file) != 1) die(conn, "Database load : Failed to read Address::name"); if (fread(row->email, conn->db->max_data * sizeof(*row->email), 1, conn->file) != 1) die(conn, "Database load : Failed to read Address::email"); } printf("Database load : success\n"); } struct Connection *Database_open(const char *filename, char mode, int max_data, int max_rows) { struct Connection *conn = malloc(sizeof(struct Connection)); if (!conn) die(NULL, "Memory error"); conn->db = malloc(sizeof(struct Database)); if (!conn->db) die(conn, "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(conn, "Failed to open the file"); else printf("Database open : success\n"); return conn; } void Database_write(struct Connection *conn) { rewind(conn->file); if (fwrite(&conn->db->max_data, sizeof(conn->db->max_data), 1, conn->file) != 1) die(conn, "Database write : Failed to write MAX_DATA"); if (fwrite(&conn->db->max_rows, sizeof(conn->db->max_rows), 1, conn->file) != 1) die(conn, "Database write : Failed to write MAX_ROWS"); for (int i = 0; i < conn->db->max_rows; i++) { struct Address *row = conn->db->rows[i]; if (fwrite(&row->id, sizeof(row->id), 1, conn->file) != 1) die(conn, "Database write : Failed to write Address(id)"); if (fwrite(&row->set, sizeof(row->set), 1, conn->file) != 1) die(conn, "Database write : Failed to write Address(set)"); if (fwrite(row->name, sizeof(char) * conn->db->max_data, 1, conn->file) != 1) die(conn, "Database write : Failed to write Address(name)"); if (fwrite(row->email, sizeof(char) * conn->db->max_data, 1, conn->file) != 1) die(conn, "Database write : Failed to write Address(email)"); } if (fflush(conn->file) == -1) die(conn, "Database_write : Cannot flush database."); else printf("Database write : success\n"); } void Database_create(struct Connection *conn, int max_data, int max_rows) { conn->db->max_data = max_data; conn->db->max_rows = max_rows; conn->db->rows = (struct Address **)malloc(sizeof(struct Address *) * max_rows); for (int i = 0; i < max_rows; i++) { conn->db->rows[i] = (struct Address *)malloc(sizeof(struct Address)); conn->db->rows[i]->id = i; conn->db->rows[i]->set = 0; conn->db->rows[i]->name = (char *)malloc(conn->db->max_data); conn->db->rows[i]->name = (char *)memset(conn->db->rows[i]->name, ' ', conn->db->max_data); conn->db->rows[i]->email = (char *)malloc(conn->db->max_data); conn->db->rows[i]->email = (char *)memset(conn->db->rows[i]->email, ' ', conn->db->max_data); } printf("Database Create : success\n"); } void Database_set(struct Connection *conn, int id, const char *name, const char *email) { if (!(conn && conn->db && conn->db->rows && conn->db->rows[id])) die(conn, "Wrong database structure"); struct Address *addr = conn->db->rows[id]; int MAX_DATA = conn->db->max_data; if (addr->set == 1) die(conn, "Already set, delete it first"); addr->set = 1; addr->name = malloc(sizeof(char) * MAX_DATA); addr->email = malloc(sizeof(char) * MAX_DATA); char *res = strncpy(addr->name, name, MAX_DATA); addr->name[MAX_DATA - 1] = '\0'; if (!res) die(conn, "Name copy failed"); res = strncpy(addr->email, email, MAX_DATA); addr->name[MAX_DATA - 1] = '\0'; if (!res) die(conn, "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(conn, "ID is not set"); } } void Database_delete(struct Connection *conn, int id) { struct Address addr = {.id = id, .set = 0}; conn->db->rows[id] = &addr; printf("SUCCESS!\n"); } void Database_list(struct Connection *conn) { struct Database *db = conn->db; for (int i = 0; i < db->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(NULL, "USAGE: ex17 <dbfile> <action> [action params]"); char *filename = argv[1]; char action = argv[2][0]; struct Connection *conn = NULL; int id = 0; int max_data = 0; int max_rows = 0; if (action == 'c') { if (argc != 5) die(NULL, "Need max_data and max_rows to create"); max_data = atoi(argv[3]); max_rows = atoi(argv[4]); conn = Database_open(filename, action, max_data, max_rows); Database_create(conn, max_data, max_rows); Database_write(conn); } else { conn = Database_open(filename, action, 0, 0); if (argc > 3) id = atoi(argv[3]) - 1; if (id > conn->db->max_rows) die(conn, "There's not that many records."); switch (action) { case 'g': if (argc != 4) die(conn, "Need an id to get"); Database_get(conn, id); break; case 's': if (argc != 6) die(conn, "Need id, name, email to set"); Database_set(conn, id, argv[4], argv[5]); Database_write(conn); break; case 'd': if (argc != 4) die(conn, "Need id to delete"); Database_delete(conn, id); Database_write(conn); break; case 'l': Database_list(conn); break; default: die(conn, "Invalid action, only: c=create, g=get, s=set, d=del, l=list"); } } Database_close(conn); return 0; }

向数据库添加更多操作,比如find。

#include <stdio.h> #include <assert.h> #include <stdlib.h> #include <errno.h> #include <string.h> struct Address { int id; int set; char *name; char *email; }; struct Database { int max_data; int max_rows; struct Address **rows; }; struct Connection { FILE *file; struct Database *db; }; void Database_close(struct Connection *conn) { if (conn) { if (conn->db && conn->db->rows) { for (int i = 0; i < conn->db->max_rows; i++) { struct Address *cur = conn->db->rows[i]; free(cur); } } if (conn->file) fclose(conn->file); if (conn->db) free(conn->db); free(conn); printf("Database close : success\n"); } } void die(struct Connection *conn, const char *message) { if (errno) { perror(message); } else { printf("ERROR: %s\n", message); } Database_close(conn); exit(1); } void Address_print(struct Address *addr) { printf("%d %s %s\n", addr->id + 1, addr->name, addr->email); } void Database_load(struct Connection *conn) { assert(conn->db && conn->file); if (!(conn->db && conn->file)) die(conn, "Database load : Invalid Connection info"); if (fread(&conn->db->max_data, sizeof(conn->db->max_data), 1, conn->file) != 1) die(conn, "Database load : Couldn't read MAX_DATA"); if (fread(&conn->db->max_rows, sizeof(conn->db->max_rows), 1, conn->file) != 1) die(conn, "Database load : Couldn't read MAX_ROWS"); conn->db->rows = (struct Address **)malloc(sizeof(struct Address *) * conn->db->max_rows); assert(conn->db->rows); if (!(conn->db->rows)) die(conn, "Database_load : Could not read MAX_ROWS Address structures"); for (int i = 0; i < conn->db->max_rows; i++) { conn->db->rows[i] = (struct Address *)malloc(sizeof(struct Address)); struct Address *row = conn->db->rows[i]; if (fread(&row->id, sizeof(row->id), 1, conn->file) != 1) die(conn, "Database load : Could not read Address::id"); if (fread(&row->set, sizeof(row->set), 1, conn->file) != 1) die(conn, "Database load : Couldn't read Address::set"); row->name = malloc(conn->db->max_data * sizeof(*row->name)); row->email = malloc(conn->db->max_data * sizeof(*row->email)); assert(row->email && row->name); if (!(row->name && row->email)) die(conn, "Database load : Failed to Allocate Address strings"); if (fread(row->name, conn->db->max_data * sizeof(*row->name), 1, conn->file) != 1) die(conn, "Database load : Failed to read Address::name"); if (fread(row->email, conn->db->max_data * sizeof(*row->email), 1, conn->file) != 1) die(conn, "Database load : Failed to read Address::email"); } printf("Database load : success\n"); } struct Connection *Database_open(const char *filename, char mode, int max_data, int max_rows) { struct Connection *conn = malloc(sizeof(struct Connection)); if (!conn) die(NULL, "Memory error"); conn->db = malloc(sizeof(struct Database)); if (!conn->db) die(conn, "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(conn, "Failed to open the file"); else printf("Database open : success\n"); return conn; } void Database_write(struct Connection *conn) { rewind(conn->file); if (fwrite(&conn->db->max_data, sizeof(conn->db->max_data), 1, conn->file) != 1) die(conn, "Database write : Failed to write MAX_DATA"); if (fwrite(&conn->db->max_rows, sizeof(conn->db->max_rows), 1, conn->file) != 1) die(conn, "Database write : Failed to write MAX_ROWS"); for (int i = 0; i < conn->db->max_rows; i++) { struct Address *row = conn->db->rows[i]; if (fwrite(&row->id, sizeof(row->id), 1, conn->file) != 1) die(conn, "Database write : Failed to write Address(id)"); if (fwrite(&row->set, sizeof(row->set), 1, conn->file) != 1) die(conn, "Database write : Failed to write Address(set)"); if (fwrite(row->name, sizeof(char) * conn->db->max_data, 1, conn->file) != 1) die(conn, "Database write : Failed to write Address(name)"); if (fwrite(row->email, sizeof(char) * conn->db->max_data, 1, conn->file) != 1) die(conn, "Database write : Failed to write Address(email)"); } if (fflush(conn->file) == -1) die(conn, "Database_write : Cannot flush database."); else printf("Database write : success\n"); } void Database_create(struct Connection *conn, int max_data, int max_rows) { conn->db->max_data = max_data; conn->db->max_rows = max_rows; conn->db->rows = (struct Address **)malloc(sizeof(struct Address *) * max_rows); for (int i = 0; i < max_rows; i++) { conn->db->rows[i] = (struct Address *)malloc(sizeof(struct Address)); conn->db->rows[i]->id = i; conn->db->rows[i]->set = 0; conn->db->rows[i]->name = (char *)malloc(conn->db->max_data); conn->db->rows[i]->name = (char *)memset(conn->db->rows[i]->name, ' ', conn->db->max_data); conn->db->rows[i]->email = (char *)malloc(conn->db->max_data); conn->db->rows[i]->email = (char *)memset(conn->db->rows[i]->email, ' ', conn->db->max_data); } printf("Database Create : success\n"); } void Database_set(struct Connection *conn, int id, const char *name, const char *email) { if (!(conn && conn->db && conn->db->rows && conn->db->rows[id])) die(conn, "Wrong database structure"); struct Address *addr = conn->db->rows[id]; int MAX_DATA = conn->db->max_data; if (addr->set == 1) die(conn, "Already set, delete it first"); addr->set = 1; addr->name = malloc(sizeof(char) * MAX_DATA); addr->email = malloc(sizeof(char) * MAX_DATA); char *res = strncpy(addr->name, name, MAX_DATA); addr->name[MAX_DATA - 1] = '\0'; if (!res) die(conn, "Name copy failed"); res = strncpy(addr->email, email, MAX_DATA); addr->name[MAX_DATA - 1] = '\0'; if (!res) die(conn, "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(conn, "ID is not set"); } } void Database_delete(struct Connection *conn, int id) { struct Address addr = {.id = id, .set = 0}; conn->db->rows[id] = &addr; printf("SUCCESS!\n"); } void Database_list(struct Connection *conn) { struct Database *db = conn->db; for (int i = 0; i < db->max_rows; i++) { struct Address *cur = db->rows[i]; if (cur->set) { Address_print(cur); } } } void Database_find(struct Connection *conn, const char *type, const char *aim) { struct Database *db = conn->db; if (strcmp(type, "name") == 0) { for (int i = 0; i < db->max_rows; i++) { struct Address *cur = db->rows[i]; if (cur->set) { if (strcmp(aim, cur->name) == 0) { Address_print(cur); } } } } else if (strcmp(type, "email") == 0) { for (int i = 0; i < db->max_rows; i++) { struct Address *cur = db->rows[i]; if (cur->set) { if (strcmp(aim, cur->email) == 0) { Address_print(cur); } } } } else { die(conn, "Wrong type"); } } int main(int argc, char *argv[]) { if (argc < 3) die(NULL, "USAGE: ex17 <dbfile> <action> [action params]"); char *filename = argv[1]; char action = argv[2][0]; struct Connection *conn = NULL; int id = 0; int max_data = 0; int max_rows = 0; if (action == 'c') { if (argc != 5) die(NULL, "Need max_data and max_rows to create"); max_data = atoi(argv[3]); max_rows = atoi(argv[4]); conn = Database_open(filename, action, max_data, max_rows); Database_create(conn, max_data, max_rows); Database_write(conn); } else { conn = Database_open(filename, action, 0, 0); if (argc > 3) id = atoi(argv[3]) - 1; if (id > conn->db->max_rows - 1) die(conn, "There's not that many records."); switch (action) { case 'g': if (argc != 4) die(conn, "Need an id to get"); Database_get(conn, id); break; case 's': if (argc != 6) die(conn, "Need id, name, email to set"); Database_set(conn, id, argv[4], argv[5]); Database_write(conn); break; case 'd': if (argc != 4) die(conn, "Need id to delete"); Database_delete(conn, id); Database_write(conn); break; case 'l': Database_list(conn); break; case 'f': Database_find(conn, argv[3], argv[4]); break; default: die(conn, "Invalid action, only: c=create, g=get, s=set, d=del, l=list"); } } Database_close(conn); return 0; }

查询C如何打包结构体,并且试着弄清楚为什么你的文件是相应的大小。看看你是否可以计算出结构体添加一些字段之后的新大小。

+-----------------+ +--------------------+ +--------------------+ | struct Address |<--->| struct Database |<--->| struct Connection | +-----------------+ +--------------------+ +--------------------+ | id | | MAX_DATA | | file | | set | | MAX_ROWS | | db | | name[MAX_DATA] | | rows[MAX_ROWS] | | | | email[MAX_DATA] | | - Address[0] | | | +-----------------+ | - Address[...] | +--------------------+ +--------------------+ [4bit + 4bit + MAX_DATA + MAX_DATA] * MAX_ROWS + 4bit +4bit

向Address添加一些字段,使它们可被搜索。

#include <stdio.h> #include <assert.h> #include <stdlib.h> #include <errno.h> #include <string.h> struct Address { int id; int set; char *name; char *email; char *phone; // 新增字段 }; struct Database { int max_data; int max_rows; struct Address **rows; }; struct Connection { FILE *file; struct Database *db; }; void Database_close(struct Connection *conn) { if (conn) { if (conn->db && conn->db->rows) { for (int i = 0; i < conn->db->max_rows; i++) { struct Address *cur = conn->db->rows[i]; free(cur); } } if (conn->file) fclose(conn->file); if (conn->db) free(conn->db); free(conn); printf("Database close : success\n"); } } void die(struct Connection *conn, const char *message) { if (errno) { perror(message); } else { printf("ERROR: %s\n", message); } Database_close(conn); exit(1); } void Address_print(struct Address *addr) { printf("%d %s %s %s\n", addr->id + 1, addr->name, addr->email, addr->phone); } void Database_load(struct Connection *conn) { assert(conn->db && conn->file); if (!(conn->db && conn->file)) die(conn, "Database load : Invalid Connection info"); if (fread(&conn->db->max_data, sizeof(conn->db->max_data), 1, conn->file) != 1) die(conn, "Database load : Couldn't read MAX_DATA"); if (fread(&conn->db->max_rows, sizeof(conn->db->max_rows), 1, conn->file) != 1) die(conn, "Database load : Couldn't read MAX_ROWS"); conn->db->rows = (struct Address **)malloc(sizeof(struct Address *) * conn->db->max_rows); assert(conn->db->rows); if (!(conn->db->rows)) die(conn, "Database_load : Could not read MAX_ROWS Address structures"); for (int i = 0; i < conn->db->max_rows; i++) { conn->db->rows[i] = (struct Address *)malloc(sizeof(struct Address)); assert(conn->db->rows[i]); if (!(conn->db->rows[i])) die(conn, "Database_load : Could not create Address structure"); if (fread(&conn->db->rows[i]->id, sizeof(conn->db->rows[i]->id), 1, conn->file) != 1) die(conn, "Database_load : Couldn't read ID"); if (fread(&conn->db->rows[i]->set, sizeof(conn->db->rows[i]->set), 1, conn->file) != 1) die(conn, "Database_load : Couldn't read SET"); conn->db->rows[i]->name = (char *)malloc(conn->db->max_data); conn->db->rows[i]->email = (char *)malloc(conn->db->max_data); conn->db->rows[i]->phone = (char *)malloc(conn->db->max_data); // 为 phone 分配内存 if (!conn->db->rows[i]->name || !conn->db->rows[i]->email || !conn->db->rows[i]->phone) die(conn, "Database_load : Could not allocate memory for address data"); if (fread(conn->db->rows[i]->name, conn->db->max_data, 1, conn->file) != 1) die(conn, "Database_load : Couldn't read name"); if (fread(conn->db->rows[i]->email, conn->db->max_data, 1, conn->file) != 1) die(conn, "Database_load : Couldn't read email"); if (fread(conn->db->rows[i]->phone, conn->db->max_data, 1, conn->file) != 1) die(conn, "Database_load : Couldn't read phone"); // 读取 phone 数据 } } struct Connection *Database_open(const char *filename, char mode, int max_data, int max_rows) { struct Connection *conn = malloc(sizeof(struct Connection)); if (!conn) die(NULL, "Memory error"); conn->db = malloc(sizeof(struct Database)); if (!conn->db) die(conn, "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(conn, "Failed to open the file"); else printf("Database open : success\n"); return conn; } void Database_write(struct Connection *conn) { rewind(conn->file); if (fwrite(&conn->db->max_data, sizeof(conn->db->max_data), 1, conn->file) != 1) die(conn, "Database write : Could not write MAX_DATA"); if (fwrite(&conn->db->max_rows, sizeof(conn->db->max_rows), 1, conn->file) != 1) die(conn, "Database write : Could not write MAX_ROWS"); for (int i = 0; i < conn->db->max_rows; i++) { struct Address *cur = conn->db->rows[i]; if (fwrite(&cur->id, sizeof(cur->id), 1, conn->file) != 1) die(conn, "Database write : Could not write ID"); if (fwrite(&cur->set, sizeof(cur->set), 1, conn->file) != 1) die(conn, "Database write : Could not write SET"); if (fwrite(cur->name, conn->db->max_data, 1, conn->file) != 1) die(conn, "Database write : Could not write name"); if (fwrite(cur->email, conn->db->max_data, 1, conn->file) != 1) die(conn, "Database write : Could not write email"); if (fwrite(cur->phone, conn->db->max_data, 1, conn->file) != 1) die(conn, "Database write : Could not write phone"); // 写入 phone 数据 } if (fflush(conn->file) == -1) die(conn, "Database write : Could not flush database"); } void Database_create(struct Connection *conn, int max_data, int max_rows) { conn->db->max_data = max_data; conn->db->max_rows = max_rows; conn->db->rows = (struct Address **)malloc(sizeof(struct Address *) * max_rows); assert(conn->db->rows); if (!(conn->db->rows)) die(conn, "Database_create : Could not allocate memory for rows"); for (int i = 0; i < max_rows; i++) { conn->db->rows[i] = (struct Address *)malloc(sizeof(struct Address)); assert(conn->db->rows[i]); if (!(conn->db->rows[i])) die(conn, "Database_create : Could not create Address structure"); conn->db->rows[i]->id = i; conn->db->rows[i]->set = 0; conn->db->rows[i]->name = (char *)malloc(max_data); conn->db->rows[i]->email = (char *)malloc(max_data); conn->db->rows[i]->phone = (char *)malloc(max_data); // 为 phone 分配内存 if (!conn->db->rows[i]->name || !conn->db->rows[i]->email || !conn->db->rows[i]->phone) die(conn, "Database_create : Could not allocate memory for address data"); memset(conn->db->rows[i]->name, 0, max_data); memset(conn->db->rows[i]->email, 0, max_data); memset(conn->db->rows[i]->phone, 0, max_data); // 初始化 phone 字段 } } void Database_set(struct Connection *conn, int id, const char *name, const char *email, const char *phone) { struct Address *addr = conn->db->rows[id]; if (addr->set) die(conn, "Already set, delete it first"); addr->set = 1; strncpy(addr->name, name, conn->db->max_data); addr->name[conn->db->max_data - 1] = '\0'; strncpy(addr->email, email, conn->db->max_data); addr->email[conn->db->max_data - 1] = '\0'; strncpy(addr->phone, phone, conn->db->max_data); // 设置 phone 字段 addr->phone[conn->db->max_data - 1] = '\0'; } void Database_get(struct Connection *conn, int id) { struct Address *addr = conn->db->rows[id]; if (addr->set) { Address_print(addr); } else { die(conn, "ID is not set"); } } void Database_delete(struct Connection *conn, int id) { struct Address *addr = conn->db->rows[id]; addr->set = 0; } void Database_list(struct Connection *conn) { for (int i = 0; i < conn->db->max_rows; i++) { struct Address *cur = conn->db->rows[i]; if (cur->set) { Address_print(cur); } } } void Database_find(struct Connection *conn, const char *field, const char *value) { if (strcmp(field, "name") == 0) { for (int i = 0; i < conn->db->max_rows; i++) { struct Address *cur = conn->db->rows[i]; if (cur->set && strcmp(cur->name, value) == 0) { Address_print(cur); } } } else if (strcmp(field, "email") == 0) { for (int i = 0; i < conn->db->max_rows; i++) { struct Address *cur = conn->db->rows[i]; if (cur->set && strcmp(cur->email, value) == 0) { Address_print(cur); } } } else if (strcmp(field, "phone") == 0) // 增加 phone 字段的查找功能 { for (int i = 0; i < conn->db->max_rows; i++) { struct Address *cur = conn->db->rows[i]; if (cur->set && strcmp(cur->phone, value) == 0) { Address_print(cur); } } } else { die(conn, "Wrong type"); } } int main(int argc, char *argv[]) { if (argc < 3) die(NULL, "USAGE: ex17 <dbfile> <action> [action params]"); char *filename = argv[1]; char action = argv[2][0]; struct Connection *conn = NULL; int id = 0; int max_data = 0; int max_rows = 0; if (action == 'c') { if (argc != 5) die(NULL, "Need max_data and max_rows to create"); max_data = atoi(argv[3]); max_rows = atoi(argv[4]); conn = Database_open(filename, action, max_data, max_rows); Database_create(conn, max_data, max_rows); Database_write(conn); } else { conn = Database_open(filename, action, 0, 0); if (argc > 3) id = atoi(argv[3]) - 1; if (id > conn->db->max_rows - 1) die(conn, "There's not that many records."); switch (action) { case 'g': if (argc != 4) die(conn, "Need an id to get"); Database_get(conn, id); break; case 's': if (argc != 7) die(conn, "Need id, name, email, phone to set"); Database_set(conn, id, argv[4], argv[5], argv[6]); Database_write(conn); break; case 'd': if (argc != 4) die(conn, "Need id to delete"); Database_delete(conn, id); Database_write(conn); break; case 'l': Database_list(conn); break; case 'f': Database_find(conn, argv[3], argv[4]); break; default: die(conn, "Invalid action, only: c=create, g=get, s=set, d=del, l=list, f=find"); } } Database_close(conn); return 0; }

编写一个脚本来通过以正确顺序运行命令执行自动化测试。

import subprocess import argparse import time def run_commands(program, commands): print("⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞") for command in commands: command_str = " ".join(command) print(f"⮞⮞⮞ {command_str}") result = subprocess.run([program] + command, capture_output=True, text=True) time.sleep(1) if result.stdout: print("⮞⮞⮞ Output:") print(result.stdout) if result.stderr: print("Test Command Error:") print(result.stderr) print("⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞⮞") print("") def main(): parser = argparse.ArgumentParser(description="Run a series of commands to test a specified program.") parser.add_argument('program', help="The program to test, e.g., ./ex17") args = parser.parse_args() base_commands = [ ["db.dat", "l"], ["db.dat", "d", "2"], ["db.dat", "l"], ["db.dat", "g", "2"], ["db.dat", "g", "3"] ] if args.program == './ex17_3': commands = [ ["db.dat", "c", "32", "3"], ["db.dat", "s", "1", "zed", "zed@zedshaw.com", "10086"], ["db.dat", "s", "2", "frank", "frank@zedshaw.com", "10010"], ["db.dat", "s", "3", "joe", "joe@zedshaw.com", "10000"], ["db.dat", "s", "3", "joe", "joe@zedshaw.com", "10000"], ["db.dat", "f", "phone", "10000"], ] + base_commands elif args.program == './ex17_2': commands = [ ["db.dat", "c", "32", "3"], ["db.dat", "s", "1", "zed", "zed@zedshaw.com"], ["db.dat", "s", "2", "frank", "frank@zedshaw.com"], ["db.dat", "s", "3", "joe", "joe@zedshaw.com"], ["db.dat", "s", "3", "joe", "joe@zedshaw.com"], ] + base_commands else: commands = [ ["db.dat", "c"], ["db.dat", "s", "1", "zed", "zed@zedshaw.com"], ["db.dat", "s", "2", "frank", "frank@zedshaw.com"], ["db.dat", "s", "3", "joe", "joe@zedshaw.com"], ["db.dat", "s", "3", "joe", "joe@zedshaw.com"], ] + base_commands run_commands(args.program, commands) if __name__ == "__main__": main()

尝试重构程序,使用单一的全局变量来储存数据库连接。这个新版本和旧版本比起来如何?

加载文件会变得异常麻烦……

搜索“栈数据结构”,并且在你最喜欢的语言中实现它,然后尝试在C中实现。

#include <stdio.h> #include <stdlib.h> #include <stdbool.h> // 定义栈的最大容量 #define MAX 100 // 定义栈结构 typedef struct Stack { int items[MAX]; int top; } Stack; // 初始化栈 void initialize(Stack *s) { s->top = -1; } // 检查栈是否为空 bool isEmpty(Stack *s) { return s->top == -1; } // 检查栈是否已满 bool isFull(Stack *s) { return s->top == MAX - 1; } // 入栈操作 bool push(Stack *s, int value) { if (isFull(s)) { printf("Stack is full. Cannot push %d\n", value); return false; } s->items[++(s->top)] = value; return true; } // 出栈操作 int pop(Stack *s) { if (isEmpty(s)) { printf("Stack is empty. Cannot pop\n"); exit(EXIT_FAILURE); } return s->items[(s->top)--]; } // 获取栈顶元素 int peek(Stack *s) { if (isEmpty(s)) { printf("Stack is empty. Cannot peek\n"); exit(EXIT_FAILURE); } return s->items[s->top]; } // 主函数测试栈操作 int main() { Stack s; initialize(&s); push(&s, 10); push(&s, 20); push(&s, 30); printf("Top element is %d\n", peek(&s)); printf("Stack full: %s\n", isFull(&s) ? "true" : "false"); printf("Stack empty: %s\n", isEmpty(&s) ? "true" : "false"); printf("Popped element is %d\n", pop(&s)); printf("Top element is %d\n", peek(&s)); printf("Popped element is %d\n", pop(&s)); printf("Top element is %d\n", peek(&s)); printf("Popped element is %d\n", pop(&s)); printf("Stack empty: %s\n", isEmpty(&s) ? "true" : "false"); return 0; }
⮞ ./ex17_stack Top element is 30 Stack full: false Stack empty: false Popped element is 30 Top element is 20 Popped element is 20 Top element is 10 Popped element is 10 Stack empty: true

练习 18:函数指针

#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> /** Our old friend die from ex17. */ void die(const char *message) { if(errno) { perror(message); } else { printf("ERROR: %s\n", message); } exit(1); } // a typedef creates a fake type, in this // case for a function pointer typedef int (*compare_cb)(int a, int b); /** * A classic bubble sort function that uses the * compare_cb to do the sorting. */ int *bubble_sort(int *numbers, int count, compare_cb cmp) { int temp = 0; int i = 0; int j = 0; int *target = malloc(count * sizeof(int)); if(!target) die("Memory error."); memcpy(target, numbers, count * sizeof(int)); for(i = 0; i < count; i++) { for(j = 0; j < count - 1; j++) { if(cmp(target[j], target[j+1]) > 0) { temp = target[j+1]; target[j+1] = target[j]; target[j] = temp; } } } return target; } int sorted_order(int a, int b) { return a - b; } int reverse_order(int a, int b) { return b - a; } int strange_order(int a, int b) { if(a == 0 || b == 0) { return 0; } else { return a % b; } } /** * Used to test that we are sorting things correctly * by doing the sort and printing it out. */ void test_sorting(int *numbers, int count, compare_cb cmp) { int i = 0; int *sorted = bubble_sort(numbers, count, cmp); if(!sorted) die("Failed to sort as requested."); for(i = 0; i < count; i++) { printf("%d ", sorted[i]); } printf("\n"); free(sorted); } int main(int argc, char *argv[]) { if(argc < 2) die("USAGE: ex18 4 3 1 5 6"); int count = argc - 1; int i = 0; char **inputs = argv + 1; int *numbers = malloc(count * sizeof(int)); if(!numbers) die("Memory error."); for(i = 0; i < count; i++) { numbers[i] = atoi(inputs[i]); } test_sorting(numbers, count, sorted_order); test_sorting(numbers, count, reverse_order); test_sorting(numbers, count, strange_order); free(numbers); return 0; }

用十六进制编辑器打开ex18,接着找到函数起始处的十六进制代码序列,看看是否能在原始程序中找到函数。

在你的十六进制编辑器中找到更多随机出现的东西并修改它们。重新运行你的程序看看发生了什么。字符串是你最容易修改的东西。

./ex18 bash: ./ex18: cannot execute binary file: Exec format error

将错误的函数传给compare_cb,并看看C编辑器会报告什么错误。

⮞ make ex18 cc -Wall -g -c ex18.c -o ex18.o ex18.c: In function ‘main’: ex18.c:120:34: warning: passing argument 3 of ‘test_sorting’ from incompatible pointer type [-Wincompatible-pointer-types] 120 | test_sorting(numbers, count, nothing); | ^~~~~~~ | | | void (*)(int, int) ex18.c:78:55: note: expected ‘compare_cb’ {aka ‘int (*)(int, int)’} but argument is of type ‘void (*)(int, int)’ 78 | void test_sorting(int *numbers, int count, compare_cb cmp) | ~~~~~~~~~~~^~~ cc -o ex18 ex18.o ⮞ ./ex18 9 7 3 8 0 4 5 0 3 4 5 7 8 9 f3:0f:1e:fa:55:48:89:e5:89:7d:fc:89:75:f8:8b:45:fc:2b:45:f8:5d:c3:f3:0f:1e: 9 8 7 5 4 3 0 f3:0f:1e:fa:55:48:89:e5:89:7d:fc:89:75:f8:8b:45:f8:2b:45:fc:5d:c3:f3:0f:1e: 8 9 7 3 0 5 4 f3:0f:1e:fa:55:48:89:e5:89:7d:fc:89:75:f8:83:7d:fc:00:74:06:83:7d:f8:00:75: 0 7 3 8 4 5 9 f3:0f:1e:fa:55:48:89:e5:89:7d:fc:89:75:f8:90:5d:c3:f3:0f:1e:fa:55:48:89:e5:

NULL传给它,看看程序中会发生什么。然后运行Valgrind来看看它会报告什么。

⮞ make ex18 cc -Wall -g -c ex18.c -o ex18.o cc -o ex18 ex18.o ./ex18 9 7 3 8 0 4 5 0 3 4 5 7 8 9 f3:0f:1e:fa:55:48:89:e5:89:7d:fc:89:75:f8:8b:45:fc:2b:45:f8:5d:c3:f3:0f:1e: 9 8 7 5 4 3 0 f3:0f:1e:fa:55:48:89:e5:89:7d:fc:89:75:f8:8b:45:f8:2b:45:fc:5d:c3:f3:0f:1e: 8 9 7 3 0 5 4 f3:0f:1e:fa:55:48:89:e5:89:7d:fc:89:75:f8:83:7d:fc:00:74:06:83:7d:f8:00:75: Segmentation fault

编写另一个排序算法,修改test_sorting使它接收任意的排序函数和排序函数的比较回调。并使用它来测试两种排序算法。

#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> /** Our old friend die from ex17. */ void die(const char *message) { if (errno) { perror(message); } else { printf("ERROR: %s\n", message); } exit(1); } void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } // a typedef creates a fake type, in this // case for a function pointer typedef int (*compare_cb)(int a, int b); typedef int *(*sort_method)(int *numbers, int count, compare_cb cmp); /** * A classic bubble sort function that uses the * compare_cb to do the sorting. */ int *bubble_sort(int *numbers, int count, compare_cb cmp) { int i = 0; int j = 0; int *target = malloc(count * sizeof(int)); if (!target) die("Memory error."); memcpy(target, numbers, count * sizeof(int));