1. 关于#和##
1.1).在C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号。
比如在早期的VLC版本中,有如下宏定义:
- #define STRINGIFY(z) UGLY_KLUDGE(z)
- #define UGLY_KLUDGE(z) #z
- #define MODULE_STRING STRINGIFY(MODULE_NAME)
而在每个各个modules模块下又有对MODULE_NAME的定义,以live555为例如下定义:
- #define MODULE_NAME live555
而在vlc/include/vlc_plugin.h文件中,有如下宏定义:代码1.1
- #define vlc_module_begin()
- EXTERN_SYMBOL DLL_SYMBOL
- int CDECL_SYMBOL __VLC_SYMBOL(vlc_entry) (vlc_set_cb, void *);
- EXTERN_SYMBOL DLL_SYMBOL
- int CDECL_SYMBOL __VLC_SYMBOL(vlc_entry) (vlc_set_cb vlc_set, void *opaque)
- {
- module_t *module;
- module_config_t *config = NULL;
- if (vlc_plugin_set (VLC_MODULE_CREATE, &module))
- goto error;
- if (vlc_module_set (VLC_MODULE_NAME, (MODULE_STRING)))
- goto error;
那么当在live555.cpp中调用vlc_module_begin()宏后,其展开(可通过预编译查看)就为如下形式:代码1.2
- extern "C" int vlc_entry__live555 (vlc_set_cb, void *);
- extern "C" int vlc_entry__live555 (vlc_set_cb vlc_set, void *opaque)
- {
- module_t *module;
- module_config_t *config = __null;
- if (vlc_set (opaque, __null, VLC_MODULE_CREATE, &module)) goto error;
- if (vlc_set (opaque, module, VLC_MODULE_NAME, ("live555"))) goto error;
从上面的可知,实际上MODULE_STRING最后就变成了"live555"。此为#的用法。
1.2)##被称为连接符(concatenator),用来将两个Token连接为一个Token。注意这里连接的对象是Token就行,而不一定是宏的变量。
同样以vlc/include/vlc_plugin.h文件中的宏定义为例,在该文件中有如下宏定义:
- #define CONCATENATE( y, z ) CRUDE_HACK( y, z )
- #define CRUDE_HACK( y, z ) y##__##z
- #ifdef __PLUGIN__
- # define __VLC_SYMBOL( symbol ) CONCATENATE( symbol, MODULE_SYMBOL )
- #else
- # define __VLC_SYMBOL( symbol ) CONCATENATE( symbol, MODULE_NAME )
- #endif
同样以live555.cpp为例,我们注意前面有如下的宏使用:
- int CDECL_SYMBOL __VLC_SYMBOL(vlc_entry) (vlc_set_cb vlc_set, void *opaque)
那么再结合上面的宏定义(注意为CONCATENATE( symbol, MODULE_NAME )),再结合宏定义CRUDE_HACK(y, z) y##__##z,这里对照y即为vlc_entry,z即为live555,则最终展开即为vlc_entry__live555,可以对其live555.cpp进行预编译处理,查看结果即可知,最终结果如上面的代码1.2所示。此为##的使用。
2. 关于变参宏...
...在C宏中称为Variadic Macro,也就是变参宏。比如:
- #define myprintf(templt,...) fprintf(stderr,templt,__VA_ARGS__)
或者
- #define myprintf(templt,args...) fprintf(stderr,templt,args)
第一个宏中由于没有对变参起名,我们用默认的宏__VA_ARGS__来替代它。第二个宏中,我们显式地命名变参为args,那么我们在宏定义中就可以用args来代指变参了。同C语言的stdcall一样,变参必须作为参数表的最有一项出现。当上面的宏中我们只能提供第一个参数templt时,C标准要求我们必须写成:
myprintf(templt,);
的形式。这时的替换过程为:
myprintf("Error!/n",);
替换为:
fprintf(stderr,"Error!/n",);
这是一个语法错误,不能正常编译。这个问题一般有两个解决方法。首先,GNU CPP提供的解决方法允许上面的宏调用写成:
myprintf(templt);
而它将会被通过替换变成:
fprintf(stderr,"Error!/n",);
很明显,这里仍然会产生编译错误(非本例的某些情况下不会产生编译错误)。除了这种方式外,c99和GNU CPP都支持下面的宏定义方式:
#define myprintf(templt, ...) fprintf(stderr,templt, ##__VAR_ARGS__)
这时,##这个连接符号充当的作用就是当__VAR_ARGS__为空的时候,消除前面的那个逗号。那么此时的翻译过程如下:
myprintf(templt);
被转换为:
fprintf(stderr,templt);
这样如果templt合法,将不会产生编译错误。
而在VLC源代码中同样有关于...的使用,比如vlc/include/vlc_plugin.h中有如下定义:
- #define vlc_plugin_set(...) vlc_set (opaque, NULL, __VA_ARGS__)
- #define vlc_module_set(...) vlc_set (opaque, module, __VA_ARGS__)
- #define vlc_config_set(...) vlc_set (opaque, config, __VA_ARGS__)
在代码1.1中有关于其的使用,这里以vlc_module_set为例,代码1.1中使用如下:
vlc_module_set (VLC_MODULE_NAME, (MODULE_STRING))
这里VLC_MODULE_NAME和(MODULE_STRING)即替换变参宏...,那么最终替换的结果即为如下:
vlc_set (opaque, module, VLC_MODULE_NAME, ("live555"))
以上为变参宏...的使用。
3. 关于typedef函数指针
3.1)简单的函数指针的使用
形式1:返回类型(*函数名)(参数表)
- char (*pFun)(int);
- char glFun(int a){ return;}
- void main()
- {
- pFun = glFun;
- (*pFun)(2);
- }
第一行定义了一个指针变量pFun。首先我们根据前面提到的“形式1”认识到它是一个指向某种函数的指针,这种函数参数是一个int型,返回值是char类型。只有第一句我们还无法使用这个指针,因为我们还未对它进行赋值。
第二行定义了一个函数glFun()。该函数正好是一个以int为参数返回char的函数。我们要从指针的层次上理解函数——函数的函数名实际上就是一个指针,函数名指向该函数的代码在内存中的首地址
然后就是main()函数了,它的第一句您应该看得懂了——它将函数glFun的地址赋值给变量pFun。main()函数的第二句中“*pFun”显然是取pFun所指向地址的内容,当然也就是取出了函数glFun()的内容,然后给定参数为2。
3.2)使用typedef
形式1:typedef 返回类型(*新类型)(参数表)
- typedef char (*PTRFUN)(int);
- PTRFUN pFun;
- char glFun(int a){ return;}
- void main()
- {
- pFun = glFun;
- (*pFun)(2);
- }
typedef的功能是定义新的类型。第一句就是定义了一种PTRFUN的类型,并定义这种类型为指向某种函数的指针,这种函数以一个int为参数并返回char类型。后面就可以像使用int,char一样使用PTRFUN了。
第二行的代码便使用这个新类型定义了变量pFun,此时就可以像使用形式1一样使用这个变量了。
3.3)VLC中函数指针的使用
在VLC中大量使用了函数指针,仍然以vlc/include/vlc_plugin.h中为例,有如下定义:
- typedef int (*vlc_set_cb) (void *, void *, int, ...);
仍然以live555为例,经过预编译之后有如下代码(即上面的代码1.2):
- extern "C" int vlc_entry__live555 (vlc_set_cb vlc_set, void *opaque)
- {
- module_t *module;
- module_config_t *config = __null;
- if (vlc_set (opaque, __null, VLC_MODULE_CREATE, &module)) goto error;
- if (vlc_set (opaque, module, VLC_MODULE_NAME, ("live555"))) goto error;
那么这里vlc_set最终指向的函数到底是谁呢?我们从调用vlc_entry__live555()的地方开始。在VLC-Android中定位到vlc/src/modules/bank.c文件中有如下代码:代码1.3
- extern vlc_plugin_cb vlc_static_modules[];
- static void module_InitStaticModules(void)
- {
- if (!vlc_static_modules)
- return;
- for (unsigned i = 0; vlc_static_modules[i]; i++) {
- module_t *module = module_InitStatic (vlc_static_modules[i]);
- if (likely(module != NULL))
- module_StoreBank (module);
- }
- }
而上面的vlc_static_modules[ ]在vlc-android/jni/libvlcjni.h中有如下的定义(这里仅选取一段代码):
- ...
- int vlc_entry__live555 (int (*)(void *, void *, int, ...), void *);
- ...
- const void *vlc_static_modules[] =
- {...
- vlc_entry__avformat,
- vlc_entry__h264,
- vlc_entry__voc,
- vlc_entry__avi,
- vlc_entry__live555,
- ...
- }
这里加入代码1.3运行到vlc_static_modules[i]=vlc_entry_live555,则module_InitStatic的输入参数即为vlc_entry_live555,我们再看module_InitStatic()的源代码如下:
- /**
- * Registers a statically-linked plug-in.
- */
- static module_t *module_InitStatic (vlc_plugin_cb entry)
- {
- /* Initializes the module */
- module_t *module = vlc_plugin_describe (entry);
- if (unlikely(module == NULL))
- return NULL;
- module->b_loaded = true;
- module->b_unloadable = false;
- return module;
- }
可知代码1.3中的参数符合module_InitStatic函数的输入要求,这里的entry实际上就指向vlc_entry__live555()函数,我们再看vlc_plugin_describe()(在vlc/src/modules/entry.c文件中)的源代码如下:
- /**
- * Runs a plug-in descriptor. This loads the plug-in meta-data in memory.
- */
- module_t *vlc_plugin_describe (vlc_plugin_cb entry)
- {
- module_t *module = NULL;
- vlc_object_t *debug=NULL;
- if (entry (vlc_plugin_setter, &module) != 0)
- {
- if (module != NULL) /* partially initialized plug-in... */
- {
- vlc_module_destroy (module);
- }
- module = NULL;
- }
- return module;
- }
注意上面的entry实际上指向vlc_entry__live555()函数,我们再看代码1.2中vlc_entry__live555()经过预编译之后的函数定义,则上面的函数调用实际上就成为如下所示:
vlc_entry__live555 (vlc_plugin_setter, &module)
那么整个调用过程就已经明朗了,最终vlc_set指向的实际上为vlc_plugin_setter()函数,如果我们再回到vlc_entry__live555()的函数定义中,最终的调用代码即为如下:
- extern "C" int vlc_entry__live555 (vlc_set_cb vlc_set, void *opaque)
- {
- module_t *module;
- module_config_t *config = __null;
- if (vlc_plugin_setter (opaque, __null, VLC_MODULE_CREATE, &module)) goto error;
- if (vlc_plugin_setter (opaque, module, VLC_MODULE_NAME, ("live555"))) goto error;
我们再看看vlc_plugin_setter()的函数定义(在vlc/src/modules/entry.c文件中):
- /**
- * Callback for the plugin descriptor functions.
- */
- static int vlc_plugin_setter (void *plugin, void *tgt, int propid, ...)
- {
- module_t **pprimary = plugin;
- module_t *module = tgt;
- module_config_t *item = tgt;
- va_list ap;
- int ret = 0;
- vlc_object_t* debug=NULL;
- va_start (ap, propid);
- switch (propid)
- {
- case VLC_MODULE_CREATE:
- {
- ...
则最终调用的函数代码执行过程就一目了然了。注意这里涉及到变参函数取值的知识,如上面代码中的va_list ap; va_start(ap, propid); 下面做一点解释:
函数参数实际上是以数据结构栈的形式存取,从右至左入栈,因此我们只要知道第一个参数的地址,每一个参数的类型,就可以知道后面每一个参数的地址,这也就是变参宏...的原理。
VA_LIST 是在C语言中解决变参问题的一组宏
它有这么几个成员:
1) va_list型变量:
#ifdef _M_ALPHA
typedef struct {
char *a0; /* pointer to first homed integer argument */
int offset; /* byte offset of next parameter */
} va_list;
#else
typedef char * va_list;
#endif
2)_INTSIZEOF 宏,获取类型占用的空间长度,最小占用长度为int的整数倍:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
3)VA_START宏,获取可变参数列表的第一个参数的地址(ap是类型为va_list的指针,v是可变参数最左边的参数):
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
4)VA_ARG宏,获取可变参数的当前参数,返回指定类型并将指针指向下一参数(t参数描述了当前参数的类型):
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
5)VA_END宏,清空va_list可变参数列表:
#define va_end(ap) ( ap = (va_list)0 )
VA_LIST的用法:
(1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针;
(2)然后用VA_START宏初始化变量刚定义的VA_LIST变量;
(3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数);
(4)最后用VA_END宏结束可变参数的获取。
使用VA_LIST应该注意的问题:
(1)可变参数的类型和个数完全由程序代码控制,它并不能智能地识别不同参数的个数和类型;
(2)如果我们不需要一一详解每个参数,只需要将可变列表拷贝至某个缓冲,可用vsprintf函数;
(3)因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码;
4. 关于extern"C"的使用
我们在上面的代码中看到关于如下的代码:
extern "C" int vlc_entry__live555 (vlc_set_cb, void *);
那么这个extern "C"到底代表什么意思呢?这里作如下解释:
extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。让我们来详细解读这两重含义。
(1)extern "C"限定的函数或变量是extern类型的;
extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:
extern int a;
仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配内存空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。
(2)被extern "C"修饰的变量和函数是按照C语言方式编译和链接的;
未加extern “C”声明时的编译方式
首先看看C++中对类似C的函数是怎样编译的。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。
同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
未加extern "C"声明时的连接方式
假设在C++中,模块A的头文件如下:
// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif
在模块B中引用该函数:
// 模块B实现文件 moduleB.cpp
#i nclude "moduleA.h"
foo(2,3);
实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!
加extern "C"声明后的编译和连接方式
加extern "C"声明后,模块A的头文件变为:
// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo( int x, int y );
#endif
在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:
(1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;
(2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。
如果在模块A中函数声明了foo为extern "C"类型,而模块B中包含的是extern int foo( int x, int y ) ,则模块B找不到模块A中的函数;反之亦然。
所以,可以用一句话概括extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):实现C++与C及其它语言的混合编程。
以上即为在分析VLC源码过程中用到的知识总结。
参考文章:
http://blog.csdn.net/dotphoenix/article/details/4345174
http://blog.csdn.net/qll125596718/article/details/6891881
http://www.cppblog.com/xmoss/archive/2009/07/20/90680.html
http://www.52rd.com/Blog/Archive_Thread.asp?SID=11774