这个列表收集了 C++ 语言的一些晦涩(Obscure)特性,是我经年累月研究这门语言的各个方面收集起来的。C++非常庞大,我总是能学到一些新知识。即使你对C++已了如指掌,也希望你能从列表中学到一些东西。下面列举的特性,根据晦涩程度由浅入深进行排序。 1. 方括号的真正含义 2. 最烦人的解析 3.替代运算标记符 4. 重定义关键字 5. Placement new 6.在声明变量的同时进行分支 7.成员函数的引用修饰符 8.转向完整的模板元编程 9.指向成员的指针操作符 10. 静态实例方法 11.重载++和– 12.操作符重载和检查顺序 13.函数作为模板参数 14.模板的参数也是模板 15.try块作为函数
方括号的真正含义用来访问数组元素的ptr[3]其实只是*(ptr + 3)的缩写,与用*(3 + ptr)是等价的,因此反过来与3[ptr]也是等价的,使用3[ptr]是完全有效的代码。 最烦人的解析“most vexing parse”这个词是由Scott Meyers提出来的,因为C++语法声明的二义性会导致有悖常理的行为: 05 | std::string foo(std::string()); |
两种情形下C++标准要求的是第二种解释,即使第一种解释看起来更直观。程序员可以通过包围括号中变量的初始值来消除歧义: 2 | std::string foo((std::string())); |
第二种情形让人产生二义性的原因是int y = 3;等价于int(y) = 3; 译者注:这一点我觉得有点迷惑,下面是我在g++下的测试用例: 15 | cout << bar(2) << endl; |
24 | string foo(string (*fun)()) { |
能正确输出,但如果按作者意思添加上括号后再编译就会报一堆错误:“在此作用域尚未声明”、“重定义”等,还不清楚作者的意图。 替代运算标记符标记符and, and_eq, bitand, bitor, compl, not, not_eq, or, or_eq, xor, xor_eq, <%, %>, <: 和 :>都可以用来代替我们常用的&&, &=, &, |, ~, !, !=, ||, |=, ^, ^=, {, }, [ 和 ]。在键盘上缺乏必要的符号时你可以使用这些运算标记符来代替。 重定义关键字通过预处理器重定义关键字从技术上讲会引起错误,但实际上是允许这样做的。因此你可以使用类似#define true false 或 #define else来搞点恶作剧。但是,也有它合法有用的时候,例如,如果你正在使用一个很大的库而且需要绕过C++访问保护机制,除了给库打补丁的方法外,你也可以在包含该库头文件之前关闭访问保护来解决,但要记得在包含库头文件之后一定要打开保护机制! 3 | #define protected public |
注意这种方式不是每一次都有效,跟你的编译器有关。当实例变量没有被访问控制符修饰时,C++只需要将这些实例变量顺序布局即可,所以编译器可以对访问控制符组重新排序来自由更改内存布局。例如,允许编译器移动所有的私有成员放到公有成员的后面。另一个潜在的问题是名称重整(name mangling),Microsoft的C++编译器将访问控制符合并到它们的name mangling表里,因此改变访问控制符意味着将破坏现有编译代码的兼容性。 译者注:在C++中,Name Mangling 是为了支持重载而加入的一项技术。编译器将目标源文件中的名字进行调整,这样在目标文件符号表中和连接过程中使用的名字和编译目标文件的源程序中的名字不一样,从而实现重载。 Placement newPlacement new是new操作符的一个替代语法,作用在已分配的对象上,该对象已有正确的大小和正确的赋值,这包括建立虚函数表和调用构造函数。 译者注:placement new就是在用户指定的内存位置上构建新的对象,这个构建过程不需要额外分配内存,只需要调用对象的构造函数即可。placement new实际上是把原本new做的两步工作分开来:第一步自己分配内存,第二步调用类的构造函数在自己已分配的内存上构建新的对象。placement new的好处:1)在已分配好的内存上进行对象的构建,构建速度快。2)已分配好的内存可以反复利用,有效的避免内存碎片问题。 06 | Test() { cout << "Test::Test()" << endl; } |
07 | ~Test() { cout << "Test::~Test()" << endl; } |
12 | Test *ptr = (Test *) malloc ( sizeof (Test)); |
当在性能关键的场合需要自定义分配器时可以使用Placement new。例如,一个slab分配器从单个的大内存块开始,使用placement new在块里顺序分配对象。这不仅避免了内存碎片,也节省了malloc引起的堆遍历的开销。 在声明变量的同时进行分支C++包含一个语法缩写,能在声明变量的同时进行分支。看起来既像单个的变量声明也可以有if或while这样的分支条件。 01 | struct Event { virtual ~Event() {} }; |
02 | struct MouseEvent : Event { int x, y; }; |
03 | struct KeyboardEvent : Event { int key; }; |
05 | void log (Event *event) { |
06 | if (MouseEvent *mouse = dynamic_cast <MouseEvent *>(event)) |
07 | std::cout << "MouseEvent " << mouse->x << " " << mouse->y << std::endl; |
09 | else if (KeyboardEvent *keyboard = dynamic_cast <KeyboardEvent *>(event)) |
10 | std::cout << "KeyboardEvent " << keyboard->key << std::endl; |
13 | std::cout << "Event" << std::endl; |
成员函数的引用修饰符C++11允许成员函数在对象的值类型上进行重载,this指针会将该对象作为一个引用修饰符。引用修饰符会放在cv限定词(译者注:CV限定词有三种:const限定符、volatile限定符和const-volatile限定符)相同的位置并依据this对象是左值还是右值影响重载解析: 04 | void foo() & { std::cout << "lvalue" << std::endl; } |
05 | void foo() && { std::cout << "rvalue" << std::endl; } |
转向完整的模板元编程C++模板是为了实现编译时元编程,也就是该程序能生成其它的程序。设计模板系统的初衷是进行简单的类型替换,但是在C++标准化过程中突然发现模板实际上功能十分强大,足以执行任意计算,虽然很笨拙很低效,但通过模板特化的确可以完成一些计算: 04 | enum { value = N * factorial<N - 1>::value }; |
13 | enum { result = factorial<5>::value }; |
C++模板可以被认为是一种功能型编程语言,因为它们使用递归而非迭代而且包含不可变状态。你可以使用typedef创建一个任意类型的变量,使用enum创建一个int型变量,数据结构内嵌在类型自身。 02 | template < int D, typename N> |
12 | enum { value = L::data + sum< typename L::next>::value }; |
20 | typedef node<1, node<2, node<3, end> > > list123; |
21 | enum { total = sum<list123>::value }; |
当然这些例子没什么用,但模板元编程的确可以做一些有用的事情,比如可以操作类型列表。但是,使用C++模板的编程语言可用性极低,因此请谨慎和少量使用。模板代码很难阅读,编译速度慢,而且因其冗长和迷惑的错误信息而难以调试。
|