C语言一些实用技巧
指定的初始化
C99标准实际上支持一种更为直观简单的方式来初始化各种不同的集合类数据(如:结构体,联合体和数组)。
数组
我们可以指定数组的元素来进行初始化。这非常有用,特别是当我们需要根据一组#define来保持某种映射关系的同步更新时。来看看一组错误码的定义,如:
/* Entries may not correspond to actual numbers. Some entries omitted. */
#define EINVAL 1
#define ENOMEM 2
#define EFAULT 3
/* ... */
#define E2BIG 7
#define EBUSY 8
/* ... */
#define ECHILD 12
/* ... */
现在,假设我们想为每个错误码提供一个错误描述的字符串。为了确保数组保持了最新的定义,无论头文件做了任何修改或增补,我们都可以用这个数组指定的语法。
char *err_strings[] = {
[0] = "Success",
[EINVAL] = "Invalid argument",
[ENOMEM] = "Not enough memory",
[EFAULT] = "Bad address",
/* ... */
[E2BIG ] = "Argument list too long",
[EBUSY ] = "Device or resource busy",
/* ... */
[ECHILD] = "No child processes"
/* ... */
};
这样就可以静态分配足够的空间,且保证最大的索引是合法的,同时将特殊的索引初始化为指定的值,并将剩下的索引初始化为0。
结构体与联合体
用结构体与联合体的字段名称来初始化数据是非常有用的。假设我们定义:
struct point {
int x;
int y;
int z;
}
然后,我们这样初始化struct point:
struct point p = {.x = 3, .y = 4, .z = 5};
当我们不想将所有字段都初始化为0时,这种作法可以很容易的在编译时就生成结构体,而不需要专门调用一个初始化函数。
对联合体来说,我们可以使用相同的办法,只是我们只用初始化一个字段。
宏的使用
宏列表
C中的一个惯用方法,是说有一个已命名的实体列表,需要为它们中的每一个建立函数,将它们中的每一个初始化,并在不同的代码模块中扩展它们的名字。这在Mozilla的源码中经常用到,我就是在那时学到这个技巧的。例如,在我去年夏天工作的那个项目中,我们有一个针对每个命令进行标记的宏列表。其工 作方式如下:
#define FLAG_LIST(_) \
_(InWorklist) \
_(EmittedAtUses) \
_(LoopInvariant) \
_(Commutative) \
_(Movable) \
_(Lowered) \
_(Guard)
它定义了一个FLAG_LIST宏,这个宏有一个参数称之为 _ ,这个参数本身是一个宏,它能够调用列表中的每个参数。举一个实际使用的例子可能更能直观地说明问题。假设我们定义了一个宏DEFINE_FLAG,比如:
#define DEFINE_FLAG(flag) flag,
enum Flag {
None = 0,
FLAG_LIST(DEFINE_FLAG)
Total
};
#undef DEFINE_FLAG
对FLAG_LIST(DEFINE_FLAG)做扩展能够得到如下代码:
enum Flag {
None = 0,
DEFINE_FLAG(InWorklist)
DEFINE_FLAG(EmittedAtUses)
DEFINE_FLAG(LoopInvariant)
DEFINE_FLAG(Commutative)
DEFINE_FLAG(Movable)
DEFINE_FLAG(Lowered)
DEFINE_FLAG(Guard)
Total
};
接着,对每个参数都扩展DEFINE_FLAG宏,这样我们就得到了enum如下
enum Flag {
None = 0,
InWorklist,
EmittedAtUses,
LoopInvariant,
Commutative,
Movable,
Lowered,
Guard,
Total
};
然后,我们可能要定义一些访问函数,这样才能更好的使用flag列表:
#define FLAG_ACCESSOR(flag) \
bool is##flag() const {\
return hasFlags(1 << flag);\
}\
void set##flag() {\
JS_ASSERT(!hasFlags(1 << flag));\
setFlags(1 << flag);\
}\
void setNot##flag() {\
JS_ASSERT(hasFlags(1 << flag));\
removeFlags(1 << flag);\
}
FLAG_LIST(FLAG_ACCESSOR)
#undef FLAG_ACCESSOR
一步步的展示其过程是非常有启发性的,如果对它的使用还有不解,可以花一些时间在gcc –E上。
宏列表应用于C程序中管理标志位的例子
#include <stdio.h>
#include <stdlib.h>
typedef unsigned char uint8_t;
typedef unsigned int uint16_t;
typedef signed char int8_t;
typedef int int16_t;
#define true 1
#define false 0
//宏列表
#define TAG_LIST(tag) \
tag(Run)\
tag(Alarm)\
tag(Online)\
tag(TimerOver)
//枚举处理
#define DEFINE_TAG(_tag) _tag,
enum Flag {
None = 0,
TAG_LIST(DEFINE_TAG)
EmMAX
};
#undef DEFINE_TAG
//位定义变量
uint16_t SysFlag = 0x0000;
//通用方法定义
uint8_t GetFlags(uint16_t mask)
{
return ((SysFlag & mask) != 0)? true:false;
}
void SetFlags(uint16_t mask)
{
SysFlag |= mask;
}
void ClrFlags(uint16_t mask)
{
SysFlag &= ~mask;
}
//自动生成三类函数定义
#define FLAG_Operater(flag) \
uint8_t get##flag() {\
return GetFlags(1 << flag);\
}\
void set##flag() {\
SetFlags(1 << flag);\
}\
void clr##flag() {\
ClrFlags(1 << flag);\
}
//反向函数关联
TAG_LIST(FLAG_Operater)
int main(int argc, char *argv[]) {
setRun();
setAlarm();
if(getAlarm() == true)
{
printf("set \r\n");
}
else
{
printf("clr \r\n");
}
return 0;
}
编译时断言
这其实是使用C语言的宏来实现的非常有“创意”的一个功能。有些时候,特别是在进行内核编程时,在编译时就能够进行条件检查的断言,而不是在运行时进行,这非常有用。不幸的是,C99标准还不支持任何编译时的断言。
但是,我们可以利用预处理来生成代码,这些代码只有在某些条件成立时才会通过编译(最好是那种不做实际功能的命令)。有各种各样不同的方式都可以做到这一点,通常都是建立一个大小为负的数组或结构体。最常用的方式如下:
/* Force a compilation error if condition is false, but also produce a result
* (of value 0 and type size_t), so it can be used e.g. in a structure
* initializer (or wherever else comma expressions aren't permitted). */
/* Linux calls these BUILD_BUG_ON_ZERO/_NULL, which is rather misleading. */
#define STATIC_ZERO_ASSERT(condition) (sizeof(struct { int:-!(condition); }) )
#define STATIC_NULL_ASSERT(condition) ((void *)STATIC_ZERO_ASSERT(condition) )
/* Force a compilation error if condition is false */
#define STATIC_ASSERT(condition) ((void)STATIC_ZERO_ASSERT(condition))
如果(condition)计算结果为一个非零值(即C中的真值),即! (condition)为零值,那么代码将能顺利地编译,并生成一个大小为零的结构体。如果(condition)结果为0(在C真为假),那么在试图生成一个负大小的结构体时,就会产生编译错误。
它的使用非常简单,如果任何某假设条件能够静态地检查,那么它就可以在编译时断言。例如,在上面提到的标志列表中,标志集合的类型为uint32_t,所以,我们可以做以下断言:
STATIC_ASSERT(Total <= 32)
它扩展为:
(void)sizeof(struct { int:-!(Total <= 32) })
现在,假设Total<=32。那么-!(Total <= 32)等于0,所以这行代码相当于:
(void)sizeof(struct { int: 0 })
这是一个合法的C代码。现在假设标志不止32个,那么-!(Total <= 32)等于-1,所以这时代码就相当于:
(void)sizeof(struct { int: -1 } )
因为位宽为负,所以可以确定,如果标志的数量超过了我们指派的空间,那么编译将会失败。
静态断言
#define STATIC_ASSERT(expr) (\
(void)sizeof(char[1-2*!!!(expr)]))
当然还有其他实现方法,但这种无疑是最简单、最通用的方法。
获取偏移量
define offsetof(type, member) (\
(size_t)&((type*)0->menber) )
一般来说,这个宏会在<stddef.h>文件中被定义,当然其实现形式会随编译器的不同而变化
获取容器地址
#define container_of(ptr, type, member) (\
(type *)( (char *)(ptr) - offsetof(type,member) ) )
这个宏常见于Linux内核,而它通常的定义如下:
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
仔细琢磨便可发现,上述定义中的第一个语句实际上没有实现任何功能。而且,上述定义中使用了GNUC的拓展语法,不属于标准C语言的范畴。然而,第一个定义却是完全符合标准C语言语法,但相较于第二种定义失去了类型检查的功能,变得更加不安全。
获取数组元素数目
#define ARRAY_SIZE(a) ( sizeof(a)/sizeof((a)[0]) )
同样来源于Linux内核的宏,同样为了适应标准C做了阉割。在gcc环境下,更加安全的定义如下:
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) \
+sizeof(typeof(int[1-2*!!__builtin_types_compatible_p(typeof(arr),\
typeof(&arr[0]))]))*0)
同理,利用GNUC的typeof关键字和内建函数,判断arr和&arr[0]类型是否相同,相同则会导致数组长度为负数,实现静态断言,避免在这里错误地使用指针作为宏的参数。所以,当你使用这个宏的时候,请务必谨慎!同时还要注意,这个宏的名称有歧义,我更乐意将它叫做member_of或者number_of因为它返回的是数组的元素个数,而不是数组的大小。
头文件保护符
#ifndef __XXX_H__
#define __XXX_H__
#endif
// 拓展有:
#ifndef XXX_GLOBAL
#define XXX_EXT extern
#else
#define XXX_EXT
#endif
#ifndef XXX_GLOBAL
#define INIT(val, init) (val)
#else
#define INIT(val, init) (val) = (init)
#endif
/*
__cplusplus是cpp中的自定义宏,那么定义了这个宏的话表示这是一段cpp的代码,那么加入extern "C"{和}处理其中的代码。
*/
#ifdef __cplusplus
extern "C" {
#endif
//一段代码 ......
#ifdef __cplusplus
}
#endif
还是比较有用的,都是条件编译的灵活运用。
符号转字符串
#define _STR(x) #x
//这个配合字符串的拼接,能够完成一些相当便利的操作,非常便利了。如:
#define sys_call(n) __asm volatile("svc #"#n)
符号拼接
#define _str_spice(tkn1, tkn2) tkn1##tkn2
#define str_spice(tkn1, tkn2) _str_spice(tkn1, tkn2)
泛型编程
借助宏,还可以实现泛型编程,实现c++当中的模版的部分功能。示例如下:
/*
* heap.h
*
* Author: SMS
*/
#ifndef __HEAP_H__
#define __HEAP_H__
#include <stdint.h>
#include <stdbool.h>
#define _str_spice(tkn1, tkn2) tkn1##tkn2
#define str_spice(tkn1, tkn2) _str_spice(tkn1, tkn2)
#define __template(type, tplt) str_spice(type, str_spice(_, tplt))
#ifdef __cplusplus
extern "C" {
#endif /* __CPLUSPLUS */
#ifndef heap_type
#define heap_type int
#endif
typedef bool(*compare_t)(heap_type*, heap_type*);
static inline void
__template(heap_type, heap_swap)(heap_type *a, heap_type *b)
{
heap_type t;
t = *a;
*a = *b;
*b = t;
}
static inline void
__template(heap_type, heap_build)(heap_type a[],
compare_t func,
unsigned bgn,
unsigned end)
{
heap_type t = a[bgn];
unsigned i;
for(i=(bgn<<1)+1; i<=end; bgn=i, i=(i<<1)+1)
{
if(i<end && !func(&a[i], &a[i+1]))
i++;
if(func(&t, &a[i]))
break;
a[bgn] = a[i];
a[i] = t;
}
}
static inline void
__template(heap_type, heap_adjast)(heap_type a[],
compare_t func,
unsigned bgn,
unsigned end)
{
heap_type t = a[end];
unsigned i;
for(i=(end-1)>>1; i>=bgn && end!=bgn; end=i, i=(i-1)>>1)
{
if(!func(&t, &a[i]))
break;
a[end] = a[i];
a[i] = t;
}
}
static inline void
__template(heap_type, heap_sort)(heap_type a[],
compare_t func,
unsigned len)
{
unsigned i = (len>>1)-1;
for(; i<(unsigned)-1; i--)
__template(heap_type, heap_build)(a, func, i, len-1);
for(i=len-1; i!=0; i--)
{
__template(heap_type, heap_swap)(&a[0], &a[i]);
__template(heap_type, heap_build)(a, func, 0, i-1);
}
}
static inline void
__template(heap_type, heap_push)(heap_type a[],
compare_t func,
unsigned len,
heap_type *data)
{
a[len-1] = *data;
__template(heap_type, heap_adjast)(a, func, 0, len-1);
}
static inline void
__template(heap_type, heap_pop)(heap_type a[],
compare_t func,
unsigned len,
unsigned index)
{
a[index] = a[len-1];
__template(heap_type, heap_build)(a, func, index, len-1);
}
#ifdef __cplusplus
}
#endif /* __CPLUSPLUS */
#endif /* __HEAP_H__ */
其实可以更进一步,将函数中的函数指针参数也作为一个宏参数,让上面的代码看起来更像c++的模版。但是标准C无法在宏当中做到类型检查(据我所知是这样的),所以安全起见上述代码没有这样做。
掩码
码指的是一些设置为开 (1) 或关 (0) 的位组合。
#define MASK (1<<1)
flags = flags & MASK;
// 设置位
#define MASK (1<<1)
flags |= MASK;
// 清除位
#define MASK (1<<1)
flags &= ~MASK;
// 切换位
#define MASK (1<<1)
flags ^= MASK;
// 检查位 掩码至少要与其覆盖的值的宽度相同,要避免符号位带来的意外,最好在代码中使用unsigned int操作位和字节。
#define MASK (1<<1)
(flags & MASK) == MASK
// 移位运算符可用于从较大单元中提取一些位,例如提取 RBG 颜色值:
#define BYTE_MASK 0xff
unsigned long color = 0x123456;
unsigned char blue, green, red;
red = color & BYTE_MASK;
green = (color >> 8) & BYTE_MASK;
blue = (color >> 16) & BYTE_MASK;
判断机器的字节顺序
int main(void)
{
int x = 1;
if (*((char *)&x) == 1)
printf("little - endian\n");
else
printf("big - endian\n");
return 0;
}
- 先初始化在内存中占用 4 个字节的 int 变量。
- 然后获取int 变量中第 1 个字节的地址,等效代码是:char *px = (char *)&x。
- 最后获取第 1 个字节的值:*px,观察 *px 是否为 1 就可以知道大小端了。
整数任意进制数转换
#define BUF_SIZE (33)
char *baseconv(unsigned int num,int base)
{
static char retbuf[BUF_SIZE];
char *p;
...
p = &retbuf[sizeof(retbuf)-1];
*p='\0';
do {
*--p="0123456789abcdef"[num % base];
num /=base;
} while(num !=0);
return p;
}
需要明确,整数本来就是以二进制存储的,这里说的转换只是指打印的形式。
在baseconv() 中的缓冲是 static 的,这有2 个作用:1) 将缓冲清 0,2) 只有是 static 的缓冲才能在函数外部被使用。
注意 char *p = &retbuf[sizeof(retbuf)-1] = ‘\0’ 这个操作,这里将缓冲的最高位设置为字符串结束符,同时表明了字符串是从高地址向底地址构造的,函数返回缓冲中有效数据的起始地址。
如果你这样打印:
printf("%d %s %s\n", a, baseconv(a, 2), baseconv(a, 16));
会得到这样的结果:10100 00,这是因为 baseconv() 中的缓冲是 static 的, baseconv(a, 2) 将 baseconv(a, 16) 冲刷掉了。
统计整数中为1的位的个数
静态表-4bit
static int bitcounts[] = {0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4};
int bitcount(unsigned int u)
{
int n=0;
for(; u!=0; u>>=4)
n += bitcounts[u & 0x0f];
return n;
}
// test
int main(void)
{
int i = 0;
for (i=0; i<=0x0f; i++)
printf("%d\n", bitcount(i));
return 0;
}
// result
$ gcc bit_counts.c -o bit_counts
$ ./bit_counts
0
1
1
2
1
2
2
3
1
2
2
3
2
3
3
4
静态表-8bit
首先构造一个包含256个元素的表table,table[i]即i中1的个数,这里的i是[0-255]之间任意一个值。然后对于任意一个32bit无符号整数n,我们将其拆分成四个8bit,然后分别求出每个8bit中1的个数,再累加求和即可,这里用移位的方法,每次右移8位,并与0xff相与,取得最低位的8bit,累加后继续移位,如此往复,直到n为0。
所以对于任意一个32位整数,需要查表4次。以十进制数2882400018为例,其对应的二进制数为10101011110011011110111100010010,对应的四次查表过程如下:红色表示当前8bit,绿色表示右移后高位补零。
第一次(n & 0xff) 10101011110011011110111100010010
第二次((n >> 8) & 0xff) 00000000101010111100110111101111
第三次((n >> 16) & 0xff)00000000000000001010101111001101
第四次((n >> 24) & 0xff)00000000000000000000000010101011
int BitCount7(unsigned int n)
{
unsigned int table[256] =
{
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8,
};
return table[n &0xff] +
table[(n >>8) &0xff] +
table[(n >>16) &0xff] +
table[(n >>24) &0xff] ;
}
当然,也可以搞一个16bit的表,或者更极端一点32bit的表,速度将会更快。
平行算法
int BitCount4(unsigned int n)
{
n = (n &0x55555555) + ((n >>1) &0x55555555) ;
n = (n &0x33333333) + ((n >>2) &0x33333333) ;
n = (n &0x0f0f0f0f) + ((n >>4) &0x0f0f0f0f) ;
n = (n &0x00ff00ff) + ((n >>8) &0x00ff00ff) ;
n = (n &0x0000ffff) + ((n >>16) &0x0000ffff) ;
return n ;
}
速度不一定最快,但是想法绝对巧妙。说一下其中奥妙,其实很简单,先将n写成二进制形式,然后相邻位相加,重复这个过程,直到只剩下一位。
以217(11011001)为例,有图有真相,下面的图足以说明一切了。217的二进制表示中有5个1。
完美法
int BitCount5(unsigned int n)
{
unsigned int tmp = n - ((n >>1) &033333333333) - ((n >>2) &011111111111);
return ((tmp + (tmp >>3)) &030707070707) %63;
}
这个代码太简洁啦,只是有个取模运算,可能速度上慢一些。区区两行代码,就能计算出1的个数,到底有何奥妙呢?为了解释的清楚一点,我尽量多说几句。
-
第一行代码的作用:
先说明一点,以0开头的是8进制数,以0x开头的是十六进制数,上面代码中使用了三个8进制数。
将n的二进制表示写出来,然后每3bit分成一组,求出每一组中1的个数,再表示成二进制的形式。比如n = 50,其二进制表示为110010,分组后是110和010,这两组中1的个数本别是2和3。2对应010,3对应011,所以第一行代码结束后,tmp = 010011,具体是怎么实现的呢?
由于每组3bit,所以这3bit对应的十进制数都能表示为2^2 * a + 2^1 * b + c的形式,也就是4a + 2b + c的形式,这里a,b,c的值为0或1,如果为0表示对应的二进制位上是0,如果为1表示对应的二进制位上是1,所以a + b + c的值也就是4a + 2b + c的二进制数中1的个数了。
举个例子,十进制数6(0110)= 4 * 1 + 2 * 1 + 0,这里a = 1, b = 1, c = 0, a + b + c = 2,所以6的二进制表示中有两个1。现在的问题是,如何得到a + b + c呢?注意位运算中,右移一位相当于除2,就利用这个性质!
4a + 2b + c 右移一位等于2a + b
4a + 2b + c 右移量位等于a
然后做减法:
4a + 2b + c –(2a + b) – a = a + b + c,这就是第一行代码所作的事,明白了吧。
-
第二行代码的作用:
在第一行的基础上,将tmp中相邻的两组中1的个数累加,由于累加到过程中有些组被重复加了一次,所以要舍弃这些多加的部分,这就是&030707070707的作用,又由于最终结果可能大于63,所以要取模。
需要注意的是,经过第一行代码后,从右侧起,每相邻的3bit只有四种可能,即000, 001, 010, 011,为啥呢?因为每3bit中1的个数最多为3。所以下面的加法中不存在进位的问题,因为3 + 3 = 6,不足8,不会产生进位。
tmp + (tmp >> 3)-这句就是是相邻组相加,注意会产生重复相加的部分,比如tmp = 659 = 001 010 010 011时,tmp >> 3 = 000 001 010 010,相加得:
001 010 010 011
000 001 010 010
001 011 100 101
011 + 101 = 3 + 5 = 8。注意,659只是个中间变量,这个结果不代表659这个数的二进制形式中有8个1。
注意:我们想要的只是第二组和最后一组(绿色部分),而第一组和第三组(红色部分)属于重复相加的部分,要消除掉,这就是&030707070707所完成的任务(每隔三位删除三位),最后为什么还要%63呢?
因为上面相当于每次计算相连的6bit中1的个数,最多是111111 = 77(八进制)= 63(十进制),所以最后要对63取模。
参考文档,不,抄袭文档
分享几个实用的 C 语言小技巧;
一道有趣的C语言算法题;
一些实用的C语言小技巧;