编写规范
语言风格
Google 风格:
- 函数名和变量名:小写 + 下划线
- 全局变量:不鼓励大量使用;若采用则类似
g_var - 类型名:首字母大写 + 下划线或驼峰,或者类似
my_struct_t - 宏和枚举常量:全大写 + 下划线
微软风格:
变量名前缀(“匈牙利命名”):
n/i/c:nCount(int),iIndex,cCharsz:以'\0'结尾的字符串:szNamepsz:指向字符串的指针:pszBufferdw:DWORD:dwFlagslp/p:指针:lpBuffer,pNext
- 函数名:常用帕斯卡命名法(首字母大写驼峰):
CreateWindowEx,GetModuleHandle - 类型名:
typedef struct _FOO { ... } FOO, *PFOO;成员常驼峰:dwSize,lpBuffer - 宏常量:全大写 + 下划线
注意事项
C 语言由于其特性,不建议作为日常使用语言,如果有底层开发需求,去使用 C++ or Rust。
除非你是嵌入式开发或者性能紧张场景,如果这样建议你看看下面文章,恐怕这类场景对于安全性要求也是很高的:
还有一些好习惯推荐:
- 自定义类型代替原本大小不明确的类型。
- 所有控制语句都加上大括号,即使只有一句话。
- 比起宏,多用静态内联函数代替,避免出错。
- 错误处理约定好,0 为成功,非 0 为错误码。对任何可能调用失败的函数都做好检查。不要随意 exit,尽可能把错误交给上层。
语言要求
main
任何 C 程序必须从 main 函数开始
int main(int argc, char* argv[]){
} // 函数入口强制定义argc表示参数个数,包括自己。argv表示每个命令字符串同时 main 不一定是第一个函数, 在一些调试检测需求下, 以下是有可能会出现的:
一些全局变量、对象和静态变量、对象的空间分配和赋初值就是在执行
main函数之前,而main函数执行完后,还要去执行一些诸如释放空间、释放资源使用权等操作。进程启动后,要执行一些初始化代码(CRT),然后跳转到
main执行。全局对象的构造也在main之前。通过关键字
__attribute__,让一个函数在主函数之前运行,进行一些数据初始化、模块加载验证等。
编译预处理
宏定义
// 替换
#define a 10
#define b 20+a
b * 2 = 20 + 10 * 2 // 要达到先加后乘的效果应该是把b定义为(20+a)
// 字符串定义
#define STR(s) #s
STR(hello) → "hello"
STR(123 + 456) → "123 + 456"
// 拼接
#define CAT(x, y) x##y
CAT(hello, world) → helloworld
CAT(var, 123) → var123注意点:
- 由于宏定义在预编译中,编译之前,没有检查语法。
- 可以用
\换行。 - 宏定义只是机械的替换,最好每个值加上小括号避免出问题。
常用预处理指令
#include <> #include "" // 一个先找库,一个先找当前目录
#if / #ifdef / #ifndef / #elif / #else / #endif // 根据条件来选择是否编译该段代码
#error // 编译时直接报错并终止
#warning // 编译时输出警告但不终止
#pragma once // 只允许引用一次,用来避免重复引用
#pragma pack // 控制结构体内存对齐
_Static_assert(sizeof(int) == 4, "int must be 4 bytes"); // C11 引入,编译期断言类型
基础类型
整型
因为 C 语言历史发展的缘故,它的整形数据结构大小不同平台不一致,下表:
| 类型 | 标准最小宽度 / 最小范围 | 32 位 ILP32(典型 x86 Linux/Win32) | 64 位 LP64(Linux/macOS x86_64, aarch64) | 64 位 LLP64(Windows x64) |
|---|---|---|---|---|
char | 至少 8 位;范围为 signed char 或 unsigned char | 1 字节;-128~127 或 0~255 | 1 字节;-128~127 或 0~255 | 1 字节;-128~127 或 0~255 |
signed char | ≥ 8 位;至少 -127 ~ +127 | 1 字节;通常 -128~127 | 1 字节;通常 -128~127 | 1 字节;通常 -128~127 |
unsigned char | ≥ 8 位;至少 0 ~ 255 | 1 字节;0~255 | 1 字节;0~255 | 1 字节;0~255 |
short | ≥ 16 位;至少 -32767 ~ +32767 | 2 字节;-32768~32767 | 2 字节;-32768~32767 | 2 字节;-32768~32767 |
unsigned short | ≥ 16 位;至少 0 ~ 65535 | 2 字节;0~65535 | 2 字节;0~65535 | 2 字节;0~65535 |
int | ≥ 16 位;至少 -32767 ~ +32767 | 4 字节;-2 147 483 648~2 147 483 647 | 4 字节;-2 147 483 648~2 147 483 647 | 4 字节;-2 147 483 648~2 147 483 647 |
unsigned int | ≥ 16 位;至少 0 ~ 65 535 | 4 字节;0~4 294 967 295 | 4 字节;0~4 294 967 295 | 4 字节;0~4 294 967 295 |
long | ≥ 32 位;至少 -2 147 483 647 ~ +2 147 483 647 | 4 字节;-2 147 483 648~2 147 483 647 | 8 字节;-9 223 372 036 854 775 808~9 223…807 | 4 字节;-2 147 483 648~2 147 483 647 |
unsigned long | ≥ 32 位;至少 0 ~ 4 294 967 295 | 4 字节;0~4 294 967 295 | 8 字节;0~18 446 744 073 709 551 615 | 4 字节;0~4 294 967 295 |
long long | ≥ 64 位;至少 -9 223 372 036 854 775 807 ~ +…807 | 8 字节;-9 223 372 036 854 775 808~9 223…807 | 8 字节;-9 223 372 036 854 775 808~9 223…807 | 8 字节;-9 223 372 036 854 775 808~9 223…807 |
unsigned long long | ≥ 64 位;至少 0 ~ 18 446 744 073 709 551 615 | 8 字节;0~18 446 744 073 709 551 615 | 8 字节;0~18 446 744 073 709 551 615 | 8 字节;0~18 446 744 073 709 551 615 |
在标准库 limits.h 中可以看到相应类型的最小值和最大值,是宏定义。
long long 是 C99 才开始有的。
由于各个平台规定不一,故而写 C 语言更推荐引用 <stdint.h> 库,使用 uintx_t、intx_t。
另外 C 语言有布尔类型,内建为 _Bool,可以用 stdbool.h 引入(烂尾了)。
浮点型
| 类型 | 存储大小 | 值范围 | 精度 |
|---|---|---|---|
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 位有效数字 |
字符和宽字符
char 类型,默认为 8 bit,带符号。
unsigned char 类型,默认为 8 bit,不带符号。
wchar_t 类型,默认为 16 bit,通过 <wchar.h> 或 <stddef.h> 引入,主要用于宽字符支持。
指针
指针,就是一个存放内存地址的变量。形式为 type*,这里的 type 同样可以是指针,因为指针的值也是存放在内存里的。
type 的区别取决于你想如何解释这个地址上的变量。
实际上在 C 语言中几乎所有东西都能转化为指针,甚至函数也是。
void
void 类型表示没有可用的值。它通常用于以下三种情况下:
- 函数返回为空:函数没有返回值,此时返回值类型要设置为 void,例如
void exit (int status); - 函数参数为空:函数不需要参数,此时参数类型需要设置为 void,例如
int rand(void); - 指针指向 void:类型为
void*的指针代表对象的地址,而不是类型。例如,内存分配函数void* malloc( size_t size );。返回指向 void 的指针,可以转换为任何指针类型。
字面量
字符字面量中,单引号当成字符,双引号当成字符串,L"" 为宽字符字面量。
整数字面量中,从 C99 起拥有 LL/ULL 的整数后缀。而且可以使用科学计数法。
浮点数字面量中,浮点数默认被当成 double 类型的,带上 f 或者 F 后缀,比如 2.3f,可以修改字面量为 foalt。同理 l 和 L 可以修改字面量为 long double。
转义字符和规定符
转义字符:
| 写法 | 含义 | 说明 |
|---|---|---|
\\ | 反斜杠字符 \ | 本来 \ 是转义起始,所以要写成 \\ |
\' | 单引号 ' | 用在字符/字符串中 |
\" | 双引号 " | 字符串字面量里写 " 必须转义 |
\n | 换行(LF,line feed) | 在 Unix/Linux/macOS 里就是换行 |
\r | 回车(CR,carriage return) | Windows 文本换行常是 \r\n |
\t | 水平制表(Tab) | Tab 键 |
\v | 垂直制表 | 现在很少用 |
\b | 退格(backspace) | 光标向左退一格 |
\f | 换页(form feed) | 古老打印机/终端控制符 |
\a | 响铃(bell) | 老终端会“滴”一声,现代一般没反应 |
\? | 问号 ? | 避免被老式“三字母序列”影响(几乎不用) |
\ooo | 八进制数字 | 反斜杠 + 1 3 位八进制数字(0–7),最多三位 |
\xhh... | 十六位进制数字 | 直到遇到非十六进制字符为止的那串数字组成的值 |
\uXXXX | 16 位 Unicode 码点(4 位十六进制) | 基本用不上 |
\UXXXXXXXX | 32 位 Unicode 码点(8 位十六进制) | 基本用不上 |
规定符:
| 格式 | 适用函数 | 含义 / 说明 | 对应类型 |
|---|---|---|---|
%d / %i、%ld、%lld | printf/scanf | 十进制有符号整数 | int、long int、long long int |
%u、%lu、%llu | printf/scanf | 十进制无符号整数 | unsigned int、unsigned long int、unsigned long long int |
%x / %X、%lx / %lX、%llx / %llX | printf/scanf | 无符号整数的十六进制表示(小写/大写) | unsigned int、unsigned long int、unsigned long long int |
%o、%lo、%llo | printf/scanf | 无符号整数的八进制表示 | unsigned int、unsigned long int、unsigned long long int |
%f | printf/scanf | 小数形式的单精度浮点数 | float |
%lf | printf/scanf | 小数形式的双精度浮点数,在 printf 中与 %f 等价(同样读取 double) | double |
%e / %E | printf/scanf | 指数形式的浮点数(如 1.23e+10) | double |
%g / %G | printf/scanf | 按 %e 或 %f 中输出“更短”的形式 | double |
%c | printf/scanf | 单个字符 | int / char |
%s | printf/scanf | 字符串(以 '\0' 结尾) | char * |
%p | printf | 打印指针的地址 | void * |
%p | scanf | 读入指针值 | void ** 或具体类型的 T ** |
%% | printf | 输出一个 % 字符本身 | 无 |
printf 精度:
| 格式说明 | 含义 | 示例 |
|---|---|---|
%nd | 宽度至少为 n 的十进制整数,右对齐,左侧不足时补空格 | printf("%5d", 42); 输出 " 42"(宽度5) |
%-ns | 宽度至少为 n 的字符串,左对齐,右侧不足时补空格 | printf("%-10s", "hi"); 输出 "hi "(宽度10) |
%m.nf | 总宽度至少为 m,小数部分保留 n 位,右对齐;不足时左侧补空格 | printf("%5.2f", 3.14159); 输出 " 3.14"(总宽5,2位小数) |
%0nd | 宽度至少为 n 的整数,右对齐,左侧不足时用 0 填充 | printf("%05d", 7); 输出 "00007"(宽度5,0填充) |
%#x | 输出带 0x 前缀的十六进制无符号整数(%#X 则前缀 0X) | printf("%#x", 255); 输出 "0xff" |
%*d | 宽度由一个额外的 int 实参指定,效果等同 %nd 中 n 由参数动态给出 | printf("%*d", 5, 42); 输出 " 42"(宽度为第一个参数5) |
scanf 精度:
| 格式说明 | 含义 | 示例 |
|---|---|---|
%nd | 最多读取 n 位数字构成整数,存入对应变量,多余字符留在输入缓冲区 | scanf("%4d", &a); 若输入 123456,a 得到 1234,剩余 56 留在缓冲区 |
%*d | 读取一个整数,但不赋值(抑制赋值),常用于跳过数据 | scanf("%*d %d", &x); 输入 100 200,跳过 100,x 得到 200 |
%d%*c%d | 读取整数 → 读取一个字符并丢弃(%*c)→ 再读取一个整数,常用于跳过分隔符 | scanf("%d%*c%d", &a, &b); 输入 1+2,a=1,丢弃 +,b=2 |
复合类型结构
数组
数组分为一维数组和多维数组,一维数组的定义为 type name[length],其含义为存储在内存中连续存放的 length 个大小等同于 type 的变量们, 总空间大小为内存字节数 = 元素个数 * sizeof(类型)。
多维数组格式为 type name[length1][length2]...[lengthN],在内存中仍是一维排序。
数组的第一个元素下标为 0,所有的方括号范围为 [0, 长度-1]
一维初始化方式:
int a[4]={1,2,3,4}; // 每个位置的值最开始就表示好了
int a[4]={1,2,3}; // 没给初始值的默认为0
int a[]={1,2,3,4} // 长度为初始化的长度
char fuck[] = "fjskljf" // 结尾默认加0,因为这是字符串
int a[12]; // 初始化:如果没有初始化,那么内存中是什么是不确定的多维数组初始化方式(以二维为例):
// 以下所有格式把行数去掉依旧等价,可省略行数, 但不可省略列数。
int a[2][3]={1,2,3,4,5,6};
int a[2][3]={{1,2,3},{4,5,6}};
int a[2][3]={1,2,3,4}; // 未初始化的为0即{1,2,3,4,0,0}
int a[2][3]={{1,2},{3,4}}; // 为{1,2,0,3,4,0}结构体
结构体就是把若干个变量变量打包成一个更大的变量类型,格式如下:
// 定义
struct Point {
int x;
int y;
}; // 注意这里要加上分号
// 定义的同时声明变量
struct Person {
char name[20];
int age;
struct Point; // 可嵌套
struct { // 可匿名
int a;
int b;
};
struct Person* p; // 可自引用
// struct Person p; // 错误,因为 Person 中又定义一个 Person,那么没法确认其大小
} p1, p2;
// 定义变量
struct Person p;
struct Person *pp = &p;
// 使用变量
p.age = 10;
pp->age = 4; // 等价于 (*pp).y
p.a = 1; // 可自由使用任何匿名复合体内存布局同数组类似,但是大小计算方式则不同,拿下例说明:
struct S {
char c; // 1 字节
int i; // 通常 4 字节
};在常见 32/64 位 ABI 中,sizeof(struct S) 通常是 8,而不是 5,因为中间会插入填充字节,使 int 按 4 字节对齐。
可通过位域来节省空间,常用于单片机这种需要节省空间的硬件上,下例:
struct Flags {
unsigned int a : 1; // 1 位
unsigned int b : 3; // 3 位
unsigned int c : 4; // 4 位
};有些编译器还支持 #pragma pack 或属性来改变对齐方式:
#pragma pack(push, 1)
struct Packed {
char c;
int i;
};
#pragma pack(pop)初始化方式:
// 顺序初始化,即按成员声明顺序
struct Person p = {"Alice", 20};
// 指定初始化
struct Person p = {
.age = 20,
.name = "Alice",
};
// 临时构造
func((struct Point){3, 4}); // 作为参数联合体
空间大小为所有变量中空间最大的变量,以最后赋值的值来作为其内容的结构体。语法与结构体一致,下面只展示基本写法。
// 定义
union Data {
int i;
float f;
void *p;
};
// 初始化
union Data d;
union Data d = {10};
union Data d = {.f = 3.14f};
// 使用变量
d.i = 42;枚举类型
与前两个类似,不再做说明。
enum Color {
RED, // 默认为0,逐次+1
GREEN,
BLUE
};
enum ErrorCode {
ERR_OK = 0,
ERR_IO = 1,
ERR_TIMEOUT = 2,
ERR_FATAL = 100
};
enum Weekday {
MON = 1,
TUE, // 2
WED, // 3
THU = 10,
FRI, // 11
SAT, // 12
SUN // 13
};
enum Color color;
color = RED;
if (color == GREEN) {
/* ... */
}
enum Color color = BLUE; // C99复合变量体与 typedef
定义复合变量体变量由于要反复写明其复合变量类型,十分麻烦,我们可以用 typedef 给它取别名
// 取别名
typedef struct Point {
int x, y;
} Point;
// 使用匿名函数进一步简化
typedef struct {
int x, y;
} Point;
Point p; // 不用再写 struct类型转化
分为隐式类型转化和显式类型转化,很好理解,不写了。
注意截断和溢出,C 编译器默认不检查这些东西,有一些安全选项开启后可以检查a。
运算符与表达式
下表显示了 C 语言支持的所有运算符。假设变量 A 的值为 10,变量 B 的值为 20。
运算符优先级:
| 类别 | 运算符 | 结合性 |
|---|---|---|
| 后缀 | () [] -> . ++ -- | 从左到右 |
| 一元 | + - ! ~ ++ -- (type) * & sizeof | 从右到左 |
| 乘除 | * / % | 从左到右 |
| 加减 | + - | 从左到右 |
| 移位 | << >> | 从左到右 |
| 关系 | < <= > >= | 从左到右 |
| 相等 | == != | 从左到右 |
| 位与 AND | & | 从左到右 |
| 位异或 XOR | ^ | 从左到右 |
| 位或 OR | | | 从左到右 |
| 逻辑与 AND | && | 从左到右 |
| 逻辑或 OR | || | 从左到右 |
| 条件 | ?: | 从右到左 |
| 赋值 | = += -= *= /= %= >>= <<= &= ^= |= | 从右到左 |
| 逗号 | , | 从左到右 |
算术运算符
| 运算符 | 描述 | 实例 |
|---|---|---|
+ | 把两个操作数相加 | A + B 将得到 30 |
- | 从第一个操作数中减去第二个操作数 | A - B 将得到 −10 |
* | 把两个操作数相乘 | A * B 将得到 200 |
/ | 分子除以分母,如果是整数向下取整 | B / A 将得到 2 |
% | 取模运算符,整除后的余数。注意:浮点型不能用,要用 math.h 的 fmod() 和 fmodl() | B % A 将得到 0 |
++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
-- | 自减运算符,整数值减少 1 | A-- 将得到 9 |
关系运算符
| 运算符 | 描述 | 实例 |
|---|---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 为假。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 为假。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 为假。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
逻辑运算符
| 运算符 | 描述 | 实例 |
|---|---|---|
&& | 逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
|| | 逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A || B) 为真。 |
! | 逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !(A && B) 为真。 |
位运算符
| 运算符 | 描述 | 实例 |
|---|---|---|
& | 按位与:对每一位执行逻辑与操作,两个相应位都为 1 则结果为 1,否则为 0。运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1; | (A & B) 将得到 12,即为 0000 1100 |
| | 按位或:对每一位执行逻辑或操作,两个相应位都为 0 则结果为 0,否则为 1。运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1; | (A | B) 将得到 61,即为 0011 1101 |
^ | 按位异或:对每一位执行逻辑异或操作,两个相应位值相同则结果为 0,否则为 1。运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0; | (A ^ B) 将得到 49,即为 0011 0001 |
~ | 按位取反:将每一位的 0 变为 1,1 变为 0。运算规则:~1=-2; ~0=-1; | (~A) 将得到 −61,即为 1100 0011,一个有符号二进制数的补码形式。 |
<< | 左移:将操作数的所有位向左移动指定的位数。左移 n 位相当于乘以 2 的 n 次方。左边丢弃,右边补 0。 | A << 2 将得到 240,即为 1111 0000 |
>> | 右移:将操作数的所有位向右移动指定的位数。右移 n 位相当于除以 2 的 n 次方。正数左补 0,负数左补 1,右边丢弃。 | A >> 2 将得到 15,即为 0000 1111 |
赋值运算符
| 运算符 | 描述 | 实例 |
|---|---|---|
= | 简单赋值,把右边操作数的值赋给左边操作数 | C = A + B 将把 A + B 的值赋给 C |
+= | 加且赋值,C += A 相当于 C = C + A | C += A |
-= | 减且赋值,C -= A 相当于 C = C - A | C -= A |
*= | 乘且赋值,C *= A 相当于 C = C * A | C *= A |
/= | 除且赋值,C /= A 相当于 C = C / A | C /= A |
%= | 求模且赋值,C %= A 相当于 C = C % A | C %= A |
<<= | 左移且赋值,C <<= 2 等同于 C = C << 2 | C <<= 2 |
>>= | 右移且赋值,C >>= 2 等同于 C = C >> 2 | C >>= 2 |
&= | 按位与且赋值,C &= 2 等同于 C = C & 2 | C &= 2 |
^= | 按位异或且赋值,C ^= 2 等同于 C = C ^ 2 | C ^= 2 |
|= | 按位或且赋值,C |= 2 等同于 C = C | 2 | C |= 2 |
杂项运算符:sizeof 与三元
| 运算符 | 描述 | 实例 |
|---|---|---|
sizeof() | 返回变量或类型的大小。 | sizeof(a) 将返回 4,其中 a 是 int。 |
& | 返回变量的地址。 | &a; 将给出变量的实际地址。 |
* | 解引用,指向一个变量。 | *a; 将指向一个变量。 |
? : | 三元条件表达式 | 如果条件为真 ? 则值为 X : 否则值为 Y |
控制流
// 条件判断
if(){
}else if(){
}else{
}
// 多重条件判断
switch:
case x:
xxx;
break; // 如果不带 break,则会继续朝向下一个 case 作比较
default:
xxx;
for (a;b;c){ // 除了表达式 b 都可省略
}
// 先判断再执行的循环
while(){
continue; // 跳过一次循环
break; // 跳出循环
}
// 先执行再判断的循环
do{
}while()
// 直接跳转
goto label
label:
xxx;函数
类型为 返回值类型 函数名(函数参数类型 函数参数名, ...){...}
变量函数特殊属性
作用域(scope)
作用域指的是变量或者函数的可见范围。
- 文件作用域(全局/静态作用域):所有函数之外的全局区域,从其定义点开始,到该源文件末尾都可见
- 代码块作用域(块作用域):花括号
{ ... }内是单独的一个作用域,如函数体、if、while、for 块,或者主动写的花括号。内层作用域可以看到外层作用域已定义变量,外层看不到内层。最里面的作用域定义变量可以覆盖外面作用域的同名变量。 - 函数作用域(只针对标签):
goto标签名的作用域是所在函数内部,这里一般不深究。
链接属性(linkage)
链接属性指的是跨源文件时,不同文件中同个名字是否指向同一个实体。C 里有三种链接属性:
- 外部链接(external linkage):多个文件共享同一个标识符,是文件作用域的非
static变量 / 函数 - 内部链接(internal linkage):仅在当前源文件内部可见,是通过
static修饰的、处在文件作用域的变量 / 函数 - 无链接(no linkage):每个定义独立,文件间不共享,是块作用域的变量、形参、typedef 名等,它们在每个函数/块里都是各自独立的标识符,“名字相同”也不是同一个实体。
生存期(lifetime / storage duration)
生存期 = 变量在内存中实际存在的时间段。主要有三类:
静态存储期:在程序开始运行时分配,在程序结束时释放,包括:
- 所有文件作用域的变量(不管有没有
static) - 用
static修饰的局部变量 - 所有字符串常量、常量区之类的实现细节对象(你可以先记住“全局 + static 局部”)
- 所有文件作用域的变量(不管有没有
- 自动存储期:在进入块/函数时分配,在离开块/函数时销毁,比如普通局部变量、函数形参。自动变量的初始内容在没显式初始化的情况下,就是未定义的垃圾值。
- 动态存储期:由程序员手动控制,比如动态分配内存
相应对应二进制文件中的常量段,栈,堆
关键字
常见关键字:
auto:所有变量默认就是 auto 存储期,实际上不需要写。register:建议编译器把变量放在寄存器,但是由于现代编译器的优化方式,实际上不需要写。static:文件作用域的static会改变链接属性 → 内部链接,用来避免外界使用;块作用域的static会改变存储期 → 静态存储期,用来作为代码块内部静态局部变量。extern:用于声明一个已经在别处定义的全局变量或函数,不改变其本身属性,只是告诉编译器“这个名在别的翻译单元有定义”。常用于在头文件中声明全局变量/函数接口,在某个.c中给出定义const:代表不变变量,不可修改,永远等于初始值inline:表示“建议内联”,是否真正内联由编译器决定。现在主要解决多重定义问题,inline函数可以允许函数出现在多个文件不出现多重定义问题,inline static可以允许变量出现在多个文件不出现多重定义问题。volatile:禁止编译器优化nullptr:C23 加的,同 Cpp 中一样。
常用库函数
math.h
| 函数 | 函数原型(简化) | 作用 | 示例 |
|---|---|---|---|
fabs | double fabs(double x); | 绝对值(浮点) | fabs(-3.5) → 3.5 |
sqrt | double sqrt(double x); | 开平方 | sqrt(9.0) → 3.0 |
sin / cos | double sin(double x); 等 | 正弦 / 余弦(弧度) | sin(3.14159/2) |
pow | double pow(double x, double y); | 幂运算,x 的 y 次方 | pow(2.0, 3.0) → 8.0 |
exp | double exp(double x); | e 的 x 次方 | exp(1.0) ≈ 2.71828 |
log / log10 | double log(double x); 等 | 自然对数 / 以 10 为底对数 | log(2.71828) ≈ 1 |
ctype.h
| 函数 | 函数原型 | 作用 | 示例 |
|---|---|---|---|
isdigit | int isdigit(int c); | 是否数字字符 '0'..'9' | isdigit('3') → 非0 |
isalpha | int isalpha(int c); | 是否字母字符 A..Z,a..z | isalpha('A') → 非0 |
isupper | int isupper(int c); | 是否大写字母 | isupper('A') → 非0 |
islower | int islower(int c); | 是否小写字母 | islower('a') → 非0 |
isspace | int isspace(int c); | 是否空白(空格、换行等) | isspace(' ') → 非0 |
tolower | int tolower(int c); | 转为小写(若是字母) | tolower('A') → 'a' |
toupper | int toupper(int c); | 转为大写(若是字母) | toupper('a') → 'A' |
stdio.h
标准输入输出
| 函数 | 原型 | 作用 | 简例 |
|---|---|---|---|
printf | int printf(const char *format, ...); | 格式化输出到标准输出 | printf("x=%d\n", x); |
fprintf | int fprintf(FILE *stream, const char *format, ...); | 格式化输出到指定文件流 | fprintf(fp, "x=%d\n", x); |
sprintf | int sprintf(char *str, const char *format, ...); | 格式化输出到字符串(不安全) | sprintf(buf, "%d", x); |
snprintf | int snprintf(char *str, size_t size, const char *format, ...); | 限长格式化到字符串(推荐) | snprintf(buf, 16, "%.2f", f); |
scanf | int scanf(const char *format, ...); | 从标准输入按格式读 | scanf("%d", &x); |
fscanf | int fscanf(FILE *stream, const char *format, ...); | 从文件流按格式读 | fscanf(fp, "%s", s); |
sscanf | int sscanf(const char *str, const char *format, ...); | 从字符串按格式解析 | sscanf(buf, "%d", &x); |
puts | int puts(const char *s); | 输出字符串并加换行 | puts("Hello"); |
fputs | int fputs(const char *s, FILE *stream); | 输出字符串到文件 | fputs("Hi", fp); |
putchar | int putchar(int c); | 输出一个字符 | putchar('A'); |
fputc | int fputc(int c, FILE *stream); | 向文件写一个字符 | fputc('A', fp); |
getchar | int getchar(void); | 从标准输入读一个字符 | int c = getchar(); |
fgetc | int fgetc(FILE *stream); | 从文件读一个字符 | int c = fgetc(fp); |
ungetc | int ungetc(int c, FILE *stream); | 把字符退回输入流 | ungetc(c, fp); |
文件处理
| 函数 | 原型 | 作用 | 简例 |
|---|---|---|---|
fopen | FILE *fopen(const char *path, const char *mode); | 打开文件 | FILE *fp = fopen("a.txt","r"); |
fclose | int fclose(FILE *stream); | 关闭文件 | fclose(fp); |
fflush | int fflush(FILE *stream); | 刷新输出缓冲 | fflush(stdout); |
fread | size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); | 二进制读块 | fread(buf,1,100,fp); |
fwrite | size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); | 二进制写块 | fwrite(buf,1,n,fp); |
fseek | int fseek(FILE *stream, long offset, int whence); | 移动文件位置 | fseek(fp, 0, SEEK_SET); |
ftell | long ftell(FILE *stream); | 当前文件位置 | long pos = ftell(fp); |
rewind | void rewind(FILE *stream); | 回到文件开头 | rewind(fp); |
remove | int remove(const char *path); | 删除文件 | remove("a.txt"); |
rename | int rename(const char *old, const char *new); | 重命名文件 | rename("a.txt","b.txt"); |
tmpfile | FILE *tmpfile(void); | 创建临时文件 | FILE *fp = tmpfile(); |
错误检查
| 函数 | 原型 | 作用 |
|---|---|---|
perror | void perror(const char *s); | 打印最近错误原因(结合 errno) |
feof | int feof(FILE *stream); | 是否到达 EOF |
ferror | int ferror(FILE *stream); | 是否发生 I/O 错误 |
clearerr | void clearerr(FILE *stream); | 清除 EOF/错误标志 |
stdlib.h
动态内存
| 函数 | 原型 | 作用 | 示例 |
|---|---|---|---|
malloc | void *malloc(size_t size); | 申请未初始化内存 | int *p = malloc(10*sizeof *p); |
calloc | void *calloc(size_t nmemb, size_t size); | 申请并清零 nmemb 个元素 | int *p = calloc(10,sizeof *p); |
realloc | void *realloc(void *ptr, size_t size); | 调整已分配内存大小 | p = realloc(p,20*sizeof *p); |
free | void free(void *ptr); | 释放动态内存 | free(p); p=NULL; |
字符串到数字
| 函数 | 原型 | 作用 | 示例 |
|---|---|---|---|
atoi | int atoi(const char *nptr); | 字符串 → int | int x = atoi("123"); |
atol | long atol(const char *nptr); | 字符串 → long | |
atof | double atof(const char *nptr); | 字符串 → double | double d = atof("3.14"); |
strtol | long strtol(const char *nptr, char **endptr, int base); | 字符串 → long,支持进制,能返回错误位置 | long x = strtol("0xff",NULL,0); |
strtoul | unsigned long strtoul(const char *nptr, char **endptr, int base); | 字符串 → unsigned long | |
strtod | double strtod(const char *nptr, char **endptr); | 字符串 → double |
随机数
| 函数 | 原型 | 作用 | 示例 |
|---|---|---|---|
rand | int rand(void); | 返回 [0, RAND_MAX] | int x = rand()%(n-m+1)+m; |
srand | void srand(unsigned int seed); | 设置随机种子 | srand(time(NULL)); |
程序控制 / 环境
| 函数 | 原型 | 作用 |
|---|---|---|
exit | void exit(int status); | 正常结束程序 |
abort | void abort(void); | 异常终止 |
atexit | int atexit(void (*func)(void)); | 注册进程退出时调用的函数 |
system | int system(const char *command); | 调用系统命令 |
排序与查找
| 函数 | 原型 | 作用 | 简例 |
|---|---|---|---|
qsort | void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); | 通用快速排序 | qsort(a,n,sizeof *a, cmp); |
bsearch | void *bsearch(const void *key, const void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); | 有序数组二分查找 | bsearch(&key,a,n,sizeof *a,cmp); |
string.h
字符串函数(以 '\0' 结尾)
| 函数 | 原型 | 作用 |
|---|---|---|
strlen | size_t strlen(const char *s); | 返回字符串长度(不含 '\0') |
strcpy | char *strcpy(char *dest, const char *src); | 拷贝字符串到 dest |
strncpy | char *strncpy(char *dest, const char *src, size_t n); | 最多拷贝 n 字符 |
strcat | char *strcat(char *dest, const char *src); | 把 src 接到 dest 末尾 |
strncat | char *strncat(char *dest, const char *src, size_t n); | 最多连接 n 字符 |
strcmp | int strcmp(const char *s1, const char *s2); | 比较两个字符串 |
strncmp | int strncmp(const char *s1, const char *s2, size_t n); | 比较前 n 个字符 |
strchr | char *strchr(const char *s, int c); | 查找首次出现的字符 c |
strrchr | char *strrchr(const char *s, int c); | 查找最后一次出现的字符 c |
strstr | char *strstr(const char *haystack, const char *needle); | 查找子串 |
strtok | char *strtok(char *str, const char *delim); | 分割字符串(非线程安全,慎用) |
内存函数(任意字节块)
| 函数 | 原型 | 作用 |
|---|---|---|
memset | void *memset(void *s, int c, size_t n); | 用字节 c 填充前 n 字节 |
memcpy | void *memcpy(void *dest, const void *src, size_t n); | 拷贝 n 字节(不允许重叠) |
memmove | void *memmove(void *dest, const void *src, size_t n); | 拷贝 n 字节(允许重叠) |
memcmp | int memcmp(const void *s1, const void *s2, size_t n); | 比较前 n 字节 |
memchr | void *memchr(const void *s, int c, size_t n); | 在前 n 字节中查找字符 c |
assert.h
assert 在 assert.h 中,检测参数是否为假,如果为假,则使用 abort() 退出程序,是运行时断言,同 _Static_assert 编译时断言不同。