说明:本文档为浙大翁恺老师《C 语言程序设计进阶》课程内容笔记,主要记录学习过程中的一些重要或自己不懂的知识点,方便随时反复查看,内容不一定适合其他人。
如果想看 C 语言基础版的,可以移步:浙大翁恺老师《程序设计入门——C语言》笔记 | Yam。
小感想:自从大学上过这门课后就再没碰过了。这次学完两门课,写了一些代码,才慢慢有了一些感觉。这种感觉不光是对 C,更是对写代码和深入探索未知的过程。虽然有时候也会情绪低落、状态低迷,什么都不想做,但整体还是蛮开心的,也从没有想过放弃。希望能在这条路上与更多的小伙伴同行。
目录
Week1:指针与字符串
指针的使用
指针的应用场景1
-
交换两个变量的值
1
2
3
4
5
6void swap(int *pa, int *pb)
{
int t = *pa;
*pa = *pb;
*pb = t;
} -
函数返回多个值,某些值就只能通过指针返回
- 传入的参数实际上是需要保存带回的结果的变量
指针的应用场景2
- 函数返回运算的状态,结果通过指针返回
- 常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错
- -1 或 0
- 但是当任何数值都是有效的可能结果时,就得分开返回
- 状态用函数返回
- 实际的值通过指针参数返回
- 常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错
1 | int divide(int a, int b, int *result); |
指针常见的错误
- 定义了指针变量,还没有指向任何变量,就开始使用指针
- 任何一个地址变量,没有被赋值(没有得到任何实际变量)之前,不能通过
*p去访问任何变量或数据。因为没有初始化的 p 指向的地方不一定能写,所以不一定总是能成功,总是会出错的。 int *p时,p 没有明确的值。如果被当做地址,可能会指向一个奇怪的地方,如果给*p赋值,恰好那个奇怪的地方不能写,程序会出错。- 正确的声明方式:理解 C 语言中指针的声明以及复杂声明的语法 - CSDN 博客
- 任何一个地址变量,没有被赋值(没有得到任何实际变量)之前,不能通过
指针与数组
- 函数参数表中的普通变量是值;
- 函数参数表中的指针变量是指针的值,即地址;
- 函数参数中的数组变量实际是指针
sizeof(a) == sizeof(int *)- 但是可以用数组的运算符
[]进行运算
- 以下四种函数原型是等价的(在参数表中出现时是等价的):
int sum(int *ar, int n);int sum(int *, int);int sum(int ar[], int n);int sum(int [], int);
- 实际上,数组变量是特殊的指针
- 数组变量本身表达地址,所以
int a[10]; int *p = a; // 无需用 & 取地址- 但是数组的单元表达的是变量,需要用
&取地址 a == &a[0]
[]运算符可以对数组做,也可以对指针做p[0] <==> a[0]
*运算符可以对指针做,也可以对数组做*a = 25;
- 数组变量是 const 的指针,不能被赋值
int b[] <==> int * const b;
- 数组变量本身表达地址,所以
指针与 CONST
-
指针本身可以是 const
- 表示一旦得到了某个变量的地址,不能再指向其他变量
int * const q = &i; // q 是 const*q = 26; // OKq++; // ERROR
- 表示一旦得到了某个变量的地址,不能再指向其他变量
-
所指向的值也可以是 const
- 表示不能通过指针去修改那个变量(并不能使得那个变量成为 const)
const int *p = &i;*p = 26; // ERROR (*p) 是 consti = 26; // OKp = &j; // OK
- p 可以指向其他,i 可以被赋其他值,但不能通过 p 做赋值
- 表示不能通过指针去修改那个变量(并不能使得那个变量成为 const)
-
const 在
*的前面还是后面?- 前面表示所指向的值是 const:
const int* p = &i;int const* p = &i;
- 后面表示指针是 const:
int *const p = &i;
- 前面表示所指向的值是 const:
-
转换
-
总是可以把一个非 const 的值转换成 const 的
1
2
3
4
5
6void f(const int* x);
int a = 15;
f(&a); // ok
const int b = a;
f(&b); // ok
b = a + 1; // error -
当要传递的参数的类型比地址大的时候,这是常用的手段:既能用比较少的字节数传递值给参数,又能避免函数对外面的变量的修改。
-
-
const 数组
const int a[] = {1,2,3,4,5,6};- 数组变量已经是 const 的指针了,这里的 const 表明数组的每个单元都是
const int - 所以必须通过初始化进行赋值
-
保护数组值
- 因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值
- 为了保护数组不被函数破坏,可以设置参数为 const:
int sum(const int a[], int length);
练习题
-
对于:
1
2
3
4int a[] = {5, 15, 34, 54, 14, 2, 52, 72};
int *p = &a[5]; // *p = 2
// int *p = a; 等价于 int *p = &a[0];
// int *p = &a[5]; 从 a[5] 的地址开始则
p[-2]的值是?54 -
如果:
1
2int a[] = {0};
int *p = a; // 等价于 int *p = &a[0];则:
p == &a[0]*p == a[0]p[0] == a[0]- 注意:与赋值时不同。
指针运算
-
指针加1,指针移动到下一个单元,实际加的是
sizeof(类型)int a[10];int *p = a;*(p+n) --> a[n],无论是 char 还是 int,加 n 就等于地址加 n,只是 int 每次地址增加 4,char 每次增加 1

-
如果指针不是指向一片连续分配的空间,如数组,则加 1 这种运算没有意义。
-
这些算术运算可以对指针做:
- 给指针加、减一个整数(+ += - -=)
- 递增递减(+±-)
- 两个指针相减:结果不是地址差,而是
地址差/sizeof
-
*p++- 取出 p 所指的数据,完事之后把 p 移到下一个位置去
*的优先级没有++高- 常用于数组类的连续空间操作
- for 循环
for (int i=0; i<sizeof(p)/sizeof(p[0]); i++) { printf("%d\n", p[i]) }; while (*p != -1) { printf("%d\n", *p++) };
- for 循环
- 在某些 CPU 上,这可以直接被翻译成一条汇编指令,跑的快,看机器。
-
指针比较
< <=, ==, >, >=, !=都可以对指针做- 比较它们在内存中的地址
- 数组中的单元的地址肯定是线性递增的
-
0 地址
- 内存中有 0 地址,但通常不能随便动
- 所以指针不应该具有 0 值
- 因此可以用 0 地址来表示特殊的事情:
- 返回的指针是无效的
- 指针没有被真正初始化(先初始化为 0)
NULL是一个预定义的符号,表示 0 地址- 有的编译器不愿意你用 0 来表示 0 地址
- 建议用
NULL而不是 0
-
指针的类型
-
无论指向什么类型,所有的指针的大小都是一样的,因为都是地址
-
但是指向不同类型的指针不能直接互相赋值

-
这是为了避免用错指针
-
-
指针的类型转换
void*表示不知道指向什么东西的指针- 计算时与
char*相同(但不想通)
- 计算时与
- 指针也可以转换类型
int *p = &i; void *q = (void*) p;
- 这并没有改变 p 所指的变量的类型,而是让后人用不同的眼光通过 p 看它所指的变量
- 通过 p 看 i,是 int
- 通过 q 看 i,是 void
指针用途小结
- 需要传入较大的数据时用作参数
- 传入数组后对数组做操作
- 函数返回不止一个结果:需要用函数来修改不止一个变量
- 动态申请的内存
动态内存分配
- 输入数据
- C99 可以用变量做数组定义的大小:
int a[n]; - C99 之前:
int *a = (int*)malloc(n*sizeof(int));,定义之后按正常数组操作即可。 malloc的定义,sizeof(a)会比 C99 的小 4。malloc用完用完之后,需要free
- C99 可以用变量做数组定义的大小:
malloc#include <stdlib.h>- 向 malloc 申请的空间的大小是以字节为单位的
- 返回的结果是
void*,需要类型转换为自己需要的类型(因为是字节嘛,内存并不 care 存哪种类型)(int*)malloc(n*sizeof(int))
- 没空间了?
- 如果申请失败则返回 0,或者叫 NULL
- 系统能给多大空间?我的 Mac:分配了 134208900MB 的空间
free()- 把申请的空间还给 “系统”
- 申请的空间都应该要还
- 只能还申请来的空间的首地址
free(NULL)没问题,因为 NULL 肯定不是申请来的。所以好的习惯是初始化指针时,先让它等于 0:void *p = 0;
- 常见问题:
- 申请了没 free,长时间运行内存逐渐下降
- free 过了再 free
- 地址变过了,直接去 free
练习题
-
int a[] = {1,2,3,4,5,}; int *p = a; int *q = &a[5]; printf("%d", q-p); // 5,地址差/sizeof- **结构指针作为参数**(K&R P.131) - **指向结构的指针** - 用 -> 表示指针所指的结构变量中的成员1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
## 字符串操作
### 单字符串输入输出
- `putchar`
- `int putchar(int c);`
- 向标准输出写一个字符,`int` 类型,一次一个字符
- 返回写了几个字符,正常情况下为 1,EOF(-1) 表示写失败
- `getchar`
- `int getchar(void);`
- 从标准输入读入一个字符
- 返回类型是 `int` 是为了返回 EOF(-1)
- `Ctrl+D(unix); Ctrl+Z(win)`
- 为什么输入回车才打印出来?SHELL,都是在 SHELL 的缓存区读东西,用户的输入是让 SHELL 填缓存区。
### 字符串数组
- `char **a`: a 是一个指针,指向另一个指针,那个指针指向一个字符(串)
- `char a[][]`:二维数组第二位一定要有确切的大小。
- `char a[][10];`:a 是个数组,a 里面每一个都是 `char[10]`,因此需要确定每一个 `char[10]` 的长度是否超过 10
- `char *a[];` `a[0]` 相当于 `char*`
- 
- 程序参数
- `int main(int argc, char const *argv[])`
- `argv[0]` 是命令本身:当使用 Unix 的符号链接时,反映符号链接的名字(到底是运行的链接还是本身)
- 可以读到执行程序时,名字后面跟上的内容。第一行就是那个可执行程序的名字(如 a.out)。
## 字符串函数的实现
- `string.h`
- strlen, strcmp, strcpy, strchr, strstr
- 详见:[浙大翁恺老师:C语言入门](https://yam.gift/2018/06/20/C/2018-06-20-C-Weng-ZhejiangUniversity/)
# Week2:ACLLib 的基本图形函数
## ACLLib 入门
- 基于 Win32API 的函数库
- 可以在 MSVC 和 DevC++ 中使用
## WindowsAPI
- 纯 C 函数库
- main vs WinMain
- 如何产生窗口?窗口结构。
- 如何在窗口画东西?DC(设备上下文)。
- 如何获得用户鼠键操作?消息循环和消息处理代码。
- 如何画出标准界面(菜单、按钮、输入框等)?不支持。
## 创建 ACLLIB 程序
- `int Setup()`
- `initWindow("name", loc1,loc2, width, height)`: 初始化窗口
- `void begainPaint();line(loc1,loc2,loc3,loc4);void endPaint();`: 绘制
- `initConsole();`: 交互窗口
## 基本绘图函数
- 点:
- `void putPixel(int x, int y, ACL_Color color);`
- `ACL_Color getPixel(int x, int y);`
- 颜色:
- RGB(r,g,b)
- BLACK, RED, GREEN...
- 线:
- `void moveTo(int x, int y);` 移动坐标点
- `void moveRel(int dx, ind dy);`
- `void line(int x0, int y0, int x1, int y1);`
- `void lintTo(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);` 弧
- 画笔
- `void setPenColor(ACL_Color color);`
- `void setPenWidth(int width);`
- `void setPenStyle(ACL_Pen_Style style);`
- `PEN_STYLE_SOLID`
- `PEN_STYLE_DASH ----`
- `PEN_STYLE_DOT ……`
- `PEN_STYLE_DASHDOT -.-.-.-`
- `PEN_STYLE_DASHDOTDOT -..-`
- `PEN_STYLE_NULL` 看不见的线
- 面:
- 扇形
- 椭圆
- 矩形
- ……
- 刷子
- color
- style
- 文字
## Win 下命令行编译程序
- 直接运行 `mm` 即可编译
# Week3:结构类型
## 枚举
- 常量符号化:`const int name = 0;`
- `enum COLOR {RED, YELLOW, GREEN};`:分别是 0 1 2,只能是 int
- 当需要一些可以排列起来的常量值时,定义枚举的意义就是给这些常量值名字。
- 套路:自动计数的枚举 `enum COLOR {RED, YELLOW, GREEN, NumCOLORS};` 最后一个变量是 3,就是枚举量的数量。
- 声明枚举量的时候可以指定值
- 如果有异议上排比的名字,枚举比 const int 方便;比宏好,因为枚举有 int 类型。
## 结构
- 声明结构类型
- 内部声明只能内部使用,通常放在所有函数外面
- 声明结构的形式
- `struct point {int x; int y;}; struct point p1, p2;`
- `struct {int x; int y;} p1, p2;`
- `struct point {int x; int y;} p1, p2; ` 比较常用,做了两件事情
- 结构的初始化
- `struct date today = {07, 31, 2014};`
- `struct date thismonth = {.month = 7, .year=2014};` 没给值默认为 0
- 结构成员
- 结构和数组有点像
- 数组用 `[]` 运算符和下标访问其成员;结构用 `.` 运算符和名字访问其成员(**要用结构变量的名字,而不是结构的名字**)。
- 结构运算
- 要访问整个结构,直接用结构变量的名字
- 对于**整个结构**,可以做赋值、取地址,也可以传递给函数参数
- `pl=(struct point){5,10}; //相当于 pl.x = 5; pl.y = 10;`
- `p1 =p2; //相当于 p1.x = p2.x; p1.y = p2.y;`
- 这两种运算数组不能做。
- 结构指针
- 和数组不同,结构变量的名字并不是结构变量的地址,必须用 & 运算符
- `struct date *pDate = &today;`
- 结构作为函数参数
- `int numberOfDays(struct date d)`
- 整个结构可以作为参数的**值**传入函数
- 这时候是在函数内部新建一个结构变量,并赋值调用者的结构的值
- 也可以返回一个结构
- 输入结构
- 没有直接的方式可以一次 `scanf` 一个结构
- **C 语言在函数调用时是传值的**:所以函数中的 p 与 main 中的 y 是不同的,函数读入了 p 的数值之后,没有任何东西回到 main,所以 y 还是 `{0, 0}`
- 解决方案:
- 之前方案,把一个结构传入函数,然后在函数中操作,但是没有返回去。问题在于传入函数的是外面那个结构的克隆体,不是指针。**传入结构和传入数组是不同的!**
- 我们可以,在输入函数中,创建一个临时的结构变量,然后把这个结构返回给调用者:
```C
void main()
{
struct point y = {0,0};
y = inputPoint();
output(y);
}
struct point inputPoint()
{
struct point temp;
scanf("%d", &temp.x);
scanf("%d", &temp.y);
return temp;
}- 结构参数指针1
2
3
4
5
6
7
8struct date {
int month;
int day;
int year;
} myday;
struct date *p = &myday; // myday 的地址交给指向 struct date 的指针
(*p).month = 12;
p->month = 12;- 完整的例子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void main()
{
struct point y = {0, 0};
intputPoint(&y);
output(y);
}
// 传入一个指针,处理后返回该指针
// 好处是,将来可以串到其他函数的调用当中
struct point* intputPoint(struct point *p)
{
scanf("%d", &(p->x));
scanf("%d", &(p->y));
return p;
}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
struct point
{
int x;
int y;
};
struct point* getStruct(struct point*);
void output(struct point); // 要的是值
void print(const struct point *p); // 要的结果不需要改变
int main(int argc, char const *argv[])
{
struct point y = {0, 0};
getStruct(&y);
output(y);
output(*getStruct(&y)); // output 要的是结构本身,用 * 取出函数返回的东西
print(getStruct(&y));
*getStruct(&y) = (struct point){1, 2}; // 这样也可以
}
struct point* getStruct(struct point *p)
{
scanf("%d", &p->x); // 取得 p 所指的 x
scanf("%d", &p->y);
printf("%d, %d\n", p->x, p->y);
return p;
}
void output(struct point p)
{
printf("%d, %d\n", p.x, p.y);
}
// print 函数不需要修改
void print(const struct point *p)
{
printf("%d, %d\n", p->x, p->y);
} -
结构数组
struct date dates[100];struct date dates[] = { {4,5,2005},{2,4,2005} };
-
结构中的结构(嵌套的结构)
struct dataAndTime { struct date sdate; struct time stime;};1
2
3
4
5
6
7
8
9
10struct point {
int x;
int y;
};
struct rectangle {
struct point pt1;
struct point pt2;
};
如果有 struct rectangle r;
可以有:r.pt1.x, r.pt1.y, r.pt2.x, r.pt2.y1
2
3
4
5
6
7
8如果有变量定义:
struct rectangle r, *rp;
rp = &r;
下面表达式等价:
r.pt1.x
(r.pt1).x
rp->pt1.x
(rp->pt1).x -
结构中的结构的数组
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
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} }
};
for (i=0; i<2; i++) {
printRect(rects[i]);
}
} -
练习
1
2
3
4
5
6
7struct {
int x,y;
} s[2] = {
{1,3},
{2,7},
};
printf("%d\n", s[0].y/s[1].x); // 3/2 -
自定义数据类型 typedef
-
用来声明一个已有的数据类型的新名字
-
typedef int Length;Length a,b,len;就相当于int a,b,len; -
typedef 和 最后一个单词中间的就是原来的类型:
1
2
3
4
5
6typedef struct ADate {
int month;
int day;
int year;
} Date;
Date d = {9,1,2005};1
2
3
4
5
6
7typedef struct {
int month;
int day;
int year;
} Date;
如果没有 typedef,则表示:有个 struct,没有名字,有个变量叫 Date
如果有 typedef,表示:这样的 struct,命名为 Date,不关心 struct——叫什么
-
联合
-
存储
- 所有成员共享一个空间
- 同一时间只有一个成员是有效的
- union 的大小是其最大的成员
-
初始化
- 对第一个成员做初始化
-
小端(低位在前)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef union {
int i;
char ch[sizeof(int)];
} CHI;
int main(int argc, char const *argv[])
{
CHI chi;
int i;
chi.i = 1234;
for (i=0; i<sizeof(int); i++) {
printf("%02hhX\n", chi.ch[i]);
}
return 0;
}
-
应用场合:得到内部的各个字节
Week4:链表
可变数组
Array array_create(int init_size);- 不要定义指针类型,因为没办法创建本地变量,所以最好只定义成结构。
void array_free(Array *a);int array_size(const Array *a);int* array_at(Array *a, int index);void array_inflate(Array *a, int more_size);
1 | // 一种参考 |
-
可变数组的缺陷
-
每一次增大时都要申请新的内存空间,然后拷贝。拷贝需要耗费时间。
-
可能会申请不到:

-
解决办法:每次申请一个 BLOCK 大小的内存,然后和已有的链起来。既避免了拷贝(节约了时间)又充分利用了内存。
-
链表
-
数据+指针,指针指向下一个,有个 HEAD 和结尾的标记:链表。
1
2
3
4
5
6
7
typedef struct _node {
int value;
struct _node *next; // 指向下一个一样的类型
} Node;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
int main(int argc, char const *argv[]) {
Node * head = NULL;
int number;
do {
scanf("%d", &number);
if (number != -1) {
// addd to linked-list
Node *p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
// find the last
Node *last = head;
if (last) { // 当只有一个时
while (last->next) {
last = last->next;
}
// attach
last->next = p;
} else {
head = p;
}
}
} while (number != -1);
} -
链表的函数(4.2 第三个视频)
- 全局变量
Node *head;:不好,只能使用一次。 - return 一个 head:使用时必须
head = add(head, number);但又无法强迫。 - 把 head 的指针传进去:
add(&head, number); - 使用一个自定义的结构体,便于扩充。
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
typedef struct _list {
Node* head;
} List;
void add(Node* head, int number);
void print(List *pList)
int main(int argc, char const *argv[]) {
List list;
list.head = NULL;
int number;
do {
scanf("%d", &number);
if (number != -1) {
// 添加数字给一个 链表
add(&list, number);
}
} while (number != -1);
// 打印
print(&list);
// 找到一个数字并删除
scanf("%d", &number);
Node *p;
int isFound = 0;
for (p=list.head; p; p=p->next) {
if (p->value == number) {
printf("找到了\n");
isFound = 1;
break;
}
}
if (!isFound) { printf("没找到\n"); }
// 删除
Node *q;
for (q=NULL, p=list.head; p; q=p, p=p->next) {
if (p->value == number) {
if (q) {
q->next = p->next;
} else { // q 为 NULL,刚进去的时候
list.head = p->next;
}
free(p);
}
}
// 清除
for (p=head; p; p=q) {
q = p->next;
free(p);
}
return 0;
}
void add(List* pList, int number) {
// addd to linked-list
Node *p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
// find the last
Node *last = pList->head;
// 每次从 head 开始指向,是否可以直接从 tail 开始指
if (last) {
// 当只有一个时
while (last->next) {
last = last->next;}
// attach
last->next = p;
} else { pList->head = p;}
}
void print(List *pList) {
// 遍历 print(&list)
Node *p;
for (p=list.head; p; p=p->next) {
printf("%d\t", p->value);
}
printf("\n");
} - 全局变量
-
链表的搜索
-
链表的删除
- 前面的指针指向下一个
- free
-
链表的清除
Week5:程序结构
全局变量
-
初始化
- 没有做初始化的全局变量会得到 0 值:指针会得到 NULL 值
- 只能用编译时刻已知的值来初始化全局变量
int gAll=12; int g2=gAll;不行const int gAll=12; int g2=gAll;可以- 建议:全局变量的值不应该和另外的全局变量的值联系在一块
- 它们的初始化发生在 main 函数之前
-
被隐藏的全局变量
- 如果函数内部存在于全局变量同名的变量,则全局变量被隐藏
-
静态本地变量
- 本地变量定义时加上 static 即为静态本地变量,不初始化时值为 0
- 当函数离开的时候,静态本地变量会继续存在并保持其值
- 静态本地变量的初始化只会在第一次进入这个函数时做,以后进入函数时会保持上次离开的值
- 静态本地变量实际上是特殊的全局变量,它们位于相同的内存区域
- 静态本地变量具有全局的生存期,函数内的局部作用域:static 的意思是局部作用域(本地可访问)
-
*返回指针的函数- 返回本地变量的地址是危险的;返回全局或静态本地变量的地址是安全的
- 返回在函数内 malloc 的内存是安全的,但容易造成问题,最好的做法是**返回传入的指针*
-
tips
- 不要使用全局变量来在函数间传递参数和结果
- 尽量避免使用全局变量
- 使用全局变量和静态本地变量的函数是线程不安全的
编译预处理和宏
宏定义
- 编译预处理指令
#开头的是编译预处理命令- 但不是 C 语言成分
#define用来定义一个宏
#define#define <名字> <值>- 没有结尾分号,因为不是 C 语言
- 名字必须是一个单词,值可以任意
- 在 C 语言编译器开始编译前,编译预处理程序(cpp)会把程序中的名字替换掉(完全的文本替换)
gcc --save-temps:.c->.i->.s->.o->a.out.i由 C 编译器编译,产生汇编代码文件.s- 汇编代码文件
.s汇编后成为目标代码文件.o - 目标代码文件
.o通过链接生成可执行文件a.out
- 宏
- 如果一个宏的值中有其他的宏的名字,也会被替换
- 如果一个宏的值超过一行,最后一行之前的行末要加
\ - 宏的值后面出现的注释不会被当作宏的值的一部分
- 没有值的宏
#define_DEBUG- 这类宏用于条件编译
- 预定义的宏
__LINE__行号__FILE__文件名__DATE__日期__TIME__时间__STDC__
带参数的宏
- 像函数的宏
#define cube(x) ((x)*(x)*(x))- 宏可以带参数,不管参数的类型
- 错误定义的宏
#define RADTODEG(x) (x*57.29578)#define RADTODEG(x) (x)*57.29578
- 带参数的宏的原则
- 一切都要括号
- 整个值要括号
- 参数出现的每个地方都要括号
- 一切都要括号
- 带参数的宏
- 可以带多个参数
- 也可以组合(嵌套)使用其他宏
- 使用很普遍,可以很复杂
- 部分宏会被 inline 函数替代
- 结尾不要加分号
练习题
1 |
|
大程序结构
大程序
- 多个
.c文件- main() 里的代码太长了适合分成几个函数
- 一个源代码文件太长适合分成几个文件
- 两个独立的源代码文件不能编译形成可执行的程序
- 项目
- 把源代码文件放在一个项目下
- 会把所有源代码文件编译后链接起来
头文件
- 把函数原型放到一个头文件(
.h结尾)中,在需要调用这个函数的源代码文件(.c文件)中#include这个头文件,就能让编译器在编译的时候知道函数的原型。 #include- 编译预处理指令,和宏一样,在编译之前就处理了
- 它把那个文件的全部文件内容原封不动地插入到它所在的地方
- 所以也不是一定要在
.c文件的最前面#include
gcc *.c -c参数 c 的作用是只编译,不输出可执行文件- 自己的用
"",系统的用<> #include的误区- 不是用来引入库的
- C 语言默认会引入所有的标准库
stdio.h只有 printf 的原型,printf 的代码在另外的地方:某个.lib(win)或.a(Unix)中#include <stdio.h>只是为了让编译器知道 printf 函数的原型,保证在调用时给出的参数值是正确的类型
- Tips
- 使用和定义这个函数的地方都应该
#include这个头文件 - 一般的做法就是任何的
.c都有对应的.h,把所有对外公开的函数的原型和全局变量的声明都放进去,全局变量在多个.c文件中可以共享,但是需要用恰当的方式。
- 使用和定义这个函数的地方都应该
- 不对外公开的函数
- 在函数前面加上 static 就使得它成为只能在所在的编译单元中被使用的函数
- 在全局变量前面加上 static 就使得它成为只能在所在的编译单元中被使用的全局变量
声明
-
其他
.c使用全局变量,在.h中声明,如:extern int gAll; -
int i;变量定义;extern int i;变量声明 -
声明和定义
- 声明是不产生代码的:
- 函数原型
- 变量声明
- 结构声明
- 宏声明
- 枚举声明
- 类型声明
- inline 函数
- 定义是产生代码的
- 声明是不产生代码的:
-
头文件
- 只有声明可以被放在头文件中:规律
- 否则会造成一个项目多个编译单元里有重名的实体
-
重复声明
- 同一个编译单元里,同名的结构不能被重复声明
- 如果头文件里有结构的声明,很难保证这个头文件不会在一个编译单元里被
#include多次(可能多个.h中出现) - 所以需要 “标准头文件结构”
-
标准头文件结构
-
示例
1
2
3
4
5
6
7
8
typedef struct _list {
Node* head;
Node* tail;
} List; -
运用条件编译和宏,保证这个头文件在一个编译单元中只会被
#include一次 -
#pragma once也能起到相同的作用,但是不是所有的编译器都支持
-
Week6:交互图形设计
图形的终端输入输出
- 在 scanf 时,程序是停下来的,所以不会显示任何东西(图形界面被挡住了)
函数指针及其应用
函数指针
- 变量:
&变量名取地址 - 数组:
数组名直接取地址 - 函数:
f就是个地址,f取地址 void f(void) { printf("test\n"); },void (*pf)(void) = f- 后面这个函数返回 void,参数不确定
- 这样的函数用 pf 变量指向它
- 把 f 函数交给 pf
(*pf)();可以调用 f- void 参数可以替换为 int 等,传递参数时也应传入相应类型的值
函数指针的使用
-
应用1:根据用户输入不同来判断要做的事
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// ver3.0 无论增加多少个函数,只需要加入 fa 的初始化里即可,不需要修改条件判断。
void f(int i)
{
printf("in f(), %d\n", i);
}
void g(int i)
{
printf("in g(), %d\n", i);
}
void k(int i)
{
printf("in k(), %d\n", i);
}
int main(void)
{
int i = 0;
scanf("%d", &i);
// ver1.0
// if (i == 0) { f(0); }
// else if (i == 1) { g(0); }
// else if (i == 2) { k(0); }
// ver2.0
// switch(i)
// {
// case 0: f(0); break;
// case 1: g(0); break;
// case 2: k(0); break;
// }
// ver3.0
void (*fa[])(int) = {f, g, k}; // 定义了一个 叫 fa 的函数指针数组,每一项都是一个函数指针
if (i>=0 && i<sizeof(fa)/sizeof(fa[0])) {
(*fa[i])(0);
}
} -
应用2:把函数当做参数,根据不同的函数(动作)做不同的事
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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); // 5
cal(minus); // -1
return 0;
}
交互图形程序设计
回调函数
-
先注册一个函数到主程序,当满足某个条件时,就做函数要做的事。
-
setup 做完后进入消息循环 loop,如果之前注册过回调函数,则调用回调函数,否则不动。如果回调函数是死循环,窗口都无法关闭。

图形交互消息
- Callbacks(回调)
- Keyboard:对每个键有按下和抬起两种
- Char:某个键
- Mouse
- Timer
游戏设计思路
- MVC
- View:从 Model 取数据过来
- Ctrl:告诉 Model 什么数据怎么改
- 鼠标动作不影响图像的改变,引起的是数据的改变
- 炮打飞机
- 各种对象的数据结构,记录对象的状态
- Timer(飞机自己动,炮弹飞之类)
- 到时候,Scan,扫描所有数据结构,找出每个东西在这一步做什么
- 特殊情况判断:炮弹 Hit,飞机 Crash 等
- Refresh(draw,重新画一遍画面,不用计算哪些动哪些没动)
- 其他细节
- 爆炸场面
- 效果很短,另外开一个定时器
- 爆炸是一个对象,放到数据结构里
- 爆炸场面
- 所有屏幕上的东西都是对象,动作在定时器每一次回来时做。
Week7:文件
格式化输入输出
printf
%[flags][width][.prec][hlL]type
-
flags
-左对齐+在前面放 + 或 -正数留空00 填充
-
width 或 prec
number最小字符数*下一个参数是字符数.number小数点后的位数.*下一个参数是小数点后的位数
-
hlL
hh单个字节hshortllonglllong longLlong double
-
type

scanf
%[flag]type


printf 和 scanf 的返回值
- 输出的字符数
- 读入的项目数
- 严格的程序中应判断每次调用 scanf 或 printf 的返回值,了解程序运行中是否存在问题
文件输入输出
-
用
<or>做重定向 -
FILE
FILE* fopen(const char * restrict path, const char * restrict mode);int fclose(FILE *stream);fscanf(FILE*, ...)fprintf(FILE*, ...)
-
打开文件标准代码
1
2
3
4
5FILE* fp = fopen("file", "r");
if (fp) {
fscanf(fp,...);
fclose(fp);
} else { ... } -
fopen(当文件打开出现错误时,fopen 返回 0)

二进制文件
- 二进制文件
- 文本文件是用最简单的方式可以读写的文件(给人看的)
- 二进制文件是需要专门的程序来读写的文件
- 文本文件的输入输出是格式化,可能经过转码
- 文本 VS 二进制
- Unix 喜欢用文本文件做数据存储和程序配置,shell 提供了一些读写文本的小程序
- Win 喜欢用二进制文件,DOS 不继承和熟悉 Unix 文化,且 DOS 能力更有限,二进制接近底层
- 方便人类读写、跨平台、格式化、开销大 VS 相反的
- 程序为什么要文件
- 配置:Unix 用文本,Win 用注册表
- 数据:数据库
- 媒体:二进制
- 现实是,程序通过第三方库来读写文件,很少直接读写二进制文件
- 二进制读写
size_t fread(void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);*restrict ptr:读或写的指针(内存)size_t size:内存有多大。一个结构的大小。size_t nitems:这样的内存有几个。因为二进制文件的读写一般都是通过对一个结构变量的操作进行的,nitem 就用来说明这次读写几个结构变量。
size_t fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);- FILE 指针是最后一个参数
- 返回的是成功读写的字节数
- 在文件中定位
long ftell(FILE *stream);int fseek(FILE *stream, long offset, int whence);SEEK_SET: 从头开始SEEK_CUR: 从当前位置开始SEEK_END: 从尾开始
- 可移植性
- 上面这样的二进制文件不具有可移植性(int32 机器上写出的数据文件无法直接在 int64 的机器上正确读出)
- 解决方案之一是放弃 int,用 typedef 定义明确大小的类型
- 更好的方案是用文本
按位运算
- 按位与
&(x)i == 1并且(y)i == 1,那么(x&y) = 1,否则为 0- 应用:
- 让某一位或某些位为 0
- 取一个数的一段
- 按位或
|(x)i == 1或(y)i == 1,那么(x&y) = 1,否则为 0- 应用:
- 使得一位或几位为 1
- 把两个数拼起来
- 按位反
~ - 逻辑运算 VS 按位运算
- 可以认为逻辑运算相当于把所有非 0 值全变为 1,然后按位运算
5 & 4 -> 4 而 5 && 4 -> 1 & 1 -> 15 | 4 -> 5 而 5 || 4 -> 1 | 1 -> 1!4 -> 3 而 !4 -> !1 -> 0
- 按位异或
^(x)i == (y)i,那么(x^y)i = 0,否则为 1- 两个位相等为 0,不相等为 1
- 对一个变量用同一个值异或两次,等于什么都没做
移位运算
- 左移,后面填 0,
x << n等价于x *= 2^n - 右移,对于 unsigned 类型前面填 0,对于 signed 类型,前面填入原来的最高位(保持符号不变),
x >> n等价于x /= 2^n,
位运算例子
- 输出二进制
- 单片机
位段
把一个 int 的若干位段组合成一个结构
1 | struct { |
- 可以直接用位段的成员名称访问,比移位、与、或方便
- 编译器会安排其中的位的排列,不具有可移植性
- 当所需的位超过一个 int 时会采用多个 int
期末考试
1 | while(*s++ = *t++); |
1 | 有以下结构体说明和变量定义,指针 p、q、r 依次指向一个链表中的三个连续结点。 |
1 | struct { |
1 | /* ex1.c */ |
1 | /* ex3.c */ |
1 | /* ex4.c */ |
1 | /* ex5.c */ |