C语言的奇技淫巧
C语言中的奇技淫巧
01. 宏定义用do{}while(0)
如果定义的宏函数后面有多条语句,使用这样的方式会有问题:
|
|
展开宏定义后会变成:
|
|
逻辑就不对了。可以用这一的方式解决,非常好用:
|
|
02. 数组的初始化
假如给arr的第2~6元素初始化为5,也许你会
|
|
现在告诉你C99可以这样:
|
|
03. 数组的访问
你想取数组的第6个元素(下标为5),教科书教你这样做:
|
|
其实你可以:
|
|
也不会有错,实际上
arr[5]对应*(arr+5)
,而5[arr]对应*(5+arr)
,没多大区别。
04. 结构体的初始化
结构体的初始化,传统的做法是:
|
|
对于C99,其实你可以:
|
|
05. 用include
的方式初始化大数组
|
|
06. Debug时输出文件名、函数名、行号等
|
|
07. C语言有-->
“趋向于…”操作符?
|
|
实际上C语言没有这个-->
操作符,是--
和>
的组合而已
|
|
08. 获得任意类型数组的元素数目
|
|
09. 判断运行环境的大小端
Linux有以下代码:
|
|
10. 编译时做条件检查
Linux Kernel有以下代码
|
|
例如,在某些平台为了防止内存对齐问题,检查一个结构体或者一个数组的大小是否为8的倍数。
|
|
除了这个,还有
|
|
11. 用异或运算实现数据交换
交换俩变量数据,一般做法是:
|
|
方法1需要第三个变量,方法二存在数据溢出可能,可以尝试下以下方法:
|
|
12. 判断语句中把const
数值放在前面
通常条件语句写成
|
|
但是,有可能手误写成
|
|
这种错误只有机器在运行时候知道,而人不一定能发现这种bug。
把数值放在前面就不怕了,==
写成=
,编译器就知道
|
|
13. 用冒号表达式替代if...else...
语句
这个用法应该很普遍了,不算什么特别的技巧了。
|
|
可以改成以下一行代码即可
|
|
14. 判断一个整数是否为2的幂
也许你会不断地将这个数除以2,除到底,然而Linux kernel有个巧妙的办法:
|
|
((n) & ((n) - 1)) == 0
这个不理解?那先想想2的X次方的值的二进制是怎样的。
15. 静态链表
直接看代码
|
|
16. 柔性数组
|
|
17. 数组之间直接赋值
|
|
这样是非法的,但是你可以放数组穿个马甲:
|
|
18. #include
的不一定是要.h
文件
#include
后面跟的可以是任意后缀的,但文件内容一定要是合法的。例如
|
|
19. 自动获取变量类型
|
|
是不是有点像C++ 11的auto类型?
20. 宏定义函数MIN(x,y)
的终极做法
|
|
21. 行控制#line
也许你知道用__LINE__
可以输出行号,然而你试下这个:
|
|
不单止行号被改了,文件名也被改了,是不是我们可以用这个干点啥……想想?
22. C和C++代码混合编译
在C的头文件上面
|
|
然后再头文件下面
|
|
23. 用查表法实现hex2str
直接上代码
|
|
24. 用sprintf实现hex2str
直接上代码
|
|
25. 将变量名变字符串
如果想打印一个变量名和它的值,也许会这样:
|
|
对于你有很多这样的变量要打印,建议你做个宏函数:
|
|
26. 获取结构体元素的偏移
|
|
27. 根据结构体成员获取结构体变量指针
|
|
这个怎么玩?看看链表
|
|
28. scanf
高级玩法
|
|
这是啥意思,正则表达式先了解下?然后自己试试,理解会更深入。
29. 两个数相加可以不用+
号?
|
|
30. 调试的时候打印数组
你是不是曾经为打印数组而烦恼,每次都要将元素一个个取出来?
|
|
31. 感受下这个0x5F3759DF
|
|
32. switch-case
的特殊玩法
直接看代码
|
|
实际上它是
|
|
使用最上面的switch-case
的形式大大提高了运行效率。
理解不了?汇编看看。
还是理解不了?那就网上自行搜索“Duff’s Device”
33. 防止头文件重复包含导致问题
这个用法很常见了,而且非常有用
|
|
当然,如果你的编译器支持的话,也可以
|
|
不过为了更好的兼容性,我建议你用第一种方法。
34. 2的N次幂ROUNDUP
|
|
其中,size是2的整数次幂,而
a & (2^n-1)
检查a的低位是否有值a | (2^n - 1)
将a的低n位赋值为11 + a | (2^n -1)
为a最近的下一个2^n倍值
|
|
有什么用?申请内存的时候可以按某字节对齐,减少内存碎片啊。
35. 某整数的ROUNDUP
|
|
这个不是按2的次幂ROUNDUP的,而是按某个整数的倍数ROUNDUP,例如
|
|
这个又有什么用?EEPROM或者Flash的page大小对齐的时候就非常有意义。
36. 万能的void*
想想,memcpy
函数为什么要用void*
?
|
|
因为,它不关心你传什么类型的指针过来,我void
统统都接纳。
无为而无不为。
再看看这个:
|
|
因为“空类型”可以包容“有类型”,而“有类型”则不能包容“空类型”。
所以,适可而止,不要滥用哦。
37. sizeof(空)
|
|
正所谓
空即是色,色即是空。
在C语言上,sizeof(void);
的值为1;而sizeof(StructNull);
为0。
C++的情况请自行验证,别瞎猜,哈哈哈。
38. 布尔变量的判断
正确的做法:
|
|
以下是瞎搞
|
|
为啥?布尔类型中只有两个值:假和真。 请问:假是什么,真又是什么?
假是0,而真是非0。那么非0是什么?-1,1,2,……
除了0的一切。
所以再想想以下代码中的两个叹号是否可以去掉?
|
|
39. 感受下##
的用法
我们知道##
是用来连接字符的,看看RTX怎么用,感受一下:
|
|
代码比较简单,我就不解释了,自行思考下。
40. 传值和传址
看两个例子:
|
|
请问,这个swap
可以交换x, y
的值吗?
再看看一个常见的面试题:
|
|
这个程序能输出“hello world”吗?
此处没有答案,为了加深理解,建议感兴趣的朋友请自行动手验证和思考。
41. 形参到底传值好还是传址好
接着上一条,我们从另一个角度看。函数定义一个结构体类型形参,传值好还是传址好?
|
|
实际上两种方式都行,但你要明白形参实际上就是一个临时变量,不管传值还是传址都有一个复制给临时变量的过程。这个仿真汇编看看就知道了。
很明显,如果tStructType这个类型占用空间很大,那么肯定用tStructType*
比较合算。
42. bit翻转的几个方法
bit翻转是从MSB->LSB到LSB->MSB, 所有的Bit都必须反转。例如:
|
|
1. 运算实现32位bit翻转
|
|
2. 查表法bit翻转
|
|
43. BOOL判断
面试题:如何判断一个bool变量
如果你写成if(b_flag == TRUE)
可能会给你0分。为什么?
因为非假即真,假是0,即非0即真。
可以写成if(b_flag)
或者if(!b_flag)
如果是函数呢?
例如:
|
|
这样可以吗?也许可以,也许不可以。
如果BOOL
是一个unsigned char
怎么办?
那就这样咯:
|
|
44. goto的使用
看到goto,先别慌,也忍着别吵。我们不推荐使用goto,但goto确实有它的妙用。 不详细解释了,看看以下例子代码感受下吧:
|
|
45. 类型定义
这个有点老套了,但是很有用。
|
|
用uint8
等来定义变量比unsigned char
这样的好多了,方便平台移植,特别是不用位数的单片机,这个很重要。
46. 输出预编译信息
当你想输出预编译过程中的某些信息,可以用#pragma message("message contents")
47. 输出指针地址
当你想输出指针地址的时候,往往想到的是
|
|
但你还可以
|
|
48. 数组元素赋值
|
|
你也可以
|
|
还有第三种办法,详见“17. 数组之间直接赋值” 顺便思考下并验证以下方式是否可行?(想知道答案一定要亲自试试啊,别说我坑你。)
|
|
49. likely()
与unlikely
Linux内核中有两个这样的东西:likely()
与unlikely
|
|
我们可以根据高概率发生的情况放在if
分支,低概率的放在else
分支,以提高程序运行效率。
|
|
或者
|
|
50. 解放if/else
和switch/case
对于多分支的程序设计,很多人通常会这样做:
|
|
或者
|
|
几个分支还好,如果有几十个呢?尝试下这个:
|
|