Visual C++ 和 GNU g++ 都为 cl 编译器提供了一些选项。尽管您可以使用 cl 作为独立的工具进行编译工作,但是,Visual C++ 提供了一种灵活的集成开发环境 (IDE) 以设置编译器选项。使用 Visual Studio® 开发的软件通常使用了一些编辑器特定的和平台相关的特性,可以使用编译器或者连接器来控制这些特性。当您在不同的平台(使用了不同的编译器或者工具链)之 间移植源代码的时候,了解编译器的相关选项,这一点是非常重要的。这部分内容深入分析了一些最有价值的编译器选项。
启用字符串池
可以考虑下面的代码片段:
char *string1= "This is a character buffer";
char *string2= "This is a character buffer";
如果在 Visual C++ 中启用了字符串池选项 [/GF],那么在执行期间,将在程序的映像中仅保存该字符串的单个副本,且 string1 与 string2 相等。需要说明的是,g++ 的行为正好与它相反,在缺省情况下,string1 与 string2 相等。要在 g++ 中禁用字符串池,您必须将 -fwritable-strings 选项添加到 g++ 命令行。
使用 wchar_t
C++ 标准定义了 wchar_t 宽字符类型。如果将 /Zc:wchar_t 选项传递给编译器,那么 Visual C++ 会将 wchar_t 作为本地类型。否则,需要包含一些实现特定的 Header,如 windows.h 或者一些标准的 Header(如 wchar.h)。g++ 支持本地 wchar_t 类型,并且不需要包括特定的 Header。请注意,在不同的平台之间,wchar_t 的大小是不相同的。您可以使用 -fshort-wchar g++ 选项将 wchar_t 的大小强制规定为两个字节。
C++ 运行时类型识别 (Run Time Type Identification) 的支持
如果源代码没有使用 dynamic_cast 或者 typeid 操作符,那么就可以禁用运行时类型识别 (RTTI)。在缺省情况下,Visual Studio 2005 中打开了 RTTI(即 /GR 开关处于打开状态)。可以使用 /GR- 开关在 Visual Studio 环境中禁用 RTTI。禁用 RTTI 可能有助于产生更小的可执行文件。请注意,在包含 dynamic_cast 或者 typeid 的代码中禁用 RTTI,可能会产生一些负面的影响,包括代码崩溃。可以考虑清单 1 中的代码片段。
清单 1. 演示 RTTI 的代码片段
#include <iostream>
struct A {
virtual void f()
{ std::cout << "A::f\n"; }
};
struct B : A {
virtual void f()
{ std::cout << "B::f\n"; }
};
struct C : B {
virtual void f()
{ std::cout << "C::f\n"; }
};
int main (int argc, char** argv )
{
A* pa = new C;
B* pb = dynamic_cast<B*> (pa);
if (pb)
pb->f();
return 0;
}
为在 Visual Studio IDE 之外独立的 cl 编译器中编译这个代码片段,需要显式地打开 /GR 切换开关。与 cl 不同,g++ 编译器不需要任何特殊的选项以打开 RTTI。然而,与 Visual Studio 中的 /GR- 选项一样,g++ 提供了 -fno-rtti 选项,用以显式地关闭 RTTI。在 g++ 中使用 -fno-rtti选项编译这个代码片段,将报告编译错误。然而,即使 cl 在编译这个代码时不使用 /GR 选项,但是生成的可执行文件在运行时将会崩溃。
异常处理
要在 cl 中启用异常处理,可以使用 /GX 编译器选项或者 /EHsc。如果不使用这两个选项,try 和 catch 代码仍然可以执行,并且系统执行到throw 语句时才会调用局部对象的析构函数。异常处理会带来性能损失。因为编译器将为每个 C++ 函数生成进行堆展开的代码,这种需求将导致更大的可执行文件、更慢的运行代码。对于特定的项目,有时无法接受这种性能损失,那么您需要关闭该特性。要禁用 异常处理,您需要从源代码中删除所有的 try 和 catch 块,并使用 /GX- 选项编译代码。在缺省情况下,g++ 编译器启用了异常处理。将 -fno-exceptions 选项传递给 g++,会产生所需的效果。请注意,对包含 try、catch 和 throw 关键字的源代码使用这个选项,可能会导致编译错误。您仍然需要手工地从源代码中删除 try 和 catch 块(如果有的话),然后将这个选项传递给 g++。可以考虑清单 2 中的代码。
清单 2. 演示异常处理的代码片段
#include <iostream>
using namespace std;
class A { public: ~A () { cout << "Destroying A "; } };
void f1 () { A a; throw 2; }
int main (int argc, char** argv ) {
try { f1 (); } catch (...) { cout << "Caught!\n"; }
return 0;
}
下面是 cl 和 g++ 在使用以及不使用该部分中所介绍的相关选项时得到的输出结果:
cl 使用 /GX 选项: Destroying A Caught!
cl 不使用 /GX 选项: Caught!
g++ 不使用 -fno-exceptions: Destroying A Caught!
g++ 使用 -fno-exceptions:编译时间错误
循环的一致性
对于循环的一致性,可以考虑清单 3 中的代码片段。
清单 3. for 循环的一致性
int main (int argc, char** argv )
{
for (int i=0; i<5; i++);
i = 7;
return 0;
}
根据 ISO C++ 的指导原则,这个代码将无法通过编译,因为作为循环中的一部分而声明的 i 局部变量的范围仅限于该循环体,并且在该循环之外是不能进行访问的。在缺省情况下,cl 将完成这个代码的编译,而不会产生任何错误。然而,如果 cl 使用 /Zc:forScope 选项,将导致编译错误。g++ 的行为正好与 cl 相反,对于这个测试将产生下面的错误:
error: name lookup of ‘i’ changed for new ISO ‘for’ scoping
要想禁止这个行为,您可以在编译期间使用 -fno-for-scope 标志。
使用 g++ 属性
Visual C++ 和 GNU g++ 都为语言提供了一些非标准的扩展。g++ 属性机制非常适合于对 Visual C++ 代码中的平台特定的特性进行移植。属性语法采用格式 attribute ((attribute-list)),其中属性列表是以逗号分隔的多个属性组成的列表。该属性列表中的单个元素可以是一个单词,或者是一个单词后面紧跟使用括号括起来的、该属性的可能的参数。这部分研究了如何在移植操作中使用这些属性。
函数的调用约定
您可以使用 Visual Studio 中特定的关键字,如__cdecl、__stdcall 和 __fastcall,以便向编译器说明函数的调用约定。表 1 对有关的详细内容进行了汇总。
表 1. Windows 环境中的调用约定
调用约定 隐含的语义
__cdecl(cl 选项:/Gd) 从右到左地将被调用函数的参数压入堆栈。在执行完毕之后,由调用函数将参数弹出堆栈。
__stdcall(cl 选项:/Gz) 从右到左地将被调用函数的参数压入堆栈。在执行完毕之后,由调用函数将参数弹出堆栈。
__fastcall(cl 选项:/Gr) 将最前面的两个参数传递到 ECX 和 EDX 寄存器中,同时将所有其他参数从右到左地压入堆栈。由被调用函数负责清除执行后的堆栈。
用以表示相同行为的 g++ 属性是 cdecl、stdcall 和 fastcall。清单 4 显示了 Windows® 和 UNIX® 中属性声明风格的细微差别。
清单 4. Windows 和 UNIX 中的属性声明风格
Visual C++ Style Declaration:
double __stdcall compute(double d1, double d2);
g++ Style Declaration:
double attribute((stdcall)) compute(double d1, double d2);
结构成员对齐
/Zpn 结构成员对齐选项可以控制结构在内存中的对齐方式。例如,/Zp8 以 8 个字节为单位对结构进行对齐(这也是缺省的方式),而 /Zp16则以 16 个字节为单位对结构进行对齐。您可以使用 aligned g++ 属性来指定变量的对齐方式,如清单 5 中所示。
清单 5. Windows 和 UNIX 中结构成员的对齐方式
Visual C++ Style Declaration with /Zp8 switch:
struct T1 { int n1; double d1;};
g++ Style Declaration:
struct T1 { int n1; double d1;} attribute((aligned(8)));
然而,对齐属性的有效性将受到固有的连接器局限性的限制。在许多系统中,连接器只能够以某个最大的对齐方式对变量进行对齐。
Visual C++ declspec nothrow 属性
这个属性可以告诉编译器,使用该属性声明的函数以及它调用的后续函数都不会引发异常。使用这个特性可以对减少整体代码的大小进行优化,因为在缺省情况下,即使代码不会引发异常,cl 仍然会为 C++ 源代码生成堆栈展开信息。您可以使用 nothrow g++ 属性以实现类似的目的,如清单 6 中所示。
清单 6. Windows 和 UNIX 中的 nothrow 属性
Visual C++ Style Declaration:
double __declspec(nothrow) sqrt(double d1);
g++ Style Declaration:
double attribute((nothrow)) sqrt(double d1);
一种更加具有可移植性的方法是,使用标准定义的风格: double sqrt(double d1) throw ();.
Visual C++ 和 g++ 之间相似的内容
除了前面的一些示例之外,Visual C++ 和 g++ 属性方案之间还存在一些相似的内容。例如,这两种编译器都支持noinline、noreturn、deprecated 和 naked 属性。
从 32 位的 Windows 移植到 64 位的 UNIX 环境时的潜在缺陷
在 Win32 系统中开发的 C++ 代码是基于 ILP32 模型的,在该模型中,int、long 和指针类型都是 32 位的。UNIX 系统则遵循 LP64 模型,其中 long 和指针类型都是 64 位的,但是 int 仍然保持为 32 位。大部分的代码破坏,都是由于这种更改所导致的。这部分简要讨论了您可能会遇到的两个最基本的问题。从 32 位到 64 位系统的移植是一个非常广阔的研究领域。有关这个主题的更多信息,请参见参考资料部分。
数据类型大小方面的差别
某些数据类型在 ILP32 和 LP64 模型中是相同的,使用这样的数据类型才是合理的做法。通常,您应该尽可能地避免使用 long 和 pointer 数据。另外,通常我们会使用 sys/types.h 标准 Header 中定义的数据类型,但是这个文件中的一些数据类型(如 ptrdiff_t, size_t 等等)的大小,在 32 位模型和 64 位模型之间是不一样的,您在使用时必须小心。
个别数据结构的内存需求
个别数据结构的内存需求可能会发生改变,这依赖于编译器中实现对齐的方式。可以考虑清单 7 中的代码片段。
清单 7. 错误的结构成员对齐方式
struct s {
int var1; // hole between var1 and var2
long var2;
int var3; // hole between var3 and ptr1
char* ptr1;
};
// sizeof(s) = 32 bytes
在 LP64 模型中,long 和 pointer 类型都以 64 位为单位进行对齐。另外,结构的大小以其中最大成员的大小为单位进行对齐。在这个示例中,结构 s 以 8 个字节为单位进行对齐,s.var2 变量同样也是如此。这将导致在该结构中出现一些空白的地方,从而使内存膨胀。清单 8 中的重新排列导致该结构的大小变为 24 个字节。
清单 8. 正确的结构成员对齐方式
struct s {
int var1;
int var3;
long var2;
char* ptr1;
};
// sizeof(s) = 24 bytes
移植多线程的应用程序
从技术上讲,一个线程是操作系统可以调度运行的独立指令流。在这两种环境中,线程都位于进程之中,并且使用进程的资源。只要线程的父进程存在,并且 操作系统支持线程,那么线程将具有它自己的独立控制流。它可能与其他独立(或者非独立)使用的线程共享进程资源,如果它的父进程结束,那么它也将结束。下 面对一些典型的应用程序接口 (API) 进行了概述,您可以使用这些 API 在 Windows 和 UNIX 环境中建立多线程的项目。对于 WIN32 API,所选择的接口是 C 运行时例程,考虑到简单性和清晰性,这些例程符合可移植操作系统接口(Portable Operating System Interface,POSIX)的线程。
请注意:由于本文篇幅有限,我们不可能为编写多线程应用程序的其他方式提供详细的介绍。
创建线程
Windows 使用 C 运行时库函数中的 _beginthread API 来创建线程。您还可以使用一些其他的 Win32 API 来创建线程,但是在后续的内容中,您将仅使用 C 运行时库函数。顾名思义,_beginthread() 函数可以创建一个执行例程的线程,其中将指向该例程的指针作为第一个参数。这个例程使用了 __cdecl C 声明调用约定,并返回空值。当线程从这个例程中返回时,它将会终止。
在 UNIX 中,可以使用 pthread_create() 函数完成相同的任务。pthread_create() 子程序使用线程参数返回新的线程 ID。调用者可以使用这个线程 ID,以便对该线程执行各种操作。检查这个 ID,以确保该线程存在。
删除线程
_endthread 函数可以终止由 _beginthread() 创建的线程。当线程的顺序执行完成时,该线程将自动终止。如果需要在线程中根据某个条件终止它的执行,那么 _endthread() 函数是非常有用的。
在 UNIX 中,可以使用 pthread_exit() 函数实现相同的任务。如果正常的顺序执行尚未完成,这个函数将退出线程。如果 main() 在它创建的线程之前完成,并使用 pthread_exit() 退出,那么其他线程将继续执行。否则,当 main() 完成的时候,其他线程将自动终止。
线程中的同步
要实现同步,您可以使用互斥信号量。在 Windows 中,CreateMutex() 可以创建互斥信号量。它将返回一个句柄,任何需要互斥信号量对象的函数都可以使用这个句柄,因为对这个互斥信号量提供了所有的访问权限。当拥有这个互斥信号量的线程不再需要它的时候,可以调用ReleaseMutex(),以便将它释放回系统。如果调用线程并不拥有这个互斥信号量,那么这个函数的执行将会失败。
在 UNIX 中,可以使用 pthread_mutex_init() 例程动态地创建一个互斥信号量。这个方法允许您设置互斥信号量对象的相关属性。或者,当通过 pthread_mutex_t 变量声明它的时候,可以静态地创建它。要释放一个不再需要的互斥信号量对象,可以使用 pthread_mutex_destroy()。
移植多线程应用程序的工作示例
既然您已经掌握了本文前面所介绍的内容,下面让我们来看一个小程序示例,该程序使用在主进程中执行的不同线程向控制台输出信息。清单 9是 multithread.cpp 的源代码。
清单 9. multithread.cpp 的源代码
#include <stdio.h>
#include <stdlib.h>
#ifdef WIN32
#include <windows.h>
#include <string.h>
#include <conio.h>
#include <process.h>
#else
#include <pthread.h>
#endif
#define MAX_THREADS 32
#ifdef WIN32
void InitWinApp();
void WinThreadFunction( void* );
void ShutDown();
HANDLE mutexObject;
#else
void InitUNIXApp();
void* UNIXThreadFunction( void *argPointer );
pthread_mutex_t mutexObject = PTHREAD_MUTEX_INITIALIZER;
#endif
int threadsStarted; // Number of threads started
int main()
{
#ifdef WIN32
InitWinApp();
#else
InitUNIXApp();
#endif
}
#ifdef WIN32
void InitWinApp()
{
mutexObject = CreateMutex( NULL, FALSE, NULL );
if(mutexObject == NULL && GetLastError() != ERROR_SUCCESS)
{
printf("failed to obtain a proper mutex for multithreaded application");
exit(1);
}
threadsStarted = 0;
for(;threadsStarted < 5 && threadsStarted < MAX_THREADS;
threadsStarted++)
{
_beginthread( WinThreadFunction, 0, &threadsStarted );
}
ShutDown();
CloseHandle( mutexObject );
getchar();
}
void ShutDown()
{
while ( threadsStarted > 0 )
{
ReleaseMutex( mutexObject );
threadsStarted--;
}
}
void WinThreadFunction( void *argPointer )
{
WaitForSingleObject( mutexObject, INFINITE );
printf("We are inside a thread\n");
ReleaseMutex(mutexObject);
}
#else
void InitUNIXApp()
{
int count = 0, rc;
pthread_t threads[5];
while(count < 5)
{
rc = pthread_create(&threads[count], NULL, &UNIXThreadFunction, NULL);
if(rc)
{
printf("thread creation failed");
exit(1);
}
count++;
}
// We will have to wait for the threads to finish execution otherwise
// terminating the main program will terminate all the threads it spawned
for(;count >= 0;count--)
{
pthread_join( threads[count], NULL);
}
//Note : To destroy a thread explicitly pthread_exit() function can be used
//but since the thread gets terminated automatically on execution we did
//not make explicit calls to pthread_exit();
exit(0);
}
void* UNIXThreadFunction( void *argPointer )
{
pthread_mutex_lock( &mutexObject );
printf("We are inside a thread\n");
pthread_mutex_unlock( &mutexObject );
}
#endif
我们利用 Visual Studio Toolkit 2003 和 Microsoft Windows 2000 Service Pack 4 通过下面的命令行对 multithread.cpp 的源代码进行了测试:
cl multithread.cpp /DWIN32 /DMT /TP
我们还在使用 g++ 编译器版本 3.4.4 的 UNIX 平台中通过下面的命令行对它进行了测试:
g++ multithread.cpp -DUNIX -lpthread
清单 10 是该程序在两种环境中的输出。
清单 10. multithread.cpp 的输出
We are inside a thread
We are inside a thread
We are inside a thread
We are inside a thread
We are inside a thread
结束语
在两种完全不同的平台(如 Windows 和 UNIX)之间进行移植,需要了解多个领域的知识,包括了解编译器和它们的选项、平台特定的特性(如 DLL)以及实现特定的特性(如线程)。本系列文章介绍了移植工作的众多方面。有关这个主题的更深入信息,请参见参考资料部分。