C语言高级编程技巧

简介

掌握了C语言的基础知识后,如何写出高效、安全、可维护的代码?本文从代码优化、函数指针、内存管理、回调函数、Windows编程等多个高级主题出发,结合实战经验,分享C语言进阶编程的核心技巧。

一、代码优化技巧

1.1 CPU与位运算

计算机对位操作的执行速度最快,因为CPU本身就是专门执行位运算的器件。在性能敏感场景中,应尽量将乘法和除法转换为位运算和加法。

// 性能对比:
// 除法指令:约 50 个机器周期
// 位运算/加法:约 1-2 个机器周期

x << n;        // 等价于 x * 2^n
x >> n;        // 等价于 x / 2^n(正数)
12 * 5;        // 等价于 (12 << 2) + 12

// 清零操作
a ^ a;         // 结果为 0

1.2 数学公式替代循环

// 优化前:循环求和
int sum = 0;
for (int i = 1; i <= n; i++) sum += i;

// 优化后:高斯定理
int sum = n * (n + 1) / 2;

1.3 表达式优化

x = x + 1;     // 访问两次内存
x += 1;        // 访问一次内存,更高效

1.4 if-else 分支顺序优化

在多重 if-else 判断中,将概率最高的条件放在最前面,可以减少平均判断次数。

// 统计英文文档中字母数、空格数、数字数
// 优化:字母出现概率最高,先判断字母
if (isalpha(ch)) {
    letterCount++;
} else if (isdigit(ch)) {
    digitCount++;
} else if (isspace(ch)) {
    spaceCount++;
}

1.5 循环内不变量外提

// 优化前:每次循环都计算 strlen
for (i = 0; i < strlen(buf); i++) { /* ... */ }

// 优化后:
const int len = strlen(buf);
for (i = 0; i < len; i++) { /* ... */ }

1.6 switch 的空间换时间

switch 语句会维护一张跳转表,是典型的空间换时间策略。当分支较多且为离散值时,switchif-else 链效率更高。

注意:switch 不能判断范围,此时只能使用 if-else

二、函数指针

2.1 函数指针基础

函数名的本质是一个标号,其值是存储该函数代码的内存空间首地址——函数名是一个函数指针常量,类似于数组名。

int add(int a, int b) {
    return a + b;
}

int main(void) {
    int (*fPtr)(int, int);  // 定义函数指针
    fPtr = add;             // 赋值,函数名就是地址

    printf("%d\n", fPtr(2, 5));     // 调用方式1
    printf("%d\n", (*fPtr)(2, 5));  // 调用方式2
    return 0;
}

2.2 函数指针的用途

  • 回调函数:将函数指针作为参数传递
  • 策略模式:运行时切换不同的处理逻辑
  • 函数表:用数组存储函数指针,通过索引调用
// 函数表实现命令分发
typedef void (*CommandFunc)(void);
CommandFunc commands[] = {cmd_open, cmd_close, cmd_save, cmd_exit};

void execute(int cmd_id) {
    if (cmd_id >= 0 && cmd_id < 4) {
        commands[cmd_id]();
    }
}

三、内存拷贝与重叠问题

3.1 自实现 memcpy

实现 memcpy 时必须考虑源地址和目标地址重叠的情况:

void* my_memcpy(void *dest, const void *src, size_t count) {
    char *pdest = (char*)dest;
    const char *psrc = (const char*)src;

    if (pdest > psrc && pdest < psrc + count) {
        // 地址重叠,从后向前拷贝
        for (size_t i = count - 1; i != (size_t)-1; --i)
            pdest[i] = psrc[i];
    } else {
        // 正常情况,从前向后拷贝
        for (size_t i = 0; i < count; ++i)
            pdest[i] = psrc[i];
    }
    return dest;
}

3.2 memmove vs memcpy

// memmove:能正确处理重叠区域
// memcpy:不保证重叠区域的正确性(参数加 restrict 限定)
// 原则:不确定是否重叠时,用 memmove

3.3 memmove_s(安全版本)

// memmove_s 增加了目标缓冲区大小参数,防止越界
errno_t memmove_s(void *dest, size_t destCount, const void *src, size_t count);

四、可变参数函数

4.1 可变参数函数的编写

使用 <stdarg.h> 中提供的宏来实现:

#include <stdarg.h>
#include <stdio.h>

int mon_log(char* format, ...) {
    char str_tmp[128];
    va_list vArgList;

    va_start(vArgList, format);
    int i = vsnprintf(str_tmp, sizeof(str_tmp), format, vArgList);
    va_end(vArgList);

    printf("%s\n", str_tmp);
    return i;
}

// 调用
int main(void) {
    int i = mon_log("%s,%d,%d,%d", "test", 2, 3, 4);
    printf("写入字符数:%d\n", i);
    return 0;
}

4.2 四个关键宏

功能
va_list 定义可变参数列表的指针
va_start 初始化可变参数列表
va_arg 逐个获取可变参数
va_end 清理可变参数列表

4.3 格式化函数族

sprintf(char *buf, const char *fmt, ...);        // 输出到字符串
fprintf(FILE *fp, const char *fmt, ...);          // 输出到文件
vsnprintf(char *buf, size_t n, const char *fmt, va_list ap);  // 可变参数版本

五、NULL指针与野指针

5.1 NULL 指针

// C 中的定义
#define NULL ((void*)0)
// C++ 中的定义
#define NULL 0

// 操作 NULL 指针会导致段错误
// 但 free(NULL) 是安全的,不会出错
int *p = NULL;
free(p);  // 安全

5.2 野指针的产生

野指针的产生有两种主要途径:

1. 指针未初始化

int *p;           // 未初始化,指向随机地址
*p = 10;          // 危险!未定义行为

// 正确做法
int *p = NULL;    // 初始化为 NULL

2. free/delete 后未置 NULL

int *p = (int*)malloc(sizeof(int) * 10);
free(p);
// p 此时成为野指针,指向的内存已被释放
// 但 p 的值并不为 NULL

// 正确做法
free(p);
p = NULL;

5.3 malloc 与 calloc 的区别

// calloc = malloc + memset(将内存初始化为零)
int *p = (int*)calloc(10, sizeof(int));  // 分配并清零
int *q = (int*)malloc(10 * sizeof(int)); // 仅分配,内容不确定

// 注意:calloc 不保证内存中字符串长度为 0
// char *p = calloc(1, 0); strlen(p) 的结果不确定

六、宏定义与 do-while(0) 模式

6.1 宏的注意事项

  • 宏只是简单的文本替换,不做类型检查
  • 宏参数要加括号,防止优先级问题
  • 多行宏使用 do { ... } while(0) 包裹

6.2 do-while(0) 的妙用

在C语言函数中,中途 return 容易忘记释放资源。而嵌套 if-else 又显得累赘。do-while(0) 提供了优雅的解决方案:

// 问题代码:嵌套过深
ret = func1();
if (ret == 0) {
    ret = func2();
    if (ret == 0) {
        ret = func3();
        // ...
    }
}

// 优化:使用 do-while(0)
do {
    ret = func1();
    if (ret != 0) break;

    ret = func2();
    if (ret != 0) break;

    ret = func3();
    if (ret != 0) break;

    // 成功逻辑
} while (0);

// 统一的资源清理代码在这里执行

6.3 调试宏

#define DEBUG 1

#ifdef DEBUG
    #define LOG(fmt, ...) printf("[DEBUG] " fmt "\n", ##__VA_ARGS__)
#else
    #define LOG(fmt, ...)
#endif

七、回调函数

7.1 回调函数的原理

回调函数是通过函数指针调用的函数,将函数作为参数传递给另一个函数,在适当的时候被调用。

7.2 异步场景中的回调

在网络编程中,异步发送消息后需要得到对方的响应。由于发送函数内部无法直接获取结果,可以传入回调函数:

// 定义回调函数类型
typedef void (*CallbackFunc)(void *context, int result);

// 注册回调
void async_send(const char *msg, CallbackFunc callback, void *context) {
    // 发送消息...
    // 异步接收响应后调用回调
    callback(context, 0);  // 0 表示成功
}

// 使用示例
void on_response(void *ctx, int result) {
    printf("收到响应,结果:%d\n", result);
}

async_send("Hello", on_response, my_context);

八、用C语言实现面向对象

8.1 隐藏数据接口

利用前向声明实现封装:

// conceal_data_type.h(头文件)
typedef struct conceal_data_type conceal_data_type_t;

extern int conceal_data_type_get_a(concel_data_type_t *obj);
extern void conceal_data_type_set_a(concel_data_type_t *obj, int val);
// conceal_data_type.c(源文件)
struct conceal_data_type {
    int a;
    // 其他私有成员...
};

int conceal_data_type_get_a(concel_data_type_t *obj) {
    return obj->a;
}

外部文件只能通过接口函数访问结构体成员,无法直接操作成员变量。

8.2 用C语言实现继承与多态

// 父类
struct parent {
    int dat1;
    int dat2;
};

// 子类"继承"父类(将父类作为第一个成员)
struct child {
    struct parent par;  // 必须是第一个成员
    int dat3;
    int dat4;
};

// 多态:父类指针可以指向子类对象
struct child *ch = calloc(1, sizeof(struct child));
struct parent *par = (struct parent*)ch;
// par->dat1 等价于 ch->par.dat1

8.3 零长数组(柔性数组)

struct mutable_array {
    int len;
    char data[0];  // GNU 扩展:零长数组
};

// sizeof(struct mutable_array) == 4(32位系统)
struct mutable_array *p = malloc(sizeof(struct mutable_array) + 1024);
p->len = 1024;
memcpy(p->data, "Hello", 6);

九、C语言中的奇特但有用的语法

9.1 字符串字面值拼接

const char *str = "My" "Name" "is Zhouwy";
// 编译器自动拼接,str = "MyNameis Zhouwy"

9.2 结构体指定成员初始化

struct structTest {
    int a;
    int b;
    int c;
};

struct structTest var = {
    .a = 10,
    .b = 24,
    .c = 56
};

9.3 数组指定下标初始化

int arr[10] = { [4] = 67, [5] = 34 };
// arr[4] = 67, arr[5] = 34, 其余为 0

十、Windows C编程要点

10.1 临界区(CRITICAL_SECTION)

CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);    // 初始化

EnterCriticalSection(&cs);         // 进入临界区
// ... 访问临界资源 ...
LeaveCriticalSection(&cs);         // 离开临界区

DeleteCriticalSection(&cs);        // 删除临界区

10.2 事件对象(Event)

HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);  // 创建事件
SetEvent(hEvent);                // 设置为有信号
WaitForSingleObject(hEvent, INFINITE);  // 等待信号
ResetEvent(hEvent);              // 重置为无信号

10.3 互斥对象(Mutex)

HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);  // 创建互斥锁
WaitForSingleObject(hMutex, INFINITE);           // 等待锁
ReleaseMutex(hMutex);                            // 释放锁

10.4 Windows 进程间通信

方式 特点
邮槽(Mailslot) 半双工,类似匿名管道
命名管道 面向连接,可靠传输,支持跨机器通信
匿名管道 仅限父子进程间通信

10.5 Windows 异常处理

__try {
    // 可能出错的代码
    int *p = NULL;
    *p = 10;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
    // 异常处理
    printf("捕获到异常\n");
}

__except 参数的含义:

  • EXCEPTION_CONTINUE_EXECUTION (-1):忽略异常,继续执行
  • EXCEPTION_CONTINUE_SEARCH (0):不处理,继续寻找上层处理
  • EXCEPTION_EXECUTE_HANDLER (1):识别异常,执行处理代码

10.6 端口复用

int opt = 1;
// 在 bind 之前调用
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt, sizeof(opt));

端口复用的主要用途:防止服务器重启时之前绑定的端口还未释放,或程序异常退出时系统未释放端口。

注意:多个套接字绑定同一端口时,只有最后一个套接字能正常接收数据。

十一、内存安全编程原则

11.1 核心原则

  1. malloc 分配的内存需要手动释放,函数结束也不会自动释放
  2. free 后的内存内容不变,但操作它会导致未定义行为
  3. 堆内存越界可能在 free 时才报错,难以定位
  4. 访问非法内存不会立即报错,可能在后续某条指令才崩溃

11.2 内存调试建议

当程序出现莫名其妙的崩溃时:

  1. 检查所有涉及内存操作的代码
  2. 排查指针是否越界、是否使用已释放的内存
  3. 使用调试工具(如 Valgrind)检测内存问题

总结

C语言的高级编程技巧涵盖了性能优化、内存安全、代码架构等多个层面。掌握这些技巧不仅能让你的代码更加高效和安全,还能帮助你更好地理解计算机系统的底层运作机制。

在实际开发中,请牢记以下原则:

  • 性能优化要有理有据,优先优化热点代码
  • 内存管理要严格,malloc 与 free 配对使用,free 后置 NULL
  • 善用回调函数和函数指针实现灵活的代码架构
  • 利用 do-while(0) 等模式简化错误处理流程
  • 重视跨平台兼容性,编写可移植的代码

原始笔记来源: frasight/王者归来笔记.c, jdah/StudyC.c

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐