C++你不知道的那些事儿—C++语言的15个晦涩特性
這個列表收集了?C++ 語言的一些晦澀(Obscure)特性,是我經(jīng)年累月研究這門語言的各個方面收集起來的。C++非常龐大,我總是能學(xué)到一些新知識。即使你對C++已了如指掌,也希望你能從列表中學(xué)到一些東西。下面列舉的特性,根據(jù)晦澀程度由淺入深進(jìn)行排序。
- 1. 方括號的真正含義
- 2. 最煩人的解析
- 3.替代運(yùn)算標(biāo)記符
- 4. 重定義關(guān)鍵字
- 5. Placement new
- 6.在聲明變量的同時進(jìn)行分支
- 7.成員函數(shù)的引用修飾符
- 8.圖靈完備的模板元編程
- 9.指向成員的指針操作符
- 10. 靜態(tài)實(shí)例方法
- 11.重載++和–
- 12.操作符重載和檢查順序
- 13.函數(shù)作為模板參數(shù)
- 14.模板的參數(shù)也是模板
- 15.try塊作為函數(shù)
?
方括號的真正含義
用來訪問數(shù)組元素的ptr[3]其實(shí)只是*(ptr + 3)的縮寫,與用*(3 + ptr)是等價的,因此反過來與3[ptr]也是等價的,使用3[ptr]是完全有效的代碼。
?
最煩人的解析
“most vexing parse”這個詞是由Scott Meyers提出來的,因?yàn)镃++語法聲明的二義性會導(dǎo)致有悖常理的行為:
| 1 2 3 4 5 6 7 8 9 10 11 | // 這個解釋正確? // 1) 類型std::string的變量會通過std::string()實(shí)例化嗎? // 2) 一個函數(shù)聲明,返回一個std::string值并有一個函數(shù)指針參數(shù), // 該函數(shù)也返回一個std::string但沒有參數(shù)? std::string foo(std::string()); // 還是這個正確? // 1)類型int變量會通過int(x)實(shí)例化嗎? // 2)一個函數(shù)聲明,返回一個int值并有一個參數(shù), // 該參數(shù)是一個名為x的int型變量嗎? int bar(int(x)); |
兩種情形下C++標(biāo)準(zhǔn)要求的是第二種解釋,即使第一種解釋看起來更直觀。程序員可以通過包圍括號中變量的初始值來消除歧義:
| 1 2 3 | //加括號消除歧義 std::string foo((std::string())); int bar((int(x))); |
第二種情形讓人產(chǎn)生二義性的原因是int y = 3;等價于int(y) = 3;
譯者注:這一點(diǎn)我覺得有點(diǎn)迷惑,下面是我在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> using namespace std; int bar(int(x));?? // 等價于int bar(int x) string foo(string());? // 等價于string foo(string (*)()) string test() { ????return "test"; } int main() { ????cout << bar(2) << endl; // 輸出2 ????cout << foo(test); // 輸出test ????return 0; } int bar(int(x)) {? ????return x; } string foo(string (*fun)()) { ????return (*fun)(); } |
能正確輸出,但如果按作者意思添加上括號后再編譯就會報一堆錯誤:“在此作用域尚未聲明”、“重定義”等,還不清楚作者的意圖。
?
替代運(yùn)算標(biāo)記符
標(biāo)記符and, and_eq, bitand, bitor, compl, not, not_eq, or, or_eq, xor, xor_eq, <%, %>, <: 和 :>都可以用來代替我們常用的&&, &=, &, |, ~, !, !=, ||, |=, ^, ^=, {, }, [ 和 ]。在鍵盤上缺乏必要的符號時你可以使用這些運(yùn)算標(biāo)記符來代替。
?
重定義關(guān)鍵字
通過預(yù)處理器重定義關(guān)鍵字從技術(shù)上講會引起錯誤,但實(shí)際上是允許這樣做的。因此你可以使用類似#define true false 或 #define else來搞點(diǎn)惡作劇。但是,也有它合法有用的時候,例如,如果你正在使用一個很大的庫而且需要繞過C++訪問保護(hù)機(jī)制,除了給庫打補(bǔ)丁的方法外,你也可 以在包含該庫頭文件之前關(guān)閉訪問保護(hù)來解決,但要記得在包含庫頭文件之后一定要打開保護(hù)機(jī)制!
| 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 |
注意這種方式不是每一次都有效,跟你的編譯器有關(guān)。當(dāng)實(shí)例變量沒有被訪問控制符修飾時,C++只需要將這些實(shí)例變量順序布局即可,所以編譯器可以對 訪問控制符組重新排序來自由更改內(nèi)存布局。例如,允許編譯器移動所有的私有成員放到公有成員的后面。另一個潛在的問題是名稱重整(name mangling),Microsoft的C++編譯器將訪問控制符合并到它們的name mangling表里,因此改變訪問控制符意味著將破壞現(xiàn)有編譯代碼的兼容性。
譯者注:在C++中,Name Mangling 是為了支持重載而加入的一項(xiàng)技術(shù)。編譯器將目標(biāo)源文件中的名字進(jìn)行調(diào)整,這樣在目標(biāo)文件符號表中和連接過程中使用的名字和編譯目標(biāo)文件的源程序中的名字不一樣,從而實(shí)現(xiàn)重載。
?
Placement new
Placement new是new操作符的一個替代語法,作用在已分配的對象上,該對象已有正確的大小和正確的賦值,這包括建立虛函數(shù)表和調(diào)用構(gòu)造函數(shù)。
譯者注:placement new就是在用戶指定的內(nèi)存位置上構(gòu)建新的對象,這個構(gòu)建過程不需要額外分配內(nèi)存,只需要調(diào)用對象的構(gòu)造函數(shù)即可。placement new實(shí)際上是把原本new做的兩步工作分開來:第一步自己分配內(nèi)存,第二步調(diào)用類的構(gòu)造函數(shù)在自己已分配的內(nèi)存上構(gòu)建新的對象。placement new的好處:1)在已分配好的內(nèi)存上進(jìn)行對象的構(gòu)建,構(gòu)建速度快。2)已分配好的內(nèi)存可以反復(fù)利用,有效的避免內(nèi)存碎片問題。
| 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> using namespace std; struct Test { ??int data; ??Test() { cout << "Test::Test()" << endl; } ??~Test() { cout << "Test::~Test()" << endl; } }; int main() { ??// 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); ??return 0; } |
當(dāng)在性能關(guān)鍵的場合需要自定義分配器時可以使用Placement new。例如,一個slab分配器從單個的大內(nèi)存塊開始,使用placement new在塊里順序分配對象。這不僅避免了內(nèi)存碎片,也節(jié)省了malloc引起的堆遍歷的開銷。
?
在聲明變量的同時進(jìn)行分支
C++包含一個語法縮寫,能在聲明變量的同時進(jìn)行分支。看起來既像單個的變量聲明也可以有if或while這樣的分支條件。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | struct Event { virtual ~Event() {} }; struct MouseEvent : Event { int x, y; }; struct KeyboardEvent : Event { int key; }; void log(Event *event) { ??if (MouseEvent *mouse = dynamic_cast<MouseEvent *>(event)) ????std::cout << "MouseEvent " << mouse->x << " " << mouse->y << std::endl; ??else if (KeyboardEvent *keyboard = dynamic_cast<KeyboardEvent *>(event)) ????std::cout << "KeyboardEvent " << keyboard->key << std::endl; ??else ????std::cout << "Event" << std::endl; } |
?
成員函數(shù)的引用修飾符
C++11允許成員函數(shù)在對象的值類型上進(jìn)行重載,this指針會將該對象作為一個引用修飾符。引用修飾符會放在cv限定詞(譯者注:CV限定詞有 三種:const限定符、volatile限定符和const-volatile限定符)相同的位置并依據(jù)this對象是左值還是右值影響重載解析:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <iostream> struct Foo { ??void foo() & { std::cout << "lvalue" << std::endl; } ??void foo() && { std::cout << "rvalue" << std::endl; } }; int main() { ??Foo foo; ??foo.foo(); // Prints "lvalue" ??Foo().foo(); // Prints "rvalue" ??return 0; } |
?
圖靈完備的模板元編程
C++模板是為了實(shí)現(xiàn)編譯時元編程,也就是該程序能生成其它的程序。設(shè)計模板系統(tǒng)的初衷是進(jìn)行簡單的類型替換,但是在C++標(biāo)準(zhǔn)化過程中突然發(fā)現(xiàn)模板實(shí)際上功能十分強(qiáng)大,足以執(zhí)行任意計算,雖然很笨拙很低效,但通過模板特化的確可以完成一些計算:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | // Recursive template for general case template <int N> struct factorial { ??enum { value = N * factorial<N - 1>::value }; }; // Template specialization for base case template <> struct factorial<0> { ??enum { value = 1 }; }; enum { result = factorial<5>::value }; // 5 * 4 * 3 * 2 * 1 == 120 |
C++模板可以被認(rèn)為是一種功能型編程語言,因?yàn)樗鼈兪褂眠f歸而非迭代而且包含不可變狀態(tài)。你可以使用typedef創(chuàng)建一個任意類型的變量,使用enum創(chuàng)建一個int型變量,數(shù)據(jù)結(jié)構(gòu)內(nèi)嵌在類型自身。
| 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 integers template <int D, typename N> struct node { ??enum { data = D }; ??typedef N next; }; struct end {}; // Compile-time sum function template <typename L> struct sum { ??enum { value = L::data + sum<typename L::next>::value }; }; template <> struct sum<end> { ??enum { value = 0 }; }; // Data structures are embedded in types typedef node<1, node<2, node<3, end> > > list123; enum { total = sum<list123>::value }; // 1 + 2 + 3 == 6 |
當(dāng)然這些例子沒什么用,但模板元編程的確可以做一些有用的事情,比如可以操作類型列表。但是,使用C++模板的編程語言可用性極低,因此請謹(jǐn)慎和少量使用。模板代碼很難閱讀,編譯速度慢,而且因其冗長和迷惑的錯誤信息而難以調(diào)試。
?
指向成員的指針操作符
指向成員的指針操作符可以讓你在一個類的任何實(shí)例上描述指向某個成員的指針。有兩種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> using namespace std; struct Test { ??int num; ??void func() {} }; // Notice the extra "Test::" in the pointer type int Test::*ptr_num = &Test::num; void (Test::*ptr_func)() = &Test::func; int main() { ??Test t; ??Test *pt = new Test; ??// 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; ??delete pt; ??return 0; } |
該特征實(shí)際上十分有用,尤其在寫庫的時候。例如,Boost::Python, 一個用來將C++綁定到Python對象的庫,就使用成員指針操作符,在包裝對象時很容易的指向成員。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <iostream> #include <boost/python.hpp> using namespace boost::python; struct World { ??std::string msg; ??void greet() { std::cout << msg << std::endl; } }; BOOST_PYTHON_MODULE(hello) { ??class_<World>("World") ????.def_readwrite("msg", &World::msg) ????.def("greet", &World::greet); } |
記住使用成員函數(shù)指針與普通函數(shù)指針是不同的。在成員函數(shù)指針和普通函數(shù)指針之間casting是無效的。例如,Microsoft編譯器里的成員 函數(shù)使用了一個稱為thiscall的優(yōu)化調(diào)用約定,thiscall將this參數(shù)放到ecx寄存器里,而普通函數(shù)的調(diào)用約定卻是在棧上解析所有的參 數(shù)。
而且,成員函數(shù)指針可能比普通指針大四倍左右,編譯器需要存儲函數(shù)體的地址,到正確父地址(多個繼承)的偏移,虛函數(shù)表(虛繼承)中另一個偏移的索引,甚至在對象自身內(nèi)部的虛函數(shù)表的偏移也需要存儲(為了前向聲明類型)。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <iostream> struct A {}; struct B : virtual A {}; struct C {}; struct D : A, C {}; struct E; int main() { ??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; ??return 0; } // 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編譯器里所有的成員函數(shù)都是相同的大小,這是源于這樣一個聰明的設(shè)計:生成“thunk”函數(shù)來運(yùn)用右偏移而不是存儲指針自身內(nèi)部的偏移。
?
靜態(tài)實(shí)例方法
C++中可以通過實(shí)例調(diào)用靜態(tài)方法也可以通過類直接調(diào)用。這可以使你不需要更新任何調(diào)用點(diǎn)就可以將實(shí)例方法修改為靜態(tài)方法。
| 1 2 3 4 5 6 7 | struct Foo { ??static void foo() {} }; // These are equivalent Foo::foo(); Foo().foo(); |
?
重載++和–
C++的設(shè)計中自定義操作符的函數(shù)名稱就是操作符本身,這在大部分情況下都工作的很好。例如,一元操作符的-和二元操作符的-(取反和相減)可以通 過參數(shù)個數(shù)來區(qū)分。但這對于一元遞增和遞減操作符卻不奏效,因?yàn)樗鼈兊奶卣魉坪跬耆嗤++語言有一個很笨拙的技巧來解決這個問題:后綴++和–操作 符必須有一個空的int參數(shù)作為標(biāo)記讓編譯器知道要進(jìn)行后綴操作(是的,只有int類型有效)。
| 1 2 3 4 | struct Number { ??Number &operator ++ (); // Generate a prefix ++ operator ??Number operator ++ (int); // Generate a postfix ++ operator }; |
?
操作符重載和檢查順序
重載,(逗號),||或者&&操作符會引起混亂,因?yàn)樗蚱屏苏5臋z查規(guī)則。通常情況下,逗號操作符在整個左邊檢查完畢才開始檢 查右邊,|| 和 &&操作符有短路行為:僅在必要時才會去檢查右邊。無論如何,操作符的重載版本僅僅是函數(shù)調(diào)用且函數(shù)調(diào)用以未指定的順序檢查它們的參數(shù)。
重載這些操作符只是一種濫用C++語法的方式。作為一個實(shí)例,下面我給出一個Python形式的無括號版打印語句的C++實(shí)現(xiàn):
| 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__ { ??struct print { ????bool space; ????print() : space(false) {} ????~print() { std::cout << std::endl; } ????template <typename T> ????print &operator , (const T &t) { ??????if (space) std::cout << ' '; ??????else space = true; ??????std::cout << t; ??????return *this; ????} ??}; } #define print __hidden__::print(), int main() { ??int a = 1, b = 2; ??print "this is a test"; ??print "the sum of", a, "and", b, "is", a + b; ??return 0; } |
?
函數(shù)作為模板參數(shù)
眾所周知,模板參數(shù)可以是特定的整數(shù)也可以是特定的函數(shù)。這使得編譯器在實(shí)例化模板代碼時內(nèi)聯(lián)調(diào)用特定的函數(shù)以獲得更高效的執(zhí)行。下面的例子里,函數(shù)memoize的模板參數(shù)也是一個函數(shù)且只有新的參數(shù)值才通過函數(shù)調(diào)用(舊的參數(shù)值可以通過cache獲得):
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <map> template <int (*f)(int)> int memoize(int x) { ??static std::map<int, int> cache; ??std::map<int, int>::iterator y = cache.find(x); ??if (y != cache.end()) return y->second; ??return cache[x] = f(x); } int fib(int n) { ??if (n < 2) return n; ??return memoize<fib>(n - 1) + memoize<fib>(n - 2); } |
?
模板的參數(shù)也是模板
模板參數(shù)實(shí)際上自身的參數(shù)也可以是模板,這可以讓你在實(shí)例化一個模板時可以不用模板參數(shù)就能夠傳遞模板類型。看下面的代碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | template <typename T> struct Cache { ... }; template <typename T> struct NetworkStore { ... }; template <typename T> struct MemoryStore { ... }; template <typename Store, typename T> struct CachedStore { ??Store store; ??Cache<T> cache; }; CachedStore<NetworkStore<int>, int> a; CachedStore<MemoryStore<int>, int> b; |
CachedStore的cache存儲的數(shù)據(jù)類型與store的類型相同。然而我們在實(shí)例化一個CachedStore必須重復(fù)寫數(shù)據(jù)類型(上面 的代碼是int型),store本身要寫,CachedStore也要寫,關(guān)鍵是我們這并不能保證兩者的數(shù)據(jù)類型是一致的。我們真的只想要確定數(shù)據(jù)類型一 次即可,所以我們可以強(qiáng)制其不變,但是沒有類型參數(shù)的列表會引起編譯出錯:
| 1 2 3 | // 下面編譯通不過,因?yàn)镹etworkStore和MemoryStore缺失類型參數(shù) CachedStore<NetworkStore, int> c; CachedStore<MemoryStore, int> d; |
模板的模板參數(shù)可以讓我們獲得想要的語法。注意你必須使用class關(guān)鍵字作為模板參數(shù)(他們自身的參數(shù)也是模板)
| 1 2 3 4 5 6 7 8 | template <template <typename> class Store, typename T> struct CachedStore2 { ??Store<T> store; ??Cache<T> cache; }; CachedStore2<NetworkStore, int> e; CachedStore2<MemoryStore, int> f; |
?
try塊作為函數(shù)
函數(shù)的try塊會在檢查構(gòu)造函數(shù)的初始化列表時捕獲拋出的異常。你不能在初始化列表的周圍加上try-catch塊,因?yàn)槠渲荒艹霈F(xiàn)在函數(shù)體外。為了解決這個問題,C++允許try-catch塊也可作為函數(shù)體:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | int f() { throw 0; } // 這里沒有辦法捕獲由f()拋出的異常 struct A { ??int a; ??A::A() : a(f()) {} }; // 如果try-catch塊被用作函數(shù)體并且初始化列表移至try關(guān)鍵字之后的話, // 那么由f()拋出的異常就可以捕獲到 struct B { ??int b; ??B::B() try : b(f()) { ??} catch(int e) { ??} }; |
奇怪的是,這種語法不僅僅局限于構(gòu)造函數(shù),也可用于其他的所有函數(shù)定義。
?
譯自:http://madebyevan.com/obscure-cpp-features/
轉(zhuǎn)載于:https://www.cnblogs.com/lanxuezaipiao/p/3501078.html
總結(jié)
以上是生活随笔為你收集整理的C++你不知道的那些事儿—C++语言的15个晦涩特性的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 定川寨之战宋军失败造成了什么严重影响
- 下一篇: 黄石天空之城门票对军人优待证有优惠吗天空