Problem 30 关于C和C++混合编程问题?
Ans:
如果C++程序要调用已经被编译后的C函数,该怎么办?
假设某个C函数的声明如下:
void foo(int x, int y);
该函数被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字用来支持函数重载和类型安全连接。由于编译后的名字不同,C++程序不能直接调用C函数。C++提供了一个C连接交换指定符号extern“C”来解决这个问题。例如:
extern “C”
{
void foo(int x, int y);
… // 其它函数
}或者写成
extern “C”
{
#include “myheader.h”
… // 其它C头文件
}
这就告诉C++编译译器,函数foo是个C连接,应该到库中找名字_foo而不是找_foo_int_int。C++编译器开发商已经对C标准库的头文件作了extern“C”处理,所以我们可以用#include 直接引用这些头文件。
--------------------------------------------------------延深----------------------------------------------------------------
extern int a;
仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配内存空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。
与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。
被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
#include "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及其它语言的混合编程。
明白了C++中extern "C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧。
4.extern "C"的惯用法
(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:
extern "C"
{
#include "cExample.h"
}
而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。
笔者编写的C++引用C函数例子工程中包含的三个文件的源代码如下:
/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
return x + y;
}
// c++实现文件,调用add:cppFile.cpp
extern "C"
{
#include "cExample.h"
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" { }。
(2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅在C文件中将C++中定义的extern "C"函数声明为extern类型。
笔者编写的C引用C++函数例子工程中包含的三个文件的源代码如下:
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}
如果深入理解了第3节中所阐述的extern "C"在编译和连接阶段发挥的作用,就能真正理解本节所阐述的从C++引用C函数和C引用C++函数的惯用法。对第4节给出的示例代码,需要特别留意各个细节。
补充:在C中调用C++的类方法
在C程序 中不能创建一个类的实例,从而不能直接调用类的方法, 为了从一个C程序 中调用一个类的方法,可以采用下面的思路:
1. Create an instance of a C++ class
2. Call its methods, pass arguments, etc.
3. Destroy the created instance.
下面讲看一个例子:
// cppclass.cpp
class CppClass
{
public:
char *returnHello()
{
char *ret_val = "Hello";
return ret_val;
}
};
因为我们不能在一个C程序 中直接创建一个类对象, 所以必须提供可供我们调用的Wrapper functions。对于每个公共方法,我们都必须提供这样的wrapper functions.
(Add the following lines to cppclass.cpp file)
extern "C" void *create_cppclass()
{
return new CppClass();
}
extern "C" char *call_return_hello(void *obj)
{
return reinterpret_cast<CppClass*>(obj)->returnHello();
}
extern "C" void *free_cppclass(void *obj)
{
delete reinterpret_cast<CppClass*>(obj);
}
有几点需要注意:
· All these wrapper functions are preceded with extern "C" (uppercase C)
· In create_cppclass() wrapper function, the newly created object of CppClass is returned as a void pointer (void *). Void pointers can be used to point to any type. So when we return the instance as a pointer to void in this function, it can stored it in our C program. That's the whole point here.
· In call_return_hello() and free_cppclass() wrapper functions, we use reinterpret_cast to convert the void pointer back to CppClass pointer type. The -> operator is used for dereferencing the pointers instead of the . (dot) operator.
Now, let's call these functions from our C program. We should declare these functions first in our C program before we can use them.
// cfile.c
#include <stdio.h>
extern void *create_cppclass();
extern char *call_return_hello(void *obj);
extern void *free_cppclass(void *obj);
main()
{
void *obj = create_cppclass();
char *hello = call_return_hello(obj);
free_cppclass(obj);
printf("%s/n", hello);
}
Now, I'll use g++ to compile:
g++ -o go cfile.c cppclass.cpp
./go
-o indicates the executable output file name which is "go" here. Write the code and try out for yourself. It should print "Hello"
Problem 31 some useful small programs to extract words from text?
Ans:
1. strtok
// crt_strtok.c
// compile with: /W3
// In this program, a loop uses strtok
// to print all the tokens (separated by commas
// or blanks) in the string named "string".
//
#include <string.h>
#include <stdio.h>
char string[] = "A string/tof ,,tokens/nand some more tokens";
char seps[] = " ,/t/n";
char *token;
int main( void )
{
printf( "Tokens:/n" );
// Establish string and get the first token:
token = strtok( string, seps ); // C4996
// Note: strtok is deprecated; consider using strtok_s instead
while( token != NULL )
{
// While there are tokens in "string"
printf( " %s/n", token );
// Get next token:
token = strtok( NULL, seps ); // C4996
}
}
Problem 32 #progma interface and #progma implementation?
Ans:
PRAGMAS
GNU C++支持两条`#pragma'指令使同一个头文件有两个用途:对象类的接口定义, 对象类完整的内容定义.
#pragma interface
(仅对C++)在定义对象类的头文件中,使用这个指令可以节省大部分采用该类的目标文件的大小.一般说来,某些信息 (内嵌成员函数的备份副件,调试信息,实现虚函数的内部表格等)的本地副件必须保存在包含类定义的各个目标文件中.使用这个 pragma指令能够避免这样的复制.当编译中引用包含`#pragma interface'指令的头文件时,就 不会产生这些辅助信息(除非输入的主文件使用了`#pragma implementation'指令).作为替代,目标文件 将包含可被连接时解析的引用(reference).
#pragma implementation
#pragma implementation "objects.h"
(仅对C++)如果要求从头文件产生完整的输出(并且全局可见),你应该在主输入文件中使用这条pragma.头文件 中应该依次使用`#pragma interface'指令.在implementation文件中将产生全部内嵌成员函数 的备份,调试信息,实现虚函数的内部表格等.
如果`#pragma implementation'不带参数,它指的是和源文件有相同基本名的包含文件;例如, `allclass.cc'中, `#pragma implementation'等于`#pragma implementation allclass.h'.如果某个implementation文件需要从多个头文件引入代码,就应该 使用这个字符串参数.
不可能把一个头文件里面的内容分割到多个implementation文件中.
Problem 33 how to override operator new and operator delete?
Ans:
声明:
static void *operator new(size_t objSize, size_t bufferSize);
static void operator delete(void *pObj, size_t bufferSize);
实现:
void *CIOBuffer::operator new(size_t objectSize, size_t bufferSize)
{
void *pMem = new char[objectSize + bufferSize];
return pMem;
}
void CIOBuffer::operator delete(void *pObject, size_t /* bufferSize*/)
{
delete [] pObject;
}
调用:
pBuffer = new(m_bufferSize)CIOBuffer(*this, m_bufferSize);
Problem 34 zero-sized array?
Ans:
作用:可以实现大小可变的结构体(类)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct aa{
int a;
int b;
};
struct bb{
struct aa test[0];
};
int main(void)
{
struct bb *p=(struct bb*)malloc(sizeof(struct bb)+sizeof(struct aa)*100);
p->test[0].a=10;
p->test[0].b=20;
printf("%d,%d/n",p->test[0].a,p->test[0].b);
return 0;
}
看这个结构体的定义:
typedef struct st_type
{
int nCnt;
int item[0];
}type_a;
(有些编译器会报错无法编译可以改成:)
typedef struct st_type
{
int nCnt;
int item[];
}type_a;
这样我们就可以定义一个可变长的结构,用sizeof(type_a)得到的只有4,就是sizeof(nCnt)=sizeof(int)那个0个元素的数组没有占用空间,而后我们可以进行变长操作了。
C语言版: type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
C++语言版: type_a *p = (type_a*)new char[sizeof(type_a)+100*sizeof(int)];
这样我们就产生了一个长为100的type_a类型的东西用p->item[n]就能简单地访问可变长元素,原理十分简单,分配了比sizeof(type_a)多的内存后int item[];就有了其意义了,它指向的是int nCnt;后面的内容,是没有内存需要的,而在分配时多分配的内存就可以由其来操控,是个十分好用的技巧。
而释放同样简单:
C语言版:free(p);
C++语言版:delete []p;
这个被称为灵活/弹性数组成员(fleible array member)C89不支持这种东西,C99把它作为一种特例加入了标准。但是,C99所支持的是incomplete type,而不是zero array,形同int item[0];这种形式是非法的,C99支持的形式是形同int item[];只不过有些编译器把int item[0];作为非标准扩展来支持,而且在C99发布之前已经有了这种非标准扩展了,C99发布之后,有些编译器把两者合而为一。
Problem 35 how to Registering a Control Handler Function in a console application in order to intercept CTRL+C event or other?
Ans:
#include <windows.h>
#include <stdio.h>
BOOL CtrlHandler( DWORD fdwCtrlType )
{
switch( fdwCtrlType )
{
// Handle the CTRL-C signal.
case CTRL_C_EVENT:
printf( "Ctrl-C event/n/n" );
Beep( 750, 300 );
return( TRUE );
// CTRL-CLOSE: confirm that the user wants to exit.
case CTRL_CLOSE_EVENT:
Beep( 600, 200 );
printf( "Ctrl-Close event/n/n" );
return( TRUE );
// Pass other signals to the next handler.
case CTRL_BREAK_EVENT:
Beep( 900, 200 );
printf( "Ctrl-Break event/n/n" );
return FALSE;
case CTRL_LOGOFF_EVENT:
Beep( 1000, 200 );
printf( "Ctrl-Logoff event/n/n" );
return FALSE;
case CTRL_SHUTDOWN_EVENT:
Beep( 750, 500 );
printf( "Ctrl-Shutdown event/n/n" );
return FALSE;
default:
return FALSE;
}
}
void main( void )
{
if( SetConsoleCtrlHandler( (PHANDLER_ROUTINE) CtrlHandler, TRUE ) )
{
printf( "/nThe Control Handler is installed./n" );
printf( "/n -- Now try pressing Ctrl+C or Ctrl+Break, or" );
printf( "/n try logging off or closing the console.../n" );
printf( "/n(...waiting in a loop for events...)/n/n" );
while( 1 ){ }
}
else
printf( "/nERROR: Could not set control handler");
}