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() { /* Create the mutex and reset thread count. */ mutexObject = CreateMutex( NULL, FALSE, NULL ); /* Cleared */ 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 ); /* Tell thread to die. */ 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];
/* Create independent threads each of which will execute functionC */
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)以及实现特定的特性(如线程)。本系列文章介绍了移植工作的众多方面。有关这个主题的更深入信息,请参见参考资料部分。