C语言高级编程技巧
简介
掌握了C语言的基础知识后,如何写出高效、安全、可维护的代码?本文从代码优化、函数指针、内存管理、回调函数、Windows编程等多个高级主题出发,结合实战经验,分享C语言进阶编程的核心技巧。
一、代码优化技巧
1.1 CPU与位运算
计算机对位操作的执行速度最快,因为CPU本身就是专门执行位运算的器件。在性能敏感场景中,应尽量将乘法和除法转换为位运算和加法。
// 性能对比:// 除法指令:约 50 个机器周期// 位运算/加法:约 1-2 个机器周期x<<n;// 等价于 x * 2^nx>>n;// 等价于 x / 2^n(正数)12*5;// 等价于 (12 << 2) + 12// 清零操作a^a;// 结果为 01.2 数学公式替代循环
// 优化前:循环求和intsum=0;for(inti=1;i<=n;i++)sum+=i;// 优化后:高斯定理intsum=n*(n+1)/2;1.3 表达式优化
x=x+1;// 访问两次内存x+=1;// 访问一次内存,更高效1.4 if-else 分支顺序优化
在多重 if-else 判断中,将概率最高的条件放在最前面,可以减少平均判断次数。
// 统计英文文档中字母数、空格数、数字数// 优化:字母出现概率最高,先判断字母if(isalpha(ch)){letterCount++;}elseif(isdigit(ch)){digitCount++;}elseif(isspace(ch)){spaceCount++;}1.5 循环内不变量外提
// 优化前:每次循环都计算 strlenfor(i=0;i<strlen(buf);i++){/* ... */}// 优化后:constintlen=strlen(buf);for(i=0;i<len;i++){/* ... */}1.6 switch 的空间换时间
switch语句会维护一张跳转表,是典型的空间换时间策略。当分支较多且为离散值时,switch比if-else链效率更高。
注意:switch不能判断范围,此时只能使用if-else。
二、函数指针
2.1 函数指针基础
函数名的本质是一个标号,其值是存储该函数代码的内存空间首地址——函数名是一个函数指针常量,类似于数组名。
intadd(inta,intb){returna+b;}intmain(void){int(*fPtr)(int,int);// 定义函数指针fPtr=add;// 赋值,函数名就是地址printf("%d\n",fPtr(2,5));// 调用方式1printf("%d\n",(*fPtr)(2,5));// 调用方式2return0;}2.2 函数指针的用途
- 回调函数:将函数指针作为参数传递
- 策略模式:运行时切换不同的处理逻辑
- 函数表:用数组存储函数指针,通过索引调用
// 函数表实现命令分发typedefvoid(*CommandFunc)(void);CommandFunc commands[]={cmd_open,cmd_close,cmd_save,cmd_exit};voidexecute(intcmd_id){if(cmd_id>=0&&cmd_id<4){commands[cmd_id]();}}三、内存拷贝与重叠问题
3.1 自实现 memcpy
实现memcpy时必须考虑源地址和目标地址重叠的情况:
void*my_memcpy(void*dest,constvoid*src,size_tcount){char*pdest=(char*)dest;constchar*psrc=(constchar*)src;if(pdest>psrc&&pdest<psrc+count){// 地址重叠,从后向前拷贝for(size_ti=count-1;i!=(size_t)-1;--i)pdest[i]=psrc[i];}else{// 正常情况,从前向后拷贝for(size_ti=0;i<count;++i)pdest[i]=psrc[i];}returndest;}3.2 memmove vs memcpy
// memmove:能正确处理重叠区域// memcpy:不保证重叠区域的正确性(参数加 restrict 限定)// 原则:不确定是否重叠时,用 memmove3.3 memmove_s(安全版本)
// memmove_s 增加了目标缓冲区大小参数,防止越界errno_tmemmove_s(void*dest,size_tdestCount,constvoid*src,size_tcount);四、可变参数函数
4.1 可变参数函数的编写
使用<stdarg.h>中提供的宏来实现:
#include<stdarg.h>#include<stdio.h>intmon_log(char*format,...){charstr_tmp[128];va_list vArgList;va_start(vArgList,format);inti=vsnprintf(str_tmp,sizeof(str_tmp),format,vArgList);va_end(vArgList);printf("%s\n",str_tmp);returni;}// 调用intmain(void){inti=mon_log("%s,%d,%d,%d","test",2,3,4);printf("写入字符数:%d\n",i);return0;}4.2 四个关键宏
| 宏 | 功能 |
|---|---|
va_list | 定义可变参数列表的指针 |
va_start | 初始化可变参数列表 |
va_arg | 逐个获取可变参数 |
va_end | 清理可变参数列表 |
4.3 格式化函数族
sprintf(char*buf,constchar*fmt,...);// 输出到字符串fprintf(FILE*fp,constchar*fmt,...);// 输出到文件vsnprintf(char*buf,size_tn,constchar*fmt,va_list ap);// 可变参数版本五、NULL指针与野指针
5.1 NULL 指针
// C 中的定义#defineNULL((void*)0)// C++ 中的定义#defineNULL0// 操作 NULL 指针会导致段错误// 但 free(NULL) 是安全的,不会出错int*p=NULL;free(p);// 安全5.2 野指针的产生
野指针的产生有两种主要途径:
1. 指针未初始化
int*p;// 未初始化,指向随机地址*p=10;// 危险!未定义行为// 正确做法int*p=NULL;// 初始化为 NULL2. 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 调试宏
#defineDEBUG1#ifdefDEBUG#defineLOG(fmt,...)printf("[DEBUG] "fmt"\n",##__VA_ARGS__)#else#defineLOG(fmt,...)#endif七、回调函数
7.1 回调函数的原理
回调函数是通过函数指针调用的函数,将函数作为参数传递给另一个函数,在适当的时候被调用。
7.2 异步场景中的回调
在网络编程中,异步发送消息后需要得到对方的响应。由于发送函数内部无法直接获取结果,可以传入回调函数:
// 定义回调函数类型typedefvoid(*CallbackFunc)(void*context,intresult);// 注册回调voidasync_send(constchar*msg,CallbackFunc callback,void*context){// 发送消息...// 异步接收响应后调用回调callback(context,0);// 0 表示成功}// 使用示例voidon_response(void*ctx,intresult){printf("收到响应,结果:%d\n",result);}async_send("Hello",on_response,my_context);八、用C语言实现面向对象
8.1 隐藏数据接口
利用前向声明实现封装:
// conceal_data_type.h(头文件)typedefstructconceal_data_typeconceal_data_type_t;externintconceal_data_type_get_a(concel_data_type_t*obj);externvoidconceal_data_type_set_a(concel_data_type_t*obj,intval);// conceal_data_type.c(源文件)structconceal_data_type{inta;// 其他私有成员...};intconceal_data_type_get_a(concel_data_type_t*obj){returnobj->a;}外部文件只能通过接口函数访问结构体成员,无法直接操作成员变量。
8.2 用C语言实现继承与多态
// 父类structparent{intdat1;intdat2;};// 子类"继承"父类(将父类作为第一个成员)structchild{structparentpar;// 必须是第一个成员intdat3;intdat4;};// 多态:父类指针可以指向子类对象structchild*ch=calloc(1,sizeof(structchild));structparent*par=(structparent*)ch;// par->dat1 等价于 ch->par.dat18.3 零长数组(柔性数组)
structmutable_array{intlen;chardata[0];// GNU 扩展:零长数组};// sizeof(struct mutable_array) == 4(32位系统)structmutable_array*p=malloc(sizeof(structmutable_array)+1024);p->len=1024;memcpy(p->data,"Hello",6);九、C语言中的奇特但有用的语法
9.1 字符串字面值拼接
constchar*str="My""Name""is Zhouwy";// 编译器自动拼接,str = "MyNameis Zhouwy"9.2 结构体指定成员初始化
structstructTest{inta;intb;intc;};structstructTestvar={.a=10,.b=24,.c=56};9.3 数组指定下标初始化
intarr[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 端口复用
intopt=1;// 在 bind 之前调用setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,(constvoid*)&opt,sizeof(opt));端口复用的主要用途:防止服务器重启时之前绑定的端口还未释放,或程序异常退出时系统未释放端口。
注意:多个套接字绑定同一端口时,只有最后一个套接字能正常接收数据。
十一、内存安全编程原则
11.1 核心原则
- malloc 分配的内存需要手动释放,函数结束也不会自动释放
- free 后的内存内容不变,但操作它会导致未定义行为
- 堆内存越界可能在
free时才报错,难以定位 - 访问非法内存不会立即报错,可能在后续某条指令才崩溃
11.2 内存调试建议
当程序出现莫名其妙的崩溃时:
- 检查所有涉及内存操作的代码
- 排查指针是否越界、是否使用已释放的内存
- 使用调试工具(如 Valgrind)检测内存问题
总结
C语言的高级编程技巧涵盖了性能优化、内存安全、代码架构等多个层面。掌握这些技巧不仅能让你的代码更加高效和安全,还能帮助你更好地理解计算机系统的底层运作机制。
在实际开发中,请牢记以下原则:
- 性能优化要有理有据,优先优化热点代码
- 内存管理要严格,malloc 与 free 配对使用,free 后置 NULL
- 善用回调函数和函数指针实现灵活的代码架构
- 利用
do-while(0)等模式简化错误处理流程 - 重视跨平台兼容性,编写可移植的代码
原始笔记来源:frasight/王者归来笔记.c, jdah/StudyC.c