1 揭开c语言内存底裤:strlen sizeof区别(上)
是否曾被C语言中strlen
和sizeof
这两个“磨人精”搞得一头雾水?
它们一个号称能测“长度”,一个号称能测“大小”,可当你把它们用在字符串和字符数组上时,结果却常常让你大跌眼镜,甚至引发诡异的程序崩溃!
就像你和豆包AI的对话中,那两个看似无辜的字符数组:
char a8[] = "hello";
char a9[] = {'h','e','l','l','o','!'};
仅仅是初始化方式的不同,竟然让strlen
和sizeof
的结果天壤之别!a8
的表现“正常”,而a9
的strlen
结果却可能像个“无头苍蝇”,在内存中乱窜,给出“未定义行为”的警告!
这背后到底隐藏着什么惊天秘密?为什么一个字符串的“长度”和“大小”会如此扑朔迷离?难道C语言在和我们玩“文字游戏”?
不!这并非C语言的“恶意”,而是它最核心、最底层、也最容易被忽视的内存管理哲学和字符串约定!
今天,我将带你彻底撕开这层神秘面纱,从最底层为你揭秘strlen
和sizeof
的“生死对决”,以及C语言字符串和内存的终极真相!读完这篇文章,你将不再惧怕那些诡异的字符串bug,而是能自信地驾驭C语言的每一个字节!
第一回合:擂台上的“明星”——strlen
与sizeof
的表面之战
我们先来重温一下你和豆包AI对话中的那个经典案例,看看它们在擂台上是如何表现的:
#include <stdio.h>
#include <string.h> // 包含 strlen 函数
int main() {
char a8[] = "hello";
char a9[] = {'h','e','l','l','o','!'};
printf("strlen of a8 is :%d ,sizeof is :%d \n",strlen(a8),sizeof(a8));
printf("strlen of a9 is :%d ,sizeof a9 is %d \n",strlen(a9),sizeof(a9));
return 0;
}
运行结果(可能因环境而异,但a9
的strlen
通常会出乎意料):
strlen of a8 is :5 ,sizeof is :6
strlen of a9 is :6 ,sizeof a9 is 6
(注:strlen(a9)
的结果可能因内存环境而异,这里假设后续内存中紧跟一个\0
,所以输出6。但实际情况可能更大,甚至程序崩溃!)
看到没?a8
的表现符合直觉:5个字符,加上一个隐藏的\0
,所以sizeof
是6,strlen
是5。完美!
但a9
呢?sizeof
是6,没问题,因为它确实有6个字符。可strlen
为什么也是6?甚至可能更大?这里就埋下了我们今天所有“秘密”的种子!
要解开这个谜团,我们必须深入它们的“老巢”,看看它们到底是怎么工作的!
深入strlen
的“灵魂”:一个永不满足的“探险家”
你和豆包AI的对话中已经给出了strlen
的底层实现,非常棒!但我们要比那更深入,像一个真正的“内存福尔摩斯”一样,去探究它的每一个细节,以及它可能带来的“灾难”!
strlen
函数的本质,就是一个**“探险家”**。它从你给定的地址开始,一个字节一个字节地往前“探险”,直到它发现一个特殊的“路标”——空字符(\0
)。一旦发现这个“路标”,它就立刻停下来,然后告诉你它走了多远(即字符数量)。
核心思想: 遇到\0
就停止。
让我们用C语言来模拟一个“探险家”:
// my_string_utils.h
#ifndef MY_STRING_UTILS_H
#define MY_STRING_UTILS_H
#include <stddef.h> // 包含 size_t 的定义
/**
* @brief 模拟实现 C 标准库中的 strlen 函数。
* 计算以 null 结尾的字符串的长度(不包括 null 终止符)。
*
* @param str 指向要计算长度的字符串的常量指针。
* 该指针必须指向一个以 '\0' 结尾的字符序列。
* 如果 str 不以 '\0' 结尾,函数将导致未定义行为。
* @return size_t 字符串的长度。
*/
size_t my_strlen(const char *str);
/**
* @brief 打印字符串的内存布局,包括每个字符的ASCII值和十六进制表示。
* 这是一个辅助函数,用于可视化字符串在内存中的实际存储。
*
* @param str 指向字符串的常量指针。
* @param length 要打印的字节数。
*/
void print_memory_layout(const char *str, size_t length);
/**
* @brief 演示 strlen 函数在不同初始化方式下的行为。
* 包括字符串字面量初始化和字符数组初始化。
*/
void demo_strlen_behavior();
#endif // MY_STRING_UTILS_H
```c
// my_string_utils.c
#include "my_string_utils.h"
#include <stdio.h> // For printf
#include <string.h> // For original strlen, for comparison
#include <stdlib.h> // For exit, malloc, free
/**
* @brief 模拟实现 C 标准库中的 strlen 函数。
* 计算以 null 结尾的字符串的长度(不包括 null 终止符)。
*
* @param str 指向要计算长度的字符串的常量指针。
* 该指针必须指向一个以 '\0' 结尾的字符序列。
* 如果 str 不以 '\0' 结尾,函数将导致未定义行为。
* @return size_t 字符串的长度。
*/
size_t my_strlen(const char *str) {
// 声明一个与传入指针类型相同的局部指针 s
// 它将从 str 指向的地址开始,向后遍历
const char *s = str;
// 循环条件:*s != '\0'
// 这是一个非常经典的 C 语言循环模式,用于遍历字符串直到遇到 null 终止符。
// 1. *s 表示解引用指针 s,获取 s 当前指向的字符。
// 2. != '\0' 检查这个字符是否是 null 终止符。
// 3. 如果不是 null 终止符,循环继续。
// 4. 如果是 null 终止符,循环终止。
while (*s != '\0') {
// 如果当前字符不是 null 终止符,将指针 s 向后移动一个字节。
// 这意味着 s 现在指向字符串中的下一个字符。
s++;
}
// 循环结束后,指针 s 指向了字符串的 null 终止符 '\0'。
// 而指针 str 仍然指向字符串的起始位置。
// 两个指针相减(s - str)的结果是它们之间的距离,
// 这个距离就是从字符串起始位置到 null 终止符之前的字符数量。
// 例如,如果 str 指向地址 1000,s 指向地址 1005 (即 'o' 后的 '\0'),
// 那么 s - str = 5,表示字符串长度为 5。
return s - str;
}
/**
* @brief 打印字符串的内存布局,包括每个字符的ASCII值和十六进制表示。
* 这是一个辅助函数,用于可视化字符串在内存中的实际存储。
*
* @param str 指向字符串的常量指针。
* @param length 要打印的字节数。
*/
void print_memory_layout(const char *str, size_t length) {
if (!str) {
printf(" (NULL pointer)\n");
return;
}
printf(" Memory Address | Char | ASCII (Dec) | Hex\n");
printf(" ----------------------------------------------\n");
for (size_t i = 0; i < length; ++i) {
// 获取当前字节的地址
const char *current_byte_ptr = str + i;
// 获取当前字节的值
unsigned char byte_value = (unsigned char)*current_byte_ptr;
printf(" %p | ", (void*)current_byte_ptr); // 打印内存地址
// 打印字符表示:如果是可打印字符,则打印字符本身,否则打印 '.'
if (byte_value >= 32 && byte_value <= 126) {
printf(" '%c' | ", byte_value);
} else if (byte_value == '\0') {
printf(" '\\0'| "); // 特殊处理 null 终止符
} else {
printf(" '.' | "); // 其他不可打印字符
}
printf("%-11d | 0x%02X\n", byte_value, byte_value); // 打印十进制和十六进制值
}
printf(" ----------------------------------------------\n");
}
/**
* @brief 演示 strlen 函数在不同初始化方式下的行为。
* 包括字符串字面量初始化和字符数组初始化。
*/
void demo_strlen_behavior() {
printf("\n--- 演示 strlen 行为 ---\n");
// 案例 1: 使用字符串字面量初始化 (编译器自动添加 '\0')
char s1[] = "Hello"; // 内存布局: 'H', 'e', 'l', 'l', 'o', '\0'
printf("char s1[] = \"Hello\";\n");
printf(" strlen(s1): %zu\n", my_strlen(s1)); // 期望输出 5
printf(" sizeof(s1): %zu\n", sizeof(s1)); // 期望输出 6
printf(" s1 内存布局 (前 %zu 字节):\n", sizeof(s1) + 2); // 多打印几个字节,看看后面有没有垃圾数据
print_memory_layout(s1, sizeof(s1) + 2);
// 案例 2: 使用字符列表初始化 (没有自动添加 '\0')
char s2[] = {'W', 'o', 'r', 'l', 'd', '!'}; // 内存布局: 'W', 'o', 'r', 'l', 'd', '!'
printf("\nchar s2[] = {'W', 'o', 'r', 'l', 'd', '!'};\n");
printf(" sizeof(s2): %zu\n", sizeof(s2)); // 期望输出 6
printf(" strlen(s2): ");
// 这里故意不使用 my_strlen,而是直接调用,以便观察未定义行为
// 警告:以下调用可能导致程序崩溃或输出随机值
size_t len_s2 = my_strlen(s2); // ⚠️ 潜在的未定义行为!
printf("%zu (⚠️ 潜在的未定义行为,取决于内存内容)\n", len_s2);
printf(" s2 内存布局 (前 %zu 字节):\n", sizeof(s2) + 5); // 多打印几个字节,看看后面有没有垃圾数据
print_memory_layout(s2, sizeof(s2) + 5);
// 案例 3: 使用字符列表初始化,但手动添加 '\0'
char s3[] = {'C', 'o', 'd', 'e', '\0'}; // 内存布局: 'C', 'o', 'd', 'e', '\0'
printf("\nchar s3[] = {'C', 'o', 'd', 'e', '\\0'};\n");
printf(" strlen(s3): %zu\n", my_strlen(s3)); // 期望输出 4
printf(" sizeof(s3): %zu\n", sizeof(s3)); // 期望输出 5
printf(" s3 内存布局 (前 %zu 字节):\n", sizeof(s3) + 2);
print_memory_layout(s3, sizeof(s3) + 2);
// 案例 4: 字符串字面量作为函数参数传递
// 此时 s4 在函数内部退化为 char* 指针,sizeof 将返回指针的大小
void func_with_string_param(char *param_str) {
printf("\nInside func_with_string_param(char *param_str):\n");
printf(" strlen(param_str): %zu\n", my_strlen(param_str));
printf(" sizeof(param_str): %zu (这是指针的大小,不是字符串内容的大小)\n", sizeof(param_str));
printf(" param_str 内存布局 (前 %zu 字节):\n", my_strlen(param_str) + 2);
print_memory_layout(param_str, my_strlen(param_str) + 2);
}
char s4[] = "World";
printf("\nchar s4[] = \"World\";\n");
func_with_string_param(s4);
printf("\n--- strlen 行为演示结束 ---\n");
}
```c
// main.c (用于测试 my_string_utils)
#include <stdio.h>
#include "my_string_utils.h"
int main() {
printf("Hello, 欢迎来到C语言的底层世界!\n");
// 运行 strlen 行为演示
demo_strlen_behavior();
return 0;
}
编译和运行:
gcc -o my_strlen_demo main.c my_string_utils.c -Wall -Wextra
./my_strlen_demo
核心发现:
-
strlen
是个“瞎子”,它只认识\0
! 它根本不知道你数组的边界在哪里,它只会傻傻地一个字节一个字节地往后看,直到遇到那个“路标”——\0
。如果你的字符数组没有\0
,它就会像脱缰的野马,冲出数组边界,访问到不属于你的内存区域,这正是**未定义行为(Undefined Behavior)**的温床!轻则输出乱码,重则程序崩溃! -
strlen
的效率是O(n)。 它必须遍历整个字符串才能得到结果。这意味着字符串越长,它耗费的时间就越多。
深入sizeof
的“灵魂”:一个“全知全能”的“预言家”
与strlen
这个“探险家”截然不同,sizeof
是一个**“全知全能”的“预言家”。它根本不需要运行你的程序,甚至不需要看内存里的具体内容!它在编译阶段**就已经知道了所有信息,然后直接“预言”出结果。
核心思想: 编译时确定,与运行时内容无关。
sizeof
是一个运算符(operator),而不是函数。这意味着它在编译时就已经被编译器替换成了一个常量值。
让我们看看sizeof
在不同场景下的表现:
// sizeof_demo.h
#ifndef SIZEOF_DEMO_H
#define SIZEOF_DEMO_H
#include <stddef.h> // 包含 size_t 的定义
#include <stdio.h> // 包含 printf
/**
* @brief 演示 sizeof 操作符在不同类型和变量上的行为。
*/
void demo_sizeof_behavior();
#endif // SIZEOF_DEMO_H
```c
// sizeof_demo.c
#include "sizeof_demo.h"
#include <limits.h> // For CHAR_BIT
/**
* @brief 演示 sizeof 操作符在不同类型和变量上的行为。
*/
void demo_sizeof_behavior() {
printf("\n--- 演示 sizeof 行为 ---\n");
// 1. 基本数据类型
printf("1. 基本数据类型的大小:\n");
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));
printf(" sizeof(float): %zu 字节\n", sizeof(float));
printf(" sizeof(double): %zu 字节\n", sizeof(double));
printf(" sizeof(long double): %zu 字节\n", sizeof(long double));
printf(" sizeof(void*): %zu 字节 (指针大小)\n", sizeof(void*));
printf(" 系统字节位数 (CHAR_BIT): %d 位\n", CHAR_BIT); // 通常是 8
// 2. 数组
printf("\n2. 数组的大小:\n");
int intArray[10]; // 包含 10 个 int 元素的数组
printf(" int intArray[10];\n");
printf(" sizeof(intArray): %zu 字节 (整个数组的大小)\n", sizeof(intArray));
printf(" sizeof(intArray[0]): %zu 字节 (单个元素的大小)\n", sizeof(intArray[0]));
printf(" 数组元素个数: %zu\n", sizeof(intArray) / sizeof(intArray[0]));
char charArray[5] = {'a', 'b', 'c', 'd', 'e'}; // 5个字符的数组,没有 '\0'
printf(" char charArray[5] = {'a', 'b', 'c', 'd', 'e'};\n");
printf(" sizeof(charArray): %zu 字节\n", sizeof(charArray)); // 5 字节
char stringLiteralArray[] = "Hello"; // 字符串字面量初始化,自动添加 '\0'
printf(" char stringLiteralArray[] = \"Hello\";\n");
printf(" sizeof(stringLiteralArray): %zu 字节 (包含 '\\0')\n", sizeof(stringLiteralArray)); // 6 字节
// 3. 指针
printf("\n3. 指针的大小:\n");
int *ptrInt;
char *ptrChar;
double *ptrDouble;
printf(" sizeof(ptrInt): %zu 字节 (指针本身的大小)\n", sizeof(ptrInt));
printf(" sizeof(ptrChar): %zu 字节\n", sizeof(ptrChar));
printf(" sizeof(ptrDouble): %zu 字节\n", sizeof(ptrDouble));
printf(" 注意:所有指针类型的大小通常是相同的,取决于系统架构 (32位系统通常4字节,64位系统通常8字节)。\n");
// 4. 结构体 (考虑内存对齐)
printf("\n4. 结构体的大小 (考虑内存对齐):\n");
// 结构体成员的对齐规则可能导致结构体大小大于成员大小之和
// 例如,在某些系统上,int 需要 4 字节对齐
struct MyStruct1 {
char c1; // 1 byte
int i; // 4 bytes
char c2; // 1 byte
}; // 实际大小可能是 12 字节 (1 + 3(padding) + 4 + 1 + 3(padding))
struct MyStruct2 {
char c1; // 1 byte
char c2; // 1 byte
int i; // 4 bytes
}; // 实际大小可能是 8 字节 (1 + 1 + 2(padding) + 4)
printf(" struct MyStruct1 { char c1; int i; char c2; };\n");
printf(" sizeof(struct MyStruct1): %zu 字节\n", sizeof(struct MyStruct1));
printf(" struct MyStruct2 { char c1; char c2; int i; };\n");
printf(" sizeof(struct MyStruct2): %zu 字节\n", sizeof(struct MyStruct2));
printf(" 内存对齐是编译器为了提高访问效率而做的优化,可能导致结构体大小大于成员大小之和。\n");
// 5. 函数参数中的数组退化为指针
printf("\n5. 函数参数中的数组退化为指针:\n");
void func_array_param(int arr[]) { // 实际上 arr 是 int*
printf(" Inside func_array_param(int arr[]):\n");
printf(" sizeof(arr): %zu 字节 (这是指针的大小,不是数组的大小)\n", sizeof(arr));
}
int localArray[20];
printf(" int localArray[20];\n");
printf(" sizeof(localArray) in main: %zu 字节\n", sizeof(localArray));
func_array_param(localArray);
// 6. 字符串字面量本身
printf("\n6. 字符串字面量本身的大小:\n");
printf(" sizeof(\"Hello\"): %zu 字节 (包含 '\\0')\n", sizeof("Hello")); // 6 字节
printf(" sizeof(\"\"): %zu 字节 (空字符串也包含 '\\0')\n", sizeof("")); // 1 字节
printf("\n--- sizeof 行为演示结束 ---\n");
}
```c
// main.c (用于测试 sizeof_demo)
#include <stdio.h>
#include "my_string_utils.h" // 仍然包含,因为它有 print_memory_layout
#include "sizeof_demo.h"
int main() {
printf("Hello, 欢迎来到C语言的底层世界!\n");
// 运行 strlen 行为演示
demo_strlen_behavior();
// 运行 sizeof 行为演示
demo_sizeof_behavior();
return 0;
}
编译和运行:
gcc -o my_compiler_demo main.c my_string_utils.c sizeof_demo.c -Wall -Wextra
./my_compiler_demo
核心发现:
-
sizeof
是编译时常量。 无论你给它一个类型名(如int
),一个变量名(如intArray
),还是一个表达式(如10 + 20
),它都会在编译阶段被一个确定的数值替换掉。它根本不关心程序运行时变量里存了什么值。 -
sizeof
对数组和指针的处理截然不同。-
当作用于数组名时(在声明它的作用域内),
sizeof
返回整个数组占用的字节数。 -
当作用于指针变量时,
sizeof
返回指针变量本身占用的字节数(通常是4或8字节),而不是它所指向的数据的大小!这是C语言中最常见的“坑”之一! -
当数组作为函数参数传递时,它会**退化(decay)**为指针。所以在函数内部对数组参数使用
sizeof
,得到的是指针的大小,而非原始数组的大小。
-
-
sizeof
对字符串字面量会包含\0
。 比如sizeof("Hello")
会是6,因为字符串字面量在内存中是'H', 'e', 'l', 'l', 'o', '\0'
。
第二回合:幕后的“操盘手”——C语言字符串的本质与内存布局
现在,我们已经看到了strlen
和sizeof
在擂台上的表现,也初步了解了它们的“脾气”。但要彻底理解它们为什么会这样,我们必须深入C语言的字符串本质和内存布局。
C语言字符串的“灵魂契约”:空字符(\0
)的至高无上
在C语言中,并没有一个内建的“字符串”类型。C语言的“字符串”其实就是一串以**空字符(Null Terminator,\0
,ASCII值为0)**结尾的字符数组。这个\0
,就是C语言字符串的“灵魂契约”,是所有字符串处理函数的“停止信号”!
为什么是\0
?
-
简单高效: 这种约定非常简单,只需要一个特殊的字节就能标记字符串的结束。
-
兼容性: 这种约定可以很容易地与底层硬件和汇编语言交互。
-
灵活性: 字符串可以任意长,只要内存允许。
字符串字面量(String Literal):隐形的\0
守护者
当你写下"hello"
这样的字符串字面量时,编译器会默默地为你做一件事:它会在字符串的末尾自动添加一个\0
。
例如:char a8[] = "hello";
在内存中,a8
实际存储的是:'h', 'e', 'l', 'l', 'o', '\0'
。
它占用了6个字节。这就是为什么sizeof(a8)
是6,而strlen(a8)
会从'h'
开始数,数到'o'
,然后遇到\0
停止,所以是5。一切都那么“和谐”!
字符数组初始化:手动\0
的“考验”
当你使用字符列表来初始化一个字符数组时,编译器不会自动添加\0
!它会严格按照你提供的字符数量来分配内存和填充数据。
例如:char a9[] = {'h','e','l','l','o','!'};
在内存中,a9
实际存储的是:'h', 'e', 'l', 'l', 'o', '!'
。
它占用了6个字节。所以sizeof(a9)
是6,这没问题。
但当你对a9
调用strlen
时,灾难就可能发生了!strlen
会从'h'
开始数,数到'!'
,然后呢?它还会继续往后数,因为它没有遇到\0
!它会访问到a9
数组边界之外的内存,直到它“碰巧”遇到一个\0
(可能是其他变量的尾部,也可能是随机的内存垃圾),或者更糟——访问到非法内存区域,导致程序崩溃(Segmentation Fault)!这就是所谓的未定义行为!
安全建议:
-
总是使用字符串字面量初始化字符串变量,除非你有特殊需求,并且明确知道自己在做什么。
-
如果必须使用字符列表初始化,请务必手动添加
\0
:char safe_string[] = {'h', 'e', 'l', 'l', 'o', '!', '\0'}; // 显式添加 '\0'
-
永远不要对未以
\0
结尾的字符数组使用strlen
或printf("%s")
等字符串处理函数!
变量的“豪宅”:内存布局的奥秘
C语言的魅力之一,就是它对内存的直接掌控。你的每一个变量,都在内存中拥有自己的“豪宅”。理解这些“豪宅”的布局,是理解sizeof
和指针行为的关键。
程序运行时,内存通常被划分为几个区域:
-
代码段(Text Segment): 存储程序的机器指令。通常是只读的。
-
数据段(Data Segment): 存储已初始化的全局变量和静态变量。
-
BSS段(Block Started by Symbol Segment): 存储未初始化的全局变量和静态变量。在程序加载时,这些变量会被清零。
-
堆(Heap): 用于动态内存分配(
malloc
,free
)。程序员手动管理其生命周期。 -
栈(Stack): 用于存储局部变量、函数参数、函数返回地址等。遵循“先进后出”的原则,由编译器自动管理。
sizeof
在编译时,会根据变量的类型和存储位置,计算出它在内存中占用的字节数。
让我们用代码来“窥探”一下这些内存区域:
// memory_layout_demo.h
#ifndef MEMORY_LAYOUT_DEMO_H
#define MEMORY_LAYOUT_DEMO_H
#include <stdio.h>
#include <stdlib.h> // For malloc, free
// 全局变量 (已初始化)
int global_initialized_var = 100;
// 全局变量 (未初始化)
int global_uninitialized_var;
/**
* @brief 演示不同类型变量在内存中的大小和地址。
*/
void demo_variable_sizes_and_addresses();
/**
* @brief 演示字符串字面量在内存中的存储位置。
*/
void demo_string_literal_storage();
/**
* @brief 演示函数栈帧和局部变量的内存布局。
*/
void demo_stack_frame_layout();
#endif // MEMORY_LAYOUT_DEMO_H
```c
// memory_layout_demo.c
#include "memory_layout_demo.h"
// 全局变量 (已初始化)
// 存储在数据段 (Data Segment)
int global_initialized_var = 100;
// 全局变量 (未初始化)
// 存储在 BSS 段 (Block Started by Symbol Segment),程序启动时清零
int global_uninitialized_var;
// 静态变量 (已初始化)
// 存储在数据段 (Data Segment)
static int static_initialized_var = 200;
// 静态变量 (未初始化)
// 存储在 BSS 段 (Block Started by Symbol Segment)
static int static_uninitialized_var;
/**
* @brief 演示不同类型变量在内存中的大小和地址。
*/
void demo_variable_sizes_and_addresses() {
printf("\n--- 变量大小与地址演示 ---\n");
// 局部变量 (存储在栈上)
int local_int = 10;
char local_char = 'A';
float local_float = 3.14f;
int local_array[5]; // 局部数组也在栈上
printf("局部变量:\n");
printf(" local_int: 地址 %p, 大小 %zu 字节, 值 %d\n", (void*)&local_int, sizeof(local_int), local_int);
printf(" local_char: 地址 %p, 大小 %zu 字节, 值 '%c'\n", (void*)&local_char, sizeof(local_char), local_char);
printf(" local_float: 地址 %p, 大小 %zu 字节, 值 %f\n", (void*)&local_float, sizeof(local_float), local_float);
printf(" local_array: 地址 %p, 大小 %zu 字节\n", (void*)&local_array, sizeof(local_array));
printf("\n全局变量:\n");
printf(" global_initialized_var: 地址 %p, 大小 %zu 字节, 值 %d\n", (void*)&global_initialized_var, sizeof(global_initialized_var), global_initialized_var);
printf(" global_uninitialized_var: 地址 %p, 大小 %zu 字节, 值 %d\n", (void*)&global_uninitialized_var, sizeof(global_uninitialized_var), global_uninitialized_var);
printf("\n静态变量:\n");
printf(" static_initialized_var: 地址 %p, 大小 %zu 字节, 值 %d\n", (void*)&static_initialized_var, sizeof(static_initialized_var), static_initialized_var);
printf(" static_uninitialized_var: 地址 %p, 大小 %zu 字节, 值 %d\n", (void*)&static_uninitialized_var, sizeof(static_uninitialized_var), static_uninitialized_var);
// 动态分配内存 (存储在堆上)
int *heap_int = (int*)malloc(sizeof(int));
if (heap_int) {
*heap_int = 500;
printf("\n堆上分配的变量:\n");
printf(" heap_int (指针): 地址 %p, 大小 %zu 字节\n", (void*)&heap_int, sizeof(heap_int));
printf(" *heap_int (指向的值): 地址 %p, 大小 %zu 字节, 值 %d\n", (void*)heap_int, sizeof(*heap_int), *heap_int);
free(heap_int); // 释放内存
}
printf("\n--- 变量大小与地址演示结束 ---\n");
}
/**
* @brief 演示字符串字面量在内存中的存储位置。
* 字符串字面量通常存储在只读数据段 (Text Segment 或 Read-Only Data Segment)。
*/
void demo_string_literal_storage() {
printf("\n--- 字符串字面量存储演示 ---\n");
// 字符串字面量 "Hello World" 通常存储在只读数据段
// s1 是一个字符数组,内容从字面量复制而来,存储在栈上
char s1[] = "Hello World";
// s2 是一个指针,指向字符串字面量在只读数据段的地址
const char *s2 = "Hello World";
// s3 也是一个指针,指向同一个字符串字面量
const char *s3 = "Hello World";
printf("char s1[] = \"Hello World\"; (数组,内容在栈上)\n");
printf(" s1 地址: %p\n", (void*)s1);
printf(" sizeof(s1): %zu 字节\n", sizeof(s1)); // 包含 '\0'
printf("\nconst char *s2 = \"Hello World\"; (指针,指向只读数据段)\n");
printf(" s2 地址: %p\n", (void*)s2);
printf(" sizeof(s2): %zu 字节 (指针大小)\n", sizeof(s2));
printf("\nconst char *s3 = \"Hello World\"; (指针,指向只读数据段)\n");
printf(" s3 地址: %p\n", (void*)s3);
printf(" sizeof(s3): %zu 字节 (指针大小)\n", sizeof(s3));
// 比较 s2 和 s3 的地址,它们可能相同 (编译器优化)
if (s2 == s3) {
printf("\ns2 和 s3 指向相同的字符串字面量地址 (编译器优化)。\n");
} else {
printf("\ns2 和 s3 指向不同的字符串字面量地址。\n");
}
printf("\n--- 字符串字面量存储演示结束 ---\n");
}
// 辅助函数:用于演示栈帧
void function_level_1(int param1, char param2) {
int local_var_f1 = 100;
char local_char_f1 = 'X';
printf(" 进入 function_level_1:\n");
printf(" param1 地址: %p, 值: %d\n", (void*)¶m1, param1);
printf(" param2 地址: %p, 值: '%c'\n", (void*)¶m2, param2);
printf(" local_var_f1 地址: %p, 值: %d\n", (void*)&local_var_f1, local_var_f1);
printf(" local_char_f1 地址: %p, 值: '%c'\n", (void*)&local_char_f1, local_char_f1);
// 递归调用,观察栈帧变化
if (param1 > 0) {
function_level_2(param1 - 1);
}
printf(" 退出 function_level_1\n");
}
void function_level_2(int param_f2) {
int local_var_f2 = 200;
printf(" 进入 function_level_2:\n");
printf(" param_f2 地址: %p, 值: %d\n", (void*)¶m_f2, param_f2);
printf(" local_var_f2 地址: %p, 值: %d\n", (void*)&local_var_f2, local_var_f2);
printf(" 退出 function_level_2\n");
}
/**
* @brief 演示函数栈帧和局部变量的内存布局。
*/
void demo_stack_frame_layout() {
printf("\n--- 栈帧布局演示 ---\n");
int main_local_var = 50;
printf("main 函数局部变量 main_local_var 地址: %p, 值: %d\n", (void*)&main_local_var, main_local_var);
function_level_1(2, 'Y'); // 调用函数
printf("main 函数局部变量 main_local_var (调用后) 地址: %p, 值: %d\n", (void*)&main_local_var, main_local_var);
printf("\n--- 栈帧布局演示结束 ---\n");
}
```c
// main.c (用于测试 memory_layout_demo)
#include <stdio.h>
#include "my_string_utils.h"
#include "sizeof_demo.h"
#include "memory_layout_demo.h"
int main() {
printf("Hello, 欢迎来到C语言的底层世界!\n");
// 运行 strlen 行为演示
demo_strlen_behavior();
// 运行 sizeof 行为演示
demo_sizeof_behavior();
// 运行内存布局演示
demo_variable_sizes_and_addresses();
demo_string_literal_storage();
demo_stack_frame_layout();
return 0;
}
编译和运行:
gcc -o my_compiler_demo main.c my_string_utils.c sizeof_demo.c memory_layout_demo.c -Wall -Wextra
./my_compiler_demo
核心发现:
-
不同存储期的变量在不同内存区域。 全局/静态变量通常在数据段/BSS段,局部变量在栈上,动态分配的在堆上,字符串字面量在只读数据段(通常是代码段的一部分)。
-
栈是“向下生长”的。 在大多数x86系统上,栈是从高地址向低地址增长的。这意味着在函数内部声明的局部变量,其地址会依次递减。函数调用时,参数、返回地址、局部变量等会依次压入栈,形成一个“栈帧”。
-
内存对齐(Memory Alignment)。 结构体的大小往往不是其成员大小的简单相加,而是会根据成员的类型和系统架构进行对齐,以提高CPU访问效率。这会导致结构体中出现“填充字节(padding)”。
sizeof
在编译时会精确计算这些填充。
第三回合:幕后“大魔王”——编译器如何“洞察一切”?
你可能会问,sizeof
为什么能“全知全能”?它怎么知道一个数组有多大?一个结构体要占多少空间?一个变量的类型是什么?
答案就在于我们之前“手撸编译器”系列中提到的那些核心阶段!特别是语义分析阶段!
编译器在将你的C代码翻译成机器码之前,会经历一系列复杂的步骤:
-
词法分析(Lexical Analysis): 将你的源代码分解成一个个有意义的“词语”(Token),比如
int
,main
,(
,)
,x
,=
,10
,;
。 -
语法分析(Syntax Analysis): 将这些“词语”组织成一棵有层次、有结构的抽象语法树(AST)。这棵树清晰地表达了你代码的语法结构。
-
语义分析(Semantic Analysis): 这是
sizeof
“全知全能”的关键! 在这个阶段,编译器会:-
构建符号表(Symbol Table): 这是一个巨大的“字典”,记录了程序中所有标识符(变量名、函数名、类型名等)的详细信息,包括它们的类型(Type)。
-
类型检查(Type Checking): 确保所有操作都是类型安全的。比如,
int x = "hello";
这样的代码,在语义分析阶段就会被发现类型不匹配而报错。 -
计算大小和偏移量: 对于每个变量声明,编译器会根据其类型(从符号表中获取)和内存对齐规则,计算出它需要占用多少字节,以及它在内存中的相对偏移量(例如,在栈帧中的偏移量)。
sizeof
就是在这个阶段,通过查询符号表或直接计算类型信息来得到结果的!
-
所以,当你写下sizeof(myArray)
时,编译器在语义分析阶段就已经知道myArray
是一个int[10]
类型的数组,并且知道int
是4字节,所以myArray
的大小就是10 * 4 = 40
字节。这个40
这个常量值,在编译时就直接替换了sizeof(myArray)
!
让我们用一个简化的编译器视角,来理解这个过程:
// compiler_core.h
#ifndef COMPILER_CORE_H
#define COMPILER_CORE_H
#include <stddef.h> // For size_t
#include <stdbool.h> // For bool
#include <string.h> // For strcmp, strdup
#include <stdio.h> // For fprintf
// --- 1. 模拟类型系统 (简化版) ---
typedef enum {
TYPE_UNKNOWN,
TYPE_VOID,
TYPE_CHAR,
TYPE_INT,
TYPE_FLOAT,
TYPE_ARRAY, // 新增数组类型
TYPE_POINTER // 新增指针类型
} MyTypeKind;
typedef struct MyType {
MyTypeKind kind;
size_t size; // 类型占用的字节数
size_t align; // 类型的对齐要求
// 对于数组类型:
struct MyType* base_type; // 数组元素的基类型
size_t array_size; // 数组的元素数量
// 对于指针类型:
struct MyType* pointed_type; // 指针指向的类型
} MyType;
// 全局静态类型实例 (单例模式,避免重复创建)
extern MyType* my_type_void;
extern MyType* my_type_char;
extern MyType* my_type_int;
extern MyType* my_type_float;
extern MyType* my_type_pointer_to_char; // 示例:char*
// 初始化基本类型
void init_my_types();
// 释放动态创建的类型 (例如数组类型)
void free_my_types();
// 创建数组类型
MyType* create_my_array_type(MyType* base_type, size_t array_size);
// 创建指针类型
MyType* create_my_pointer_type(MyType* pointed_type);
// 获取类型大小 (模拟 sizeof 行为)
size_t get_type_size(MyType* type);
// 获取类型对齐要求
size_t get_type_alignment(MyType* type);
// 将 MyTypeKind 转换为字符串 (用于打印)
const char* my_type_kind_to_string(MyTypeKind kind);
// 打印 MyType 详细信息
void print_my_type_info(MyType* type);
// --- 2. 模拟符号表 (简化版) ---
typedef enum {
SYM_VAR,
SYM_FUNC
} MySymbolKind;
typedef struct MySymbol {
char* name;
MySymbolKind kind;
MyType* type; // 符号的类型
size_t offset; // 在内存中的偏移量 (简化为相对栈帧或数据段起始)
bool is_global;
} MySymbol;
// 简单的哈希表桶
#define SYMBOL_TABLE_BUCKET_SIZE 127
typedef struct MySymbolBucket {
MySymbol* symbol;
struct MySymbolBucket* next;
} MySymbolBucket;
// 作用域
typedef struct MyScope {
MySymbolBucket* buckets[SYMBOL_TABLE_BUCKET_SIZE];
struct MyScope* parent; // 父作用域
size_t next_local_offset; // 下一个局部变量的可用偏移量
} MyScope;
// 符号表管理器
typedef struct MySymbolTable {
MyScope* current_scope;
size_t global_data_size; // 全局数据段的总大小
} MySymbolTable;
// 符号表函数
MySymbolTable* init_my_symbol_table();
void free_my_symbol_table(MySymbolTable* sym_table);
void enter_my_scope(MySymbolTable* sym_table);
void exit_my_scope(MySymbolTable* sym_table);
MySymbol* add_my_symbol(MySymbolTable* sym_table, const char* name, MySymbolKind kind, MyType* type, bool is_global);
MySymbol* lookup_my_symbol(MySymbolTable* sym_table, const char* name);
MySymbol* lookup_my_symbol_in_current_scope(MySymbolTable* sym_table, const char* name);
// --- 3. 模拟 AST 节点 (极度简化,只关注变量声明和字面量) ---
typedef enum {
AST_PROGRAM_SIMPLIFIED,
AST_VAR_DECL_SIMPLIFIED,
AST_IDENTIFIER_SIMPLIFIED,
AST_STRING_LITERAL_SIMPLIFIED,
AST_INTEGER_LITERAL_SIMPLIFIED,
AST_TYPE_SPECIFIER_SIMPLIFIED // 例如 int, char 等
} MyASTNodeType;
typedef struct MyASTNode {
MyASTNodeType type;
// 关联的类型信息 (语义分析后填充)
MyType* resolved_type;
// 关联的符号表条目 (对于标识符和变量声明)
MySymbol* symbol_entry;
union {
struct { char* name; } identifier;
struct { char* value; } string_literal;
struct { long long value; } integer_literal;
struct { MyTypeKind kind; } type_specifier; // 原始类型指示
struct {
struct MyASTNode* type_node;
struct MyASTNode* identifier_node;
struct MyASTNode* initializer_node; // 可选
} var_decl;
struct {
struct MyASTNode** declarations;
int num_declarations;
} program;
} data;
} MyASTNode;
// AST 节点创建函数 (简化版)
MyASTNode* my_ast_new_node(MyASTNodeType type);
MyASTNode* my_ast_new_var_decl(MyASTNode* type_node, MyASTNode* identifier_node, MyASTNode* initializer_node);
MyASTNode* my_ast_new_identifier(const char* name);
MyASTNode* my_ast_new_string_literal(const char* value);
MyASTNode* my_ast_new_integer_literal(long long value);
MyASTNode* my_ast_new_type_specifier(MyTypeKind kind);
MyASTNode* my_ast_new_program(MyASTNode** declarations, int num_declarations);
void free_my_ast(MyASTNode* node);
// --- 4. 模拟语义分析器 (极度简化,只处理变量声明和类型解析) ---
typedef struct {
MySymbolTable* sym_table;
int error_count;
} MySemanticAnalyzer;
MySemanticAnalyzer* init_my_semantic_analyzer(MySymbolTable* sym_table);
void free_my_semantic_analyzer(MySemanticAnalyzer* analyzer);
void my_semantic_error(MySemanticAnalyzer* analyzer, const char* format, ...);
// 核心分析函数
void analyze_my_program(MyASTNode* program_node, MySemanticAnalyzer* analyzer);
void analyze_my_var_decl(MyASTNode* var_decl_node, MySemanticAnalyzer* analyzer, bool is_global);
MyType* resolve_my_type_specifier(MyASTNode* type_node);
MyType* analyze_my_expression_type(MyASTNode* expr_node, MySemanticAnalyzer* analyzer);
#endif // COMPILER_CORE_H
```c
// compiler_core.c
#include "compiler_core.h"
#include <stdarg.h> // For va_list
#include <stdlib.h> // For malloc, free, exit
#include <string.h> // For strdup, strcmp
// --- 1. 模拟类型系统实现 ---
MyType _my_type_void = {TYPE_VOID, 0, 1, NULL, 0, NULL}; // void 类型大小为0,对齐1
MyType _my_type_char = {TYPE_CHAR, 1, 1, NULL, 0, NULL}; // char 类型大小1,对齐1
MyType _my_type_int = {TYPE_INT, 4, 4, NULL, 0, NULL}; // int 类型大小4,对齐4
MyType _my_type_float = {TYPE_FLOAT, 4, 4, NULL, 0, NULL}; // float 类型大小4,对齐4
// 模拟指针大小 (这里假设是 8 字节,64位系统)
MyType _my_type_pointer_to_char = {TYPE_POINTER, 8, 8, NULL, 0, &_my_type_char};
MyType* my_type_void = &_my_type_void;
MyType* my_type_char = &_my_type_char;
MyType* my_type_int = &_my_type_int;
MyType* my_type_float = &_my_type_float;
MyType* my_type_pointer_to_char = &_my_type_pointer_to_char;
// 动态分配的类型列表,用于统一释放
static MyType** dynamic_types = NULL;
static int num_dynamic_types = 0;
static int dynamic_types_capacity = 0;
void add_dynamic_type(MyType* type) {
if (num_dynamic_types >= dynamic_types_capacity) {
dynamic_types_capacity = (dynamic_types_capacity == 0) ? 4 : dynamic_types_capacity * 2;
dynamic_types = (MyType**)realloc(dynamic_types, dynamic_types_capacity * sizeof(MyType*));
if (!dynamic_types) {
fprintf(stderr, "错误: 内存重新分配失败 (dynamic_types)\n");
exit(EXIT_FAILURE);
}
}
dynamic_types[num_dynamic_types++] = type;
}
void init_my_types() {
fprintf(stderr, "模拟类型系统初始化完成。\n");
}
void free_my_types() {
for (int i = 0; i < num_dynamic_types; ++i) {
if (dynamic_types[i]) {
// 如果是数组类型,可能其 base_type 是静态的,不需要释放
// 如果是指针类型,其 pointed_type 也是静态的
free(dynamic_types[i]);
}
}
if (dynamic_types) {
free(dynamic_types);
dynamic_types = NULL;
}
num_dynamic_types = 0;
dynamic_types_capacity = 0;
fprintf(stderr, "模拟类型系统资源已清理。\n");
}
// 创建数组类型
MyType* create_my_array_type(MyType* base_type, size_t array_size) {
MyType* arr_type = (MyType*)malloc(sizeof(MyType));
if (!arr_type) {
fprintf(stderr, "错误: 内存分配失败 (数组类型)\n");
exit(EXIT_FAILURE);
}
arr_type->kind = TYPE_ARRAY;
arr_type->base_type = base_type;
arr_type->array_size = array_size;
// 数组的总大小 = 元素个数 * 元素大小
arr_type->size = base_type->size * array_size;
// 数组的对齐要求通常与基类型相同
arr_type->align = base_type->align;
arr_type->pointed_type = NULL; // 非指针类型
add_dynamic_type(arr_type);
return arr_type;
}
// 创建指针类型
MyType* create_my_pointer_type(MyType* pointed_type) {
MyType* ptr_type = (MyType*)malloc(sizeof(MyType));
if (!ptr_type) {
fprintf(stderr, "错误: 内存分配失败 (指针类型)\n");
exit(EXIT_FAILURE);
}
ptr_type->kind = TYPE_POINTER;
ptr_type->pointed_type = pointed_type;
// 指针的大小在编译时确定,通常是4或8字节,与系统架构有关
ptr_type->size = sizeof(void*); // 直接使用 void* 的大小
ptr_type->align = sizeof(void*); // 对齐要求通常与大小相同
ptr_type->base_type = NULL;
ptr_type->array_size = 0;
add_dynamic_type(ptr_type);
return ptr_type;
}
// 获取类型大小 (模拟 sizeof 行为)
size_t get_type_size(MyType* type) {
if (!type) return 0;
return type->size;
}
// 获取类型对齐要求
size_t get_type_alignment(MyType* type) {
if (!type) return 0;
return type->align;
}
// 将 MyTypeKind 转换为字符串 (用于打印)
const char* my_type_kind_to_string(MyTypeKind kind) {
switch (kind) {
case TYPE_UNKNOWN: return "UNKNOWN";
case TYPE_VOID: return "VOID";
case TYPE_CHAR: return "CHAR";
case TYPE_INT: return "INT";
case TYPE_FLOAT: return "FLOAT";
case TYPE_ARRAY: return "ARRAY";
case TYPE_POINTER: return "POINTER";
default: return "UNKNOWN_TYPE_KIND";
}
}
// 打印 MyType 详细信息
void print_my_type_info(MyType* type) {
if (!type) {
printf("NULL Type\n");
return;
}
printf("Kind: %s, Size: %zu, Align: %zu",
my_type_kind_to_string(type->kind), type->size, type->align);
if (type->kind == TYPE_ARRAY) {
printf(", Base Type: %s, Array Size: %zu",
my_type_kind_to_string(type->base_type->kind), type->array_size);
} else if (type->kind == TYPE_POINTER) {
printf(", Pointed Type: %s",
my_type_kind_to_string(type->pointed_type->kind));
}
printf("\n");
}
// --- 2. 模拟符号表实现 ---
// 简单的哈希函数 (DJB2)
static unsigned int my_hash_string(const char* str) {
unsigned int hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c; // hash * 33 + c
}
return hash % SYMBOL_TABLE_BUCKET_SIZE;
}
// 创建新符号
static MySymbol* create_my_symbol(const char* name, MySymbolKind kind, MyType* type, bool is_global) {
MySymbol* sym = (MySymbol*)malloc(sizeof(MySymbol));
if (!sym) {
fprintf(stderr, "错误: 内存分配失败 (MySymbol)\n");
exit(EXIT_FAILURE);
}
sym->name = strdup(name);
if (!sym->name) {
fprintf(stderr, "错误: 内存分配失败 (MySymbol name)\n");
free(sym);
exit(EXIT_FAILURE);
}
sym->kind = kind;
sym->type = type;
sym->offset = 0; // 初始偏移量
sym->is_global = is_global;
return sym;
}
// 释放符号
static void free_my_symbol(MySymbol* sym) {
if (sym) {
if (sym->name) free(sym->name);
free(sym);
}
}
// 释放桶链表
static void free_my_bucket_list(MySymbolBucket* head) {
MySymbolBucket* current = head;
while (current) {
MySymbolBucket* next = current->next;
free_my_symbol(current->symbol);
free(current);
current = next;
}
}
// 创建作用域
static MyScope* create_my_scope(MyScope* parent) {
MyScope* scope = (MyScope*)malloc(sizeof(MyScope));
if (!scope) {
fprintf(stderr, "错误: 内存分配失败 (MyScope)\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < SYMBOL_TABLE_BUCKET_SIZE; ++i) {
scope->buckets[i] = NULL;
}
scope->parent = parent;
scope->next_local_offset = 0;
return scope;
}
// 释放作用域
static void free_my_scope(MyScope* scope) {
if (scope) {
for (int i = 0; i < SYMBOL_TABLE_BUCKET_SIZE; ++i) {
free_my_bucket_list(scope->buckets[i]);
}
free(scope);
}
}
MySymbolTable* init_my_symbol_table() {
MySymbolTable* sym_table = (MySymbolTable*)malloc(sizeof(MySymbolTable));
if (!sym_table) {
fprintf(stderr, "错误: 内存分配失败 (MySymbolTable)\n");
exit(EXIT_FAILURE);
}
sym_table->current_scope = NULL;
sym_table->global_data_size = 0;
enter_my_scope(sym_table); // 进入全局作用域
fprintf(stderr, "模拟符号表初始化完成,进入全局作用域。\n");
return sym_table;
}
void free_my_symbol_table(MySymbolTable* sym_table) {
if (sym_table) {
while (sym_table->current_scope) {
exit_my_scope(sym_table);
}
free(sym_table);
fprintf(stderr, "模拟符号表资源已清理。\n");
}
}
void enter_my_scope(MySymbolTable* sym_table) {
MyScope* new_scope = create_my_scope(sym_table->current_scope);
sym_table->current_scope = new_scope;
fprintf(stderr, " 进入新作用域。\n");
}
void exit_my_scope(MySymbolTable* sym_table) {
if (!sym_table->current_scope) return;
MyScope* old_scope = sym_table->current_scope;
sym_table->current_scope = old_scope->parent;
free_my_scope(old_scope);
fprintf(stderr, " 退出作用域。\n");
}
MySymbol* add_my_symbol(MySymbolTable* sym_table, const char* name, MySymbolKind kind, MyType* type, bool is_global) {
if (lookup_my_symbol_in_current_scope(sym_table, name)) {
return NULL; // 符号已存在
}
MySymbol* new_sym = create_my_symbol(name, kind, type, is_global);
// 分配偏移量 (简化,不考虑对齐)
if (is_global) {
new_sym->offset = sym_table->global_data_size;
sym_table->global_data_size += type->size;
} else {
// 局部变量在栈上的偏移量,通常是负的,这里简化为正的累积偏移
new_sym->offset = sym_table->current_scope->next_local_offset;
// 考虑对齐,将下一个偏移量对齐到类型要求
size_t aligned_offset = (sym_table->current_scope->next_local_offset + type->align - 1) & ~(type->align - 1);
new_sym->offset = aligned_offset;
sym_table->current_scope->next_local_offset = aligned_offset + type->size;
}
unsigned int hash_val = my_hash_string(name);
MySymbolBucket* new_bucket = (MySymbolBucket*)malloc(sizeof(MySymbolBucket));
if (!new_bucket) {
fprintf(stderr, "错误: 内存分配失败 (MySymbolBucket)\n");
free_my_symbol(new_sym);
exit(EXIT_FAILURE);
}
new_bucket->symbol = new_sym;
new_bucket->next = sym_table->current_scope->buckets[hash_val];
sym_table->current_scope->buckets[hash_val] = new_bucket;
fprintf(stderr, " 添加符号: '%s' (类型: %s, 大小: %zu, 偏移: %zu, 全局: %d)\n",
name, my_type_kind_to_string(type->kind), type->size, new_sym->offset, is_global);
return new_sym;
}
MySymbol* lookup_my_symbol(MySymbolTable* sym_table, const char* name) {
MyScope* current = sym_table->current_scope;
while (current) {
unsigned int hash_val = my_hash_string(name);
MySymbolBucket* bucket = current->buckets[hash_val];
while (bucket) {
if (strcmp(bucket->symbol->name, name) == 0) {
return bucket->symbol;
}
bucket = bucket->next;
}
current = current->parent;
}
return NULL;
}
MySymbol* lookup_my_symbol_in_current_scope(MySymbolTable* sym_table, const char* name) {
if (!sym_table->current_scope) return NULL;
unsigned int hash_val = my_hash_string(name);
MySymbolBucket* bucket = sym_table->current_scope->buckets[hash_val];
while (bucket) {
if (strcmp(bucket->symbol->name, name) == 0) {
return bucket->symbol;
}
bucket = bucket->next;
}
return NULL;
}
// --- 3. 模拟 AST 节点实现 ---
MyASTNode* my_ast_new_node(MyASTNodeType type) {
MyASTNode* node = (MyASTNode*)malloc(sizeof(MyASTNode));
if (!node) {
fprintf(stderr, "错误: 内存分配失败 (MyASTNode)\n");
exit(EXIT_FAILURE);
}
node->type = type;
node->resolved_type = NULL;
node->symbol_entry = NULL;
memset(&node->data, 0, sizeof(node->data));
return node;
}
MyASTNode* my_ast_new_var_decl(MyASTNode* type_node, MyASTNode* identifier_node, MyASTNode* initializer_node) {
MyASTNode* node = my_ast_new_node(AST_VAR_DECL_SIMPLIFIED);
node->data.var_decl.type_node = type_node;
node->data.var_decl.identifier_node = identifier_node;
node->data.var_decl.initializer_node = initializer_node;
return node;
}
MyASTNode* my_ast_new_identifier(const char* name) {
MyASTNode* node = my_ast_new_node(AST_IDENTIFIER_SIMPLIFIED);
node->data.identifier.name = strdup(name);
if (!node->data.identifier.name) {
fprintf(stderr, "错误: 内存分配失败 (Identifier name)\n");
exit(EXIT_FAILURE);
}
return node;
}
MyASTNode* my_ast_new_string_literal(const char* value) {
MyASTNode* node = my_ast_new_node(AST_STRING_LITERAL_SIMPLIFIED);
node->data.string_literal.value = strdup(value);
if (!node->data.string_literal.value) {
fprintf(stderr, "错误: 内存分配失败 (String literal value)\n");
exit(EXIT_FAILURE);
}
return node;
}
MyASTNode* my_ast_new_integer_literal(long long value) {
MyASTNode* node = my_ast_new_node(AST_INTEGER_LITERAL_SIMPLIFIED);
node->data.integer_literal.value = value;
return node;
}
MyASTNode* my_ast_new_type_specifier(MyTypeKind kind) {
MyASTNode* node = my_ast_new_node(AST_TYPE_SPECIFIER_SIMPLIFIED);
node->data.type_specifier.kind = kind;
return node;
}
MyASTNode* my_ast_new_program(MyASTNode** declarations, int num_declarations) {
MyASTNode* node = my_ast_new_node(AST_PROGRAM_SIMPLIFIED);
node->data.program.declarations = declarations;
node->data.program.num_declarations = num_declarations;
return node;
}
void free_my_ast(MyASTNode* node) {
if (!node) return;
switch (node->type) {
case AST_PROGRAM_SIMPLIFIED:
for (int i = 0; i < node->data.program.num_declarations; ++i) {
free_my_ast(node->data.program.declarations[i]);
}
if (node->data.program.declarations) free(node->data.program.declarations);
break;
case AST_VAR_DECL_SIMPLIFIED:
free_my_ast(node->data.var_decl.type_node);
free_my_ast(node->data.var_decl.identifier_node);
if (node->data.var_decl.initializer_node) {
free_my_ast(node->data.var_decl.initializer_node);
}
break;
case AST_IDENTIFIER_SIMPLIFIED:
if (node->data.identifier.name) free(node->data.identifier.name);
break;
case AST_STRING_LITERAL_SIMPLIFIED:
if (node->data.string_literal.value) free(node->data.string_literal.value);
break;
case AST_INTEGER_LITERAL_SIMPLIFIED:
case AST_TYPE_SPECIFIER_SIMPLIFIED:
// 这些没有动态分配的子节点或字符串
break;
}
free(node);
}
// --- 4. 模拟语义分析器实现 ---
MySemanticAnalyzer* init_my_semantic_analyzer(MySymbolTable* sym_table) {
MySemanticAnalyzer* analyzer = (MySemanticAnalyzer*)malloc(sizeof(MySemanticAnalyzer));
if (!analyzer) {
fprintf(stderr, "错误: 内存分配失败 (MySemanticAnalyzer)\n");
exit(EXIT_FAILURE);
}
analyzer->sym_table = sym_table;
analyzer->error_count = 0;
fprintf(stderr, "模拟语义分析器初始化成功。\n");
return analyzer;
}
void free_my_semantic_analyzer(MySemanticAnalyzer* analyzer) {
if (analyzer) {
// sym_table 在外部管理,不在这里释放
free(analyzer);
fprintf(stderr, "模拟语义分析器资源已清理。\n");
}
}
void my_semantic_error(MySemanticAnalyzer* analyzer, const char* format, ...) {
analyzer->error_count++;
va_list args;
va_start(args, format);
fprintf(stderr, "语义错误: ");
vfprintf(stderr, format, args);
fprintf(stderr, "\n");
va_end(args);
}
// 解析类型说明符 (例如 int, char)
MyType* resolve_my_type_specifier(MyASTNode* type_node) {
if (!type_node || type_node->type != AST_TYPE_SPECIFIER_SIMPLIFIED) {
return my_type_unknown; // 错误或未知类型
}
switch (type_node->data.type_specifier.kind) {
case TYPE_INT: return my_type_int;
case TYPE_CHAR: return my_type_char;
case TYPE_FLOAT: return my_type_float;
case TYPE_VOID: return my_type_void;
default: return my_type_unknown;
}
}
// 模拟分析表达式类型 (这里只处理字面量和标识符)
MyType* analyze_my_expression_type(MyASTNode* expr_node, MySemanticAnalyzer* analyzer) {
if (!expr_node) return my_type_unknown;
switch (expr_node->type) {
case AST_INTEGER_LITERAL_SIMPLIFIED:
expr_node->resolved_type = my_type_int;
return my_type_int;
case AST_STRING_LITERAL_SIMPLIFIED:
// 字符串字面量是 char 数组,这里简化为 char*
// 实际编译器会创建 char[N] 类型,N包含 '\0'
expr_node->resolved_type = create_my_pointer_type(my_type_char);
return expr_node->resolved_type;
case AST_IDENTIFIER_SIMPLIFIED: {
MySymbol* sym = lookup_my_symbol(analyzer->sym_table, expr_node->data.identifier.name);
if (!sym) {
my_semantic_error(analyzer, "未声明的标识符 '%s'。", expr_node->data.identifier.name);
expr_node->resolved_type = my_type_unknown;
return my_type_unknown;
}
expr_node->symbol_entry = sym; // 关联符号表条目
expr_node->resolved_type = sym->type; // 表达式的类型就是符号的类型
return sym->type;
}
default:
my_semantic_error(analyzer, "未知或不支持的表达式类型进行语义分析。");
return my_type_unknown;
}
}
// 分析变量声明
void analyze_my_var_decl(MyASTNode* var_decl_node, MySemanticAnalyzer* analyzer, bool is_global) {
MyType* var_type = resolve_my_type_specifier(var_decl_node->data.var_decl.type_node);
if (var_type == my_type_unknown) {
my_semantic_error(analyzer, "无法解析变量 '%s' 的类型。", var_decl_node->data.var_decl.identifier_node->data.identifier.name);
return;
}
// 字符串字面量初始化数组的特殊处理
if (var_decl_node->data.var_decl.initializer_node &&
var_decl_node->data.var_decl.initializer_node->type == AST_STRING_LITERAL_SIMPLIFIED &&
var_type->kind == TYPE_CHAR) { // char a[] = "hello";
// 自动推断为 char 数组类型,大小为字符串长度 + 1 (for '\0')
size_t string_len = strlen(var_decl_node->data.var_decl.initializer_node->data.string_literal.value);
var_type = create_my_array_type(my_type_char, string_len + 1);
fprintf(stderr, " 推断为字符串数组类型: char[%zu]\n", string_len + 1);
} else if (var_decl_node->data.var_decl.initializer_node &&
var_decl_node->data.var_decl.initializer_node->type == AST_STRING_LITERAL_SIMPLIFIED &&
var_type->kind == TYPE_POINTER && var_type->pointed_type->kind == TYPE_CHAR) { // char* p = "hello";
// 这是一个 char* 指针,指向字符串字面量
// 类型已经是 char*,不需要额外处理
} else if (var_type->kind == TYPE_VOID) {
my_semantic_error(analyzer, "不能声明类型为 'void' 的变量 '%s'。", var_decl_node->data.var_decl.identifier_node->data.identifier.name);
return;
}
char* var_name = var_decl_node->data.var_decl.identifier_node->data.identifier.name;
MySymbol* sym = add_my_symbol(analyzer->sym_table, var_name, SYM_VAR, var_type, is_global);
if (!sym) {
my_semantic_error(analyzer, "变量 '%s' 重复定义。", var_name);
return;
}
var_decl_node->symbol_entry = sym; // 关联到声明节点
var_decl_node->data.var_decl.identifier_node->symbol_entry = sym; // 关联到标识符节点
var_decl_node->resolved_type = var_type; // 声明节点本身也有类型
// 分析初始化表达式的类型 (这里简化为只获取类型,不检查兼容性)
if (var_decl_node->data.var_decl.initializer_node) {
analyze_my_expression_type(var_decl_node->data.var_decl.initializer_node, analyzer);
}
}
// 分析整个程序 (简化版,只处理顶层变量声明)
void analyze_my_program(MyASTNode* program_node, MySemanticAnalyzer* analyzer) {
if (program_node->type != AST_PROGRAM_SIMPLIFIED) {
my_semantic_error(analyzer, "AST根节点不是程序类型。");
return;
}
fprintf(stderr, "开始模拟语义分析...\n");
for (int i = 0; i < program_node->data.program.num_declarations; ++i) {
MyASTNode* decl_node = program_node->data.program.declarations[i];
if (decl_node->type == AST_VAR_DECL_SIMPLIFIED) {
analyze_my_var_decl(decl_node, analyzer, true); // 假设都是全局变量
} else {
my_semantic_error(analyzer, "顶层只能是变量声明 (简化模式)。");
}
}
fprintf(stderr, "模拟语义分析完成,发现 %d 个错误。\n", analyzer->error_count);
}
```c
// main.c (用于测试 compiler_core)
#include <stdio.h>
#include "my_string_utils.h"
#include "sizeof_demo.h"
#include "memory_layout_demo.h"
#include "compiler_core.h" // 包含模拟编译器核心
int main() {
printf("Hello, 欢迎来到C语言的底层世界!\n");
// 运行 strlen 行为演示
demo_strlen_behavior();
// 运行 sizeof 行为演示
demo_sizeof_behavior();
// 运行内存布局演示
demo_variable_sizes_and_addresses();
demo_string_literal_storage();
demo_stack_frame_layout();
printf("\n--- 模拟编译器对 sizeof 的处理 ---\n");
// 1. 初始化模拟编译器组件
init_my_types(); // 初始化类型系统
MySymbolTable* sym_table = init_my_symbol_table(); // 初始化符号表
MySemanticAnalyzer* analyzer = init_my_semantic_analyzer(sym_table); // 初始化语义分析器
// 2. 构建一个简化的AST来模拟你的代码
// char a8[] = "hello";
MyASTNode* a8_type = my_ast_new_type_specifier(TYPE_CHAR);
MyASTNode* a8_identifier = my_ast_new_identifier("a8");
MyASTNode* a8_initializer = my_ast_new_string_literal("hello");
MyASTNode* a8_decl = my_ast_new_var_decl(a8_type, a8_identifier, a8_initializer);
// char a9[] = {'h','e','l','l','o','!'};
// 这种初始化方式,编译器不会自动添加 '\0'
MyASTNode* a9_type = my_ast_new_type_specifier(TYPE_CHAR);
MyASTNode* a9_identifier = my_ast_new_identifier("a9");
// 这里我们无法直接表示字符列表,只能模拟一个已知大小的 char 数组
// 假设在语法分析阶段我们已经知道它是 char[6]
MyType* a9_explicit_type = create_my_array_type(my_type_char, 6); // 明确指定大小
MyASTNode* a9_decl = my_ast_new_var_decl(a9_type, a9_identifier, NULL); // 没有初始化表达式,或者复杂初始化
// 手动设置 a9_decl 的 resolved_type,模拟编译器推断
// 实际上,编译器会根据 {'h','e','l','l','o','!'} 列表的长度推断出 char[6]
a9_decl->resolved_type = a9_explicit_type;
a9_decl->data.var_decl.identifier_node->resolved_type = a9_explicit_type;
// char* p = "world";
MyASTNode* p_type = my_ast_new_type_specifier(TYPE_CHAR); // 原始是 char
MyASTNode* p_identifier = my_ast_new_identifier("p");
MyASTNode* p_initializer = my_ast_new_string_literal("world");
MyASTNode* p_decl = my_ast_new_var_decl(p_type, p_identifier, p_initializer);
// 对于 char* p = "world",p_type 实际上应该是 char*
// 模拟语法分析器识别出 char* 类型
p_decl->resolved_type = create_my_pointer_type(my_type_char);
p_decl->data.var_decl.identifier_node->resolved_type = p_decl->resolved_type;
MyASTNode* program_declarations[] = {a8_decl, a9_decl, p_decl};
MyASTNode* program_ast = my_ast_new_program(program_declarations, 3);
// 3. 执行模拟语义分析
analyze_my_program(program_ast, analyzer);
printf("\n--- 语义分析结果 (模拟 sizeof 行为) ---\n");
if (analyzer->error_count == 0) {
printf("模拟语义分析成功,变量信息已存储在符号表中。\n");
MySymbol* sym_a8 = lookup_my_symbol(sym_table, "a8");
if (sym_a8) {
printf("变量 'a8': ");
print_my_type_info(sym_a8->type);
printf(" 模拟 sizeof(a8) 结果: %zu (从符号表获取类型大小)\n", get_type_size(sym_a8->type));
}
MySymbol* sym_a9 = lookup_my_symbol(sym_table, "a9");
if (sym_a9) {
printf("变量 'a9': ");
print_my_type_info(sym_a9->type);
printf(" 模拟 sizeof(a9) 结果: %zu (从符号表获取类型大小)\n", get_type_size(sym_a9->type));
}
MySymbol* sym_p = lookup_my_symbol(sym_table, "p");
if (sym_p) {
printf("变量 'p': ");
print_my_type_info(sym_p->type);
printf(" 模拟 sizeof(p) 结果: %zu (从符号表获取类型大小)\n", get_type_size(sym_p->type));
}
// 模拟 sizeof("hello")
printf("\n模拟 sizeof(\"hello\") 结果: %zu (编译器直接知道字符串字面量大小)\n", strlen("hello") + 1);
} else {
printf("模拟语义分析发现错误,无法继续。\n");
}
// 4. 释放模拟编译器资源
free_my_ast(program_ast);
free_my_semantic_analyzer(analyzer);
free_my_symbol_table(sym_table);
free_my_types(); // 最后释放类型系统
printf("\n--- 模拟编译器演示结束 ---\n");
return 0;
}
编译和运行:
gcc -o my_compiler_demo main.c my_string_utils.c sizeof_demo.c memory_layout_demo.c compiler_core.c -Wall -Wextra
./my_compiler_demo
核心发现:
-
类型系统是基础。 编译器内部维护着一套类型系统,它知道每种基本类型(
int
,char
,float
)的大小和对齐要求。 -
数组和指针也是“类型”。 编译器能够识别
char[6]
是一个数组类型,其大小是6 * sizeof(char)
。它也能识别char*
是一个指针类型,其大小是sizeof(void*)
。 -
符号表是“字典”。 在语义分析阶段,每当你声明一个变量,编译器就会在符号表中为它创建一个条目,记录它的名称和类型。
-
sizeof
就是“查字典”。 当编译器遇到sizeof(variable)
或sizeof(type_name)
时,它会去类型系统或符号表中“查字典”,直接获取这个类型或变量所占用的字节数,然后将sizeof
表达式替换为一个编译时常量。这就是它“全知全能”的秘密!
总结(上):\0
是灵魂,sizeof
是编译时“预言”
至此,我们已经深入剖析了strlen
和sizeof
的本质差异:
-
strlen
: 是一个运行时函数,它是一个“探险家”,必须遍历字符串直到遇到**空字符\0
**才能确定长度。如果字符串没有\0
,它就会引发未定义行为。 -
sizeof
: 是一个编译时运算符,它是一个“预言家”,在编译阶段就通过编译器内部的类型系统和符号表,精确计算出变量或类型在内存中占用的字节数。它不关心内存中的内容,只关心“类型”本身。
而这一切的根源,在于C语言对字符串的特殊约定——以\0
作为结束符。这个小小的\0
,决定了strlen
的行为,也决定了字符串字面量初始化数组时的sizeof
结果。
你和豆包AI的对话,揭示的正是C语言中一个最常见、最核心的误区。理解了\0
的重要性,理解了strlen
和sizeof
的工作机制,你才能真正掌握C语言的字符串和内存管理,避免那些让人头疼的bug!
这仅仅是冰山一角!在下一部分中,我们将继续深入,探讨更多关于C语言字符串和内存的“高级话题”:比如动态字符串、字符串拷贝的陷阱、以及如何用更“C”的方式安全地操作字符串。
准备好了吗?继续保持这份对底层的渴望,我们下一部分不见不散!
敬请期待下篇:
《C语言的“隐秘角落”:strlen
与sizeof
的生死对决,揭秘你从未深究的内存真相(下)》
在下篇中,我们将深入探讨:
-
字符串操作的“雷区”:
strcpy
,strcat
等函数的安全隐患与替代方案。 -
动态字符串的“魔法”: 如何在堆上灵活管理字符串,以及
malloc
和free
的艺术。 -
字符数组与指针的“变脸”: 数组名何时退化为指针,以及如何避免常见的指针错误。
-
内存越界与缓冲区溢出: 那些导致程序崩溃的“幕后黑手”及其防范。
-
C语言的“内存哲学”: 为什么C语言要如此直接地暴露内存,以及这带来的力量与危险。
------------------------------------------------------------------------------------更新于2025.6.24日 下午5点30
2 揭开c语言内存底裤:strlen sizeof区别(下)
在上一篇中,我们彻底解密了strlen
和sizeof
这对“欢喜冤家”的本质差异,并深入探讨了C语言字符串的“灵魂契约”——空字符\0
。你已经知道,strlen
是一个在运行时寻找\0
的“探险家”,而sizeof
则是一个在编译时“预言”大小的“全知全能者”。
但,这只是C语言内存与字符串世界的入门券!真正的挑战,那些足以让你的程序瞬间“暴毙”的陷阱,还在后面等着你!
今天,我们将继续深入,直面C语言字符串操作的“雷区”,揭示动态内存管理的“魔法”,剖析数组与指针的“变脸”玄机,并最终触及C语言最核心的“内存哲学”!
准备好了吗?让我们一起,把这些C语言的“隐秘角落”,彻底曝光在阳光之下!
第四回合:字符串操作的“雷区”——那些让你程序“暴毙”的陷阱
C语言标准库提供了一系列字符串处理函数,如strcpy
、strcat
、sprintf
等。它们看似方便,实则暗藏杀机!如果你不了解它们的工作原理和潜在风险,它们随时可能成为你程序的“定时炸弹”!
陷阱一:strcpy
与strcat
——无边界的“复制”与“连接”
strcpy(dest, src)
:将src
指向的字符串(包括\0
)复制到dest
指向的内存区域。 strcat(dest, src)
:将src
指向的字符串(包括\0
)连接到dest
指向的字符串的末尾。
致命缺陷: 这两个函数都不检查目标缓冲区的大小!它们会盲目地复制或连接,直到遇到源字符串的\0
。如果目标缓冲区不够大,就会发生缓冲区溢出(Buffer Overflow)!
缓冲区溢出是C语言中最常见、最危险的漏洞之一。它会导致:
-
数据损坏: 覆盖目标缓冲区后面的合法数据。
-
程序崩溃: 覆盖了重要的程序数据(如返回地址、栈帧指针),导致程序异常终止(Segmentation Fault)。
-
安全漏洞: 攻击者可能利用缓冲区溢出注入恶意代码,劫持程序控制流。
让我们用代码来模拟这个“灾难现场”:
// string_pitfalls.h
#ifndef STRING_PITFALLS_H
#define STRING_PITFALLS_H
#include <stdio.h>
#include <string.h> // For strcpy, strcat, sprintf, strncpy, strncat, snprintf
/**
* @brief 演示 strcpy 函数的缓冲区溢出风险。
*/
void demo_strcpy_overflow();
/**
* @brief 演示 strcat 函数的缓冲区溢出风险。
*/
void demo_strcat_overflow();
/**
* @brief 演示 sprintf 函数的缓冲区溢出风险。
*/
void demo_sprintf_overflow();
/**
* @brief 演示使用 strncpy 的安全复制。
*/
void demo_strncpy_safe();
/**
* @brief 演示使用 strncat 的安全连接。
*/
void demo_strncat_safe();
/**
* @brief 演示使用 snprintf 的安全格式化。
*/
void demo_snprintf_safe();
#endif // STRING_PITFALLS_H
```c
// string_pitfalls.c
#include "string_pitfalls.h"
#include <stdlib.h> // For exit
#include "../my_string_utils/my_string_utils.h" // 引入上一篇的内存打印工具
/**
* @brief 演示 strcpy 函数的缓冲区溢出风险。
* 目标缓冲区 `dest` 的大小不足以容纳源字符串 `src`,导致内存越界写入。
*/
void demo_strcpy_overflow() {
printf("\n--- 演示 strcpy() 缓冲区溢出风险 ---\n");
char src[] = "This is a very long string that will definitely overflow the small buffer.";
char dest[20]; // 目标缓冲区只有 20 字节,不足以容纳 src
printf("源字符串 (src): \"%s\" (长度: %zu)\n", src, strlen(src));
printf("目标缓冲区 (dest) 大小: %zu 字节\n", sizeof(dest));
printf("dest 初始内存布局 (前 %zu 字节):\n", sizeof(dest) + 10);
print_memory_layout(dest, sizeof(dest) + 10); // 打印 dest 及其后方内存
printf("\n尝试使用 strcpy() 复制字符串...\n");
// 警告:以下代码将导致缓冲区溢出,可能导致程序崩溃或不可预测的行为!
// 在实际开发中,绝对不能这样使用 strcpy()。
strcpy(dest, src);
printf("\nstrcpy() 完成 (如果程序没崩溃的话)。\n");
printf("dest 内容: \"%s\"\n", dest); // 打印可能被截断或乱码的 dest
printf("dest 最终内存布局 (前 %zu 字节):\n", sizeof(dest) + 10);
print_memory_layout(dest, sizeof(dest) + 10); // 再次打印,观察溢出效果
printf("--- strcpy() 缓冲区溢出风险演示结束 ---\n");
// 注意:在某些编译环境下,编译器可能会发出警告。
// 在某些操作系统上,可能会立即触发段错误 (Segmentation Fault)。
// 在其他情况下,程序可能暂时正常运行,但后续行为不可预测。
}
/**
* @brief 演示 strcat 函数的缓冲区溢出风险。
* 目标缓冲区 `dest` 在连接后不足以容纳原始内容和连接内容,导致内存越界写入。
*/
void demo_strcat_overflow() {
printf("\n--- 演示 strcat() 缓冲区溢出风险 ---\n");
char initial[] = "Hello"; // 初始字符串,包含 '\0',占用 6 字节
char append[] = ", World! This is a very very long string to append."; // 要连接的字符串
char buffer[20]; // 目标缓冲区只有 20 字节
// 初始复制,确保 buffer 中有有效字符串
strcpy(buffer, initial); // buffer 现在是 "Hello\0"
printf("初始缓冲区 (buffer): \"%s\" (长度: %zu, 大小: %zu)\n", buffer, strlen(buffer), sizeof(buffer));
printf("要连接的字符串 (append): \"%s\" (长度: %zu)\n", append, strlen(append));
printf("buffer 初始内存布局 (前 %zu 字节):\n", sizeof(buffer) + 10);
print_memory_layout(buffer, sizeof(buffer) + 10);
printf("\n尝试使用 strcat() 连接字符串...\n");
// 警告:以下代码将导致缓冲区溢出,可能导致程序崩溃或不可预测的行为!
// buffer 初始占用 6 字节,append 长度远超剩余的 14 字节。
strcat(buffer, append);
printf("\nstrcat() 完成 (如果程序没崩溃的话)。\n");
printf("buffer 内容: \"%s\"\n", buffer);
printf("buffer 最终内存布局 (前 %zu 字节):\n", sizeof(buffer) + 10);
print_memory_layout(buffer, sizeof(buffer) + 10);
printf("--- strcat() 缓冲区溢出风险演示结束 ---\n");
}
/**
* @brief 演示 sprintf 函数的缓冲区溢出风险。
* 格式化后的字符串长度超过目标缓冲区大小。
*/
void demo_sprintf_overflow() {
printf("\n--- 演示 sprintf() 缓冲区溢出风险 ---\n");
char buffer[30]; // 目标缓冲区 30 字节
int num = 123456789;
char* text = "This is some additional text that makes the string even longer.";
printf("目标缓冲区 (buffer) 大小: %zu 字节\n", sizeof(buffer));
printf("buffer 初始内存布局 (前 %zu 字节):\n", sizeof(buffer) + 10);
print_memory_layout(buffer, sizeof(buffer) + 10);
printf("\n尝试使用 sprintf() 格式化字符串...\n");
// 警告:以下代码将导致缓冲区溢出。
// 格式化字符串的长度 (数字 + 文本 + 额外字符) 远超 30 字节。
sprintf(buffer, "Number: %d, Text: %s, End.", num, text);
printf("\nsprintf() 完成 (如果程序没崩溃的话)。\n");
printf("buffer 内容: \"%s\"\n", buffer);
printf("buffer 最终内存布局 (前 %zu 字节):\n", sizeof(buffer) + 10);
print_memory_layout(buffer, sizeof(buffer) + 10);
printf("--- sprintf() 缓冲区溢出风险演示结束 ---\n");
}
### 安全替代方案:带长度限制的字符串函数
为了解决上述问题,C语言标准库提供了更安全的替代方案,它们都带有一个额外的参数来指定目标缓冲区的最大大小:
* `strncpy(dest, src, n)`:最多复制`n`个字符。**注意:如果源字符串长度大于等于`n`,`strncpy`不会自动添加`\0`!你需要手动添加。**
* `strncat(dest, src, n)`:最多从`src`中连接`n`个字符。**它会确保目标字符串以`\0`结尾。** `n`表示可以从`src`复制的最大字符数,而不是目标缓冲区的总剩余空间。
* `snprintf(dest, size, format, ...)`:最多向`dest`写入`size-1`个字符,并确保以`\0`结尾。这是最推荐的格式化字符串方法。
让我们看看它们是如何“守护”你的程序的:
```c
// string_pitfalls.c (续)
/**
* @brief 演示使用 strncpy 的安全复制。
* strncpy(dest, src, n) 最多复制 n 个字符。
* ⚠️ 注意:如果 src 长度 >= n,dest 不会自动以 '\0' 结尾。
*/
void demo_strncpy_safe() {
printf("\n--- 演示 strncpy() 的安全复制 ---\n");
char src[] = "A longer source string.";
char dest[10]; // 目标缓冲区 10 字节
printf("源字符串 (src): \"%s\" (长度: %zu)\n", src, strlen(src));
printf("目标缓冲区 (dest) 大小: %zu 字节\n", sizeof(dest));
printf("\n场景 1: 源字符串短于目标缓冲区大小 - 1 (有空间放 '\\0')\n");
char dest1[10];
memset(dest1, 'X', sizeof(dest1)); // 用 'X' 填充,方便观察
dest1[sizeof(dest1) - 1] = '\0'; // 确保至少有一个 '\0'
printf("dest1 初始内存布局:\n");
print_memory_layout(dest1, sizeof(dest1));
strncpy(dest1, "Short", sizeof(dest1) - 1); // 留出 '\0' 的空间
dest1[sizeof(dest1) - 1] = '\0'; // 确保 null 终止
printf("dest1 内容: \"%s\"\n", dest1);
printf("dest1 最终内存布局:\n");
print_memory_layout(dest1, sizeof(dest1));
printf("\n场景 2: 源字符串长于目标缓冲区大小 - 1 (没有空间放 '\\0')\n");
char dest2[10];
memset(dest2, 'Y', sizeof(dest2)); // 用 'Y' 填充
dest2[sizeof(dest2) - 1] = '\0';
printf("dest2 初始内存布局:\n");
print_memory_layout(dest2, sizeof(dest2));
strncpy(dest2, "VeryLongString", sizeof(dest2)); // 复制 10 个字符,不留 '\0' 空间
// ⚠️ 此时 dest2 可能没有 '\0' 终止!
dest2[sizeof(dest2) - 1] = '\0'; // 必须手动添加 '\0'
printf("dest2 内容: \"%s\"\n", dest2); // 打印可能被截断的字符串
printf("dest2 最终内存布局:\n");
print_memory_layout(dest2, sizeof(dest2));
printf("--- strncpy() 安全复制演示结束 ---\n");
}
/**
* @brief 演示使用 strncat 的安全连接。
* strncat(dest, src, n) 最多从 src 连接 n 个字符,并确保目标以 '\0' 结尾。
* n 是可以从 src 复制的最大字符数,而不是目标缓冲区的总剩余空间。
*/
void demo_strncat_safe() {
printf("\n--- 演示 strncat() 的安全连接 ---\n");
char initial_buf[20] = "Hello"; // 初始 "Hello\0",大小 20
char append_str[] = ", World! This is a long string.";
printf("初始缓冲区 (initial_buf): \"%s\" (长度: %zu, 大小: %zu)\n", initial_buf, strlen(initial_buf), sizeof(initial_buf));
printf("要连接的字符串 (append_str): \"%s\" (长度: %zu)\n", append_str, strlen(append_str));
// 计算 initial_buf 剩余空间:sizeof(initial_buf) - strlen(initial_buf) - 1 (为 '\0' 留一个字节)
size_t remaining_space = sizeof(initial_buf) - strlen(initial_buf) - 1;
printf("initial_buf 剩余空间: %zu 字节\n", remaining_space);
printf("initial_buf 初始内存布局 (前 %zu 字节):\n", sizeof(initial_buf) + 5);
print_memory_layout(initial_buf, sizeof(initial_buf) + 5);
printf("\n尝试使用 strncat() 连接字符串...\n");
// strncat 会将 append_str 的内容连接到 initial_buf 的 '\0' 处
// 最多连接 remaining_space 个字符,并确保目标以 '\0' 结尾
strncat(initial_buf, append_str, remaining_space);
printf("\nstrncat() 完成。\n");
printf("initial_buf 内容: \"%s\"\n", initial_buf);
printf("initial_buf 最终内存布局 (前 %zu 字节):\n", sizeof(initial_buf) + 5);
print_memory_layout(initial_buf, sizeof(initial_buf) + 5);
printf("--- strncat() 安全连接演示结束 ---\n");
}
/**
* @brief 演示使用 snprintf 的安全格式化。
* snprintf(dest, size, format, ...) 最多向 dest 写入 size-1 个字符,并确保以 '\0' 结尾。
*/
void demo_snprintf_safe() {
printf("\n--- 演示 snprintf() 的安全格式化 ---\n");
char buffer[30]; // 目标缓冲区 30 字节
int num = 123456789;
char* text = "This is some additional text that makes the string even longer.";
printf("目标缓冲区 (buffer) 大小: %zu 字节\n", sizeof(buffer));
printf("\n尝试使用 snprintf() 格式化字符串 (限制大小)...\n");
// snprintf 会确保写入的字符数不超过 sizeof(buffer) - 1,并添加 '\0'
int chars_written = snprintf(buffer, sizeof(buffer), "Number: %d, Text: %s, End.", num, text);
printf("\nsnprintf() 完成。\n");
printf("buffer 内容: \"%s\"\n", buffer);
printf("实际写入字符数 (不含 '\\0'): %d\n", chars_written);
printf("buffer 最终内存布局 (前 %zu 字节):\n", sizeof(buffer) + 5);
print_memory_layout(buffer, sizeof(buffer) + 5);
// 再次演示,当格式化结果小于缓冲区时
printf("\n--- snprintf() 场景 2: 格式化结果小于缓冲区 ---\n");
char small_buffer[20];
int small_num = 42;
char* short_text = "Short text.";
snprintf(small_buffer, sizeof(small_buffer), "Num: %d, Txt: %s.", small_num, short_text);
printf("small_buffer 内容: \"%s\"\n", small_buffer);
printf("small_buffer 最终内存布局 (前 %zu 字节):\n", sizeof(small_buffer) + 5);
print_memory_layout(small_buffer, sizeof(small_buffer) + 5);
printf("--- snprintf() 安全格式化演示结束 ---\n");
}
编译和运行:
gcc -o string_pitfalls_demo string_pitfalls.c my_string_utils.c -Wall -Wextra
./string_pitfalls_demo
核心发现:
-
不带长度限制的字符串函数是危险的!它们是缓冲区溢出的主要原因。
-
strncpy
虽然限制了复制数量,但不会自动添加\0
,需要手动处理。 -
strncat
和snprintf
是更安全的替代品,它们会确保目标缓冲区以\0
结尾,防止溢出。 -
永远记住:在使用任何字符串操作函数时,都要确保目标缓冲区有足够的空间,并且总是使用带长度限制的安全版本!
第五回合:动态字符串的“魔法”——在堆上跳舞的内存
在C语言中,如果你需要处理长度不确定的字符串,或者在程序运行时改变字符串的长度,那么静态分配的字符数组(如char arr[100];
)就显得力不从心了。这时,你就需要掌握动态内存分配的“魔法”,让字符串在**堆(Heap)**上“跳舞”!
malloc
、realloc
、free
的艺术
-
malloc(size)
:在堆上分配size
字节的内存,并返回指向这块内存起始地址的指针。如果分配失败,返回NULL
。 -
realloc(ptr, new_size)
:重新调整ptr
指向的内存块的大小为new_size
。它可能会在原地扩展,也可能分配新的内存块并将旧内容复制过去,然后释放旧内存。 -
free(ptr)
:释放ptr
指向的内存块,将其归还给系统。
动态字符串的生命周期:
-
分配: 使用
malloc
为字符串分配初始内存。 -
使用: 将字符复制到这块内存中,并确保以
\0
结尾。 -
调整: 如果字符串长度需要改变(如连接更多内容),使用
realloc
调整内存大小。 -
释放: 当不再需要字符串时,使用
free
释放内存,防止内存泄漏(Memory Leak)。
让我们构建一个简单的动态字符串管理工具:
// dynamic_string.h
#ifndef DYNAMIC_STRING_H
#define DYNAMIC_STRING_H
#include <stddef.h> // For size_t
#include <stdbool.h> // For bool
// 定义一个动态字符串结构体
typedef struct {
char* data; // 指向字符串数据的指针 (堆上分配)
size_t length; // 字符串当前长度 (不含 '\0')
size_t capacity;// 字符串当前容量 (包含 '\0' 的总大小)
} DynamicString;
/**
* @brief 初始化一个空的动态字符串。
* @return 指向新创建的 DynamicString 结构体的指针。
*/
DynamicString* ds_init();
/**
* @brief 初始化一个带有初始内容的动态字符串。
* @param initial_str 初始字符串内容。
* @return 指向新创建的 DynamicString 结构体的指针。
*/
DynamicString* ds_init_with_str(const char* initial_str);
/**
* @brief 释放动态字符串占用的所有内存。
* @param ds 指向要释放的 DynamicString 结构体的指针。
*/
void ds_free(DynamicString* ds);
/**
* @brief 确保动态字符串有足够的容量。
* 如果当前容量不足,则重新分配内存。
* @param ds 指向 DynamicString 结构体的指针。
* @param required_capacity 所需的最小容量 (包含 '\0')。
* @return true 如果成功确保容量,false 如果内存分配失败。
*/
bool ds_ensure_capacity(DynamicString* ds, size_t required_capacity);
/**
* @brief 设置动态字符串的内容。
* @param ds 指向 DynamicString 结构体的指针。
* @param new_str 新的字符串内容。
* @return true 如果成功设置,false 如果内存分配失败。
*/
bool ds_set(DynamicString* ds, const char* new_str);
/**
* @brief 将字符串连接到动态字符串的末尾。
* @param ds 指向 DynamicString 结构体的指针。
* @param append_str 要连接的字符串。
* @return true 如果成功连接,false 如果内存分配失败。
*/
bool ds_append(DynamicString* ds, const char* append_str);
/**
* @brief 获取动态字符串的 C 风格字符串指针。
* @param ds 指向 DynamicString 结构体的指针。
* @return C 风格字符串指针 (以 '\0' 结尾)。
*/
const char* ds_get_cstr(DynamicString* ds);
/**
* @brief 打印动态字符串的详细信息。
* @param ds 指向 DynamicString 结构体的指针。
*/
void ds_print_info(DynamicString* ds);
/**
* @brief 演示动态字符串的使用。
*/
void demo_dynamic_string();
#endif // DYNAMIC_STRING_H
```c
// dynamic_string.c
#include "dynamic_string.h"
#include <stdio.h>
#include <stdlib.h> // For malloc, realloc, free
#include <string.h> // For strlen, strcpy, strcat, memset
// 初始容量,通常是一个小整数,例如 16 或 32
#define INITIAL_DS_CAPACITY 32
/**
* @brief 初始化一个空的动态字符串。
* @return 指向新创建的 DynamicString 结构体的指针。
*/
DynamicString* ds_init() {
DynamicString* ds = (DynamicString*)malloc(sizeof(DynamicString));
if (!ds) {
perror("ds_init: 内存分配失败");
return NULL;
}
ds->data = (char*)malloc(INITIAL_DS_CAPACITY);
if (!ds->data) {
perror("ds_init: 字符串数据内存分配失败");
free(ds);
return NULL;
}
ds->data[0] = '\0'; // 确保字符串以 null 终止
ds->length = 0;
ds->capacity = INITIAL_DS_CAPACITY;
return ds;
}
/**
* @brief 初始化一个带有初始内容的动态字符串。
* @param initial_str 初始字符串内容。
* @return 指向新创建的 DynamicString 结构体的指针。
*/
DynamicString* ds_init_with_str(const char* initial_str) {
DynamicString* ds = (DynamicString*)malloc(sizeof(DynamicString));
if (!ds) {
perror("ds_init_with_str: 内存分配失败");
return NULL;
}
size_t initial_len = (initial_str) ? strlen(initial_str) : 0;
// 确保容量足够容纳初始字符串和 null 终止符
size_t required_capacity = initial_len + 1;
// 确保容量至少是初始容量,以避免频繁的小分配
if (required_capacity < INITIAL_DS_CAPACITY) {
required_capacity = INITIAL_DS_CAPACITY;
}
ds->data = (char*)malloc(required_capacity);
if (!ds->data) {
perror("ds_init_with_str: 字符串数据内存分配失败");
free(ds);
return NULL;
}
if (initial_str) {
strcpy(ds->data, initial_str); // 使用 strcpy,因为我们已确保容量
} else {
ds->data[0] = '\0';
}
ds->length = initial_len;
ds->capacity = required_capacity;
return ds;
}
/**
* @brief 释放动态字符串占用的所有内存。
* @param ds 指向要释放的 DynamicString 结构体的指针。
*/
void ds_free(DynamicString* ds) {
if (ds) {
if (ds->data) {
free(ds->data);
ds->data = NULL; // 防止悬空指针
}
free(ds);
}
}
/**
* @brief 确保动态字符串有足够的容量。
* 如果当前容量不足,则重新分配内存。
* @param ds 指向 DynamicString 结构体的指针。
* @param required_capacity 所需的最小容量 (包含 '\0')。
* @return true 如果成功确保容量,false 如果内存分配失败。
*/
bool ds_ensure_capacity(DynamicString* ds, size_t required_capacity) {
if (!ds) return false;
if (ds->capacity >= required_capacity) {
return true; // 容量已足够
}
// 扩容策略:通常是当前容量的两倍或更多,以减少 realloc 调用次数
size_t new_capacity = ds->capacity;
while (new_capacity < required_capacity) {
new_capacity *= 2;
}
// 确保新容量至少是 required_capacity
if (new_capacity < required_capacity) {
new_capacity = required_capacity;
}
char* new_data = (char*)realloc(ds->data, new_capacity);
if (!new_data) {
perror("ds_ensure_capacity: 内存重新分配失败");
return false;
}
ds->data = new_data;
ds->capacity = new_capacity;
return true;
}
/**
* @brief 设置动态字符串的内容。
* @param ds 指向 DynamicString 结构体的指针。
* @param new_str 新的字符串内容。
* @return true 如果成功设置,false 如果内存分配失败。
*/
bool ds_set(DynamicString* ds, const char* new_str) {
if (!ds || !new_str) return false;
size_t new_len = strlen(new_str);
if (!ds_ensure_capacity(ds, new_len + 1)) { // +1 for '\0'
return false;
}
strcpy(ds->data, new_str);
ds->length = new_len;
return true;
}
/**
* @brief 将字符串连接到动态字符串的末尾。
* @param ds 指向 DynamicString 结构体的指针。
* @param append_str 要连接的字符串。
* @return true 如果成功连接,false 如果内存分配失败。
*/
bool ds_append(DynamicString* ds, const char* append_str) {
if (!ds || !append_str) return false;
size_t append_len = strlen(append_str);
size_t new_len = ds->length + append_len;
if (!ds_ensure_capacity(ds, new_len + 1)) { // +1 for '\0'
return false;
}
strcat(ds->data, append_str); // 使用 strcat,因为我们已确保容量
ds->length = new_len;
return true;
}
/**
* @brief 获取动态字符串的 C 风格字符串指针。
* @param ds 指向 DynamicString 结构体的指针。
* @return C 风格字符串指针 (以 '\0' 结尾)。
*/
const char* ds_get_cstr(DynamicString* ds) {
if (!ds) return NULL;
return ds->data;
}
/**
* @brief 打印动态字符串的详细信息。
* @param ds 指向 DynamicString 结构体的指针。
*/
void ds_print_info(DynamicString* ds) {
if (!ds) {
printf("动态字符串: NULL\n");
return;
}
printf("动态字符串信息:\n");
printf(" 内容: \"%s\"\n", ds->data ? ds->data : "(null)");
printf(" 长度: %zu (不含 '\\0')\n", ds->length);
printf(" 容量: %zu (含 '\\0')\n", ds->capacity);
printf(" 数据地址: %p\n", (void*)ds->data);
}
/**
* @brief 演示动态字符串的使用。
*/
void demo_dynamic_string() {
printf("\n--- 演示动态字符串 (DynamicString) ---\n");
// 1. 初始化一个空字符串
DynamicString* my_str = ds_init();
if (!my_str) return;
ds_print_info(my_str);
// 2. 设置内容
printf("\n设置内容: \"Hello, \"\n");
if (!ds_set(my_str, "Hello, ")) {
ds_free(my_str);
return;
}
ds_print_info(my_str);
// 3. 追加内容 (不触发扩容)
printf("\n追加内容: \"World!\"\n");
if (!ds_append(my_str, "World!")) {
ds_free(my_str);
return;
}
ds_print_info(my_str);
// 4. 追加更多内容 (可能触发扩容)
printf("\n追加更多内容: \" This is a very long sentence that will definitely exceed the current capacity.\"\n");
if (!ds_append(my_str, " This is a very long sentence that will definitely exceed the current capacity.")) {
ds_free(my_str);
return;
}
ds_print_info(my_str);
// 5. 再次设置较短内容 (容量不变)
printf("\n再次设置较短内容: \"Short string.\"\n");
if (!ds_set(my_str, "Short string.")) {
ds_free(my_str);
return;
}
ds_print_info(my_str);
// 6. 释放内存
printf("\n释放动态字符串内存。\n");
ds_free(my_str);
my_str = NULL; // 防止野指针
// 7. 初始化带初始内容的字符串
printf("\n初始化带初始内容的字符串: \"Initial content.\"\n");
DynamicString* another_str = ds_init_with_str("Initial content.");
if (!another_str) return;
ds_print_info(another_str);
ds_free(another_str);
printf("\n--- 动态字符串演示结束 ---\n");
}
编译和运行:
gcc -o dynamic_string_demo dynamic_string.c -Wall -Wextra
./dynamic_string_demo
核心发现:
-
堆内存的灵活性:
malloc
和realloc
允许你在程序运行时根据需要分配和调整内存大小,这对于处理可变长度的字符串至关重要。 -
容量管理: 为了避免频繁的
realloc
操作(它可能很耗时),动态字符串通常会预留一些额外的容量。当实际长度接近容量时,会一次性扩容到更大的倍数(例如2倍)。 -
手动管理: 动态分配的内存必须手动
free
!忘记free
会导致内存泄漏,程序会不断消耗内存,最终可能耗尽系统资源。 -
NULL
检查:malloc
和realloc
都可能返回NULL
(内存分配失败),因此在使用返回的指针之前,务必进行NULL
检查。
第六回合:字符数组与指针的“变脸”——C语言的“魔术”与“陷阱”
在C语言中,数组和指针的关系是如此紧密,以至于常常让人混淆。它们之间存在一种特殊的“变脸”机制,即数组名在大多数情况下会退化(decay)为指向其第一个元素的指针。
数组名何时“变脸”为指针?
当数组名作为表达式使用时(除了少数例外),它会“变脸”为指向其第一个元素的指针。
例外情况:
-
当数组名作为
sizeof
运算符的操作数时:sizeof(arr)
返回整个数组的大小。 -
当数组名作为
&
运算符的操作数时:&arr
返回指向整个数组的指针(类型是array_type*
)。 -
当数组名用于初始化字符数组时:
char arr[] = "hello";
。
为什么会“变脸”?
这是C语言设计哲学的一部分,它简化了数组的传递和操作。但同时也带来了困惑,尤其是在函数参数传递时。
// array_pointer_decay.h
#ifndef ARRAY_POINTER_DECAY_H
#define ARRAY_POINTER_DECAY_H
#include <stdio.h>
#include <stddef.h> // For size_t
/**
* @brief 演示数组名在不同场景下的行为,特别是其退化为指针的特性。
*/
void demo_array_pointer_decay();
/**
* @brief 接收一个字符数组作为参数的函数。
* 实际上,arr 在这里会退化为 char* 指针。
* @param arr 字符数组参数。
*/
void print_char_array_info(char arr[]);
/**
* @brief 接收一个整型数组作为参数的函数。
* 实际上,arr 在这里会退化为 int* 指针。
* @param arr 整型数组参数。
* @param size 数组的实际元素数量。
*/
void print_int_array_info(int arr[], size_t size);
#endif // ARRAY_POINTER_DECAY_H
```c
// array_pointer_decay.c
#include "array_pointer_decay.h"
#include <string.h> // For strlen
/**
* @brief 接收一个字符数组作为参数的函数。
* 实际上,arr 在这里会退化为 char* 指针。
* @param arr 字符数组参数。
*/
void print_char_array_info(char arr[]) { // 编译器会将 char arr[] 视为 char* arr
printf(" --- 在 print_char_array_info 函数内部 ---\n");
printf(" 参数 arr (声明为 char arr[]):\n");
printf(" sizeof(arr): %zu 字节 (这是指针的大小,不是原始数组的大小!)\n", sizeof(arr));
printf(" strlen(arr): %zu (正常计算字符串长度)\n", strlen(arr));
printf(" arr 的地址: %p\n", (void*)arr);
printf(" --------------------------------------\n");
}
/**
* @brief 接收一个整型数组作为参数的函数。
* 实际上,arr 在这里会退化为 int* 指针。
* @param arr 整型数组参数。
* @param size 数组的实际元素数量。
*/
void print_int_array_info(int arr[], size_t size) { // 编译器会将 int arr[] 视为 int* arr
printf(" --- 在 print_int_array_info 函数内部 ---\n");
printf(" 参数 arr (声明为 int arr[]):\n");
printf(" sizeof(arr): %zu 字节 (这是指针的大小,不是原始数组的大小!)\n", sizeof(arr));
printf(" arr 的地址: %p\n", (void*)arr);
printf(" 遍历数组元素 (使用传入的 size):\n");
for (size_t i = 0; i < size; ++i) {
printf(" arr[%zu] = %d\n", i, arr[i]);
}
printf(" --------------------------------------\n");
}
/**
* @brief 演示数组名在不同场景下的行为,特别是其退化为指针的特性。
*/
void demo_array_pointer_decay() {
printf("\n--- 演示数组与指针的退化 (Decay) ---\n");
char my_char_array[] = "Hello World"; // 字符数组,包含 '\0'
int my_int_array[5] = {10, 20, 30, 40, 50}; // 整型数组
printf("原始数组声明:\n");
printf(" char my_char_array[] = \"Hello World\";\n");
printf(" int my_int_array[5] = {10, 20, 30, 40, 50};\n");
printf("\n--- 在 main 函数内部 (数组名未退化) ---\n");
printf("my_char_array:\n");
printf(" sizeof(my_char_array): %zu 字节 (整个数组大小,包含 '\\0')\n", sizeof(my_char_array)); // 12 字节
printf(" strlen(my_char_array): %zu (字符串长度)\n", strlen(my_char_array)); // 11
printf(" my_char_array 的地址: %p\n", (void*)my_char_array);
printf(" &my_char_array 的地址: %p (指向整个数组的指针)\n", (void*)&my_char_array);
printf("\nmy_int_array:\n");
printf(" sizeof(my_int_array): %zu 字节 (整个数组大小)\n", sizeof(my_int_array)); // 5 * sizeof(int)
printf(" my_int_array 的地址: %p\n", (void*)my_int_array);
printf(" &my_int_array 的地址: %p (指向整个数组的指针)\n", (void*)&my_int_array);
printf("\n--- 数组作为函数参数传递 (数组名退化为指针) ---\n");
print_char_array_info(my_char_array); // 传递数组名,退化为 char*
print_int_array_info(my_int_array, sizeof(my_int_array) / sizeof(my_int_array[0])); // 传递数组名和大小
printf("\n--- 数组名赋值给指针 ---\n");
char *ptr_char = my_char_array; // 数组名退化为 char*
int *ptr_int = my_int_array; // 数组名退化为 int*
printf("char *ptr_char = my_char_array;\n");
printf(" sizeof(ptr_char): %zu 字节 (指针大小)\n", sizeof(ptr_char));
printf(" ptr_char 的地址: %p\n", (void*)ptr_char);
printf(" *ptr_char 的值: '%c'\n", *ptr_char);
printf("\nint *ptr_int = my_int_array;\n");
printf(" sizeof(ptr_int): %zu 字节 (指针大小)\n", sizeof(ptr_int));
printf(" ptr_int 的地址: %p\n", (void*)ptr_int);
printf(" *ptr_int 的值: %d\n", *ptr_int);
printf("\n--- 演示通过指针访问数组元素 ---\n");
printf("通过 ptr_int 访问 my_int_array:\n");
for (int i = 0; i < 5; ++i) {
printf(" *(ptr_int + %d) = %d\n", i, *(ptr_int + i)); // 指针算术
printf(" ptr_int[%d] = %d\n", i, ptr_int[i]); // 数组下标语法
}
printf("注意:指针和数组名在访问元素时语法上是等价的,但底层含义不同。\n");
printf("\n--- 数组指针 (指向整个数组的指针) ---\n");
// 声明一个指向包含 5 个 int 元素的数组的指针
int (*array_ptr)[5] = &my_int_array;
printf("int (*array_ptr)[5] = &my_int_array;\n");
printf(" sizeof(array_ptr): %zu 字节 (指针大小)\n", sizeof(array_ptr));
printf(" sizeof(*array_ptr): %zu 字节 (指向的整个数组大小)\n", sizeof(*array_ptr)); // 5 * sizeof(int)
printf(" array_ptr 的地址: %p\n", (void*)array_ptr);
printf(" (*array_ptr)[0] 的值: %d\n", (*array_ptr)[0]);
printf("\n--- 数组与指针的退化演示结束 ---\n");
}
编译和运行:
gcc -o array_decay_demo array_pointer_decay.c -Wall -Wextra
./array_decay_demo
核心发现:
-
数组名是数组的“别名”: 在其声明的作用域内,
sizeof(数组名)
给出整个数组的大小。 -
数组名作为表达式时“退化”为指针: 这意味着当你将数组名传递给函数,或者将其赋值给一个指针变量时,它会变成一个指向其第一个元素的指针。此时对它使用
sizeof
,得到的是指针的大小,而不是数组的大小! -
函数参数中的数组声明是“语法糖”:
void func(int arr[])
实际上等同于void func(int *arr)
。你无法在函数内部通过sizeof
获取原始数组的大小。 -
数组指针:
int (*array_ptr)[5]
这种语法是声明一个指向整个数组的指针,这与指向数组元素的指针int *ptr
是不同的。
第七回合:内存越界与缓冲区溢出——程序崩溃的“幕后黑手”
我们前面已经多次提到内存越界和缓冲区溢出,它们是C语言程序中最致命的错误之一。
内存越界(Out-of-bounds Access): 访问了数组或已分配内存区域之外的地址。 缓冲区溢出(Buffer Overflow): 特指当写入的数据量超过了缓冲区本身的容量,导致数据覆盖了相邻的内存区域。
这些错误通常不会在编译时被发现,而是在运行时悄无声息地发生,直到引发程序崩溃、数据损坏或安全漏洞。
常见原因:
-
不检查目标缓冲区大小的字符串函数(如
strcpy
)。 -
循环条件错误,导致数组下标超出范围。
-
指针算术错误,导致指针指向非法地址。
-
free
后继续使用已释放的内存(Use-After-Free)。 -
多次
free
同一块内存(Double-Free)。
如何防范?
-
始终检查边界: 在所有涉及内存写入的操作中,都要确保不会超出目标缓冲区的边界。
-
使用安全函数: 优先使用
strncpy
、strncat
、snprintf
等带长度限制的函数。 -
动态分配内存: 对于长度不确定的字符串或数据,使用
malloc
、realloc
进行动态分配,并根据需要调整大小。 -
严格的指针管理:
-
初始化所有指针为
NULL
。 -
free
内存后,立即将指针设置为NULL
,防止野指针(Dangling Pointer)。 -
避免多次
free
同一块内存。
-
-
编译器警告和静态分析工具: 开启编译器的所有警告(如
-Wall -Wextra
),并使用静态分析工具(如Coverity, Clang Static Analyzer, Valgrind等)来检测潜在的内存错误。 -
防御性编程: 假设所有外部输入都是恶意的,对输入进行严格的验证和过滤。
第八回合:C语言的“内存哲学”——权力与责任的平衡
现在,我们已经走完了C语言字符串和内存的“深海之旅”。你可能已经感受到了C语言的强大,也体会到了它的危险。
这正是C语言的**“内存哲学”**:
C语言将内存的直接控制权交给了程序员。
这意味着:
-
极致的性能: 你可以精确地控制数据在内存中的布局,最大限度地利用硬件资源,编写出运行速度极快的程序。
-
高度的灵活性: 你可以实现各种复杂的数据结构和算法,不受高级语言的限制。
-
底层交互: 你可以直接与操作系统、硬件进行交互,编写设备驱动、嵌入式系统等。
然而,这种无上的权力也伴随着巨大的责任:
-
手动内存管理: 你必须手动分配和释放内存。一旦出错,就会导致内存泄漏、野指针、双重释放等问题。
-
缺乏运行时保护: C语言在运行时几乎不进行边界检查。这意味着如果你访问了非法内存,程序很可能直接崩溃,而不是给出友好的错误提示。
-
安全风险: 内存错误很容易被攻击者利用,成为安全漏洞的突破口。
C语言的这种哲学,就像一把双刃剑。它赋予你无与伦比的力量,但也要求你具备高度的纪律性和责任感。
掌握C语言的内存哲学,意味着你必须成为一个“内存守护者”!
-
你必须清楚每一个变量在内存中的位置和大小。
-
你必须知道每一个指针指向何方,以及它是否仍然有效。
-
你必须像管理自己的财产一样,精确地分配和释放每一块内存。
只有这样,你才能真正驾驭C语言,编写出高效、稳定、安全的程序!
总结(下):成为C语言的“内存守护者”
朋友们,恭喜你!你已经完成了C语言“隐秘角落”的深度探险!
从strlen
与sizeof
的表面之战,到空字符\0
的灵魂契约;从字符串操作的“雷区”,到动态内存的“魔法”;从数组与指针的“变脸”,到内存越界与缓冲区溢出的致命威胁;最终,我们触及了C语言那既强大又危险的“内存哲学”!
你现在已经拥有了:
-
透彻的理解: 深刻理解
strlen
和sizeof
的底层实现和工作机制。 -
洞察力: 能够看穿C语言字符串的本质和内存布局。
-
警惕性: 识别并避免
strcpy
、strcat
等危险函数带来的缓冲区溢出。 -
掌控力: 熟练运用
malloc
、realloc
、free
进行动态字符串管理。 -
辨识力: 区分数组与指针的“变脸”,避免常见的指针陷阱。
-
守护力: 掌握防范内存越界和缓冲区溢出的关键策略。
这不仅仅是一篇技术文章,更是你成为一名真正“C语言高手”的必经之路!当你能够自信地在内存的“海洋”中遨游,驾驭每一个字节,你就会发现,C语言的底层世界是如此的迷人,如此的充满力量!
现在,你不再是那个只知其然的“代码搬运工”,而是能洞悉其所以然的“内存福尔摩斯”,一个真正能够驾驭C语言底层力量的**“代码守护者”**!
这趟“肝爆”之旅虽然告一段落,但你对C语言的探索永无止境!
点赞!收藏!关注! 让我们一起,在C语言的底层世界中,继续我们的代码传奇!
附录:所有代码文件汇总
为了方便你编译和运行,以下是本系列文章中所有代码文件的结构和内容。请将它们放在同一个目录下。
my_string_utils.h
// my_string_utils.h
#ifndef MY_STRING_UTILS_H
#define MY_STRING_UTILS_H
#include <stddef.h> // 包含 size_t 的定义
#include <stdio.h> // 包含 printf
/**
* @brief 模拟实现 C 标准库中的 strlen 函数。
* 计算以 null 结尾的字符串的长度(不包括 null 终止符)。
*
* @param str 指向要计算长度的字符串的常量指针。
* 该指针必须指向一个以 '\0' 结尾的字符序列。
* 如果 str 不以 '\0' 结尾,函数将导致未定义行为。
* @return size_t 字符串的长度。
*/
size_t my_strlen(const char *str);
/**
* @brief 打印字符串的内存布局,包括每个字符的ASCII值和十六进制表示。
* 这是一个辅助函数,用于可视化字符串在内存中的实际存储。
*
* @param str 指向字符串的常量指针。
* @param length 要打印的字节数。
*/
void print_memory_layout(const char *str, size_t length);
/**
* @brief 演示 strlen 函数在不同初始化方式下的行为。
* 包括字符串字面量初始化和字符数组初始化。
*/
void demo_strlen_behavior();
#endif // MY_STRING_UTILS_H
my_string_utils.c
// my_string_utils.c
#include "my_string_utils.h"
#include <stdio.h> // For printf
#include <string.h> // For original strlen, for comparison
#include <stdlib.h> // For exit, malloc, free
/**
* @brief 模拟实现 C 标准库中的 strlen 函数。
* 计算以 null 结尾的字符串的长度(不包括 null 终止符)。
*
* @param str 指向要计算长度的字符串的常量指针。
* 该指针必须指向一个以 '\0' 结尾的字符序列。
* 如果 str 不以 '\0' 结尾,函数将导致未定义行为。
* @return size_t 字符串的长度。
*/
size_t my_strlen(const char *str) {
// 声明一个与传入指针类型相同的局部指针 s
// 它将从 str 指向的地址开始,向后遍历
const char *s = str;
// 循环条件:*s != '\0'
// 这是一个非常经典的 C 语言循环模式,用于遍历字符串直到遇到 null 终止符。
// 1. *s 表示解引用指针 s,获取 s 当前指向的字符。
// 2. != '\0' 检查这个字符是否是 null 终止符。
// 3. 如果不是 null 终止符,循环继续。
// 4. 如果是 null 终止符,循环终止。
while (*s != '\0') {
// 如果当前字符不是 null 终止符,将指针 s 向后移动一个字节。
// 这意味着 s 现在指向字符串中的下一个字符。
s++;
}
// 循环结束后,指针 s 指向了字符串的 null 终止符 '\0'。
// 而指针 str 仍然指向字符串的起始位置。
// 两个指针相减(s - str)的结果是它们之间的距离,
// 这个距离就是从字符串起始位置到 null 终止符之前的字符数量。
// 例如,如果 str 指向地址 1000,s 指向地址 1005 (即 'o' 后的 '\0'),
// 那么 s - str = 5,表示字符串长度为 5。
return s - str;
}
/**
* @brief 打印字符串的内存布局,包括每个字符的ASCII值和十六进制表示。
* 这是一个辅助函数,用于可视化字符串在内存中的实际存储。
*
* @param str 指向字符串的常量指针。
* @param length 要打印的字节数。
*/
void print_memory_layout(const char *str, size_t length) {
if (!str) {
printf(" (NULL pointer)\n");
return;
}
printf(" Memory Address | Char | ASCII (Dec) | Hex\n");
printf(" ----------------------------------------------\n");
for (size_t i = 0; i < length; ++i) {
// 获取当前字节的地址
const char *current_byte_ptr = str + i;
// 获取当前字节的值
unsigned char byte_value = (unsigned char)*current_byte_ptr;
printf(" %p | ", (void*)current_byte_ptr); // 打印内存地址
// 打印字符表示:如果是可打印字符,则打印字符本身,否则打印 '.'
if (byte_value >= 32 && byte_value <= 126) {
printf(" '%c' | ", byte_value);
} else if (byte_value == '\0') {
printf(" '\\0'| "); // 特殊处理 null 终止符
} else {
printf(" '.' | "); // 其他不可打印字符
}
printf("%-11d | 0x%02X\n", byte_value, byte_value); // 打印十进制和十六进制值
}
printf(" ----------------------------------------------\n");
}
/**
* @brief 演示 strlen 函数在不同初始化方式下的行为。
* 包括字符串字面量初始化和字符数组初始化。
*/
void demo_strlen_behavior() {
printf("\n--- 演示 strlen 行为 ---\n");
// 案例 1: 使用字符串字面量初始化 (编译器自动添加 '\0')
char s1[] = "Hello"; // 内存布局: 'H', 'e', 'l', 'l', 'o', '\0'
printf("char s1[] = \"Hello\";\n");
printf(" strlen(s1): %zu\n", my_strlen(s1)); // 期望输出 5
printf(" sizeof(s1): %zu\n", sizeof(s1)); // 期望输出 6
printf(" s1 内存布局 (前 %zu 字节):\n", sizeof(s1) + 2); // 多打印几个字节,看看后面有没有垃圾数据
print_memory_layout(s1, sizeof(s1) + 2);
// 案例 2: 使用字符列表初始化 (没有自动添加 '\0')
char s2[] = {'W', 'o', 'r', 'l', 'd', '!'}; // 内存布局: 'W', 'o', 'r', 'l', 'd', '!'
printf("\nchar s2[] = {'W', 'o', 'r', 'l', 'd', '!'};\n");
printf(" sizeof(s2): %zu\n", sizeof(s2)); // 期望输出 6
printf(" strlen(s2): ");
// 这里故意不使用 my_strlen,而是直接调用,以便观察未定义行为
// 警告:以下调用可能导致程序崩溃或输出随机值
size_t len_s2 = my_strlen(s2); // ⚠️ 潜在的未定义行为!
printf("%zu (⚠️ 潜在的未定义行为,取决于内存内容)\n", len_s2);
printf(" s2 内存布局 (前 %zu 字节):\n", sizeof(s2) + 5); // 多打印几个字节,看看后面有没有垃圾数据
print_memory_layout(s2, sizeof(s2) + 5);
// 案例 3: 使用字符列表初始化,但手动添加 '\0'
char s3[] = {'C', 'o', 'd', 'e', '\0'}; // 内存布局: 'C', 'o', 'd', 'e', '\0'
printf("\nchar s3[] = {'C', 'o', 'd', 'e', '\\0'};\n");
printf(" strlen(s3): %zu\n", my_strlen(s3)); // 期望输出 4
printf(" sizeof(s3): %zu\n", sizeof(s3)); // 期望输出 5
printf(" s3 内存布局 (前 %zu 字节):\n", sizeof(s3) + 2);
print_memory_layout(s3, sizeof(s3) + 2);
// 案例 4: 字符串字面量作为函数参数传递
// 此时 s4 在函数内部退化为 char* 指针,sizeof 将返回指针的大小
void func_with_string_param(char *param_str) {
printf("\nInside func_with_string_param(char *param_str):\n");
printf(" strlen(param_str): %zu\n", my_strlen(param_str));
printf(" sizeof(param_str): %zu (这是指针的大小,不是字符串内容的大小)\n", sizeof(param_str));
printf(" param_str 内存布局 (前 %zu 字节):\n", my_strlen(param_str) + 2);
print_memory_layout(param_str, my_strlen(param_str) + 2);
}
char s4[] = "World";
printf("\nchar s4[] = \"World\";\n");
func_with_string_param(s4);
printf("\n--- strlen 行为演示结束 ---\n");
}
sizeof_demo.h
// sizeof_demo.h
#ifndef SIZEOF_DEMO_H
#define SIZEOF_DEMO_H
#include <stddef.h> // 包含 size_t 的定义
#include <stdio.h> // 包含 printf
/**
* @brief 演示 sizeof 操作符在不同类型和变量上的行为。
*/
void demo_sizeof_behavior();
#endif // SIZEOF_DEMO_H
sizeof_demo.c
// sizeof_demo.c
#include "sizeof_demo.h"
#include <limits.h> // For CHAR_BIT
/**
* @brief 演示 sizeof 操作符在不同类型和变量上的行为。
*/
void demo_sizeof_behavior() {
printf("\n--- 演示 sizeof 行为 ---\n");
// 1. 基本数据类型
printf("1. 基本数据类型的大小:\n");
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));
printf(" sizeof(float): %zu 字节\n", sizeof(float));
printf(" sizeof(double): %zu 字节\n", sizeof(double));
printf(" sizeof(long double): %zu 字节\n", sizeof(long double));
printf(" sizeof(void*): %zu 字节 (指针大小)\n", sizeof(void*));
printf(" 系统字节位数 (CHAR_BIT): %d 位\n", CHAR_BIT); // 通常是 8
// 2. 数组
printf("\n2. 数组的大小:\n");
int intArray[10]; // 包含 10 个 int 元素的数组
printf(" int intArray[10];\n");
printf(" sizeof(intArray): %zu 字节 (整个数组的大小)\n", sizeof(intArray));
printf(" sizeof(intArray[0]): %zu 字节 (单个元素的大小)\n", sizeof(intArray[0]));
printf(" 数组元素个数: %zu\n", sizeof(intArray) / sizeof(intArray[0]));
char charArray[5] = {'a', 'b', 'c', 'd', 'e'}; // 5个字符的数组,没有 '\0'
printf(" char charArray[5] = {'a', 'b', 'c', 'd', 'e'};\n");
printf(" sizeof(charArray): %zu 字节\n", sizeof(charArray)); // 5 字节
char stringLiteralArray[] = "Hello"; // 字符串字面量初始化,自动添加 '\0'
printf(" char stringLiteralArray[] = \"Hello\";\n");
printf(" sizeof(stringLiteralArray): %zu 字节 (包含 '\\0')\n", sizeof(stringLiteralArray)); // 6 字节
// 3. 指针
printf("\n3. 指针的大小:\n");
int *ptrInt;
char *ptrChar;
double *ptrDouble;
printf(" sizeof(ptrInt): %zu 字节 (指针本身的大小)\n", sizeof(ptrInt));
printf(" sizeof(ptrChar): %zu 字节\n", sizeof(ptrChar));
printf(" sizeof(ptrDouble): %zu 字节\n", sizeof(ptrDouble));
printf(" 注意:所有指针类型的大小通常是相同的,取决于系统架构 (32位系统通常4字节,64位系统通常8字节)。\n");
// 4. 结构体 (考虑内存对齐)
printf("\n4. 结构体的大小 (考虑内存对齐):\n");
// 结构体成员的对齐规则可能导致结构体大小大于成员大小之和
// 例如,在某些系统上,int 需要 4 字节对齐
struct MyStruct1 {
char c1; // 1 byte
int i; // 4 bytes
char c2; // 1 byte
}; // 实际大小可能是 12 字节 (1 + 3(padding) + 4 + 1 + 3(padding))
struct MyStruct2 {
char c1; // 1 byte
char c2; // 1 byte
int i; // 4 bytes
}; // 实际大小可能是 8 字节 (1 + 1 + 2(padding) + 4)
printf(" struct MyStruct1 { char c1; int i; char c2; };\n");
printf(" sizeof(struct MyStruct1): %zu 字节\n", sizeof(struct MyStruct1));
printf(" struct MyStruct2 { char c1; char c2; int i; };\n");
printf(" sizeof(struct MyStruct2): %zu 字节\n", sizeof(struct MyStruct2));
printf(" 内存对齐是编译器为了提高访问效率而做的优化,可能导致结构体大小大于成员大小之和。\n");
// 5. 函数参数中的数组退化为指针
printf("\n5. 函数参数中的数组退化为指针:\n");
void func_array_param(int arr[]) { // 实际上 arr 是 int*
printf(" Inside func_array_param(int arr[]):\n");
printf(" sizeof(arr): %zu 字节 (这是指针的大小,不是数组的大小)\n", sizeof(arr));
}
int localArray[20];
printf(" int localArray[20];\n");
printf(" sizeof(localArray) in main: %zu 字节\n", sizeof(localArray));
func_array_param(localArray);
// 6. 字符串字面量本身
printf("\n6. 字符串字面量本身的大小:\n");
printf(" sizeof(\"Hello\"): %zu 字节 (包含 '\\0')\n", sizeof("Hello")); // 6 字节
printf(" sizeof(\"\"): %zu 字节 (空字符串也包含 '\\0')\n", sizeof("")); // 1 字节
printf("\n--- sizeof 行为演示结束 ---\n");
}
memory_layout_demo.h
// memory_layout_demo.h
#ifndef MEMORY_LAYOUT_DEMO_H
#define MEMORY_LAYOUT_DEMO_H
#include <stdio.h>
#include <stdlib.h> // For malloc, free
// 全局变量 (已初始化)
int global_initialized_var = 100;
// 全局变量 (未初始化)
int global_uninitialized_var;
/**
* @brief 演示不同类型变量在内存中的大小和地址。
*/
void demo_variable_sizes_and_addresses();
/**
* @brief 演示字符串字面量在内存中的存储位置。
*/
void demo_string_literal_storage();
/**
* @brief 演示函数栈帧和局部变量的内存布局。
*/
void demo_stack_frame_layout();
// 辅助函数声明 (用于栈帧演示)
void function_level_1(int param1, char param2);
void function_level_2(int param_f2);
#endif // MEMORY_LAYOUT_DEMO_H
memory_layout_demo.c
// memory_layout_demo.c
#include "memory_layout_demo.h"
// 全局变量 (已初始化)
// 存储在数据段 (Data Segment)
int global_initialized_var = 100;
// 全局变量 (未初始化)
// 存储在 BSS 段 (Block Started by Symbol Segment),程序启动时清零
int global_uninitialized_var;
// 静态变量 (已初始化)
// 存储在数据段 (Data Segment)
static int static_initialized_var = 200;
// 静态变量 (未初始化)
// 存储在 BSS 段 (Block Started by Symbol Segment)
static int static_uninitialized_var;
/**
* @brief 演示不同类型变量在内存中的大小和地址。
*/
void demo_variable_sizes_and_addresses() {
printf("\n--- 变量大小与地址演示 ---\n");
// 局部变量 (存储在栈上)
int local_int = 10;
char local_char = 'A';
float local_float = 3.14f;
int local_array[5]; // 局部数组也在栈上
printf("局部变量:\n");
printf(" local_int: 地址 %p, 大小 %zu 字节, 值 %d\n", (void*)&local_int, sizeof(local_int), local_int);
printf(" local_char: 地址 %p, 大小 %zu 字节, 值 '%c'\n", (void*)&local_char, sizeof(local_char), local_char);
printf(" local_float: 地址 %p, 大小 %zu 字节, 值 %f\n", (void*)&local_float, sizeof(local_float), local_float);
printf(" local_array: 地址 %p, 大小 %zu 字节\n", (void*)&local_array, sizeof(local_array));
printf("\n全局变量:\n");
printf(" global_initialized_var: 地址 %p, 大小 %zu 字节, 值 %d\n", (void*)&global_initialized_var, sizeof(global_initialized_var), global_initialized_var);
printf(" global_uninitialized_var: 地址 %p, 大小 %zu 字节, 值 %d\n", (void*)&global_uninitialized_var, sizeof(global_uninitialized_var), global_uninitialized_var);
printf("\n静态变量:\n");
printf(" static_initialized_var: 地址 %p, 大小 %zu 字节, 值 %d\n", (void*)&static_initialized_var, sizeof(static_initialized_var), static_initialized_var);
printf(" static_uninitialized_var: 地址 %p, 大小 %zu 字节, 值 %d\n", (void*)&static_uninitialized_var, sizeof(static_uninitialized_var), static_uninitialized_var);
// 动态分配内存 (存储在堆上)
int *heap_int = (int*)malloc(sizeof(int));
if (heap_int) {
*heap_int = 500;
printf("\n堆上分配的变量:\n");
printf(" heap_int (指针): 地址 %p, 大小 %zu 字节\n", (void*)&heap_int, sizeof(heap_int));
printf(" *heap_int (指向的值): 地址 %p, 大小 %zu 字节, 值 %d\n", (void*)heap_int, sizeof(*heap_int), *heap_int);
free(heap_int); // 释放内存
}
printf("\n--- 变量大小与地址演示结束 ---\n");
}
/**
* @brief 演示字符串字面量在内存中的存储位置。
* 字符串字面量通常存储在只读数据段 (Text Segment 或 Read-Only Data Segment)。
*/
void demo_string_literal_storage() {
printf("\n--- 字符串字面量存储演示 ---\n");
// 字符串字面量 "Hello World" 通常存储在只读数据段
// s1 是一个字符数组,内容从字面量复制而来,存储在栈上
char s1[] = "Hello World";
// s2 是一个指针,指向字符串字面量在只读数据段的地址
const char *s2 = "Hello World";
// s3 也是一个指针,指向同一个字符串字面量
const char *s3 = "Hello World";
printf("char s1[] = \"Hello World\"; (数组,内容在栈上)\n");
printf(" s1 地址: %p\n", (void*)s1);
printf(" sizeof(s1): %zu 字节\n", sizeof(s1)); // 包含 '\0'
printf("\nconst char *s2 = \"Hello World\"; (指针,指向只读数据段)\n");
printf(" s2 地址: %p\n", (void*)s2);
printf(" sizeof(s2): %zu 字节 (指针大小)\n", sizeof(s2));
printf("\nconst char *s3 = \"Hello World\"; (指针,指向只读数据段)\n");
printf(" s3 地址: %p\n", (void*)s3);
printf(" sizeof(s3): %zu 字节 (指针大小)\n", sizeof(s3));
// 比较 s2 和 s3 的地址,它们可能相同 (编译器优化)
if (s2 == s3) {
printf("\ns2 和 s3 指向相同的字符串字面量地址 (编译器优化)。\n");
} else {
printf("\ns2 和 s3 指向不同的字符串字面量地址。\n");
}
printf("\n--- 字符串字面量存储演示结束 ---\n");
}
// 辅助函数:用于演示栈帧
void function_level_1(int param1, char param2) {
int local_var_f1 = 100;
char local_char_f1 = 'X';
printf(" 进入 function_level_1:\n");
printf(" param1 地址: %p, 值: %d\n", (void*)¶m1, param1);
printf(" param2 地址: %p, 值: '%c'\n", (void*)¶m2, param2);
printf(" local_var_f1 地址: %p, 值: %d\n", (void*)&local_var_f1, local_var_f1);
printf(" local_char_f1 地址: %p, 值: '%c'\n", (void*)&local_char_f1, local_char_f1);
// 递归调用,观察栈帧变化
if (param1 > 0) {
function_level_2(param1 - 1);
}
printf(" 退出 function_level_1\n");
}
void function_level_2(int param_f2) {
int local_var_f2 = 200;
printf(" 进入 function_level_2:\n");
printf(" param_f2 地址: %p, 值: %d\n", (void*)¶m_f2, param_f2);
printf(" local_var_f2 地址: %p, 值: %d\n", (void*)&local_var_f2, local_var_f2);
printf(" 退出 function_level_2\n");
}
/**
* @brief 演示函数栈帧和局部变量的内存布局。
*/
void demo_stack_frame_layout() {
printf("\n--- 栈帧布局演示 ---\n");
int main_local_var = 50;
printf("main 函数局部变量 main_local_var 地址: %p, 值: %d\n", (void*)&main_local_var, main_local_var);
function_level_1(2, 'Y'); // 调用函数
printf("main 函数局部变量 main_local_var (调用后) 地址: %p, 值: %d\n", (void*)&main_local_var, main_local_var);
printf("\n--- 栈帧布局演示结束 ---\n");
}
compiler_core.h
// compiler_core.h
#ifndef COMPILER_CORE_H
#define COMPILER_CORE_H
#include <stddef.h> // For size_t
#include <stdbool.h> // For bool
#include <string.h> // For strcmp, strdup
#include <stdio.h> // For fprintf
// --- 1. 模拟类型系统 (简化版) ---
typedef enum {
TYPE_UNKNOWN,
TYPE_VOID,
TYPE_CHAR,
TYPE_INT,
TYPE_FLOAT,
TYPE_ARRAY, // 新增数组类型
TYPE_POINTER // 新增指针类型
} MyTypeKind;
typedef struct MyType {
MyTypeKind kind;
size_t size; // 类型占用的字节数
size_t align; // 类型的对齐要求
// 对于数组类型:
struct MyType* base_type; // 数组元素的基类型
size_t array_size; // 数组的元素数量
// 对于指针类型:
struct MyType* pointed_type; // 指针指向的类型
} MyType;
// 全局静态类型实例 (单例模式,避免重复创建)
extern MyType* my_type_void;
extern MyType* my_type_char;
extern MyType* my_type_int;
extern MyType* my_type_float;
extern MyType* my_type_pointer_to_char; // 示例:char*
// 初始化基本类型
void init_my_types();
// 释放动态创建的类型 (例如数组类型)
void free_my_types();
// 创建数组类型
MyType* create_my_array_type(MyType* base_type, size_t array_size);
// 创建指针类型
MyType* create_my_pointer_type(MyType* pointed_type);
// 获取类型大小 (模拟 sizeof 行为)
size_t get_type_size(MyType* type);
// 获取类型对齐要求
size_t get_type_alignment(MyType* type);
// 将 MyTypeKind 转换为字符串 (用于打印)
const char* my_type_kind_to_string(MyTypeKind kind);
// 打印 MyType 详细信息
void print_my_type_info(MyType* type);
// --- 2. 模拟符号表 (简化版) ---
typedef enum {
SYM_VAR,
SYM_FUNC
} MySymbolKind;
typedef struct MySymbol {
char* name;
MySymbolKind kind;
MyType* type; // 符号的类型
size_t offset; // 在内存中的偏移量 (简化为相对栈帧或数据段起始)
bool is_global;
} MySymbol;
// 简单的哈希表桶
#define SYMBOL_TABLE_BUCKET_SIZE 127
typedef struct MySymbolBucket {
MySymbol* symbol;
struct MySymbolBucket* next;
} MySymbolBucket;
// 作用域
typedef struct MyScope {
MySymbolBucket* buckets[SYMBOL_TABLE_BUCKET_SIZE];
struct MyScope* parent; // 父作用域
size_t next_local_offset; // 下一个局部变量的可用偏移量
} MyScope;
// 符号表管理器
typedef struct MySymbolTable {
MyScope* current_scope;
size_t global_data_size; // 全局数据段的总大小
} MySymbolTable;
// 符号表函数
MySymbolTable* init_my_symbol_table();
void free_my_symbol_table(MySymbolTable* sym_table);
void enter_my_scope(MySymbolTable* sym_table);
void exit_my_scope(MySymbolTable* sym_table);
MySymbol* add_my_symbol(MySymbolTable* sym_table, const char* name, MySymbolKind kind, MyType* type, bool is_global);
MySymbol* lookup_my_symbol(MySymbolTable* sym_table, const char* name);
MySymbol* lookup_my_symbol_in_current_scope(MySymbolTable* sym_table, const char* name);
// --- 3. 模拟 AST 节点 (极度简化,只关注变量声明和字面量) ---
typedef enum {
AST_PROGRAM_SIMPLIFIED,
AST_VAR_DECL_SIMPLIFIED,
AST_IDENTIFIER_SIMPLIFIED,
AST_STRING_LITERAL_SIMPLIFIED,
AST_INTEGER_LITERAL_SIMPLIFIED,
AST_TYPE_SPECIFIER_SIMPLIFIED // 例如 int, char 等
} MyASTNodeType;
typedef struct MyASTNode {
MyASTNodeType type;
// 关联的类型信息 (语义分析后填充)
MyType* resolved_type;
// 关联的符号表条目 (对于标识符和变量声明)
MySymbol* symbol_entry;
union {
struct { char* name; } identifier;
struct { char* value; } string_literal;
struct { long long value; } integer_literal;
struct { MyTypeKind kind; } type_specifier; // 原始类型指示
struct {
struct MyASTNode* type_node;
struct MyASTNode* identifier_node;
struct MyASTNode* initializer_node; // 可选
} var_decl;
struct {
struct MyASTNode** declarations;
int num_declarations;
} program;
} data;
} MyASTNode;
// AST 节点创建函数 (简化版)
MyASTNode* my_ast_new_node(MyASTNodeType type);
MyASTNode* my_ast_new_var_decl(MyASTNode* type_node, MyASTNode* identifier_node, MyASTNode* initializer_node);
MyASTNode* my_ast_new_identifier(const char* name);
MyASTNode* my_ast_new_string_literal(const char* value);
MyASTNode* my_ast_new_integer_literal(long long value);
MyASTNode* my_ast_new_type_specifier(MyTypeKind kind);
MyASTNode* my_ast_new_program(MyASTNode** declarations, int num_declarations);
void free_my_ast(MyASTNode* node);
// --- 4. 模拟语义分析器 (极度简化,只处理变量声明和类型解析) ---
typedef struct {
MySymbolTable* sym_table;
int error_count;
} MySemanticAnalyzer;
MySemanticAnalyzer* init_my_semantic_analyzer(MySymbolTable* sym_table);
void free_my_semantic_analyzer(MySemanticAnalyzer* analyzer);
void my_semantic_error(MySemanticAnalyzer* analyzer, const char* format, ...);
// 核心分析函数
void analyze_my_program(MyASTNode* program_node, MySemanticAnalyzer* analyzer);
void analyze_my_var_decl(MyASTNode* var_decl_node, MySemanticAnalyzer* analyzer, bool is_global);
MyType* resolve_my_type_specifier(MyASTNode* type_node);
MyType* analyze_my_expression_type(MyASTNode* expr_node, MySemanticAnalyzer* analyzer);
#endif // COMPILER_CORE_H
compiler_core.c
// compiler_core.c
#include "compiler_core.h"
#include <stdarg.h> // For va_list
#include <stdlib.h> // For malloc, free, exit
#include <string.h> // For strdup, strcmp
// --- 1. 模拟类型系统实现 ---
MyType _my_type_void = {TYPE_VOID, 0, 1, NULL, 0, NULL}; // void 类型大小为0,对齐1
MyType _my_type_char = {TYPE_CHAR, 1, 1, NULL, 0, NULL}; // char 类型大小1,对齐1
MyType _my_type_int = {TYPE_INT, 4, 4, NULL, 0, NULL}; // int 类型大小4,对齐4
MyType _my_type_float = {TYPE_FLOAT, 4, 4, NULL, 0, NULL}; // float 类型大小4,对齐4
// 模拟指针大小 (这里假设是 8 字节,64位系统)
MyType _my_type_pointer_to_char = {TYPE_POINTER, 8, 8, NULL, 0, &_my_type_char};
MyType* my_type_void = &_my_type_void;
MyType* my_type_char = &_my_type_char;
MyType* my_type_int = &_my_type_int;
MyType* my_type_float = &_my_type_float;
MyType* my_type_pointer_to_char = &_my_type_pointer_to_char;
// 动态分配的类型列表,用于统一释放
static MyType** dynamic_types = NULL;
static int num_dynamic_types = 0;
static int dynamic_types_capacity = 0;
void add_dynamic_type(MyType* type) {
if (num_dynamic_types >= dynamic_types_capacity) {
dynamic_types_capacity = (dynamic_types_capacity == 0) ? 4 : dynamic_types_capacity * 2;
dynamic_types = (MyType**)realloc(dynamic_types, dynamic_types_capacity * sizeof(MyType*));
if (!dynamic_types) {
fprintf(stderr, "错误: 内存重新分配失败 (dynamic_types)\n");
exit(EXIT_FAILURE);
}
}
dynamic_types[num_dynamic_types++] = type;
}
void init_my_types() {
fprintf(stderr, "模拟类型系统初始化完成。\n");
}
void free_my_types() {
for (int i = 0; i < num_dynamic_types; ++i) {
if (dynamic_types[i]) {
// 如果是数组类型,可能其 base_type 是静态的,不需要释放
// 如果是指针类型,其 pointed_type 也是静态的
free(dynamic_types[i]);
}
}
if (dynamic_types) {
free(dynamic_types);
dynamic_types = NULL;
}
num_dynamic_types = 0;
dynamic_types_capacity = 0;
fprintf(stderr, "模拟类型系统资源已清理。\n");
}
// 创建数组类型
MyType* create_my_array_type(MyType* base_type, size_t array_size) {
MyType* arr_type = (MyType*)malloc(sizeof(MyType));
if (!arr_type) {
fprintf(stderr, "错误: 内存分配失败 (数组类型)\n");
exit(EXIT_FAILURE);
}
arr_type->kind = TYPE_ARRAY;
arr_type->base_type = base_type;
arr_type->array_size = array_size;
// 数组的总大小 = 元素个数 * 元素大小
arr_type->size = base_type->size * array_size;
// 数组的对齐要求通常与基类型相同
arr_type->align = base_type->align;
arr_type->pointed_type = NULL; // 非指针类型
add_dynamic_type(arr_type);
return arr_type;
}
// 创建指针类型
MyType* create_my_pointer_type(MyType* pointed_type) {
MyType* ptr_type = (MyType*)malloc(sizeof(MyType));
if (!ptr_type) {
fprintf(stderr, "错误: 内存分配失败 (指针类型)\n");
exit(EXIT_FAILURE);
}
ptr_type->kind = TYPE_POINTER;
ptr_type->pointed_type = pointed_type;
// 指针的大小在编译时确定,通常是4或8字节,与系统架构有关
ptr_type->size = sizeof(void*); // 直接使用 void* 的大小
ptr_type->align = sizeof(void*); // 对齐要求通常与大小相同
ptr_type->base_type = NULL;
ptr_type->array_size = 0;
add_dynamic_type(ptr_type);
return ptr_type;
}
// 获取类型大小 (模拟 sizeof 行为)
size_t get_type_size(MyType* type) {
if (!type) return 0;
return type->size;
}
// 获取类型对齐要求
size_t get_type_alignment(MyType* type) {
if (!type) return 0;
return type->align;
}
// 将 MyTypeKind 转换为字符串 (用于打印)
const char* my_type_kind_to_string(MyTypeKind kind) {
switch (kind) {
case TYPE_UNKNOWN: return "UNKNOWN";
case TYPE_VOID: return "VOID";
case TYPE_CHAR: return "CHAR";
case TYPE_INT: return "INT";
case TYPE_FLOAT: return "FLOAT";
case TYPE_ARRAY: return "ARRAY";
case TYPE_POINTER: return "POINTER";
default: return "UNKNOWN_TYPE_KIND";
}
}
// 打印 MyType 详细信息
void print_my_type_info(MyType* type) {
if (!type) {
printf("NULL Type\n");
return;
}
printf("Kind: %s, Size: %zu, Align: %zu",
my_type_kind_to_string(type->kind), type->size, type->align);
if (type->kind == TYPE_ARRAY) {
printf(", Base Type: %s, Array Size: %zu",
my_type_kind_to_string(type->base_type->kind), type->array_size);
} else if (type->kind == TYPE_POINTER) {
printf(", Pointed Type: %s",
my_type_kind_to_string(type->pointed_type->kind));
}
printf("\n");
}
// --- 2. 模拟符号表实现 ---
// 简单的哈希函数 (DJB2)
static unsigned int my_hash_string(const char* str) {
unsigned int hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c; // hash * 33 + c
}
return hash % SYMBOL_TABLE_BUCKET_SIZE;
}
// 创建新符号
static MySymbol* create_my_symbol(const char* name, MySymbolKind kind, MyType* type, bool is_global) {
MySymbol* sym = (MySymbol*)malloc(sizeof(MySymbol));
if (!sym) {
fprintf(stderr, "错误: 内存分配失败 (MySymbol)\n");
exit(EXIT_FAILURE);
}
sym->name = strdup(name);
if (!sym->name) {
fprintf(stderr, "错误: 内存分配失败 (MySymbol name)\n");
free(sym);
exit(EXIT_FAILURE);
}
sym->kind = kind;
sym->type = type;
sym->offset = 0; // 初始偏移量
sym->is_global = is_global;
return sym;
}
// 释放符号
static void free_my_symbol(MySymbol* sym) {
if (sym) {
if (sym->name) free(sym->name);
free(sym);
}
}
// 释放桶链表
static void free_my_bucket_list(MySymbolBucket* head) {
MySymbolBucket* current = head;
while (current) {
MySymbolBucket* next = current->next;
free_my_symbol(current->symbol);
free(current);
current = next;
}
}
// 创建作用域
static MyScope* create_my_scope(MyScope* parent) {
MyScope* scope = (MyScope*)malloc(sizeof(MyScope));
if (!scope) {
fprintf(stderr, "错误: 内存分配失败 (MyScope)\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < SYMBOL_TABLE_BUCKET_SIZE; ++i) {
scope->buckets[i] = NULL;
}
scope->parent = parent;
scope->next_local_offset = 0;
return scope;
}
// 释放作用域
static void free_my_scope(MyScope* scope) {
if (scope) {
for (int i = 0; i < SYMBOL_TABLE_BUCKET_SIZE; ++i) {
free_my_bucket_list(scope->buckets[i]);
}
free(scope);
}
}
MySymbolTable* init_my_symbol_table() {
MySymbolTable* sym_table = (MySymbolTable*)malloc(sizeof(MySymbolTable));
if (!sym_table) {
fprintf(stderr, "错误: 内存分配失败 (MySymbolTable)\n");
exit(EXIT_FAILURE);
}
sym_table->current_scope = NULL;
sym_table->global_data_size = 0;
enter_my_scope(sym_table); // 进入全局作用域
fprintf(stderr, "模拟符号表初始化完成,进入全局作用域。\n");
return sym_table;
}
void free_my_symbol_table(MySymbolTable* sym_table) {
if (sym_table) {
while (sym_table->current_scope) {
exit_my_scope(sym_table);
}
free(sym_table);
fprintf(stderr, "模拟符号表资源已清理。\n");
}
}
void enter_my_scope(MySymbolTable* sym_table) {
MyScope* new_scope = create_my_scope(sym_table->current_scope);
sym_table->current_scope = new_scope;
fprintf(stderr, " 进入新作用域。\n");
}
void exit_my_scope(MySymbolTable* sym_table) {
if (!sym_table->current_scope) return;
MyScope* old_scope = sym_table->current_scope;
sym_table->current_scope = old_scope->parent;
free_my_scope(old_scope);
fprintf(stderr, " 退出作用域。\n");
}
MySymbol* add_my_symbol(MySymbolTable* sym_table, const char* name, MySymbolKind kind, MyType* type, bool is_global) {
if (lookup_my_symbol_in_current_scope(sym_table, name)) {
return NULL; // 符号已存在
}
MySymbol* new_sym = create_my_symbol(name, kind, type, is_global);
// 分配偏移量 (简化,不考虑对齐)
if (is_global) {
new_sym->offset = sym_table->global_data_size;
sym_table->global_data_size += type->size;
} else {
// 局部变量在栈上的偏移量,通常是负的,这里简化为正的累积偏移
new_sym->offset = sym_table->current_scope->next_local_offset;
// 考虑对齐,将下一个偏移量对齐到类型要求
size_t aligned_offset = (sym_table->current_scope->next_local_offset + type->align - 1) & ~(type->align - 1);
new_sym->offset = aligned_offset;
sym_table->current_scope->next_local_offset = aligned_offset + type->size;
}
unsigned int hash_val = my_hash_string(name);
MySymbolBucket* new_bucket = (MySymbolBucket*)malloc(sizeof(MySymbolBucket));
if (!new_bucket) {
fprintf(stderr, "错误: 内存分配失败 (MySymbolBucket)\n");
free_my_symbol(new_sym);
exit(EXIT_FAILURE);
}
new_bucket->symbol = new_sym;
new_bucket->next = sym_table->current_scope->buckets[hash_val];
sym_table->current_scope->buckets[hash_val] = new_bucket;
fprintf(stderr, " 添加符号: '%s' (类型: %s, 大小: %zu, 偏移: %zu, 全局: %d)\n",
name, my_type_kind_to_string(type->kind), type->size, new_sym->offset, is_global);
return new_sym;
}
MySymbol* lookup_my_symbol(MySymbolTable* sym_table, const char* name) {
MyScope* current = sym_table->current_scope;
while (current) {
unsigned int hash_val = my_hash_string(name);
MySymbolBucket* bucket = current->buckets[hash_val];
while (bucket) {
if (strcmp(bucket->symbol->name, name) == 0) {
return bucket->symbol;
}
bucket = bucket->next;
}
current = current->parent;
}
return NULL;
}
MySymbol* lookup_my_symbol_in_current_scope(MySymbolTable* sym_table, const char* name) {
if (!sym_table->current_scope) return NULL;
unsigned int hash_val = my_hash_string(name);
MySymbolBucket* bucket = sym_table->current_scope->buckets[hash_val];
while (bucket) {
if (strcmp(bucket->symbol->name, name) == 0) {
return bucket->symbol;
}
bucket = bucket->next;
}
return NULL;
}
// --- 3. 模拟 AST 节点实现 ---
MyASTNode* my_ast_new_node(MyASTNodeType type) {
MyASTNode* node = (MyASTNode*)malloc(sizeof(MyASTNode));
if (!node) {
fprintf(stderr, "错误: 内存分配失败 (MyASTNode)\n");
exit(EXIT_FAILURE);
}
node->type = type;
node->resolved_type = NULL;
node->symbol_entry = NULL;
memset(&node->data, 0, sizeof(node->data));
return node;
}
MyASTNode* my_ast_new_var_decl(MyASTNode* type_node, MyASTNode* identifier_node, MyASTNode* initializer_node) {
MyASTNode* node = my_ast_new_node(AST_VAR_DECL_SIMPLIFIED);
node->data.var_decl.type_node = type_node;
node->data.var_decl.identifier_node = identifier_node;
node->data.var_decl.initializer_node = initializer_node;
return node;
}
MyASTNode* my_ast_new_identifier(const char* name) {
MyASTNode* node = my_ast_new_node(AST_IDENTIFIER_SIMPLIFIED);
node->data.identifier.name = strdup(name);
if (!node->data.identifier.name) {
fprintf(stderr, "错误: 内存分配失败 (Identifier name)\n");
exit(EXIT_FAILURE);
}
return node;
}
MyASTNode* my_ast_new_string_literal(const char* value) {
MyASTNode* node = my_ast_new_node(AST_STRING_LITERAL_SIMPLIFIED);
node->data.string_literal.value = strdup(value);
if (!node->data.string_literal.value) {
fprintf(stderr, "错误: 内存分配失败 (String literal value)\n");
exit(EXIT_FAILURE);
}
return node;
}
MyASTNode* my_ast_new_integer_literal(long long value) {
MyASTNode* node = my_ast_new_node(AST_INTEGER_LITERAL_SIMPLIFIED);
node->data.integer_literal.value = value;
return node;
}
MyASTNode* my_ast_new_type_specifier(MyTypeKind kind) {
MyASTNode* node = my_ast_new_node(AST_TYPE_SPECIFIER_SIMPLIFIED);
node->data.type_specifier.kind = kind;
return node;
}
MyASTNode* my_ast_new_program(MyASTNode** declarations, int num_declarations) {
MyASTNode* node = my_ast_new_node(AST_PROGRAM_SIMPLIFIED);
node->data.program.declarations = declarations;
node->data.program.num_declarations = num_declarations;
return node;
}
void free_my_ast(MyASTNode* node) {
if (!node) return;
switch (node->type) {
case AST_PROGRAM_SIMPLIFIED:
for (int i = 0; i < node->data.program.num_declarations; ++i) {
free_my_ast(node->data.program.declarations[i]);
}
if (node->data.program.declarations) free(node->data.program.declarations);
break;
case AST_VAR_DECL_SIMPLIFIED:
free_my_ast(node->data.var_decl.type_node);
free_my_ast(node->data.var_decl.identifier_node);
if (node->data.var_decl.initializer_node) {
free_my_ast(node->data.var_decl.initializer_node);
}
break;
case AST_IDENTIFIER_SIMPLIFIED:
if (node->data.identifier.name) free(node->data.identifier.name);
break;
case AST_STRING_LITERAL_SIMPLIFIED:
if (node->data.string_literal.value) free(node->data.string_literal.value);
break;
case AST_INTEGER_LITERAL_SIMPLIFIED:
case AST_TYPE_SPECIFIER_SIMPLIFIED:
// 这些没有动态分配的子节点或字符串
break;
}
free(node);
}
// --- 4. 模拟语义分析器实现 ---
MySemanticAnalyzer* init_my_semantic_analyzer(MySymbolTable* sym_table) {
MySemanticAnalyzer* analyzer = (MySemanticAnalyzer*)malloc(sizeof(MySemanticAnalyzer));
if (!analyzer) {
fprintf(stderr, "错误: 内存分配失败 (MySemanticAnalyzer)\n");
exit(EXIT_FAILURE);
}
analyzer->sym_table = sym_table;
analyzer->error_count = 0;
fprintf(stderr, "模拟语义分析器初始化成功。\n");
return analyzer;
}
void free_my_semantic_analyzer(MySemanticAnalyzer* analyzer) {
if (analyzer) {
// sym_table 在外部管理,不在这里释放
free(analyzer);
fprintf(stderr, "模拟语义分析器资源已清理。\n");
}
}
void my_semantic_error(MySemanticAnalyzer* analyzer, const char* format, ...) {
analyzer->error_count++;
va_list args;
va_start(args, format);
fprintf(stderr, "语义错误: ");
vfprintf(stderr, format, args);
fprintf(stderr, "\n");
va_end(args);
}
// 解析类型说明符 (例如 int, char)
MyType* resolve_my_type_specifier(MyASTNode* type_node) {
if (!type_node || type_node->type != AST_TYPE_SPECIFIER_SIMPLIFIED) {
return my_type_unknown; // 错误或未知类型
}
switch (type_node->data.type_specifier.kind) {
case TYPE_INT: return my_type_int;
case TYPE_CHAR: return my_type_char;
case TYPE_FLOAT: return my_type_float;
case TYPE_VOID: return my_type_void;
default: return my_type_unknown;
}
}
// 模拟分析表达式类型 (这里只处理字面量和标识符)
MyType* analyze_my_expression_type(MyASTNode* expr_node, MySemanticAnalyzer* analyzer) {
if (!expr_node) return my_type_unknown;
switch (expr_node->type) {
case AST_INTEGER_LITERAL_SIMPLIFIED:
expr_node->resolved_type = my_type_int;
return my_type_int;
case AST_STRING_LITERAL_SIMPLIFIED:
// 字符串字面量是 char 数组,这里简化为 char*
// 实际编译器会创建 char[N] 类型,N包含 '\0'
expr_node->resolved_type = create_my_pointer_type(my_type_char);
return expr_node->resolved_type;
case AST_IDENTIFIER_SIMPLIFIED: {
MySymbol* sym = lookup_my_symbol(analyzer->sym_table, expr_node->data.identifier.name);
if (!sym) {
my_semantic_error(analyzer, "未声明的标识符 '%s'。", expr_node->data.identifier.name);
expr_node->resolved_type = my_type_unknown;
return my_type_unknown;
}
expr_node->symbol_entry = sym; // 关联符号表条目
expr_node->resolved_type = sym->type; // 表达式的类型就是符号的类型
return sym->type;
}
default:
my_semantic_error(analyzer, "未知或不支持的表达式类型进行语义分析。");
return my_type_unknown;
}
}
// 分析变量声明
void analyze_my_var_decl(MyASTNode* var_decl_node, MySemanticAnalyzer* analyzer, bool is_global) {
MyType* var_type = resolve_my_type_specifier(var_decl_node->data.var_decl.type_node);
if (var_type == my_type_unknown) {
my_semantic_error(analyzer, "无法解析变量 '%s' 的类型。", var_decl_node->data.var_decl.identifier_node->data.identifier.name);
return;
}
// 字符串字面量初始化数组的特殊处理
if (var_decl_node->data.var_decl.initializer_node &&
var_decl_node->data.var_decl.initializer_node->type == AST_STRING_LITERAL_SIMPLIFIED &&
var_type->kind == TYPE_CHAR) { // char a[] = "hello";
// 自动推断为 char 数组类型,大小为字符串长度 + 1 (for '\0')
size_t string_len = strlen(var_decl_node->data.var_decl.initializer_node->data.string_literal.value);
var_type = create_my_array_type(my_type_char, string_len + 1);
fprintf(stderr, " 推断为字符串数组类型: char[%zu]\n", string_len + 1);
} else if (var_decl_node->data.var_decl.initializer_node &&
var_decl_node->data.var_decl.initializer_node->type == AST_STRING_LITERAL_SIMPLIFIED &&
var_type->kind == TYPE_POINTER && var_type->pointed_type->kind == TYPE_CHAR) { // char* p = "hello";
// 这是一个 char* 指针,指向字符串字面量
// 类型已经是 char*,不需要额外处理
} else if (var_type->kind == TYPE_VOID) {
my_semantic_error(analyzer, "不能声明类型为 'void' 的变量 '%s'。", var_decl_node->data.var_decl.identifier_node->data.identifier.name);
return;
}
char* var_name = var_decl_node->data.var_decl.identifier_node->data.identifier.name;
MySymbol* sym = add_my_symbol(analyzer->sym_table, var_name, SYM_VAR, var_type, is_global);
if (!sym) {
my_semantic_error(analyzer, "变量 '%s' 重复定义。", var_name);
return;
}
var_decl_node->symbol_entry = sym; // 关联到声明节点
var_decl_node->data.var_decl.identifier_node->symbol_entry = sym; // 关联到标识符节点
var_decl_node->resolved_type = var_type; // 声明节点本身也有类型
// 分析初始化表达式的类型 (这里简化为只获取类型,不检查兼容性)
if (var_decl_node->data.var_decl.initializer_node) {
analyze_my_expression_type(var_decl_node->data.var_decl.initializer_node, analyzer);
}
}
// 分析整个程序 (简化版,只处理顶层变量声明)
void analyze_my_program(MyASTNode* program_node, MySemanticAnalyzer* analyzer) {
if (program_node->type != AST_PROGRAM_SIMPLIFIED) {
my_semantic_error(analyzer, "AST根节点不是程序类型。");
return;
}
fprintf(stderr, "开始模拟语义分析...\n");
for (int i = 0; i < program_node->data.program.num_declarations; ++i) {
MyASTNode* decl_node = program_node->data.program.declarations[i];
if (decl_node->type == AST_VAR_DECL_SIMPLIFIED) {
analyze_my_var_decl(decl_node, analyzer, true); // 假设都是全局变量
} else {
my_semantic_error(analyzer, "顶层只能是变量声明 (简化模式)。");
}
}
fprintf(stderr, "模拟语义分析完成,发现 %d 个错误。\n", analyzer->error_count);
}
main.c
// main.c (用于测试所有模块)
#include <stdio.h>
#include <stdlib.h> // For EXIT_SUCCESS, EXIT_FAILURE
// 引入所有演示模块的头文件
#include "my_string_utils.h"
#include "sizeof_demo.h"
#include "memory_layout_demo.h"
#include "string_pitfalls.h"
#include "dynamic_string.h"
#include "array_pointer_decay.h"
#include "compiler_core.h" // 引入模拟编译器核心
int main() {
printf("--- 欢迎来到C语言的底层世界:strlen与sizeof的生死对决!---\n");
printf("--- 揭秘你从未深究的内存真相!---\n");
// 第一部分演示
demo_strlen_behavior();
demo_sizeof_behavior();
demo_variable_sizes_and_addresses();
demo_string_literal_storage();
demo_stack_frame_layout();
// 模拟编译器对 sizeof 的处理
printf("\n\n--- 模拟编译器对 sizeof 的处理 (编译时行为) ---\n");
init_my_types(); // 初始化类型系统
MySymbolTable* sym_table = init_my_symbol_table(); // 初始化符号表
MySemanticAnalyzer* analyzer = init_my_semantic_analyzer(sym_table); // 初始化语义分析器
// 构建一个简化的AST来模拟你的代码
// char a8[] = "hello";
MyASTNode* a8_type = my_ast_new_type_specifier(TYPE_CHAR);
MyASTNode* a8_identifier = my_ast_new_identifier("a8");
MyASTNode* a8_initializer = my_ast_new_string_literal("hello");
MyASTNode* a8_decl = my_ast_new_var_decl(a8_type, a8_identifier, a8_initializer);
// char a9[] = {'h','e','l','l','o','!'};
// 这种初始化方式,编译器不会自动添加 '\0'
MyASTNode* a9_type = my_ast_new_type_specifier(TYPE_CHAR);
MyASTNode* a9_identifier = my_ast_new_identifier("a9");
MyType* a9_explicit_type = create_my_array_type(my_type_char, 6); // 明确指定大小
MyASTNode* a9_decl = my_ast_new_var_decl(a9_type, a9_identifier, NULL);
a9_decl->resolved_type = a9_explicit_type;
a9_decl->data.var_decl.identifier_node->resolved_type = a9_explicit_type;
// char* p = "world";
MyASTNode* p_type = my_ast_new_type_specifier(TYPE_CHAR);
MyASTNode* p_identifier = my_ast_new_identifier("p");
MyASTNode* p_initializer = my_ast_new_string_literal("world");
MyASTNode* p_decl = my_ast_new_var_decl(p_type, p_identifier, p_initializer);
p_decl->resolved_type = create_my_pointer_type(my_type_char);
p_decl->data.var_decl.identifier_node->resolved_type = p_decl->resolved_type;
MyASTNode* program_declarations[] = {a8_decl, a9_decl, p_decl};
MyASTNode* program_ast = my_ast_new_program(program_declarations, 3);
// 执行模拟语义分析
analyze_my_program(program_ast, analyzer);
printf("\n--- 语义分析结果 (模拟 sizeof 行为) ---\n");
if (analyzer->error_count == 0) {
printf("模拟语义分析成功,变量信息已存储在符号表中。\n");
MySymbol* sym_a8 = lookup_my_symbol(sym_table, "a8");
if (sym_a8) {
printf("变量 'a8': ");
print_my_type_info(sym_a8->type);
printf(" 模拟 sizeof(a8) 结果: %zu (从符号表获取类型大小)\n", get_type_size(sym_a8->type));
}
MySymbol* sym_a9 = lookup_my_symbol(sym_table, "a9");
if (sym_a9) {
printf("变量 'a9': ");
print_my_type_info(sym_a9->type);
printf(" 模拟 sizeof(a9) 结果: %zu (从符号表获取类型大小)\n", get_type_size(sym_a9->type));
}
MySymbol* sym_p = lookup_my_symbol(sym_table, "p");
if (sym_p) {
printf("变量 'p': ");
print_my_type_info(sym_p->type);
printf(" 模拟 sizeof(p) 结果: %zu (从符号表获取类型大小)\n", get_type_size(sym_p->type));
}
printf("\n模拟 sizeof(\"hello\") 结果: %zu (编译器直接知道字符串字面量大小)\n", strlen("hello") + 1);
} else {
printf("模拟语义分析发现错误,无法继续。\n");
}
// 释放模拟编译器资源
free_my_ast(program_ast);
free_my_semantic_analyzer(analyzer);
free_my_symbol_table(sym_table);
free_my_types();
// 第二部分演示
demo_strcpy_overflow();
demo_strcat_overflow();
demo_sprintf_overflow();
demo_strncpy_safe();
demo_strncat_safe();
demo_snprintf_safe();
demo_dynamic_string();
demo_array_pointer_decay();
printf("\n--- C语言的底层世界探索完成!希望你有所收获!---\n");
return EXIT_SUCCESS;
}
编译所有文件:
gcc -o c_deep_dive main.c my_string_utils.c sizeof_demo.c memory_layout_demo.c string_pitfalls.c dynamic_string.c array_pointer_decay.c compiler_core.c -Wall -Wextra
运行:
./c_deep_dive
通过我这篇文章,相信你对C语言的内存和字符串有了更深刻的理解,点赞关注收藏走起来!