笔记③:牛客校招冲刺集训营---C++工程师(5.9 C++新特性)
0625
C++工程師
- 第5章 高頻考點與真題精講
- 5.1 指針 & 5.2 函數
- 5.3 面向對象(和5.4、5.5共三次直播課)
- 5.3.1 - 5.3.11
- 5.3.12-38
- 5.6 內存管理①(結合計算機操作系統筆記)
- 5.7 內存管理②(結合計算機操作系統筆記)
- 5.7 名稱空間、模板
- 5.8 STL(標準模板庫)
- 5.9 C++新特性
- 5.9.1 新類型、路徑表示
- 5.9.2 統一的初始化(初始化列表)
- 5.9.3 聲明
- 1.關鍵字auto
- 2.關鍵字decltype(declare type)☆☆
- 3.返回類型后置
- 4.模板別名(using 別名 = )
- 5.關鍵字nullptr
- 5.9.4 智能指針☆☆☆☆☆
- 為什么要引入智能指針?
- 智能指針的原理和使用
- 智能指針注意事項(auto_ptr、unique_ptr、shared_ptr)
- 如何選擇智能指針?
- weak_ptr(解決 循環引用 問題)
- 5.9.5 異常規范方面的修改(關鍵字noexcept)
- 5.9.6 作用域內枚舉
- 5.9.7 對類的修改
- 顯示轉換運算符explicit(禁止自動轉換)
- 類內成員初始化(還有一個初始化列表)
- 5.9.8 ☆右值引用(&&) --- 指向右值的引用
- 5.9.9 ☆☆☆移動語義
- move()函數
- 5.9.10 ☆☆☆Lambda表達式(匿名的函數)(全局函數、仿函數、Lambda表示式)
- 5.9.11 ☆☆☆類型轉換運算符
- 靜態轉換static_cast
- 動態轉換dynamic_cast
- static_cast 和 dynamic_cast 的比較
- 常量轉換const_cast
- 重解釋轉換reinterpret_cast
- 5.10-5.13 項目回顧(Linux系統編程、網絡編程、計算機網絡、操作系統)
第5章 高頻考點與真題精講
5.1 指針 & 5.2 函數
5.3 面向對象(和5.4、5.5共三次直播課)
5.3.1 - 5.3.11
見筆記①:牛客校招沖刺集訓營—C++工程師
5.3.12-38
5.6 內存管理①(結合計算機操作系統筆記)
5.7 內存管理②(結合計算機操作系統筆記)
5.7 名稱空間、模板
5.8 STL(標準模板庫)
見筆記②:牛客校招沖刺集訓營—C++工程師
5.9 C++新特性
視頻課5.9的全部加上視頻課5.10的前01:14:39都是關于C++新特性的。
5.9.1 新類型、路徑表示
C++ 11 新增了類型 long long 和 unsigned long long,以支持 64 位(或更寬)的整型;
新增了類型 char16_t 和 char32_t,以支持 16 位和 32 位的字符表示;
還新增了”原始“字符串。
表示路徑的時候,因為一般的路徑都是用反斜杠隔開的,或者包含一些其他類型的符號(問號、逗號等),而路徑是使用雙引號括起來的,所以就涉及到轉義字符的情況,三種方法:
5.9.2 統一的初始化(初始化列表)
C++ 11 擴大了用大括號括起的列表(初始化列表)的適用范圍,使其可用于所有內置類型和用戶定義的類型(即類對象)。使用初始化列表時,可添加等號(=),也可不添加:
int x = {5}; double y {2.75}; short quar[5] {1, 2, 3, 4, 5}; int * ar = new int [4] {2, 3, 4, 5}; class Stump { private: int roots; double weight; public: Stump(int r, double w) : roots(r), weight(w) {} }; Stump s1(3, 15.6); Stump s2{5, 43.4}; Stump s3 = {4, 32.1};5.9.3 聲明
1.關鍵字auto
auto:自動類型推斷(轉換)。
vector<int> v = {1, 2, 3, 4, 5};//for(vector<int>::iterator it = v.begin(); it != v.end(); ++it){for(auto it = v.begin(); it != v.end(); ++it){cout << *it << ", ";}cout << endl;2.關鍵字decltype(declare type)☆☆
先看個例子:
int a; float b;//float c = a * b;decltype(a * b) c = a * b;//假如不知道c的類型,可用decltype推導出來double x;int n;decltype(x * n) q;//double類型decltype(&x) pd;//double*類型template<typename T, typename U> void ef(T t, U u) {decltype(T * U) tu;// }decltype 將變量的類型聲明為表達式指定的類型。一般用在模板中。
(以下內容來自C++ Primer Plus(嵌入式公開課)—第8章 函數探幽中的8.5.6 模板函數的發展—關鍵字decltype)
假如有以下模板:
考慮xpy是什么類型?
可能是T1、T2或者其他類型。
當T1是double型,T2是int型,x+y是double型,xpy是T1類型;
當T1是short型,T2是int型,x+y是int型,xpy是T2類型;
當T1時short型,T2是char型,x+y的結果自動整型提升,xpy是int型。
C++新增的關鍵字decltype提供了解決方案:
template <class T1, class T2> void ft(T1 x, T2 y){...decltype(x + y) xpy = x + y;... }當使用關鍵字decltype時,為確定類型,編譯器必須遍歷一個核對表。假如有如下聲明:
decltype(expression) var; decltype(x) y; // 讓y的類型與x相同,x是一個表達式則核對表的簡化版如下:
1.如果expression是一個沒有用括號括起的標識符,則var的類型與該標識符的類型相同,包括const等限定符:
2.如果expression是一個函數調用,則var的類型與函數的返回類型相同:
long indeed(int); decltype(indeed(3)) m;//m和indeed()函數的返回值類型相同,即long3.如果expression是一個用括號括起來的標識符,則var為指向其類型的引用:
(注意:這里的expression必須是一個左值;如果是個右值,var的類型就是這個右值的類型,而不是它的引用)
補充:左值&右值
左值:可修改的值,例如普通變量,int m, n;
非左值:不可修改的值,例如常量(10)和表達式(m+3);
4.如果前面的條件都不滿足,則var的類型與expression的類型相同:
int j = 3; int& k = j; int& n = j; decltype(j + 6) i1;//i1是int decltype(100L) i2; //i2是long decltype(k + n) i3;//i3是int decltype((k + n)) i4;//i4是int,不是int&,因為k+n是右值請注意,雖然k和n都是引用,但表達式k + n不是引用,它是兩個int的和,是個右值,所以i3是int類型;
由于k + n是右值,所以給它加個括號之后,i4的類型跟這個右值的類型相同,而不是它的引用。
3.返回類型后置
C++11 新增了一種函數聲明語法:
在函數名和參數列表后面(而不是前面)指定返回類型:
注意:這里的auto是一個占位符,它并不能推導出返回值的類型,因為在寫返回值的時候還沒有使用過形參,根本無法推導出返回值的類型。
有一個相關的問題是 decltype 本身無法解決的:
template<typename T1, typename T2> ??? gt(T1 x, T2 y) {...return x + y; }用decltype來解決:decltype 在參數聲明后面,因此x和y位于作用域內,可以使用
template<typename T1, typename T2) auto eff(T1 x, T2 y) -> decltype(x*y){ // decltype 在參數聲明后面,因此x和y位于作用域內,可以使用...return x * y; }4.模板別名(using 別名 = )
(這個用的很少,了解下)
對于冗長或復雜的標識符,如果能夠創建其別名將很方便。之前,C++ 提供了 typedef:
C++ 11 提供了另一種創建別名的語法:
using itType = std::vector<std::string>::iterator;差別在于,這種方式也可以用于模板部分具體化,但 typedef 不能:
template<typename T> using arr12 = std::array<T,12>;上述語句具體化模板array<T,int>,對于下述聲明:
std::array<double,12> a1; std::array<std::string,12> a2可將它們替換為如下聲明:
arr12<double> a1; arr12<std::string> a2;5.關鍵字nullptr
以前是NULL,C++11中變成了nullptr。
5.9.4 智能指針☆☆☆☆☆
(視頻課中從50:20開始,到01:49:49結束)
第一次涉及到智能指針是在筆記②:牛客校招沖刺集訓營—C++工程師中的☆☆☆指針運算符重載(* 和 ->)(寫個智能指針類),寫了個智能指針類,類中重載解引用*和箭頭->運算符,并且在析構函數中將new的空間釋放掉,就可以避免因為忘記手動delete而帶來的內存泄漏問題,所以叫智能指針。
為什么要引入智能指針?
如果在程序中使用 new 從堆(自由存儲區)分配內存,等到不再需要時,應使用 delete 將其釋放。C++ 引入了智能指針 auto_ptr(C++98), 以幫助自動完成這個過程。智能指針是行為類似于指針的類對象。
在使用時(尤其是使用STL),需要更精致的機制,C++ 11 摒棄了 auto_ptr,并新增了三種智能指針:unique_ptr、shared_ptr 和 weak_ptr。(嚴格意義上來說前兩種是智能指針,第三種是為了解決第二種智能指針出現的循環引用的問題)
上面的函數每當調用時,該函數都分配堆中的內存,但從不收回,從而導致了內存泄露。解決方案,在return 語句之前添加 delete 語句釋放分配的內存:delete ps;
但即使添加了 delete 語句,有時也會有問題:
void remodel(std::string & str) {std::string * ps = new std::string(str);// ...if(...) {throw exception();}str = *ps;delete ps;return; }如果在程序中拋出異常,就執行不到delete ps;這句,依然會導致內存泄漏,所以就引入了智能指針。
智能指針的原理和使用
智能指針解決問題的思想:將常規指針進行包裝,當智能指針對象過期時,讓它的析構函數對常規指針進行內存釋放。
auto_ptr、unique_ptr 和 shared_ptr 這三個智能指針模板都定義了類似指針的對象,可以將 new 獲得(直接或間接)的地址賦給這種對象。當智能指針過期時,其析構函數將使用 delete 來釋放內存。
智能指針的頭文件:
#include <memory>語法:
(注意:參數必須是new的內容)
示例:
#include <iostream> #include <string> #include <memory> using namespace std; class Person { private :string m_name;int m_age; public:Person(string name, int age) : m_name(name), m_age(age) {cout << "對象創建" << endl;}~Person() {cout << "對象銷毀" << endl;}void showInfo() {cout << m_name << " : " << m_age << endl;} }; int main() {{auto_ptr<Person> ps(new Person("zs", 20));ps->showInfo();}{unique_ptr<Person> ps(new Person("ww", 22));ps->showInfo();}{shared_ptr<Person> ps(new Person("ls", 21));ps->showInfo();}return 0; }結果:
對象創建 zs : 20 對象銷毀 對象創建 ww : 22 對象銷毀 對象創建 ls : 21 對象銷毀所有智能指針類都有一個 explicit 構造函數,該構造函數將指針作為參數。因此不能自動將指針轉換為智能指針對象:
由于智能指針模板類的定義方式,智能指針對象的很多方面都類似于常規指針。
例如,如果 ps 是一個智能指針對象,則可以對它執行解除引用操作(*ps)、用它來訪問結構成員(ps->m_name)、將它賦給指向相同類型的常規指針。還可以將智能指針對象賦給另一個同類型的智能指針對象。
智能指針注意事項(auto_ptr、unique_ptr、shared_ptr)
示例1:
(ps和vo指向兩塊內存)
示例2:
(ps和vo指向同一塊內存,但可以在delete ps;之前先讓ps指向空,斷了跟10的聯系,這也是后面要講的移動語義的原理)
示例3:
(智能指針的參數必須是new出來的內容)
示例4:
//常規指針:int* ps = new int(10);int* vo;vo = ps;cout << *ps << ", " << ps << endl;//cout << *vo << ", " << vo << endl;// //ps = nullptr;//沒有這行會出錯delete ps;delete vo;如果沒有ps = nullptr;這行會出錯,因為ps和vo都指向同一塊內存,而在后面對這一塊內存釋放了兩次,就會報錯!
這也是淺拷貝的機制:直接對地址值進行拷貝(值拷貝)。
解決方法就是上面的示例1(讓ps和vo分別指向兩塊不同的內存)和示例2(先讓ps指向空,然后再delete ps;);
或者可以用智能指針來解決:
1.建立所有權(ownership)概念,對于特定的對象,只能有一個智能指針可擁有它,這樣只有擁有該對象的智能指針的析構函數會刪除該對象,然后讓賦值操作轉讓所有權,這就是auto_ptr和unique_ptr的策略,其中unique_ptr的策略更嚴格(也就更安全);
2.創建智能更高的指針,跟蹤引用特定對象的智能指針計數,稱為引用計數(reference counting)。例如,賦值時計數將加一,而指針過期時計數將減一,僅當最后一個指針過期時,才調用delete,這就是shared_ptr的策略。
那么上面說的智能指針具體怎么解決上面的重復釋放同一塊內存的問題呢?
auto_ptr運行后會報錯:
auto_ptr<string> ps(new string("abc")); auto_ptr<string> vo; vo = ps; cout << *ps << endl;//上一句將所有權轉讓給了vo,就不能再對ps進行操作了shared_ptr可以:
shared_ptr<string> ps(new string("abc")); shared_ptr<string> vo; vo = ps; cout << *ps << endl;unique_ptr編譯出錯:
unique_ptr<string> ps(new string("abc")); // #1 unique_ptr<string> vo; // #2 vo = ps; // #3 cout << *ps << endl; // #4
編譯器認為語句 #3 非法,避免了 vo 不再指向有效數據的問題。因此,unique_ptr 比 auto_ptr 更安全(編譯階段錯誤)。
小結:
上面的auto_ptr在編譯階段不會報錯,運行時才會報錯;
而unique_ptr在編譯時就報錯,所以說后者后嚴格,也就更安全。
(因為在編譯階段報錯比在運行時報錯更容易找到問題所在)
以上兩種情況是允許的,程序試圖將一個unique_ptr賦給另一個時:
如果源 unique_ptr是個臨時右值,編譯器允許這樣做;
如果源unique_ptr將存在一段時間,編譯器將禁止這樣做。
補充:
臨時右值指的是使用一下之后就會被釋放,就像int a = 10;中的10一樣,這個10不會存在一段時間,用完就立馬被釋放了;
上面程序中demo()函數中return的tmp就是一個臨時的右值,雖然這個tmp在函數里面是左值,但return的是它的副本,屬于一個臨時右值;
賦給ps1的是一個匿名對象,它也屬于一個臨時右值。
unique_ptr 相比 auto_ptr 還有一個優點,它有一個可用于數組的變體:
模板 auto_ptr 使用 delete 而不是 delete[],因此只能與 new 一起使用,而不能與 new [] 一起使用;
但 unique_ptr 有使用 new [] 和 delete [] 的版本:
如何選擇智能指針?
如果程序要使用多個指向同一個對象的指針,應該選擇 shared_ptr;
如果程序不需要多個指向同一個對象的指針,則可以使用 unique_ptr;
如果使用 new [] 分配內存,應該選擇 unique_ptr;
如果函數使用 new 分配內存,并返回指向該內存的指針,將其返回類型聲明為 unique_ptr 是不錯的選擇。
weak_ptr(解決 循環引用 問題)
C++11 標準雖然將 weak_ptr 定位為智能指針的一種,但該類型指針通常不單獨使用(沒有實際用處),只能和 shared_ptr 類型指針搭配使用。甚至于,可以將 weak_ptr 類型指針視為 shared_ptr 指針的一種輔助工具,借助 weak_ptr 類型指針, 我們可以獲取 shared_ptr 指針的一些狀態信息,比如有多少指向相同的 shared_ptr 指針、shared_ptr 指針指向的堆內存是否已經被釋放等等。
需要注意的是,當 weak_ptr 類型指針的指向和某一 shared_ptr 指針相同時,weak_ptr 指針并不會使所指堆內存的引用計數加 1;同樣,當 weak_ptr 指針被釋放時,之前所指堆內存的引用計數也不會因此而減 1。也就是說,weak_ptr 類型指針并不會影響所指堆內存空間的引用計數。
除此之外,weak_ptr 模板類中沒有重載 * 和 -> 運算符,這也就意味著,weak_ptr 類型指針只能訪問所指的堆內存,而無法修改它。
weak_ptr 可以用來解決循環引用問題。
循環引用:
加了weak_ptr:
weak_ptr 的使用:(配合著shared_ptr一起使用)
5.9.5 異常規范方面的修改(關鍵字noexcept)
以前,C++ 提供了一種語法,可用于指出函數可能引發哪些異常:
void test() throw(int); // throw(int, double) void test() throw();//不拋出任何異常C++ 11摒棄了異常規范,標準委員會認為,指出函數不會引發異常有一定的價值,為此添加了關鍵字noexcept:
void test() noexcept;//不拋出任何異常5.9.6 作用域內枚舉
傳統的枚舉存在一些問題,其中之一是兩個枚舉定義中的枚舉量可能發生沖突。
枚舉名的作用域為枚舉定義所屬的作用域,這意味著如果在同一個作用域內定義兩個枚舉,它們的枚舉成員不能同名。為避免這種問題,C++11 提供了一種新枚舉,其枚舉量的作用域為類:
也可以使用關鍵字 struct 代替 class,新枚舉要求進行顯示限定,以免發生名稱沖突:
egg choice = egg::Large; t_shirt Floyd = t_shirt::Large;5.9.7 對類的修改
顯示轉換運算符explicit(禁止自動轉換)
C++很早就支持對象自動轉換,但是自動類型轉換可能導致意外轉換的問題,為了解決這個問題,C++引入了關鍵字 explicit,以禁止單參數構造函數導致的自動轉換:
class Plebe{Plebe(int);explicit Plebe(double);// ... };Plebe a, b; a = 5; //會調用構造函數Plebe(int);因為它前面沒加explicit b = 0.5; // 不允許 b = Plebe(0.5); // 顯示轉換C++11 拓展了 explicit 的這種用法,使得可以對轉換函數做類似的處理:
class Plebe {operator int() const;explicit operator double() const;// ... };Plebe a, b; int n = a; //被允許,因為operator int()前面沒有explicit double x = b; // 不允許 x = double(b); // 只能顯示轉換類內成員初始化(還有一個初始化列表)
類內成員初始化是指第二行和第三行的代碼;
第六行的代碼是初始化列表,這個在上面的5.9.2 統一的初始化(初始化列表)
注意:
只能使用等號或大括號版本的初始化,但不能使用圓括號版本的初始化;
如果構造函數在成員初始化列表中提供了相應的值,這些默認值將被覆蓋。
5.9.8 ☆右值引用(&&) — 指向右值的引用
傳統的 C++ 引用(現在稱為左值引用)使得標識符關聯到左值。
左值是一個表示數據的表達式(如變量名或解除引用的指針),程序可獲取其地址,也可對其取引用。
最初,左值可出現在賦值語句的左邊,但修飾符 const 的出現使得可以聲明這樣的標識符,即不能給它賦值,但可獲取其地址,也可對其取引用:
左值: int n;//左值 int* pt = &n;//取地址-->int* int& rn = n;//取左值引用-->int&const修飾的左值: const int b = 101; //b = 100;//錯誤! 不能給const修飾的內容賦值 表達式必須是可修改的左值 const int* rr = &b;//但可以獲取它的地址 const int& rb = b; //也可以對它取引用右值: //int& rnum = 10;//10是右值,不能對右值去左值引用操作 //cout << &10 << endl;//也不能對右值取地址 const int& rc = 10;//可以 //const int* rr = &10;//報錯,不能對右值取地址C++ 11 新增了右值引用&&(rvalue reference),這種引用可指向右值(即可出現在賦值表達式右邊的值),但不能對其應用地址運算符(例如,不能對10和x + y取地址)。
右值包括字面常量(C-風格字符串除外,它表示地址)即字符串常量、10、諸如 x + y等表達式、函數的返回值(必須是返回一個值,不能是返回一個引用);
右值是個臨時值;
右值引用使用 && 聲明:
int x = 10; int y = 23; //int& rnum = 10;//不能對右值進行左值引用(&)操作 const int& rnum = 10;//可以 int&& r1 = 13;//13是右值 int&& r2 = x + y;//x+y是右值 double&& r3 = std::sqrt(2.0);//sqrt()函數的返回值是右值新增右值引用的主要目的之一是實現移動語義,讓庫設計人員能夠提供有些操作的更有效實現。
5.9.9 ☆☆☆移動語義
(視頻課從06:00開始)
看個示例:
vector<string> allcaps(const vector<string> & vs) {vector<string> temp;// 在temp中存儲vs的全大寫版本,即將參數vs傳進來的string轉換成大寫后存儲到tempreturn temp; }vector<string> vstr; // 建立一個 vector, 20000 個 string,每個 string 1000個字符 vector<string> vstr_copy1(vstr); // #1 vector<string> vstr_copy2(allcaps(vstr)); // #2解釋:
allcaps() 創建了對象 temp,該對象管理著 20,000,000 個字符;vector 和 string 的復制構造函數創建這20,000,000 個字符的副本,然后程序刪除 allcaps() 返回的臨時對象。這里做了大量的無用功。考慮到臨時對象被刪除了,如果編譯器將對數據的所有權直接轉讓給 vstr_copy2,不是更好么?也就是說,不將20,000,000 個字符復制到新地方,再刪除原來的字符,而將字符留在原來的地方,并將 vstr_copy2 與之相關聯。
這類似于在計算機中移動文件的情形:實際文件還留在原來的地方,而只修改記錄。這種方法被稱為移動語義(move semantics).
要實現移動語義,需要采取某種方式,讓編譯器知道什么時候需要復制,什么時候不需要。這就是右值引用發揮作用的地方。
可定義兩個構造函數:
- 其中一個是常規復制構造函數,它使用 const 左值引用作為參數,這個引用關聯到左值實參,如語句 #1 中的 vstr;
- 另一個是移動構造函數,它使用右值引用作為參數,該引用關聯到右值實參,如語句 #2 中 allcaps(vstr) 的返回值;
- 復制構造函數可執行深復制(深拷貝),而移動構造函數只調整記錄。
- 在將所有權轉移給新對象的過程中,移動構造函數可能修改其實參(修改指向),這意味著右值引用參數不應加const。
再看個例子:
#include <iostream> using namespace std; class demo {public:demo() : num(new int(0)) {//無參構造函數cout << "無參構造函數!" << endl;}demo(const demo& d) : num(new int(*d.num)) {//普通的構造函數(深拷貝)cout << "拷貝構造函數!" << endl;}~demo() {//析構函數delete num;cout << "析構!" << endl;} private:int* num;//成員變量是int*類型 }; //返回demo對象的函數: demo getDemo() {return demo(); } int main() {//demo a = getDemo();{demo a;}cout << "----------------------" << endl;{demo b;demo c(b);}cout << "----------------------" << endl;{demo d = getDemo();}return 0; }結果:
無參構造函數! 析構! ---------------------- 無參構造函數! 拷貝構造函數! 析構! 析構! ---------------------- 無參構造函數! 析構!視頻課中是通過g++ demo.cpp -fno-elide-constructors來看未優化的過程:
上面的兩次拷貝構造都是深拷貝,會重新在堆區開辟空間,然后將內容復制一份;可以直接把深拷貝改成淺拷貝,即不用重新在堆區開辟新的空間,而是將指向(地址)進行復制,然后把原來的指向改成nullptr,這就是移動構造函數的內容,也就是移動語義的原理。
所謂移動語義,指的就是以移動而非深拷貝的方式初始化含有指針成員的類對象。簡單的理解,移動語義指的就是將其他對象(通常是臨時對象—右值)擁有的內存資源“移為己用”,而不用做這樣的無用功:將其復制一份然后再將其刪除。
以前面程序中的 demo 類為例,該類的成員都包含一個整型的指針成員,其默認指向的是容納一個整型變量的堆空間。當使用 getDemo() 函數返回的臨時對象初始化 a 時,我們只需要將臨時對象的 num 指針直接淺拷貝給 a.num,然后修改該臨時對象中 num 指針的指向(通常讓其指向 nullptr),這樣就完成了 a.num 的初始化。
事實上,對于程序執行過程中產生的臨時對象,往往只用于傳遞數據(沒有其它的用處),并且會很快會被銷毀。因此在使用臨時對象初始化新對象時,我們可以將其包含的指針成員指向的內存資源直接移給新對象所有,無需再新拷貝一份,這大大提高了初始化的執行效率。
在demo類中加上移動構造函數(右值引用作為參數,該引用關聯到右值實參):
#include <iostream> using namespace std; class demo {public:demo() : num(new int(0)) {//無參構造函數cout << "無參構造函數!" << endl;}demo(const demo& d) : num(new int(*d.num)) {//普通的構造函數(深拷貝)cout << "拷貝構造函數!" << endl;}//添加移動構造函數:(右值引用)demo(demo&& d) :num(d.num) {//把形參d的成員num賦值給this->num,相當于淺拷貝(值傳遞)d.num = nullptr;//然后記得手動將形參d的成員num指向空,這樣就斷了指針num和它指向的內容之間的聯系,//也就不會導致重復釋放同一塊內存兩次,//相當于是對淺拷貝進行了優化,也避免了用深拷貝帶來的重復拷貝的無用功操作cout << "移動構造函數" << endl;}~demo() {//析構函數delete num;cout << "析構!" << endl;} private:int* num; }; //返回demo對象的函數: demo getDemo() {return demo(); } int main() {//demo a = getDemo();{demo a;}cout << "----------------------" << endl;{demo b;demo c(b);}cout << "----------------------" << endl;{demo d = getDemo();}return 0; }結果還跟上面一樣:
無參構造函數! 析構! ---------------------- 無參構造函數! 拷貝構造函數! 析構! 析構! ---------------------- 無參構造函數! 析構!視頻課中是通過g++ demo.cpp -fno-elide-constructors來看未優化的過程:
講義中的其他內容:
一、
當類中同時包含拷貝構造函數和移動構造函數時,如果使用臨時對象初始化當前類的對象,編譯器會優先調用移動構造函數來完成此操作。只有當類中沒有合適的移動構造函數時,編譯器才會退而求其次,調用拷貝構造函數。
二、
默認情況下,左值初始化同類對象只能通過拷貝構造函數完成,如果想調用移動構造函數,則必須使用右值進行初始化。C++11 標準中為了滿足用戶使用左值初始化同類對象時也通過移動構造函數完成的需求,新引入了std::move() 函數,它可以將左值強制轉換成對應的右值,由此便可以使用移動構造函數。
默認調用拷貝構造:
int main() {{demo a;//無參構造demo b = a;//拷貝構造//demo b = move(a);//移動構造}return 0; }結果:
無參構造函數! 拷貝構造函數! 析構! 析構!用std::move() 函數將左值強制轉換成對應的右值,就會調用移動構造:
int main() {{demo a;//無參構造//demo b = a;//拷貝構造demo b = move(a);//移動構造}return 0; }結果:
無參構造函數! 移動構造函數! 析構! 析構!三、實現完美語義
用函數std::forward()
四、總結:
移動語義其實就是加了一個移動構造函數,具體實現原理是:
將源對象(臨時對象)tmp的指針num拷貝給目的對象d 的指針num,然后讓臨時對象tmp的指針num指向nullptr,這就相當于是淺拷貝的升級;
而拷貝構造函數一般都是深拷貝,會重新在堆區開辟空間,然后將內容復制一份;可以直接把深拷貝改成淺拷貝,即不用重新在堆區開辟新的空間,而是將源指針復制給目的指針,然后把原來的指向改成nullptr,這就是移動構造函數的內容,也就是移動語義的原理。
當要拷貝的數據量特別多時,就會有很多無用功;移動語義就是對深拷貝進行了改進,相當于是先改成了淺拷貝,但比淺拷貝做的更多的一步就是讓原來的指針指向nullptr,這樣就可以斷絕原來的指針和數據的聯系了,然后delete原來的指針(此時已經是nullptr了)時就不會刪掉它指向的數據了,也就不會帶來重復釋放堆區內存的問題。
移動構造函數:
深拷貝:
move()函數
參考鏈接1:C++ move()函數
參考鏈接2:c++ 之 std::move 原理實現與用法總結
參考鏈接3:c++11之std::move函數
①std::move并不能移動任何東西,它唯一的功能是將一個左值強制轉化為右值引用,繼而可以通過右值引用使用該值,以用于移動語義。從實現上講,std::move基本等同于一個類型轉換:static_cast<T&&>(lvalue);
②std::move是將對象的狀態或者所有權從一個對象 轉移 到另一個對象,只是轉移,沒有內存的搬遷或者內存拷貝。
std::move 的函數原型定義:
template <typename T> typename remove_reference<T>::type&& move(T&& t) {return static_cast<typename remove_reference<T>::type&&>(t);}示例:
int &&rr1 = 42; //正確,字面值常量是右值 int &&rr2 = rr1; //錯誤,表達式rr1是左值 int &&rr3 = std::move(rr1); //正確move()函數告訴編譯器:我們有一個左值,但我們希望像一個右值一樣處理它。
注意:調用move意味著承諾:除了對rr1賦值和銷毀它以外,我們不再使用它。
在調用move之后,我們不能對移后源對象的值做任何假設。
(我們可以銷毀一個移后源對象,也可以賦予它新值,但是不能使用一個移后源對象的值。)
我們對move不提供using聲明,我們直接調用std::move而不是move。原因是:如果在應用程序中定義一個標準庫中已有的名字,則將出現以下兩種可能中的一種:
(1)要么根據一般的重載規則確定某次調用應該執行函數的哪個版本;
(2)要么應用程序根本就不會執行函數的標準庫版本。
因此,對于move的名字沖突相比其他標準庫函數的沖突頻繁的多。于是我們在調用move函數時,是使用std::move而不是move。
5.9.10 ☆☆☆Lambda表達式(匿名的函數)(全局函數、仿函數、Lambda表示式)
(視頻課從38:06開始)
Lambda表達式其實就是匿名的函數。
需求:生成一個隨機整數列表,并判斷其中多少個整數可被3整除,多少個整數可被13整除。
#include <iostream> #include <vector> #include <algorithm> #include <cmath> using namespace std; //全局函數: bool f3(int x) {return x % 3 == 0; } bool f13(int x) {return x % 13 == 0; } //仿函數: class f_mod { private :int dv; public:f_mod(int d = 1) : dv(d) {//這里給d一個默認值1,但會被傳進來的d覆蓋}bool operator()(int x) {return x % dv == 0;} }; int main() {vector<int> numbers(1000);generate(numbers.begin(), numbers.end(), rand);//生成1000個隨機數// 函數指針:(全局函數)int count3 = count_if(numbers.begin(), numbers.end(), f3);cout << "能被3整除的個數為:" << count3 << endl;int count13 = count_if(numbers.begin(), numbers.end(), f13);cout << "能被13整除的個數為:" << count13 << endl;// 仿函數:count3 = count_if(numbers.begin(), numbers.end(), f_mod(3));cout << "能被3整除的個數為:" << count3 << endl;count13 = count_if(numbers.begin(), numbers.end(), f_mod(13));cout << "能被13整除的個數為:" << count13 << endl;// lambda表達式:count3 = count_if(numbers.begin(), numbers.end(), [](int x) {return x % 3 == 0;});cout << "能被3整除的個數為:" << count3 << endl;count13 = count_if(numbers.begin(), numbers.end(), [](int x) {return x % 13 == 0; });cout << "能被13整除的個數為:" << count13 << endl;count3 = count13 = 0;//下面用的for_each遍歷整個numbers容器,所以要從0開始累加for_each(numbers.begin(), numbers.end(), [&](int x) {count3 += x % 3 == 0; count13 += x % 13 == 0; });cout << "能被3整除的個數為:" << count3 << endl;cout << "能被13整除的個數為:" << count13 << endl;return 0; }結果:
能被3整除的個數為:354 能被13整除的個數為:73 能被3整除的個數為:354 能被13整除的個數為:73 能被3整除的個數為:354 能被13整除的個數為:73 能被3整除的個數為:354 能被13整除的個數為:73解釋:(來自我的C++八股文中的筆記9最大的收獲:)
用到了count_if()和for_each()函數,這兩個函數的第三個參數是_pred ,
表示此參數是個函數或者函數對象,函數就是自定義的一個全局函數;函數對象就是一個類,類中實現函數調用運算符()的重載,也叫仿函數。
寫到參數列表中的時候:
- 如果寫函數,后面就沒有(),直接寫全局函數名;
- 如果寫函數對象,要寫上類名,后面再加上一個(),表示一個匿名函數對象。
距離、簡潔、效率、功能
5.9.11 ☆☆☆類型轉換運算符
(視頻課從48:00開始,到01:14:39結束)
使用C風格的類型轉換可以把想要的任何東西轉換成我們需要的類型,但是這種類型轉換太過松散。
為此,C++11提供了更加嚴格的類型轉換,可以提供更好的控制轉換過程,并添加4個類型轉換運算符,使得轉換過程更規范:
- 靜態轉換static_cast
- 動態轉換dynamic_cast
- 常量轉換const_cast
- 重解釋轉換reinterpret_cast
靜態轉換static_cast
static_cast< type-name > (expression)用于類層次結構中基類(父類)和派生類(子類)之間的指針或引用的轉換:
- 進行上行轉換(把派生類的指針或引用轉換成基類表示)是安全的;
- 進行下行轉換(把基類的指針或引用轉換成派生類表示)時,由于沒有動態類型檢查,所以是不安全的。
當用于基本數據類型之間的轉換時,如把int轉換成char,或者把char轉換成int。這種轉換的安全性也要開發人員來保證,因為有可能會損失精度。
#include<iostream> using namespace std;class Animal {}; class Dog : public Animal{}; class Other {}; int main(){//基本數據類型轉換://char-->double:char a = 'a';double b = static_cast<double>(a);cout << b << endl;//97//double-->int:(精度有下降)double d = 3.14;int i = static_cast<int>(d);cout << i << endl;//3//繼承關系(指針/引用)互轉:Animal* ani = nullptr;Dog* dog = nullptr;//把子類的指針轉換成父類的指針:Animal* ani1 = static_cast<Animal*>(dog);//把父類的指針轉換成子類的指針:Dog* dog1 = static_cast<Dog*>(ani);Animal ani0;Dog dog0;Animal& ani2 = ani0;Dog& dog2 = dog0;//把子類的引用轉換成父類的引用:Animal& ani3 = static_cast<Animal&>(dog2);//把父類的引用轉換成子類的引用:Dog& dog3 = static_cast<Dog&>(ani2);//沒有關系的指針轉換:Other* oth = nullptr;Animal* ani5 = nullptr;//Other* oth1 = static_cast<Other*>(ani5);//類型轉換無效//Animal* ani6 = static_cast<Animal*>(oth);//類型轉換無效使用C風格進行強制類型轉換:(Animal*)oth;//沒報錯//Other* oth2 = (Animal*)oth;//這個會報錯,C風格的強制類型轉換也不行return 0; }視頻課中說的:
用C風格對沒有關系的指針進行強制類型轉換不會報錯;但用靜態類型轉換static_cast就會報錯,這樣更容易發現問題在哪。
動態轉換dynamic_cast
動態類型轉換不支持基本類型轉換(double–>int),必須是指向完整的類類型的指針或者引用;
父類指針轉換成子類指針會報錯 ;(如果產生多態,則可以進行向下轉型)
前面的靜態類型轉換中父類向子類轉換不會報編譯錯誤,所以說不安全;
到了動態類型轉換,直接報編譯錯誤,這樣更安全。
示例代碼:
#include<iostream> using namespace std;class Animal {virtual void speak(){}//虛函數 }; class Dog : public Animal{void speak(){}//子類重寫虛函數 }; class Other {}; int main(){ //基本數據類型轉換://char-->double:char a = 'a';double b = dynamic_cast<double>(a);//報錯cout << b << endl;//97//double-->int:double d = 3.14;int i = dynamic_cast<int>(d);//報錯cout << i << endl;//3//繼承關系(指針/引用)互轉:Animal* ani = new Animal;Dog* dog = new Dog;//把子類的指針轉換成父類的指針:Animal* ani1 = dynamic_cast<Animal*>(dog);//把父類的指針轉換成子類的指針:Dog* dog1 = dynamic_cast<Dog*>(ani);//報錯//如果發生多態,則可以向下轉型:Animal* ani11 = new Dog;//父類指針指向子類對象:Dog* dog11 = dynamic_cast<Dog*>(ani11);//報錯Animal ani0;Dog dog0;Animal& ani2 = ani0;Dog& dog2 = dog0;//把子類的引用轉換成父類的引用:Animal& ani3 = dynamic_cast<Animal&>(dog2);//把父類的引用轉換成子類的引用:Dog& dog3 = dynamic_cast<Dog&>(ani2);//報錯//沒有關系的指針轉換:Other* oth = nullptr;Animal* ani5 = nullptr;Other* oth1 = dynamic_cast<Other*>(ani5);//報錯Animal* ani6 = dynamic_cast<Animal*>(oth);//報錯//使用C風格進行強制類型轉換:(Animal*)oth;Other* oth2 = (Animal*)oth;//C風格的強制類型轉換也不行return 0; }解釋:
對于基本數據類型轉換和沒有關系的指針轉換,動態類型轉換都是不支持的:
對于有繼承關系的指針/引用轉換:
子類指針/引用–>父類指針/引用:ok;
父類指針/引用–>子類指針/引用:不ok;但如果發生多態,就ok了。
發生多態之后:
static_cast 和 dynamic_cast 的比較
基本數據類型的轉換:static_cast行 dynamic_cast不行;
父類指針–>子類指針:static_cast不報錯,dynamic_cast會報錯
dynamic_cast中 如果發生了多態,父類指針–>子類指針的轉換就可以
| 基本數據類型的轉換 | √(不安全) | × |
| 有繼承關系的 指針/引用 互轉: | ||
| 子類指針/引用–>父類指針/引用 | √ | √ |
| 父類指針/引用–>子類指針/引用 | √(不安全) | × |
| 如果發生多態: | √ | |
| 沒有關系的指針互轉 | × | × |
常量轉換const_cast
常量轉換:
只針對指針和引用,實現const和非const之間的相互轉換;
普通變量不行(int a -->const int a 這個不行)
重解釋轉換reinterpret_cast
重新解釋轉換reinterpret_cast很不安全,一般不用
5.10-5.13 項目回顧(Linux系統編程、網絡編程、計算機網絡、操作系統)
(視頻課從01:15:00開始)
大概是7個小時的課
可以先看Linux高并發服務器開發(40h);
或者第4章 項目制作與技能提升(錄播)(26h30min);
最后再來看這7個小時的課,照著寫的筆記快速回顧一遍。
項目的詳細筆記見筆記①:牛客網—Linux高并發服務器開發。
(20200627 23:07)
總結
以上是生活随笔為你收集整理的笔记③:牛客校招冲刺集训营---C++工程师(5.9 C++新特性)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 团队项目开发流程总结
- 下一篇: Windows10企业版 VS2017编