MSC、GCC、LLVM

虽然MSC和GCC同是C/C++编译器,但怎么说还是存在些差别,要让代码能被MSC和GCC成功编译,在写时就要注意些细节。

1、定义嵌套STL类型,右侧要用“> >”而不是“>>”

要定义一种类型:std::pair<int, std::pair<size_t, int>>,在最右侧会同时出现两个结束符“>”。使用“>>”,MSC没问题,但GCC会报错。

2、std::pair中,如果有成员是class或struct,那该成员必须提供没有参数的构造函数

定义std::pair<map_location, unit>,当中map_location和unit不是struct就是class,那么它们都必须提没有参数的构造函数。一旦有一个不存在,MSC没问题,但GCC会报错。

实在不想为unit提供没参数构造函数,那只好使用std::pair<map_location, unit*>定义了。

3、设置函数参数默认值时,不能使用类似std::vector<size_t>()

声明函数,

gui::menu* get_ability_menu(hero_map& heros, std::vector<hero*>& partial_heros = std::vector<hero*>(), int selected = 0, bool checkbox = false, std::vector<size_t>& checked = std::vector<size_t>());

以上声明在MSC没问题,但GCC会报错

要解决这个问题,可以先定义空vector变量。

std::vector<hero*> empty_vector_hero_ptr = std::vector<hero*>();
std::vector<size_t> empty_vector_size_t = std::vector<size_t>();
把它们作为参数值在函数定义中使用
gui::menu* get_ability_menu(hero_map& heros, std::vector<hero*>& partial_heros = empty_vector_hero_ptr, int selected = 0, bool checkbox = false, std::vector<size_t>& checked = empty_vector_size_t);

4、在一些场合,不能省略std::string前的const

void wml_config_from_file(std::string& fname, config &cfg)

以以下方式进行调用:

wml_config_from_file(game_config::path + "/xwml/" + BASENAME_DATA, game_config_);

对于参数fname,它的值是game_config::path + "/xwml/" + BASENAME_DATA,注:game_config::path是std::string类型。

对以上的声明和调用,MSC没问题,但GCC有问题。——如果把fname声明函数中可变,game_config::path + "/xwml/" + BASENAME_DATA是该如何处理呢?

要解决这问题就是把fname严格定义为const std::string& fname。

5、不要把源文件编码为Unicode - Codepage 1200

MSC没问题,GCC会报N个错误。源文件包括*.h,*.hpp,*.c,*.cpp,……

作为解决方法,可换为Chinese Simplified (GB2312) - Codepage 936

6、希望多字节对齐的“char”指针不要使用静态分配

以下定义的变量可能不是4字节对齐的!至少iOS下不是。

unsigned char savegame_cache_[CONSTANT_1M];

要申请出一块4字节对齐地址给一个unsigned char指针可使用malloc。32位系统下,malloc会确保出来的地址是8字节对齐,而64位下是8字节或16字节对齐。(malloc返回的是void*,它这地址需能满足可转换为“所有”元类型。)

7、std::make_pair中enum值到int须显示转换

std::map<const std::string, int> tags;
tags.insert(std::make_pair("attack", (int)ATTACK));

tags的second是int,当中的“ATTACK”是enum,当要把“ATTACK”作为second加入时须执行显示转换,否则Android下的GCC会报错。

jni/../../../src/unit_types.cpp: In function 'void apply_to_tag::fill_tags()':
jni/../../../src/unit_types.cpp:2160:45: error: no matching function for call to
 'make_pair(char const [7], apply_to_tag::<anonymous enum>)'
jni/../../../src/unit_types.cpp:2160:45: note: candidate is:
c:/movie/android/android-ndk-r8d-windows/android-ndk-r8d/sources/cxx-stl/gnu-lib
stdc++/4.6/include/bits/stl_pair.h:272:5: note: template<class _T1, class _T2> s
td::pair<_T1, _T2> std::make_pair(_T1, _T2)

8、要对STL容器使用erase时,不要使用const_iterator

LLVM下(Xcode 4.5.2集成)、GCC,erase std::multimap使用const_iterator编译时会报错,需改为使用iterator。

9、声明友员时须显示加上类型关键字,像class

class default_map_generator : public map_generator
{
	......
	friend class gui2::tmap_generator;
	......
}

如果不在gui2::tmap_generator前加上“class”,MSC、LLVM不会报错,但GCC会报以下错误。

10、for中删除std::map元素时不要使用it = std::map.erase(it)

MSC支持for中it = std::map.erase,GCC的则可能不支持。

for (std::map<...>::iterator it = m.begin(); it != m.end(); ) {
	it = m.erase(it);
}

以上代码会有平台问题,应使用下面格式。

for (std::map<...>::iterator it = m.begin(); it != m.end(); ) {
	m.erase(it ++);
}

其它容器,像std::vector,则两种都支持,而且网上有说“m.erase(it ++)”才是STL推荐使用的格式。但我实测下来,当是std::vector时,在vc2008下,m.erase(it ++)会出怪现象,就是一旦m.erase(it ++)后则一定会满足“it == m.end()”,导致程序达不到自个要求。

  • 对非自动排序容器,像std::vector:使用it = m.erase(it)
  • 对自动排序容器,像std::set、std::map:使用m.erase(it ++)

11、std::string中的back、pop_back方法

MSC使用的STL支持std::string::back获得最后一个字符,pop_back移除最后一个字符,但这两个方法,至少GCC经常使用的gnu_stl是都不支持。

12、尽量不要用snprintf

MSC的各版本对snprintf经常存在不一致。像vs2013使用_snprintf,到vs2015就用函数实现了snprintf。

13、含有对象的std::vector,不要在迭代中改变值

struct titem {
	std::string s;
};

titem是个含有std::string对象的结构,变量items是以它为单元构建的std::vector,不要用类似以下的迭代改变值。

for (std::vector<titem>::iterator it = items.begin(); it != items.end(); ++ it) {
	it.s = "hello";
}

原因是当改变对象值时,有可能会造成STL对items重分配内存。基于同样原因,重分配会影响到指向各单元的指针,因此尽量不要使用指向std::vector的某个单元的指针。

titem* ptr = &items[2];

当items内容发生改变时,ptr有可能变得指向一个错误地址。

14、用{}格式初始化struct时,float到int要进行显示转换

struct SDL_Point {
	int x, y;
};
float pi = 3.14;
SDL_Point point{0, y * pi};

给point赋值的语句用了{}格式,LLVM会报类似下错误。

修正方法是强制类型转换。

SDL_Point point = {0, (int)(y * pi)};

15、int被强制转到size_t,如何由size_t转回int?

int val = (long)(val);

64位平台时size_t是64位,32位平台时size_t是32位,而long的位置和size_t是一致的,int却总是32位。

16、如何把一整数设为特定类型

使用INT64_C、UINT64_C、INT32_C、UINT32_C宏,它们定义在stdint.h。

定义1为int64_t类型
INT64_C(1)

 

17、printf如何输出64位整数

不要使用%lld、%llu,改用在头文件inttypes.h定义的这些宏:PRId64,PRIu64,PRIo64,PRIo64

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h> // for PRId64  PRIu64 PRIo64 PRIo64

int main(int argc, char *argv[])
{
    int64_t  a = 4294967295;
    uint64_t b = 0xFFFFFFFFFFFF;
    printf("a=%"PRId64"\n", a);//64位有符号10进制打印
    printf("b=%"PRIu64"\n", b);//64位无符号10进制打印
    printf("a=%"PRIo64"\n", a);//64位有符号8进制打印
    printf("b=%"PRIo64"\n", b);//64位无符号8进制打印
    return 0;
}

更多参考“C语言printf 64位数(跨平台 32位系统 64位系统)”。

 

18、各平台下sizeof(long)

 win10(32位)win10(64位)android(armeabi-v7a)android(arm64-v8a)ios-x64
sizeof(long)4448 

不要总认为sizeof(long)是8,至少64位windows还是4。一定要用8字节整数的,建议用int64_t或uint64_t。另外,在64位windows,sizeof(size_t)和sizeof(void*)是8。

 

19、std::string是空时,c_str()是否会返回NULL/nullptr

切记不要把nullptr直接赋给std::string!

std::string s = nullptr;
std::string s = NULL;
在windows,以上赋值就会造成app异常退出,其它操作系统未测试。

std::string是空时,Windows、Android时不会。iOS未测试。即使nullptr时,"%s"打印时不会非法,而是显示"(null)"

std::string str;
SDL_Log("s1 c_str %s", str.c_str() != nullptr? "isn't null": "is null");
SDL_Log("s1 c_str: %p, %s", str.c_str(), str.c_str());
str = "abc";
SDL_Log("s2 c_str %s", str.c_str() != nullptr? "isn't null": "is null");
SDL_Log("s2 c_str: %p, %s", str.c_str(), str.c_str());
str.clear();
SDL_Log("s3 c_str %s", str.c_str() != nullptr? "isn't null": "is null");
SDL_Log("s3 c_str: %p, %s", str.c_str(), str.c_str());
const char* ptr = nullptr;
SDL_Log("s4 ptr: %p, %s", ptr, ptr);
ptr = "123";
SDL_Log("s4 ptr: %p, %s", ptr, ptr);
-------输出
s1 c_str isn't null
s1 c_str: 0x9ebff581, 
s2 c_str isn't null
s2 c_str: 0x9ebff581, abc
s3 c_str isn't null
s3 c_str: 0x9ebff581,
s4 ptr: 0, (null)
s4 ptr: 110cf4c, 123

20、多线程时需要同步写int变量

多线程时,如果可能同时写某个int变量,须要给这变量采取同步措施。举个例子,线程A对变量a先后执行a++、a--,线程B也先后执行a++、a--,那也会造成a出现错误结果。

同步措施像加互斥锁,对int/long变量,还有一种方法是使用操作系统提供的api,像winapi中的InterlockedIncrement、InterlockedDecrement,对包含了webrtc的,可使用当中的跨平台函数rtc::AtomicOps::Increment、rtc::AtomicOps::Decrement。

 

21、不要让int64_t转为double,再转回int64_t,这可能会改变数值

double表示的整数是不连续的,离0越远,不连续越厉害。

{
    int64_t n64 = -5476376616833095248;
    double dbl = n64;
    int64_t n64result = (int64_t)dbl;
    SDL_Log("n64: %lld, dbl: %.3f, n64result: %lld", n64, dbl, n64result);
}
------output------
n64: -5476376616833095248, dbl: -2526517248.000, n64result: -5476376616833095680

int64_t原值是-5476376616833095248,double已不能表示这个值。由它转换为int64_t后,变成了-5476376616833095680

 

22、/INCREMENTAL

VC Linker默认会使用/INCREMENTAL选项产生incremental linking效果。使用此选项产生的exe或dll文件将比不使用此选项产生的文件要大一些,因为Linker会向代码中插入填料代码或数据。Linker这样做的目的是为了在代码有变化时不用重新产生整个exe或者dll,而只用将里面的填料替换掉,从而达到incremental linking的目的。由于Linker向其中加入填料后,有可能会改变代码中某些变量或函数的地址,Linker在发现有这种情况发生时,会向其中插入必要的跳转指令,以保证函数调用的正确。

一般来说,这个选项能够加快链接速度,并且不会对最终生成的二进制代码的执行效果产生太大的影响,对于大型项目是很有用的,但有一种情况应当避免使用此选项。如果程序的执行依赖于最终生成的二进制代码中各种变量或函数的地址时,如果使用/INCREMENTAL选项进行链接,可能会使程序产生意想不到的错误,这种错误往往不合逻辑,很难调试。因此,在写此类程序时最好使用/INCREMENTAL:NO将这个选项关掉。

 

23、std::set,总是让比较函数在等值情况下返回false

自定义类型要能放入std::set,它必须实现“operator<”。但不必实现“operator==”,std::set的判断等于时还是用“operator<”,如果两次参数顺序不同的“operator<”都返回false,那这两值相等。为此,在实现“operator==”要切记:总是让比较函数在等值情况下返回false!

std::set自定义类型还有“严格的弱序化”问题,关于它参考“std::set、自定义类型与比较函数”。

全部评论: 0

    写评论: