这一篇从 2.0 开始,主要整理 C 语言里最基础的一组表达方式:怎么命名、怎么定义变量、怎么表示常量、怎么输出、怎么接收输入。

这些内容看起来都很普通,但后面学指针、内存、结构体、文件读写和 Windows API 时,所有问题都会落回这些基础细节:类型是否匹配、缓冲区是否够用、格式化字符串是否正确、输入边界是否检查。

2.0 关键字以及命名规范

C 语言里的关键字是语言已经占用的词,不能再拿来当变量名、函数名或自定义标识符。

类别 常见关键字 作用
类型相关 intcharfloatdoublevoid 描述数据类型或函数返回值
控制流程 ifelseswitchcaseforwhiledo 控制程序执行顺序
跳转控制 breakcontinuereturngoto 改变当前执行流程
存储相关 autostaticexternregister 描述变量的存储方式或链接属性
类型限定 constvolatilesignedunsignedshortlong 补充类型的取值范围或访问规则
自定义类型 structunionenumtypedef 组织更复杂的数据结构
大小计算 sizeof 计算类型或对象占用的字节数

标识符规则

变量名、函数名、结构体名都属于标识符。基本规则先记住这几条:

  1. 只能由字母、数字和下划线组成。
  2. 不能以数字开头。
  3. 不能使用 C 语言关键字。
  4. 区分大小写,valueValue 是两个不同名字。
  5. 尽量不要用中文、空格和奇怪符号,后续跨平台、命令行和脚本处理都会麻烦。
1
2
3
4
5
6
7
8
int age = 20;
int user_score = 95;
int UserScore = 100;

// 错误示例:
// int 2num = 10;
// int for = 1;
// int user-name = 3;

命名习惯

命名不是为了“显得规范”,而是为了让几个月后的自己还能看懂这段代码。

场景 示例 说明
普通变量 scoreuser_age 直接表达变量含义
函数名 print_menucalc_sum 尽量用动词或动宾结构
常量 MAX_SIZEBUFFER_LEN 常量可以用全大写加下划线
临时变量 ijtmp 只适合很短的局部范围
布尔含义 is_validhas_error 让条件判断更自然

2.1 变量定义及初始化

变量可以理解成一块有名字的内存空间。类型决定这块空间怎么解释,变量名方便我们在代码里使用它。

1
2
3
int age = 20;
double height = 1.75;
char level = 'A';
概念 示例 说明
定义变量 int age; 创建一个变量
初始化 int age = 20; 创建变量时给初始值
赋值 age = 21; 变量已经存在,再修改它的值
读取变量 printf("%d", age); 使用变量当前保存的值

初始化很重要

没有初始化的局部变量,它里面可能是内存中原本残留的数据。初学阶段不要依赖这种“碰巧能跑”的结果。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(void)
{
int count = 0;
int total = 0;

total = count + 10;
printf("total = %d\n", total);

return 0;
}

作用域和生命周期

变量位置 作用范围 初学理解
函数内部 从定义处到所在代码块结束 局部变量,用完就结束
函数外部 当前文件或多个文件中可见 全局变量,谨慎使用
代码块内部 {} 内部 只在这一小段逻辑里可见
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int global_count = 100;

int main(void)
{
int local_count = 10;

if (local_count > 0)
{
int block_value = 1;
printf("%d\n", block_value);
}

printf("%d %d\n", global_count, local_count);
return 0;
}

2.2 类型限定符

类型限定符用来补充说明一个基础类型的范围、符号或访问方式。

写法 含义 例子
signed 有符号,可以表示正负数 signed int a = -10;
unsigned 无符号,只表示非负数 unsigned int size = 100;
short 较短整数类型 short s = 10;
long 较长整数类型 long value = 1000L;
long long 更长整数类型 long long big = 100000LL;
const 值不应该被修改 const int max = 100;
volatile 值可能被外部因素改变 常见于硬件寄存器、多线程或系统编程场景

signedunsigned

unsigned 很常见,比如数组长度、缓冲区大小、文件大小等。但它也容易带来比较问题。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(void)
{
signed int a = -1;
unsigned int b = 1;

printf("a = %d\n", a);
printf("b = %u\n", b);

return 0;
}

安全开发里经常会遇到长度、索引和偏移。只要涉及 unsigned,就要特别小心减法、比较和边界判断,避免把一个负数转换成非常大的无符号数。

sizeof 验证类型大小

不同平台和编译器上,部分类型大小可能不同。不要只靠背,最好用 sizeof 验证。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(void)
{
printf("sizeof(char) = %zu\n", sizeof(char));
printf("sizeof(short) = %zu\n", sizeof(short));
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(long) = %zu\n", sizeof(long));
printf("sizeof(long long) = %zu\n", sizeof(long long));

return 0;
}

2.3 常量特性及表现形式

常量是不希望在程序运行过程中被随意修改的数据。C 语言里常见常量可以分成字面量、宏常量、const 常量和枚举常量。

形式 示例 说明
整型字面量 100x10010 十进制、十六进制、八进制
浮点字面量 3.141.0f 小数常量
字符常量 'A''\n' 单个字符
字符串常量 "hello" 一串字符,结尾包含 \0
宏常量 #define MAX_SIZE 100 预处理阶段替换
const 常量 const int max = 100; 有类型检查,更推荐
枚举常量 enum Color { RED, BLUE }; 表示一组具名整数值

不同进制

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void)
{
int dec = 16; // 十进制
int hex = 0x10; // 十六进制
int oct = 020; // 八进制

printf("%d %d %d\n", dec, hex, oct);
return 0;
}

#defineconst

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

#define MAX_BUFFER 256

int main(void)
{
const int retry_count = 3;

printf("buffer = %d\n", MAX_BUFFER);
printf("retry = %d\n", retry_count);

return 0;
}
写法 优点 注意
#define 简单直接,常用于宏替换 没有类型检查,调试时不一定直观
const 有类型,更像普通变量 在 C 里不一定能用于所有编译期常量场景
enum 适合一组相关常量 本质上常作为整数处理

2.4 拓展:突破编译器限制

这里的重点不是鼓励写“绕过规则”的代码,而是理解:编译器提供的是一层类型和语法约束,真正运行时,数据最终还是落在内存字节上。

比如一个 int 在内存里就是若干个字节。我们可以通过字符指针观察它的底层字节表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(void)
{
int value = 0x12345678;
unsigned char *p = (unsigned char *)&value;

for (size_t i = 0; i < sizeof(value); i++)
{
printf("byte[%zu] = 0x%02X\n", i, p[i]);
}

return 0;
}

这段代码适合用来观察大小端和内存布局,但不要把强制类型转换当成万能工具。越是接近内存底层,越要清楚自己在改哪块内存、按什么类型解释、边界在哪里。

const、类型转换、指针重解释这些内容后面会反复出现。学习时可以用小程序观察现象,但不要在正式代码里靠“强行转换”绕过类型约束,这类写法很容易变成未定义行为。

2.5 拓展:内存保护属性

程序运行时,内存不是一整块随便读写的空间。操作系统会按页管理内存,并给不同区域设置不同的访问属性。

区域 常见内容 常见属性理解
代码区 程序指令 可执行,通常不希望随意写
只读数据区 字符串常量、只读数据 可读,不可写
局部变量、函数调用信息 可读写
动态申请的内存 可读写
映射区域 DLL、文件映射、系统库 属性取决于映射方式

从安全开发角度看,内存保护属性很重要。很多漏洞利用、防护机制和崩溃原因,本质上都和“这块内存能不能读、能不能写、能不能执行”有关。

常见访问异常

行为 可能结果
写入只读内存 访问冲突
执行不可执行内存 触发数据执行保护相关异常
读取无效地址 访问冲突
访问已经释放的堆内存 结果不确定,可能崩溃,也可能暂时看不出来

2.6 拓展:物理页属性探测

这里更准确地说,是在用户态观察“虚拟内存页”的信息。普通应用程序通常不能直接操作真实物理页,但可以查询自己进程里某个地址所在内存区域的状态、大小和保护属性。

在 Windows 下可以用 VirtualQuery 观察一块地址对应的内存信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#ifdef _WIN32

#include <stdio.h>
#include <windows.h>

int main(void)
{
int value = 10;
MEMORY_BASIC_INFORMATION info;

SIZE_T result = VirtualQuery(&value, &info, sizeof(info));

if (result == 0)
{
printf("VirtualQuery failed.\n");
return 1;
}

printf("Address : %p\n", (void *)&value);
printf("BaseAddress : %p\n", info.BaseAddress);
printf("RegionSize : %llu\n", (unsigned long long)info.RegionSize);
printf("Protect : 0x%lx\n", info.Protect);
printf("State : 0x%lx\n", info.State);
printf("Type : 0x%lx\n", info.Type);

return 0;
}

#endif

这类代码暂时只当作“观察工具”来理解。后面学 Windows、PE、进程内存和调试时,再展开保护属性、内存区域、模块加载和权限变化。

这一节先记住一句话:地址不是孤立的数字,它属于某个内存区域,而这个区域有大小、状态和访问权限。

2.7 转义字符

转义字符用来表示一些不能直接写出来,或者写出来会产生歧义的字符。

转义字符 含义
\n 换行
\t 水平制表符
\\ 反斜杠
\" 双引号
\' 单引号
\0 字符串结束符
\r 回车
\b 退格
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void)
{
printf("hello\nworld\n");
printf("name\tage\n");
printf("\"C language\"\n");
printf("path: C:\\\\Users\\\\demo\n");

return 0;
}

\0 很重要

C 字符串以 \0 作为结束标志。后面学字符数组和字符串处理时,很多越界和乱码问题都和这个结束符有关。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main(void)
{
char text[] = "ABC";

printf("%c\n", text[0]);
printf("%c\n", text[1]);
printf("%c\n", text[2]);
printf("%d\n", text[3]);

return 0;
}

text[3] 是字符串结尾的 \0,作为整数输出通常是 0

2.8 数据输出与格式化字符串

printf 通过格式化字符串控制输出内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(void)
{
int age = 20;
double score = 95.5;
char level = 'A';

printf("age = %d\n", age);
printf("score = %.2f\n", score);
printf("level = %c\n", level);

return 0;
}

常见格式占位符

占位符 含义 示例
%d 有符号十进制整数 printf("%d", num);
%u 无符号十进制整数 printf("%u", size);
%x / %X 十六进制整数 printf("%X", value);
%c 字符 printf("%c", ch);
%s 字符串 printf("%s", text);
%f 浮点数 printf("%f", score);
%p 地址 printf("%p", ptr);
%zu size_t 类型 printf("%zu", sizeof(int));

宽度和精度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(void)
{
int value = 42;
double pi = 3.1415926;

printf("[%5d]\n", value);
printf("[%-5d]\n", value);
printf("[%.2f]\n", pi);
printf("[%08d]\n", value);

return 0;
}
写法 效果
%5d 宽度为 5,右对齐
%-5d 宽度为 5,左对齐
%.2f 保留两位小数
%08d 宽度为 8,不足补 0

格式化字符串安全

格式化字符串本身也是安全开发里很重要的点。不要把用户输入直接当作格式化字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(void)
{
char input[64] = "hello";

// 不推荐:如果 input 来自外部输入,可能被当成格式化字符串解释
// printf(input);

// 推荐:明确把 input 当字符串输出
printf("%s\n", input);

return 0;
}

以后写日志、命令行工具或网络服务时,要养成 printf("%s", user_input) 这种习惯。格式化字符串错误虽然看起来像“小语法问题”,但在安全领域可以变成真正的漏洞入口。

2.9 数据交互传递

输入输出是程序和外界交换数据的第一步。初学阶段最常见的是控制台输入,后面会扩展到文件、网络、命令行参数和 GUI。

scanfscanf_s

Visual Studio 中常提示使用 scanf_s。这是 MSVC 提供的安全版本,对部分输入需要额外传入缓冲区大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(void)
{
int age = 0;

printf("input age: ");

#ifdef _MSC_VER
scanf_s("%d", &age);
#else
scanf("%d", &age);
#endif

printf("age = %d\n", age);
return 0;
}

字符串输入要限制长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

#define NAME_SIZE 32

int main(void)
{
char name[NAME_SIZE] = {0};

printf("input name: ");

#ifdef _MSC_VER
scanf_s("%31s", name, (unsigned int)sizeof(name));
#else
scanf("%31s", name);
#endif

printf("hello, %s\n", name);
return 0;
}

%31s 的意思是最多读取 31 个字符,留 1 个位置给字符串结束符 \0

fgets 读取一整行

如果输入里可能包含空格,可以用 fgets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string.h>

#define LINE_SIZE 64

int main(void)
{
char line[LINE_SIZE] = {0};

printf("input line: ");

if (fgets(line, sizeof(line), stdin) == NULL)
{
printf("input failed.\n");
return 1;
}

line[strcspn(line, "\n")] = '\0';

printf("line = %s\n", line);
return 0;
}

本章最小复盘代码

这段代码把变量、常量、格式化输出和基础输入放在一起。复盘时重点看:变量是否初始化、输入是否限制长度、格式占位符是否匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <string.h>

#define NAME_SIZE 32
#define PASS_SCORE 60

int main(void)
{
char name[NAME_SIZE] = {0};
int score = 0;
const char level = 'A';

printf("input name: ");

if (fgets(name, sizeof(name), stdin) == NULL)
{
printf("input name failed.\n");
return 1;
}

name[strcspn(name, "\n")] = '\0';

printf("input score: ");

#ifdef _MSC_VER
scanf_s("%d", &score);
#else
scanf("%d", &score);
#endif

printf("\n==== result ====\n");
printf("name : %s\n", name);
printf("score : %d\n", score);
printf("level : %c\n", level);
printf("pass line : %d\n", PASS_SCORE);
printf("status : %s\n", score >= PASS_SCORE ? "pass" : "failed");

return 0;
}

常见错误整理

错误 示例 问题
变量未初始化 int a; printf("%d", a); 输出结果不可靠
格式不匹配 printf("%d", 3.14); 输出异常或产生未定义行为
忘记取地址 scanf("%d", age); 应该传 &age
字符串输入不限制长度 scanf("%s", name); 可能写爆数组
混用有符号和无符号 intunsigned int 比较 结果可能不符合直觉
使用中文符号 printf("hi"); 编译器无法识别
误用八进制 int x = 010; 实际是十进制 8
直接输出用户输入 printf(input); 可能产生格式化字符串风险

学习检查清单

检查项 状态
能说出关键字和普通变量名的区别 待复盘
能按规则写出清晰的变量名和函数名 待复盘
能定义变量,并在使用前完成初始化 待复盘
能区分赋值、初始化和读取变量 待复盘
能解释 signedunsignedshortlong 的基本含义 待复盘
能用 sizeof 观察不同类型的字节大小 待复盘
能区分字面量、宏常量、const 常量和枚举常量 待复盘
能解释 \n\t\\\0 等常见转义字符 待复盘
能正确使用 %d%u%x%c%s%f%p%zu 待复盘
能说明为什么不能直接 printf(user_input) 待复盘
能用 scanf_sscanffgets 完成基础输入 待复盘
能给字符串输入设置长度边界 待复盘
能大致理解内存页有读、写、执行等访问属性 待复盘

这一篇的目标不是把语法背成表格,而是形成一种写 C 程序的基本警觉:变量要初始化,类型要匹配,输出要写对格式,输入要限制边界。