笨方法学C语言(Learn C the Hard Way) 学习笔记总结与思考题
exercise 10:
for(INITIALIZE, CONDITION, INCREMENT)
https://stackoverflow.com/questions/12959415/comma-operator-in-condition-of-loop-in-c 一般的逗号可以被看做一个二元操作符,先进行逗号左边内容的计算,然后再进行逗号右边内容的计算,并返回逗号右边计算的内容作为返回值。那么有什么一般的用处呢?可以在for循环的条件部分改造成i < (x++, x/2) 这样实际上仅从的操作就是,x++ , 判断i < (x + 1) / 2。上面的提问实际上等价于for(i, 5, i++)
将NULL作为字符串指针之后,就是将其指向了一块不对应任何内容的零内存空间,最后打印的效果是(null)
可以
exercise 11:
简单的编程实现如下所示:![ex11_1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex11_1.png)
在1的基础上,结合3的条件,改造后的代码如下所示:
![ex11_2_3](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex11_2_3.png)
- 根据输入测试输出:
![ex11_3](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex11_3.png)
在输出远多于states大小的情况下,正确倒序输出并且valgrind没有检测到任何漏洞
- 并没有真正复制这些字符串,实际上仅仅是将argv中存储的字符串地址指针复制给了states字符串数组。
exercise 12:
额外的布尔运算符有||或运算,否定运算!,> ,< , >=, <= 。当然,也包括逗号运算符。
多测试一些输入就可,如a b c;abc abcd abcde…
![ex12_3](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex12_3.png)
不正确,程序本身的第一个参数一定./ex11,而用户输入的第一个参数实际上在argv数组里从下标1开始计算,也即用户输入的参数个数是argc-1。这句话应该改成”You have input no argument“
exercise 13:
编译器处理switch语句的过程:
- 如图所示:
![ex13_1_1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex13_1_1.png)
![ex13_1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex13_1.png)
- 如图所示,只需要在,左边执行letter的赋值即可
![ex13_2](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex13_2.png)
- 如图所示:
![ex13_3](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex13_3.png)
![ex13_3_1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex13_3_1.png)
- 如图所示:
![ex13_4_1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex13_4_1.png)
- 这样可以使得字母y或Y出现在用户输入的参数字符串前三个位置的时候不被打印出来。这样避免了y被认为vowel的情况。当把break放到if里面的时候,会导致当字母y/Y出现在前三个位置的时候无法进入if,进而无法直接退出switch。这样最后会进入到default中,输出错误的结果。如图所示:
![ex13_5](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex13_5.png)
exercise 14:
notes:
- ctype.h头文件包含的内容:https://en.cppreference.com/w/cpp/header/cctype
- blank与space的区别,在c里面的blank是包含tab、换行、空格等一系列visual blank的内容,而space则对应ASCII码中的一个space。
- 移除前向声明会导致编译器无法正常编译,报“implicit decalaration of function”,即若在一个函数a的实现里面调用了b函数,则b函数的定义或者预先声明必须在a的实现之前。
- 如果在main调用print_arguments时,对argc+1,那么它在访问argv的时候就会越过argv数组的最后一个元素,通过valgrind对错误进行检测,可知argv的下一个内存比特内容为空,那么print_letters就会接受一个空指针,并尝试打印0x0处的字符串。而0x0并不是程序合法的地址空间,从而造成了访问越界Segmentation fault.
extra credits:
1.
![ex14_1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex14_1.png)
2.
![ex14_2](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex14_2.png)
3.
![ex14_3](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex14_3.png)
- Detailed information on K&R syntax: https://jameshfisher.com/2016/11/27/c-k-and-r/ https://stackoverflow.com/questions/3092006/function-declaration-kr-vs-ansi
exercise 15:
notes: Revision on Pointer Operations
Increment/Decrement or Addition/Substraction numerically.
p++ —> p = p + sizeof(type(p))
ptr++, ptr–, ptr = ptr + 5, ptr = ptr - 3
同理addition/subtraction也是处理+- i * sizeof(int …)
指针的加减法,加减的单位数字实质上是指针指向元素类型的多少。在地址上永远要对指针运算的数值乘以类型的大小sizeof(…)
指针之间的减法有效当且仅当两个指针的类型相同,对于同int类型指针p1, p2。p1-p2的结果实际上是: (p1 - p2) / sizeof(int); 也即p1的地址减去p2的地址,除以int类型的大小4,得到p1,p2之间间隔的int类型元素的个数。
Detailed Article: https://www.geeksforgeeks.org/pointer-arithmetics-in-c-with-examples/
编程题:
![ex15_ex1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex15_ex1.png)
extra credits:
1.
![ex15_1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex15_1.png)
2.
![ex15_2](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex15_2.png)
见ex15_1.c,用指针改写了所有使用数组的地方
对ex10进行修改,如下所示:
![ex15_4](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex15_4.png)
- ex15.c中第二个循环的实现就将获取值与获取地址结合到了一起,外层for循环中的循环变量i通过指针运算做到了获取地址,也即cur_name + i获取到了指针的地址;然后通过一层解引用*(cur_name + i)来对获取到的地址解引用,从而获取了值。
![ex15_6code](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex15_6code.png)
![ex15_6](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex15_6.png)
7.
![ex15_7](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex15_7.png)
![ex15_7_code](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex15_7_code.png)
8.
![ex15_8](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex15_8.png)
![ex15_8_test](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex15_8_test.png)
前三种都是while循环方便,只有最后一种写法while循环比较方便。
exercise 16
Notes:
- strdup函数可以用来拷贝一个传入的字符串s,返回一个指向复制出来相同字符串的指针。这个字符串是通过malloc申请内存来获取的。在这一讲的练习里面,Person_create函数中会先malloc一块空间给Person结构体,然后在strdup中其实也会对字符串进行空间上的malloc。因此,最后删除Person结构体的时候,我们需要同时将Person结构体中所有malloc的空间free掉。
- 注意到,如果我们不想显式地free内存,这回潜在地导致内存泄漏,那么就需要引入LibGC库,并用GC_malloc
- printf函数中的\t是垂直格式限定符,会在垂直方向上进行格式的控制。%p格式控制符可以打印出某个变量的地址。注意这里的%p要搭配取地址符来实现,或者说其对应打印的对象必须是一个指针。
- 创建结构体、摧毁结构体的过程可以预先设计好两个函数。这里借鉴了面向对象设计的思想,实际上,每个结构体对应面向对象程序设计中的一个类。那么,我们创建结构体的过程实际上就可以被包含在构造函数之中。而摧毁结构体的过程实际上就是析构函数。
如何使它崩溃:
- 传入NULL给Person_destroy实际上会导致Abort,因为Person_destroy首先进行的就是一个assert,然后不符合就会导致。
- 向valgrind传递–leak-check=full参数来进行内存泄漏的测试
![ex16_err1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex16_err1.png)
这里可以看到,在valgrind中报错指出了内存泄漏和损失问题。并指出了造成这一问题的函数,是main里面调用Person_create中strdup里面调用了malloc导致的。
- 当我们向Person_print传入NULL时,会显示地址0x0不是在程序映射到的地址段,访存越界!导致Segmentation Fault!
extra_credits:
- 在栈上创建结构体的方式相当直接,只需要直接用struct Person x = {…}来进行初始化即可
- 对于x指针,只需要将x->y替换为*(x).y即可,而对于一般的结构体变量x,直接x.y初始化即可
- 使用指针来将结构体传入函数,实际上做的是“传址调用”,而直接将结构体传给其他函数,可以通过传值调用来实现。
具体修改的代码如下所示:
![ex16_1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex16_1.png)
![ex16_2](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex16_2.png)
exercise17:
notes:
- errno - number of last error 这里涉及到具体的stdin, stdout和stderr之间的关系了。Linux将这三种数据流视作文件,一般来说他们的文件描述符分别为0-stdin,1-stdout, 2-stderr。stdout在正常的时候作为输出流,而当程序出错的时候,stderr就会打印输出在bash中。perror所做的就是这样的一件事,向stderr填充错误信息,这样当程序出错的时候,我们就可以及时地得到反馈了。
例子:
1 |
|
- 对这部分源码的解读:
1 |
|
printf默认是将内容打印到stdout这个流,而一般的输入输出流stdin、stdout都是有缓冲区的。对于输出来说,printf实际上将要打印的内容先输入到stdout的缓冲区,
1 |
|
- strncpy的设计缺陷,它只会讲src的前n个字节做复制,若src的前n个字节中不包含”\0”,那么dst作为字符串被拷贝的情况下就可能不以”\0”作为结束,导致不符合一般的字符串格式。
- 在database_set函数里面,是通过strncpy来进行邮箱、姓名的复制的。而strncpy执行复制的过程中一般没有包含’\0’。在我们的Address结构体中可知name和email的数组最大是512.那么当我们输入的字符串长度超过512的时候,会将输入字符串的前512个字符复制到address的name/email里。这就导致我们最终复制的字符串不是以’\0’结尾的。
测试数据:
1 | ./ex17 db.dat s 4 hjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxfly hjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxflyhjrxfly@163.com |
![ex17_b1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex17_b1.png)
![ex17_b2](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex17_b2.png)
在修改最后一个为’\0’后,可见后者打印出来的字符串是要比前者短的。
1 | ./ex17 db.dat s 4 aaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbccccddddeeeeffff hjrxfly@163.com |
上述样例存入数据库,之后访问得到的结果为:
![ex17_b3](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex17_b3.png)
也就是说,存入的时候,addr->name保存了输入姓名的前512个字符,addr->email正常存储。而在系统存储内部,name和email是连续存储的。也就是说,在打印name的时候,因为name字符串没有以’\0’结尾,那么在输出的时候,打印完name所有内容后也不会停止,会接着打印email的内容直到触摸到第一个’\0’,这也就解释了为什么name后面会多一个邮箱地址。后面输出的name的长度,也是name+email的长度。这个现象就是当输入超过512个字符时的结果。
Extra credits:
1.
![ex17_1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex17_1.png)
这里die额外维护一个传入的Connection即可,对于conn非NULL时分别尝试fclose和free。
细节:
1 | //假设conn->db->rows为一个address数组的起始指针 |
2.
出现bug:
id:1的地址为0x5567fb3b28e8,set为0x5567fb3b28ec
程序设计的逻辑上是有问题的。
我在database_create的时候想当然地去malloc空间来给database,但是实际上,database进行实时交互地内存在每一次操作的时候需要单独malloc来进行分配。也就是说,实际上我们的database去申请空间的过程应该在database_open中实现。至于每次改动的内容,应该由database_write写入到文件里来实现保存。
当我们正确创建database后,实际上往文件conn->file写入的内容只有一个Database结构体的内容,也就是说,我们只写入了一个类型为struct Address *的指针,和max_data、max_rows的内容…而没有写入具体的,每一行的内容。因为每一个具体的Address行内容都是在create的时候malloc的,在堆区!当create结束后,实际上堆区内存就会被释放,这部分内存在下面s的时候也不会被检索到!也就是说,上一次申请的内存在下一次调用程序输入内容的时候是非法的,所以会segmentation fault。
我们的做法是,在Database_open中来做堆区内存的申请,每次结束的时候将对数据库的修改写回文件中!
我们设计的db.dat文件格式如下所示:
其中有max_rows个Address,文件模块,后面都跟着对应的字符串。
1 | File begin: |struct Database|struct Address id:0|id:0 name string|id:0 email string|struct Address id:1|.... |
最终得到的程序代码为:
1 |
|
使用原有的测试样例来完成测试,如下所示:
![ex17_2](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex17_2.png)
- 我们实现的find如下代码所示:
1 | void Database_find(struct Connection *conn, char *name) |
![ex17_3](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex17_3.png)
如何打包结构体?
(1)首先,结构体需要按照i386 System V的ABI规约去进行对齐,整个结构体变量的对其方式需要按照其中最严格的成员来进行。
(2)每个成员在满足对齐方式的前提下,取最小的可用位置作为成员在结构体中的偏移量,这可能会导致内部插空。
(3)结构体大小应为对齐边界长度的整数倍,这可能会导致尾部插空。
前两条规则是为了保证结构体中任意成员都能以对齐的方式访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28//一些关于对齐的例子:
struct SDT{
int i;
short si;
double d;
char c;
}sa[10];
/*
该结构体会按照double来进行对齐,也即地址必定是8的倍数,所以结构体最终的大小应该是8的整数倍。
4 + 2 + 8 + 1 = 15,按照边界对齐,那么结构体的大小最后就是24
其内存安排如下所示:
0 4 6 8 16 17 24
| int | si |--| double | char |------|
总体需要24个字节
*/
//重新换一种结构体的组织形式,注意到si后面为了对齐double的留空可以用来插入一个char,这样重新安排之后:
struct SDT{
int i;
short si;
char c;
double d;
}sa[10];
/*
0 4 6 7 8 16
| int | si | char |-| double |
这样改之后结构体总体只需要16个字节
*/根据ls -l指令我们得到了指定文件db.dat的文件大小为2536b,根据我们的文件组织形式可以知道,含有1个database结构体,max_r个address结构体,max_r个长度为max_d的name和email字符串。
考虑到对齐与64位机器上指针变量的大小,可知database结构体占16b,address结构体占24b,一个name/email字符串为30b。
那么计算公式为16 + 24 * 30 + 30 * 1 * 2 * 30 = 2536b
- 额外添加一些信息即可,这样在database_load中需要额外malloc对应的空间。
- 我完成的自动测试脚本文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29!/bin/bash
set -e
echo "Creating database ..."
./ex17 db.dat c
echo ""
echo "Saving zed, frank, joe to database ..."
./ex17 db.dat s 1 zed zed@zedshaw.com
./ex17 db.dat s 2 frank frank@zedshaw.com
./ex17 db.dat s 3 joe joe@zedshaw.com
echo ""
echo "Printing all records ..."
output=$(./ex17 db.dat l)
./ex17 db.dat l
echo $output
echo ""
echo "Deleting record by id 3 ..."
./ex17 db.dat d 3
echo ""
echo "Printing all records ..."
./ex17 db.dat l
echo ""
echo "Getting record by id 2 ..."
./ex17 db.dat g 2为了方便原有程序的编写,其实只需要将一开始在database_load中用malloc申请堆区内存的conn指针,改为指向一个全局变量即可。这个全局变量在database_close中不需要最后执行free(conn)了,因为并不是malloc在堆区上的内存,程序运行结束后其所占内存会随着程序结束自行释放。
实现的源代码和测试结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
struct stk{
__uint32_t top; //mark the top of the stack
char elem[STK_SIZE + 1]; //store the char elements in the stack
};
// 0 and the STK_SIZE are the floor and top! 1-STK_SIZE are valid!
void warn(char *msg)
{
printf("%s \n", msg);
exit(1);
}
void stack_push(struct stk *s, char ch)
{
if(s->top + 1 > STK_SIZE)
{
warn("The stack is overflow!");
return;
}
s->top++;
s->elem[s->top] = ch;
printf("The letter %c is pushed in the stack \n", ch);
}
char stack_pop(struct stk *s)
{
if(s->top == 0)
{
warn("The stack is empty! Nothing to pop!");
return '\0';
}
char ret = s->elem[s->top];
s->top--;
return ret;
}
char stack_top(struct stk *s)
{
if(s->top == 0)
{
warn("The stack is empty! Nothing at top!");
return '\0';
}
else
return s->elem[s->top];
}
bool stack_is_empty(struct stk *s)
{
return (s->top == 0);
}
struct stk *stack_init()
{
struct stk *mystk = malloc(sizeof(struct stk));
mystk->top = 0;
memset(mystk->elem, 0, sizeof(mystk->elem));
return mystk;
}
int main()
{
struct stk *s = stack_init();
stack_push(s, 'A');
stack_push(s, 'B');
stack_push(s, 'C');
stack_push(s, 'D');
if(stack_is_empty(s)) printf("Stack is empty!\n");
else printf("Stack is not empty!\n");
printf("Stack pop is %c\n", stack_pop(s));
printf("Stack pop is %c\n", stack_pop(s));
printf("Stack pop is %c\n", stack_pop(s));
printf("Stack pop is %c\n", stack_pop(s));
if(stack_is_empty(s)) printf("Stack is empty!\n");
else printf("Stack is not empty!\n");
return 0;
}![ex17_8](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex17_8.png)
Exercise 18 函数指针:
Notes:
函数实际上只是指向程序中某一个代码存在位置的指针,类似结构体指针、字符串和数组所代表的指针,我们也可以创建指向函数的指针。函数指针的主要用途是向其他函数传递“回调”,或者模拟类和对象。
函数指针的格式如下所示:
1 | int (*POINTER_NAME)(int a, int b) |
例如:
1 | int (*compare_ab)(int a, int b) |
上述格式定义完毕后,我们的指针变量名称为compare_db,而我们可以将它用作函数。这类似于指向数组的指针可以表示所指向的数组,指向函数的指针同样也可以表示对应的函数,只不过所用的是不同的名字。
1 |
|
补充知识:
typedef关键字可以用来给一个类型重新起一个新的名字。例如
1 | typedef unsigned char BYTE; |
typedef和#define预定义宏的区别:typedef只能对变量类型做符号上等价的名称替代,而#define预定义宏也可以对变量来进行相应的名称替代;typedef是由编译器来进行转译处理的,而#define预处理宏是由预处理器来做处理的。
注意,函数指针如果定义成typedef int (*compare_ab)(int a, int b),那么作为类型定义的compare_ab cmp调用时可以直接cmp(x, y)来实现两个元素的比较。
附加题:
1.
![ex18_1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex18_1.png)
使用objdump工具来分析ex18的二进制源码,并以十六进制的形式来解读。
![ex18_11](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex18_11.png)
根据打印出来的十六进制内容可知,指针转换后打印的内容分别为三个函数具体的汇编源码。
2.
![ex18_2_1](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex18_2_1.png)
![ex18_2_2](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex18_2_2.png)
修改字符串,使用工具hexedit,对一开始的保存信息做一些修改。即USAGE: ex18 4 3 1 5 6
3.
![ex18_3](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex18_3.png)
传错误的函数,编译器会报错,指出我们传入的指针类型与compare_ab时不同的。也即传入了一个void ( * )(const char *)的函数指针,与定义里的int ( * )(int a, int b)不同。
4.
![ex18_4](C:\Users\Junrong Huang\Desktop\ysyx-proj\pre-learning\ex_records\ex18_4.png)
因为传入的是NULL,那么在访问地址解析函数的过程中就会发生越界现象。报错segmentation fault
5.
1 |
|
额外编写了插入排序的方式,并修改test_sorting使其接受不同的排序回调函数和排序规则函数。
ex19 预处理与OOP
cpp - The C Preprocessor,c预处理器。c预处理器,是一种宏预处理器,会被C编译器自动使用来转换程序。
pipe: 管道是Linux通信机制中很有效的一个工具,对于 command1 | command2,管道通常存在的意义就是,将command1输出的内容重定向为command2输入的内容。 https://fedoramagazine.org/command-line-quick-tips-using-pipes-to-connect-tools/
grep: 进行模式匹配的查找
Linux less 工具,可以展示文件的内容。 https://linuxize.com/post/less-command-in-linux/
作者在设计每个类(如Room,Monster和Map)的时候,都将对象Object放在了结构体的开头,这样的话,NEW里面传入了每个申请结构体的sizeof申请一块空间之后,Object_new返回的指针转换为Object*之后也一定指向了其原型结构体的指针。
ex20 debug宏
- 出现在dbg.h中的有意思的宏:
1 | __FILE__ //有的编译器并不支持,但是gcc支持该宏,用来获取当前文件的路径,在我们需要生成日志文件的时候尤其有用。一般调用的时候会返回一个const char* file. |
- strerror(errornum)函数可以接受一个错误码errno,然后返回一个解释该错误码的字符串指针。所以clean_errno宏可以用于获取errno的可读的版本,并作为一个参量传入fprintf中
- 预处理器在解析宏的过程:
1 | ##__VA_ARGS__ 是传入的剩余参数,对应...部分 |
更进一步,我们使用宏来做debug而不是函数,因为我们debug的过程需要具体的文件名、行号等信息来辅助我们debug。
- Makefile中,我们可以向CFLAGS传参-D NDEBUG来实现宏的引入定义。这样即使源文件中没有定义宏NDEBUG,我们也可以达到有NDEBUG宏的效果。
- 附加题中让我们额外添加调用函数的宏,只需要在格式控制字符串中额外添加一个输出_FUNCTION _的部分即可。
ex21 类型、限定
- int虽然在32或64位环境下为32位,但是它并不应该被是做是与平台无关的。如果需要用到与平台无关的定长整数,请使用int(n)_t。
- 符号修饰符signed/unsigned只对char和 *** int有效, 后者默认为signed,而char根据具体实现可以默认为signed。在需要做memcpy等的时候最好还是转换为unsigned来做。
- 类型限定符volatile,表示会做最坏的打算,编译器不会对他做让任何优化。
- stdint.h中会为定长的整数类型做一些typedef,同时也有一些用户这些类型的宏。如int32_t、uint64_t等等。这些数据的最值可以用宏INT(N)_MAX来获得,如INT16_MAX可以得到int16 _ t的最大值。UINT(N) _ MAX同理
- intptr_t:足够存放指针的符号整数 uintptr_t: 足够存放指针的无符号整数
- size_t:是无符号整型数据,其命名原有是用来代表对象占有的字节数,作为sizeof操作的返回值,显然是无符号的。
ex22:栈、作用域和全局
- extern关键字:告诉编译器,这个变量已存在,但是它在其他的“外部区域”中。通常它出现的情况是一个.c文件要用到另一个.c文件所定义的变量。这种情况下,我们可以说ex22.c中的THE_SIZE变量能被ex22_main.c访问到
- static(文件)关键词的意思是指这个变量只能在当前的.c文件中使用,程序的其他部分不可访问。编译生成的ELF文件中会被存储到.bss段。
- static(函数) 如何使用static在函数中声明变量,那么它和文件中的static定义类似,但是只能够在该函数中访问。他是一种创建某个函数的持续状态的方法。
ex23: duff device
https://mthli.xyz/duff-device/
ex24: 输入输出和文件
- 在读取的细节上,fscanf和fgets是由区别的。scanf在读取之前往往不知道输入缓冲区有多大,同理函数gets也是一个具有该问题的函数·。而fgets可以解决该问题,不会读取过多的呢欸容。