C复习
约 4380 字大约 15 分钟
2025-07-08
若使用bool类型,可以
#include <stdbool.h> //或 #define FALSE 0 #define TRUE 1传统风格注释
/*...*/不能嵌套,单行注释//...可以嵌套在传统风格注释中变量命名(用户所定义的标识符)只允许以数字、字母、下划线
_开头。printf函数格式化printf("%-1.2d") //'-'代表左对齐 //'1'是至少占用字符空间 //'.2'是精度常见转义字符表
转义字符 含义 ASCII 值 \a响铃(警报) 0x07 \b退格(Backspace) 0x08 \f换页(Form Feed) 0x0C \n换行(Newline) 0x0A \r回车(Carriage Return) 0x0D \t水平制表符(Tab) 0x09 \v垂直制表符 0x0B \\反斜杠 0x5C \'单引号 0x27 \"双引号 0x22 \?问号 0x3F \0空字符(Null) 0x00 \xhh十六进制字符(如 \x41是 'A')自定义 \ooo八进制字符(如 \101是 'A')自定义 %i也可用于读写整数,但会自动通过0或0x前缀识别八进制或十六进制基本格式说明符
格式符 类型 输出示例 说明 %dint-123十进制有符号整数 %uunsigned int456十进制无符号整数 %ounsigned int777八进制无符号整数 %xunsigned int1a3f十六进制小写(无符号) %Xunsigned int1A3F十六进制大写(无符号) %ffloat/double3.141593小数形式浮点数(默认6位小数) %efloat/double3.141593e+00科学计数法小写 %Efloat/double3.141593E+00科学计数法大写 %gfloat/double3.14159自动选择 %f或%e(更简洁)%Gfloat/double3.14159自动选择 %f或%E%ccharA单个字符 %schar*(字符串)Hello字符串 %pvoid*0x7ffd3456指针地址(十六进制) %%- %输出百分号本身 宽度和精度控制
格式 作用 示例 输出结果 %5d最小宽度为5(右对齐) printf("%5d", 10)10%-5d最小宽度为5(左对齐) printf("%-5d", 10)10%05d宽度5,不足补0 printf("%05d", 10)00010%.2f保留2位小数 printf("%.2f", 3.14159)3.14%8.2f宽度8,保留2位小数 printf("%8.2f", 3.14)3.14长度修饰符(用于整数和浮点数)
修饰符 类型 示例 说明 %hdshort int-123短整型 %ldlong int-123456长整型 %lldlong long int-123456长长整型 %luunsigned long123456无符号长整型 %Lflong double3.141593长双精度浮点数 scnaf()安全性问题
scanf("%s", str)可能导致缓冲区溢出。
解决方案:使用%ns(n为最大长度):char str[10]; scanf("%9s", str); // 最多读取 9 个字符 + '\0'在C89中,负数除法的结果可以向上或向下取整。C99中,除法的结果全部向0取整。
一元运算符的优先级 > 二元运算符,比如
-i * -k == (-i) * (-k)二元运算符都是左结合的(left associative),比如
i - j - k == (i - j) - k一元运算符都是右结合的(right associative),比如
-+i == -(+i)赋值运算符是右结合的,
i = j = k = 0;等价于i = (j = (k = 0))int i, j, k; i = 1; k = 1 + (j = i); printf("%d,%d,%d", i, j, k); //prints "1,1,2"复合赋值运算符与
=一样是右结合的,i += j += k == i += (j += k)运算符优先级列表
数组下标、圆括号、成员选择(对象)、成员选择(指针) 运算符: [], (), ., -> 结合方向: 左到右
单目运算符 运算符: -, ~, ++, --, *, &, !, (类型), sizeof 结合方向: 右到左
乘法、除法、取模 运算符: *, /, % 结合方向: 左到右
加法、减法 运算符: +, - 结合方向: 左到右
位移运算 运算符: <<, >> 结合方向: 左到右
关系运算符 运算符: >, >=, <, <= 结合方向: 左到右
相等运算符 运算符: ==, != 结合方向: 左到右
按位与 运算符: & 结合方向: 左到右
按位异或 运算符: ^ 结合方向: 左到右
按位或 运算符: | 结合方向: 左到右
逻辑与 运算符: && 结合方向: 左到右
逻辑或 运算符: || 结合方向: 左到右
条件运算符 运算符: ?: 结合方向: 右到左
赋值运算符 运算符: =, /=, *=, %=, +=, -=, <<=, >>=, &=, ^=, |= 结合方向: 右到左
逗号运算符 运算符: , 结合方向: 左到右
C 语言运算符优先级完整表格(从高到低)
以下是 C 语言中所有运算符的优先级顺序,同一行的运算符优先级相同,按结合性决定计算顺序。
1. 最高优先级(从左到右结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
() | 函数调用 | func(a, b) |
[] | 数组下标 | arr[5] |
. | 结构体成员访问 | obj.member |
-> | 指针结构体成员访问 | ptr->member |
++ -- (后缀) | 后自增/自减 | i++, arr[i--] |
2. 单目运算符(从右到左结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
++ -- (前缀) | 前自增/自减 | ++i, --j |
+ - | 正负号 | -5, +3.14 |
! ~ | 逻辑非、按位取反 | !flag, ~mask |
(type) | 强制类型转换 | (int)3.14 |
* | 解引用 | *ptr |
& | 取地址 | &var |
sizeof | 计算大小 | sizeof(int) |
3. 乘除取模(从左到右结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
* / % | 乘、除、取模 | a * b, 10 % 3 |
4. 加减(从左到右结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
+ - | 加、减 | a + b, x - y |
5. 位移(从左到右结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
<< >> | 左移、右移 | a << 2, b >> 1 |
6. 关系运算符(从左到右结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
< <= > >= | 大小比较 | a < b, x >= 10 |
7. 相等性判断(从左到右结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
== != | 等于、不等于 | a == b, x != 0 |
8. 按位与(从左到右结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
& | 按位与 | flags & MASK |
9. 按位异或(从左到右结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
^ | 按位异或 | a ^ b |
10. 按位或(从左到右结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
| ` |
11. 逻辑与(从左到右结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
&& | 逻辑与 | a > 0 && b < 10 |
12. 逻辑或(从左到右结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
| | |
13. 条件运算符(从右到左结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
?: | 三元条件 | a > b ? a : b |
14. 赋值运算符(从右到左结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
= += -= *= /= %= <<= >>= &= ` | = ^=` | 赋值及复合赋值 |
15. 逗号运算符(从左到右结合)
| 运算符 | 描述 | 示例 |
|---|---|---|
, | 逗号(顺序求值) | a = 1, b = 2 |
关键记忆技巧
- 括号最高:
()>[]>->>. - 单目右结合:
++--!~(type)*&sizeof - 算术 > 移位 > 比较 > 位运算 > 逻辑 > 条件 > 赋值 > 逗号
经典问题分析
问题 1:*ptr++ 是什么含义?
- 等价于
*(ptr++)(因为后缀++优先级高于*)。 - 先返回
*ptr,然后ptr自增。
问题 2:a & b == c 的陷阱
- 实际解析为
a & (b == c)(因为==优先级高于&)。 - 正确写法:
(a & b) == c
问题 3:a = b = c 的执行顺序
- 从右到左结合:
a = (b = c)。
总结
优先级决定运算顺序,结合性决定同优先级时的方向。
不确定时用括号明确优先级(如
(a & b) == c)。避免依赖复杂表达式,拆分为多行更安全。
%运算符要求操作数是整数,浮点数使用fmod()函数a[i++] += 2在 C 中,同一表达式内对同一变量的多次修改是未定义行为。
a[i++] = a[i++] + 2int的取值范围为-2^31 ~ 2^31 - 1(double)a / b`` (double)(a / b)补码的规则:对于有符号数,补码的最高位是符号位,“0” 表示正数,“1” 表示负数。当符号位为 “1” 时,求其原码的方法是:先对除符号位以外的其他位取反,然后再加 1。
1. 原码中的零:存在歧义
- 定义:原码的最高位为符号位,其余位表示绝对值。
- 问题:
对于 0,存在两种编码方式:- +0:符号位为 0,数值位全 0 →
0000 0000(8 位)。 - -0:符号位为 1,数值位全 0 →
1000 0000(8 位)。
- +0:符号位为 0,数值位全 0 →
- 缺陷:
同一个数值(0)有两种不同的二进制表示,导致以下问题:- 逻辑冗余:计算机需要额外判断两种零是否相等。
- 浪费编码:占用了一个额外的编码位置(原码中
1000 0000本可用于表示其他数值)。
2. 补码中的零:唯一表示
- 定义:正数的补码是其本身,负数的补码是原码取反加 1。
- 推导:
- +0 的原码为
0000 0000,补码保持不变 →0000 0000。 - -0 的原码为
1000 0000,补码计算步骤:- 取反:
1111 1111 - 加 1:
1 0000 0000→ 溢出最高位后结果为0000 0000。
- 取反:
- +0 的原码为
- 结论:
无论正零还是负零,补码都表示为0000 0000,消除了歧义。
十六进制数
0xff转换为十进制数是 255,转换为二进制数是11111111。- 十六进制转十进制的计算方法:在十六进制中,
f代表 15,根据位权展开计算,0xff即 15×161+15×160=255。
- 十六进制转十进制的计算方法:在十六进制中,
sizeof(a)是int类型的大小,通常为 4 字节(取决于编译器和系统)~是按位取反运算符int x = 20;,~x是-21fabs()即为浮点数版本的abs()在 C 语言中,函数参数传递方式分为值传递(单项值传递)和地址传递
每进入更深一层的递归时,问题的规模相对于前一次递归不一定是变化的,可能减小(阶乘)、不变(DFS)甚至增大(斐波那契),具体取决于递归算法的设计和实现。
#include ""的搜索规则编译器行为:
- 优先搜索用户指定的目录(如果路径被显式指定)。
例如:#include "src/header.h"会搜索当前目录下的src子目录。 - 若未指定路径,则:
- 先搜索当前源文件所在的目录。
- 再搜索编译器或 IDE 设置的用户自定义目录(如项目配置的头文件路径)。
- 最后搜索标准系统目录(如
/usr/include或编译器安装路径)。
- 优先搜索用户指定的目录(如果路径被显式指定)。
#include <>的搜索规则编译器行为:
- 直接跳过当前目录,仅搜索:
- 标准系统目录(如 GCC 的
/usr/include)。 - 编译器或 IDE 设置的系统头文件路径(如
-I选项指定的路径)。
- 标准系统目录(如 GCC 的
- 直接跳过当前目录,仅搜索:
#pragma once告诉编译器,包含该指令的头文件在一个编译单元(通常是一个源文件及其包含的所有头文件)中只被处理一次。当编译器第一次遇到包含#pragma once的头文件时,它会记住这个头文件已经被包含,并在后续的编译过程中忽略对该头文件的再次包含。在 C 语言中,
#include预处理指令主要用于包含头文件(.h文件),但从语法上来说,它可以包含任何文本文件在 C 语言中,外部变量和静态变量都存储在静态存储区。当它们未显式赋初值时,系统会自动将它们初始化为 0(对于数值型变量)或空指针(对于指针型变量)。
void f(int b) { static int a = 10; a = a - b; printf(":%d", a); } int main() { static int a = 2; { f(a); static int a = 1; f(a); } return 0; } // f(a)中的a和外面的a不是同一个a // 最终输出8 7数组省略只可省略第一维。但
场景 是否可省略第一维 示例 声明并初始化 ✅ 可省略 int arr[][3] = { ... };未初始化 ❌ 不可省略 int arr[2][3];结构体 / 联合体成员 ❌ 不可省略 struct { int arr[2][3]; };函数参数(指针形式) ❌ 不可省略 void func(int (*arr)[2][3]);typedef 定义数组类型 ❌ 不可省略 typedef int ArrType[2][3];动态内存分配(malloc) ❌ 不可省略 int (*arr)[3] = malloc(2*...);若要移动指针,需显式转换
若需要遍历数组,应将数组名转换为指针变量:
char s[10] = "hello"; char *p = s; // p指向s[0] p++; // ✅ p现在指向s[1](即'e')5. 数组名与指针的行为差异
操作 数组名 s指针 ps++❌ 编译错误 ✅ 指针自增 sizeof(s)返回数组总大小(如 10) 返回指针大小(如 4/8) &s类型为 char (*)[10]类型为 char**作为函数参数 隐式转换为 char*直接传递指针 指针之间可以互相比较。数组的地址是连续的,所以可以比较。
C语言中访问数组
a[i]允许写成i[a],因为*(a + i) == *(i + a)C语言中,字符串常量存储在只读数据段,多个相同的字符串常量在内存中只会存储一份。
strlen()不计算\0#include <stdio.h> int main() { char *ch; scanf("%s", ch); switch(ch) { case "1": printf("1"); break; } }问题分析
未初始化的指针
char *ch声明了一个指针,但没有分配内存。scanf("%s", ch)会尝试将输入写入随机内存地址,导致段错误。使用字符串字面量作为
switch的 caseswitch语句只能比较整数类型(如int、char)或枚举类型,而"1"是字符串字面量(const char*),无法直接用于case。缺少输入长度限制
scanf("%s", ch)没有指定最大输入长度,可能导致缓冲区溢出。
#include <stdio.h> int main() { int day = 3; switch (day) { // 整型表达式 case 1: printf("Monday\n"); break; case 2: printf("Tuesday\n"); break; case 3: case 4: // 合并 case printf("Midweek\n"); break; default: // 可选 printf("Another day\n"); } return 0; }case可以合并。swtich语句中case后面必须是常量表达式,不可以是变量。并且要求类型必须和switch表达式中的类型一致。
数组的初始化只能在声明时进行,后续赋值只能逐个元素操作。
声明数组时如果没有进行初始化,则必须提供所有维度的长度。
\x是十六进制转义序列,在字符串定义中会被转化成十六进制的数字
char str[50]; strcpy(str, "guess-what?"); strcpy(&str[3], "none"); strcat(str, "blue?"); //str为guenoneblue?char *c = "sysuer"; sizeof(c) == 4;const int*可以指向int类型变量,但不能通过指针修改其值。int*不能指向const int类型变量。函数指针不支持算术运算,因为函数在内存中的布局不是连续的。但可以进行相等性的比较操作,判断是否为同一个函数。
int arr[3][3] = {1,4,7,2,5,8,3,6,9}; int (*p)[3] = arr; //p是指向一个大小为3的数组的指针 int a = *p[1]; // a == 2 int b = **p + 1; // b == 2 int c = **(p + 1); // c == 2 int d = *(*p + 1) + 1); // d == 5int* restrict p如果指针p指向的对象之后需要修改,那么该对象不会允许通过除指针p之外的任何方式访问。C语言数据类型大小
在C语言中,数据类型决定了变量存储占用的空间以及如何解释存储的位模式。不同的数据类型在不同的系统中可能占用不同的字节数和位数。以下是C语言中常见数据类型的大小和取值范围。
整数类型
有符号整数类型
signed char: 1字节,取值范围为-128到127。
short int: 2字节,取值范围为-32,768到32,767。
int: 4字节,取值范围为-2,147,483,648到2,147,483,647。
long int: 4字节,取值范围为-2,147,483,648到2,147,483,647。
long long int: 8字节,取值范围为-9,223,372,036,854,775,808到9,223,372,036,854,775,807。
无符号整数类型
unsigned char: 1字节,取值范围为0到255。
unsigned short int: 2字节,取值范围为0到65,535。
unsigned int: 4字节,取值范围为0到4,294,967,295。
unsigned long int: 4字节,取值范围为0到4,294,967,295。
unsigned long long int: 8字节,取值范围为0到18,446,744,073,709,551,615。
浮点类型
float: 4字节,取值范围为±3.4e38,精确到6位小数。
double: 8字节,取值范围为±1.7e308,精确到15位小数。
long double: 12字节,取值范围为±1.19e4932,精确到18位小数。
结构化程序的三种结构:顺序结构、选择结构、循环结构。goto语句不算。
include、define和printf不是C语言关键字。程序调试的任务是诊断和改正程序中的错误。
结构化程序可以完成任何复杂的任务。
使用
strcpy时,目标空间必须可变,不可以是常量字符串。strlen不止遇到\0会终止,遇到0也会。但得看清楚是'0'还是\0\,前者ASCII编码是48。二维数组第二维长度不可以省略。
int a[1][2], *p; p = a; // 这里的p是指向int类型的指针,不能赋a,a是int**类型指针字符数组不能用
=赋值,除非在初始化的时候。字符串字面量char *str则可以。字符数组和字符串是两个东西。
宏定义
#define square(x) x * x存在运算符优先级问题,导致意外结果。例如:int a = 3; printf("%d", square(a + 1)); // 预期 16,实际计算 3 + 1 * 3 + 1 = 7正确写法应添加括号确保运算顺序:
#define square(x) ((x) * (x))小写字母的ASCII码大于大写字母。
两个指针变量不可以相加。
int func(char *s) { char *t = s; while (*t++); return t - s; }该函数不能计算字符串长度,若要计算应该
return t - s - 1;结构体类型名是由关键字
struct和结构体名组成的。注意
fprintf()读写的是字符数据,对象是文本文件(ASCII文件), 而getw()、putw()、fread()、fwrite()是读写二进制数据,对象是二进制文件;
