/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
C 语言诞生于 20 世纪 70 年代。它的语法并不复杂,却能直接操作内存、组织底层资源,因此至今仍广泛用于操作系统、嵌入式、编译器、驱动和高性能程序。
从第一性原理看,程序只有两件事:保存数据,处理数据。C 语言提供了类型、变量、运算符、流程控制、函数、数组、指针和结构体,帮助我们把这两件事表达清楚。
本文不追求一次讲完所有细节,而是沿着一条主线,走完 C 语言最重要的基础概念。
1. 第一个 C 语言程序
#include <stdio.h>
int main(void) {
printf("Hello, C!\\n");
return 0;
}
main 是程序的入口函数。一个可执行的 C 程序必须有且只有一个 main 函数,程序从这里开始执行。
逐行看:
- #include <stdio.h>:包含标准输入输出库的声明,printf 的声明就在这里。
- int main(void):定义一个名为 main 的函数,它返回 int,且不接收参数。
- printf(…):把文字输出到终端。
- return 0:告诉操作系统程序正常结束。
C 源文件不会被计算机直接执行。编译器会先把源代码翻译成目标代码,再由链接器把目标代码和库连接起来,最终生成可执行文件。

2. 数据类型:数据应该如何解释
计算机内存本质上只是一串字节。数据类型告诉编译器:这些字节代表什么,以及要占用多少空间。
C 语言常见的基本类型如下:
| char | 字符或小整数 | 1 字节 |
| short | 短整型 | 2 字节 |
| int | 常用整型 | 4 字节 |
| long | 长整型 | 4 或 8 字节 |
| long long | 更大范围的整型 | 8 字节 |
| float | 单精度浮点数 | 4 字节 |
| double | 双精度浮点数 | 8 字节 |
| bool | 真或假 | 通常 1 字节 |
具体大小由编译器和平台决定,32 位平台小一些(4 字节),64 位平台大一些(8 字节)。
声明变量时,需要同时写出类型和名字:
#include <stdio.h>
int main(void) {
char ch = 'A';
int count = 10;
double price = 1.23;
printf("%c %d %.2f\\n", ch, count, price);
printf("int: %zu bytes\\n", sizeof(int));
return 0;
}
sizeof 计算对象或类型占用的字节数,结果类型是 size_t,通常使用 %zu 输出。
类型不仅决定大小,也决定运算规则。例如,两个 int 相除仍是整数除法:5 / 2 的结果是 2;如果希望得到小数,需要让至少一个操作数参与浮点运算:5.0 / 2。
*注:在 C99 至 C17 中,使用 bool、true 和 false 需要包含头文件:
#include <stdbool.h>
bool finished = false;
这些版本中的 bool 实际上由标准库映射到内置类型 _Bool。从 C23 开始,bool、true 和 false 可以直接使用。日常代码写 bool 即可,认识 _Bool 是为了能看懂标准和旧代码。
3. 变量与常量
程序需要保存数据。数据在执行过程中可能改变,就用变量;不应改变或希望明确表达固定含义,就用常量。
3.1 变量的定义
int age = 18;
double score = 95.5;
char grade = 'A';
一条变量定义可以理解为:向编译器申请一块符合指定类型的存储空间,给它起一个名字,并在需要时写入初始值。
定义但没有初始化的局部变量,其值是不确定的,使用前必须先赋值:
int n; // 不要直接读取 n
n = 10; // 先赋值,再使用
3.2 局部变量与全局变量
#include <stdio.h>
int value = 100; // 全局变量
int main(void) {
int value = 300; // 局部变量,与全局变量同名
printf("%d\\n", value);
return 0;
}
输出是 300。当局部变量和全局变量同名时,在该局部作用域内,局部变量会遮蔽全局变量。
3.3 作用域与生命周期
- 作用域:名字可以被使用的代码范围。块内定义的局部变量通常只在该块中可见;普通全局变量从定义处开始,在整个源文件可见,也可以通过 extern 跨文件声明。
- 生命周期:对象实际存在的时间。普通局部变量进入代码块时创建,离开代码块时销毁;全局变量从程序启动一直存在到程序结束。
3.4 使用输入输出
#include <stdio.h>
int main(void) {
int a = 0;
int b = 0;
printf("请输入两个整数:");
if (scanf("%d %d", &a, &b) == 2) {
printf("%d + %d = %d\\n", a, b, a + b);
}
return 0;
}
printf 负责输出,scanf 负责输入。scanf 需要把输入结果写入变量,所以要传入变量的地址:&a、&b。关于地址和指针,后文会详细解释。
3.5 常量
C 中常见的“固定值”有四种形式:
10 // 字面常量
const int days = 7; // const 对象:不能通过 days 修改
#define MAX 100 // 宏常量:预处理阶段进行文本替换
enum color { RED, GREEN, BLUE }; // 枚举常量
const 表示通过这个名字不能修改对象,但它不等同于 C++ 意义上的编译期常量;需要预处理替换时才使用 #define,并注意宏没有类型检查。
4. 字符、字符串与转义字符
4.1 字符与字符串
字符常量用单引号表示,例如 'A';字符串字面量用双引号表示,例如 "Hello"。
C 没有独立的字符串类型。字符串本质上是一个以空字符 \\0 结尾的 char 数组:
char text[] = "Hello";
内存中实际存放的是 6 个字符:H、e、l、l、o、\\0。

如果没有 \\0,像 printf("%s", text) 这样的字符串函数就不知道应该在哪里停止读取。
4.2 转义字符
反斜杠 \\ 引出转义字符,用来表示换行、制表或源代码中不方便直接写出的字符。
printf("Hello\\nWorld\\n");
printf("A\\tB\\n");
printf("c:\\\\code\\\\test.c\\n");
输出目录时必须把反斜杠写成 \\\\。否则,\\t 会被解释为制表符:
printf("c:\\code\\test.c"); // 错误示例:\\t 会变成 Tab
printf("c:\\\\code\\\\test.c"); // 正确
常用转义字符:
| \\0 | 空字符,字符串结束标志 |
| \\n | 换行 |
| \\t | 水平制表符 |
| \\\\ | 反斜杠 |
| \\" | 双引号 |
| \\' | 单引号 |
| \\r | 回车,将光标移到当前行开头 |
| \\b | 退格 |
| \\a | 响铃(是否有声音取决于终端) |
C 还支持八进制 \\ooo 和十六进制 \\xhh 表示字符,例如 \\x41 表示字符 A。
4.3 注释
注释只给人阅读,编译器会忽略它们:
/* C 风格注释,可以跨多行 */
// C99 及之后支持的单行注释
5. 选择语句:根据条件决定路径
C 中,条件表达式的结果为 0 时表示假,非 0 时表示真。
#include <stdio.h>
int main(void) {
int score = 85;
if (score >= 60) {
printf("及格\\n");
} else {
printf("不及格\\n");
}
return 0;
}
if 先计算条件,再执行一个分支。else if 可以继续添加条件:
if (score >= 90) {
printf("A\\n");
} else if (score >= 60) {
printf("B\\n");
} else {
printf("C\\n");
}
当要根据同一个表达式匹配多个固定值时,可以使用 switch:
switch (choice) {
case 1:
printf("开始\\n");
break;
case 2:
printf("退出\\n");
break;
default:
printf("未知选项\\n");
break;
}
break 用来结束当前 switch。如果省略它,程序会继续执行下一个 case,这叫“贯穿”。
6. 循环语句:重复执行一段代码
循环的核心是:先判断是否满足条件,满足就执行一次,再回到判断处。
6.1 while
int count = 1;
while (count <= 10) {
printf("%d ", count);
count++;
}
while 先判断条件,因此一次也可能不执行。
6.2 for
for (int count = 1; count <= 10; count++) {
printf("%d ", count);
}
for 把初始化、条件和迭代写在一行,适合计数循环。上面的代码等价于:
int count = 1;
while (count <= 10) {
printf("%d ", count);
count++;
}
6.3 do…while
int count = 1;
do {
printf("%d ", count);
count++;
} while (count < 1);
do…while 先执行一次,再判断条件,因此至少执行一次。注意结尾的分号不能省略。
循环中还常用两个控制语句:
- break:立即结束当前循环。
- continue:跳过本轮剩余代码,进入下一轮判断。
7. 函数:给一段操作命名
函数是完成特定任务的一段代码。它可以接收输入(参数),经过处理后返回结果,从而避免重复编写相同逻辑。
#include <stdio.h>
int add(int x, int y) {
return x + y;
}
int main(void) {
int x = 0;
int y = 0;
printf("请输入两个整数:");
scanf("%d %d", &x, &y);
printf("结果:%d\\n", add(x, y));
return 0;
}
函数定义包含返回类型、函数名、参数列表和函数体。如果函数定义写在调用之后,需要先声明函数原型:
int add(int x, int y); // 函数声明
C 的参数传递默认是值传递:调用函数时,实参的值会复制给形参。函数内修改形参,不会改变调用者的变量;如果要修改调用者的变量,需要传递地址,这正是指针的用途。
8. 数组:连续存放同类型数据
数组是一组类型相同、在内存中连续存放的元素。数组长度在定义时确定,元素下标从 0 开始。
int nums[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
printf("nums[%d] = %d\\n", i, nums[i]);
}
如果初始化列表少于数组长度,剩余元素会被初始化为 0:
int nums[5] = {1, 2}; // 实际为 {1, 2, 0, 0, 0}
访问越界不会自动报错,结果是未定义行为。数组长度必须由程序员保证。
在数组仍是数组的作用域内,可以用下面的方式计算元素个数:
size_t length = sizeof nums / sizeof nums[0];
数组作为函数参数时会退化为指针,此时 sizeof 得到的是指针大小,不能再用上面的公式计算长度,通常需要额外传入长度。
9. 操作符:对数据执行操作
操作符连接一个或多个操作数,产生一个结果,或改变对象的值。
| 算术 | + – * / % | 加、减、乘、除、取余 | a + b |
| 自增自减 | ++ — | 加一、减一 | i++ |
| 关系 | < <= > >= == != | 比较大小或相等 | a == b |
| 逻辑 | `&& | !` | |
| 位运算 | `& | ^ ~ << >>` | 按位与、或、异或、取反、移位 |
| 赋值 | = += -= *= /= %= … | 给变量赋值 | a += 1 |
| 指针 | & * | 取地址、解引用 | &a、*p |
| 成员访问 | . -> | 访问结构体成员 | s.age、p->age |
| 下标与调用 | [] () | 访问数组、调用函数 | arr[i]、f() |
| 条件 | ?: | 根据条件返回两个结果之一 | a > b ? a : b |
| 其他 | sizeof、(type)、, | 求大小、类型转换、逗号表达式 | sizeof(int) |
9.1 最容易混淆的符号
- = 是赋值,== 是比较:a = 3 把 3 写入 a,a == 3 判断 a 是否为 3。
- & 可能表示按位与,也可能表示取地址,要看上下文:a & b 是按位与,&a 是取地址。
- 可能表示乘法、声明指针或解引用:a * b、int *p、*p。
- | 是按位或,|| 是逻辑或;& 是按位与,&& 是逻辑与。
9.2 优先级
常见优先级从高到低大致是:
不确定优先级时,直接加括号。括号不仅能改变求值顺序,也能让代码更容易阅读。
10. 关键字
关键字是 C 语言保留的单词,不能用作变量名、函数名或结构体成员名。常见关键字按用途可以分为:
| 类型 | char、short、int、long、float、double、void、signed、unsigned、struct、union、enum |
| 流程控制 | if、else、switch、case、default、for、while、do、break、continue、goto、return |
| 存储与链接 | auto、register、static、extern |
| 类型与限定 | const、volatile、sizeof、typedef |
不同 C 标准定义的关键字略有差异,例如 C99 引入了 _Bool、inline 和 restrict,C23 又将 bool、true 和 false 纳入语言。实际编程时,优先掌握上表,再根据项目采用的 C 标准查阅完整列表。
10.1 static:改变生命周期或可见范围
static 放在不同位置,含义不同。
| 局部变量前 | 生命周期延长到整个程序结束,但作用域不变 |
| 全局变量前 | 只允许当前源文件访问(内部链接) |
| 函数定义前 | 只允许当前源文件调用(内部链接) |
static 修饰局部变量
#include <stdio.h>
void normal(void) {
int count = 0;
printf("normal: %d\\n", ++count);
}
void persistent(void) {
static int count = 0;
printf("static: %d\\n", ++count);
}
int main(void) {
normal(); normal(); normal();
persistent(); persistent(); persistent();
return 0;
}
language/c/1–intro master !? ❯ ./main
normal: 1
normal: 1
normal: 1
static: 1
static: 2
static: 3
输出中,normal 每次都是 1;persistent 依次是 1、2、3。静态局部变量只初始化一次,之后保留上一次调用结束时的值。静态存储期对象如果没有显式初始化,默认初始化为 0。
static 修饰全局变量或函数
先看一个可以正常跨文件访问的例子。下面是两个源文件:
// counter.c
int g_count = 0;
void reset(void) { g_count = 0; }
// main.c
#include <stdio.h>
extern int g_count;
void reset(void);
int main(void) {
g_count = 42;
reset();
printf("g_count = %d\\n", g_count); // 0
return 0;
}
编译时把两个源文件一起交给编译器:
cc counter.c main.c -o demo
这里的 g_count 和 reset 都具有外部链接,main.c 可以通过声明使用它们。
如果把 counter.c 中的变量改成 static:
// counter.c
static int g_count = 0;
void reset(void) {
g_count = 0;
}
此时 g_count 只在 counter.c 内可见。reset 仍然可以被其他文件调用,但 main.c 中的 extern int g_count; 将无法链接到它。通常的做法是:把数据设为 static,只对外提供操作数据的函数。
同理,如果把函数定义为 static:
// counter.c
static void reset(void) {
/* 只能在 counter.c 中调用 */
}
那么 main.c 即使写出 void reset(void);,调用时也会在链接阶段报“未定义引用”。
因此,文件作用域的 static 并不是改变变量的生命周期,而是把变量或函数的链接属性改为“仅当前源文件可见”。

11. #define:预处理阶段的替换
#define 属于预处理指令,不是变量定义。预处理器会在编译前,把宏名替换成宏体。
#include <stdio.h>
#define MAX 100
#define ADD(x, y) ((x) + (y))
int main(void) {
printf("%d\\n", MAX);
printf("%d\\n", ADD(1, 2));
return 0;
}
宏参数和整个宏体都应尽量加括号,避免运算优先级导致意外结果。宏没有类型检查,也可能重复计算参数,因此复杂逻辑通常更适合写成函数。
12. 指针:保存地址的变量
12.1 先理解内存
程序运行时,变量需要放在内存中。内存可以看成许多连续的小格子,每个格子至少有一个地址;在 C 语言里,地址是一个用于定位对象的数值。
需要区分两个概念:地址是位置,指针是保存地址的对象(通常就是指针变量)。 地址本身不是指针变量。
int a = 10;
printf("%p\\n", (void *)&a);
&a 是取地址运算,得到变量 a 的地址。%p 用于输出指针,按 C 标准应转换为 void *。

12.2 定义和使用指针
#include <stdio.h>
int main(void) {
int a = 10;
int *p = &a;
printf("a = %d\\n", a);
printf("address = %p\\n", (void *)p);
printf("*p = %d\\n", *p);
return 0;
}
int *p 表示 p 是一个“指向 int 的指针”;p 中保存 a 的地址。*p 是解引用运算,表示沿着 p 保存的地址访问那块内存,因此 *p 的值就是 a 的值。
指针类型很重要。int *、double * 虽然都保存地址,但解引用时,编译器会按不同类型解释内存,并使用不同的步长。指针变量自身的大小由平台决定,可以用 sizeof p 查询,不要假定永远是 4 或 8 字节。
未指向有效对象的指针不能解引用。没有目标时,应初始化为空指针:
int *p = NULL;
12.3 为什么需要指针
C 的参数传递是值传递。下面的 x 是 a 的副本:
void change(int x) {
x = 20;
}
int main(void) {
int a = 10;
change(a);
printf("%d\\n", a); // 10
return 0;
}

如果把地址传进去,函数就能找到并修改原变量:
#include <stdio.h>
void change(int *pa) {
*pa = 20;
}
int main(void) {
int a = 10;
change(&a);
printf("%d\\n", a); // 20
return 0;
}
这里发生了三步:

可以把 *pa 暂时理解为“通过 pa 找到的那个变量”,在本例中就是 a。
13. 结构体:把相关数据放在一起
数组只能保存一组相同类型的数据。现实中的一个对象往往由不同类型的数据组成,例如学生同时有姓名、年龄和学号。结构体用于描述这样的复合数据。
#include <stdio.h>
struct student {
char name[20];
int age;
char id[15];
};
int main(void) {
struct student s = {"张三", 18, "20260001"};
struct student *ps = &s;
printf("name = %s, age = %d, id = %s\\n",
s.name, s.age, s.id);
printf("name = %s, age = %d, id = %s\\n",
ps->name, ps->age, ps->id);
return 0;
}
- s.name:通过结构体变量访问成员。
- ps->name:通过结构体指针访问成员。
- ps->name 等价于 (*ps).name。括号不能省略,因为 . 的优先级高于 。
结构体成员按声明顺序排列,但编译器可能为了对齐在成员之间插入填充字节,因此 sizeof(struct student) 不一定等于各成员大小的简单相加。
如果不想每次都写 struct student,可以用 typedef 起别名:
typedef struct {
char name[20];
int age;
char id[15];
} Student;
Student s = {"张三", 18, "20260001"};
结语:把知识串起来
现在可以用一条主线回顾 C 语言:
- 类型规定数据如何解释;
- 变量给内存中的数据命名;
- 运算符处理数据;
- 选择和循环控制执行路径;
- 函数组织可复用的操作;
- 数组连续保存同类型数据;
- 指针保存地址,让程序能够间接访问和修改内存;
- 结构体把不同类型的数据组合成一个更大的对象;
- static、extern 和宏控制名字的可见范围、生命周期和编译前替换。
理解这些概念后,再学习动态内存、文件、函数指针和数据结构,就不再是记忆孤立的语法,而是在同一套“数据、内存和操作”的模型上继续展开。
网硕互联帮助中心
![buuctf-pwn [NewStarCTF 2023 公开赛道]stack migration(64位栈迁移)题解(学习过程持续更新)-网硕互联帮助中心](https://www.wsisp.com/helps/wp-content/uploads/2026/09/20260918160847-6aad620fbd11a-220x150.png)



评论前必须登录!
注册