碎碎念
最近搬迁博客,就把这些文章都过了一遍,修改了许多东西。
为了写出来的 C 语言能和 Cpp(C++20 标准)兼容,我写的一般是 C99 标准,高于 C99 标准的我会说明。
这个只是速记,不能代替正常的学习过程。
好习惯
用 stdint.h 代替原本大小不明确的类型。
所有控制语句都加上大括号,即使只有一句话。
错误处理约定好,0 为成功,非 0 为错误码。对任何可能调用失败的函数都做好检查。不要随意 exit,尽可能把错误交给上层。
比起宏,多用静态内联函数代替,避免出错。
语言编写风格
Google C 风格(Google C Style)
Google 的公开风格指南主要是 C++,但在写 C 代码时通常沿用类似约定。核心特点:小写 + 下划线,为主;驼峰只在类型名等少数地方用。
典型约定(简略版):
- 变量名(局部、全局、参数):
使用小写 + 下划线:total_count,file_size,index_i - 函数名:
同样小写 + 下划线:read_file(),compute_sum() - 类型名(struct/enum/typedef):
一般首字母大写 + 下划线或驼峰:MyStruct,ErrorCode,或写成:my_struct_t - 宏 / 常量宏:
全大写 + 下划线:MAX_BUFFER_SIZE,DEFAULT_TIMEOUT_MS - 枚举常量:
全大写 + 下划线:COLOR_RED,COLOR_GREEN - 全局变量:
不鼓励大量使用;若用也写成普通的g_size、g_config或直接global_config,不会特意用匈牙利前缀。
一个符合 Google 风味的 C 例子:
typedef struct {
int width;
int height;
} Rectangle;
int compute_area(const Rectangle *rect) {
return rect->width * rect->height;
}
static int max_int(int a, int b) {
return a > b ? a : b;
}
特点:整体偏“UNIX/C 社区”风格,不使用匈牙利命名,不用成员名前缀类型信息,简单直接。
微软风格(Microsoft C/C++ 命名习惯)
微软内部风格历史上深受 Windows API 和早期 MFC 影响,特点是:喜欢匈牙利命名 + 大小写混合(驼峰/帕斯卡)。
不过需要注意:现在官方也在弱化“匈牙利记号法”,但 Windows SDK/MFC 里仍然能广泛看到这种风格。
典型传统习惯:
- 变量名前缀(“匈牙利命名”):
n/i/c:nCount(int),iIndex,cCharsz:以'\0'结尾的字符串:szNamepsz:指向字符串的指针:pszBufferdw:DWORD:dwFlagslp/p:指针:lpBuffer,pNext
- 函数名:
- 常用 帕斯卡命名法(首字母大写驼峰):
CreateWindowEx,GetModuleHandle
- 常用 帕斯卡命名法(首字母大写驼峰):
- 结构体 / 类型名:
typedef struct _FOO { ... } FOO, *PFOO;
成员常驼峰:dwSize,lpBuffer
- 宏常量:
- 全大写 + 下划线:
MAX_PATH,ERROR_ACCESS_DENIED
- 全大写 + 下划线:
一个典型带微软味道的片段:
typedef struct _RECT {
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT, *PRECT;
int nCount;
char szName[64];
DWORD dwFlags;
编译预处理
宏定义
define 的宏定义只是替换(一般来说, 宏定义用大写字母, 与普通变量区分), 下例:
#include<stdio.h>
#define a 10
#define b 20+a
b*2=20+10*2 //要达到先加后乘的效果应该是把b定义为(20+a)
注意: 由于宏定义在预编译中,编译之前,没有检查语法。用#undef 可以终止宏定义的作用域.
宏定义只是机械的替换,所以会出现这样的情况:
#define s(x) x*x
int main(){
int x = 5;
printf("s(x+1)");
}
// 输出x+1*x+1
所以最好每个值加上小括号避免这种情况。
#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
#if / #ifdef / #ifndef / #elif / #else / #endif
#error
#warning
#pragma once
#pragma pack
_Static_assert(sizeof(int) == 4, "int must be 4 bytes"); // C11
数据类型
数据二进制底层形式
原码,反码和补码
负数补码: “取反 + 1”(对整个数取反再加一,符号位也一起参与)
浮点数
以 32 位浮点数为例子,其底层为:
[S][ EEEEEEEE ][ FFFFFFFFFFFFFFFFFFFFFFF ]
^ 8 位指数 23 位尾数字段
符号位 exponent fraction
$$ \text{值} = (-1)^S \times 1.\text{F} \times 2^{E_{\text{实际}}} $$
一些技巧
sizeof 可以获得相应类型或者变量的内存大小。
C 语言可以是用科学记数法,e 和 E 都可以用。
类型转化
分为隐式类型转化和显示类型转化,很好理解,不写了。
注意截断和溢出。
基础类型
布尔型
实际上 C 语言有布尔类型,内建为 _Bool,可以用 stdbool.h 引入(烂尾了)。
整形
因为 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 中可以看到相应类型的最小值和最大值,是宏定义,我就不一一列出来了。
故而写 C 语言我更推荐引用 <stdint.h> 库,使用里面的数据类型,里面的格式都是 uintx_t、intx_t,非常简洁。
C99 起同样拥有 long long 与 LL/ULL 后缀
字符类型
本来字符类型属于整数类型是不用再写的,但是 C 语言存在转义字符和规定符。
转义字符
| 写法 | 含义 | 说明 |
|---|---|---|
\\ |
反斜杠字符 \ |
本来 \ 是转义起始,所以要写成 \\ |
\' |
单引号 ' |
用在字符/字符串中 |
\" |
双引号 " |
字符串字面量里写 " 必须转义 |
\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 |
printf/scanf | 十进制有符号整数 | int / int * |
%u |
printf/scanf | 十进制无符号整数 | unsigned int / unsigned int * |
%ld |
printf/scanf | 十进制有符号长整型(“32 位”只是常见实现,实际位数由 long 决定) |
long int / long int * |
%lu |
printf/scanf | 十进制无符号长整型 | unsigned long int / unsigned long int * |
%lld |
printf/scanf | 十进制有符号长长整型(常见为 64 位) | long long int / long long int * |
%llu |
printf/scanf | 十进制无符号长长整型 | unsigned long long int / unsigned long long int * |
%x / %X |
printf/scanf | 无符号整数的十六进制表示(小写/大写) | unsigned int / unsigned int * |
%o |
printf/scanf | 无符号整数的八进制表示 | unsigned int / unsigned int * |
%f |
printf | 小数形式的浮点数(printf 中参数类型为 double) |
double |
%lf |
printf | 在 printf 中与 %f 等价(同样读取 double) |
double |
%f |
scanf | 读入 float |
float * |
%lf |
scanf | 读入 double |
double * |
%e / %E |
printf/scanf | 指数形式的浮点数(如 1.23e+10) |
double / double * |
%g / %G |
printf/scanf | 按 %e 或 %f 中输出“更短”的形式 |
double / double * |
%c |
printf/scanf | 单个字符 | int / char * |
%s |
printf/scanf | 字符串(以 '\0' 结尾) |
char * / char * |
%p |
printf | 指针的值(地址) | void *(自动提升) |
%p |
scanf | 读入指针值 | void ** 或具体类型的 T ** |
%% |
printf | 输出一个 % 字符本身 |
无 |
补充要点:
printf中所有浮点实参都会默认提升为double,因此%f/%lf实际一样,都读double。scanf中%f和%lf有区别:分别是float *和double *。
printf 中的宽度/精度:
-
%nd: 在printf中表示:宽度至少为 n 的十进制整数,右对齐,不足时左侧补空格。 示例:
printf("%5d", x);表示宽度至少 5。 -
%-ns: 在printf中表示:宽度至少为 n 的字符串,左对齐,不足时右侧补空格。 示例:
printf("%-10s", s);表示宽度至少 10。 -
%m.nf: 在printf中表示:总宽度至少为 m,小数部分保留 n 位,右对齐; 若实际位数不足 m,则左侧补空格。 示例:
printf("%5.2f", y);即 m = 5, n = 2。 -
%0nd: 在printf中表示:宽度至少为 n 的整数,右对齐,不足时左侧用
0填充。 示例:printf("%05d", x);即 n = 5。 -
%#x: 在printf中表示:以十六进制输出无符号整数,并带有
0x前缀(%#X为0X前缀)。 -
%*d: 在printf中表示:宽度由 额外的一个
int实参 指定。 例如: printf("%*d “, w, x); // 等价于 printf("%nd “, x); 其中 n = w
scanf 中的宽度/抑制赋值:
-
%nd: 在scanf中表示:最多读取 n 位数字 构成一个整数,存入对应变量,多余的字符留在输入缓冲区。 示例:
scanf("%4d", &a);即 n = 4。 -
%*d: 在scanf中表示:读取一个整数,但 不赋值、不保存(抑制赋值),常用于跳过不需要的数据。
-
%d%*c%d: 在scanf中表示:先读一个整数给第一个参数,然后读一个字符并丢弃(
%*c),再读一个整数给第二个参数。 例如输入1+2时:a=1,+被丢弃,b=2。
单引号当成字符,双引号当成字符串。
宽字符
<wchar.h> 或 <stddef.h> 中引入
wchar_t 宽字符
L"" 宽字符字面量
浮点型
| 类型 | 存储大小 | 值范围 | 精度 |
|---|---|---|---|
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 位有效数字* |
正常的浮点数字默认被当成 double 类型的,带上 f 或者 F,比如 2.3f,可以修改字面量为 foalt。同理 l 和 L 可以修改字面量为 long double
void
void 类型指定没有可用的值。它通常用于以下三种情况下:
函数返回为空:C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status);
函数参数为空:C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void);
指针指向 void:类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数 * void *malloc( size_t size );* 返回指向 void 的指针,可以转换为任何数据类型。
指针
指针,就是一个专门指向内存一个地方的东西,形式为 type*,这里的 type 同样可以是指针,因为指针的值也是存放在内存里的。
实际上在 C 语言中几乎所有东西都能是指针,甚至函数也是!
数组
一维数组
定义: type name[length]
存储在连续内存中, 根据个数和类型占空间, 内存字节数 = 元素个数*sizeof(类型)。
注意: 引用范围为 [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 score[3][4](先行后列)
其实仍是一维排序, 在内存里就是
[0][0],[0][1],[0][2],[0][3],[1][0],[1][1],[1][2],[1][3](如此依序排列)
初始化:
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;
} p1, p2;
struct Point p; // 定义变量
struct Point *pp = &p;
p.x = 10;
pp->y = 4; // 等价于 (*pp).y
初始化
顺序初始化:
struct Person p = {"Alice", 20}; // 按成员声明顺序
指定初始化:
struct Person p = {
.age = 20,
.name = "Alice",
};
复合字面量:
可以直接在表达式里构造一个“临时结构体对象”:
func((struct Point){3, 4}); // 作为参数
内存布局
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)
- 可减少空间,但可能导致访问变慢,甚至在某些硬件上非法(未对齐访问)。
- 标准 C 不定义这些行为,属于编译器扩展;跨平台时要谨慎。
结构体与 typedef
下面的写法十分繁琐:
struct Point {
int x, y;
};
struct Point p;
typedef struct Point {
int x, y;
} Point;
Point p; // 不用再写 struct
typedef struct { // 使用匿名函数
int x, y;
} Point;
嵌套结构体与自引用
struct Date { int y, m, d; };
struct Person {
char name[20];
int age;
struct Date birthday;
};
struct Person p = {
.name = "Bob",
.age = 30,
.birthday = {1990, 1, 1}
};
struct Node {
int value;
struct Node *next; // OK,指针大小固定
// struct Node next; // 错误,类型不完整
};
struct S {
int tag;
union {
int i;
float f;
}; // 匿名 union C11标准
};
struct S s;
s.i = 10; // 不需要 s.u.i
联合体
其实等于只有一个地址的结构体,以最后赋值的值为内容, 最大占其中最大成员的内存。语法与结构体一致。
union Data d = {10}; // 初始化 .i = 10
union Data d = {.f = 3.14f};
经典用法:
typedef enum {
TYPE_INT,
TYPE_FLOAT,
TYPE_PTR
} ValueType;
typedef struct {
ValueType type;
union {
int i;
float f;
void *p;
} data;
} Value;
Value v;
v.type = TYPE_INT;
v.data.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
运算符与表达式
运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。
算术运算符
下表显示了 C 语言支持的所有算术运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
| 运算符 | 描述 | 实例 |
|---|---|---|
| + | 把两个操作数相加 | A + B 将得到 30 |
| - | 从第一个操作数中减去第二个操作数 | A - B 将得到 -10 |
| * | 把两个操作数相乘 | A * B 将得到 200 |
| / | 分子除以分母,如果是整数向下取整 | B / A 将得到 2 |
| % | 取模运算符,整除后的余数.ps: 浮点型不能用, 要用 math.h 的 fmod()和 fmodl() | B % A 将得到 0 |
| ++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
| – | 自减运算符,整数值减少 1 | A– 将得到 9 |
关系运算符
下表显示了 C 语言支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
| 运算符 | 描述 | 实例 |
|---|---|---|
| == | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 为假。 |
| != | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
| > | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 为假。 |
| < | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
| >= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 为假。 |
| <= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
逻辑运算符
下表显示了 C 语言支持的所有关系逻辑运算符。假设变量 A 的值为 1,变量 B 的值为 0,则:
| 运算符 | 描述 | 实例 |
|---|---|---|
| && | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (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 |
| ^ | 对两个操作数的每一位执行逻辑异或操作,如果两个相应的位值相同,则结果为 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 语言支持的赋值运算符:
| 运算符 | 描述 | 实例 |
|---|---|---|
| = | 简单的赋值运算符,把右边操作数的值赋给左边操作数 | C = A + B 将把 A + B 的值赋给 C |
| += | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | 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 |
| |= | 按位或且赋值运算符 | C|= 2 等同于 C = C | 2 |
杂项运算符 ↦ sizeof & 三元
下表列出了 C 语言支持的其他一些重要的运算符,包括 sizeof 和 ? :。
| 运算符 | 描述 | 实例 |
|---|---|---|
| sizeof() | 返回变量的大小。 | sizeof(a) 将返回 4,其中 a 是整数。 |
| & | 返回变量的地址。 | &a; 将给出变量的实际地址。 |
| * | 指向一个变量。 | * a; 将指向一个变量。 |
| ? : | 条件表达式 | 如果条件为真 ? 则值为 X : 否则值为 Y |
C 中的运算符优先级
下表将按运算符优先级从高到低列出各个运算符,具有较高优先级的运算符出现在表格的上面,具有较低优先级的运算符出现在表格的下面。在表达式中,较高优先级的运算符会优先被计算。
| 类别 | 运算符 | 结合性 |
|---|---|---|
| 后缀 | () [] -> . ++ - - | 从左到右 |
| 一元 | + - ! ~ ++ - - (type)* & sizeof | 从右到左 |
| 乘除 | * / % | 从左到右 |
| 加减 | + -s | 从左到右 |
| 移位 | « > > | 从左到右 |
| 关系 | < <= > >= | 从左到右 |
| 相等 | == != | 从左到右 |
| 位与 AND | & | 从左到右 |
| 位异或 XOR | ^ | 从左到右 |
| 位或 OR | | | 从左到右 |
| 逻辑与 AND | && | 从左到右 |
| 逻辑或 OR | || | 从左到右 |
| 条件 | ?: | 从右到左 |
| 赋值 | = += -= *= /= %=»= «= &= ^=|= | 从右到左 |
| 逗号 | , | 从左到右 |
变量和函数
命名规则
语法层面的规则(编译器强制):
- 可以包含:字母(a–z / A–Z)、数字(0–9)、下划线
_ - 第一个字符:必须是字母或
_,不能是数字 - 区分大小写:
myVar和myvar是两个不同标识符 - 不能包含空格或特殊符号:
& * $ @ - +等都不允许 - 不能是关键字:
if、else、int、static、return等
作用域(scope)
作用域 = 这个标识符“在哪些地方可见”。
代码块作用域(块作用域)
- 出现于:花括号
{ ... }内,如函数体、if、while、for 块 - 典型:局部变量、形参
void f(int x) { // x 也是块作用域
int a = 10; // a 只在 f 的花括号内可见
if (x > 0) {
int b = 20; // b 只在 if 块内可见
} // 这里 b 已经“不可见”
}
文件作用域(全局/静态作用域)
- 出现于:所有函数之外、全局区域
- 从定义点开始,到 该源文件末尾 都可见
int g = 0; // 文件作用域
void foo(void) {
g++;
}
函数作用域(只针对标签)
goto标签名的作用域是所在函数内部,这里一般不深究。
链接属性(linkage)
链接属性 = 跨源文件时,这个名字是否“指向同一个实体”。
C 里有三种链接属性:
- 外部链接(external linkage)
- 内部链接(internal linkage)
- 无链接(no linkage)
外部链接:多个文件共享同一个标识符
- 默认:文件作用域的非
static变量 / 函数
// a.c
int g = 0; // g 有外部链接
void foo(void) {} // foo 也有外部链接
// b.c
extern int g; // 和 a.c 里的 g 是同一个
void foo(void); // 声明 a.c 中的 foo
内部链接:仅在当前源文件内部可见
- 通过
static修饰的、处在文件作用域的变量 / 函数
// a.c
static int counter = 0; // 只能在 a.c 内使用
static void helper(void) { // 只能在 a.c 内调用
counter++;
}
- 其他源文件即使声明
extern int counter;也无法访问到这里的counter,因为链接属性不同。
无链接:每个定义独立,文件间不共享
- 块作用域的变量、形参、typedef 名等都属于“无链接”:
- 它们在每个函数/块里都是各自独立的标识符,“名字相同”也不是同一个实体。
void f(void) {
int x; // 无链接
}
void g(void) {
int x; // 另一个完全无关的 x
}
生存期(lifetime / storage duration)
生存期 = 变量在内存中实际存在的时间段,和作用域(可见范围)不是一回事。
主要有两类:
- 静态存储期(static storage duration)
- 自动存储期(automatic storage duration)
静态存储期
- 在程序开始运行时分配,在程序结束时释放
- 包括:
- 所有文件作用域的变量(不管有没有
static) - 用
static修饰的局部变量 - 所有字符串常量、常量区之类的实现细节对象(你可以先记住“全局 + static 局部”)
- 所有文件作用域的变量(不管有没有
int g; // 静态存储期
void f(void) {
static int s = 0; // 也是静态存储期,只是作用域在块内
s++;
}
自动存储期
- 典型:普通局部变量、函数形参
- 在进入块/函数时分配,在离开块/函数时销毁
void f(int x) { // x 自动存储期
int y = 10; // y 自动存储期
} // 运行到这里 x 和 y 就“消亡”
- 自动变量的初始内容 如果你没显式初始化,就是未定义的垃圾值。
(动态分配内存 malloc 那块,属于“动态存储期”,由你手动控制,这里不归类进关键字存储类型里。)
存储类关键字与常见关键字
这里把常见的相关关键字一块讲清楚:auto,register,static,extern,const,inline。
auto(自动存储,几乎不用显式写)
- 默认就是
auto,你写不写都一样:
void f(void) {
int x; // 等价于 auto int x;
auto int y; // 很少有人这样写
}
-
作用:
- 块作用域
- 自动存储期
- 无链接
-
现代 C 基本不用显式写
auto,只要知道“普通局部变量就是 auto 存储期”即可。
register(建议放在寄存器)
- 语法:
void f(void) {
register int x = 0;
}
- 本意:建议编译器把这个变量放在 CPU 寄存器里,加快访问(现在优化器通常更聪明,
register多数被忽略)。 - 限制:
- 理论上不能对
register变量取地址(&x不一定允许)。
- 理论上不能对
- 作用:
- 块作用域
- 自动存储期
- 无链接
在现代 C 里,普遍建议:不用写 register,交给编译器自动优化就好。
static
分几种情况:
文件作用域的 static(改变链接属性 → 内部链接)
static int g = 0; // 只在本源文件内可见(内部链接)
static void helper(void) {
/* 只在本文件内可调用 */
}
- 作用域:文件作用域
- 存储期:静态存储期
- 链接属性:内部链接(仅本文件共享)
这是一种“隐藏实现细节”的手段,非常推荐用于库/模块内部的全局量和函数。
块作用域的 static(改变存储期 → 静态存储期)
void counter(void) {
static int c = 0; // 只初始化一次,整个程序期间都存在
c++;
printf("%d\n", c);
}
- 作用域:块作用域(只在函数内部可见)
- 存储期:静态存储期(整个程序运行期一直存在)
- 链接属性:无链接
典型用途:
- 在函数内做“静态局部变量”,用来记忆调用状态(如计数器、缓存等)。
- 注意:初始值默认是 0(全局/static 变量默认会清零)。
extern
用于 声明一个已经在别处定义的全局变量或函数,不改变其本身属性,只是告诉编译器“这个名在别的翻译单元有定义”。
// a.c
int g = 42;
// b.c
extern int g; // 声明:g 在别处定义
适用场景:
- 在头文件中声明全局变量/函数接口,在某个
.c中给出定义:
// mylib.h
extern int my_global;
void my_func(void);
// mylib.c
#include "mylib.h"
int my_global = 0;
void my_func(void) { ... }
特性:
- 只能用在文件作用域(不能在函数里面用
extern定义一个新变量,还想有外部链接)。 - 不会改变变量本身的生存期/链接属性,只是一个声明。
const
const = 只读 语义限定符,和存储期/作用域无直接关系,但用在变量定义和声明时极其常用。
const int x = 10; // x 不能被修改
int *p;
const int *pc; // 指向 const int 的指针:*pc 不能改
int * const cp = &x; // const 指针:cp 本身不能改,*cp 可以(这里示例不严谨,只看语法)
几点要注意:
const不等于“放在只读存储区”,只是 语义上的不可改,具体放哪是实现细节。- 作用域、生存期、公/私有都由“位置和 static/extern”决定,
const只是增加“禁止修改”的约束。
常见模式:文件内只读全局常量
static const double PI = 3.141592653589793;
- 作用域:文件作用域
- 存储期:静态存储期
- 链接属性:内部链接
- 语义:不可修改
inline(内联函数)
- 关键字:
inline,表示“建议内联”,是否真正内联由编译器决定。 - 一般配合
static或extern一起用(否则可能出现多重定义问题),在 C99/C11 中需要注意用法:
常见安全写法(放在头文件):
static inline int max_int(int a, int b) {
return a > b ? a : b;
}
static inline函数:- 作用域:翻译单元内部(类似内部链接)
- 通常会被内联,也可以生成本地函数体
或在 C11 里配合 extern inline 用于某些高级优化场景,这个比较复杂,入门阶段记住:
- 把小工具函数写成
static inline放头文件里,即可。
inline 不改变存储期,它只影响函数的链接属性和是否生成独立函数体,与“变量的生命周期”不同类概念,这里放一起只是顺带说明。
“就近原则”的变量遮蔽(shadowing)
当你在内层作用域定义了与外层同名的变量时,内层会“遮蔽”外层:
int x = 1; // 全局 x
void f(void) {
int x = 2; // 局部 x,遮蔽全局 x
printf("%d\n", x); // 打印 2,而不是 1
}
建议:
- 尽量避免在函数内使用和全局变量相同的名字,免得混淆。
- 若必须使用全局变量,给全局量加统一前缀或放在结构体里,减少直接裸露的全局。
程序控制流
控制流关键字
if(){
}else if(){
}else{
}
switch:
case x:
xxx;
break;
default:
xxx;
for (a;b;c){ // 除了b都可省略
}
while(){
continue; // 跳过一次循环
break; // 跳出循环
}
do{
}while()
goto xxx
xxx:
xxx;
main
int main(int argc, char* argv[]){
} // 函数入口强制定义argc表示参数个数,包括自己。argv表示每个命令字符串
同时 main 不一定是第一个函数, 在一些调试检测需求下, 以下是有可能会出现的:
-
全局对象的构造函数会在 main 函数之前执行。(Cpp)
-
一些全局变量、对象和静态变量、对象的空间分配和赋初值就是在执行 main 函数之前,而 main 函数执行完后,还要去执行一些诸如释放空间、释放资源使用权等操作。
-
进程启动后,要执行一些初始化代码(CRT),然后跳转到 main 执行。全局对象的构造也在 main 之前。
-
通过关键字
__attribute__,让一个函数在主函数之前运行,进行一些数据初始化、模块加载验证等。
其余关键字
volatile:禁止编译器优化
auto:表示“自动存储期的局部变量”(和默认一样,所以不用管)
nullptr:C23 加的,没绷住。
常用库函数
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 在 assert.h 中, 检测参数是否为否, 如果问否, 则使用 abort 退出程序。是运行时断言,_Static_assert 是编译时。