这是本人 笨方法学C的学习记录,仅供参考,如果有问题可以留言。
练习1附加题
1.
问:在你的文本编辑器中打开ex1文件,随机修改或删除一部分,之后运行它看看发生了什么。
答:运行不了。二进制代码被改了,机器无法识别
2.
问:再多打印5行文本或其他比Hello world更复杂的东西。
答:
#include<stdio.h>
int main(int argc, char *argv[])
{
puts("Hello world.");
puts("Hello world.");
puts("Hello world.");
puts("Hello world.");
puts("Hello world.");
puts("Hello world.");
return 0;
}
3.
问:执行man 3 puts来阅读这个函数和其他函数的文档
答:阅读puts的文档,可以发现puts函数输出文本和换行到标准输出
练习2附加题
1.
问:创建目标all:ex1,可以以单个命令make构建ex1。
答:在Makefile文件第一行加入all:ex1,表明目标是ex1。
2.阅读man make来了解关于如何执行它的更多信息。
3.
问:阅读man cc来了解关于-Wall和-g行为的更多信息。
答:
-Wall:显示所有的warning
-g:在生成是二进制文件中记录调试需要用的信息。
4.
问:在互联网上搜索Makefile文件,看看你是否能改进你的文件。
答:仿照第五题
CC = gcc
CFLAGS = -Wall
SOURCES = ex1.c
OBJECTS = $(SOURCES:.c=.o)
EXECUTABLE = ex1
all: $(EXECUTABLE)
$(EXECUTABLE): $(OBJECTS)
$(CC) $(CFLAGS) $(OBJECTS) -o $@
.c.o:
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJECTS) $(EXECUTABLE)
5.
问:在另一个C语言项目中找到Makefile文件,并且尝试理解它做了什么。
答:
CC = gcc
CFLAGS = -Wall
SOURCES = main.c utils.c
OBJECTS = $(SOURCES:.c=.o)
EXECUTABLE = myprogram
all: $(EXECUTABLE)
$(EXECUTABLE): $(OBJECTS)
$(CC) $(CFLAGS) $(OBJECTS) -o $@
.c.o:
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJECTS) $(EXECUTABLE)
解释:
CC被赋值为gcc,用以指示编译器名称
CFLAGS被赋值为-Wall,使编译时显示所有warning
SOURCES是源文件,即main.c和utils.c
OBJECTS在赋值时引用了SOURCES变量,所以加上了$()。冒号后面的.c=.o表明要将SOURCES中的.c后缀换成.o后缀后再赋给OBJECTS。
EXECUTABLE被赋值为最后的输出目标。
all:后面跟的是默认的目标
makefile中的目标和依赖通常是这个格式:
目标:依赖
(TAB) gcc -Wall 依赖 -o 目标
$ @在这代表指明的目标,而不是shell中的所有变量,因为makefile有一套自己的规则。顺带一提,下面的$<指代依赖文件。
.c.o:
是另外一种格式的目标和依赖,其使用规则如下:
依赖 目标:
(TAB)gcc -Wall 依赖 -o 目标
这种情况适用于多文件对应的时候,此时你不能写成目标:依赖 ,因为每个目标都有不同的依赖,而这个格式则会让makefile以为每个目标文件都依赖于所有的依赖文件。想要描述这种情况还可以用静态模式规则,即
%.o:%.c
这表示每个目标文件都依赖于相同名字的依赖文件。
练习3附加题
1.
问:找到尽可能多的方法使ex3崩溃。
答:我把printf里的整型变量删了就已经error了。
2.
问:执行man 3 printf来阅读其它可用的’%'格式化占位符。如果你在其它语言中使用过它们,应该看着非常熟悉(它们来源于printf)。
答:以下是常用的
\n 换行
\s 空格
\r 回车
%d 十进制decimalism
%o 八进制octonary
%f 浮点型(float、double)
%s 字符串(string)
至于里面加数字限制长度和小数点的,我就不赘述了,整型类的和浮点型的区别还是挺大的,大家想了解可以看这篇文章:
3.
问:将ex3添加到你的Makefile的all列表中。到目前为止,可以使用make clean all来构建你所有的练习。
答:参考回答4
4.
问:将ex3添加到你的Makefile的clean列表中。当你需要的时候使用make clean可以删除它。
答:
CC = gcc
1 CFLAGS = -Wall
2 SOURCES = ex1.c ex3.c
3 OBJECTS = $(SOURCES:.c=.o)
4 EXECUTABLE = ex1 ex3
5
6 all: $(EXECUTABLE)
7
8 ex1: ex1.o
9 $(CC) $(CFLAGS) $< -o $@
10
11 ex3: ex3.o
12 $(CC) $(CFLAGS) $< -o $@
13
14 .c.o:
15 $(CC) $(CFLAGS) -c $< -o $@
16
17 clean:
18 rm -f $(OBJECTS) $(EXECUTABLE)
练习4附加题
1.
问:按照上面的指导,使用Valgrind和编译器修复这个程序。
答:就是ex3.c
2.
问:在互联网上查询Valgrind相关的资料。
3.
问:下载另一个程序并手动构建它。尝试一些你已经使用,但从来没有手动构建的程序。
答:这个问题就交给家人们了。
4.
问:看看Valgrind的源码是如何在目录下组织的,并且阅读它的Makefile文件。不要担心,这对我来说没有任何意义。
答:太高估我了,除了一些赋值语句我基本上看不懂
练习5附加题
1.
问:对于每一行,写出你不理解的符号,并且看看是否能猜出它们的意思。在纸上写下你的猜测,你可以在以后检查它,看看是否正确。
答:不懂的就gpt。
2.
问:回头去看之前几个练习的源代码,并且像这样分解代码,来看看你是否了解它们。写下你不了解和不能自己解释的东西。
答:这几个问题都看自己把
练习6附加题
1.
问:寻找其他通过修改printf使这段C代码崩溃的方法。
答:这可就太多了,忘记写括号,printf的数据与设计的不对应;等等等等。让程序崩溃不是我天天干的事吗hh。
2.
问:搜索“printf格式化”,试着使用一些高级的占位符。
答:练习3有个连接,这个链接做补充
可以发现,m.n格式大部分都是左补空格,只有%.nd和%0nd是左补0。
3.
问:研究可以用几种方法打印数字。尝试以八进制或十六进制打印,或者其它你找到的方法。
答:①puts(char * string)用来输出字符串的;
4.
问:试着打印空字符串,即""
答:可以打印空字符串,但是不可以打印空字符,会报错。
练习7附加题
1.
问:把为universe_of_defects赋值的数改为不同的大小,观察编译器的警告
答:
①如果数字太大:
//提示溢出
ex7.c:9:76: warning: integer overflow in expression of type ‘long int’ results in ‘0’ [-Woverflow]
9 | long universe_of_defects = 1024L * 1024L * 1024L * 1024L *1024L *1024L * 1024L * 1024L * 1024L * 1024L * 1024L;
| ^
2.
问:这些巨大的数字实际上打印成了什么?
答:被打印成了0。
3.
问:将long改为unsigned long,并试着找到对它来说太大的数字。
答:多乘几个1024就行了。
4.
问:上网搜索unsigned做了什么。
答:无符号数。
5.
问:试着自己解释(在下个练习之前)为什么char可以和int相乘。
答:char是按ASCII存储的,都是数字没有理由不能乘吧?
练习8
如何使它崩溃
①将full_name最后的’\0’去掉,并重新运行它,在valgrind下再运行一遍。现在将full_name的定义从main函数中移到它的上面,尝试在Valgrind下运行它来看看是否能得到一些新的错误。有些情况下,你会足够幸运,不会得到任何错误。
答:以下是输出结果
The size of an int: 4
The size of areas (int[]): 20
The number of ints in areas: 5
The first area is 10, the 2nd 12.
The size of a char: 1
The size of name (char[]): 4
The number of chars: 4
The size of full_name (char[]): 11
The number of chars: 11
name="Zed" and full_name="Zed A. Shaw" ^
ps:理论上来说应该是不能正确执行的,因为字符串数组没有以’\0’结尾(长度只有11位),用printf打印输出时因为没有\0而不知道结尾在哪,出现乱码;但这里没报错,可能是当前版本的gcc编译器对其做了处理,也许换个编译器就会报错了。有时间的同学可以在DEV C++试试。当然,我不觉得这是一种幸运,这是编写程序的时候必须注意的规范。
②将areas[0]改为areas[10]并打印,来看看Valgrind会输出什么。
答:
1 #include <stdio.h>
1
2 char full_name[] = {
3 'Z', 'e', 'd',
4 ' ', 'A', '.', ' ',
5 'S', 'h', 'a', 'w'
6 };
7 int main(int argc, char *argv[])
8 {
9 int areas[] = {10, 12, 13, 14, 20};
10 char name[] = "Zed";
11
12 // WARNING: On some systems you may have to change the
13 // %ld in this code to a %u since it will use unsigned ints
14 printf("The size of an int: %ld\n", sizeof(int));
15 printf("The size of areas (int[]): %ld\n",
16 sizeof(areas));
17
18 printf("The number of ints in areas: %ld\n",
19 sizeof(areas) / sizeof(int));
20 printf("The first area is %d, the 2nd %d.\n",
21 areas[10], areas[1]);
22
23 printf("The size of a char: %ld\n", sizeof(char));
24 printf("The size of name (char[]): %ld\n",
25 sizeof(name));
26 printf("The number of chars: %ld\n",
27 sizeof(name) / sizeof(char));
28 sizeof(name) / sizeof(char));
29
30 printf("The size of full_name (char[]): %ld\n",
31 sizeof(full_name));
32 printf("The number of chars: %ld\n",
33 sizeof(full_name) / sizeof(char));
34
35 printf("name=\"%s\" and full_name=\"%s\"\n",
36 name, full_name);
37
38 return 0;
39 }
输出结果:
The size of an int: 4
The size of areas (int[]): 20
The number of ints in areas: 5
The first area is -10314352, the 2nd 12.
The size of a char: 1
The size of name (char[]): 4
The number of chars: 4
The size of full_name (char[]): 11
The number of chars: 11
name="Zed" and full_name="Zed A. Shaw"
③尝试上述操作的不同变式,也对name和full_name执行一遍。
答:添加一个printf name
printf("the last number of name: %c\n",name[9]);
输出结果:
mber of areas: 0,0,0,0,0
The size of a char: 1
The size of name (char[]): 10
The number of chars: 10
this is name:Zed
The size of full_name (char[]): 15
The number of chars: 15
name="Zed" and full_name="Zed A. Shaw"
the last number of name: �
附加题
1.
问:尝试使用areas[0] = 100;以及相似的操作对areas的元素赋值。
答:数组赋值的方法主要有:
①
int areas[10] = {10, 12, 13, 14, 20}; //整型数组
char full_name[15] = { //字符数组
'Z', 'e', 'd',
' ', 'A', '.', ' ',
'S', 'h', 'a', 'w'
};
②
char name[10] = "Zed"; //字符数组
③按单个位置赋值
areas[0] = 100; //一般放在循环里赋值
2.
问:尝试对name和full_name的元素赋值。
答:同上
3.
问:尝试将areas的一个元素赋值为name中的字符。因为字符是按照ASCII码值来存储数据的,ASCII码值有上限。
答:在 C 语言中,char 类型的大小通常是 1 字节,范围通常是 -128 到 127(对于有符号 char)或者 0 到 255(对于无符号 char)。当你尝试将一个超过 char 类型范围的值(例如 10086)赋给一个 char 变量时,发生的是 溢出,即该值会被截断到 char 能容纳的范围内。
如果我将full_name这样赋值:
char full_name[] = {
10086, 'e', 'd',
' ', 'A', '.', ' ',
'S', 'h', 'a', 'w'
};
10086 % 256 = 102
f正好对应102
输出的full_name="fed A. Shaw"
4.
问:上网搜索在不同的CPU上整数所占的不同大小
答:32,64位的电脑,整型一般都是32位的,我理解的是32位足以应付多数情况,如果需要64位的,可以使用长整型。对于更小位数的电脑,一般int的位数跟随电脑CPU的位数。
练习9
如何使它崩溃
①删掉name的初始化表达式。
gcc不报错,valgrind报错。
Conditional jump or move depends on uninitialised value(s)
②将数据赋过长的值,gcc报警告,valgrind不报错。(char name[4] = {‘a’,‘a’,‘a’,‘a’,‘a’};)
warning: excess elements in array initializer
附加题
1.
问:将一些字符赋给numbers的元素,之后用printf一次打印一个字符,你会得到什么编译器警告?
答:
#如果是 int numbers[4] = {a,5,6,7};
#这是无效声明,会报错
error: ‘a’ undeclared (first use in this function)
#如果是 int numbers[4] = {'a',5,6,7};
#不会报错,执行后输出97,对应a的ascii码。
numbers: 97 5 6 7
2.
问:对names执行上述的相反操作,把names当成int数组,并一次打印一个int,Valgrind会提示什么?
答:类似练习八附加题第三问,存储的是数字,printf的是对应的ascii码值。
将输出改为%d,输出的就是数字了。因此可以下一个浅显的结论。不论是何种数组,存储的时候都是以数字的形式存储,无非是输出的时候按照你的需要以%d,%c,%s的形式输出,单位置字长不同,字符数组需要以\0结尾来判断长度而已。
3.
问:有多少种其它的方式可以用来打印它?
答:%s肯定不行,识别不到\0。
4.
问:如果一个字符数组占四个字节,一个整数也占4个字节,你可以像整数一样使用整个name吗?你如何用黑魔法实现它?
答:
①第一种方法,把字符指针强制转化为int型指针intPtr,此时*intPtr对应的就是int型数组对应的值’ABCD‘,即0x44434241。
char name[4] = "ABCD";
int *intPtr = (int*)name;
int intValue = *intPtr;
②第二种方法,循环按位或。以下答案出自GPT,经过测试没有问题。
char name[4] = "ABCD";
int intValue = 0;
for (int i = 4; i > 0; i--) {
intValue = (intValue << 8) | name[i];
}
5.
问:将name转换成another的形式,看看代码是否能正常工作。
答:不行,指针形式的数组赋值只能采用如下形式:
char *name = "abcd";
练习10
如何使它崩溃
1.
问:将i初始化为0看看会发生什么。是否也需要改动argc,不改动的话它能正常工作吗?为什么下标从o开始可以正常工作?
答:可以正常工作,但是printf(“arg %d: %s\n”, i, argv[i]);这句话不会被循环。
2.
问:将num_states改为错误的值使他变大,来看看会发生什么。
答:gcc显示分段错误,valgrind显示无效读取。读取超出数组长度了。
附加题
2.
问:查询如何使用’,‘(逗号)字符来在for循环的每一部分中,’;'(分号)之间分隔多条语句。
答:在for循环语句里,','起划分语句的作用,相当于平常写程序时候的分号。
3.
问:查询NULL是什么东西,尝试将它用做states的一个元素,看看它会打印出什么。
答:NULL是空指针,表示指针不指向任何东西。
尝试打印会输出如下结果:
state 3: (null)
4.
问:看看你是否能在打印之前将states的一个元素赋值给argv中的元素,再试试相反的操作。
答:
①“打印之前将states的一个元素赋值给argv中的元素”
不可以。c是按照顺序执行的,states都还没创造呢怎么给argv赋值。
②“打印之前将argv的一个元素赋值给中states的元素”
可以。理由同①。
练习11
附加题
1.
问:让这些循环倒序执行,通过使用i–从argc开始递减直到0。你可能需要做一些算数操作让数组的下标正常工作。
答:可以正常工作,但是printf(“arg %d: %s\n”, i, argv[i]);这句话不会被循环。
#修改后的程序
1 #include <stdio.h>
2
3 int main(int argc, char *argv[])
4 {
5 // go through each string in argv
6
7 int i = argc;
8 while(i > 0) {
9 i--;
10 printf("arg %d: %s\n", i, argv[i]);
11 }
12
13 // let's make our own array of strings
14 char *states[] = {
15 "California", "Oregon",
16 "Washington", "Texas"
17 };
18
19 int num_states = 4;
20 i = num_states; // watch for this
21 while(i > 0) {
22 i--;
23 printf("state %d: %s\n", i, states[i]);
24 }
25
26 return 0;
27 }
执行:
./ex11.exe a b
输出:
arg 2: b
arg 1: a
arg 0: ./ex11.exe
state 3: Texas
state 2: Washington
state 1: Oregon
state 0: California
2.
问:使用while循环将argv中的值复制到states。
答:按已学的知识,答案如下应该:
1 #include <stdio.h>
2
3 int main(int argc, char *argv[])
4 {
5 // go through each string in argv
6
7 int i = argc;
8 while(i > 0) {
9 i--;
10 printf("arg %d: %s\n", i, argv[i]);
11 }
12
13 // let's make our own array of strings
14 char *states[10];
15
16 int num_states = argc;
17 i = num_states; // watch for this
18 while(i > 0) {
19 i--;
20 states[i] = argv[i];
21 printf("state %d: %s\n", i, states[i]);
22 }
23
24 return 0;
25 }
但实际上你可以注意到一个问题,states[i]接收到的仅仅只是地址而已,也就是说,则并不能叫复制,只是让states[i]也指向了argv[i]指向的元素。因此在程序上我做如下修改:
1 #include <stdio.h>
2 #include <string.h>
3 #include <stdlib.h>
4
5 int main(int argc, char *argv[])
6 {
7 // go through each string in argv
8
9 int i = argc;
10 while(i > 0) {
11 i--;
12 printf("arg %d: %s\n", i, argv[i]);
13 }
14
15 // let's make our own array of strings
16 char *states[argc];
17 int malloc_length = 0
18 for(int j = 0; j < argc; j++){
19 malloc_length += (strlen(argv[j]) + 1);
20 }
21 states[0] = (char *)malloc(malloc_length);
22 char *temp_states = states[0];
23
24 for(int i = 0; i < argc; i++) {
25 strcpy(temp_states ,argv[i]);
26 printf("state %d: %s\n", i, temp_states);
27 temp_states += strlen(argv[i])+1;
28 }
29 free(states[0]);
30
29 return 0;
30 }
使用一个字符串复制函数strcpy,它存在于string.h头文件中,其功能是将第二个地址上的字符串复制到第一个地址处。
这里使用一个malloc来给字符串数组开辟存储空间,它属于stdlib.h头文件。strlen函数会计算每个元素的字节数,+1是补充\0的长度,因为strlen不会将其计算在内(不能用sizeof,它指代的是变量类型的存储空间大小)。malloc创建的是viod型指针,需要强制类型转换成char *型指针。
3.
问:让这个复制循环不会执行失败,即使argv之中有很多元素也不会全部放进states。
答:程序如下:
1 #include <stdio.h>
2 #include <string.h>
3 #include <stdlib.h>
4
5 int main(int argc, char *argv[])
6 {
7 // go through each string in argv
8
9 int i = argc;
10 while(i > 0) {
11 i--;
12 printf("arg %d: %s\n", i, argv[i]);
13 }
14
15 // let's make our own array of strings
argc = (argc >=4 ? 4 : argc);
16 char *states[argc];
17 int malloc_length = 0
18 for(int j = 0; j < argc; j++){
19 malloc_length += (strlen(argv[j]) + 1);
20 }
21 states[0] = (char *)malloc(malloc_length);
22 char *temp_states = states[0];
23
24 for(int i = 0; i < argc; i++) {
25 strcpy(temp_states ,argv[i]);
26 printf("state %d: %s\n", i, temp_states);
27 temp_states += strlen(argv[i])+1;
28 }
29 free(states[0]);
30
29 return 0;
30 }
将states限定四个字符串。(argc >=4 ? 4 : argc)防止没有数据的时候printf空元素报错。
4.
问:研究你是否真正复制了这些字符串。答案可能会让你感到意外和困惑
答:详见第二问。
练习12
附加题
1.
问:我已经向你简短地介绍了 && ,它执行“与”操作。上网搜索与之不同的“布尔运算符”。
答:主要还是 &&、||、&、|这些。
3.
问:回到练习10和11,使用if语句使循环提前退出。你需要break语句来实现它,搜索它的有关资料。
答:练习10程序如下,在printf中加入了if判断break;练习11类似。
#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]);
if(i == 2){
break;
}
}
return 0;
}
4.
问:第一个判断所输出的话真的正确吗?由于你的“第一个参数”不是用户输入的第一个参数,把它改正。
答:输入的第一个参数就是命令本身。判断时增加1即可。
#include <stdio.h>
int main(int argc, char *argv[])
{
int i = 0;
if(argc == 2) {
printf("You only have one argument. You suck.\n");
} else if(argc > 2 && argc < 5) {
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;
}
练习13
附加题
1.
问:编写另一个程序,在字母上做算术运算将它们转换为小写,并且在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];
if(letter >= 'A' && letter <= 'Z'){
letter += 32;
}
switch(letter) {
case 'a':
printf("%d: 'a'\n", i);
break;
case 'e':
printf("%d: 'e'\n", i);
break;
case 'i':
printf("%d: 'i'\n", i);
break;
case 'o':
printf("%d: 'o'\n", i);
break;
case 'u':
printf("%d: 'u'\n", i);
break;
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;
}
2.
问:使用’,'(逗号)在for循环中初始化letter。
答:程序如下
#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;
char letter;
for(i = 0, letter = argv[1][0]; letter != '\0'; letter = argv[1][i++]) {
if(letter >= 'A' && letter <= 'Z'){
letter += 32;
}
switch(letter) {
case 'a':
printf("%d: 'a'\n", i);
break;
case 'e':
printf("%d: 'e'\n", i);
break;
case 'i':
printf("%d: 'i'\n", i);
break;
case 'o':
printf("%d: 'o'\n", i);
break;
case 'u':
printf("%d: 'u'\n", i);
break;
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;
}
3.
问:使用另一个for循环来让它处理你传入的所有命令行参数。
答:加入一个j的for循环。
#include <stdio.h>
int main(int argc, char *argv[])
{
if(argc == 1) {
printf("ERROR: You need at least one argument.\n");
// this is how you abort a program
return 1;
}
int i, j;
char letter;
for(j = 1; j < argc; j++){
for(i = 0, letter = argv[j][0]; letter != '\0'; letter = argv[j][i++]) {
if(letter >= 'A' && letter <= 'Z'){
letter += 32;
}
switch(letter) {
case 'a':
printf("%d: 'a'\n", i);
break;
case 'e':
printf("%d: 'e'\n", i);
break;
case 'i':
printf("%d: 'i'\n", i);
break;
case 'o':
printf("%d: 'o'\n", i);
break;
case 'u':
printf("%d: 'u'\n", i);
break;
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;
}
4.
问:将这个switch语句转为if语句,你更喜欢哪个呢?
答:改成if后如下,喜欢哪个不用我多说了吧。
if(letter == 'a')
printf("%d: 'a'\n", i);
else if(letter == 'e')
printf("%d: 'e'\n", i);
else if(letter == 'i')
printf("%d: 'i'\n", i);
else if(letter == 'O' )
printf("%d: 'o'\n", i);
else if(letter == 'u')
printf("%d: 'u'\n", i);
else if(letter == 'y' || i >2)
printf("%d: 'y'\n", i);
else
printf("%d: %c is not a vowel\n", i, letter);
5.
问:在“Y”的例子中,我在if代码块外面写了个break。这样会产生什么效果?如果把它移进if代码块,会发生什么?自己试着解答它,并证明你是正确的。
答:如果放进if里,那只有i > 2的时候才会执行break;会导致在i <=2的时候,如果是y或者Y,没有break语句,会向下执行default中的内容。
练习14
附加题
1.
问:重新编写这些函数,使它们的数量减少。比如,你真的需要can_print_it吗?
答:程序如下:
#include <stdio.h>
#include <ctype.h>
// forward declarations
void print_arguments(int argc, char *argv[])
{
int i = 0;
for(i = 0; i < argc; i++) {
for(int 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;
}
2.
问:使用strlen函数,让print_arguments知道每个字符串参数都有多长,之后将长度传入print_letters。然后重写print_letters,让它只处理固定的长度,不按照’\0’终止符。你需要#include <string.h>来实现它。
答:程序如下
#include <stdio.h>
#include <ctype.h>
#include <string.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] ,strlen(argv[i]));
}
}
void print_letters(char arg[] ,int letter_len)
{
int i = 0;
for(i = 0; i < letter_len; 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;
}
3.
问:使用man来查询isalpha和isblank的信息。使用其它相似的函数来只打印出数字或者其它字符。
答:isdigit函数检查是不是数字,是数字返回非0,不是返回0。
4.
问:上网浏览不同的人喜欢什么样的函数格式。永远不要使用“K&R”语法,因为它过时了,而且容易使人混乱,但是当你碰到一些人使用这种格式时,要理解代码做了什么。
答:通常都用ANSI C风格的函数格式,也就是我们平常教学使用的。
练习15
大多数情况下,除了sizeof、&操作和声明之外,指针和数组在使用上没有明显区别。
附加题
6.
问:在程序末尾添加一个for循环,打印出这些指针所指向的地址。你需要在printf中使用%p。
答:
#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("%p lived %p years so far.\n",
*cur_name, *cur_age);
}
// print the ptr of the name and the age
for(i = 0; i < count; i++) {
printf("%p is %p years old again.\n",
cur_name[i], &cur_age[i]);
}
return 0;
}
7.
问:对于每一种打印数组的方法,使用函数来重写程序。试着向函数传递指针来处理数据。记住你可以声明接受指针的函数,但是可以像数组那样用它。
答:
#include <stdio.h>
void print_age( int *cur_age ,char **cur_name ,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);
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");
//other way to printf.
print_age(ages, names, count);
return 0;
}
//print_age函数:包含了剩下三种打印方法
void print_age(int *cur_age, char **cur_name ,int count){
int j = 0;
// second way using pointers
for(j = 0; j < count; j++) {
printf("%s is %d years old.\n",
*(cur_name+j), *(cur_age+j));
}
printf("---\n");
// third way, pointers are just arrays
for(j = 0; j < count; j++) {
printf("%s is %d years old again.\n",
cur_name[j], cur_age[j]);
}
printf("---\n");
char **four_name;
int *four_age;
// fourth way with pointers in a stupid complex way
for(four_name = cur_name,four_age = cur_age;
(four_age - cur_age) < count;
four_name++, four_age++)
{
printf("%s lived %d years so far.\n",
*four_name, *four_age);
}
}
8.
问:将for循环改为while循环,并且观察对于每种指针用法哪种循环更方便。
答:随便举一个例子,不难发现,for循环可以容纳的初始条件,循环后条件,这都是while循环所没有的。所以一般情况下都用for。至于对指针用法的影响倒是没有发现有什么优势。
int i = 0;
while(i < count){
printf("%s has %d years alive.\n",names[i], ages[i]);
i++;
}
练习16
附加题
问:在这个练习的附加题中我想让你尝试一些有难度的东西:将这个程序改为不用指针和malloc的版本。这可能很困难,所以你需要研究下面这些东西:
- 如何在栈上创建结构体,就像你创建任何其它变量那样。
- 如何使用x.y而不是x->y来初始化结构体。
- 如何不使用指针来将结构体传给其它函数。
答:在这里还是使用了strdup在堆上分配,仍需要free。如果想在栈上分配,最好是定义结构体时就定义字符数组而不是指针。
#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;
who.name = strdup(name);
who.age = age;
who.height = height;
who.weight = weight;
return 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);
}
void Person_destroy(struct Person who)
{
free(who.name);
}
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);
return 0;
}
练习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; //数据id
int set; //数据状态
char name[MAX_DATA]; //数据1:name
char email[MAX_DATA]; //数据2:email
};
struct Database {
struct Address rows[MAX_ROWS]; //最大堆栈深度:100
};
struct Connection { //struct Connection这个结构体用于将文件和堆栈结合起来,方便查找
FILE *file;
struct Database *db;
};
void die(const char *message)
{
if(errno) { //检查一下errno,如果非0则说明有错误,perror输出错误以及message;
perror(message);
} else { //如果errno没有问题,则输出ERROR:message,
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) //根据file初始化db指针指向的结构体数组
{
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) //为conn和db两个结构体指针指向的结构体开辟一片存储区域,并根据file文件初始化。
{
struct Connection *conn = malloc(sizeof(struct Connection)); //开辟区域给结构体指针conn
if(!conn) die("Memory error");
conn->db = malloc(sizeof(struct Database)); //开辟区域给结构体指针db
if(!conn->db) die("Memory error");
if(mode == 'c') {
conn->file = fopen(filename, "w"); //如果mode是create,检查一下*filename的可写性
} else {
conn->file = fopen(filename, "r+"); //如果mode是其他的,检查一下*filename的读写性
if(conn->file) {
Database_load(conn); //其他模式下如果文件可读写,则将file文件的部分内容读取至database
}
}
if(!conn->file) die("Failed to open the file"); //如果file指针是NULL,则输出错误
return conn;
}
void Database_close(struct Connection *conn) //关闭file,释放内存
{
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); //把db指向的经过修改的结构体成员rows[i]写回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++) { //给Database结构体中的每一个数组元素(也是结构体)赋id和set初值
// 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) //根据提供的id name email 写入结构体rows[id]
{
struct Address *addr = &conn->db->rows[id]; //addr指向结构体rows[id]
if(addr->set) die("Already set, delete it first"); //set的作用就是判断有无数据
addr->set = 1;
// WARNING: bug, read the "How To Break It" and fix this
char *res = strncpy(addr->name, name, MAX_DATA); //字符串复制,返回值是addr->name的地址
// demonstrate the strncpy bug
if(!res) die("Name copy failed"); //res为NULL则报错
res = strncpy(addr->email, email, MAX_DATA); //字符串复制,返回值是addr->email的地址
if(!res) die("Email copy failed"); //res为NULL则报错
}
void Database_get(struct Connection *conn, int id) //打印输出rows[id]
{
struct Address *addr = &conn->db->rows[id]; //取rows[id]的地址赋给指针addr
if(addr->set) {
Address_print(addr); //如果rows[id]的set是1,则打印该rows[id]的name id email
} else {
die("ID is not set"); //如果set为0,说明该rows[id]未设置,退出
}
}
void Database_delete(struct Connection *conn, int id) //根据id清空name、email等信息
{
struct Address addr = {.id = id, .set = 0};
conn->db->rows[id] = addr;
}
void Database_list(struct Connection *conn) //只要set为1,代表这个rows[id]写入过,就printf信息
{
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]"); //argc<3则提前终止程序,即:argc至少要为3(至少要这两个参数:<dbfile文件名> <action执行什么操作>)
char *filename = argv[1];
char action = argv[2][0];
struct Connection *conn = Database_open(filename, action); //Database_open 刨一块区域给结构体指针conn,并根据file文件初始化
int id = 0;
if(argc > 3) id = atoi(argv[3]); //如果不止3个参数,则取第三个参数为id
if(id >= MAX_ROWS) die("There's not that many records.");
switch(action) {
case 'c': //create,初始化一片区域
Database_create(conn); //给conn->db结构体中的每一个数组元素rows[i](也是结构体)赋id和set = 0;
Database_write(conn); //把db指向的经过修改的结构体成员rows[i]写回conn->file
break;
case 'g': //get,用于打印输出
if(argc != 4) die("Need an id to get"); //这个操作要5个参数,<dbfile> <action>以及id
Database_get(conn, id); //打印输出rows[id]
break;
case 's': //set,设置name和email
if(argc != 6) die("Need id, name, email to set"); //这个操作要5个参数:<dbfile> <action> id name email
Database_set(conn, id, argv[4], argv[5]); //根据提供的id name email 写入结构体rows[id]
Database_write(conn); //把db指向的经过修改的结构体成员rows[i]写回conn->file
break;
case 'd': //delete
if(argc != 4) die("Need id to delete"); //delete需要3个参数:<dbfile> <action> id;
Database_delete(conn, id); //delete主要是根据id将set复位,感觉实际上此位置还有数据,但因为set为0,已经是无用数据了
Database_write(conn); //同步至file
break;
case 'l':
Database_list(conn); //printf已经写入过(set为1)的结构体的信息
break;
default:
die("Invalid action, only: c=create, g=get, s=set, d=del, l=list");
}
Database_close(conn); //关闭file,释放内存
return 0;
}
库函数解释
- fopen
FILE *fopen(const char *filename, const char *mode);
答:fopen用于打开一个文件。第一个参数为文件名,第二个参数是打开的模式:表示对文件的操作,可以是 “r”(只读),“w”(写入),“a”(追加),“r+”(读写),“w+”(读写,如果文件存在则截断为零长度),“a+”(读写,如果文件存在则在末尾追加)等。
fopen会在堆上分配一些地址以存储文件相关信息(大约几百k),并返回文件指针。
- fread
size_t fread(const void *ptr, size_t size, size_t nmemb, FILE *stream);
答:从文件中读取size*nmemb大小的内容到ptr指针指向的内存块中。
- fwrite
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
答:从ptr指针指向的内存块中读取size*nmemb大小的内容到文件中。
- fflush
int fflush(FILE *stream);
fflush 用于确保数据被及时写入文件,而不是缓冲区。(通常当数据被写入文件时,它通常首先被存储在内存缓冲区中,而不是立即写入磁盘。)
- exit
void exit(int status);
答:用于中止程序的执行。非0状态就退出程序。
- memset
void *memset(void *s, int c, size_t n);
答:将s指向的位置连续n个存储单元初始化为c。
- rewind
void rewind(FILE *stream);
答:刷新文件指针位置(因为fwrite或者fgetc等函数会改变文件指针位置)。
如何使它崩溃
1.
问:这个程序中有个bug,因为strncpy有设计缺陷。查询strncpy的相关资料,然后试着弄清楚如果name或者address超过512个字节会发生什么。可以通过简单把最后一个字符设置成’\0’来修复它,你应该无论如何都这样做(这也是函数原本应该做的)。
答:这个问题是在问这一行:
char *res = strncpy(addr->name, name, MAX_DATA);
查询strncpy的定义,会发现它是纯粹的字符复制,如果name数组的位数少于MAX_DATA,addr->name的末尾会被补充MAX_DATA-name个’\0’;如果name的位数大于MAX_DATA,那*addr->name末尾不会有’\0’,这会导致字符串的结构不完整。总之,这个函数需要人为填充’\0’。
修改后的程序如下:
char *res = strncpy(addr->name, name, MAX_DATA -1);
*(addr->name + MAX_DATA -1) = '\0';
附加题
1.
问:die函数需要接收conn变量作为参数,以便执行清理并关闭它。
答:这是修改后的函数。
void die(const char *message ,struct Connection *conn)
{
if(errno) { //检查一下errno,如果非0则说明有错误,perror输出错误以及message;
perror(message);
} else { //如果errno没有问题,则输出ERROR:message,
printf("ERROR: %s\n", message);
}
Database_close(conn);
exit(1); //一旦进入这个函数,横竖都会输出错误且退出。
}
注意,需要在前面声明 Database_close函数,因为它定义在die函数后面;
其次,所有的die函数引用处都要修改为die(const char *message ,struct Connection *conn)形式;
最后,主函数的第一行应该修改为
if(argc < 3) printf("ERROR: USAGE: ex17 <dbfile> <action> [action params]\n");
2.问:修改代码,使其接收参数作为MAX_DATA和MAX_ROWS,将它们储存在Database结构体中,并且将它们写到文件。这样就可以创建任意大小的数据库。
3.问:向数据库添加更多操作,比如find。
5.问:向Address添加一些字段,使它们可被搜索。
答:经修改的程序如下:
其实有很多bug,比如create,可以对同一个文件名进行操作。这理论上是不允许的。
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
struct Address {
int id; //数据id
int set; //数据状态
char *name; //数据1:name
char *email; //数据2:email
char *address; //数据3:address
};
struct Database {
int max_data;
int max_rows;
struct Address *rows; //最大堆栈深度取决于输入的MAX_ROWS
};
struct Connection { //struct Connection这个结构体用于将文件和堆栈结合起来,方便查找
FILE *file;
struct Database *db;
};
void Database_close(struct Connection *conn);
void die(const char *message ,struct Connection *conn)
{
if(errno) { //检查一下errno,如果非0则说明有错误,perror输出错误以及message;
perror(message);
} else { //如果errno没有问题,则输出ERROR:message,
printf("ERROR: %s\n", message);
}
Database_close(conn);
exit(1); //一旦进入这个函数,横竖都会输出错误且退出。
}
void Address_print(struct Address *addr)
{
printf("%d %s %s %s\n",
addr->id, addr->name, addr->email, addr->address);
}
void Database_load(struct Connection *conn) //根据file初始化db指针指向的结构体数组
{
int rc0 = fread(&conn->db->max_data, sizeof(int), 1, conn->file); //前两个参数是max_data和max_rows
int rc1 = fread(&conn->db->max_rows, sizeof(int), 1, conn->file);
if(rc0 != 1 || rc1 != 1) die("Failed to load max_data and max_rows to database." ,conn);
for(int i =0; i< conn->db->max_rows; i++){
int rc2 = fread(&conn->db->rows[i].id, sizeof(int), 1, conn->file);
int rc3 = fread(&conn->db->rows[i].set, sizeof(int), 1, conn->file); //把db指向的经过修改的结构体成员rows[i]写回conn->file
int rc4 = fread(conn->db->rows[i].name, conn->db->max_data, 1, conn->file);
int rc5 = fread(conn->db->rows[i].email, conn->db->max_data, 1, conn->file);
int rc6 = fread(conn->db->rows[i].address, conn->db->max_data, 1, conn->file);
if(rc2 != 1 || rc3 != 1 || rc4 != 1 || rc5 != 1 || rc6 != 1) die("Failed to load database." ,conn);
}
}
struct Connection *Database_open(const char *filename, char mode, int max_data, int max_rows) //为conn和db两个结构体指针指向的结构体开辟一片存储区域,并根据file文件初始化。
{
struct Connection *conn = malloc(sizeof(struct Connection)); //开辟区域给结构体指针conn
if(!conn) die("Memory error" ,conn);
conn->db = malloc(sizeof(struct Database)); //开辟区域给结构体指针db
if(!conn->db) die("Memory error" ,conn);
conn->db->max_data = max_data;
conn->db->max_rows = max_rows;
conn->db->rows = malloc(max_rows * sizeof(struct Address)); //开辟区域给所有rows[i]
if(!conn->db->rows) die("Memory error" ,conn);
for(int j = 0; j < max_rows ; j++){
conn->db->rows[j].name = malloc(max_data); //开辟区域给每个rows[i]的name、email以及address
conn->db->rows[j].email = malloc(max_data);
conn->db->rows[j].address = malloc(max_data);
if(!conn->db->rows[j].name || !conn->db->rows[j].email || !conn->db->rows[j].address) die("Memory error" ,conn);
}
if(mode == 'c') {
conn->file = fopen(filename, "w"); //如果mode是create,检查一下*filename的可写性
} else {
conn->file = fopen(filename, "r+"); //如果mode是其他的,检查一下*filename的读写性
if(conn->file) {
Database_load(conn); //其他模式下如果文件可读写,则将file文件的部分内容读取至database
}
}
if(!conn->file) die("Failed to open the file" ,conn); //如果file指针是NULL,则输出错误
return conn;
}
void Database_close(struct Connection *conn) //关闭file,释放内存,先malloc的后释放
{
if(conn) {
for(int i = 0; i < conn->db->max_rows ; i++){
free(conn->db->rows[i].name);
free(conn->db->rows[i].email);
free(conn->db->rows[i].address);
}
if(conn->db->rows) free(conn->db->rows);
if(conn->file) fclose(conn->file);
if(conn->db) free(conn->db);
free(conn);
}
}
void Database_write(struct Connection *conn)
{
rewind(conn->file);
int rc0 = fwrite(&conn->db->max_data, sizeof(int), 1, conn->file); //将max_data和max_rows写入文件
int rc1 = fwrite(&conn->db->max_rows, sizeof(int), 1, conn->file);
if(rc0 != 1 || rc1 != 1) die("Failed to write max_data and max_rows to database." ,conn);
for(int i =0; i< conn->db->max_rows; i++){
int rc2 = fwrite(&conn->db->rows[i].id, sizeof(int), 1, conn->file);
int rc3 = fwrite(&conn->db->rows[i].set, sizeof(int), 1, conn->file); //把需要存储的参数写回conn->file
int rc4 = fwrite(conn->db->rows[i].name, conn->db->max_data, 1, conn->file);
int rc5 = fwrite(conn->db->rows[i].email, conn->db->max_data, 1, conn->file);
int rc6 = fwrite(conn->db->rows[i].address, conn->db->max_data, 1, conn->file);
if(rc2 != 1 || rc3 != 1 || rc4 != 1 || rc5 != 1 || rc6 != 1) die("Failed to write database." ,conn);
}
int rc7 = fflush(conn->file); //将缓冲区的东西直接写入文件
if(rc7 == -1) die("Cannot flush database." ,conn);
}
void Database_create(struct Connection *conn)
{
int i = 0;
for(i = 0; i < conn->db->max_rows; i++) { //初始化Database结构体中的每一个数组元素(也是结构体)
// make a prototype to initialize it
conn->db->rows[i].id= i;
conn->db->rows[i].set = 0;
memset(conn->db->rows[i].name, 0, conn->db->max_data);
memset(conn->db->rows[i].email, 0, conn->db->max_data);
memset(conn->db->rows[i].address, 0, conn->db->max_data);
}
}
void Database_set(struct Connection *conn, int id, const char *name, const char *email, const char *address) //根据提供的id name email 写入结构体rows[id]
{
struct Address *addr = &conn->db->rows[id]; //addr指向结构体rows[id]
if(addr->set) die("Already set, delete it first" ,conn); //set的作用就是判断有无数据
addr->set = 1;
// WARNING: bug, read the "How To Break It" and fix this
char *res = strncpy(addr->name, name, conn->db->max_data - 1); //字符串复制,返回值是addr->name的地址
*(addr->name + conn->db->max_data -1) = '\0';
// demonstrate the strncpy bug
if(!res) die("Name copy failed" ,conn); //res为NULL则报错
res = strncpy(addr->email, email, conn->db->max_data - 1);
*(addr->email + conn->db->max_data -1) = '\0';
if(!res) die("Email copy failed" ,conn);
res = strncpy(addr->address, address, conn->db->max_data - 1);
*(addr->address + conn->db->max_data -1) = '\0';
if(!res) die("Email copy failed" ,conn);
}
void Database_get(struct Connection *conn, int id) //打印输出rows[id]
{
struct Address *addr = &conn->db->rows[id]; //取rows[id]的地址赋给指针addr
if(addr->set) {
Address_print(addr); //如果rows[id]的set是1,则打印该rows[id]的name id email
} else {
die("ID is not set" ,conn); //如果set为0,说明该rows[id]未设置,退出
}
}
void Database_delete(struct Connection *conn, int id) //根据id清空name、email等信息
{
conn->db->rows[id].id= id;
conn->db->rows[id].set = 0;
}
void Database_list(struct Connection *conn) //只要set为1,代表这个rows[id]写入过,就printf信息
{
int i = 0;
struct Database *db = conn->db;
for(i = 0; i < conn->db->max_rows; i++) {
struct Address *cur = &db->rows[i];
if(cur->set) {
Address_print(cur);
}
}
}
void Database_find(struct Connection *conn, char *message)
{
int comp1, comp2, comp3;
for(int i = 0;i < conn->db->max_rows; i++){
comp1 = strcmp(message, conn->db->rows[i].name); //将要寻找的字段和存储的name、email、address比较,只要有一个函数返回值为0即匹配成功
comp2 = strcmp(message, conn->db->rows[i].email);
comp3 = strcmp(message, conn->db->rows[i].address);
if(!comp1 || !comp2 || !comp3){
Address_print(&conn->db->rows[i]);
}
}
}
int main(int argc, char *argv[])
{
if(argc < 3) {
printf("ERROR: USAGE: ex17 <dbfile> <action> [action params]\n"); //argc<4则提前终止程序,即:argc至少要为4(至少要这两个参数:<dbfile文件名> <action执行什么操作>)
exit(1);
}
char *filename, action;
int MAX_DATA, MAX_ROWS;
if(argv[2][0] == 'c'){ //c模式下,其命令行格式:可执行文件 c MAX_DATA MAX_ROWS
filename = argv[1];
action = argv[2][0];
MAX_DATA = atoi(argv[3]);
MAX_ROWS = atoi(argv[4]);
}
else {
filename = argv[1]; //其他模式则从file文件中读取MAX_DATA和MAX_ROWS
action = argv[2][0];
struct Connection connect;
connect.db = NULL;
int temp[2];
connect.file = fopen(filename, "r+");
if(connect.file) {
fread(temp, 2 * sizeof(int), 1, connect.file);
MAX_DATA = temp[0];
MAX_ROWS = temp[1];
fclose(connect.file);
}
else
exit(1);
}
struct Connection *conn = Database_open(filename, action, MAX_DATA, MAX_ROWS); //Database_open 刨一块区域给结构体指针conn,并根据file文件初始化
int id = 0;
if(argc > 3 && action != 'c' && action != 'f') id = atoi(argv[3]); //如果不止3个参数,则取第三个参数为id
if(id >= conn->db->max_rows && action != 'c' && action != 'f') die("There's not that many records." ,conn);
switch(action) {
case 'c': //create,初始化一片区域
if(argc != 5) die("Need filename c max_size max_rows" ,conn);
Database_create(conn); //给conn->db结构体中的每一个数组元素rows[i](也是结构体)赋id和set = 0;
Database_write(conn); //把db指向的经过修改的结构体成员rows[i]写回conn->file
break;
case 'g': //get,用于打印输出
if(argc != 4) die("Need an id to get" ,conn); //这个操作要5个参数,<dbfile> <action>以及id
Database_get(conn, id); //打印输出rows[id]
break;
case 's': //set,设置name和email
if(argc != 7) die("Need id, name, email to set" ,conn); //这个操作要6个参数:<dbfile> <action> id name email address
Database_set(conn, id, argv[4], argv[5], argv[6]); //根据提供的id name email 写入结构体rows[id]
Database_write(conn); //把db指向的经过修改的结构体成员rows[i]写回conn->file
break;
case 'd': //delete
if(argc != 4) die("Need id to delete" ,conn); //delete需要3个参数:<dbfile> <action> id;
Database_delete(conn, id); //delete主要是根据id将set复位,感觉实际上此位置还有数据,但因为set为0,已经是无用数据了
Database_write(conn); //同步至file
break;
case 'l':
Database_list(conn); //printf已经写入过(set为1)的结构体的信息
break;
case 'f':
if(argc != 4) die("Need message to find" ,conn);
Database_find(conn, argv[3]); //取第四个参数为要寻找的字符串
break;
default:
die("Invalid action, only: c=create, g=get, s=set, d=del, l=list" ,conn);
}
Database_close(conn); //关闭file,释放内存
return 0;
}
第二版
答:我第一版写的是按照之前的思维,先在堆中创建好完整的数据库再进行操作。这样会导致Database_open函数需要max_data和max_rows参数,而不进行create操作时,这些数据是封存在文件中的,因此我必须先打开文件一遍,读取两个参数。新的版本将字符数组的堆分配分别放在了Databae_create和Database_load中,避免了这一操作。
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
struct Address {
int id; //数据id
int set; //数据状态
char *name; //数据1:name
char *email; //数据2:email
char *address; //数据3:address
};
struct Database {
int max_data;
int max_rows;
struct Address *rows; //最大堆栈深度取决于输入的MAX_ROWS
};
struct Connection { //struct Connection这个结构体用于将文件和堆栈结合起来,方便查找
FILE *file;
struct Database *db;
};
void Database_close(struct Connection *conn);
void die(const char *message ,struct Connection *conn)
{
if(errno) { //检查一下errno,如果非0则说明有错误,perror输出错误以及message;
perror(message);
} else { //如果errno没有问题,则输出ERROR:message,
printf("ERROR: %s\n", message);
}
Database_close(conn);
exit(1); //一旦进入这个函数,横竖都会输出错误且退出。
}
void Address_print(struct Address *addr)
{
printf("%d %s %s %s\n",
addr->id, addr->name, addr->email, addr->address);
}
void Database_load(struct Connection *conn) //根据file初始化db指针指向的结构体数组
{
int rc0 = fread(&conn->db->max_data, sizeof(int), 1, conn->file); //前两个参数是max_data和max_rows
int rc1 = fread(&conn->db->max_rows, sizeof(int), 1, conn->file);
if(rc0 != 1 || rc1 != 1) die("Failed to load max_data and max_rows to database." ,conn);
conn->db->rows = malloc(conn->db->max_rows * sizeof(struct Address)); //开辟区域给所有rows[i]
if(!conn->db->rows) die("Memory error" ,conn);
int j;
for(j = 0; j < conn->db->max_rows ; j++){
conn->db->rows[j].name = malloc(sizeof(char)* conn->db->max_data); //开辟区域给每个rows[i]的name、email以及address
conn->db->rows[j].email = malloc(sizeof(char)* conn->db->max_data);
conn->db->rows[j].address = malloc(sizeof(char)* conn->db->max_data);
if(!conn->db->rows[j].name || !conn->db->rows[j].email || !conn->db->rows[j].address) die("Memory error" ,conn);
}
for(int i =0; i< conn->db->max_rows; i++){
int rc2 = fread(&conn->db->rows[i].id, sizeof(int), 1, conn->file);
int rc3 = fread(&conn->db->rows[i].set, sizeof(int), 1, conn->file); //把db指向的经过修改的结构体成员rows[i]写回conn->file
int rc4 = fread(conn->db->rows[i].name, conn->db->max_data, 1, conn->file);
int rc5 = fread(conn->db->rows[i].email, conn->db->max_data, 1, conn->file);
int rc6 = fread(conn->db->rows[i].address, conn->db->max_data, 1, conn->file);
if(rc2 != 1 || rc3 != 1 || rc4 != 1 || rc5 != 1 || rc6 != 1) die("Failed to load database." ,conn);
}
}
struct Connection *Database_open(const char *filename, char mode) //为conn和db两个结构体指针指向的结构体开辟一片存储区域,并根据file文件初始化。
{
struct Connection *conn = malloc(sizeof(struct Connection)); //开辟区域给结构体指针conn
if(!conn) die("Memory error" ,conn);
conn->db = malloc(sizeof(struct Database)); //开辟区域给结构体指针db
if(!conn->db) die("Memory error" ,conn);
if(mode == 'c') {
conn->file = fopen(filename, "w"); //如果mode是create,检查一下*filename的可写性
} else {
conn->file = fopen(filename, "r+"); //如果mode是其他的,检查一下*filename的读写性
if(conn->file) {
Database_load(conn); //其他模式下如果文件可读写,则将file文件的部分内容读取至database
}
}
if(!conn->file) die("Failed to open the file" ,conn); //如果file指针是NULL,则输出错误
return conn;
}
void Database_close(struct Connection *conn) //关闭file,释放内存,先malloc的后释放
{
if(conn) {
for(int i = 0; i < conn->db->max_rows ; i++){
free(conn->db->rows[i].name);
free(conn->db->rows[i].email);
free(conn->db->rows[i].address);
}
if(conn->db->rows) free(conn->db->rows);
if(conn->file) fclose(conn->file);
if(conn->db) free(conn->db);
free(conn);
}
}
void Database_write(struct Connection *conn)
{
rewind(conn->file);
int rc0 = fwrite(&conn->db->max_data, sizeof(int), 1, conn->file); //将max_data和max_rows写入文件
int rc1 = fwrite(&conn->db->max_rows, sizeof(int), 1, conn->file);
if(rc0 != 1 || rc1 != 1) die("Failed to write max_data and max_rows to database." ,conn);
for(int i =0; i< conn->db->max_rows; i++){
int rc2 = fwrite(&conn->db->rows[i].id, sizeof(int), 1, conn->file);
int rc3 = fwrite(&conn->db->rows[i].set, sizeof(int), 1, conn->file); //把需要存储的参数写回conn->file
int rc4 = fwrite(conn->db->rows[i].name, conn->db->max_data, 1, conn->file);
int rc5 = fwrite(conn->db->rows[i].email, conn->db->max_data, 1, conn->file);
int rc6 = fwrite(conn->db->rows[i].address, conn->db->max_data, 1, conn->file);
if(rc2 != 1 || rc3 != 1 || rc4 != 1 || rc5 != 1 || rc6 != 1) die("Failed to write database." ,conn);
}
int rc7 = fflush(conn->file); //将缓冲区的东西直接写入文件
if(rc7 == -1) die("Cannot flush database." ,conn);
}
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 = malloc(max_rows * sizeof(struct Address)); //开辟区域给所有rows[i]
if(!conn->db->rows) die("Memory error" ,conn);
int j;
for(j = 0; j < max_rows ; j++){
conn->db->rows[j].name = malloc(sizeof(char)* max_data); //开辟区域给每个rows[i]的name、email以及address
conn->db->rows[j].email = malloc(sizeof(char)* max_data);
conn->db->rows[j].address = malloc(sizeof(char)* max_data);
if(!conn->db->rows[j].name || !conn->db->rows[j].email || !conn->db->rows[j].address) die("Memory error" ,conn);
}
int i = 0;
for(i = 0; i < conn->db->max_rows; i++) { //初始化Database结构体中的每一个数组元素(也是结构体)
// make a prototype to initialize it
conn->db->rows[i].id= i;
conn->db->rows[i].set = 0;
memset(conn->db->rows[i].name, 0, max_data);
memset(conn->db->rows[i].email, 0, max_data);
memset(conn->db->rows[i].address, 0, max_data);
}
}
void Database_set(struct Connection *conn, int id, const char *name, const char *email, const char *address) //根据提供的id name email 写入结构体rows[id]
{
struct Address *addr = &conn->db->rows[id]; //addr指向结构体rows[id]
if(addr->set) die("Already set, delete it first" ,conn); //set的作用就是判断有无数据
addr->set = 1;
char *res = strncpy(addr->name, name, conn->db->max_data - 1); //字符串复制,返回值是addr->name的地址
*(addr->name + conn->db->max_data -1) = '\0';
// demonstrate the strncpy bug
if(!res) die("Name copy failed" ,conn); //res为NULL则报错
res = strncpy(addr->email, email, conn->db->max_data - 1);
*(addr->email + conn->db->max_data -1) = '\0';
if(!res) die("Email copy failed" ,conn);
res = strncpy(addr->address, address, conn->db->max_data - 1);
*(addr->address + conn->db->max_data -1) = '\0';
if(!res) die("Address copy failed" ,conn);
}
void Database_get(struct Connection *conn, int id) //打印输出rows[id]
{
struct Address *addr = &conn->db->rows[id]; //取rows[id]的地址赋给指针addr
if(addr->set) {
Address_print(addr); //如果rows[id]的set是1,则打印该rows[id]的name id email
} else {
die("ID is not set" ,conn); //如果set为0,说明该rows[id]未设置,退出
}
}
void Database_delete(struct Connection *conn, int id) //根据id清空name、email等信息
{
struct Address *addr = &conn->db->rows[id];
addr->id= id;
addr->set = 0;
memset(addr->name, 0, conn->db->max_data);
memset(addr->email, 0, conn->db->max_data);
memset(addr->address, 0, conn->db->max_data);
}
void Database_list(struct Connection *conn) //只要set为1,代表这个rows[id]写入过,就printf信息
{
int i = 0;
struct Database *db = conn->db;
for(i = 0; i < conn->db->max_rows; i++) {
struct Address *cur = &db->rows[i];
if(cur->set) {
Address_print(cur);
}
}
}
void Database_find(struct Connection *conn, char *message)
{
int comp1, comp2, comp3;
for(int i = 0;i < conn->db->max_rows; i++){
struct Address *cur = &conn->db->rows[i];
comp1 = strcmp(message, cur->name); //将要寻找的字段和存储的name、email、address比较,只要有一个函数返回值为0即匹配成功
comp2 = strcmp(message, cur->email);
comp3 = strcmp(message, cur->address);
if(cur->set){
if(!comp1 || !comp2 || !comp3){
Address_print(&conn->db->rows[i]);
}
}
}
}
int main(int argc, char *argv[])
{
if(argc < 3) {
printf("ERROR: USAGE: ex17 <dbfile> <action> [action params]\n"); //argc<4则提前终止程序,即:argc至少要为4(至少要这两个参数:<dbfile文件名> <action执行什么操作>)
exit(1);
}
char *filename = argv[1];
char action = argv[2][0];
struct Connection *conn = Database_open(filename, action); //Database_open 刨一块区域给结构体指针conn,并根据file文件初始化
int id = 0;
if(argc > 3 && action != 'c' && action != 'f') id = atoi(argv[3]); //如果不止3个参数,则取第三个参数为id
if(action != 'c' && action != 'f') {
if(id >= conn->db->max_rows){
die("There is not that many records." ,conn);
}
}
switch(action) {
case 'c': //create,初始化一片区域
if(argc != 5) die("Need parameter: filename c max_size max_rows" ,conn);
int MAX_DATA = atoi(argv[3]);
int MAX_ROWS = atoi(argv[4]);
Database_create(conn, MAX_DATA, MAX_ROWS); //给conn->db结构体中的每一个数组元素rows[i](也是结构体)赋id和set = 0;
Database_write(conn); //把db指向的经过修改的结构体成员rows[i]写回conn->file
break;
case 'g': //get,用于打印输出
if(argc != 4) die("Need parameter: id" ,conn); //这个操作要5个参数,<dbfile> <action>以及id
Database_get(conn, id); //打印输出rows[id]
break;
case 's': //set,设置name和email
if(argc != 7) die("Need parameter:id, name, email" ,conn); //这个操作要6个参数:<dbfile> <action> id name email address
Database_set(conn, id, argv[4], argv[5], argv[6]); //根据提供的id name email address写入结构体rows[id]
Database_write(conn); //把db指向的经过修改的结构体成员rows[i]写回conn->file
break;
case 'd': //delete
if(argc != 4) die("Need parameter: id" ,conn); //delete需要3个参数:<dbfile> <action> id;
Database_delete(conn, id); //delete主要是根据id将set复位,感觉实际上此位置还有数据,但因为set为0,已经是无用数据了
Database_write(conn); //同步至file
break;
case 'l':
Database_list(conn); //printf已经写入过(set为1)的结构体的信息
break;
case 'f':
if(argc != 4) die("Need message to find" ,conn);
Database_find(conn, argv[3]); //取第四个参数为要寻找的字符串
break;
default:
die("Invalid action, only: c=create, g=get, s=set, d=del, l=list f=find" ,conn);
}
Database_close(conn); //关闭file,释放内存
return 0;
}
4.
问:查询C如何打包结构体,并且试着弄清楚为什么你的文件是相应的大小。看看你是否可以计算出结构体添加一些字段之后的新大小。
答:在liunux上,结构体是以4字节模式对齐的。
我尝试将数据库的大小定义为如下大小:
#define MAX_DATA 61
#define MAX_ROWS 64
得到的文件为6448kb。
实际上的Address结构体为:
struct Address {
int id; /* 4 bytes */
int set; /* 4 byte */
char name[61]; /* 61 bytes */
char email[61];/* 61 bytes */
char pad[3]; /* 3 bytes */
};
Tips:剩下的几道题就交给大家了,没时间做了┭┮﹏┭┮。
练习18
附加题
2.
问:在你的十六进制编辑器中找到更多随机出现的东西并修改它们。重新运行你的程序看看发生了什么。字符串是你最容易修改的东西。
答:不太懂题意,意思是让我修改十六进制的函数?
./ex18.exe 5 4 8 9 3
3 4 5 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 5 4 3
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:
5 8 4 9 3
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:
将十六进制的第一个函数8b修改为8d,输出如下:
./ex18.exe 5 4 8 9 3
5 4 8 9 3
f3:0f:1e:fa:55:48:89:e5:89:7d:fc:89:75:f8:8d:45:fc:2b:45:f8:5d:c3:f3:0f:1e:
9 8 5 4 3
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:
5 8 4 9 3
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:
显然第一个函数的排序失去了作用,但暂时不影响执行。
3.
问:将错误的函数传给compare_cb,并看看C编辑器会报告什么错误。
答:程序如下,修改了第一个函数的形参。
test_sorting(numbers, count, die);
输出如下:
Segmentation fault (core dumped)
一般是内存访问不对或者越界。
4.
问:将NULL传给它,看看程序中会发生什么。然后运行Valgrind来看看它会报告什么。
答:输出如下,传递了无效参数,导致内存泄漏。
==7949== Memcheck, a memory error detector
==7949== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==7949== Using Valgrind-3.21.0 and LibVEX; rerun with -h for copyright info
==7949== Command: ./ex18.exe 5 4 8 9 3
==7949==
==7949== Jump to the invalid address stated on the next line
==7949== at 0x0: ???
==7949== by 0x10947E: test_sorting (ex18.c:76)
==7949== by 0x109612: main (ex18.c:113)
==7949== Address 0x0 is not stack'd, malloc'd or (recently) free'd
==7949==
==7949==
==7949== Process terminating with default action of signal 11 (SIGSEGV)
==7949== Bad permissions for mapped region at address 0x0
==7949== at 0x0: ???
==7949== by 0x10947E: test_sorting (ex18.c:76)
==7949== by 0x109612: main (ex18.c:113)
==7949==
==7949== HEAP SUMMARY:
==7949== in use at exit: 40 bytes in 2 blocks
==7949== total heap usage: 2 allocs, 0 frees, 40 bytes allocated
==7949==
==7949== LEAK SUMMARY:
==7949== definitely lost: 0 bytes in 0 blocks
==7949== indirectly lost: 0 bytes in 0 blocks
==7949== possibly lost: 0 bytes in 0 blocks
==7949== still reachable: 40 bytes in 2 blocks
==7949== suppressed: 0 bytes in 0 blocks
==7949== Rerun with --leak-check=full to see details of leaked memory
==7949==
==7949== For lists of detected and suppressed errors, rerun with: -s
==7949== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
5.
问:编写另一个排序算法,修改test_sorting使它接收任意的排序函数和排序函数的比较回调。并使用它来测试两种排序算法。
答:写了一个乱七八糟算法,将就看一看吧。重要的是新定义了一个回调函数,使我能在两种外算法x三种内算法中自由切换了。
#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);
typedef int *(*algo_cb)(int *number, 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 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 *add_num(int *numbers, int count, compare_cb cmp)
{
int *target = malloc(sizeof(int) * count);
int temp = 0;
memcpy(target, numbers, count * sizeof(int));
for(int i =0; i < count; i++) {
temp += cmp(target[0], target[i]);
target[i] = 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, algo_cb algorithm)
{
int i = 0;
int *sorted = algorithm(numbers, count, cmp);
if(!sorted) die("Failed to algorium 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, bubble_sort);
test_sorting(numbers, count, reverse_order, add_num);
test_sorting(numbers, count, strange_order, add_num);
free(numbers);
return 0;
}