C++在VC++(Windows)与GCC/LLVM(macOS/Linux)上的一些区别

6 篇文章 0 订阅

在此,把C++在不同编译器,即VC++与GCC(或LLVM)上的一些区别记录下来,以备查阅。

(注:以下VC++均是指VC++ 10.)

基本数据类型、const关键字

1. 基本数据类型的大小

C++基本数据类型的大小对比
WindowsmacOS/Linux
wchar_t16 bits32 bits
long32 bits64 bits

影响:对程序在文件的保存和读取上可能会造成问题。如在Windows上保存文件,然后在macOS/Linux上读取,或者在macOS/Linux上保存文件,然后在Windows上读取。我们知道读取文件时,我们一般会使用到文件中的数据地址的偏移量来定位数据,对于读取32 bits和64 bits的整数,显然其造成的地址偏移量是不一样的。

2. const引起的虚函数重载相关的编译问题

我们知道子类可以重载(override)父类的虚函数,但如果我们在虚函数末尾添加一个const,则会产生一个全新的函数,而不是重载原有的父类的虚函数。

然而,如果在子类的虚函数末尾漏写了const,不同的编译器则有不同处理方式。这个区别需要谨慎对待,因为其造成的问题,不易被发现,甚至只能通过调试才能发现。我们知道在运行阶段(如测试、调试)发现问题,比在编译阶段发现问题,成本高出太多。如果在编译阶段发现问题,一般来说编译器会“强迫”程序员当即解决问题。

  • VC++:会忽略漏写的const,不报编译错误
  • GCC/LLVM:能正确地报出编译错误

C++多态

如果一个类中所有的虚函数为内联函数,那么:

1. 向下转型(dynamic_cast)

  • VC++:运行dynamic_cast会得到期望的结果。
  • GCC/LLVM:运行dynamic_cast可能不会得到您期望的结果。

原因分析:dynamic_cast依赖于类(class)的虚函数表地址(vtable: virtual function table)。如果把所有的虚函数内联,那么在某些情况下,虚函数表地址会被优化掉,所以造成dynamic_cast无法得出正确的结果。

2. 内联构造函数(Inline constructor)

  • VC++:不管构造函数是否内联,在使用该构造函数时不会造成明显的区别
  • GCC/LLVM:把构造函数内联在某些情况下会造成链接错误(link error)。我们知道链接错误比较难定位,特别是对于这种少见的链接错误,调查起来是比较耗时的。如果对C++了解不够深刻的话,甚至有无从下手的感觉。解决办法就是把内联的构造函数改成非内联的。

原因分析:构造函数可能会把虚函数表的地址作为一个隐藏的参数,就像"this"指针,也是一个隐藏的参数。在任何.cpp文件(编译单元)中使用一个类(class)的构造函数后,链接器(linker)必须知道这个类(class)的虚函数表的地址,否则链接失败。但是,这个vtable隐藏参数可能已被编译器优化掉了,所以链接器在链接时再也找不到这个参数了。

RTTI

在不同的编译器上,运行std::type_info返回的结果不一致,如下所示:

RTTI结果对比
ClassClassFoo
VC++ RTTI Result"class ClassFoo"
GCC RTTI Result"8ClassFoo"

解决办法是写一个我们自己的type_name函数,把std::type_info的返回结果进行如下修正:

template<class T>
static const char* type_name(T& t)
{
    const std::type_info& ti = typeid(t);
    if (strncmp(ti.name(), "class ", 6) == 0)
    {
        return ti.name() + 6;
    }
    else
    {
        return ti.name();
    }
}

模板(template)的参数

我们知道,使用模板以后,在编译阶段需要对模板的参数进行解析,最终生成所需要的函数:

  • VC++:模板是在使用时进行解析的,所以模板实参的头文件可以不包含。
  • GCC/LLVM:模板是在声明及定义时进行解析的,所以模板实参的头文件必须包含。

在调用(call)模板(参数)时:

  • VC++:不需要在调用(call)的前面加上typename
  • GCC/LLVM:必须在调用(call)的前面加上typename

注:我们可以尝试使用编译参数“-fdelayed-template-parsing”来解决以上模板编译解析的问题,但可惜的是它与C++11 runtime library有冲突,所以并不建议使用它。

STL

STL的容器(container)很多都提供erase(iterator)方法,如std::map::erase(iterator)等。但是不同的编译器提供的实现有一个比较有趣的区别:

  • VC++:erase方法有返回值,它返回的是下一个有效的迭代器(iterator)
  • GCC/LLVM:erase方法没有返回值

这个区别造成的问题是,在循环遍历容器时,调用了erase(iterator)方法之后,如何正确的移动到下一个迭代器,比如这段代码在GCC/LLVM上有编译错误:

for (iterator it = container.begin(); it != container.end(); it++)
{
    // do something with it...

    if (needToErase)
    {
        it = container.erase(it);
    }
    else
    {
        // do something with it...
    }
}

可以用以下方法解决:

for (iterator it = container.begin(); it != container.end(); )
{
    // do something with it...
    
    if (needToErase)
    {
        container.erase(it++); 
    }
    else
    {
        // do something with it...
        
        it++;
    }
}

编译速度

我的体会是在macOS的上使用LLVM编译C++代码,比在Windows上使用VC++的速度快很多。

编译速度对比
代码行数(Lines of code)大于一百万行
Windows(多台电脑集合编译,20~50个CPU可用)1小时
Mac Server(一台电脑,一个12核CPU,内存12GB)20分钟

异常处理(Exception handling)

对于操作系统级别的异常处理,不同编译器的支持也有区别:

  • VC++:提供__try / __except / __finally,用法跟C++的try / catch / finally类似,但是能捕获一些操作系统级别的异常。比如最常见的对空指针的访问造成的异常,如果不处理则会直接导致程序崩溃。
  • GCC/LLVM:需使用UNIX signals

注:对空指针的访问造成的异常不是C++语言级别的异常,所以使用C++的本身的try / catch / finally是捕获不到该异常的。C++的本身的try / catch / finally能捕获的异常是调用C++本身的throw所抛出的异常。

结语

总的来说,给我的感觉是,GCC/LLVM会更严格地按照C++语言的标准来检查编译问题,而VC++会容忍并支持一些标准之外但有用的语法糖。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值