@个人C语言学习笔记
/
数据结构复习 @Flowe2
-
编译&链接 预处理 -> 编译 -> 链接
-
程序结构(详见 程序结构)
#include <...>; int main() { // ... return 0; } // ... type function() { // ... }
- 预处理器指令
- 主函数 函数
- 语句 & 表达式
- 常量
#define MAX_NUM 10
- 变量
int a=10;
-
注释
- 单行:
// content
- 多行:
/* content */
- 单行:
-
变量
- 类型
详见 数据类型
- 声明
- 赋值
- 类型
printf & scanf
- 转换说明,
%-m.pX
格式-
: 指定左对齐, 无则默认右对齐m
: 最小字段宽度p
: 精度X
: 转换说明符,d - 十进制, e - 指数形浮点数, f - 定点十进制浮点数, g - 指数形式/定点十进制浮点数
- 那么如何显示
%
? 使用%%
即可print一个%
- 转义序列
- 常见:
\a - 警报符, \b - 退回符, \n - 换行符, \t - 水平制表符
-
详见 数据类型
- 常见:
-
算术运算符
type description +
相加(一元时为正号) -
相减(一元时为符号) *
相乘 /
相除 %
模运算/取余 ++
自增(区别作为前缀和后缀时) --
自减(区别作为前缀和后缀时) -
赋值运算符
type description =` 最简单基础的赋值运算符 +=
加且赋值运算符, C += A 相当于 C = C + A
-=
减且赋值运算符, C -= A 相当于 C = C - A
*=
乘且赋值运算符, C *= A 相当于 C = C * A
/=
除且赋值运算符, C /= A 相当于 C = C / A
%=
求模且赋值运算符, C %= A 相当于 C = C % A
<<=
左移且赋值运算符, C <<= 2 等同于 C = C << 2
>>=
右移且赋值运算符, C >>= 2 等同于 C = C >> 2
&=
按位与且赋值运算符, C &= 2 等同于 C = C & 2
^=
按位异或且赋值运算符, C ^= 2 等同于 C = C ^ 2
` =` -
关系运算符 (逻辑表达式)
type description ==
是否相等 !=
是否不等 >
左值是否大于右值 <
左值是否小于右值 >=
左值是否大于或等于右值 <=
左值是否小于或等于右值 -
逻辑运算符 (逻辑表达式)
type description &&
与运算符 ` !
非运算符
短路赋值
int a = 3, b = 3;
(a=0) && (b=2);
// a=0, b=3
(a=1) || (b=5);
// a=1, b=3
---
### 数据类型
1. **整数**
| type | size | rage |
| :------------- | :---------- | :------------------------------------------------- |
| char | 1 字节 | -128 ~ 127 或 0 ~ 255 |
| unsigned char | 1 字节 | 0 ~ 255 |
| signed char | 1 字节 | -128 ~ 127 |
| int | 2 或 4 字节 | -32,768 ~ 32,767 或 -2,147,483,648 ~ 2,147,483,647 |
| unsigned int | 2 或 4 字节 | 0 ~ 65,535 或 0 ~ 4,294,967,295 |
| short | 2 字节 | -32,768 ~ 32,767 |
| unsigned short | 2 字节 | 0 ~ 65,535 |
| long | 4 字节 | -2,147,483,648 ~ 2,147,483,647 |
| unsigned long | 4 字节 | 0 ~ 4,294,967,295 |
2. **浮点数**
| type | size | rage | accuracy |
| :---------- | :------ | :-------------------- | :-------- |
| float | 4 字节 | 1.2E-38 ~ 3.4E+38 | 6 位小数 |
| double | 8 字节 | 2.3E-308 ~ 1.7E+308 | 15 位小数 |
| long double | 16 字节 | 3.4E-4932 ~ 1.1E+4932 | 19 位小数 |
3. **void**
| type | description |
| :------------ | :--------------------------------------------------------------------------------------------------------------------------------------------- |
| 函数返回为空 | 不返回值的函数的返回类型为空。例如 void exit (int status); |
| 函数参数为空 | 不带(不接受)参数的函数可以接受一个 void。例如 int rand(void); |
| 指针指向 void | 类型为 void * 的指针代表对象的地址, 而不是类型。例如, 内存分配函数 void *malloc( size_t size ); 返回指向 void 的指针, 可以转换为任何数据类型。 |
4. 转义序列
| type | description | ASCII value(DEC) |
| :--- | :--------------------------------- | :--------------- |
| `\a` | 响铃(BEL) | 007 |
| `\b` | 退格(BS), 将当前位置移到前一列 | 008 |
| `\f` | 换页(FF), 将当前位置移到下页开头 | 012 |
| `\n` | 换行(LF), 将当前位置移到下一行开头 | 010 |
| `\r` | 回车(CR), 将当前位置移到本行开头 | 013 |
| `\t` | 水平制表(HT) | 009 |
| `\v` | 垂直制表(VT) | 011 |
| `\'` | 单引号 | 039 |
| `\"` | 双引号 | 034 |
| `\\` | 反斜杠 | 092 |
---
### 判断与循环
1. 判断
* **if** - (else if) - else
```C
if( boolean_expression 1 )
{
/* 当布尔表达式 1 为真时执行 */
}
else if( boolean_expression 2)
{
/* 当布尔表达式 2 为真时执行 */
}
else
{
/* 当上面条件都不为真时执行 */
}
```
> 判断boolean_expression, 为真则执行对应语句块
> 没有 {} 辅助时, else默认与其最近的if匹配
* **switch** - case - default
```C
switch(expression)
{
case constant-expression 1 :
statement(s);
break; /* 可选的 */
case constant-expression 2 :
statement(s);
break; /* 可选的 */
/* 您可以有任意数量的 case 语句 */
default : /* 可选的 */
statement(s);
}
```
> 根据expression判断, 跳转执行对应constant-expression下的语句块, 若无匹配则执行default下的语句块
> 也可以不写defaul情况
> 漏写break会使下一条件的语句被执行
> 有时也故意不写来达到几个条件分支共享代码的目的
* **条件运算符** (三元运算符)
```C
Exp1 ? Exp2 : Exp3;
```
> 若Exp1为真, 则执行Exp2, 否则执行Exp3
2. 循环
* **while**
```C
while(condition)
{
statement(s);
}
```
> 判断condition, 为true则执行statement(s), 否则结束循环
* **do...while**
```C
do
{
statement(s);
}while( condition );
```
> 执行statements(s), 然后判断condition, 为true则继续执行, 否则结束循环
* **for**
```C
for ( init; condition; increment )
{
statement(s);
}
```
> 1. init 首先执行, 且仅执行一次, 声明并初始化任何循环控制变量 (也可以不在这里写任何语句, 只要有一个分号出现即可)
> 2. 判断 condition, 如果为真, 则执行循环主体, 为假则不执行并结束循环
> 3. 执行 statement(s) 后, 会执行 increment 语句, 更新循环控制变量 (该语句也可以留空, 只要在条件后有一个分号出现即可)
> 4. 同2.再次判断循环条件, 重复上述2~4
* **循环控制语句**
| type | size |
| :------------ | :--------------------------------------------------------------- |
| break 语句 | 终止循环或`switch`语句, 继续执行紧接着循环或`switch`的下一条语句 |
| continue 语句 | 立刻停止本次循环迭代, 重新开始下次循环迭代 |
| goto 语句 | 将控制转移到被标记的语句 (但不建议使用) |
> goto 语句配对的标记语句为"xxx:", e.g.:`goto myGoto; Goto:`, 其坏处在于过多使用会使程序逻辑看上去逻辑混乱, 毕竟goto可往前也可往后跳转
---
### 数组
* 变量: 标量 & 聚合
* 标量: 保存单一数据项
* 聚合: 可储存一组一组的数据, 数组&结构
* 一维度数组
* 声明: `type arrayName [ arraySize ];`
* 初始化:
* 声明时初始化语句 (大小可省略): `type arrayName [ (arraySize) ] = {ele 1, ele 2, ..., ele n}; //若初始化数组短语length, 则剩余元素都为0`
* 指定初始化: `type arrayName [ (arraySize) ] = { [i] = ele i, [j] = ele j, ..., [n] = ele n}; //其余初始化为0`
* 声明后初始化, 逐一访问进行初始化
* 访问: `arrayName[i] = xxx;`
* 下标: 长度为 n 的数组下标从 0 ~ n-1
* 多维数组
* 数组可有任意维数
* 声明: `type arrayName [ oneDSize ][ twoDSize ];`
* 初始化 (二维为例):
* 嵌套一维数组: `type arrayName [ oneDSize ][ twoDSize ] = {{ele 1, ele 2, ... , ele n}, ..., {...}};`
* 若嵌套初始化式不够填满行数, 则后面的行默认初始化为`{0, 0, ..., 0}`
* 若某一嵌套初始化式不够填满当前行, 同一维, 其余位默认为`0`
* 甚至可以省略掉嵌套的括号 (因为**行主序**)
* 访问: `arrayName[i][j] = xxx;`, 考点: `arrayName[i, j]`逗号运算符, 实际等于`arrayName[j]`
* 存储: 二维为例, 在内存中也为`行主序`,
* C99 支持变长数组
* 声明: `type arrayName [ n ]; // n为变量`;
* `n`可以为表达式
* 常见于除`main`外的函数 (main也可用)
---
### 函数
* 定义:
```C
return_type function_name( parameter list )
{
body of the function
}
```
* 返回类型: `return_type` 是函数返回的值的数据类型。若不返回值 return_type 为 `void`, 未给出的话编译器会警告, 但不报错, 默认`int`型
* 函数名称: `function_name`, 函数名和参数列表一起构成了*函数签名*
* 参数: `parameter list` 形式参数参数, 可选, 需要**按顺序以<类型, 形参名>的形式**给出, 当函数被调用时, 传递的值为实际参数
* 函数主体:函数主体包含一组定义函数执行任务的语句, 函数的实际主体可以单独定义
* 声明
```C
return_type function_name( parameter list ); // 注意与函数完整定义不同, 声明语句需要以";"结尾
```
* C语言并未规定函数需在调用点前定义, 对于为给出定义的函数, 编译器会给一个**隐式声明** (返回值默认为int型)
* **函数原型**: 即函数声明, 早期C语言中, 函数的声明是简单的`return_type function_name()`, 函数原型的名字即与早期这种声明区分
* 调用
```C
function_name( actual_parameters )
```
* 实际参数转换: C语言允许实参类型与形参类型不匹配的调用,
* 调用前遇到原型: 自动转为相应类型
* 调用前未遇到原型: 将进行默认参数提升, `float`转`double`, `char`和`short`转`int`
* 数组型参数: 函数无法知道其长度, 需通过额外参数提供数组长度
* C99新特性
* 支持变长数组形式参数
* 支持在数组参数声明中使用`static`
* 支持复合字面量(做实际参数), 适用于仅使用一次的(临时)变量, 有效避免浪费, 调用: `func_name((int []){1, 2, 3, 4}, 4);`
* Return
* 除`void`型函数外, 函数需要有一个返回值
* 递归
* 若函数调用自身, 称其为**递归**的
---
### 程序结构
* 局部变量
* 特性
1. 自动存储期限
> 存储期限/存储长度: 在变量存在期间内程序执行部分
> 局部变量在函数调用时自动分配, 结束时自动回收
2. 块作用域
> 从声明开始至函数结尾
* 静态局部变量
* 使用`static`关键词可将局部变量变量声明为静态局部变量
* 在程序执行期间永久存在 (可以用来记录程序调用了几次)
* 但仍是**块作用域**, 对程序其他部分不可见
* 形式参数
* 和局部变量一样的性质: **自动存储期限**, **块作用域**
* 与局部变量的区别: 每次函数调用时, 会对形参进行自动初始化
* 外部变量/全局变量
* 特性
1. 静态存储期限
> 在程序执行期间永久存在
3. 文件作用域
> 从声明开始至文件结尾
* 程序块
* 形如 `{ 多条语句 }` 的代码块
* 程序块中的声明变量是局部变量
* 作用域
* 若一标识符已经是可见的了, 在程序块内命名相同标识符时, 新的声明会隐藏旧的声明
* 然后在程序块末尾, 标识符重新获得旧的含义
---
### 指针
> Byte = 8 bit
> 每个字节都有地址
* 指针的声明: `type *pointer_name;`
* 同基本变量声明一样, 只是多了一个星号`*`
* 指针的使用
* 取地址运算符: `&`
> `&a` 即取变量`a`的地址
* 间接寻址运算符: `*`
> `*p` 即取指针`p`所指地址的变量值 (`*p`是实际变量`a`的别名, 改变`*p`也会改变`a`)
* 在声明时可以将实际变量与指针一起声明并赋值, 不过要求实际变量先声明: `int a=0, *p=&a`
* 指针赋值
* 对两指针使用赋值运算, 即进行指针的复制 **(前提: 两指针具有相同类型)**
* 指针作为函数参数
* 定义方法即将对应指针类型形参添加`*`即可
* C语言的函数无法改变普通的实际变量
* 使用指针变量作为函数参数传入则可以达到改变实参的目的
* 若不希望函数改变指针指向变量, 则可以使用`const`关键词来保护参数, 即 `const type * pointer`
* 指针作为返回值
* 定义方法即将函数返回值后添加`*`即可
* 函数除返回传入指针参数外, 还可以返回指向外部变量/声明为`static`的局部变量指针
* 可以用于返回数组
* **但是**不能返回自动局部变量指针, 因为函数执行完后, 其就不存在了
* 指针与数组
> `int a[10], *p`
* 赋值
> `p = a`, 即`p`指向数组`a`的起始地址, 等价于`p = &a[0]`
> 实际上数组名`a`可以一个指针, `*a`即`a[0]`, `*(a+2)`即`a[2]`
> 不过, 试图给数组名`a`赋值的操作是错误的, 例如`a++`, 可先将`a`赋给另一指针变量再来改变 (⭐)
* 指向复合常量的指针
> 例如: `int *p = (int []){1, 2, 3}` (C99 supported), 可省略声明数变量的过程
* 运算 (仅支持三种)
> 加上整数: 可以通过对指针`p`做运算, 得到数组元素, 当`p = a`时, `p += 2`即`p`指向`a[2]`, 绝不能`指针+指针`
> 减去整数: 同理, 但可以`指针-指针`
> 两指针相减: 即得两指针间的距离
* 比较
> 关系运算符&判等运算符可用, 判断的是两指针的相对位置
* 处理数组
> 可用指针来遍历数组, 自增指针即可 `for (p = a; p < &a[N]; p++) {...}` (可用`a + N`代替`&a[N]`)
> 可以用间接寻址运算符`*`和自增运算符`++`等组合
* 用指针做数组名
> 既然可用数组名做指针, C语言也允许将指针看做数组名, 直接进行取下标的操作, `p[i]`即`*(p+i)`
* 指针处理多维数组
* 由于C语言对于多维数组仍然是按**行主序**存储的, 在遍历多维数组时, 可将嵌套循环用指针递增来代替
* 处理行:
```C
int a[ROWS][COLS], *p, i; // i为待处理行号
for ( p = &a[i][0]; p < &a[i][0] + COLS; p++ )
{ *p = ...; }
```
> 注意声明的`p`为普通`int`指针
> `p = &a[i][0]`简写`p = a[i]`
* 处理列:
```C
int a[ROWS][COLS], (*p)[COLS], i; // i为待处理列号
for ( p = &a[0]; p < &a[ROWS]; p++ )
{ (*p)[i] = ...; }
```
> 声明`p`为指向长度为`COLS`的`int`数组的指针
> `(*p)[COLS]`使用时需要带括号, 若无括号, 编译器会认为p为指针数组并解释为`*(p[COLS])`, 而非指向数组的指针
> `p++`此时就会自增一个`COLS`长度, 从而实现对列遍历
* 多维数组名做指针时需小心
* 指针与变长数组
> 一维好用, 多维需维度一致
---
### 指针(高级应用)
* **动态存储分配**
* **内存分配函数**
* `void *malloc(int num);`: 分配内存块, 但不对其进行初始化
* `void *calloc(int num, int size);`: 分配内存块, 并将其清零
* `void *realloc(void *address, int newsize);`: 调整先前分配的内存块大小
* 当拓展内存块时, 不会对其进行初始化
* 当拓展失败时, 会返回空指针, 且原有内存块内数据不变
* 若调用时第一个参数为空指针, 则实际效果等同`malloc`
* 若调用时第二个参数为`0`, 则会释放掉内存块
* `void free(void *address);`: 该函数释放 address 所指向的内存块,释放的是动态分配的内存空间, address必须是由内存分配函数返回的指针
* 空指针
* 不指向任何地方的指针, 一个区别于有效指针的特殊值
* 用名为`NULL`的宏来表示, `<locale.h> / <stddef.h> / <stdio.h> / <stdlib.h> / <string.h> / <time.h> / <wchar.h>(C99)`中都有定义, 程序包含任一即可
* 释放存储空间
* 内存分配函数所获得的的内存块都来自大小有限的`堆`
* `垃圾`: 程序不可再访问到的内存块
* 若程序没有回收自身产生的垃圾, 则该程序存在`内存泄漏`现象
* `垃圾收集器`: 有些高级语言提供, 但C没有, C语言要求程序自行回收各自的垃圾
* `悬空指针`: 调用`free(p)`之后, 并不会改变`p`本身, 此时`p`成为悬空指针, 再试图修改`p`所指的内存会导致严重错误
* 指向指针的指针
* 例如`char *`数组, 指向其元素的指针`char **`
* 链式数据结构中也常用到
* 当希望函数通过指针指向别处来改变数据时也会用到
* 指向函数的指针
* 是指向函数的指针变量
* 可以像一般函数一样, 用于调用函数/传递参数
* 声明: `typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型`
* 受限指针(C99)
* C99中, 允许使用`restrict`关键字来修饰指针声明, `type * restrict p`, `p`指向的对象不允许除`p`外的任何方式访问
* `别名`: 同一个对象的不同访问方式
* 链表
* 详见[数据结构](#数据结构)部分
* `->`运算符: 利用指针访问结构成员, `p->member`等价`(*p).member`
---
### 字符串
* 字符串字面量
> `string literal`, 是一用一对双引号`""`括起来的字符序列
1. 可包含转义序列
2. 一行太长, 则可用`\`来延续字符串, 也可以直接使用`"string 1" (换行) "string 2"`, C语言会自动拼接二者
3. 存储: C语言将字符串字面量作为数组存储, 类型`char *`, 长度为`n+1`, 末位为空字符`\0`
4. ==操作: 允许取下标, **不允许改变字符串字面量**==
* 因为部分编译器仅为相同字面量在内存中保存一个副本, 改变后, 所有指向该字面量的指针都会受影响
5. 与字符常量区别: 字符串字面量是用指针表示的, 而字符常量(如`"a"`)是用整数(ASSIC码)表示
* 字符串变量
* 声明&初始化:
`char str[length + 1] = "my string"`
* 因为末位有空字符, 故长度应为字符串长度+1
* 长度不一定一直为`length`, 只要以`\0`结尾就是结束, 即长度实际取决于空字符位置
* 编译器会自动在末位追加`\0`, 长度不够时, 余下元素也会被初始化为`\0`
* 若初始化时省略长度, 则编译器会自动计算, 如`char test[] = "test string"`
* 字符数组&字符指针
* `char str[] = "string"` & `char *str = "string"`
* 前者声明的是数组, 后者声明的是指针
* 对于任何期望传递字符数组/指针的函数, 都能接受这两种形式声明的`str`作为参数
* 但是二者实际存在较大差异:
* 声明为数组时, 可以随意改动; 但声明为指针时, 不允许随意改动
* 声明为数组时, `str`是数组名; 但声明为指针时, `str`是变量, 可指向其他字符串
* 写(输出)字符串`printf`&`puts`
* `printf("%s", str);`
> `printf`函数会数个输出`str`的字符, 直到遇到`\0`, 若字符串的`\0`丢失, 函数会越过字符串末尾继续输出内存内容, 直到遇到`\0`
* `puts(str);`
> `puts`函数只有一个参数, 在输出完之后, 会自动添加换行符
* 读(读入)字符串`scanf`&`gets`
* `scanf("%s", str);`
* `scanf`函数, 调用时不需要对`str`使用取址符`&`, 因为`str`为数组, 函数会默认将其视为指针处理
* 会跳过前面的空白字符, 当遇到`换行符`/`空格符`/`制表符`会停下
* `gets(str);`
* `gets`函数, 会一次性读入一整行, 包括前后的空字符等, 直到遇到`换行符`
* 并且会忽略`换行符`, 不会将其存入`str`
* **自定义函数, 逐字符读入✔**
* 访问字符串中的字符
```C
// example
int count_str (const char * str) {
int n = 0;
for( ; *str != '\0'; str++){
if( *str != ' ') {
n ++;
}
}
}
-
因为字符串以数组方式存储, 所以可以直接取下标
-
可用
const
来保护, 可读不可写 -
C语言字符串库
string.h
- 引用:
#include <string.h>
strcpy
函数- 原型:
char *strcpy(char *s1, const char *s2);
- 将
s2
指向的字符串复制给s1
指向的字符串中, 并返回s1
- 通常忽略返回值 (即, 直接调用函数, 不管返回值)
- 考虑安全: 使用
strncpy
函数 (速度会略慢于strcpy
), 限制str2
的str1
长度部分复制给str1
; 但若str2
的长度大于str1
的长度, 将导致str1
没有终止的空字符\0
, 故最好如下两句一起使用strncpy(str1, str2, sizeof(str1) - 1);
str1[sizeof(str1)] = '\0';
- 原型:
strlen
函数- 原型:
size_t strlen(const char *s);
- 返回字符串
s
第一个空字符前的长度 (即终止符\0
前),size_t
是C语言中一种无符号整型 strlen
不是返回s
本身长度, 而是返回s
指向的字符串的实际长度 (且不含\0
)
- 原型:
strcat
函数- 原型:
char *strcat(char *s1, const char *s2);
- 将字符串
s2
的内容追加到s1
后面, 并返回s1
- 通常忽略返回值
- 考虑安全: 使用
strncat
函数 (速度会略慢于strcat
), 限制str2
的str1
空余长度部分追加给str1
strncat(str1, str1, sizeof(str1)-strlen(str2)-1
- 原型:
strcmp
函数- 原型:
int strcmp(const char *s1, const char *s2);
- 比较字符串
s1
和s2
, 返回一个小于/等于/大于0的值
- 原型:
- 引用:
-
字符串数组
char * stringArr [] = {"string1", "string2", ..., "stringN"};
- 这种"参差不齐"的数组, 不会造成空间浪费
- 访问: 访问某一字符串, 只需对
stringArr
去下标即可
-
命令行参数
- 为了能够够访问
命令行参数
, 需要把main
函数定义为含有两个参数的函数 int main(int argc, char *argv[])
argc
: 参数计数, 命令行参数数量 (含程序名本身)argv
: 参数向量, 命令行参数的具体指令数组
- 为了能够够访问
-
工作原理:
- C源码 👉 预处理器 👉 修改后的C程序(不再含预处理指令) 👉 编译器 👉目标代码(机器码)
-
预处理指令
instruction description #define
定义宏 #include
包含一个源代码文件 #undef
取消已定义的宏 #ifdef
如果宏已经定义, 则返回真 #ifndef
如果宏没有定义, 则返回真 #if
如果给定条件为真, 则编译下面代码 #else
#if 的替代方案 #elif
如果前面的 #if 给定条件不为真, 当前条件为真, 则编译下面代码 #endif
结束一个 #if……#else 条件编译块 #error
当遇到标准错误时, 输出错误消息 #pragma
使用标准化方法, 向编译器发布特殊的命令到编译器中 -
宏定义
#define 标识符 替换列表
- 替换列表可包括: 标识符/关键字/数值常量/字符常量/字符串字面量/操作符/排列
- 作用:
- 程序更易读
- 程序更易修改
- 可避免前后输入不一致导致的错误
- 对C语言语法做小修改
- 对类型重命名
- 控制条件编译
- 带参数的宏定义
#define 标识符(x1, x2, ..., xN) 替换列表
- 例如:
#define (IS_EVEN(n)) ((n)%2==0)
, 调用时if(IS_EVEN(i)) i++;
- 作用:
- 程序可能优化变快
- 宏更通用
- 缺点:
- 编译后的文件通常变大
- 无法用指针指向宏
- 宏可能需要不止一次的计算其参数
- 例如:
- 宏定义的
#
运算符- 将宏的参数转换为字符串字面量
- 宏定义的
##
运算符- 记号粘合
- 若其中一个操作数为宏参数, 粘合会在相应形式参数被实际参数替换后发生
- 例如:
#define MAKE_ID(n) i##n
, 调用时int MAKE_ID(1), MAKE_ID(2), ...;
即int i1, i2, ...;
- 宏的通用属性
- 可以被其他宏调用
- 作用范围从出现到所在文件末尾
- 宏不可被定义两遍 (除非新旧定义一致)
- 可以用
#undef 标识符
来取消定义 - 一定使用
()
, 避免发生意料之外的情况
- 一些预定义宏:
macro description __LINE__
这会包含当前行号, 一个十进制常量 __FILE__
这会包含当前文件名, 一个字符串常量 __DATE__
当前日期, 一个以 "MMM DD YYYY" 格式表示的字符常量 __TIME__
当前时间, 一个以 "HH:MM:SS" 格式表示的字符常量 __STDC__
当编译器以 ANSI 标准编译时, 则定义为 1 (C99)新增 略 - C99 支持空的宏参数
- C99 支持可变数量的宏参数,
__VA_ARGS__
指代未定的可变的参数 __func__
标识符, 指代所处的函数的名称
-
条件编译
#if 常量表达式 {...} #elif 常量表达式 {...} #else {...} #endif
- 使用条件编译的好处
- 提高程序的可移植性
- 可适应不同的编译器
- 为宏提供默认定义
- 临时屏蔽包含注释的代码
defined
运算符defined(标识符)
或defined 标识符
- 若标识符为定义过的宏, 则返回
1
, 否则返回0
#ifdef
&#ifndef
指令- 测试一个标识符是否已经/还未定义为宏
#ifdef 标识符
等价于#if defined 标识符
#ifndef 标识符
等价于#if !defined 标识符
- 使用条件编译的好处
-
其他指令
#error
指令#error message
- message不需要用引号包裹
- 当预处理器遇到
#error
时会显示一条包含message的报错消息
#line
指令#line n
或#line n "文件"
- 用于改变程序行编号, 这条指令会导致程序后续行被编号为
n, n+1, ...
- 若带文件, 则后续行会被编号为认为来自
文件
, 行号由n
开始 - 会改变
__LINE__
, 甚至__FILE__
#program
指令#program 记号
- 该指令要求编译器执行某些特殊操作
- 对大程序/需要使用指定编译器特殊功能的程序很有用
_Pragama
运算符_Pragama (字符串字面量)
- 表达式结果被视为出现在
#program
中 - 预处理器通过移除字符串两端的双引号并分别用字符
"
和\
代替转义字符序列\"
和\\
-
结构
struct
- 一种用户自定义的可用的数据类型, 允许存储不同类型的数据项
- 声明
struct struct_tag { memeber_type member_name; memeber_type member_name; ... } struct_variable1, struct_variable2, ... ;
struct {...}
指明变量类型,struct_variable
指明为具有该变量类型的变量struct_tag
是结构标记- 用于标识某种特定结构的名称, 定义一个结构类型
- 一旦标记
tag
, 便可以用struct tag
来声明变量了 tag
不是类型名, 单独使用没有任何意义, 只有当与struct
一起时标识结构类型- 结构标记可以与结构变量的声明合并(如上), 如不跟
struct_variable
则为单独定义结构类型 - C语言也支持
typedef
形式来定义typedef struct {...} tag
, 使用typedef
时,tag
视为一种变量类型, 直接使用, 不需要跟struct
了
- 结构的成员在内存中是按声明的顺序存储的, 成员间没有间隙
- 初始化
struct ...{ ... } variable_name1 = { member1, member2, ... }, variable_name2 = { member1, member2, ... };
- 同普通变量一样, 可以在声明时直接初始化, 将初始化成员变量依次放入
{}
即可- 同数组类似, 未初始化的成员会默认为
0
或空字符串""
- 同数组类似, 未初始化的成员会默认为
- 指定初始化:
struct {} variable_name1 = { .member1 = member1, ...};
.
和成员名组合成为指示符
- 同普通变量一样, 可以在声明时直接初始化, 将初始化成员变量依次放入
- 访问
- 通过
struct_variable.struct_member
来访问struct
中的成员 .
运算符的优先级几乎高于其他所有运算符struct
允许整体复制,struct_variable1 = struct_variable2;
, 但仅限于结构兼容的struct
- 通过
- 作为参数和返回值
- 参数
return_type func_name (struc tag parameter) {...};
- 返回值
struct tag func_name () {}
- 参数
- C99支持作为复合字面量
(struct tag) { member1, ...}
- 未初始化的默认
0
- 适用于仅使用一次的(临时)变量, 有效避免浪费
- 嵌套结构&结构数组
- 结构允许嵌套操作, 某结构变量作为另一结构变量的成员
- 结构数组可以作为简单的数据库,
struct tag variable[N];
- 结构数组的初始化,
struct tag variable[(N)] = { { member1.1, member 1.2, ...}, { member2.1, member2.1, ...}, ... }
typedef
详解- 用法1:
typedef struct newType{ int data[10]; int size; }; // 使用时, struct newType作为新的数据类型 struct newType variableType;
- 用法2:
typedef struct newType{ int data[10]; int size; }myType; // 上述操作实现两步, 定义新的数据类型struct newType, 并取别名为myType // 使用时, 可以直接使用数据类型别名myType来定义 myType variableType;
- 更多讲解
- 用法1:
-
联合
union
- 一种特殊的数据类型, 允许在相同的内存位置存储不同的数据类型
- 由一个/多个成员构成, 但编译器只为联合中最大的成员分配足够内存, 联合的成员在该空间内彼此覆盖
struct
和union
性质操作等几乎一样, 不同的是前者同时储存成员变量, 后者只能同时存储某一成员变量union
主要用来节省空间, 也可以用来构造混合数据结构 (例如浮点数&整数数组)- 可以通过将
union
嵌套到struct
中, 来为联合添加标记字段, 告诉外部联合里面所存变量类型 - 声明(其余基本相同)
union union_tag { memeber_type member_name; memeber_type member_name; ... } union_variable1, union_variable2, ... ;
-
枚举
enum
- 一种基本数据类型,它可以让数据更简洁,更易读
- 声明:
enum enum_tag { element1, element2, ... } enum_variable;
- 也可以和
struct
/union
一样, 先声明, 再定义; 或省略tag
作为一次性的类型
- 也可以和
- 枚举作为整数
- 其中的元素默认从0开始, 依次递增1
- 可以单独指明首个元素, 改变枚举始量,
enum tag { element1 = 10, element2, ... };
(此时枚举量为10, 11, ...) - 也可以指明每个元素, 对于没有指定的值总比前一个大1
- C语言允许枚举变量与普通整数混合
- 枚举非常适合用来做
union
的标记字段
-
源文件
- 可以把程序分为任意个
.c
源文件 - 优点:
- 使程序结构更清晰 (将相关变量&函数分组放在同一文件中)
- 可有效节约时间 (可分别对每个源文件单独编译, 对于大规模程序而言尤为重要)
- 利于复用
- 可以把程序分为任意个
-
头文件
#include
指令指向的.h
文件- 引用方式
#include <xxx.h>
- 用于属于C语言自身库的头文件, 搜寻系统头文件所在(多个)目录#include "xxx.h"
- 用于其他头文件/自己编写的文件, 先搜寻当前目录, 然后搜寻系统头文件所在(多个)目录#include 记号
- 记号即任意预处理标识符, 会自动完成宏替换, 配合#if-#elif-#else
可大大提高系统的可移植性
- 共享宏定义&类型定义&共享函数原型&共享变量声明
- 头文件允许嵌套包含
- 保护头文件: 在待保护头文件中, 用
#ifndef
&#endif
来防止头文件被多次包含编译
-
访问结构的成员时使用点运算符, 而通过指针访问结构的成员时, 则使用箭头运算符
-
逗号运算符
,
exp1, exp2;
一行执行
只有一个表达式的情况下执行多条表达式 -
空语句
;
什么也不做, 常用于编写空循环体的循环 -
函数如何返回数组的三种方法:
- 函数外初始化数组, 并传入地址, 返回地址
- 使用静态局部变量数组(
static
) - 使用结构体(结构体成员深拷贝)