C语言高频错误实例对比:8段代码帮你避开90%的坑

简介: 本文精选8组典型C语言错误与正确代码对比,直击数组越界、字符串溢出、野指针、内存泄漏、有无符号混用、返回局部地址、sizeof误用、未定义行为等高频陷阱,以实例培养安全编码直觉。(239字)

很多C语言的bug,不是因为算法复杂,而是因为忽略了最基础的语法细节和底层规则。本文用8组错误代码 vs 正确代码的直观对比,直击高频踩坑现场,用实例帮你建立安全编码的直觉。


1. 数组越界:差一个索引,毁一片内存

错误代码

#include <stdio.h>
int main() {
   
    int arr[5] = {
   1,2,3,4,5};
    // 致命错误:数组下标从0开始,最大为4
    for (int i = 0; i <= 5; i++) {
   
        printf("%d ", arr[i]); // arr[5]越界,破坏栈上其他数据
    }
    return 0;
}

坑点分析
C语言不做任何数组边界检查。arr[5]访问的是数组后方的未知栈内存,轻则打印乱码,重则修改函数返回地址,导致程序直接崩溃。

正确代码

#include <stdio.h>
int main() {
   
    int arr[5] = {
   1,2,3,4,5};
    // 安全边界:i < 数组长度
    for (int i = 0; i < 5; i++) {
   
        printf("%d ", arr[i]);
    }
    return 0;
}

2. 字符串溢出:看不见的\0才是杀手

错误代码

#include <stdio.h>
#include <string.h>
int main() {
   
    char str[5];
    strcpy(str, "hello"); // 致命错误:"hello"含\0共需6字节
    printf("%s\n", str);
    return 0;
}

坑点分析
C语言字符串以\0(ASCII码0)结尾。"hello"看似5个字符,实际占用6字节内存。溢出的\0会覆盖相邻内存,导致缓冲区溢出漏洞。

正确代码

#include <stdio.h>
#include <string.h>
int main() {
   
    char str[6]; // 预留\0的位置
    strcpy(str, "hello");
    // 更安全的写法:指定最大拷贝长度
    // strncpy(str, "hello", sizeof(str)-1);
    // str[sizeof(str)-1] = '\0'; // 手动确保结束
    printf("%s\n", str);
    return 0;
}

3. 野指针与空指针:未判空,不使用

错误代码

#include <stdio.h>
void print_val(int* p) {
   
    // 致命错误:未判空直接解引用
    printf("%d\n", *p);
}

int main() {
   
    int* p = NULL;
    print_val(p); // 传入空指针
    return 0;
}

坑点分析
NULL解引用,几乎必然触发段错误(Segmentation Fault),导致程序直接崩溃。任何指针在使用前,必须先判空。

正确代码

#include <stdio.h>
void print_val(int* p) {
   
    if (p != NULL) {
    // 先判空,后使用
        printf("%d\n", *p);
    } else {
   
        printf("空指针,无法访问\n");
    }
}

int main() {
   
    int val = 10;
    int* p = &val;
    print_val(p);
    return 0;
}

4. 内存泄漏与重复释放:成对管理,释放即置空

错误代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct User {
   
    char* name;
};

int main() {
   
    struct User* u = malloc(sizeof(struct User));
    u->name = malloc(10);
    strcpy(u->name, "test");

    free(u); // 致命错误1:只释放了结构体,没释放name指向的内存(内存泄漏)
    free(u); // 致命错误2:重复释放同一块内存(未定义行为)
    return 0;
}

坑点分析

  1. 只释放结构体外壳,内部指针指向的堆内存会永远丢失,造成内存泄漏;
  2. 重复释放同一块内存,会直接破坏堆内存管理结构,导致程序崩溃。

正确代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct User {
   
    char* name;
};

int main() {
   
    struct User* u = malloc(sizeof(struct User));
    if (u == NULL) return -1;

    u->name = malloc(10);
    if (u->name == NULL) {
   
        free(u); // 分配失败,记得释放已分配的内存
        u = NULL;
        return -1;
    }
    strcpy(u->name, "test");

    // 释放顺序:先内后外
    free(u->name);
    u->name = NULL; // 释放后立即置空
    free(u);
    u = NULL; // 释放后立即置空
    return 0;
}

5. 有符号与无符号比较:负数变极大值的陷阱

错误代码

#include <stdio.h>
#include <string.h>
int main() {
   
    char* s = "";
    // 致命错误:strlen返回size_t(无符号),0-1变成极大值,死循环
    for (int i = 0; i < strlen(s) - 1; i++) {
   
        printf("这行永远不会执行,但循环永远不会停\n");
    }
    return 0;
}

坑点分析
s为空字符串时,strlen(s) = 00 - 1在无符号运算中会变成0xFFFFFFFF(极大值),循环条件永远成立,导致死循环。

正确代码

#include <stdio.h>
#include <string.h>
int main() {
   
    char* s = "";
    size_t len = strlen(s);
    // 先判断长度,避免减法溢出;或显式转为有符号
    if (len > 1) {
   
        for (int i = 0; i < (int)len - 1; i++) {
   
            printf("安全执行\n");
        }
    }
    return 0;
}

6. 返回局部变量地址:栈帧销毁,地址失效

错误代码

#include <stdio.h>
int* get_val() {
   
    int a = 10;
    return &a; // 致命错误:返回局部变量地址
}

int main() {
   
    int* p = get_val();
    printf("%d\n", *p); // 解引用已失效的栈地址,结果不可控
    return 0;
}

坑点分析
局部变量a存储在栈上,get_val函数返回后,栈帧立即被销毁,&a指向的内存会被后续函数调用覆盖,解引用会读到垃圾值,甚至导致崩溃。

正确代码

#include <stdio.h>
#include <stdlib.h>

// 方案1:返回堆内存地址(调用者需负责free)
int* get_val_heap() {
   
    int* p = malloc(sizeof(int));
    if (p != NULL) {
   
        *p = 10;
    }
    return p;
}

// 方案2:使用static(线程不安全,仅适用于单线程)
int* get_val_static() {
   
    static int a = 10; // 静态变量存储在静态数据区,生命周期全程有效
    return &a;
}

int main() {
   
    int* p1 = get_val_heap();
    if (p1 != NULL) {
   
        printf("%d\n", *p1);
        free(p1);
        p1 = NULL;
    }
    return 0;
}

7. sizeof的单位陷阱:分配内存,勿忘乘类型大小

错误代码

#include <stdio.h>
#include <stdlib.h>
int main() {
   
    int* arr = malloc(5); // 致命错误:只分配了5字节,而非5个int
    for (int i = 0; i < 5; i++) {
   
        arr[i] = i; // 越界访问,触发未定义行为
    }
    free(arr);
    return 0;
}

坑点分析
malloc的参数是字节数。32位系统下int占4字节,存储5个int需要20字节,只分配5字节会导致严重的越界。

正确代码

#include <stdio.h>
#include <stdlib.h>
int main() {
   
    int* arr = malloc(5 * sizeof(int)); // 正确:元素个数 * 单个元素大小
    // 更健壮的写法:不依赖具体类型名
    // int* arr = malloc(5 * sizeof(*arr));
    if (arr == NULL) return -1;

    for (int i = 0; i < 5; i++) {
   
        arr[i] = i;
        printf("%d ", arr[i]);
    }
    free(arr);
    arr = NULL;
    return 0;
}

8. 序列点与未定义行为:一个表达式,变量只改一次

错误代码

#include <stdio.h>
int main() {
   
    int i = 0;
    // 致命错误:两个序列点之间,i被修改多次,结果完全不可控
    printf("%d %d\n", i++, i++);
    i = i++ + ++i; // 同样是未定义行为
    return 0;
}

坑点分析
C标准只保证序列点(如分号、&&左操作数后)的执行顺序。两个序列点之间,同一个变量被修改多次,编译器可以任意安排执行顺序,结果完全不可预测。

正确代码

#include <stdio.h>
int main() {
   
    int i = 0;
    // 拆分成独立语句,用临时变量保存结果,时序完全可控
    int t1 = i++;
    int t2 = i++;
    printf("%d %d\n", t1, t2); // 输出0 1

    i = 0;
    int a = i++;
    int b = ++i;
    i = a + b;
    printf("i = %d\n", i); // 输出2
    return 0;
}

总结

这8个实例覆盖了C语言开发中90%的高频低级错误。核心避坑原则可以总结为5句话:

  1. 数组下标不越界,字符串预留\0
  2. 指针未判空,绝不解引用
  3. malloc与free成对出现,释放后立即置空
  4. 有符号无符号不混用,返回值不碰局部栈
  5. 一个表达式,同一个变量最多修改一次

时刻牢记这5条原则,你就能避开绝大多数C语言的底层陷阱,写出更稳定、更安全的代码。

相关文章
|
5月前
|
缓存 编译器 程序员
C语言深度解析:restrict关键字——编译器性能优化的终极钥匙
C99的`restrict`关键字是C语言性能优化的“终极钥匙”:它向编译器承诺指针独占访问内存,彻底解决同类型指针别名问题,解锁循环向量化、寄存器缓存等激进优化。滥用致未定义行为,善用则性能飙升数倍——这才是真正高阶C程序员的必修课。(239字)
|
5月前
|
存储 安全 C语言
C语言深度解析:函数指针的底层本质与避坑指南
本文深入剖析C语言函数指针的本质——函数名即代码段入口地址,厘清其与数据指针的根本差异;系统梳理回调、跳转表、中断向量、动态库等核心应用场景;重点警示签名不匹配、`void*`强转、野指针调用三大致命陷阱,并给出`typedef`封装、空值校验、边界防护等最佳实践。(239字)
757 134
|
6月前
|
存储 缓存 安全
C语言深度解析:volatile 关键字——编译器优化的「禁区」
`volatile`是C语言中被严重低估却至关重要的关键字:它不改变存储位置,而是强制编译器禁用优化,确保每次访问都直读/写内存——用于硬件寄存器、中断变量、多线程共享数据等场景,是嵌入式与驱动开发正确性的基石。(239字)
1035 3
|
5月前
|
安全 编译器 C语言
C语言「宏的暗门」:预处理阶段的隐形篡改与避坑守则
宏是C语言预处理阶段的纯文本替换工具,无类型、无作用域、不检语法,易引发括号缺失、副作用、分号错误、类型混乱和命名污染等六大陷阱。安全使用须严守括号规范、避免参数复用、善用`do{...}while(0)`、优先选用内联函数,并及时`#undef`。(239字)
394 10
|
5月前
|
存储 网络协议 安全
C语言「内存对齐潜规则」:结构体里看不见的填充字节
内存对齐是CPU硬件要求的数据地址约束规则:变量须存于其字节大小的整数倍地址。编译器自动插入填充字节确保对齐,导致结构体体积“膨胀”、硬件寄存器读写错位或协议异常。合理排序成员(从大到小)、慎用`packed`、明确对齐控制,是嵌入式与底层开发的关键避坑要点。(239字)
|
5月前
|
人工智能 安全 编译器
C语言的「隐形时序契约」:序列点、副作用与求值顺序终极拆解
本文深入解析C语言中极易被忽视的“序列点”机制,揭示Debug/Release模式差异、跨平台结果不一致等玄学bug的根源——未定义行为(UB)。从副作用定义出发,系统梳理7类标准序列点,剖析4大高频陷阱(如`i=i++ + ++i`),并提供6条安全编码铁律,助你写出稳定、可移植的C代码。(239字)
445 11
|
5月前
|
存储 网络协议 安全
C语言深度解析:位域的底层逻辑与跨平台避坑指南
位域是C语言专为bit级内存操控设计的特性,用于嵌入式寄存器映射、协议解析等场景,可大幅节省内存。但其布局受编译器、字节序、对齐规则影响,存在有符号陷阱、跨平台不一致、不可取地址等风险。慎用,优先选unsigned int,跨平台务必改用位运算。
|
5月前
|
缓存 网络协议 编译器
C语言深度解析:柔性数组的底层本质与最优实践
C99柔性数组(`char data[]`)是处理变长结构体的原生方案:结构体末尾声明,不占`sizeof`空间,与数据共享连续内存。相比指针方案,它避免内存碎片、序列化困难和缓存失效,仅需一次分配/释放。但须注意——仅限堆分配、必有前置成员、不可栈上定义、禁用直接赋值。
|
5月前
|
存储 C语言 内存技术
C语言深度解析:大小端字节序——多字节数据的底层存储规则
大小端指CPU对多字节数据在内存中的存放顺序:大端高字节存低地址,小端反之。x86/ARM默认小端,网络字节序统一为大端。跨平台、网络通信、二进制协议开发中必须显式处理字节序转换,否则数据解析必错。
1118 138
|
5月前
|
存储 安全 编译器
C语言深度解析:变长数组(VLA)的底层逻辑与避坑指南
变长数组(VLA)是C99引入的栈上动态数组,长度运行时确定,访问快但无安全检查。易致栈溢出、野指针、跨平台兼容问题,仅适用于小尺寸、短生命周期场景,大数组务必用malloc。
647 38