C语言的高级技巧

c语言是一门古老的语言,可以看下下面的C语言的介绍:

1969-1973年在美国电话电报公司(AT&T)贝尔实验室开始了C语言的最初研发。根据C语言的发明者丹尼斯·里奇 (Dennis Ritchie) 说,C 语言最重要的研发时期是在1972年。
说明:丹尼斯·里奇(Dennis Ritchie),C语言之父,UNIX之父。1978年与布莱恩·科尔尼干(Brian Kernighan)一起出版了名著《C程序设计语言(The C Programming Language)》,现在此书已翻译成多种语言,成为C语言方面最权威的教材之一。2011年10月12日(北京时间为10月13日),丹尼斯·里奇去世,享年70岁。
C语言之所以命名为C,是因为C语言源自Ken Thompson发明的 B语言,而B语言则源自BCPL语言。
C语言的诞生是和UNIX操作系统的开发密不可分的,原先的UNIX操作系统都是用汇编语言写的,1973年UNIX操作系统的核心用C语言改写,从此以后,C语言成为编写操作系统的主要语言。

C语言既简单又复杂,说它简单是因为它的关键字少,语法规则简单,看看就可以编写个hello,world程序;说它复杂是因为它接近于底层,有指针,可以直接操作内存,由此引起的一堆麻烦事情调试起来有非常的复杂,而且没有丰富的库的支持,很多东西都要自己手写,比较麻烦。

C语言的另外复杂点在于,你也许对C的语法早就了然于胸,但是仍然对开源的库代码阅读起来非常吃力,除了算法和数据结构复杂之外,C语言还有自己的奇技淫巧,常在开源的代码中应用,但是却很少有书去总结,这个文章算是对C的常用技巧做一个总结吧,资料来源于网上和自己看代码的一些体会。

一 编译器判断优化技巧

#define likely(x)       __builtin_expect(!!(x),1)
#define unlikely(x)     __builtin_expect(!!(x),0)

likely这个宏的期望x是非0,是在绝大多数情况下,x都是非0,比如我们在内存申请后判断指针是否为空可以用,而unlikely正好相反,是绝大多数情况下,x为空时候使用。

char * p =(char*)malloc(sizeof(int));
if (likely(p)) {
  do_something();
}

引入这两个宏,可以增加条件判断的分支预测准确性,cpu会提前装载后面的指令。在汇编级别的表现是预测大多数可能发生的条件是顺序的指令,而少数可能发生的情况是跳转指令,顺序指令在执行的时候可以利用CPU的缓存优势。
极端情况下,性能可以提升30%左右。

二 定长类型

在Java这种语言中,byte是8个位一个字节,short是16位,2个字节,int是32位,4个字节这些都是确定的。C语言中,经常出现同一个类型在不同的平台的字节长度是不一样的,比如long在32位系统中为4个字节,在64位系统中为8个字节。这就给我们编写跨平台的系统产生了麻烦,
为了跨平台,很多系统定义了自己的一套类型。stdint.h头文件到了确定大小的类型,比如:

1字节     uint8_t  
2字节     uint16_t  
4字节     uint32_t  
8字节     uint64_t

编程中,我们应该多使用这些类型,少使用int,long等。

三 利用宏实现日志功能

日志功能很常用,但是我们如何获取打印日志的位置和行号那,我特意和同事讨论了下,在Java中这个功能是通过定义异常来实现的,那么在C中如何实现,废话不说,看下代码吧:

#define LOG_PRINTF(pres, fmt, ...)                                                  \
log_printf(pres "[%s:%s:%d]" fmt "\n", __FILE__, __func__, __LINE__, ##__VA_ARGS__)

#define log_info(logLevel,fmt,...)  do { \
        if (INFO_LEVEL >= logLevel) {\
                 LOG_PRINTF_S("[INFO]", fmt, ##__VA_ARGS__);\
            }\
        }while (0);

void log_printf(const char * fmt, ...) {
    va_list ap;
    // 每条日志大小, 按照系统缓冲区走
    char str[BUFSIZ];
    int len = times_fmt("["STR_TIMES"]", str, sizeof str);
    time_t now_time = time(NULL);
    //每到新的一天切换日志
    if (!time_day(now_time,last_time)) {
        log_init(g_app_conf.app_conf.common_conf_data.log_file_name);
    }
    // 日志内容填充
    va_start(ap, fmt);
    vsnprintf(str + len, sizeof str - len, fmt, ap);
    va_end(ap);

    // 数据写入到文件中
    fputs(str, log_file);
    if (is_write_to_console == 1) {
        fputs(str, stderr);
    }
}

说明:
1) FILE表示正在运行程序文件名,func正在执行的函数,LINE是程序文件中的行号,这些是C的编译器内置的宏。
2) 对于#宏解释下,#用在预编译语句里面可以把预编译函数的变量直接格式成字符串。
比如:

#define my_printf(x) printf(#x" is %d\n", x)
int a = 100;
my_printf(a);
/*打印a is 100*/

可以用在switch语句中,将enum转成相关的字符串,非常方便:

#define CASE_CODE(E)  case E: return #E
const char * PacketProfileDetectIdToString(PacketProfileDetectId id)
{
    switch (id) {
        CASE_CODE (PROF_DETECT_SETUP);
        CASE_CODE (PROF_DETECT_GETSGH);
        CASE_CODE (PROF_DETECT_IPONLY);
        default:
            return "UNKNOWN";
    }
}

3) 对于##宏解释:

/* ## 是变量连接符,将两个字符连接成一个变量 */
#define FUN(a) printf("The square of " #a " is %d.\n",b##a)  
int bm = 2
FUN(m)

/* 打印 The square of m is 2.*/

4) VA_ARGS是可变参数宏,表示可变参数列表。

 #define Debug(...) printf(__VA_ARGS__)

 Debug("Y = %d\n", y);
/*自动替换成:
printf("Y = %d\n", y);
*/

5) 对于 ##__VA_ARGS__宏 作用是如果最后的可变变量为空忽略后面的逗号。
6) vsnprintf 是按照特定格式将可变参数打印到字符串中,方便后面的输出。

顺便说一下,宏在C语言中真是离不开,虽然很多书都推荐不要用宏,因为不便调试,但是宏可以简化代码,提高效率,使用范围是相当的广。

四 变长结构体

在C语言中,本身是不支持动态数组的,但是有些技巧可以实现类似动态数组的效果,一般人可能这样定义:

typedef struct Arry {
  int arry_len;
  char * content;
} * Arry;
//申请内存
Arry*p_ptr = (Arry*)malloc(sizeof(parry));
p_ptr ->content = (char *)malloc(100);
//释放内存
free(p_ptr ->content);
free(p_ptr);

这里面我们注意到这个这个结构体有两个变量,一个是int,一个是char ,char 里面保存的是申请内存的指针,如果用sizeof去求的话,会发现整个结构体的大小为4+4 = 8。
p_ptr 指针和p_ptr ->content 指针指向的内存没有任何关系。

变长结构体定义如下:

typedef struct
{
    int a;
    char b[0];
} * DArray;
//申请内存 100
DArray p_darray = (DArray)malloc(sizeof(*DArray)+100);
//释放内存
free(p_darray);

相比上面的定义好处如下:
1)只需要申请和释放内存一次即可。
2)内存分配是连续的,可以减少内存碎片。
3)节省内存,sizeof(*DArray) = = 4.
4) 可以方便用来做socket数据包传输,解析数据,等。

五 求数组和枚举的小技巧

对于一些情况,我们需要用到数组成员的个数和枚举的大小,一般对数组求大小可以这样做:
sizeof(array)/sizeof(array[0])得到;对于枚举类型,我们可以在最后定义一个最大宏,标识宏的结束:

enum { ONE,TWO,THREE,MAX};

我们在循环的时候就可以用MAX,以后添加变量在MAX前面添加即可,相关代码不用改变。

六 宏定义的小技巧

#define EXAMPLE do{
   xxxx \
   xxxxx \
 }while(0);
//这样好处把宏定义封装起来,后面多加分号不容易出错。

七 NULL 判断颠倒处理

我们判断NULL指针的时候,一般用if (p == NULL),但是这种写法,有可能少写一个=号,而程序不报错,可以改成 if (NULL == p) 这样写之后如果少写一个=号,程序显然会报错的。

八 其他提示

C语言的告警一定要多注意,尽量让代码零告警,不仅仅看起来清爽,还可以避免不少难查的Bug,还有一点就是程序写好之后,用valgrind --tool=memcheck --leak-check=full 运行检查,
看看是否有内存泄漏,还有就是invaild 读和写,这往往是程序运行core的根源,切记切记!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章