这里有一个游戏:要求写一个符合C++标准的程序,包含至少十个连续而且不同的关键字。连续是指不能被标示符、运算符、标点符号分割。注意这里的“不同”要求,别想用
int main() { return sizeof sizeof sizeof sizeof sizeof sizeof sizeof sizeof (int); }
这个交卷,而且这个可以任意长。动动脑经,应该是可以想出来的。我们从很久很久以前(long long ago)开始吧,
unsigned long long int ago;
const volatile unsigned long long int ago;
extern const volatile unsigned long long int ago;
extern inline const volatile unsigned long long int f();
template extern inline const volatile unsigned long long int operator *(const Type&);
template extern inline const volatile unsigned long long int operator and(const Type& lhs, const Type& rhs); // 如果允许运算符&变成关键字and
我们强调了不同的关键字,所以只好用 long int 而不是 long long int。
typeid 也可以级联,与 sizeof 不同的是,必须添加括号。如果参数是表达式而非数据类型,sizeof 可以不用加括号。new 则没有这个限制。
#include <typeinfo> int main() { const std::type_info& info = typeid(typeid(typeid(typeid(int)))); return 0; }
C++规范里还要求使用 typeid 关键字的时候必须 #include <typeinfo>,否则任何使用到的地方都是 ill-formed。new 有时候也需要 #include <new> 头文件。
sizeof new const volatile signed long int();
限定不同的关键字,目前最好的答案是:
int main() { if(false); else do throw sizeof new const volatile signed long int(); while(false); return 0; }
spoiler alert
回到正题,C++ 诞生于1983年,现在有很多的关键字了,这里有详细的列表。
C++ 的一些运算符和标点符号需要 ISO 646 的代码集之外的字符:{, }, [, ], #, \, ^, |, ~。要能够使用不存在这些字符编码的字符集(如德国的 DIN 66003),C++定义了两种替代方案:额外的关键字对应这些操作符,使用 ISO 646 兼容的字符构成的特殊的两元组或三元组解释成一个非 ISO 646 字符。
首选 | && | &= | & | | | ~ | ! | != | || | |= | ^ | ^= |
替代方案 | and | and_eq | bit_and | bitor | compl | not | not_eq | or | or_eq | xor | xor_eq |
使用两元组和三元组可能会遇到一些奇怪的问题,而且也影响代码阅读。C++标准打算在C++17版本废除掉三元组符号 ??< ??> ??( ??) ??= ??/ ??‘ ??! ??-。两元组 <: 会被替换成 [ 符号,于是 std::vector<::std::string> 会被错误的当成 std::vector[:std::string> 对待。现在的键盘都有这些符号,所以不用用他们的替代符号。毕竟符号比字符串易懂,想想数学运算中的加减乘除符号如果用 add/subtract/multiply/devide 英文替换表示,读起来就不舒服,这也是为什么C++引入操作符重载,矩阵的运算可以直接写 (A+B)/(C*D); 而不是 divide(add(A, B), multiply(C, D));
基本数据类型 void bool char wchar_t short int long unsigned float double,布尔值 true false,以及 C++11 新增的 char16_t char32_t nullptr 关键字。
char、signed char、unsigned char 是不同的类型,这个需要注意一下。GCC 编译的时候,可以用编译选项 -fsigned-char 或 -funsigned-char,它们分别将 char 指定为 signed char 或 unsigned char。很多程序直接写char,希望它是有符号的或者无符号的。程序员喜欢简短,跟 int 关键字一样,不写就默认有符号的类型;但有时候处理二进制流,表示成[0, 256)区间的整数。
看到这个表格,原来整形有这么多。其实我更喜欢用简短的 uint8_t/int8_t/.../int32_t 等。你会看到很多代码工程都自己定义了一套整形数据,避免 16位整形的程序移植到32位系统、32位整形的程序移植到64位系统 出现问题。而且C++ 对关键字的修饰顺序也没有要求,这让一些程序员甚是纠结。
GNU 的 STL 库也有这种顺序的 long unsigned int
#ifndef __SIZE_TYPE__
#define __SIZE_TYPE__ long unsigned int
#endif
true/false 的类型是 bool,那 nullptr 的类型是什么?
typedef decltype(nullptr) nullptr_t;
这种类型定义妙不可言,也横空推出新的关键字 decltype。如果两个重载的函数 fun 可以接受不同的指针类型,比如 void fun(int*); void fun(float*); 那么编译 fun(NULL) 会有二义性错误。不妨再定义一个 std::nullptr_t 空指针类型的函数 void fun(std::nullptr_t nullp),这次 fun(NULL) 编译失败但是 fun(nullptr) 可以通过。
wchar_t 用来表示一个 Unicode 字符集中的编码,在 Windows 上是 UTF-16,类 Unix 系统上是 UTF-32。sizeof(wchar_t) 是 implementation defined,移植性差。一般在Windows上是2个字节,在类 Unix 系统上是4个字节。Windows 接受了宽字符并使之成为标准,可以看到许多Windows API 有两个版本,functionNameA 和 functionNameW 分别对应ANSI版本和宽字符版本。这篇博客列举了使用wchar_t可能犯的错误。
C规范中并没有写明宽字符 wchar_t 的具体类型,与编译器实现相关,可能8/16/32位,也可能是signed 或者 unsigned。关键在于选用的编码字符集,注意字符集(Charset)与字符编码(Character Encoding)的区别,ASCII/GBK/BIG5/GB18030 是字符集,Unicode 是字符集。当时各个国家都做了自己的编码方案,中国有 GBK/GB18030/BIG5 等,这在国内没有问题,但是网络遍及世界各地,外面的人访问就出现乱码。于是 Unicode 应运而生。Unicode 字符集有 UTF-7/UTF-8/UTF-16/UTF-32 这几种编码。
推荐使用 Unicode 字符,不推荐使用 wchar_t,取而代之使用固定长度的 char16_t/char32_t 类型。
ISO/IEC 10646:2003 Unicode 标准 4.0 里讲到:
"The width of wchar_t is compiler-specific and can be as small as 8 bits. Consequently, programs that need to be portable across any C or C++ compiler should not use wchar_t for storing Unicode text. The wchar_t type is intended for storing compiler-defined wide characters, which may be Unicode characters in some compilers."
C 语言关键字 int 是用的最多的,我经常觉得 short/long 的修饰让 int 显得多余。short int 与 short 语义一样,long int 与 int 语义一样。long 与 short 是相对来讲的,于是又出现了 long long int 这一类型。long long 是1995年提议加没加成,C++ 因为 C 没有加所以也不肯加入。很多编译器都自己实现了,10年之后,大家觉得有必要标准化了。(说不定将来某一天,整数的计算范围需要扩充到 128 位,于是要唤出 long long long long int?好囧)个人觉得,程序语言诞生的时候,就应该尽量让关键字语义正交,虽然标准规定了 sizeof(short) <= sizeof(int) <= sizeof(long),但是很多编译器还是将 int 与 long int 等同看待。C 语言可移植好不由争辩;Java 很聪明,直接规定了各自的长度。short/int/long 分别为 2/4/8 个字节。
有一个头文件 <stdint.h> 专门定了各种整形数 int8_t/int16_t/int32_t/int64_t 等,还有 unsigned 类型。其实 short/long/signed 都是形容词,int 是名词,所以声明整形没有以 int 结尾也是对的,与其写 long long int,不如写 long long,甚至 int64_t。我也想过如果 sizeof(int) == sizeof(long int),在 32 位机器上,要么 long 多余,要么 int 多余。然后我想到了 long double 类型,觉得还是 int 多余吧。
选取关键字要尽量做到语法正交,尽量能表达所有的意思,小部分可以留给扩展。从 C++11 的改变可以看出,尽量不添加新的关键字,双中括号扩展 [[]] 和右值符号 &&;不得已时添加新的关键字 alignas/alignof/thread_local 等。对差不多不用的关键字回收,auto 重新焕发光芒,template 能做的活,auto 现在也可以简单完成。
添加新的关键字的时候,需要考虑几乎不会在历史代码中用到的。一些上下文相关的(contex-sensitive)关键字 final,override 的出现。它们不算关键字,但是放到函数末尾可以让编译器查错。可想而知,如果像 final override 这样常见的单词选入关键字,会有多少历史代码需要修改。很显然,final 和 override 是从 Java 语言中学过来的。Python 从 2.x 升到 3.x,各种不兼容,让人对这个语言又爱又恨,虽然有帮助迁移代码的文档和工具。
因为 final 与 override 是上下文相关的,所以你可以这么写也会编译通过。
class Base { public: virtual int override(int ) { std::cout<<"please override me"<<std::endl; return 0; } }; class Fun: public Base { public: virtual int override(int ) override final { int override = 42; return override; } }; int main() { int me = 0x3e; Fun hey; hey.override(me); return 0; }
在 C++11 之前,C++ 标准交了很多东西给编译器自行处理,比如是否自行产生构造函数,内存对齐,RVO 优化,enum 的类型等等,现在都可以显式要求,避免各搞各的一套。enum 的类型是 implementation defined,很多编译器都是用能容纳的最小的整形范围来表示。所以你会在微软的很多代码里看到很多这样的写法:
typedef enum D3DTEXTUREADDRESS { D3DTADDRESS_WRAP = 1, D3DTADDRESS_MIRROR = 2, D3DTADDRESS_CLAMP = 3, D3DTADDRESS_BORDER = 4, D3DTADDRESS_MIRRORONCE = 5, D3DTADDRESS_FORCE_DWORD = 0x7fffffff } D3DTEXTUREADDRESS, *LPD3DTEXTUREADDRESS;
最后一个 enum 常量是 XXX_FORCE_DWORD = 0x7fffffff,对其取值 sizeof(D3DTEXTUREADDRESS) 长度固定为 4。
对齐本来是语言设计者想掩盖的细节,不过在C++11编程方式越发复杂的情况下,提供给用户更底层的手段往往是必不可少的。在一些情况下,用户虽然不能保证总是写出平台无关,或者说各平台习惯你能最优的代码,但只需要改造 alignas 之后的对齐值参数就可以保证程序的移植性及性能良好,也不失为一种好的选择。而 C++11 对对齐方式的支持从语法规则到库,基本上考虑了各种情况,可以说是相当完备的。
template <typename T> class alignas(sizeof(T)<<2) Color { T r, g, b, a; };
让关键字增加功能。以前 default 和 delete 只有一种用途:default 就是 switch 语句里的默认分支,delete 就是释放内存。现在都多了一种用法 = delete 表示删除函数,= default 表示使用编译期默认产生的。以前 using 只能与 namespace 为伴,而现在,typedef 所干的活,全部可以接手过来,而且在写法上更直观易懂。以前写 typedef long long int int64; 现在你可以写 using int64 = long long int; 怎么样?数据类型也可以用等号“赋值”了。
C++11 出来后,你不用看编译器的脸色行事了。这里不需要定义构造函数,好让编译器自己生成;那里需要定义构造函数,否则编译器会做 shallow copying。现在你可以显示指示编译器怎么做。
class noncopyable { private: noncopyable(const noncopyable& ); // not defined noncopyable& operator=(const noncopyable&); // not defined };
之前,我们不想让对象之间赋值,会讲赋值构造函数和 operator = 限制为 private,并不提供定义。现在我们有更好的写法:
class noncopyable { public: noncopyable(const noncopyable& ) = delete; noncopyable& operator=(const noncopyable&) = delete; };
声明为 private 可以阻止被调用。故意不实现这些函数,意味着如果外部通过成员函数或者 friend 类想访问它们,虽然编译可以通过,但是链接的时候会因未定义的符号而报错。在 C++11 里,情况就不一样了。使用 = delete 表示编译期不会产生这个方法,所以这个可以扼杀在编译期,不用推迟到链接的时候。虽然这里的访问权限对 C++11 不重要,但一般来说,标记 delete 的函数最好声明 public,而不是 private。因为有的编译器在检查成员函数的调用时,只报访问权限错误(错误如下)。很显然,这里 delete 才是重点,或者说错误的优先级高些。当然,并不是只有成员函数才能 delete,非成员函数和模版实例化的函数也是可以的。相比 C++98 而言,算是一项改进。
error: ‘pea::Log::Log(const pea::Log&)’ is private
Log(const Log& log) = delete;
提醒一下,using namespace std; 不要写在 .h 文件里,要写也是写在 .cpp 文件里。出于头文件会被其他头文件包含的可能,污染命名空间,但是 .cpp 文件不会。除非你故意想 #include "XXX.cpp" 这么写。在cocos2d-x 里的 class HelloWorld 里面添加方法 void onKeyReleased(EventKeyboard::KeyCode keyCode, Event* event); 会编译不过,报错如下:
1>classes\HelloWorldScene.h(17): error C2653: ‘EventKeyboard‘ : is not a class or namespace name (..\Classes\AppDelegate.cpp)
1>classes\HelloWorldScene.h(17): error C2061: syntax error : identifier ‘KeyCode‘ (..\Classes\AppDelegate.cpp)
1>classes\HelloWorldScene.h(17): error C2653: ‘EventKeyboard‘ : is not a class or namespace name (..\Classes\HelloWorldScene.cpp)
1>classes\HelloWorldScene.h(17): error C2061: syntax error : identifier ‘KeyCode‘ (..\Classes\HelloWorldScene.cpp)
1>classes\HelloWorldScene.cpp(72): error C2276: ‘&‘ : illegal operation on bound member function expression
其实修改很简单,添加 EventKeyboard 和 KeyCode 所在的命名空间就好了。
void onKeyReleased(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* event);
然而在 HelloWorldScene.cpp 里不用添加,因为开头有这么一句 USING_NS_CC; 也就是 using namespace cocos2d; 引入了 cocos2d 所有的东西。
void HelloWorld::onKeyReleased(EventKeyboard::KeyCode keyCode, Event* event) { //TODO }
C++11 增加的 operator "" 的重载,用户自定义语义(user literal)限制在以下几种类型,
( const char * )
( unsigned long long int )
( long double )
( char )
( wchar_t )
( char16_t )
( char32_t )
( const char * , std::size_t )
( const wchar_t * , std::size_t )
( const char16_t * , std::size_t )
( const char32_t * , std::size_t )
发现没有,比如浮点类型只有 long double,却没有 float 和 double 类型,为什么呢?假设我们有一个角度制和弧度制转换的函数语义。
constexpr long double operator"" _deg ( long double deg ) { return deg*M_PI/180; }
我们要表达 M_PI 弧度时候,可以写成 180_deg ,注意 180 和 _deg 是一个整体,之间不能有空格。(就像 C/C++ 语言内的后缀语义 long int degree = 180L; float pi = 3.14159265f 一样)。如果要区分 float、double、long double 数据类型,我们的定义应该是这样的:
float radian1 = 180f_deg;
double radian2 = 180_deg;
long double radian3 = 180L_deg;
很显然,编译器会把没有空格的合法字符当成一个单词(token)解析,于是 f_deg 变成了一个单词,前面也说了 f 与 _deg 之间不能有空格,所以我们无法满足不同浮点类型的重载,也包括整数类型。为了不截断或窄收,于是采取最大表示范围的类型,整形用 unsigned long long int,浮点类型用 long double。整形是 long long int 而不是 unsigned long long int 是因为有符号和无符号数相加减,会扩充到无符号类型。这里似乎暗示了 unsigned long long int 是最大范围的整形,所以应该不会出现 128 位的整形。
尽量让计算推前到编译器而不是运行期,在 C++11 增加了新的关键字 static_cast、constexpr。boost 库里面有模拟 static_cast 的功能。
之前我们用宏定义 FOURCC
#define MAKE_FOURCC(ch0, ch1, ch2, ch3) \ ((uint32_t)(uint8_t)(ch0) | ((uint32_t)(uint8_t)(ch1) << 8) | ((uint32_t)(uint8_t)(ch2) << 16) | ((uint32_t)(uint8_t)(ch3) << 24)) \
现在我们可以直接用 constexpr 函数来写,而不是类型不安全的宏了。因为是编译器计算出值,所以算出来的整形值可以直接用于 switch case 语句中而不会出错。
constexpr uint32_t makeFourCC(uint8_t ch0, uint8_t ch1, uint8_t ch2, uint8_t ch3) { return static_cast<uint32_t>(ch0 | (ch1<<8) | (ch2<<16) | (ch3<<24)); } constexpr uint32_t RIFF = makeFourCC(‘R‘, ‘I‘, ‘F‘, ‘F‘); constexpr uint32_t WAVE = makeFourCC(‘W‘, ‘A‘, ‘V‘, ‘E‘); constexpr uint32_t FMT_ = makeFourCC(‘F‘, ‘M‘, ‘T‘, ‘ ‘); constexpr uint32_t DATA = makeFourCC(‘D‘, ‘A‘, ‘T‘, ‘A‘);
C++ 十几年的时间没有动静,造就了 boost 优秀库。又从较自己晚出身的语言身上也学到了很多,比如 Java 类的成员在定义的时候给定默认值,委托构造函数(delegate constructor)、Python 的原生字符串常量,也算方便了程序员对 C++ 字符串的学习和使用。
未完待续。。