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语言的底层陷阱,写出更稳定、更安全的代码。

相关文章
|
6天前
|
自然语言处理 物联网 测试技术
大模型应用:中小显存适配方案:大模型微调底座选型指标与应用实现.52
本文聚焦8G显存中小算力场景,以中文电商文案生成为例,提出大模型微调底座选型体系:围绕任务适配性、算力匹配度、生态成熟度等六大核心指标,通过“需求分析—筛选排除—打分排序—验证落地”四步流程,结合量化加载、LoRA微调与实测对比(如ChatGLM-6B vs Qwen-7B),为开发者提供可复用、可落地的精准选型方案。
116 8
|
9天前
|
人工智能 监控 安全
为阿里云“养虾人”装上安全护栏:JEP Guard 插件开发实践
OpenClaw在阿里云上一键部署量激增,但其高风险权限带来误删、隐私泄露等隐患。JEP Guard开源插件应运而生,通过拦截rm等危险命令、用户确认弹窗、临时授权令牌及JEP协议密码学收据,为AI执行操作提供“安全护栏”。本文详解插件设计、代码实现及阿里云部署实践,助力开发者构建安全可控的智能体环境。
285 13
|
12天前
|
存储 安全 C语言
C语言深度解析:函数指针的底层本质与避坑指南
本文深入剖析C语言函数指针的本质——函数名即代码段入口地址,厘清其与数据指针的根本差异;系统梳理回调、跳转表、中断向量、动态库等核心应用场景;重点警示签名不匹配、`void*`强转、野指针调用三大致命陷阱,并给出`typedef`封装、空值校验、边界防护等最佳实践。(239字)
336 134
|
16天前
|
网络协议 编译器 C语言
C语言深度解析:内存对齐与结构体填充的底层逻辑
C语言中,内存对齐是CPU硬件强制要求的底层规则,直接影响结构体大小、访问性能与硬件兼容性。合理排列成员可减少填充、节省内存;滥用`#pragma pack`则易致崩溃或性能暴跌。嵌入式、网络协议与跨平台开发必备核心知识。(239字)
171 14
|
22天前
|
存储 编译器 程序员
C语言核心剖析:堆与栈的本质差异及避坑指南
C语言中,栈与堆是内存管理的两大核心区域:栈由编译器自动管理,高效但易栈溢出;堆由程序员手动管理,灵活却易致内存泄漏、野指针等陷阱。本文深入剖析二者本质差异与典型风险,助你夯实底层基础。
409 11
|
8天前
|
存储 安全 编译器
C语言深度解析:变长数组(VLA)的底层逻辑与避坑指南
变长数组(VLA)是C99引入的栈上动态数组,长度运行时确定,访问快但无安全检查。易致栈溢出、野指针、跨平台兼容问题,仅适用于小尺寸、短生命周期场景,大数组务必用malloc。
164 38
|
10天前
|
存储 C语言 内存技术
C语言深度解析:大小端字节序——多字节数据的底层存储规则
大小端指CPU对多字节数据在内存中的存放顺序:大端高字节存低地址,小端反之。x86/ARM默认小端,网络字节序统一为大端。跨平台、网络通信、二进制协议开发中必须显式处理字节序转换,否则数据解析必错。
433 138
|
20天前
|
存储 缓存 安全
C语言深度解析:volatile 关键字——编译器优化的「禁区」
`volatile`是C语言中被严重低估却至关重要的关键字:它不改变存储位置,而是强制编译器禁用优化,确保每次访问都直读/写内存——用于硬件寄存器、中断变量、多线程共享数据等场景,是嵌入式与驱动开发正确性的基石。(239字)
|
8天前
|
人工智能 安全 Java
给“氛围编程”系上安全带:阿里集团 AI 代码评审实践与 Benchmark 开源
阿里集团历时一年半、经数万亿Token真实场景打磨,推出AI代码评审助手,实现人机协作新范式:AI接管基础评审,人类聚焦核心风险。联合南京大学开源业界首个支持10语言、具备仓库级上下文感知的CodeReview Benchmark(AACR-Bench),由80+资深工程师多轮交叉标注,显著提升隐性缺陷检出率。
给“氛围编程”系上安全带:阿里集团 AI 代码评审实践与 Benchmark 开源
|
20天前
|
运维 API 调度
中国企业级大模型市场,阿里千问占比32%位列第一!
沙利文报告指出,2025年下半年中国企业级大模型日均调用量达37.0万亿tokens,千问(Qwen)占比32.1%,近乎翻倍,稳居第一。企业应用动因转向提效降本,开源意愿显著增强,千问已开源400+模型,下载超10亿次,成全球第一开源大模型。

热门文章

最新文章