C语言高级编程技巧
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 语句会维护一张跳转表,是典型的空间换时间策略。当分支较多且为离散值时,switch 比 if-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 核心原则
- malloc 分配的内存需要手动释放,函数结束也不会自动释放
- free 后的内存内容不变,但操作它会导致未定义行为
- 堆内存越界可能在
free时才报错,难以定位 - 访问非法内存不会立即报错,可能在后续某条指令才崩溃
11.2 内存调试建议
当程序出现莫名其妙的崩溃时:
- 检查所有涉及内存操作的代码
- 排查指针是否越界、是否使用已释放的内存
- 使用调试工具(如 Valgrind)检测内存问题
总结
C语言的高级编程技巧涵盖了性能优化、内存安全、代码架构等多个层面。掌握这些技巧不仅能让你的代码更加高效和安全,还能帮助你更好地理解计算机系统的底层运作机制。
在实际开发中,请牢记以下原则:
- 性能优化要有理有据,优先优化热点代码
- 内存管理要严格,malloc 与 free 配对使用,free 后置 NULL
- 善用回调函数和函数指针实现灵活的代码架构
- 利用
do-while(0)等模式简化错误处理流程 - 重视跨平台兼容性,编写可移植的代码
原始笔记来源: frasight/王者归来笔记.c, jdah/StudyC.c
更多推荐


所有评论(0)