CPP小技巧

内置宏

标准C语言预处理要求定义某些对象宏,每个预定义宏的名称一两个下划线字符开头和结尾,这些预定义宏不能被取消定义(#undef)或由编程人员重新定义。下面预定义宏:

  • __LINE__ 当前程序行的行号,表示为十进制整型常量
  • __FILE__ 当前源文件名,表示字符串型常量
  • __DATE__ 转换的日历日期,表示为Mmm dd yyyy 形式的字符串常量,Mmm是由asctime产生的。
  • __TIME__ 转换的时间,表示”hh:mm:ss”形式的字符串型常量,是有asctime产生的。(asctime貌似是指的一个函数)
  • __STDC__ 编辑器为ISO兼容实现时位十进制整型常量
  • __STDC_VERSION__ 如何实现复合C89整部1,则这个宏的值为19940SL;如果实现符合C99,则这个宏的值为199901L;否则数值是未定义
  • __STDC_EOBTED__ (C99)实现为宿主实现时为1,实现为独立实现为0
  • __STDC_IEC_559__ (C99)浮点数实现复合IBC 60559标准时定义为1,否者数值是未定义
  • __STDC_IEC_559_COMPLEX__ (C99)复数运算实现复合IBC 60559标准时定义为1,否者数值是未定义
  • __STDC_ISO_10646__ (C99)定义为长整型常量,yyyymmL表示wchar_t值复合ISO 10646标准及其指定年月的修订补充,否则数值未定义

C++中还定义了 __cplusplus, C语言中的__FILE____LINE____DATE__等都在头文件#include<stdio.h>

__attribute__机制

GNU C的一大特色就是__attribute__机制.__attribute__可以设置函数属性(Function Attribute),变量属性(Variable Attribute)和类型属性(Type Attribute)

其位置约束为: 放与生命的尾部”;”之前 __attribute__书写特征为:__attribute__前后都有两个下划线,并且后面会紧跟一对圆括号,括号里面是相应的__attribute__参数. __attribute__语法格式为:`attribute((attribute-list))

一, 函数属性(Function Attribute)

函数属性可以帮助开发者把一些特性添加到函数声明中,从而可以使编译器在错误检查方面的功能更强大。attribute机制也很容易同非GNU应用程序做到兼容之功效。GNU CC需要使用 –Wall编译器来击活该功能,这是控制警告信息的一个很好的方式。

下面介绍几个常见的属性参数:

__attribute__ format 该属性可以给被声明的函数加上类似printf或者scanf的特征,它可以使编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。该功能十分有用,尤其是处理一些很难发现的bug。

format的语法格式为:format (archetype, string-index, first-to-check)

format属性告诉编译器,按照 printf, scanf, strftime或strfmon的参数表格式规则对该函数的参数进行检查。“archetype”指定是哪种风格;“string-index”指定传 入函数的第几个参数是格式化字符串;“first-to-check”指定从函数的第几个参数开始按上述规则进行检查。 具体使用格式如下:

1
2
__attribute__((format(printf,m,n)))
__attribute__((format(scanf,m,n)))

其中参数m与n的含义为:

  • m:第几个参数为格式化字符串(format string);
  • n:参数集合中的第一个,即参数“…”里的第一个参数在函数参数总数排在第几.这里需要注意,有时函数参数里还有“隐身”的,如C++的类成员函数的第一个参数实际上是”隐身”的”this”指针; 在使用上,__attribute__((format(printf,m,n)))是常用的,而另一种却很少见到。

下面举例说明,其中myprint为自己定义的一个带有可变参数的函数,其功能类似于printf:

1
2
3
4
5
// m = 1, n = 2...如果在这里myprint()为类成员函数,则gcc编译后会提示"format argument is not a pointer"的警告
extern void myprint(const char *format,...) __attribute__((format(printf,1,2)));

// m = 2, n = 3
extern void myprint(short num,const char *format,...) __attribute__((format(printf,2,3)));

最经典的应用可以去看linux源码里的函数device_create(…)和class_device_create(…), 做linux驱动开发的小伙伴应该对这两个函数不陌生.

__attribute__ noreturn 该属性通知编译器函数从不返回值。当遇到函数需要返回值却还没运行到返回值处就已退出来的情况,该属性可以避免出现错误信息。

C库函数中的abort()和exit()的声明格式就采用了这种格式:

1
2
extern void exit(int)  __attribute__((noreturn));
extern void abort(void) __attribute__((noreturn));

例如下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extern void myexit(int);

int testFunc(void)
{
printf("-- Enter %s --", __func__);

myexit(0);

// 其实函数运行不到这里
printf("-- Exit %s --", __func__);
}

void myexit(int i)
{
exit(i);
}

编译时会报”control reaches end of non-void function”的警告, 但若将”extern void myexit(int);”改为”extern void myexit(int) attribute((noreturn));” 就不会再报警告了. attribute constructor/destructor 若函数被设定为constructor属性,则该函数会在 main()函数执行之前被自动的执行。类似的,若函数被设定为destructor属性,则该函数会在main()函数执行之后或者exit()被调用后被自动的执行。拥有此类属性的函数经常隐式的用在程序的初始化数据方面。

这两个属性还没有在面向对象C中实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__attribute__((constructor)) void before_main() {
printf("--- %s\n", __func__);
}

__attribute__((destructor)) void after_main() {
printf("--- %s\n", __func__);
}

int main(int argc, char **argv) {
printf("--- %s\n", __func__);

exit(0);

printf("--- %s, exit ?\n", __func__);

return 0;
}

其执行结果为:

1
2
3
--- before_main
--- main
--- after_main

弱引用符号__attribute__((weak))

情况是这样的,碰到一个棘手的问题:我们不确定外部模块是否提供一个函数func,但是我们不得不用这个函数,即自己模块的代码必须用到func函数:

1
2
3
4
5
6
7
8
extern int func(void);
...................
int a = func();
if( a > .....)
{
..........
}
............

我们不知道func函数是否被定义了,这会导致2个结果:

  1. 外部存在这个函数func,并且EXPORT_SYMBOL(func),那么在我自己的模块使用这个函数func,正确。
  2. 外部其实不存在这个函数,那么我们使用func,程序直接崩溃。

所以这个时候,__attribute__((weak)) 派上了用场。 在自己的模块中定义:

1
2
3
4
int  __attribute__((weak))  func(......)
{
return 0;
}

将本模块的func转成弱符号类型,如果遇到强符号类型(即外部模块定义了func),那么我们在本模块执行的func将会是外部模块定义的func。如果外部模块没有定义,那么,将会调用这个弱符号,也就是在本地定义的func,直接返回了一个1(返回值视具体情况而定),相当于增加了一个默认函数。

原理:连接器发现同时存在弱符号和强符号,有限选择强符号,如果发现不存在强符号,只存在弱符号,则选择弱符号。如果都不存在:静态链接,恭喜,编译时报错,动态链接:对不起,系统无法启动。

TIPS weak属性只会在静态库(.o .a )中生效,动态库(.so .ko(内核模块))中不会生效。

举个例子: strong.c //生成libstrong.so

1
2
3
4
5
6
#include <stdio.h>

void real_func()
{
printf("int real func\n");
}

weak.c //生成libweak.so

1
2
3
4
5
6
7
8
#include <stdio.h>
void real_func() __attribute__((weak));


void real_func()
{
printf("fake func\n");
}

main.c //

1
2
3
4
5
6
7
#include <stdio.h>
extern void real_func();

void main()
{
real_func();
}

如果

1
gcc main.c -lstrong -lweak

那么输出结果”real func”。

如果

1
gcc main.c -lweak -lstrong

那么输出结果为”fake func”。

可见,对于动态库,weak属性毫无作用,且main中调用哪个real_func(),取决于链接的顺序。 如果将strong.c 和 weak.c编译成.a或者.o

1
gcc main.c strong.o weak.o

或者

1
gcc main.c strong.o weak.o

那么输出结果都是”real func”。

所以,如果在so中使用weak属性,那么任何不符合预期的情况,都是可能出现的。

官方解释:https://sourceware.org/bugzilla/show_bug.cgi?id=3946

alias属性可以给函数名取一个外号。使用方法如下:

1
2
void func(void);
void alias_func(void) __attribute__((alias("func"))); 需要注意c++的符号修饰机制!

这样的意思就是函数func的别名或外号是alias_func,那么就是调用alias_func()和func()的效果是一样的,有兴趣的话可以自己写代码验证。这时需要主意func函数必须是要有定义的,否则会编译报错的。

__attribute__((weakref))为弱引用,请注意引用与定义的区别。weakref就是申明某个引用为弱引用,弱引用时如果需引用符号不存在也不会链接出错,而是将需要引用的符号定义为WEAK属性及0地址(跟前面的WEAK属性很相似吧)。 weakref的用法有点特别,必须要配合alias使用及必须是static定义。attribute((weak(“target”)))相当于attribute((weakref,alias(“target”))),以下看个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
** weakref_test.c
*/

/* 申明func_alias函数func的带弱引用的别名 */
void func(void)
{
  printf("func:%s in\n", __FUNC__);
}

static void func_alias(void) __attribute__((weakref,alias("func")));

int main()
{
  func_alias(); /* 相当于调用func */
}

编译运行,会发现实际运行的就是func函数。func_alias相当于是func的一个带有weakref属性的另一份申明,可以这样理解:void *func = func;void *func_alias = func(“weakref”)。 注意到前面alias属性如果func不存在时申明alias会出错,通过weakref方法,可以让func未定义就可以编译通过,使用static void alias_func(void) __attribute__((weakref,alias("func")))时即使func未定义也能链接通过,只不过func或alias_func的地址为0,可以去掉func的实现,验证一下即可。

宏参数的字符串化和宏参数的连接

在宏定义中,有时还会用到#和##两个符号,它们能够对宏参数进行操作。

# 的用法

#用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号。例如有如下宏定义:

1
#define STR(s) #s

那么:

1
2
printf("%s", STR(c.biancheng.net));
printf("%s", STR("c.biancheng.net"));

分别被展开为:

1
2
printf("%s", "c.biancheng.net");
printf("%s", "\"c.biancheng.net\"");

以发现,即使给宏参数“传递”的数据中包含引号,使用#仍然会在两头添加新的引号,而原来的引号会被转义。

##的用法

##称为连接符,用来将宏参数或其他的串连接起来。例如有如下的宏定义:

1
2
#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00

那么:

1
2
printf("%f\n", CON1(8.5, 2));
printf("%d\n", CON2(12, 34));

将被展开为:

1
2
printf("%f\n", 8.5e2);
printf("%d\n", 123400);

`extern “C”的作用

extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern “C”就是其中的一个策略。

这个功能主要用在下面的情况:

  • C++代码调用C语言代码
  • 在C++的头文件中使用
  • 在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到

看一个简单的例子: 有moduleA、moduleB两个模块,B调用A中的代码,其中A是用C语言实现的,而B是利用C++实现的,下面给出一种实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//moduleA头文件
#ifndef __MODULE_A_H //对于模块A来说,这个宏是为了防止头文件的重复引用
#define __MODULE_A_H
int fun(int, int);
#endif

//moduleA实现文件moduleA.C //模块A的实现部分并没有改变
#include"moduleA"
int fun(int a, int b)
{
return a+b;
}

//moduleB头文件
#idndef __MODULE_B_H //很明显这一部分也是为了防止重复引用
#define __MODULE_B_H
#ifdef __cplusplus //而这一部分就是告诉编译器,如果定义了__cplusplus(即如果是cpp文件,因为cpp文件默认定义了该宏),则采用C语言方式进行编译
extern "C"{
#include"moduleA.h"
#endif
… //其他代码

#ifdef __cplusplus
}
#endif
#endif

//moduleB实现文件 moduleB.cpp //B模块的实现也没有改变,只是头文件的设计变化了
#include"moduleB.h"
int main()
{
  cout<<fun(2,3)<<endl;
}

补充介绍: 由于C、C++编译器对函数的编译处理是不完全相同的,尤其对于C++来说,支持函数的重载,编译后的函数一般是以函数名和形参类型来命名的。例如函数void fun(int, int),编译后的可能是_fun_int_int(不同编译器可能不同,但都采用了类似的机制,用函数名和参数类型来命名编译后的函数名);而C语言没有类似的重载机制,一般是利用函数名来指明编译后的函数名的,对应上面的函数可能会是_fun这样的名字。

看下面的一个面试题:为什么标准头文件都有类似的结构?

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef __INCvxWorksh /*防止该头文件被重复引用*/
#define __INCvxWorksh
#ifdef __cplusplus//告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
extern "C"{
#endif

/*…*/

#ifdef __cplusplus
}
#endif
#endif /*end of __INCvxWorksh*/

分析:

  • 显然,头文件中编译宏”#ifndef __INCvxWorksh 、#define __INCvxWorksh、#endif”的作用是为了防止该头文件被重复引用
  • 那么以下代码的作用是什么呢?
    1
    2
    3
    4
    5
    6
    #ifdef __cplusplus (其中__cplusplus是cpp中自定义的一个宏!!!)
    extern "C"{
    #endif
    #ifdef __cplusplus
    }
    #endif

extern “C”包含双重含义,从字面上可以知道,首先,被它修饰的目标是”extern”的;其次,被它修饰的目标代码是”C”的。

  • 被extern “C”限定的函数或变量是extern类型的. extern是C/C++语言中表明函数和全局变量的作用范围的关键字,该关键字告诉编译器,其申明的函数和变量可以在本模块或其他模块中使用。记住,语句:extern int a; 仅仅是一个变量的声明,其并不是在定义变量a,也并未为a分配空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出错。 通常来说,在模块的头文件中对本模块提供给其他模块引用的函数和全局变量以关键字extern生命。例如,如果模块B要引用模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但并不会报错;它会在链接阶段从模块A编译生成的目标代码中找到该函数。 extern对应的关键字是static,static表明变量或者函数只能在本模块中使用,因此,被static修饰的变量或者函数不可能被extern C修饰。
  • 被extern “C”修饰的变量和函数是按照C语言方式进行编译和链接的:这点很重要!!!! 上面也提到过,由于C++支持函数重载,而C语言不支持,因此函数被C++编译后在符号库中的名字是与C语言不同的;C++编译后的函数需要加上参数的类型才能唯一标定重载后的函数,而加上extern “C”后,是为了向编译器指明这段代码按照C语言的方式进行编译

未加extern “C”声明时的链接方式:

1
2
3
4
5
6
//模块A头文件 moduleA.h
#idndef _MODULE_A_H
#define _MODULE_A_H

int foo(int x, int y);
#endif

在模块B中调用该函数:

1
2
3
//模块B实现文件 moduleB.cpp
#include"moduleA.h"
foo(2,3);

实际上,在链接阶段,链接器会从模块A生成的目标文件moduleA.obj中找_foo_int_int这样的符号,显然这是不可能找到的,因为foo()函数被编译成了_foo的符号,因此会出现链接错误。

extern “C”的使用要点总结

  1. 可以是如下的单一语句

    1
    extern "C" double sqrt(double);
  2. 可以是复合语句, 相当于复合语句中的声明都加了extern “C”

    1
    2
    3
    4
    5
    extern "C"
    {
    double sqrt(double);
    int min(int, int);
    }
  3. 可以包含头文件,相当于头文件中的声明都加了extern “C”

    1
    2
    3
    4
    extern "C"
    {
    #include <cmath>
    }
  4. 不可以将extern “C” 添加在函数内部 如果函数有多个声明,可以都加extern “C”, 也可以只出现在第一次声明中,后面的声明会接受第一个链接指示符的规则。 除extern “C”, 还有extern “FORTRAN” 等。

C++11对匿名函数的支持

C++11提供了对匿名函数的支持,称为Lambda函数(也叫Lambda表达式). Lambda表达式具体形式如下:

1
[capture](parameters)->return-type{body}

  如果没有参数,空的圆括号()可以省略.返回值也可以省略,如果函数体只由一条return语句组成或返回类型为void的话.形如:

1
[capture](parameters){body}

下面举了几个Lambda函数的例子:

1
2
3
4
5
6
[](int x, int y) { return x + y; } // 隐式返回类型
[](int& x) { ++x; } // 没有return语句 -> lambda 函数的返回类型是'void'
[]() { ++global_x; } // 没有参数,仅访问某个全局变量
[]{ ++global_x; } // 与上一个相同,省略了()
```    
可以像下面这样显示指定返回类型:

[](int x, int y) -> int { int z = x + y; return z; }

1
在这个例子中创建了一个临时变量z来存储中间值. 和普通函数一样,这个中间值不会保存到下次调用. 什么也不返回的Lambda函数可以省略返回类型, 而不需要使用 -> void 形式.Lambda函数可以引用在它之外声明的变量. 这些变量的集合叫做一个闭包. 闭包被定义在Lambda表达式声明中的方括号[]内. 这个机制允许这些变量被按值或按引用捕获.下面这些例子就是:

[] //未定义变量.试图在Lambda内使用任何外部变量都是错误的. [x, &y] //x 按值捕获, y 按引用捕获. [&] //用到的任何外部变量都隐式按引用捕获 [=] //用到的任何外部变量都隐式按值捕获 [&, x] //x显式地按值捕获. 其它变量按引用捕获 [=, &z] //z按引用捕获. 其它变量按值捕获

1
接下来的两个例子演示了Lambda表达式的用法.

std::vector some_list; int total = 0; for (int i=0;i<5;++i) some_list.push_back(i); std::for_each(begin(some_list), end(some_list), [&total](int x) { total += x; });

1
此例计算list中所有元素的总和. 变量total被存为lambda函数闭包的一部分. 因为它是栈变量(局部变量)total的引用,所以可以改变它的值.

std::vector some_list; int total = 0; int value = 5; std::for_each(begin(some_list), end(some_list), [&, value, this](int x) { total += x * value * this->some_func(); }); ```

坚持原创技术分享,您的支持将鼓励我继续创作!