本笔记主要使用:https://www.icourse163.org/course/ZJU-200001?tid=236001
C语言程序设计进阶(翁恺)
前言:
学完了入门课程,正巧上一个页面在编辑的时候已经会发生卡顿的情况了,所以开了新的页面。链接最后一段想不好该叫什么,暂时写作https://lingyun67.cn/cplus/
据入门课笔记后记中的猜想,进阶课程观看的是2015年开讲的课程,而入门课程观看的是2020年的,个人猜测可能是在某次开课后修改了课程,主要是把数个进阶课的片段移动到入门课中。所以为了看 改之前的课程,进阶课程观看的是2015年的。
指针的使用
交换两个变量的值:
1 2 3 4 5 6 |
void swap(int *pa, int *pb) { int t = *pa; *pa = *pb; *pb = t; } |
使用的时候注意传进去的是地址
函数返回多个值:
函数返回多个值,某些值就只能通过指针返回
传入的参数实际上是需要保存带回的结果的变量
如传入数组,数组长度,即将接受最大最小值的两变量的地址,函数最终运行结束最大最小值将得到赋值。
函数返回状态,结果用指针返回:
函数返回运算的状态,结果通过指针返回
常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错
-1或0(在文件操作会看到大量的例子)
当任何数值都可能是有效的结果时,就得分开返回:即状态return,实际值用指针。
因为运算可能会出错,所以需要返回状态。在后续的语言(C++,Java)采用了异常机制来解决这个问题
初学者的常见错误:
可以看作是入门课程48.1 常见错误的补充:
定义指针后不初始化,它会随机指向一个地址,这个时候对它进行写入,很可能会导致错误。
所以任何一个地址变量,在没有得到赋值之前、没有得到任何有实际意义的地址之前,不能够用*去访问任何的数据,那都是没有任何意义的。
向函数传入数组传入的是什么:
在入门篇42.5出现的课程,完全一样。个人感觉在入门篇中出现的少数“突然出现的”课程就是从进阶篇中直接转过去的。
指针与const(C99)
指针是const:
表示一旦得到了某个变量的地址,不能再指向其他变量
1 2 3 |
int *const q = &i; //q是const *q = 26; //OK q++; /ERROR |
所指是const:
表示不能通过这个指针去修改那个变量(注意:并不会使那个变量成为const)
1 2 3 4 |
const int *q = &i; *q = 26; //ERROR (*p)是const i = 26; //OK p = &j; //OK |
多个写法:
1 2 3 4 5 |
int i; int *const p1 = &i; const int *p2 = &i; int const *p3 = &i; |
判断哪个被const了的标志是const在*前面还是后面
所以p2与p3相等
转换:
总是可以把一个非const的值转换成const的
1 2 3 4 5 |
void f(const int *x); int a = 15; f(&a); //OK const int b = a; f(&b); //OK |
事实上void f(const int *x)的意思是这个函数保证不会修改传进来的这个地址里面的值。
当要传递的参数的类型比地址大的时候,这是常用的手段:技能用比较少的字节数传递值给参数,又能避免函数对外面的变量的修改。
const数组:
const int a[] = {1,2,3,4,5,6,};
数组变量已经是const的指针了,这里的const表明数组的每个单元都是const int
所以必须通过初始化进行赋值。
所以可以对数组值进行保护:
因为把数组传入函数时传递的时地址,所以那个函数内部可以修改数组的值
为了保护数组不被函数破坏,可以设置参数为const
int sum(const int a[], int length);
指针运算
指针+1:
有int *p = ai;对比p+1与p
p+1比p大4 因为int的长度是4字节
而char *q = ac;对比q+1与q
q+1比q大1 因为char的长度是1字节

0号位第一个字节的地址是2c,那么q+1就会移动到30也就是一号位
为什么移动的是变量类型的长度,因为如果直接让地址本身加1,那么得到的地址毫无意义(会取前面单元里的三个和后面的单元里一个一共四个字节)
星号是一个单目运算符,他的优先级比加号高,所以要这样*(p+1)
给一个指针加1表示要让指针指向下一个变量
int a[10];
int *p = a;
*(p+1) --> a[1]
如果指针不是指向一片连续分配的空间如数组,则这种运算没有实际意义。
除了加,减一个整数 递增递减以外,指针还可以:
两个指针相减:
两个指针相减,得到的不是两个指针的地址差,得到的是地址差除以变量类型的长度。
*p++:
*的优先级没有++高,所以
取出p所指的那个数据来,完事之后顺便把p移到下一个位置去
(个人感觉其实是先移动指针p的位置,然后输出的是移动之前的数据)
常用于数组类的连续空间操作
在某些cpu上,这可以直接被翻译成一条汇编指令。
指针比较:
<,<=,==,>,>=,!= 都可以对指针做
比较它们在内存中的地址
数组中的单元的地址肯定是线性递增的
0地址:
多进程的操作系统会给进程分配一个虚拟的地址空间,所有的程序在运行的时候都以为自己拥有一片从“0”开始的连续的空间
当然内存中有0地址,但是0地址通常是不能随便碰的地址
所以指针不应该具有0值
因此可以用0地址来表示特殊的事情:
返回的指针是无效的
指针没有被真正初始化(先初始化位0)
NULL(注意大写)是一个预定定义的符号,表示0地址
有的编译器不愿意你用0来表示0地址
指针的类型:
有一个指向char类型的p 一个指向int的q
如果这件事真的做成了,代表指向int的指针q会在表示char的区域取四个字节
无论指向什么类型,所有的指针的大小都是一样的,因为都是指针。
但是指向不同类型的指针是不能直接互相赋值的,这是为了避免用错指针。
指针的类型转换:
void* 表示不知道只想什么东西的指针
计算时与char*相同(但不相通)
往往会用在底层程序,系统程序中,希望直接去访问某个内存地址,直接去访问内存地址所代表的一些外部设备等。
指针可以转换类型
int *p = &i; void *q = (void*)p;
这并没有改变p所指的变量的类型,而是让后人用不同的眼光通过p看它所指的变量。通过p看i,就是int;通过q看i那就是void。
总结,用指针来做什么:
需要传入较大的数据时用作参数
传入数组后用指针对数组做操作
函数返回不止一个结果
需要用函数不止一个变量
动态申请的内存..
动态内存分配
输入数据:
如果输入数据时,先告诉个数,然后再输入,要记录每个数据
C99可以用变量做数组定义的大小,那么C99之前:
1 |
int *a (int*)malloc(n*sizeof(int)) |
比如要三十个int 那就是告诉malloc我需要120个字节。得到的是void*所以需要转换成int*

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <stdio.h> #include <stdlib.h> int main(void) { int number; int* a = NULL; int i; printf("输入数量:"); scanf_s("%d", &number); //int a[number]; C99only a = (int*)malloc(number * sizeof(int)); //给a分配number个int的空间 for (i = 0; i < number; i++) { scanf_s("%d", &a[i]); } for (i = number - 1; i >= 0; i--) { printf("%d", a[i]); } free(a); //释放内存 return 0; }</stdlib.h></stdio.h> |
需要调用标准库stdlib.h
void* malloc(size_t size);
向malloc申请的空间的大小是以字节为单位的
返回的结果是void*,需要类型转换为自己需要的类型
(int*)malloc(n*sizeof(int));
没空间了:
如果申请失败则返回0,或者叫做NULL
你的系统能给你多大的空间?
free():
把申请的来的空间还给“系统”
申请过的空间,最终都应该还回去
只能归还申请来的空间的首地址
free一个地址0(NULL)是可以的,free函数判断如果输入了一个NULL那就不做任何事情,为的是配合p = 0的好习惯,当p在malloc中没有得到数据,这时它为0,如果是1或类似的则不行。
常见问题:
申请了没free->长时间运行内存逐渐下降
新手:忘了
老手:找不到合适的free时机
free过了再free
地址变过了直接去free
没有free对小程序来说不是问题,因为程序结束之后操作系统的保护机制会将程序曾经使用过的所有的内存都会清除干净,但是并不是个好习惯。
建议:
1:牢牢记住malloc之后需要free
2:对程序的整体架构有一个良好的设计,保证有合适的地方去放free
3:经验,多阅读别人的代码,多在失败中总结
课后练习:
对于以下代码段,正确的说法是:
1 2 3 4 5 |
char *p; while (1) { p = malloc(1); *p = 0; } |
- A.最终程序会因为没有没有空间了而退出
- B.最终程序会因为向0地址写入而退出
- C.程序会一直运行下去
- D.程序不能被编译
解析:
程序一直分配内存,肯定会引起内存耗尽。而malloc在分配内存失败时并不会终止程序,而是返回NULL指针。而第5行代码试图向NULL指针位置写入数据,这会引起程序终止(通常操作系统会因为“段错误”而终止程序)。
所以 B 才是引起程序退出的直接原因。*引用源自文末标注
字符串操作
单字符输入输出:
int putchar(int c);
向标准输出写一个字符
它接受的是int 但要输入一个字符的char
返回写了几个字符,是一个int,EOF(-1)表示写失败
int getchar(void);
从标准输入读入一个字符
它返回从标准输入中读到的字符
返回类型是int是为了返回EOF(-1)
编写这样一个程序:
1 2 3 4 5 6 7 8 9 10 11 12 |
int main (int argc, char const *argv[]) { int ch; while ((ch = getchar()) != EOF){ putchar(ch); } printf("EOF\n"); return 0; } |
在输入任意字符回车后会返回完全相同的字符 原封不动
如果按下ctrl + C 程序会被强制结束,程序没有返回EOF
如果输入ctrl + Z EOF会被打印出来 因为:

在没有按下回车之前,行编辑的功能使得输入的字符并没有送到程序中去,停留在shell。
按下回车之后,在shell中的缓冲区:

123回车 如果程序还没有读入,用户输入 345回车 后面有一个结束的标志(不一定是0)
getchar 与 scanf 他们读入的不同,但相同的是读入结束标志后,因为是在循环里,所以他们还在等用户输入:

当用户按下了ctrl + D 这个时候shell最后会填入一个"表达EOF"的标志(不一定是-1)

如果输入ctrl + C 那么shell直接就把程序关闭了

字符串数组:
char **a
a是一个指针,指向另一个指针,那个指针指向一个字符(串)
char a[][] (二维数组)
关于此部分,在搜索资料的时候发现了一篇文章,于是进行转载。
深入 理解char * ,char ** ,char a[ ] ,char *a[] 的区别
C++ 学习——char * ,char a[ ],char ** ,char *a[] 的区别
main函数的参数:
1 |
int main(int argc, char const *argv[]) //*argv[]字符串数组 argc是它的大小 |
- argv[0]是命令本身
- 当使用Unix的符号链接时,反应符号链接的名字
1 2 3 4 5 6 7 8 9 10 |
//使用代码查看*argv[] int main(int argc, char const *argv[]) { int i; for (i = 0; i < argc; i++){ printf("%d:%s\n", i, argv[i]); } return 0; } |

argv[0]让程序可以知道“是如何运行这个程序的”即“通过何种方式”
扩展:busybox会让你明白为什么这个事情在Unix会非常有用
字符串函数:
在入门课程中插入了相同的视频:入门课笔记
头文件:<string.h>
strlen(计算):
这个函数会告诉你一个字符串的长度是多少
1 |
size_t strlen(const char *s); //直接使用了指针,效果一样 |
返回s的字符串长度(不包括结尾的0)
自制strlen(?):
1 2 3 4 5 6 7 8 9 |
size_t mylen(const char* s) { int idx = 0; while (s[idx] != '\0'){ idx++; } return idx; } |
strcmp(比较):
1 |
int strcmp<strong>(</strong>const char *s1, const char *s2<strong>)</strong>; |
比较两个字符串,返回:
0 : s1 == s2
1 : s1 > s2
-1 : s1 < s2
事实上,如果直接进行字符串的比较 如s1 == s2 这个结果永远为0,因为进行比较的不是字符串的内容,是第一个字符的地址,而这两个地址是不可能相等的。
有一个计算的逻辑结果是abc > bbc
如果比较"abc"与"Abc",输出的结果是32,因为a与A的距离是32
比较"abc"与"abc "(结尾多了一个空格)输出结果是-32(与上一个无关联)因为在比对到第四个字符的时候,比对的是"/0"与"空格"空格的位置是32,所以是"0 - 32"结果为-32。

所以自制的话
就需要逐个比较s1[0] == s2[0]一直到不相等为止,找到不相等后算出哪个大。
相等的结束标准为前面字符都一样,并且最后一个也都为'\0'
好像有点复杂 代码还得看看
strchr(寻找):
在字符串中找字符 (这节视频在入门课程没有出现
char *strchr(const char *s, int c);
在s字符串中从左向右找字符c第一次出现的位置
char *strrchr(const char *s, int c);
在s字符串中从右向左找字符c第一次出现的位置
注意 返回的是指针
返回NULL表示没有找到
输出某个字符第二次出现的位置:
1 2 3 4 5 6 7 8 9 10 |
#include <stdio.h> int main(void) { char s[] = "hello"; char* p = strchr(s, 'l'); //在字符串中寻找'l',将地址赋给p printf("%s\n", p); //将p以字符串形式输出 return 0; } |
输出结果为:
1 |
llo |
那么如果想寻找第二次出现的这个字符:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> int main(void) { char s[] = "hello"; char* p = strchr(s, 'l'); //在字符串中寻找'l',将地址赋给p printf("%s\n", p); //将p以字符串形式输出 输出结果为llo p = strchr(p + 1, 'l'); //忽略p[0]位置,寻找字符l printf("%s\n", p); return 0; } |
结果为:
1 2 |
llo lo |
一些骚操作:
字符串复制:
输出:某个字符前面的所有字符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <stdio.h> int main(void) { char s[] = "hello"; char* p = strchr(s, 'l'); //在字符串中寻找'l',将地址赋给p char c = *p; *p = '\0'; char* t = (char*)malloc(strlen(s) + 1); strcpy(t, s); printf("%s\n", t); free(t); return 0; } |
结果为
1 |
he |
strstr(寻找):
1 2 |
char * strstr(const char *s1, const char *s2); char * strcasestr(const char *s1, const char *s2); //在寻找过程中忽略大小写 |
strcpy(复制):
char *strcpy(char *restrick dst, const char * restrict src);
把src参数表达的的字符串拷贝到dst的那个空间里去。
(逐个字符的拷贝,包括"\0")
restrict表明src和dst不重叠(C99)
返回dst
为了能链起代码来
strcat(连接):
char *strcat(char *restrict s1, const char *restrict s2);
逐字符把s2拷贝到s1的后面,接成一个长的字符串
s1需要有足够空间去容纳s2,s1的\0会直接被覆盖
*返回s1
安全问题:
strcpy和strcat都可能出现安全问题,如果目的地没有足够的空间会出现问题
尽可能不要使用它们
可以使用安全的版本:char *strncpy(char *restrick dst, const char * restrict src, size_t n);char *strncat(char *restrick s1, const char * restrict s2, size_t n);
对于cpy来说,n意味着能拷过去最多多少个字符
对于cat来说,n意味着能连上最多多少个字符int strncmp(const char *s1, const char *s2, size_t n);
只比较前n个字符,剩下的字符忽略掉
ACLLib入门:
ACLLib是一个基于Win32API的函数库,提供了相对较为简单的方式来做Windows程序。
实际提供了一个.c和两个.h,可以在MSVC和Dev C++中使用
以GPL方式开源在github上
纯教学用途,但是编程模型和思想可以借鉴。
Windows API:
从第一个32位的Windows开始就出现了,就叫做Win32API
它是一个纯C的函数库,就和C标准库一样,使你可以写Windows应用程序
过去很多windows程序使用这个方式做出来的。
main()?:
main()成为C语言的入口函数其实和C语言本身无关,代码是被启动代码的程序所调用的,他需要一个叫做mian的地方
操作系统把你的可执行程序装载到内存里,启动运行,然后调用你的main函数。
如果编译器选择其他的名称作为函数入口,那么就与main无关系了
对于win32API来说,程序的入口是:
WinMain()
一些疑问:
如何产生窗口?
如何在窗口中画东西?
如何获得用户的鼠标和键盘动作?
如何画出标准的界面:菜单、按钮、输入框?
——此条ACLLib目前不能实现
前三条疑问对应着:窗口结构、DC、消息循环和消息处理代码。
这对初学者来说过于困难。
经权衡,暂时放弃此部分
枚举:
常量符号化:保证可读性(const int)。目前 可以用更好的方法来实现这些
枚举是一种用户定义的数据类型,它用关键字enum以如下语法来声明:
1 |
enum 枚举类型名字 {名字0, 名字1, ...., 名字n}; |
枚举类型名字通常并不真的使用,要用的是在大括号里的名字,因为他们就是常量符号,他们的类型是int,值从0到n。如:
1 |
enum colors{red, yellow, green}; |
这样便创建了三个常量,red的值是0,yellow是1,green是2。
当需要一些可以排列起来的常量时,定义枚举的意义就是给了这些常量值名字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> enum color {red, yellow, green}; //事实上,这是在声明一种新的数据类型,这种数据类型名叫color void f(enum color c); //所以在接下来color可以当作int float这种变量类型来用。 但必须带着enum int main(void) { enum color t = red; scanf_s("%d", &t); f(t); return 0; } void f(enum color c) { printf("%d\n", c); } |
但事实上,enum就是int,是可以当作int来进行输入与输出的。
一个套路:自动计数的枚举:
1 |
enum COLOR {RED, YELLOW, GREEN, NumCOLORS}; |
NumCOLORS的数值,就是枚举的数量
这样需要遍历所有的枚举量或者建立一个用枚举量做下标的数组的时候就很方便了。
枚举量:
声明枚举量的时候可以指定值。
1 2 3 4 5 6 7 8 9 10 |
#include <stdio.h> enum color {red = 1, yellow, green = 5}; int main(int argc, char const *argv[]) { printf("code for GREEN is %d\n", green); return 0; } |
代码中,red = 1; yellow = 2; green = 5;
枚举只是int:
虽然枚举类型可以当作类型使用,但实际上很少(不好)用
如果有意义上排比的名字,用枚举比const int方便
枚举比宏(macro)好,因为枚举有int类型。
结构:
声明结构类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> int main(int argc, char const *argv[]) { struct date { int month; int day; int year; }; //初学者最常见的错误:漏掉此处的分号 struct date today; today.month = 07; today.day = 31; today.year = 2014; printf("Today's date is %i-%i-%i.\n", today.year, today.month, today.day); return 0; } |
(VS在声明结构类型的时候自动补全了大括号后面的分号;)
注意:不能忘掉结构体声明的结尾分号
和本地变量一样,在函数内部声明的结构类型只能在函数内部使用,所以通常在函数外部声明结构类型就可以被多个函数使用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <stdio.h> struct date { int month; int day; int year; }; int main(int argc, char const *argv[]) { struct date today; today.month = 07; today.day = 31; today.year = 2014; printf("Today's date is %i-%i-%i.\n", today.year, today.month, today.day); return 0; } |
声明结构的形式:
1 2 3 4 5 6 7 |
struct point { int x; int y; }; struct point p1, p2; |
p1和p2都是point,里面有x与y。
另外一种:
1 2 3 4 5 |
struct { int x; int y; }p1, p2; |
p1和p2都是一种无名结构,里面有x与y。
此时p1与p2并不是结构类型的名字,是某个结构类型的变量,而这种结构类型的名字是不存在的。
这种做法并不常见,常见的是下面的做法:
1 2 3 4 5 |
struct point { int x; int y; }p1, p2; |
p1h和p2都是point,里面有x和y的值
对于第一和第三种形式,都声明了结构point。但是第二种形式没有声明point,只是定义了两个变量。
1 2 3 4 5 6 7 |
struct point { int x; int y; }; struct point p1, p2; |
对于struct point{},是在声明一种新的类型,在声明结构类型之后可以定义很多个结构变量(结构类型)
而struct point p1, p2;是在定义这种结构类型的一个变量(结构变量)
每一种结构变量会按照声明的样子里面含有year,month,day。
结构的初始化:
第一种方式:
1 |
struct date today = {07, 31, 2014}; |
按照声明结构类型时的变量顺序来初始化
另一种:
1 |
struct date thismonth = {.month = 7, .yesr = 2014}; |
与数组的初始化的思路大相径庭,代码中没有给的.day的值会初始化为0
结构成员:
在声明结构类型的时候,里面的变量就是成员,我们把它叫做成员变量。
- 结构和数组有点像
- 数组用[]运算符和下标访问其成员
- a[] = 10
- 结构用 . 运算符和名字访问其成员
- today.day
- student.firstName
- p1.x
- p1.y
结构运算:
- 要访问整个结构,直接用结构变量的名字
- 对于整个结构,可以做赋值、取地址,也可以传递给函数参数。
- p1 = (struct point){5, 10};
//相当于p1.x = 5; p1.y = 10;
//(struct point)是强制类型转换,将5,10转换为point这种结构的一个变量 - p1 = p2;
//相当于p1.x = p2.x; p1.y = p2.y;
- p1 = (struct point){5, 10};
数组无法做这两种运算!
结构指针:
和数组不同,结构变量的名字并不是结构变量的地址,必须使用&运算符
1 2 |
struct date today; struct date *pDate = &today; |
pDate 是一个指向结构的指针
结构与函数:
结构作为函数参数:
1 |
int numberofDays(struct date d) |
整个结构可以作为参数的值传入函数
这时候是在函数内新建一个结构变量,并复制调用者的结构的值
(这时候它是一个新的结构变量,这和数组是完全不一样的)
(个人感觉:貌似是不想在函数之外定义,并且在有两个函数有需要的情况下使用的)
也可以返回一个结构
这些都与数组完全不同
1 |
scanf("%i %i %i", &today.month, &today.day, &today.year); |
从上面的&today.month可以注意到,取地址'&'的优先级比取成员'.'的优先级要低,为先取成员,后取地址。(如果是先取地址那么情况就是取到了一个指针,但对一个指针取成员是没有意义的)
结构指针作为参数:
没听懂 可恶
指向结构的指针:
1 2 3 4 5 6 7 8 9 10 |
struct date { int month; int day; int year; }myday; struct date* p = &myday; (*p).month = 12; p->month = 12; |
用->表示指针所指的结构变量中的成员
用一种更简单方法表示
最后一句可读作:p所指的那个month
事实上->也是一个运算符,它和'.'运算符类似
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 |
#include <stdio.h> struct point { int x; int y; }; struct point* getStruct(struct point*); void output(struct point); void print(const struct point* p); int main(void) { struct point y = { 0, 0 }; getStruct(&y); output(y); output(*getStruct(&y)); print(getStruct(&y)); getStruct(&y)->x = 0; *getStruct(&y) = (struct point){ 1,2 }; return 0; } struct point* getStruct(struct point* p) { scanf_s("%d", &p->x); scanf_s("%d", &p->y); printf("%d %d", p->x, p->y); return p; } void output(struct point p) { printf("%d, %d", p.x, p.y); } void print(const struct point* p) { printf("%d, %d", p->x, p->y); } |
以上代码说明了其他用法。
结构中的结构
结构数组:
1 2 3 4 |
struct date dates[100]; struct date dates[] = { {4, 5, 2005},{2, 4, 2005} }; |
定义一个结构数组,此数组每一个单元都是一个结构变量:
1 2 3 4 5 |
struct time { int hour; int minutes; int seconds; }; |
1 |
struct timne testTime[5] = {{11,59,59}, {12,0,0}, {1,29,59}, {23,59,59}}; |
在使用的时候与数组相同,只是看起来比较复杂
1 2 3 4 |
for (i = 0; i < 5; ++i){ printf("Tume us %.2i:%.2i:%.2i"), testTimes[i].hour, testTimes[i].minutes, testTimes[i].seconds); } |
结构的嵌套:
1 2 3 4 5 6 7 8 |
struct point{ int x; int y; }; struct rectangle{ struct point pt1; struct point pt2; }; |
如果有变量
1 |
struct rectangle r; |
就可以有:
1 |
r.pt1.x r.pt1.y r.pt2.x r.pt2.y |
如果定义
1 2 |
struct rectangle r, *rp; rp = &r; |
那么下面的四种形式是等价的:
1 2 3 4 |
r.pt1.x r->pt1.x (r.pt1).x (rp->pt1).x |
但是没有rp->pt1->x(因为pt1不是指针)
相当于r里面有pt1与pt2,每个pt里还有x与y,在此之上rp指向了r
再套娃的话,也可以有如结构中的结构的数组这种用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <stdio.h> struct point{ int x; int y; }; struct rectangle { struct point p1; struct point p2; }; void printRect(struct rectangle r) { printf("<%d, %d> to <%d, %d>\n", r.p1.x, r.p1.y, r.p2.x, r.p2.y); } int main(int argc, char const *argv[]) { int i; struct rectangle rects[ ] = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}; // 2 rectangles for(i=0;i<2;i++) printRect(rects[i]); } |
他变难了,也变复杂了,但仍然用的是上述的知识。
课后练习:
以下两行代码能否出现在一起?
1 2 |
struct { int x; int y; } x; struct { int x; int y; } y; |
- A.可以
- B.不可以
一时大意,正确答案为A可以,个人觉得原因应该是定义的这两个结构变量只是没有名字的原因。
类型定义:
自定义数据类型(typedef)
C语言提供了一个叫做typedef的功能来声明一个已有的数据类型的新名字。比如:
1 |
typedef int Length; |
使得Length成为int类型的别名。
这样,Length这个名字就可以代替int出现在变量定义和参数声明的地方了。
1 2 |
Length a, b, len; Length numbers[10]; |
"typedef 最后那个名字是最后出来的能代表前面一大串的标识符"
1 2 3 4 |
typedef *char[10] Strings; //Strings是10个字符串的数组的类型 typedef struct node aNode; //这样用aNode就可以代替struct node |
联合
联合与结构十分相似,但联合内的所有成员使用的是同一块内存。
1 |
sizeof(union ...) = sizeof(每个成员)的最大值 |
- 存储
- 所有的成员共享一个空间
- 同一时间只有一个成员是有效的
- union的大小是其最大的成员
- 初始化
- 对第一个成员做初始化
联合一般是用来做如下的事情的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> typedef union { int i; char ch[sizeof(int)]; //因为在不同的机器上int的大小不同,所以用sizeof使此处的char与int相同大小 } CHI; int main(void) { CHI chi; int i; chi.i = 1234; for (i = 0; i < sizeof(int); i++) { printf("%02hhX", chi.ch[i]); } printf("\n"); return 0; } |
在union,例中的chi中有四个字节,这四个字节可以被看作是int,也可以被看作是char
进行 chi = 1234这步的时候,会向chi的四个字节写入1234的十六进制形式,即0x4D2
程序输出的结果是:
1 |
D2040000 |
也就是说,在chi中,1234的十六进制存储不是顺序存储的即不是00 00 04 D2,而是D2 04 00 00(D2是低位)
因为我们接触到的x86机器是小端,在计算机术语中被称为 低位在前(一个数放在内存里的时候要把低的数放在前面)
可以通过这种方式得到一个整数内部的各个字节,同样,也可以得到double、float等等
当我们要做文件操作的时候,当要把一个整数以二进制的方式写到文件里的时候,这就是一个媒介。
可变数组与链表:
本周整个标题就打了星号。本周的内容是让你了解C语言的某些较为复杂的应用的。本周的内容严格来说,不是在教C语言是什么,而是C语言怎么用,也可以算做是和后续的数据结构之间的一个链接吧。
可变数组:
链表:
先跳过 我去.....
全局变量:
- 定义在函数外面的变量是全局变量
- 全局变量具有全局的生存期和作用域
- 他们与任何函数都无关
- 在任何函数内部都可以使用它们
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <stdio.h> int f(void); int gAll = 12; int main(int argc, char const* argv[]) { printf("in %s gAll = %d\n", __func__, gAll); f(); printf("agn in %s gAll = %d\n", __func__, gAll); return 0; } int f(void) { printf("in %s gAll = %d\n", __func__, gAll); gAll += 2; printf("agn in %s gAll = %d\n", __func__, gAll); return gAll; } |
输出为:
1 2 3 4 |
in main gAll = 12 in f gAll = 12 agn in f gAll = 14 agn in main gAll = 14 |
__func__ 是一个字符串,表达的是当前函数的名字
- 没有做初始化的全局变量会得到0值
- 指针会得到NULL值
- 只能用编译时已知的值来初始化全局变量
- 它们的初始化发生在main函数之前
全局变量的隐藏:
如果函数内有 与全局变量同名的本地变量,全局变量会被隐藏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <stdio.h> int f(void); int gAll = 12; int main(int argc, char const* argv[]) { printf("in %s gAll = %d\n", __func__, gAll); f(); printf("agn in %s gAll = %d\n", __func__, gAll); return 0; } int f(void) { int gAll = 1; printf("in %s gAll = %d\n", __func__, gAll); gAll += 2; printf("agn in %s gAll = %d\n", __func__, gAll); return gAll; } |
输出结果为:
1 2 3 4 |
in main gAll = 12 in f gAll = 1 agn in f gAll = 3 agn in main gAll = 12 |
事实上如果再写一个大括号,大括号内再定义一个gAll,那么此时定义的这个gAll又把外面的隐藏了
1 2 3 4 5 6 7 8 9 10 11 |
int f(void) { int gAll = 1; { int gAll = 2; printf("in %s gAll = %d\n", __func__, gAll); gAll += 2; printf("agn in %s gAll = %d\n", __func__, gAll); } return gAll; } |
静态本地变量:
- 在本地变量定义时加上static修饰符就成为静态本地变量
- 当函数离开的时候,静态本地变量会继续存在并保持其值
- 静态本地变量的初始化只会在第一次进入这个函数的时候做,以后进入这个函数时会保持上次离开时的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <stdio.h> int f(void); int gAll = 12; int main(int argc, char const* argv[]) { f(); f(); f(); return 0; } int f(void) { static int All = 1; printf("in %s All = %d\n", __func__, All); All += 2; printf("agn in %s All = %d\n", __func__, All); return All; } |
输出结果为:
1 2 3 4 5 6 |
in f All = 1 agn in f All = 3 in f All = 3 agn in f All = 5 in f All = 5 agn in f All = 7 |
静态本地变量它实际上是一个全局变量。
对程序一开始定义的本地变量与静态本地变量还有本地变量进行地址输出,可以看到:
1 2 3 |
全局变量 = 0x3800c 静态本地 = 0x38010 本地变量 = 0xbffc9d4c |
全局变量与静态本地变量是紧挨着的,它们的首地址相差4字节。
所以静态本地变量拥有:
- 和全局变量一样的生存期
- 和本地变量一样的作用域
作为静态变量,它有全局的生存期
作为本地变量,它有本地的作用域
返回指针的函数:
返回本地变量的地址是危险的:
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 |
#include <stdio.h> int* f(void); void g(void); int main(int argc, char const* argv[]) { int* p = f(); printf("*p = %d\n", *p); g(); printf("*p = %d\n", *p); return 0; } int* f(void) { int i = 12; return &i; } void g(void) { int k = 24; printf("k = %d\n", k); } |
结果为:
1 2 3 |
*p = 12 k = 24 *p = 24 |
int* p = f(); 获取到了地址 是因为之前所说的“不存在”指的是本地变量不受控了, 无法保证这个12能不被改变,进行了k = 24之后,再输出*p其结果为24。
事实上,如果在f函数中打印i的地址,在g函数打印g的地址,它们的地址是一样的。
在a函数定义的本地变量,在a函数结束后,其本地变量的地址会被继续分配给其他变量。
返回全局变量或静态本地变量的地址是安全的,因为它们的生存期是全局的。
返回在函数内malloc的内存是安全的,但是容易造成问题。
最好的做法是返回传入的指针。
全局变量贴士:
- 不要使用全局变量来在函数间传递参数和结果
- 尽量避免使用全局变量
- 丰田汽车的案子
- *使用全局变量和静态本地变量的函数是线程不安全的
如果函数使用了全局变量 那么函数是不可重入的
什么是可重入函数与不可重入函数? 可重入和不可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误; 而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。 也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。 如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。 *引用源自文末标注 |
编译预处理和宏:
编译预处理指令:
- #开头的是编译预处理指令
- 他们不是C语言的成分,但是C语言程序离不开它们
- #define用来定义一个宏
1 2 3 4 5 6 7 8 9 |
#include <stdio.h> #define PI 3.14159 int main(void) { printf("%f\n", 2 * PI * 3.0); //printf("%f\n", 2 * 3.14159 * 3.0); return 0; } |
预处理流程:
预处理:对#进行处理,删除注释生成.i
编译:对.i进行语法分析、词法分析、语义分析,生成.s,这步是C的编译器真正做的
汇编:生成二进制文件,windows为.obj文件 linux是.o
链接:生成二进制文件之后,将二进制文件与库文件进行绑定,完成后生成可执行文件
宏定义:
1 |
#define PI 3.14159 |
这一行定义了一个符号,我们把这样定义出来的符号叫宏,这个宏的名字是PI,3.14159是这个宏的值。
C语言在编译之前会进行一次编译预处理,编译预处理的时候会把程序里所有的PI都替换成3.14159,这个替换是一种简单原始的文本替换。如果定义一个宏,希望将FORMAT替换为"%f\n",放到printf函数里是没有问题的。(FORMAT如果在""内作为一个字符串是不行的,这个时候编译器不会管双引号里面的东西)
- #define <名字> <值>
- 注意没有结尾的分号,因为不是C的语句
- 名字必须是一个单词,值可以是各种东西
- 在C语言的编译器开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值
- 完全的文本替换
注意:
- 如果一个宏的值中有其他宏的名字,也是会被替换的
- 如果一个宏的值超过一行,最后一行之前的行末需要加\
- 宏的值后面出现的注释不会当作宏的值的一部分
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> #define PI 3.14159 #define PI2 PI*2 //宏套娃 #define PRT printf("%f ", PI);\ printf("%f", PI2); int main(void) { PRT; return 0; } |
没有值的宏:
1 |
#define _EDBUG |
这类宏是用于条件编译的,后面有其他的编译预处理指令来检查这个宏是否已经被定义过了
(??????)
预定义的宏:
- __LINE__ 代表当前源代码文件行的行号
- __FILE__ 代表当前源代码文件的文件名
- __DATE__ 代表编译时候的日期
- __TIME__ 代表编译时的时间
- __STDC__ 当编译器以 ANSI 标准编译时,则定义为 1
(判断该文件是不是标准C程序)
带参数的宏
像函数的宏:
宏可以带参数:
1 |
#define cube(x) ((x)*(x)*(x)) |
注意:x作为一个参数是没有类型的,只是说这里的x将来会被替换为其他东西
如程序中有一句:
1 |
printf("%d\n, cube(5)"); |
那么它在预处理后将会变成:
1 |
printf("%d\n, ((5)*(5)*(5))"); |
如果是变量同理,cube(i + 2)也是可以的。
这就引来一个问题:
错误定义的宏:
1 2 |
#define RADTODEG(x)(x * 57.29578) #define RADTODEG(x)(x) * 57.29578 |
这样的宏,在使用例如下中会出现问题:
1 2 |
printf("%f\n", RADTODEG1(5+2)); printf("%f\n", 180/RADTODEG1(1)); |
输出结果为:
1 2 |
119.571560 10313.240400 |
这显然不是期望的结果值,是因为预处理后的代码变成了:
1 2 |
printf("%f\n", (5+2 * 57.29578)); printf("%f\n", 180/(1) * 57.2957800); |
带参数的宏的原则:
- 一切都要括号:
- 整个值要括号
- 参数出现的每个地方都要括号
1 |
#define RADTODEG(x) ((x) * 57.29578) |
多个参数的宏:
1 |
#define MIN(a,b) ((a)>(b)?(b):(a)) |
也可以组合(嵌套)使用其他宏
宏定义的时候不要加分号,它不是C的语句
总结:
- 在大型程序的代码中使用非常普遍
- 可以非常复杂,在#和##两个运算符的帮助下,可以"产生"函数
- 西方使用较多,国内较少
有一个缺点是宏的参数中是没有类型检查的,所以部分宏会被inline函数替代
其他编译预处理指令:条件编译、error等等.......
课后练习:
假设宏定义如下:
1 |
<code>#define TOUPPER(c) ('a'<=(c)&&(c)<='z'?(c)-'a'+'A':(c))</code> |
设s是一个足够大的字符数组,i是int型变量,则以下代码段的输出是:
1 |
<code>strcpy(s, "abcd"); i = 0; putchar(TOUPPER(s[++i]));</code> |
正确答案是D,我不理解
大程序结构
项目(多个源代码文件):
- 在Dev C++中新建一个项目,然后把几个源代码文件加进去
- 对于项目,Dev C++的编译会把一个项目中所有的源代码文件都编译后,链接起来
- 有的IDE有分开的编译和构建两个按钮,前者是对单个源代码文件编译,后者是对整个项目做链接
Dev C++是比较特殊的,它允许不创建项目
而有些IDE的分开的两个按钮是因为:
- 一个.c文件是一个编译单元
- 编译器每次编译只处理一个编译单元
- 他们形成.o文件 然后进行链接
如果有多个源代码文件,而不在main.c中 声明 在其他文件的 而在main.c中使用的 函数的话
C语言根据传统会猜测类型,比如
1 2 3 |
int a = 5; int b = 6; printf("%d\n", max(a,b)); |
在函数调用的时候,C语言会猜测max函数接受的是两个int,返回的也是int,而如果事实并非如此,编译器也不会报错,但结果并不是正确的。
这时两个文件各自编译,链接器也会正确的链接函数之间的调用,但是结果是错误的。
如何保证这点不会出错,我们需要一个媒介:
头文件:
我们把函数原型放到一个头文件(以.h结尾)中,在需要调用这个函数的源代码文件(.c)中#include这个头文件,就能让编译器在编译的时候知道函数的原型。
1 |
#include "max.h" |
在所有调用此函数的地方都需要include,在函数定义的地方也需要include
- #include是一个编译预处理指令,和宏一样,在编译之前就处理了。
- 它把那个文件的全部文本内容原封不动的插入到它所在的地方
- 所以也不是一定要.c文件的最前面#include
""还是<>:
- #include有两种形式来指出要插入的文件
- ""要求编译器首先在当前目录(.c文件所在的目录)寻找这个文件,如果没有,到编译器指定的目录去寻找。
- <>让编译器只在指定的目录去找
- 编译器自己知道自己的标准库的头文件在哪里
- 环境变量和编译器命令行参数也可以指定寻找头文件的目录
#include的误区
- #include不是用来引入库的,它只是把文件内的内容插入到#include行
- stdio.h里只有printf的原型,printf的源代码在另外的地方,某个.lib(windows)或.a(Unix)中
- 现在的C语言编译器默认会引入所有的标准库
- #include<stdio.h>只是为了让编译器知道printf函数的原型,保证你调用时给出的参数值是正确的类型。
头文件注意:
- 在使用和定义这个函数的地方都应该#include这个头文件
- 一般的做法就是任何.c都有对应的同名的.h,把所有对外公开的函数的原型和全局变量的声明都放进去。
全局变量是可以在多个.c文件之间共享的,前提是有恰当的方式告诉别人全局变量的原型是什么。
不对外公开的函数:
在函数前面加上static就使得它成为一个只能在所在的编译单元中被使用的函数
在全局变量前面加上static就使得它成为只能在所在的编译单元中被使用的全局变量
声明
变量的声明:
- int i; 是变量的定义
- extern int i; 是变量的声明
变量的定义和声明是两种不同的事情
如果说max.c里有一个gAll,而目前想在main.c中访问就需要声明
声明与定义:
在C语言中,声明是不产生代码的东西,如:
函数原型、变量声明、结构声明、宏声明、枚举声明、类型声明、inline函数。
定义是产生代码的东西。
头文件的注意:
- 只有声明可以被放在头文件中
- 是规则不是法律
- 否则会造成一个项目中多个编译单元里有重名的实体
- *某些编译器允许几个编译单元中存在同名的函数,或者用weak修饰符来强调这种存在、
重复声明的问题:
- 同一个编译单元里,同名的结构不能被重复声明
- 如果头文件里有结构的声明,很难这个头文件不会在一个编译单元里被#include多次
- 所以需要“标准头文件结构”
比如一个项目中有如下文件:
main.c max.c max.h min.h
main.c #include了 max.h 与 min.h,max.h里声明了一个结构,同时min.h也#include了max.h
这使得main里声明了两次相同的结构,这种场景非常常见并且很难避免。
因此我们需要这样改造max.h:
1 2 3 4 5 6 7 8 9 10 11 12 |
#ifndef __MAX_H__ //如果没有名为__MAX_H__的宏 #define __MAX_H__ //那么就define__MAX_H__ double max(double a, double b); extern int gAll; struct Node { int value; char* name; }; #endif //限制如果的范围 |
1 2 3 4 5 6 7 8 9 10 11 12 |
#define AAA #ifndef AAA //如果有AAA被定义,执行到endif。否则跳至endif。 double max(double a, double b); extern int gAll; struct Node { int value; char* name; }; #endif //限制如果的范围 |
学到此处恍然大悟,明白了前面的空宏的意义何在
标准头文件结构
- 运用条件编译和宏,保证这个头文件在一个编译单元中只会被#include一次
- #pragama once也能起到相同的作用,但是不是所有的编译器都支持
(比如vs是支持的,dev C是不行的)
ACLLib
因第六周内容涉及ACLLib,遂返回到第二周学习。
ACLLib是一个基于Win32API的函数库,提供了相对较为简单的方式来做Windows程序。
实际提供了一个.c和两个.h,可以在MSVC和Dev C++中使用
以GPL方式开源在github上
纯教学用途,但是编程模型和思想可以借鉴。
Windows API:
从第一个32位的Windows开始就出现了,就叫做Win32API
它是一个纯C的函数库,就和C标准库一样,使你可以写Windows应用程序
过去很多windows程序使用这个方式做出来的。
main()?:
main()成为C语言的入口函数其实和C语言本身无关,代码是被启动代码的程序所调用的,他需要一个叫做mian的地方,main不是C语言的关键字
操作系统把你的可执行程序装载到内存里,启动运行,然后调用你的main函数。
如果编译器选择其他的名称作为函数入口,那么就与main无关系了
对于win32API来说,编译器需要寻找的程序入口不是main,而是:
WinMain()。
一些疑问:
如何产生窗口?
如何在窗口中画东西?
如何获得用户的鼠标和键盘动作?
如何画出标准的界面:菜单、按钮、输入框?
——此条ACLLib目前不能实现
前三条疑问对应着:窗口结构、DC、消息循环和消息处理代码。
这对初学者来说过于困难,因为需要先理解一堆外围的东西才能写出第一行代码。
ACLLib的安装
新建一个项目(Dev C++),选择WindowsApplication、C语言。
将ACLLib.c与ACLLib.h移动到项目目录并将其添加到项目中。
然后在项目属性中:

[项目属性]->[参数]->[连接器(link)]->[加入库或者对象]中
选中[C:/Program Files (x86)/Dev-Cpp/MinGW32/lib/libwinmm.a],
笔者在此处没有找到此文件,寻找之下发现在 [C:/Program Files (x86)/Dev-Cpp/MinGW32/i686-w64-mingw32/lib/libwinmm.a] 。

以相同步骤,共添加八个文件:
1 2 3 4 5 6 7 8 |
"C:/Program Files/Dev-Cpp/MinGW32/lib/libwinmm.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/libmsimg32.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/libkernel32.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/libuser32.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/libgdi32.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/libole32.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/liboleaut32.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/libuuid.a" |
添加后如下:

1 2 3 4 5 6 7 8 9 |
"../../../../../Program Files (x86)/Dev-Cpp/MinGW32/i686-w64-mingw32/lib/libwinmm.a" "../../../../../Program Files (x86)/Dev-Cpp/MinGW32/i686-w64-mingw32/lib/libmsimg32.a" "../../../../../Program Files (x86)/Dev-Cpp/MinGW32/i686-w64-mingw32/lib/libkernel32.a" "../../../../../Program Files (x86)/Dev-Cpp/MinGW32/i686-w64-mingw32/lib/libuser32.a" "../../../../../Program Files (x86)/Dev-Cpp/MinGW32/i686-w64-mingw32/lib/libgdi32.a" "../../../../../Program Files (x86)/Dev-Cpp/MinGW32/i686-w64-mingw32/lib/libole32.a" "../../../../../Program Files (x86)/Dev-Cpp/MinGW32/i686-w64-mingw32/lib/liboleaut32.a" "../../../../../Program Files (x86)/Dev-Cpp/MinGW32/i686-w64-mingw32/lib/libuuid.a" |
然后删除main.c内的全部代码,开始输入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include "acllib.h" #include <stdio.h> int Setup() //ACLLib程序的入口函数 { initConsole(); printf("请输入宽度:"); int w; scanf("%d", &w); initWindow("test", 100, 100, w, w); //定义一个标题是test的,将在100,100打开,大小是w,W的窗口 beginPaint(); //开始画图 line(20, 20, w-20, w-20); //在20,20的位置向w-20,w-20画一条线 endPaint(); //结束画图 return 0; } |
Dev C++直接复制代码会出现汉字乱码的现象,上面是通过其他文本编辑器打开后复制的。
课程内的安装说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
在Win7或Win8上正常安装的Dev C++,无论是用的MinGW编译器还是TDM编译器都是可以正常使用ACLLib的,但是要注意以下几点: 1. 如果还在使用4.9.9.2的Dev C++,一定要升级到5以上,建议都升级到最新的5.10的版本; 2. 在新建项目的时候选择Windows Application类型; 3. 根据自己机器是32位还是64位来选择编译类型,如果是32位的机器选择MinGW32位方式,如果是64位的机器建议选择TDM的64位方式; 4. 在配置项目的时候,根据32位还是64位选择正确目录下的库文件来加入: 1. 32位下,库文件是: "C:/Program Files/Dev-Cpp/MinGW32/lib/libwinmm.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/libmsimg32.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/libkernel32.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/libuser32.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/libgdi32.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/libole32.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/liboleaut32.a" "C:/Program Files/Dev-Cpp/MinGW32/lib/libuuid.a" 2. 64位下,库文件是: C:/Program Files/Dev-Cpp/MinGW64/x86_64-w64-mingw32/lib/libwinmm.a C:/Program Files/Dev-Cpp/MinGW64/x86_64-w64-mingw32/lib/libmsimg32.a C:/Program Files/Dev-Cpp/MinGW64/x86_64-w64-mingw32/lib/libkernel32.a C:/Program Files/Dev-Cpp/MinGW64/x86_64-w64-mingw32/lib/libuser32.a C:/Program Files/Dev-Cpp/MinGW64/x86_64-w64-mingw32/lib/libgdi32.a C:/Program Files/Dev-Cpp/MinGW64/x86_64-w64-mingw32/lib/libole32.a C:/Program Files/Dev-Cpp/MinGW64/x86_64-w64-mingw32/lib/liboleaut32.a C:/Program Files/Dev-Cpp/MinGW64/x86_64-w64-mingw32/lib/libuuid.a 5. 最后,如果出现“undefined reference to `TransparentBlt' ”这个错误,两个解决方案: 1. 偷懒的,打开acllib.c,找到“TransparentBlt”所在的行,把整行注释掉; 2. 打开工程配置,找到编译器选项,加入-DWINVER=0x0500。 6. 如果还有问题,别忘了到讨论区来问大家。 |
第一个ACLLib项目:
initWindow函数左上角坐标可以使用如下方式:
1 |
initWindow("test, DEFAULT, DEFAULT, 200, 200"); |
使用DEFAULT会将图像输出在Windows觉得合适的地方
坐标系:
在Windows中,坐标是以像素点的数字来定义的。因为最初的时候文本是从左上角到右下角的,所以对于创建出来的窗口,左上角是(0,0),x轴自左向右增长,而y轴自上向下增长,也就是相比于一般的纸面上的坐标系,x轴相同,y轴相反。
终端窗口:
如果需要用scanf与printf,需要首先:
1 |
initConsole(); |
然后就可以在弹出的窗口上使用scanf和printf了
启动/结束绘图:
1 2 |
void beginPaint(); void endPaint(); |
任何绘图函数的调用必须在这一对函数调用之间
点:
1 2 |
void putPixel(int x, int y, ACL_Color color); ACL_Color getPixel(int x, int y); |
颜色:
- RGB(r,g,b)
- 红色 -> RGB(255,0 ,0)
- BLACK,RED,GREEN,BLUE,CYAN,MAGENTA,YELLOW,WHITE
比如要画一个带颜色的点:
1 2 |
putPixle(100,150,RGB(255,0,255)); putPixle(150,150,RGB(GREEN)); |
线:
1 2 3 4 5 6 7 8 |
void moveTo(int x,int y); void moveRel(int dx,int dy); void line(int x0,int y0,int x1,int y1); void lineTo(int x,int y); void lineRel(int dx,int dy); void arc(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect, int nXStartArc, int nYStartArc, int nXEndArc, int nYEndArc); |
以上的参数可以试一下来知道其用途,各类参数以下省略。
基础逻辑就是关于点线面,先设置画笔/刷子的参数,然后进行绘画。
交互图形设计:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> #include "acllib.h" int Setup() { initWindow("Test", DEFAULT, DEFAULT, 800, 600); initConsole(); printf("Hello!"); int x; beginPaint(); line(10,10,100,100); scanf("%d", &x); line(100,100,x,0); endPaint(); return 0; } |
程序等待用户输入x,然后画出line
注意,在用户输入x之前initWindow生成的窗口是不会绘画出line(10,10,100,100);生成的线的, "Test"窗口会未响应,只有在输入了x之后才会一并输出这两条线
接下来讲的就是真正的交互:
函数指针及其应用:
函数的地址:
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> int main() { int i = 0; int a[] = {1,2}; printf("%p\n", main); printf("%p\n", a); return 0; } |
直接以%p输出函数名,可以看到地址被输出,并且还比较小。
1 2 |
0040160c 0061fec4 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <stdio.h> void f(void) { } int main() { int i = 0; int a[] = {1,2}; printf("%p\n", main); printf("%p\n", f); printf("%p\n", a); return 0; } |
1 2 3 |
00401612 0040160c 0061fec4 |
每一个函数,都有它的地址。
用函数的名字就可以得到地址
然而用int*p这样的指针变量是不能存放函数地址的。
p的类型是int*,那么如果有一个pf指针,他想指向f,它的类型应该是void (void)
1 2 |
void (*pf)(void) = f; //函数的返回类型 指针名(带空格) 函数参数 |
也不能像数组名那样使用地址:*p[];
需要加上括号来使用:
1 |
(*pf)(); |
应用:
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 |
#include <stdio.h> void f(int i) { printf("in f(), %d\n", i); } void g(int i) { printf("in g(), %d\n", i); } void h(int i) { printf("in h(), %d\n", i); } int main() { int i = 0; void (*fa[])(int) = {f,g,h}; scanf("%d", &i); if(i > 0 && i < sizeof(fa)/sizeof(fa[0])){ //自动判断而不是直接写个3,不出现magic_number (*fa[i])(i); } return 0; } |
根据用户输入的数值运行相应的函数,在这之前想实现此功能只能用if-else或者是swit-case,而函数地址数组的使用可以很方便的去调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <stdio.h> int plus(int a, int b) { return a + b; } int minus(int a, int b) { return a - b; } void cal(int (*f)(int, int)) { printf("%d", (*f)(2, 3)); } int main(void) { cal(plus); printf("\n"); cal(minus); return 0; } |
把函数传进去,让目前调用的函数用 "我传进去的函数"来做事情
回调函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <stdio.h> #include "acllib.h" void mouseListener(int x, int y, int button, int event){ printf("x=%d y=%d button=%d event=%d", x,y,button,event); } int Setup() { initWindow("Test", DEFAULT, DEFAULT, 800, 600); initConsole(); printf("Hello!\n"); registerMouseEvent(mouseListener); beginPaint(); line(10,10,100,100); endPaint(); return 0; } |
一个普通的程序是顺序进行的,什么时候结束是由程序决定的。
一个windows程序不是这样的,上面的代码实现后程序并没有直接结束。
开始 -> 进入WinMain(任何一个Win32程序都是先进入WinMain)(在系统.ACLLib.c里) -> 调用Setup函数 -> Setup函数运行一些东西,结束之后 -> 转到了一个叫消息循环的地方,这个地方是死循环的

左边是main.c里的,右边是ACLLib.c里的
之前如果注册过回调函数,比如鼠标移动,会进入我们写的mouseListener函数,函数做完之后还会回到循环里

但是如果在自己的回调函数里创建死循环,回不去acllib的消息循环,那么甚至会关不掉程序,因为消息循环接受消息,windows窗口的右上角的X也是一个消息。

结合:因为函数有地址,所以才能将函数交给回调函数来监控。
可以处理的四种消息:
1 |
typedef void (*KeyboardEventCallback)(const char key); |
键盘,可以知道一些特殊功能键,对于每个键有按下与抬起两个状态
1 |
typedef void (*CharEventCallback)(int key); |
在键盘上的可读/可现实的字符,很简单,收到按键就是按键
1 2 |
typedef void (*MouseEventCallback)(int x, int y, int button, int status); |
鼠标,可以知道鼠标的移动、鼠标的按下与抬起
1 |
typedef void (*TimerEventCallback)(int timerID); |
定时器。
这四种消息在程序中都是可以回调、可以感知到的。
鼠标:
- button按键:
- 移动中:5
- 没有button:0
- 左键:1
- 中键:2
- 右键:3
- 事件enent:
- 按下:0
- 双击:1
- 抬起:2
- 滚轮抬起:3
- 滚轮按下:4
- 移动:5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
typedef enum { NO_BUTTON = 0, LEFT_BUTTON, MIDDLE_BUTTON, RIGHT_BUTTON } ACL_Mouse_Button; typedef enum { BUTTON_DOWN, BUTTON_DOUBLECLICK, BUTTON_UP, ROLL_UP, ROLL_DOWN, MOUSEMOVE } ACL_Mouse_Event; |
键盘:
- 事件:
- 按键按下:0
- 按键抬起:1
1 2 3 4 5 |
typedef enum { KEY_DOWN, KEY_UP } ACL_Keyboard_Event; |
一个例子:
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 |
#include <stdio.h> #include "acllib.h" void MouseListener(int x, int y, int button, int event){ static int ox = 0, oy = 0; //保存上一次调用函数的值 printf("x = %d, y = %d, button = %d, event = %d\n", x, y, button, event); beginPaint(); line(ox,oy,x,y); //绘图 endPaint(); ox = x; oy = y; //保存 } void KeyListener (int key, int event){ printf("key = %d, event = %d\n", key, event); } int Setup() { initWindow("Test", DEFAULT, DEFAULT, 800, 600); initConsole(); printf("Hello!"); registerMouseEvent(MouseListener); //注册回调函数_鼠标 registerKeyboardEvent(KeyListener); //注册回调函数_键盘 return 0; } |
定时器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include <stdio.h> #include "acllib.h" void MouseListener(int x, int y, int button, int event){ static int ox = 0, oy = 0; //保存上一次调用函数的值 printf("x = %d, y = %d, button = %d, event = %d\n", x, y, button, event); beginPaint(); line(ox,oy,x,y); //绘图 endPaint(); ox = x; oy = y; //保存 } void KeyListener (int key, int event){ printf("key = %d, event = %d\n", key, event); } void TimerListener(int id){ static int cnt = 0; printf("id = %d\n", id); if(id == 0){ cnt++; if(cnt >= 5){ //timer_0执行五次后将其取消 cancelTimer(0); } } } int Setup() { initWindow("Test", DEFAULT, DEFAULT, 800, 600); initConsole(); printf("Hello!\n"); registerMouseEvent(MouseListener); //注册回调函数_鼠标 registerKeyboardEvent(KeyListener); //注册回调函数_键盘 registerTimerEvent(TimerListener); //注册回调函数_计时器 //相当于对ACLLIib说目前有个定时器的接收者 startTimer(0, 500); //启动定时器,每500ms启动一次 startTimer(1, 1000); return 0; } |
MVC设计模式:
MVC设计模式指的是程序由三部分组成:
View | Model | Ctrl
View:
只做一件事,就是从Model取数据,然后画出来。注意,View不是告诉Model干什么,而是他从Model取(Get)数据过来,根据数据把画面画出来,View这部分代码只做一件事情:当需要画东西的时候,从Model取数据然后画、他不管其他任何事情。

Ctrl:
告诉Model什么数据该怎么改。
比如按了向右的方向键,那么Ctrl就告诉Model炮管的角度要向右偏移十五度。
Model:
告诉View,我的数据被改过了。注意:仅仅只是告诉,与此同时,View也不管是往右还是往左了,他也只是画出来

在MVC模式下,Ctrl和View是不直接打交道的,鼠标与键盘的动作不会直接引起图像的改变,引起的是数据的改变,他们隔开来,让Ctrl和View不发生关系,让View写的很单纯。
程序写到后面会有很纠结的问题:代码不知道该写在哪里,MVC模式就很好的解决这个问题。
作业:
翁恺老师的灵魂画作
找个时间做出来吧。
文件:
本周是关于C语言如何做文件和底层操作的。
文件操作,从根本上说,和C语言无关。这部分的内容,是教你如何使用C语言的标准库所提供的一系列函数来操作文件,最基本的最原始的文件操作。你需要理解,我们在这部分所学习的,是函数库的使用,而非C语言。顺便我们还学习了很多和计算机相关的知识,比如重定向、文本文件和二进制文件的江湖恩怨。但是既然不是C语言,也就意味着你将来的工业环境下,未必还会使用这么原始的文件操作函数了。这些函数,只是一个标本,让你知道可以这样来操纵文件。但是,不见得所有的库都是以这样的方式来操纵文件的。
围绕文件操作,还有一个C语言受时代局限,处理得不够好的东西,就是错误处理。因为文件操作的每一个步骤都很可能在实行过程中遇到问题:文件打不开啦,读了一半出错啦,等等等等,所以设计文件操作函数如何反馈和处理这类运行时刻的问题是需要很好的手段的。C的这个函数库,主要是通过特殊的返回值来实现的。后来的语言,如C++和Java,则引入了异常机制来处理这些事情。
关于底层操作,我们主要是介绍了按位操作,包括按位的运算、移位和位段。这些操作在日常程序中难得遇到,主要是用于接触硬件的底层程序的。这些内容在本课程是选读的。
本周我们不安排课堂小测验和编程题。
格式化的输入输出:
printf和scanf的格式化。
Printf的格式化:
格式字符串:%[flags][width][.prec][hlL]type
[Flag]:
[Flag] | 含义 |
- | 左对齐(输出空间有余量时才有意义) |
+ | 在前面放+或-(其实就是强制输出加号) |
(space) | 正数留空 |
0 | 0填充 |
-:
1 2 |
printf("%9d\n", 123); //9是[width] printf("%-9d\n, 123"); |
[width]:9的意思是这个数字(123)的输出要占据九个字符的空间。
-9d就是左对齐占九个字符
+:
强制输出加号
0:
在前面用0填充空位,同样需要有空余的字符空间
所以在左对齐的情况下不能填0
[width] & [.prec]:
width 或 prec | 含义 |
number | 最小字符数 |
* | 下一个参数是字符数 |
.number | 小数点后的位数 |
.* | 下一个参数是小数点后的位数 |
number与.number:
如果将123以%9.2f输出,将得到[ 123.00]即三个空格、123、小数点、小数点后两位数(零)
其中9是number、.2是.number。
* 与 .*:
*会将字符串后面的参数作为字符数,也就是以参数的方式代替number
.*同理,是以参数的方式代替.number
所以代码如果是printf("%*d\n", 6, 123);将输出[ 123]也就是三个空格和123
这意味着可以通过变量作为参数的方式来动态更改输出形式。
[hlL]:
类型修饰 | 含义 |
hh | 单个字节(对整数来说把他当作char单个字节输出) |
h | short |
l | long |
ll | long long |
L | long double |
如果代码是这样的:[printf("%hhd\n", 12345);]
那么输出的是57,因为12345的16进制是0x3039,char只取了最低位39。
type:
type | 用于 | type | 用于 |
i 或 d | int | g | float |
u | unsigned int | G | float |
o | 八进制 | a 或 A | 十六进制浮点 |
x | 十六进制 | c | char |
X | 字母大写的十六进制 | s | 字符串 |
f 或 F | float, 6 | p | 指针 |
e 或 E | 指数 | n | 读入/写出的个数 |
注意,n我们之前没有见过。
1 2 3 4 5 6 7 8 9 10 |
int num; printf("%hhd%n\n", (char)12345, &num); printf("%d\n", num); printf("%d%n\n", 12345, &num); printf("%d\n", num); printf("%dty%n\n", 12345, &num); printf("%d\n", num); |
程序的输出如下:
1 2 3 4 5 6 |
57 2 12345 5 12345 7 |
所以%n的意思是检测 当printf做到%n的时候,已经输出了多少字符。然后填到参数中给的指针所指的那个变量里去。
scanf的格式化:
格式字符串:%[flag]type
[Flag]:
flag | 含义 | flag | 含义 |
* | 跳过 | l | long, double |
数字 | 最大字符数 | ll | long long |
hh | char | L | long double |
h | short |
*:
比如%*d表示要跳过这个整数的输入
比如要这样:
1 2 3 |
scanf("%*d%d", &num); //然后用户输入了123 456 printf("%d\n", num); |
程序会输出456。
type:
type | 用于 | type | 用于 |
d | int | s | 字符串(单词) |
i | 整数,可能为十六进制或八进制 | [...] | 所允许的字符 |
u | unsigned int | p | 指针 |
o | 八进制 | ||
x | 十六进制 | ||
a,e,f,g | float | ||
c | char |
%i:
是一种灵活的输入,比如有如下代码:
1 2 3 |
int num; scanf("%i", &num); printf("%d", num); |
运行三次,分别输入123、0x12、012:
1 2 3 4 5 6 7 8 |
(输入)123 (输出)123 (输入)0x12 (输出)18 (输入)012 (输出)10 |
[...]:
我们可能从用户那得到这样的信息:

(这是GPS模块会产生的1083的协议数据)
1 |
scanf("%*[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]",sTime,sAV,sLati,&sNW,sLong,&sEW,sSpeed,sAngle,sDate); |
[^,] 的意思是到(,)逗号之前的所有的东西,但是又有一个*星号,所以从一开始读,到逗号为止,这前面的东西都跳过忽略,也就是$GPRMC,然后在[^,]之后是逗号,这代表程序读掉一个逗号。然后就是%[^,]表示到第二个逗号前的任何东西他作为一个字符串交给sTime这个字符串变量。
这个方括号的用法比较复杂,目前不做过多研究(?)
printf和scanf的返回值:
- scanf的返回值代表着读入的项目数(读了几个变量)
- printf的返回值代表输出的字符数
在要求严格的程序中(比如长期运行的程序),应该判断每次调用scanf或printf的返回值,从而了解程序运行中是否存在问题。
1 2 3 4 |
int num; int i1 = scanf("%i", &num); int i2 = printf("%d\n", num) printf("%d:%d", i1, i2); |
如果输入1234
程序将会输出:
1 2 |
1234 1:5 |
1代表scanf读入了一个1234,代表printf输出了1234和一个换行。
文件输入输出
用>和<做重定向:
交互的情况下:

第二行是输入12345,程序会输出12345和1:6
在运行test的时候后面加>大于号然后加上文件

第二行是用户输入12345,命令行没有显示输出,因为输出的数据在12.out中
(打开12.out文件)

这个时候如果写一个文件12.in,他的内容是12345


运行test的时候用<小于号说 他是从12.in输入的

那么命令行会直接输出结果
也可以在12.in输入,然后将结果输出到12.out

但是上述方式并不是一般的方式
FILE:
在stdio.h里已经声明好了叫做FILE*的这种指针
1 2 3 4 |
FILE* fopen(const char * restrict path, const char * restrict mode); int fclose(FILE *stream); fscanf(FILE*, ...); fprintf(FILE*, ...); |
打开文件的标准代码:
1 2 3 4 5 6 7 8 9 10 11 |
FILE* fp = fopen("file", "r"); //需要先定义一个FILE*类型的变量(其实FILE是结构) //"file"中应该写文件名,"r"表示读 if(fp){ //fp作为一个fopen的返回值他表示文件是否打开,如果没有打开文件fopen会返回NULL fscanf(fp, ...); fclose(fp); //最后要关掉 } else{ ... } |
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <stdio.h> int main(int argc, char const *argv[]) { FILE* fp = fopen("12.in", "r"); //用fopen打开12.in这个文件 if(fp){ //如果打开了 int num; fscanf(fp, "%d", &num);//fscanf的第一项是FILE*,后面就和scanf是一样的了 printf("%d\n", num); fclose(fp); } else{ printf("无法打开文件"); } return 0; } |

把运行文件命名为test
执行test
程序输出了12345(因为12.in有12345)
删除12.in
执行test
输出了无法打开文件
fopen:
fopen有两个参数,第一个参数是文件名,第二个参数如下:
r | 打开只读 |
r+ | 打开读写,从文件头开始 |
w | 打开只写。如果不存在则新建,如果存在则清空 |
w+ | 打开读写。如果不存在则新建,如果存在则清空 |
a | 打开追加。如果不存在则新建,如果存在则从文件尾开始 |
..x | 只新建,如果文件已存在则不能打开 |
r+是打开文件读,也可以写,是从头读,也是从头写(一般用来修改)
w+是打开文件写,并且可以读。因为会清空以前的东西所以w+一开始是没东西可读的,但他可以读你写进去的东西。
a是追加,不存在新建,存在的话不清空原来的东西,从末尾开始加进去
在这五种参数中都可以加上x(但是其实主要是针对w和a(老师好像口误了,貌似是w和w+)), 只新建,如果文件已存在则不能打开。这样可以避免对已有的文件做破坏。
二进制文件:
其实所有的文件最终都是二进制的
文本文件无非是用最简单的方式可以读写的文件
more、tail、cat、vi
而二进制文件是需要专门的程序来读写的文件
文本文件的输入输出是格式化,可能经过转码
文本与二进制:
- Unix喜欢用文本文件来做数据存储和程序配置
- 交互式终端的出现使得人们喜欢用文本和计算机"talk"
- Unix的shell提供了一些读写文本的小程序
- Windows喜欢用二进制文件
- DOS是草根文化,并不继承和熟悉Uinx文化
- PC刚开始的时候能力有限,DOS的能力更有限,二进制更接近底层
- 文本的优势是方便人类读写,而且跨平台
- 文本的缺点是程序输入输出要经过格式化,开销大
- 二进制的缺点是人类读写困难,不跨平台
- int大小不一致、大小端的问题...
- 二进制的优点是程序读写快
一个程序为什么要文件:
- 配置
- Unix用文本,Windows用注册表
- 数据
- 稍微有点量的数据都放数据库了
- 媒体
- 这个只能是二进制的
- 现实是,程序通过第三方库来读写文件,很少直接读二进制文件了。
(注册表是一个非常大的二进制文件,整个Windows里所有软件的配置信息全部写在里面。)
所以不再有机会去做这么底层的事情了。
如果一定要做(?):
二进制读写:
fscanf和fprintf是对文本文件的格式化的输入输出,对二进制的数据我们用:

(不听,跳过。)
第七周-7.1文件
位运算:
C语言有一些比较接近底层的操作,这些操作如果不是要直接操作硬件,直接去做一些接近底层的东西一般是用不到的
这章就讲一些这样的操作
按位运算:
C语言有这些按位运算的运算符:
& | 按位的与 |
| | 按位的或 |
~ | 按位取反 |
^ | 按位异或 |
<< | 左移 |
>> | 右移 |

比如0101 1010 (这个是十六进制的5A)
他和1000 1100 (这个是是十六进制的8C)
这两个数的按位与是:0000 1000 得到08 因为全1的位数是1,其他都0
图中所说的应用:
某位为0:0xFE与任意一个数按位与,会将最后一位变成0,因为0xFE是1111 1110
取一段:0xFF是1111 1111

int是32个bit,四个字节,前三个都是00,第四个是FF的话第四个字节的八个bit就是1111 1111,那么与另一个数按位与就只剩下第四个字节。

比如1010 1010 (这个是十六进制的AA)
他和0101 0100 (这个是是十六进制的54)
取按位或得到1111 1110 是FE
应用:
使得一位或几位为1: x | 0x01 (让x最右边为1)
两数拼起来:0x00FF | 0XFF00 结果是0xFFFF



因为在计算机内部实际上只有按位运算
(最后一行好像应该是~4->3而!4->~1->0)

比如1011 0100
他和0100 1011
异或的结果是1111 1111 因为每一位都不相等
异或两次:



这俩跳过

7.2的第三个视频[位运算例子]也跳过

位段也跳过

搜索:
在一个数组中找到某个数的位置(或确认是否存在)
基本方法:遍历(线性搜索)
还有二分搜索,然后就没了!
后语:
就这么戛然而止了!
好想再多跟翁恺老师学一些,不知道老师还会不会开C#的课,但是不管怎样我都要继续前进了,接下来是C#与Unity的学习,我这次要制作一个满意的游戏。
本笔记内直接或间接引用了如下内容:
- 猿问-C 关于malloc()函数的疑问-onemoo :
http://www.imooc.com/wenda/detail/324009 - 芯语-可重入与不可重入函数的区别-strongerHuang :
https://www.eet-china.com/mp/a28562.html - GitHub-ACLLib-WengKai:
https://github.com/wengkai/ACLLib