C语言一些实用技巧

C语言一些实用技巧

    • 指定的初始化
      • 数组
      • 结构体与联合体
    • 宏的使用
      • 宏列表
      • 编译时断言
      • 静态断言
      • 获取偏移量
      • 获取容器地址
      • 获取结构体中field所占用的字节数
      • 获取数组元素数目
      • 头文件保护符
      • 符号转字符串与符号拼接
      • 泛型编程
      • 掩码
      • 防止溢出的加1方法
    • 判断机器的字节顺序
    • 整数任意进制数转换
    • 统计整数中为1的位的个数
      • 静态表-4bit
      • 静态表-8bit
      • 平行算法
      • 完美法
    • 目标文件加入编译时间
    • 头文件互锁和包含问题
    • 求平均值
    • 无锁原子读操作
    • 参考文档,不,抄袭文档

指定的初始化

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 
#include 

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) )

一般来说,这个宏会在文件中被定义,当然其实现形式会随编译器的不同而变化

获取容器地址

#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语言语法,但相较于第二种定义失去了类型检查的功能,变得更加不安全。

获取结构体中field所占用的字节数

#define FSIZE(type, field) sizeof(((type *)0)->field)

获取数组元素数目

#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 Conn(x,y) 	x##y
#define ToChar(x) 	#@x
#define ToString(x) #x

// x##y表示什么?表示x连接y,
int n = Conn(123,456); // 结果就是n=123456;
char* str = Conn("asdf","adf")结果就是 str = "asdfadf";
#define ToChar(x) #@x

// #@x,其实就是给x加上单引号,结果返回是一个constchar
char a = ToChar(1); // 结果就是a='1';
// 做个越界试验
char a = ToChar(123); // 结果是a='3';
// 如参数超过四个字符,编译器就给给你报错了!error C2015:too many characters in constant :P

// #x是给x加双引号
char* str = ToString(123132); //就成了str="123132";

// 如果有#define FUN(a,b) vo##a##b()那么FUN(idma,in)会被替换成void main()

#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 
#include 

#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;

防止溢出的加1方法

#define INC_SAT( val ) (val = ((val)+1 > (val)) ? (val)+1 : (val))

判断机器的字节顺序

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。

C语言一些实用技巧_第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取模。

目标文件加入编译时间

// 获取时间
// Example of __DATE__ string: "Dec 27 2017"
// Example of __TIME__ string: "15:06:19"
const char *BuildInfo = "Version: " VERSION " " __DATE__ " " __TIME__;


// 代码实现获取日期和时间的方法很多,比如:
unsigned int mk_Build_Date(void)
{
    int year = 0, month = 0, day = 0;
    int hour = 0, minute = 0, seconds = 0;
    char m[4] = {0};

    sscanf(__DATE__, "%3s %2d %4d", m, &day, &year);

    for (month = 0; month < 12; month++)
    {
        if (strcmp(m, short_char_months[month]) == 0)
        {
            break;
        }
    }

    sscanf(__TIME__, "%2d:%2d:%2d", &hour, &minute, &seconds);

    #ifdef SHORT_DATA_CHAR__
        printf("[null]  ** Build at:\t%04u-%02u-%02us %02u:%02u:%02u\n",
                year, month, day,
                hour, minute,seconds);

    #else
        printf("[null]  ** Build at:\t%04u-%02u-%02u %02u:%02u:%02u\n",
                year, month, day,
                hour, minute,seconds);
    #endif

    DEBUG("buildDate: %s %s\n", __DATE__, __TIME__);

    return 0;
}

把上面的函数加入到代码中,就能获取工程编译的时间。

但是如果该代码所在的文件没有被修改,在非build-all情况下,编译器不会再次编译此文件,所以时间信息也就不会被更新。

如果每次都使用re-build all,一来繁琐,二来也不能保证每次都会记得点击build all按钮,具体手段来参考:如何把编译时间加入到目标文件。

头文件互锁和包含问题

C程序的编译过程:预编译 - 编译 - 汇编 - 链接

  • 预编译主要是对源文件中的头文件,宏定义、预编译符等等进行展开和替换,不会设计到源文件与源文件的交换。
  • 编译也是以每个C文件为核心,进行语法上的分析与检查,并把C文件转化为汇编文件。
  • 汇编过程就是把编译过程生成的汇编程序进一步转化为与平台相关的机器码,最终获得目标文件,同样该过程也不会有C文件之间的交换。
  • 最后的链接过程,由于目标文件中含有非常多变量信息、函数等信息,链接器根据链接脚本等,进行目标文件的分析与整合最终生成可执行文件。

一个最常见的现象:有些人的工程改一处代码整个工程都得重新编译,而有些工程却不需要,其实这就是一个文件的关联问题。

引起头文件互锁问题的原因,一方面是头文件滥用导致的,另外一方面还是因为功能结构层次设计上存在问题。

#ifndef __XXXX_H__
#define __XXXX_H__
#endif

头文件保护符是为了减少头文件的重复包含问题,大部分重复包含的头文件都幸免能够编译通过。但是它无法解决有一类包含–递归包含。

递归包含时,编译器会提示相应的结构体没有定义,你可以在源文件中展开然后逐个往前找相应的结构体定义,最终形成了一种递归形式的包含问题。

在相应的.c文件中尝试着调整头文件的位置,而并不会有所改善。解决此类问题的办法有很多,比如把两个头文件直接整合成一个头文件等,常用的都是抽出公共部分单独成为一个头文件,这样程序的层次更加鲜明。所以头文件的设计也就在一定程度上决定了你的程序设计架构。

求平均值

unsigned int average(unsigned int low, unsigned int high)
{
	return low + (high - low) / 2;
}
// tow
unsigned average(unsigned a, unsigned b)
{
	return (a & b) + (a ^ b) / 2;
}

无锁原子读操作

在使用RTOS或裸机状态编程时,如需要一个全局时钟基准,通常是在一个定时器中断中累加实现,如下:

static unsigned long volatile __jiffies = 0; /* 全局时钟基准节拍累加器 */
ISR_TIMER() /* 定时中断服务函数 */
{
    ++__jiffies;
}

其中,__jiffies变量就是全局时间基准,程序中其它地方都会对其进行原子读操作来判断时间,典型实现如:

unsigned long get_jiffies(void)
{
	unsigned long tmp;

	CLOCK_IRQ_DIS(); /* 关定时中断 */
	tmp = __jiffies;
	CLOCK_IRQ_EN(); /* 开定时中断 */

	return tmp;
}

中断的开关影响系统的中断快速响应能力。

假设一个8位机(AVR、51等等),其上只有8位单字节数据的读写是单指令原子的,当你读了32位变量任何一个字节的时候,剩下的几个字节都可能改变。
如用单字节原子锁,如中断里发现锁被占有了,则这个周期不能+1操作,无论是用变量缓存还是丢弃,所记时间都不准了。

优化后的实现如下:

unsigned long get_jiffies(void)
{
	unsigned long tmp;

	do 
	{
		tmp = __jiffies;
	} while(tmp != __jiffies);

	return tmp
}

上面代码可以完成对__jiffies变量的原子操作(必须单核)。

参考文档,不,抄袭文档

分享几个实用的 C 语言小技巧;
一道有趣的C语言算法题;
一些实用的C语言小技巧;
如何把编译时间加入到目标文件?

你可能感兴趣的:(嵌入式C编程,c语言,开发语言,后端,技巧,嵌入式)