C语言基础技巧杂谈

发表于2016-07-07
评论0 1.9w浏览

C语言基础技巧杂谈

gloryliu

 

         如果你交给某人一个程序,你将折磨他一整天;如果你教某人如何编写程序,你将折磨他一辈子。 -- David Leinweber

         当你略有兴趣的点开本篇文章时,首先发现自己可能是一个饱受折磨的倒霉蛋,这确实有点让人愉快不起来。不过暂时可以庆幸一点,本文讨论的内容将足够基础和简单,可以负责任地说,与折磨二字完全扯不上任何关系。

         当然这主要取决于笔者的程序水平和记忆力,正处于持续稳步下降的窘迫状态。闲言少叙。

                                                       

一、 结构体初始化

         以如下Position结构为例。

    typedef struct {        
        int x;        
        int y;        
        int z;    
    } Position;

         假如你习惯于memset来初始化整个结构的话,或许也可以试试以下方式:

    Position pos = { 0 };           // 整个结构初始化为0    
    Position pos = {};               // 不写0也是可以的       

         不过当你想要用非0值来初始化时,下面这样可能不行:

    Position pos = { 1 };       // 这样只能将只有x=1yz仍然是0         

         你需要按顺序逐一列出每个字段的数值,来将整个结构初始化

    Position pos = { 1, 1, 1 }      // 逐一列出,按顺序赋值 

         假如我们有一个更加复杂的结构,而只需要对少数字段设定初值,逐一列出可能会有点啰嗦。此时你可以尝试如下的“标记化结构初始化”方式:

    Position pos = { .y = 2 };     // 选择特定字段赋值,C99标准    
    Position pos = { y: 2 };        // 也可以,C89方式,或许已不推荐

         看起来是不是简单明了了许多,为C99标准(冒号方式为C89),GCC下可用,其他编译器笔者未经测试。此种方式中列举顺序可与定义不同,同时未列举字段会按0值进行初始化。

         不过有点不巧,C++中并不支持这种的方式,只能在纯C下使用。但在C++环境下,相信读者可以找到更加优雅的方式。

 

二、 数组的初始化

         假定需要创建一个数组,且只需要对几个下标元素进行初始化,除了先定义数组,再写几行代码逐一赋值的原始方式,还有下面这种方式:

    Position array[10] =    
    {        
        [1] = { 1,1,1 },        // 对array[1]初始化 
        [3] = { 3,3,3 },        // 对array[3]初始化    
    };

         其中[1][3]指定了对应的数组下标,自然是从0开始。

         你还可以同时把前面的结构体初始化方式用上:    

    Position array[10] =    
    {        
        [1] = { .x = 1, .z = 1 },   // 对array[1]x,z初始化        
        [3] = { .y = 3, .z = 3 },   // 对array[3]y,z初始化    
    };

         如果喜欢你也可以这样,只是看起来稍微有点怪怪的:

    Position array[10] =    
    {        
        [1].x = 1, [1].z = 1,       // 对array[1]x,z初始化        
        [3].z = 3, [3].z = 3,       // 对array[3]y,z初始化    
    };

         如果你觉得不太满意的话,还可以再填点乱,甚至可以把下标的和常规方式进行混用:

    Position array[10] =    
    {        
        [1].x = 1, [1].z = 1,       // 对array[1]x,z初始化        
        { 2, 2, 2 },                      // 对array[2]的常规初始化        
        [3].z = 3, [3].z = 3,       // 对array[3]x,z初始化    
    };

         但我个人建议不到万不得已,还是最好不要这样写为妙。    

 

三、 静态断言

         你应该使用过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)     
switch(0) {     
case 0:     
case (cond):         
break;     
} 

         当然以上这三个版本都不完美,最明显的问题就是在断言失败时,无法产生有意义且充分的诊断信息。不过在大部分场合,它已经能够使用了。足够简单是这几种方式的主要优势,也刚好处在本文着重讨论的基础技巧的范畴。

 

. 获得数组长度

    static int array[] = { 1, 2, 3, 4, 5 };

         如何求出这个数组的实际长度,一定有比对着屏幕数一数,然后手写一个大大的5,来的更好的方法。通常的做法应该是这样的:

    int size = sizeof(array) / sizeof(array[0]);    // 正常写法

         同时或许还可以这样:

    int size = *(&array + 1) - array;       // 进阶写法    
    int size = (&array)[1] - array;           // 文艺写法    
    int size = 1[&array] - array;              // 二逼写法

         这三种写法的核心点在于,首先要搞清楚&array是个什么东西。如果和笔者一样,大学的C语言课本毕业时不巧变成了两块钱的话,使用gdbwhatis命令,应该也是可以的。或者你是刚入职不久的小鲜肉,或者曾经是资深学霸,也不用考虑这个困扰。

         但无论如何,&array的类型是一个数组指针,一个指向了长度为5的整形数组的指针,而&array + 1,应该是按照类型的长度移动,也就是跨过了一个数组(5个整形),指向下一个数组。这样来看*(&array +1) - array,也就是下一个数组地址减去这个数组。同时我们也知道,数组地址实际上也是首元素的地址。至此,上面进阶写法也就合情合理,童叟无欺了。

         至于二逼写法,如果你知道在C语言中array[1] 1[array] 两种写法完全等价的话,也就没什么好奇怪的了。甚至你高兴的话,还可以写出这样的奇葩:

    printf("%cn", 1["hello world"]);        // 将打印出字符e    
    printf("%sn", &6["hello world"]);      // 将打印出字符串world    

         有一点必须声明,如果有人第一次从我这里看到这种写法,并且愉快地在项目中实践起来。有朝一日被Leader看到并器重地揪出来痛扁,可千万别说这烂玩意儿是从我这里学到的。

         当然如果你正好要去参加“代码混乱大赛”,这货或许可以派上点儿用场。

         编程的时候,总是想着那个维护你代码的人,是一个有暴力倾向的精神病患者,并且他还知道你住在哪儿。  -- Martin Golding

 

四、位运算的应用

         按位运算更加侧重数学上的技法,并不依赖语言的某些特性,理论上任何支持位运算的语言皆可想通。限于篇幅和本文定位于基础技巧的初衷,笔者这里只例举一些简单的技巧。

         写位运算需要一定的数学基础,对于此类工作,其关键点在于两个方面:

         一是你要有足够的数学修养和先天的缜密思维,二是你所拷贝代码的作者,是一个有足够数学修养,且先天思维缜密的人。

         1、实现两个整数交换

         如果哪天你很幸运地遇上一位面试官,他和蔼可亲并诚恳地要求你不使用中间变量,还要交换两个整数,也不要大惊小怪,下面的代码也许刚好可以派上用场。

    void Swap(int& a, int& b)    
    {        
        if (a != b)          
        {            
            a ^= b;            
            b ^= a;            
            a ^= b;        
        }    
    }

         最初听闻这种使用技巧的年代,已经没法准确追溯了。其基本思路大致如此,如果ab做两次异或,则其值仍然回到a自己。我们可以引入a1b1a2 来重演计算过程,变量每赋值一次,做一次数字标注,以清晰区分其原始数值。

    a1= a ^ b;

    b1 = b ^ a1 = b ^ a ^ b= a;

    a2' = a1 ^ b1 = a ^ b ^a = b;

    最终b1 = aa2 = b; 即完成了ab的交换。

         实际上使用减法操作同样可以实现。对于这种表面上限制临时变量的,并带着一点点恶意的面试题,笔者只能说,偶尔分析一下还是挺有趣的。

         2、 求绝对值

    int abs(int n)    
    {        
        return (n ^ (n >> 31)) - (n >> 31);      
    }

         或许标准库中已经躺着一个正确、高效且好用的abs了,而且我们大家都爱用它。但看到作者非要把代码写成这样,还是忍不住要来分析一下,搞清楚他到底在瞎折腾个啥。

         先不管如何运转脑壳才能写出以上的代码,仅仅考虑做一个正确与否的验证,总归还是一件简单的事情。

         对于n = 0时,不管怎么折腾,最终都是0,此时结果应该是对的;

         对于n > 0时,n >> 31 应该只剩首位符号位,还是0n0异或将保持不变,再减去0仍然保持不变,此时看起来也是对的;

         对于n < 0 时,首位符号位是1,右移会补符号位,n >> 31 应该是-1,即所有位为1n-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)是个什么玩意儿,其正确性应该是显而易见的。

         相信单就位运算这一块内容,其各种生涩的数学原理和稀奇古怪的程序技巧,足够出一本书来让许多人头痛了。网上应该也可以找到相关的专题。本文所举例子都还简单初级,不足以体现出它的真正魅力,但希望可以起到抛砖引玉的效果吧。        

 

四、最后

         本文所述的基础技巧,并非总是恰当和好用的,根据实际需要来使用它们,或者毫不怀疑地用你所知道的更好方式。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引