C语言基础技巧杂谈
C语言基础技巧杂谈
gloryliu
如果你交给某人一个程序,你将折磨他一整天;如果你教某人如何编写程序,你将折磨他一辈子。 -- David Leinweber
当你略有兴趣的点开本篇文章时,首先发现自己可能是一个饱受折磨的倒霉蛋,这确实有点让人愉快不起来。不过暂时可以庆幸一点,本文讨论的内容将足够基础和简单,可以负责任地说,与折磨二字完全扯不上任何关系。
当然这主要取决于笔者的程序水平和记忆力,正处于持续稳步下降的窘迫状态。闲言少叙。
一、 结构体初始化
以如下Position结构为例。
typedef struct {假如你习惯于memset来初始化整个结构的话,或许也可以试试以下方式:
Position pos = { 0 }; // 整个结构初始化为0不过当你想要用非0值来初始化时,下面这样可能不行:
Position pos = { 1 }; // 这样只能将只有x=1,y,z仍然是0你需要按顺序逐一列出每个字段的数值,来将整个结构初始化
Position pos = { 1, 1, 1 } // 逐一列出,按顺序赋值假如我们有一个更加复杂的结构,而只需要对少数字段设定初值,逐一列出可能会有点啰嗦。此时你可以尝试如下的“标记化结构初始化”方式:
Position pos = { .y = 2 }; // 选择特定字段赋值,C99标准看起来是不是简单明了了许多,为C99标准(冒号方式为C89),GCC下可用,其他编译器笔者未经测试。此种方式中列举顺序可与定义不同,同时未列举字段会按0值进行初始化。
不过有点不巧,C++中并不支持这种的方式,只能在纯C下使用。但在C++环境下,相信读者可以找到更加优雅的方式。
二、 数组的初始化
假定需要创建一个数组,且只需要对几个下标元素进行初始化,除了先定义数组,再写几行代码逐一赋值的原始方式,还有下面这种方式:
Position array[10] =其中[1],[3]指定了对应的数组下标,自然是从0开始。
你还可以同时把前面的结构体初始化方式用上:
Position array[10] =如果喜欢你也可以这样,只是看起来稍微有点怪怪的:
Position array[10] =如果你觉得不太满意的话,还可以再填点乱,甚至可以把下标的和常规方式进行混用:
Position array[10] =但我个人建议不到万不得已,还是最好不要这样写为妙。
三、 静态断言
你应该使用过assert断言,它可以帮助你对一些条件做严格检查,条件不能满足时,它便会以最直接的方式暴露出来(当然就是崩溃啦)。而且,他是运行时的检查。
实践告诉我们,还需要一个编译时的检查工具,以便在编译时就对一些静态条件进行校验,它就是静态断言static_assert。大名鼎鼎的boost库和loki库,多年前便已支持。而新版C++标准(C++0x)中,已经从语言层面引入了静态断言。
但如果你碰巧还没有用上C++11,也不太想导入boost一大套的实现,只需要随手Copy几行代码,也可以拥有这个简单版的实现。
1、利用数组长度不能为负数
#define static_assert(cond) typedef char s_assert[(cond) ? 0 : -1]2、 利用位域长度不能为负
#define static_assert(cond) sizeof(struct { int:-!(cond); })3、利用switch的case项不可重复
#define static_assert(cond)当然以上这三个版本都不完美,最明显的问题就是在断言失败时,无法产生有意义且充分的诊断信息。不过在大部分场合,它已经能够使用了。足够简单是这几种方式的主要优势,也刚好处在本文着重讨论的基础技巧的范畴。
四. 获得数组长度
static int array[] = { 1, 2, 3, 4, 5 };如何求出这个数组的实际长度,一定有比对着屏幕数一数,然后手写一个大大的5,来的更好的方法。通常的做法应该是这样的:
int size = sizeof(array) / sizeof(array[0]); // 正常写法同时或许还可以这样:
int size = *(&array + 1) - array; // 进阶写法这三种写法的核心点在于,首先要搞清楚&array是个什么东西。如果和笔者一样,大学的C语言课本毕业时不巧变成了两块钱的话,使用gdb的whatis命令,应该也是可以的。或者你是刚入职不久的小鲜肉,或者曾经是资深学霸,也不用考虑这个困扰。
但无论如何,&array的类型是一个数组指针,一个指向了长度为5的整形数组的指针,而&array + 1,应该是按照类型的长度移动,也就是跨过了一个数组(5个整形),指向下一个数组。这样来看*(&array +1) - array,也就是下一个数组地址减去这个数组。同时我们也知道,数组地址实际上也是首元素的地址。至此,上面进阶写法也就合情合理,童叟无欺了。
至于二逼写法,如果你知道在C语言中array[1] 与 1[array] 两种写法完全等价的话,也就没什么好奇怪的了。甚至你高兴的话,还可以写出这样的奇葩:
printf("%cn", 1["hello world"]); // 将打印出字符e有一点必须声明,如果有人第一次从我这里看到这种写法,并且愉快地在项目中实践起来。有朝一日被Leader看到并器重地揪出来痛扁,可千万别说这烂玩意儿是从我这里学到的。
当然如果你正好要去参加“代码混乱大赛”,这货或许可以派上点儿用场。
编程的时候,总是想着那个维护你代码的人,是一个有暴力倾向的精神病患者,并且他还知道你住在哪儿。 -- Martin Golding
四、位运算的应用
按位运算更加侧重数学上的技法,并不依赖语言的某些特性,理论上任何支持位运算的语言皆可想通。限于篇幅和本文定位于基础技巧的初衷,笔者这里只例举一些简单的技巧。
写位运算需要一定的数学基础,对于此类工作,其关键点在于两个方面:
一是你要有足够的数学修养和先天的缜密思维,二是你所拷贝代码的作者,是一个有足够数学修养,且先天思维缜密的人。
1、实现两个整数交换
如果哪天你很幸运地遇上一位面试官,他和蔼可亲并诚恳地要求你不使用中间变量,还要交换两个整数,也不要大惊小怪,下面的代码也许刚好可以派上用场。
void Swap(int& a, int& b)最初听闻这种使用技巧的年代,已经没法准确追溯了。其基本思路大致如此,如果a对b做两次异或,则其值仍然回到a自己。我们可以引入a1,b1,a2 来重演计算过程,变量每赋值一次,做一次数字标注,以清晰区分其原始数值。
a1= a ^ b;
b1 = b ^ a1 = b ^ a ^ b= a;
a2' = a1 ^ b1 = a ^ b ^a = b;
最终b1 = a,a2 = b; 即完成了a与b的交换。
实际上使用减法操作同样可以实现。对于这种表面上限制临时变量的,并带着一点点恶意的面试题,笔者只能说,偶尔分析一下还是挺有趣的。
2、 求绝对值
int abs(int n)或许标准库中已经躺着一个正确、高效且好用的abs了,而且我们大家都爱用它。但看到作者非要把代码写成这样,还是忍不住要来分析一下,搞清楚他到底在瞎折腾个啥。
先不管如何运转脑壳才能写出以上的代码,仅仅考虑做一个正确与否的验证,总归还是一件简单的事情。
对于n = 0时,不管怎么折腾,最终都是0,此时结果应该是对的;
对于n > 0时,n >> 31 应该只剩首位符号位,还是0,n与0异或将保持不变,再减去0仍然保持不变,此时看起来也是对的;
对于n < 0 时,首位符号位是1,右移会补符号位,n >> 31 应该是-1,即所有位为1。n与-1异或,n的首位为1,异或时消掉;对其他位是一个取反的过程,然后再减-1实际上是加1。取反加1就是求补码,补码的补码就是原码。虽然有点复杂,但此时公式也是对的。
尽管不能确定如此折腾是否会让计算更加高效(希望它有些场景下是的),但鉴于它至少还是对的,并且强迫笔者回忆起遗忘多年的补码知识,也算是有些价值吧。
3、整数提升(2的幂)
#define ALIGN(size, align) ((size + align - 1) & (~(align - 1)))该宏定义的作用是将一个size提升到按照align对齐的大小,align应该是2的幂。如果size本身已经是按对齐大小,则返回size本身。
比如当你把结构存到一个buffer里时,你可以按8字节对齐来申请,那么可以用这个宏来计算。在C语言编译器计算结构体的实际占用空间时应该也会用到。
linux内核中也有类似的定义,在/usr/include/linux/kernel.h中也可以找到。
只要找个本子随便画画~(align -1)是个什么玩意儿,其正确性应该是显而易见的。
相信单就位运算这一块内容,其各种生涩的数学原理和稀奇古怪的程序技巧,足够出一本书来让许多人头痛了。网上应该也可以找到相关的专题。本文所举例子都还简单初级,不足以体现出它的真正魅力,但希望可以起到抛砖引玉的效果吧。
四、最后
本文所述的基础技巧,并非总是恰当和好用的,根据实际需要来使用它们,或者毫不怀疑地用你所知道的更好方式。