C++你不知道的那些事儿—C++语言的15个晦涩特性
这个列表收集了 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++语法声明的二义性会导致有悖常理的行为:
| 1 2 3 4 5 6 7 8 9 10 11 | // 这个解释正确?// 1) 类型std::string的变量会通过std::string()实例化吗?// 2) 一个函数声明,返回一个std::string值并有一个函数指针参数,// 该函数也返回一个std::string但没有参数?std::string foo(std::string());// 还是这个正确?// 1)类型int变量会通过int(x)实例化吗?// 2)一个函数声明,返回一个int值并有一个参数,// 该参数是一个名为x的int型变量吗?intbar(int(x)); | 
两种情形下C++标准要求的是第二种解释,即使第一种解释看起来更直观。程序员可以通过包围括号中变量的初始值来消除歧义:
| 1 2 3 | //加括号消除歧义std::string foo((std::string()));intbar((int(x))); | 
第二种情形让人产生二义性的原因是int y = 3;等价于int(y) = 3;
译者注:这一点我觉得有点迷惑,下面是我在g++下的测试用例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #include <iostream>#include <string>usingnamespacestd;intbar(int(x));   // 等价于int bar(int x)string foo(string());  // 等价于string foo(string (*)())string test() {    return"test";}intmain(){    cout << bar(2) << endl; // 输出2    cout << foo(test); // 输出test    return0;}intbar(int(x)) {      returnx;}string foo(string (*fun)()) {    return(*fun)();} | 
能正确输出,但如果按作者意思添加上括号后再编译就会报一堆错误:“在此作用域尚未声明”、“重定义”等,还不清楚作者的意图。
替代运算标记符
标记符and, and_eq, bitand, bitor, compl, not, not_eq, or, or_eq, xor, xor_eq, <%, %>, <: 和 :>都可以用来代替我们常用的&&, &=, &, |, ~, !, !=, ||, |=, ^, ^=, {, }, [ 和 ]。在键盘上缺乏必要的符号时你可以使用这些运算标记符来代替。
重定义关键字
通过预处理器重定义关键字从技术上讲会引起错误,但实际上是允许这样做的。因此你可以使用类似#define true false 或 #define else来搞点恶作剧。但是,也有它合法有用的时候,例如,如果你正在使用一个很大的库而且需要绕过C++访问保护机制,除了给库打补丁的方法外,你也可 以在包含该库头文件之前关闭访问保护来解决,但要记得在包含库头文件之后一定要打开保护机制!
| 1 2 3 4 5 6 7 8 9 | #define class struct#define private public#define protected public#include "library.h"#undef class#undef private#undef protected | 
注意这种方式不是每一次都有效,跟你的编译器有关。当实例变量没有被访问控制符修饰时,C++只需要将这些实例变量顺序布局即可,所以编译器可以对 访问控制符组重新排序来自由更改内存布局。例如,允许编译器移动所有的私有成员放到公有成员的后面。另一个潜在的问题是名称重整(name mangling),Microsoft的C++编译器将访问控制符合并到它们的name mangling表里,因此改变访问控制符意味着将破坏现有编译代码的兼容性。
译者注:在C++中,Name Mangling 是为了支持重载而加入的一项技术。编译器将目标源文件中的名字进行调整,这样在目标文件符号表中和连接过程中使用的名字和编译目标文件的源程序中的名字不一样,从而实现重载。
Placement new
Placement new是new操作符的一个替代语法,作用在已分配的对象上,该对象已有正确的大小和正确的赋值,这包括建立虚函数表和调用构造函数。
译者注:placement new就是在用户指定的内存位置上构建新的对象,这个构建过程不需要额外分配内存,只需要调用对象的构造函数即可。placement new实际上是把原本new做的两步工作分开来:第一步自己分配内存,第二步调用类的构造函数在自己已分配的内存上构建新的对象。placement new的好处:1)在已分配好的内存上进行对象的构建,构建速度快。2)已分配好的内存可以反复利用,有效的避免内存碎片问题。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include <iostream>usingnamespacestd;structTest {  intdata;  Test() { cout << "Test::Test()"<< endl; }  ~Test() { cout << "Test::~Test()"<< endl; }};intmain() {  // Must allocate our own memory  Test *ptr = (Test *)malloc(sizeof(Test));  // Use placement new  new(ptr) Test;  // Must call the destructor ourselves  ptr->~Test();  // Must release the memory ourselves  free(ptr);  return0;} | 
当在性能关键的场合需要自定义分配器时可以使用Placement new。例如,一个slab分配器从单个的大内存块开始,使用placement new在块里顺序分配对象。这不仅避免了内存碎片,也节省了malloc引起的堆遍历的开销。
在声明变量的同时进行分支
C++包含一个语法缩写,能在声明变量的同时进行分支。看起来既像单个的变量声明也可以有if或while这样的分支条件。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | structEvent { virtual~Event() {} };structMouseEvent : Event { intx, y; };structKeyboardEvent : Event { intkey; };voidlog(Event *event) {  if(MouseEvent *mouse = dynamic_cast<MouseEvent *>(event))    std::cout << "MouseEvent "<< mouse->x << " "<< mouse->y << std::endl;  elseif(KeyboardEvent *keyboard = dynamic_cast<KeyboardEvent *>(event))    std::cout << "KeyboardEvent "<< keyboard->key << std::endl;  else    std::cout << "Event"<< std::endl;} | 
成员函数的引用修饰符
C++11允许成员函数在对象的值类型上进行重载,this指针会将该对象作为一个引用修饰符。引用修饰符会放在cv限定词(译者注:CV限定词有 三种:const限定符、volatile限定符和const-volatile限定符)相同的位置并依据this对象是左值还是右值影响重载解析:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <iostream>structFoo {  voidfoo() & { std::cout << "lvalue"<< std::endl; }  voidfoo() && { std::cout << "rvalue"<< std::endl; }};intmain() {  Foo foo;  foo.foo(); // Prints "lvalue"  Foo().foo(); // Prints "rvalue"  return0;} | 
图灵完备的模板元编程
C++模板是为了实现编译时元编程,也就是该程序能生成其它的程序。设计模板系统的初衷是进行简单的类型替换,但是在C++标准化过程中突然发现模板实际上功能十分强大,足以执行任意计算,虽然很笨拙很低效,但通过模板特化的确可以完成一些计算:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | // Recursive template for general casetemplate<intN>structfactorial {  enum{ value = N * factorial<N - 1>::value };};// Template specialization for base casetemplate<>structfactorial<0> {  enum{ value = 1 };};enum{ result = factorial<5>::value }; // 5 * 4 * 3 * 2 * 1 == 120 | 
C++模板可以被认为是一种功能型编程语言,因为它们使用递归而非迭代而且包含不可变状态。你可以使用typedef创建一个任意类型的变量,使用enum创建一个int型变量,数据结构内嵌在类型自身。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Compile-time list of integerstemplate<intD, typenameN>structnode {  enum{ data = D };  typedefN next;};structend {};// Compile-time sum functiontemplate<typenameL>structsum {  enum{ value = L::data + sum<typenameL::next>::value };};template<>structsum<end> {  enum{ value = 0 };};// Data structures are embedded in typestypedefnode<1, node<2, node<3, end> > > list123;enum{ total = sum<list123>::value }; // 1 + 2 + 3 == 6 | 
当然这些例子没什么用,但模板元编程的确可以做一些有用的事情,比如可以操作类型列表。但是,使用C++模板的编程语言可用性极低,因此请谨慎和少量使用。模板代码很难阅读,编译速度慢,而且因其冗长和迷惑的错误信息而难以调试。
指向成员的指针操作符
指向成员的指针操作符可以让你在一个类的任何实例上描述指向某个成员的指针。有两种pointer-to-member操作符,取值操作符*和指针操作符->:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | #include <iostream>usingnamespacestd;structTest {  intnum;  voidfunc() {}};// Notice the extra "Test::" in the pointer typeintTest::*ptr_num = &Test::num;void(Test::*ptr_func)() = &Test::func;intmain() {  Test t;  Test *pt = newTest;  // Call the stored member function  (t.*ptr_func)();  (pt->*ptr_func)();  // Set the variable in the stored member slot  t.*ptr_num = 1;  pt->*ptr_num = 2;  deletept;  return0;} | 
该特征实际上十分有用,尤其在写库的时候。例如,Boost::Python, 一个用来将C++绑定到Python对象的库,就使用成员指针操作符,在包装对象时很容易的指向成员。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <iostream>#include <boost/python.hpp>usingnamespaceboost::python;structWorld {  std::string msg;  voidgreet() { std::cout << msg << std::endl; }};BOOST_PYTHON_MODULE(hello) {  class_<World>("World")    .def_readwrite("msg", &World::msg)    .def("greet", &World::greet);} | 
记住使用成员函数指针与普通函数指针是不同的。在成员函数指针和普通函数指针之间casting是无效的。例如,Microsoft编译器里的成员 函数使用了一个称为thiscall的优化调用约定,thiscall将this参数放到ecx寄存器里,而普通函数的调用约定却是在栈上解析所有的参 数。
而且,成员函数指针可能比普通指针大四倍左右,编译器需要存储函数体的地址,到正确父地址(多个继承)的偏移,虚函数表(虚继承)中另一个偏移的索引,甚至在对象自身内部的虚函数表的偏移也需要存储(为了前向声明类型)。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <iostream>structA {};structB : virtualA {};structC {};structD : A, C {};structE;intmain() {  std::cout << sizeof(void(A::*)()) << std::endl;  std::cout << sizeof(void(B::*)()) << std::endl;  std::cout << sizeof(void(D::*)()) << std::endl;  std::cout << sizeof(void(E::*)()) << std::endl;  return0;}// 32-bit Visual C++ 2008:  A = 4, B = 8, D = 12, E = 16// 32-bit GCC 4.2.1:        A = 8, B = 8, D = 8,  E = 8// 32-bit Digital Mars C++: A = 4, B = 4, D = 4,  E = 4 | 
在Digital Mars编译器里所有的成员函数都是相同的大小,这是源于这样一个聪明的设计:生成“thunk”函数来运用右偏移而不是存储指针自身内部的偏移。
静态实例方法
C++中可以通过实例调用静态方法也可以通过类直接调用。这可以使你不需要更新任何调用点就可以将实例方法修改为静态方法。
| 1 2 3 4 5 6 7 | structFoo {  staticvoidfoo() {}};// These are equivalentFoo::foo();Foo().foo(); | 
重载++和–
C++的设计中自定义操作符的函数名称就是操作符本身,这在大部分情况下都工作的很好。例如,一元操作符的-和二元操作符的-(取反和相减)可以通 过参数个数来区分。但这对于一元递增和递减操作符却不奏效,因为它们的特征似乎完全相同。C++语言有一个很笨拙的技巧来解决这个问题:后缀++和–操作 符必须有一个空的int参数作为标记让编译器知道要进行后缀操作(是的,只有int类型有效)。
| 1 2 3 4 | structNumber {  Number &operator ++ (); // Generate a prefix ++ operator  Number operator ++ (int); // Generate a postfix ++ operator}; | 
操作符重载和检查顺序
重载,(逗号),||或者&&操作符会引起混乱,因为它打破了正常的检查规则。通常情况下,逗号操作符在整个左边检查完毕才开始检 查右边,|| 和 &&操作符有短路行为:仅在必要时才会去检查右边。无论如何,操作符的重载版本仅仅是函数调用且函数调用以未指定的顺序检查它们的参数。
重载这些操作符只是一种滥用C++语法的方式。作为一个实例,下面我给出一个Python形式的无括号版打印语句的C++实现:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #include <iostream>namespace__hidden__ {  structprint {    boolspace;    print() : space(false) {}    ~print() { std::cout << std::endl; }    template<typenameT>    print &operator , (constT &t) {      if(space) std::cout << ' ';      elsespace = true;      std::cout << t;      return*this;    }  };}#define print __hidden__::print(),intmain() {  inta = 1, b = 2;  print "this is a test";  print "the sum of", a, "and", b, "is", a + b;  return0;} | 
函数作为模板参数
众所周知,模板参数可以是特定的整数也可以是特定的函数。这使得编译器在实例化模板代码时内联调用特定的函数以获得更高效的执行。下面的例子里,函数memoize的模板参数也是一个函数且只有新的参数值才通过函数调用(旧的参数值可以通过cache获得):
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <map>template<int(*f)(int)>intmemoize(intx) {  staticstd::map<int, int> cache;  std::map<int, int>::iterator y = cache.find(x);  if(y != cache.end()) returny->second;  returncache[x] = f(x);}intfib(intn) {  if(n < 2) returnn;  returnmemoize<fib>(n - 1) + memoize<fib>(n - 2);} | 
模板的参数也是模板
模板参数实际上自身的参数也可以是模板,这可以让你在实例化一个模板时可以不用模板参数就能够传递模板类型。看下面的代码:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | template<typenameT>structCache { ... };template<typenameT>structNetworkStore { ... };template<typenameT>structMemoryStore { ... };template<typenameStore, typenameT>structCachedStore {  Store store;  Cache<T> cache;};CachedStore<NetworkStore<int>, int> a;CachedStore<MemoryStore<int>, int> b; | 
CachedStore的cache存储的数据类型与store的类型相同。然而我们在实例化一个CachedStore必须重复写数据类型(上面 的代码是int型),store本身要写,CachedStore也要写,关键是我们这并不能保证两者的数据类型是一致的。我们真的只想要确定数据类型一 次即可,所以我们可以强制其不变,但是没有类型参数的列表会引起编译出错:
| 1 2 3 | // 下面编译通不过,因为NetworkStore和MemoryStore缺失类型参数CachedStore<NetworkStore, int> c;CachedStore<MemoryStore, int> d; | 
模板的模板参数可以让我们获得想要的语法。注意你必须使用class关键字作为模板参数(他们自身的参数也是模板)
| 1 2 3 4 5 6 7 8 | template<template<typename> classStore, typenameT>structCachedStore2 {  Store<T> store;  Cache<T> cache;};CachedStore2<NetworkStore, int> e;CachedStore2<MemoryStore, int> f; | 
try块作为函数
函数的try块会在检查构造函数的初始化列表时捕获抛出的异常。你不能在初始化列表的周围加上try-catch块,因为其只能出现在函数体外。为了解决这个问题,C++允许try-catch块也可作为函数体:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | intf() { throw0; }// 这里没有办法捕获由f()抛出的异常structA {  inta;  A::A() : a(f()) {}};// 如果try-catch块被用作函数体并且初始化列表移至try关键字之后的话,// 那么由f()抛出的异常就可以捕获到structB {  intb;  B::B() try: b(f()) {  } catch(inte) {  }}; | 
奇怪的是,这种语法不仅仅局限于构造函数,也可用于其他的所有函数定义。
译自:http://madebyevan.com/obscure-cpp-features/
C++你不知道的那些事儿—C++语言的15个晦涩特性的更多相关文章
- (转)C++语言的15个晦涩特性
		原文链接: Evan Wallace 翻译: 伯乐在线- 敏敏 译文链接: http://blog.jobbole.com/54140/ 这个列表收集了 C++ 语言的一些晦涩(Obscure)特 ... 
- 转:C++语言的15个晦涩特性
		转自 http://blog.jobbole.com/54140/ 操作符重载和检查顺序 重载,(逗号),||或者&&操作符会引起混乱,因为它打破了正常的检查规则.通常情况下,逗号操作 ... 
- JDK 15 JAVA 15的新特性展望
		目录 JEP 371: Hidden Classes JEP 372: 删除 Nashorn JavaScript Engine JEP 377: 新的垃圾回收器ZGC正式上线了 JEP 378: T ... 
- Java你不知道的那些事儿—Java隐藏特性(上)
		每种语言都很强大,不管你是像我一样的初学者还是有过N年项目经验的大神,总会有你不知道的东西.就其语言本身而言,比如Java,也许你用Java开发了好几年,对其可以说是烂熟于心,但你能保证Java所有的 ... 
- Java你不知道的那些事儿—Java隐藏特性
		转载自:http://www.cnblogs.com/lanxuezaipiao/p/3460373.html 每 种语言都很强大,不管你是像我一样的初学者还是有过N年项目经验的大神,总会有你不知道的 ... 
- C程序设计语言练习题1-5
		练习1-5 修改温度转换程序,要求以逆序(即按照从300度到0度的顺序)打印温度转换表. 代码如下: #include <stdio.h> // 包含标准库的信息. int main() ... 
- C++语言基础(15)-友元函数和友元类
		一个类中可以有 public.protected.private 三种属性的成员,通过对象可以访问 public 成员,只有本类中的函数可以访问本类的 private 成员.现在,我们来介绍一种例外情 ... 
- C语言基础(15)-多文件编译
		一.头文件的使用 如果把main函数放在第一个文件中,而把自定义函数放在第二个文件中,那么就需要在第一个文件中声明函数原型.如果把函数原型包含在一个头文件里,那么就不必每次使用函数的时候都声明其原型了 ... 
- Java入门 - 语言基础 - 15.StringBuffer
		原文地址:http://www.work100.net/training/java-stringbuffer.html 更多教程:光束云 - 免费课程 StringBuffer 序号 文内章节 视频 ... 
随机推荐
- 【poj2823】 Sliding Window
			http://poj.org/problem?id=2823 (题目链接) 题意 维护滑动窗口最大最小值. Solution sb单调队列 代码 // poj2823 #include<algo ... 
- Compiler Theory(编译原理)、词法/语法/AST/中间代码优化在Webshell检测上的应用
			catalog . 引论 . 构建一个编译器的相关科学 . 程序设计语言基础 . 一个简单的语法制导翻译器 . 简单表达式的翻译器(源代码示例) . 词法分析 . 生成中间代码 . 词法分析器的实现 ... 
- 小米手机(HM1SW)高通开发android程序全过程
			小米手机(HM1SW)开发android程序全过程 修改历史: 2016年5月9日 -------- 整理文档 a.增加了手机基本信息. b.增加360手机助手连接说明 2016年2月26日 - ... 
- c#自适应窗体的实现
			using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.W ... 
- maven仓库没有的包依赖
			如果有个jar包是我们自己打的,怎么放到maven中呢? 首先在项目里面新建一个lib目录,如果有lib目录则不需要新建,然后放自己的jar包进去,maven的pom.xml配置是: <depe ... 
- Scala的sealed关键字
			Scala的sealed关键字 缘起 今天在学习Akka的监控策咯过程中看到了下面一段代码: def supervisorStrategy(): SupervisorStrategy = OneFor ... 
- 数据结构作业——brothers(二叉树)
			brothers Description 给你一棵节点编号从 1 到 n 的,根节点为 1 的二叉树.然后有 q 个询问,每个询问给出一个整数表示树的节点,要求这个节点的兄弟节点数目和堂兄弟节点的数目 ... 
- BZOJ2152:聪聪可可
			传送门 点分治常规题.练习模板 //OJ 2077 //by Cydiater //2016.9.23 #include <iostream> #include <cstdio> ... 
- BZOJ1812 [IOI2005]river
			传送门: 很常规的一道树规,转为左儿子右兄弟. 然后$f[node][anc][K]$表示在node节点上,最近的有贡献祖先在anc上,在node的儿子和兄弟上有k个有贡献节点的最优值. 然后得出以下 ... 
- JavaScript DES 加密tripledes.js:
			<html> <head> <meta http-equiv="content-type" content="text/html; char ... 
