一樣是C++工程師,有的人寫的是 C with object,有的人寫的是 C++ 98,fashion一點兒的寫 C++ 11/14,還有些程序員使用譚++。html
上文只是段子,不少同窗對 C++ 的瞭解僅停留在課堂上的理解,而不關注 C++ 的最新發展;事實上,C++ 的新特性不少能夠大幅提升開發效率、程序運行效率以及提升代碼的安全性和穩定性等。java
本文主要關於左值右值、auto 關鍵字、智能指針、default、delete、override、final、{}初始化、using別名、基於範圍的 for 循環、lambda 表達式 等內容,篇幅較長,請你們耐心閱讀多作實驗。python
C++( 包括 C) 中全部的表達式和變量要麼是左值,要麼是右值。通俗的左值的定義就是非臨時對象,那些能夠在多條語句中使用的對象。全部的變量都知足這個定義,在多條代碼中均可以使用,都是左值。右值是指臨時的對象,它們只在當前的語句中有效。IBM 右值引用與轉移語義ios
有一種甄別表達式是否左值的方法,是檢查可否得到該表達式的地址;若是能夠取得,基本上能夠判定是左值表達式;若是不能取得則一般是右值。程序員
C++ 中全部值都屬於 左值、右值二者之一;若細分的話,右值能夠分爲:純右值、將亡值。編程
咱們能夠理解右值爲臨時對象,像有些函數返回的對象是臨時對象,該句執行完畢就會釋放臨時對象空間,所以留下右值的引用在之前並無用。數組
C++11 是提出了右值引用,能夠延長臨時對象的生存週期,其建立方法爲type && vb = xx;
對應的左值引用的生命符號爲&
。安全
#include <iostream>
#include <string>
int main() {
std::string s1 = "Test";
// std::string&& r1 = s1; // 錯誤:不能綁定到左值
const std::string& r2 = s1 + s1; // okay:到 const 的左值引用延長生存期
// r2 += "Test"; // 錯誤:不能經過到 const 的引用修改
std::string&& r3 = s1 + s1; // okay:右值引用延長生存期
r3 += "Test"; // okay:能經過到非 const 的引用修改
std::cout << r3 << '\n';
}
複製代碼
更重要的是,當函數同時具備右值引用和左值引用的重載時,右值引用重載綁定到右值(包含純右值和亡值),而左值引用重載綁定到左值:ide
#include <iostream>
#include <utility>
void f(int& x) {
std::cout << "lvalue reference overload f(" << x << ")\n";
}
void f(const int& x) {
std::cout << "lvalue reference to const overload f(" << x << ")\n";
}
void f(int&& x) {
std::cout << "rvalue reference overload f(" << x << ")\n";
}
int main() {
int i = 1;
const int ci = 2;
f(i); // 調用 f(int&)
f(ci); // 調用 f(const int&)
f(3); // 調用 f(int&&)
// 若不提供 f(int&&) 重載則會調用 f(const int&)
f(std::move(i)); // 調用 f(int&&)
// 右值引用變量在用於表達式時是左值
int&& x = 1;
f(x); // calls f(int& x)
f(std::move(x)); // calls f(int&& x)
}
複製代碼
移動語義是經過盜取將亡值的變量內存空間,首先確保該部分空間以後不會使用,而後將該空間佔爲己有,看起來像是一個拷貝操做。 移動語義位於頭文件#include<algorithm>
,函數名爲std::move
。函數
#include <iostream>
#include <vector>
#include <list>
#include <iterator>
#include <thread>
#include <chrono>
void f(int n) {
std::this_thread::sleep_for(std::chrono::seconds(n));
std::cout << "thread " << n << " ended" << '\n';
}
int main() {
std::vector<std::thread> v;
v.emplace_back(f, 1);
v.emplace_back(f, 2);
v.emplace_back(f, 3);
std::list<std::thread> l;
// copy() 沒法編譯,由於 std::thread 不可複製
std::move(v.begin(), v.end(), std::back_inserter(l));
for (auto& t : l) t.join();
}
複製代碼
auto
和decltype
關鍵詞是新增的關鍵詞,咱們知道C++是強類型語言,但使用這兩個關鍵詞,能夠不用手寫完整類型,而是讓編譯器自行推導真實類型。
auto
用法很是簡單,示例以下:
#include <iostream>
#include <utility>
template<class T, class U>
auto add(T t, U u) { return t + u; } // 返回類型是 operator+(T, U) 的類型
// 在其所調用的函數返回引用的狀況下
// 函數調用的完美轉發必須用 decltype(auto)
template<class F, class... Args>
decltype(auto) PerfectForward(F fun, Args&&... args)
{
return fun(std::forward<Args>(args)...);
}
template<auto n> // C++17 auto 形參聲明
auto f() -> std::pair<decltype(n), decltype(n)> // auto 不能從花括號初始化器列表推導
{
return {n, n};
}
int main()
{
auto a = 1 + 2; // a 的類型是 int
auto b = add(1, 1.2); // b 的類型是 double
static_assert(std::is_same_v<decltype(a), int>);
static_assert(std::is_same_v<decltype(b), double>);
auto c0 = a; // c0 的類型是 int,保有 a 的副本
decltype(auto) c1 = a; // c1 的類型是 int,保有 a 的副本
decltype(auto) c2 = (a); // c2 的類型是 int&,爲 a 的別名
std::cout << "a, before modification through c2 = " << a << '\n';
++c2;
std::cout << "a, after modification through c2 = " << a << '\n';
auto [v, w] = f<0>(); // 結構化綁定聲明
auto d = {1, 2}; // OK:d 的類型是 std::initializer_list<int>
auto n = {5}; // OK:n 的類型是 std::initializer_list<int>
// auto e{1, 2}; // C++17 起錯誤,以前爲 std::initializer_list<int>
auto m{5}; // OK:C++17 起 m 的類型爲 int,以前爲 initializer_list<int>
// decltype(auto) z = { 1, 2 } // 錯誤:{1, 2} 不是表達式
// auto 經常使用於無名類型,例如 lambda 表達式的類型
auto lambda = [](int x) { return x + 3; };
// auto int x; // 於 C++98 合法,C++11 起錯誤
// auto x; // 於 C 合法,於 C++ 錯誤
}
複製代碼
decltype
的用處則是檢查實體的聲明類型,或者表達式的類型和值的類型。用法以下:decltype(實體/表達式)
。可使用另外一個的實體的類型來定義新的變量。
#include <iostream>
struct A { double x; };
const A* a;
decltype(a->x) y; // y 的類型是 double(其聲明類型)
decltype((a->x)) z = y; // z 的類型是 const double&(左值表達式)
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) // 返回類型依賴於模板形參
{ // C++14 開始能夠推導返回類型
return t+u;
}
int main()
{
int i = 33;
decltype(i) j = i * 2;
std::cout << "i = " << i << ", "
<< "j = " << j << '\n';
auto f = [](int a, int b) -> int
{
return a * b;
};
decltype(f) g = f; // lambda 的類型是獨有且無名的
i = f(2, 2);
j = g(3, 3);
std::cout << "i = " << i << ", "
<< "j = " << j << '\n';
}
複製代碼
能夠經過這種寫法,將函數的返回值申明放在函數聲明的最後;auto function_name( 形參 ) (屬性,如 override等) (異常說明,可選) -> 返回值類型
。 老實說,這種寫法讓我以爲本身寫的不是C++,估計大部分狀況我不回去使用這個特性吧。。。
// 返回指向 f0 的指針的函數
auto fp11() -> void(*)(const std::string&)
{
return f0;
}
複製代碼
C++11起已經不建議使用C語言樣式的強制類型轉換,推薦使用static_cast、const_cast、reinterpret_cast、dynamic_cast
等方法的類型轉換。
關鍵詞 | 說明 |
---|---|
static_cast (經常使用) |
用於良性轉換,通常不會致使意外發生,風險很低。 |
const_cast |
用於 const 與非 const、volatile 與非 volatile 之間的轉換。 |
reinterpret_cast |
高度危險的轉換,這種轉換僅僅是對二進制位的從新解釋,不會藉助已有的轉換規則對數據進行調整,可是能夠實現最靈活的 C++ 類型轉換。 |
dynamic_cast |
藉助 RTTI,用於類型安全的向下轉型(Downcasting)。 |
C++四種類型轉換運算符:static_cast、dynamic_cast、const_cast和reinterpret_cast
參見C++ 智能指針
聽說一般C++頭文件中NULL都是定義爲#define NULL 0
,所以本質上NULL
的類型是int
,使用NULL
來表示空指針是很是不合適的行爲,因而C++11從新定義了一個不是int
類型且適用於空指針的關鍵詞。
關鍵詞 nullptr 表明指針字面量。它是 std::nullptr_t 類型的純右值。存在從 nullptr 到任何指針類型及任何成員指針類型的隱式轉換。一樣的轉換對於任何空指針常量也存在,空指針常量包括 std::nullptr_t 的值,以及宏 NULL。nullptr,指針字面量
咱們知道default
自己是switch
語句的關鍵詞,C++11中又擴展了新的用法,能夠用來告訴編譯器生成默認的成員函數(默認構造函數等)。 特殊成員函數以及比較運算符 (C++20 起)是僅有能被預置的函數,即便用 = default 替代函數體進行定義(細節見其相應頁面)
例如:默認構造函數可使用 類名 ( ) = default ; (C++11 起)
方式聲明,而後能夠不用在 *.cpp
文件中寫函數體實現,這個函數會使用編譯器默認生成。
delete的新用法--棄置函數,相比於讓對象中的構造函數爲私有,如今有了刪除該函數的方法。
若是取代函數體而使用特殊語法
= delete ;
,則該函數被定義爲棄置的(deleted)。任何棄置函數的使用都是非良構的(程序沒法編譯)。這包含調用,包括顯式(以函數調用運算符)及隱式(對棄置的重載運算符、特殊成員函數、分配函數等的調用),構成指向棄置函數的指針或成員指針,甚或是在不求值表達式中使用棄置函數。可是,容許隱式 ODR 式使用 恰好被棄置的非純虛成員函數。
struct sometype {
void* operator new(std::size_t) = delete;
void* operator new[](std::size_t) = delete;
};
sometype* p = new sometype; // 錯誤:嘗試調用棄置的 sometype::operator new
複製代碼
這個關鍵詞翻譯爲改寫,當指定一個虛函數覆蓋另外一個虛函數時使用,Effective Modern C++一書中建議在該狀況時必定加上該關鍵詞,這樣可讓編譯器幫助咱們檢查咱們是否正肯定義了覆蓋的函數(若是不正肯定義則會編譯報錯)。
這部分代碼將不會正確編譯,由於加了 override 後,編譯器會爲咱們尋找繼承的基類中對應的虛函數,而這裏就能夠發現咱們函數聲明上的一些錯誤。而若是不加override,這裏會成功編譯,但絕對不是咱們想要的編譯結果。
/* * Key idea: * * The below code won't compile, but, when written this way, compilers will * kvetch about all the overriding-related problems. */
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
void mf4() const;
};
// Uncomment this, compile and see the compiler errors.
//class Derived: public Base {
//public:
// virtual void mf1() override;
// virtual void mf2(unsigned int x) override;
// virtual void mf3() && override;
// void mf4() const override;
//};
複製代碼
能夠只有override修飾的函數聲明正確纔可以成功編譯。
/* * Key idea: * * This the code-example that uses override and is correct. */
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
virtual void mf4() const;
};
class Derived: public Base {
public:
virtual void mf1() const override;
virtual void mf2(int x) override;
virtual void mf3() & override;
void mf4() const override; // adding "virtual" is OK,
}; // but not necessary
複製代碼
聲明某一個虛函數不得被覆蓋。
有更多的方法初始化一個對象,好比花括號初始化列表實例以下:
/* * Key idea: * * The treatment of braced initializers is the only way in which auto type * deduction and template type deduction differ. */
#include <initializer_list>
template<typename T> // template with parameter
void f(T param) {} // declaration equivalent to
// x's declaration
template<typename T>
void f2(std::initializer_list<T> initList) {}
int main() {
{
int x1 = 27;
int x2(27);
int x3 = {27};
int x4{27};
}
{
auto x1 = 27; // type is int, value is 27
auto x2(27); // ditto
auto x3 = {27}; // type is std::initializer_list<int>,
// value is {27}
auto x4{27}; // ditto
//auto x5 = {1, 2, 3.0}; // error! can't deduce T for
// // std::initializer_list<T>
}
{
auto x = { 11, 23, 9 }; // x's type is
// std::initializer_list<int>
//f({ 11, 23, 9 }); // error! can't deduce type for T
f2({ 11, 23, 9 }); // T deduced as int, and initList's
// type is std::initializer_list<int>
}
}
複製代碼
除了 typedef
關鍵詞,還可使用using
關鍵詞建立別名,Effective Modern C++一書更推薦使用別名聲明。
/* * Key Idea: * * Using alias declarations is easier to read than function pointers. */
#include <string>
// FP is a synonym for a pointer to a function taking an int and
// a const std::string& and returning nothing
typedef void (*FP)(int, const std::string&); // typedef
// same meaning as above
using FP = void (*)(int, const std::string&); // alias
// declaration
複製代碼
經過在枚舉類型定義中加一個關鍵詞,能夠限制枚舉類型的做用域。enum test
-> enum class test
;
/* * Key Idea: * * In C++11, the names of scoped enums do not belong to the scope containing * the enum. */
enum class Color { black, white, red }; // black, white, red
// are scoped to Color
auto white = false; // fine, no other
// "white" in scope
//Color c1 = white; // error! no enumerator named
// "white" is in this scope
Color c2 = Color::white; // fine
auto c3 = Color::white; // also fine (and in accord
// with Item4's advice)
複製代碼
C++也能夠像python語言那樣使用基於範圍的for循環了,是一個進步吧,集各家之所長。 基於範圍的for循環語法是for(範圍聲明:範圍表達式)
。其中,範圍聲明:一個具名變量的聲明,其類型是由 範圍表達式 所表示的序列的元素的類型,或該類型的引用,一般用 auto 說明符進行自動類型推導;範圍表達式:任何能夠表示一個合適的序列(數組,或定義了 begin 和 end 成員函數或自由函數的對象,見下文)的表達式,或一個花括號初始化器列表,基本上std中幾個常見容器,如:vector、list等都是支持基於範圍的for循環的。
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {0, 1, 2, 3, 4, 5};
for (const int& i : v) // 以 const 引用訪問
std::cout << i << ' ';
std::cout << '\n';
for (auto i : v) // 以值訪問,i 的類型是 int
std::cout << i << ' ';
std::cout << '\n';
for (auto& i : v) // 以引用訪問,i 的類型是 int&
std::cout << i << ' ';
std::cout << '\n';
for (int n : {0, 1, 2, 3, 4, 5}) // 初始化器能夠是花括號初始化器列表
std::cout << n << ' ';
std::cout << '\n';
int a[] = {0, 1, 2, 3, 4, 5};
for (int n : a) // 初始化器能夠是數組
std::cout << n << ' ';
std::cout << '\n';
for (int n : a)
std::cout << 1 << ' '; // 沒必要使用循環變量
std::cout << '\n';
}
複製代碼
lambda 表達式便是無名函數,很像java中的臨時函數(集各家之所長,比各家難用……) lambda的語法以下:
[ 俘獲 ] <模板形參>(可選)(C++20) ( 形參 ) 說明符(可選) 異常說明 attr -> ret requires(可選)(C++20) { 函數體 }
[ 俘獲 ] ( 形參 ) -> ret { 函數體 }
[ 俘獲 ] ( 形參 ) { 函數體 }
[ 俘獲 ] { 函數體 }
複製代碼
lambda 表達式細節更多,有可能單獨寫一個博客進行解釋說明,若是你們有興趣的話,能夠先看看zh.cppreference.com這篇說明。