首頁 > 軟體

C++11語法之右值參照的範例講解

2022-04-04 16:00:53

一、{}的擴充套件

在原先c++的基礎上,C++11擴充套件了很多初始化的方法。

#include<iostream>
using namespace std;
struct A
{
	int _x;
	int _y;
};
int main()
	int a[] = { 1,2,3,4,5 };
	
	int a1[] { 1,2,3,4,5 };
	int* p = new int[5]{ 1,2,3,4,5 };
	A b = { 1,2 };//初始化
	A b2[5]{ {1,1},{2,2},{3,3},{4,4},{5,5} };
	A* pb = new A{ 1,2 };
	A* pb2 = new A[5]{ {1,1},{2,2},{3,3},{4,4},{5,5} };
	return 0;
}

結果:

全部初始化正常,vs下指標後面跟數位可以表示顯示多少個。

除了上面的 new[]{}我認為是比較有意義的,很好的解決了new的物件沒有建構函式又需要同時建立多個物件的場景。

除了上面的,下面的這種方式底層實現不相同。

initializer_list的講解:

vector<int> v{1,2,3,4};

跳轉initializer_list實現

實際上上面就是通過傳參給initializer_list物件,這個物件相當於淺拷貝了外部的{1,2,3,4}的頭指標和尾指標,這樣vector的建構函式就可以通過迭代器遍歷的方式一個個的push_back到自己的容器當中。上述過程initializer_list是很高效的,因為他只涉及淺拷貝指標和一個整形。

#include <iostream>
template <class T>
class initializer_list
{
public:
    typedef T         value_type;
    typedef const T&  reference; //注意說明該物件永遠為const,不能被外部修改!
    typedef const T&  const_reference;
    typedef size_t    size_type;
    typedef const T*  iterator;  //永遠為const型別
    typedef const T*  const_iterator;
private:
    iterator    _M_array; //用於存放用{}初始化列表中的元素
    size_type   _M_len;   //元素的個數
    
    //編譯器可以呼叫private的建構函式!!!
    //建構函式,在呼叫之前,編譯會先在外部準備好一個array,同時把array的地址傳入模板
    //並儲存在_M_array中
    constexpr initializer_list(const_iterator __a, size_type __l)
    :_M_array(__a),_M_len(__l){};  //注意建構函式被放到private中!
    constexpr initializer_list() : _M_array(0), _M_len(0){} // empty list,無參建構函式
    //size()函數,用於獲取元素的個數
    constexpr size_type size() const noexcept {return _M_len;}
    //獲取第一個元素
    constexpr const_iterator begin() const noexcept {return _M_array;}
    //最後一個元素的下一個位置
    constexpr const_iterator end() const noexcept
    {
        return begin() + _M_len;
    }  
};

而{}初始化,和{}呼叫initializer_list組合起來是可以讓初始化變得方便起來的,下面的m0用了initializer_list進行初始化,但還是比較麻煩。但m1用了{}進行單個物件初始化加initializer_list的組合之後變得方便快捷起來。

#include<map>
int main()
{
	map<int, int> m0 = { pair<int,int>(1,1), pair<int,int>(2,2), pair<int,int>(3,3) };
	
	map<int, int> m1= { {1,1},{2,2},{3,3} };
	return 0;
}

小總結:

一個自定義型別呼叫{}初始化,本質是呼叫對應的建構函式;自定義型別物件可以使用{}初始化,必須要有對應的引數型別和個數;STL容器支援{}初始化,則容器必須有一個initializer_list作為引數的建構函式。

二、C++11一些小的更新

auto:
定義變數前加auto表示自動儲存,表示不用管物件的銷燬,但是預設定義的就是自動型別,所以這個關鍵字後面就不這樣用了,C++11中改成了自動推導型別。

#include<cstring>
int main()
{
	int i = 10;
	auto p = &i;
	auto pf = strcmp;

	cout << typeid(p).name() << endl;
	cout << typeid(pf).name() << endl;
	return 0;
}

結果:

int *
int (__cdecl*)(char const *,char const *)

decltype

auto只能推導型別,但推匯出來的型別不能用來定義物件,decltype解決了這點,推導型別後可以用來定義物件。
decltype(表示式,變數),不能放型別!

#include<cstring>
int main()
{
	int i = 10;
	auto p = &i;
	auto pf = strcmp;

	decltype(p) pi;//int*
	pi = &i;
	cout << *pi << endl;//10
	return 0;
}

nullptr

NULL在C中是0,是int型別。C++11新增nullptr表示((void*)0),避免匹配錯引數。

範圍for

支援迭代器就支援範圍for

新容器

array,沒啥用,靜態的陣列,不支援push_back,支援方括號,裡面有assert斷言防止越界,保證了安全。

foward_list,沒啥用,單連結串列,只能在節點的後面插入。

unordered_map,很有用,後面講

unordered_set,很有用,後面講

三、右值參照

左值
作指示一個資料表示式(變數名或解除參照的指標)。
左值可以在賦值符號左右邊,右值不能出現在賦值符號的左邊。

const修飾符後的左值,不能給他賦值,但是可以取他的地址。左值參照就是給左值的參照,給左值取別名。

左值都是可以獲取地址,基本都可以可以賦值
但const修飾的左值,只能獲取地址,不能賦值。

右值?
右值也是一個資料的表示式,如字面常數,表示式返回值,傳值返回的函數的返回值(不能是左值參照返回)。右值不能取地址,不能出現在賦值符號的左邊。

關鍵看能不能取地址

給右值取別名就用右值參照,右值參照是左值了,放在賦值符號的左邊了。

右值不能取地址,但是給右值參照後,參照的變數是可以取地址的,並且可以修改!
右值參照存放的地方在棧的附近。

int main()
{
	int&& rra = 10;
	//不想被修改 const int&& rra
	cout << &rra << endl;
	rra = 100;
	return 0;
}

左值參照總結:

  • 左值參照只能參照左值,不能參照右值。
  • 但是const修飾的左值參照既可以參照左值,又可以參照右值。在沒有右值參照的時候就必須採用這種方式了。
  • 左值最重要的特徵是都可以取地址,即使自定義型別也有預設的取地址過載。

右值參照總結:
右值參照只能參照右值,不能參照左值。
右值參照可以參照move以後的左值。

左值參照可以接著參照左值參照,右值參照不可以。
原因:右值參照放到左邊表示他已經是一個左值了,右值參照不能參照左值!

int main()
{
	int a = 10;
	int& ra = a;
	int& rb = ra;
	int&& rra = 10;
	int&& rrb = rra;//err:無法從「int」轉換為「int && "
	return 0;
}

匹配問題:

void func(const int& a)
{
	cout << "void func(const int& a)" << endl;
}
void func(int&& a)
	cout << "void func(int&& a)" << endl;
int main()
	int a = 10;
	func(10);
	func(a);
	return 0;

右值在有右值參照會去匹配右值參照版本!

右值真正的用法

本質上參照都是為了減少拷貝,提高效率。而左值參照解決了大部分的場景,但是左值參照在傳值返回的時候比較吃力,由右值參照來間接解決。

左值參照在參照傳參可以減少拷貝構造,但是返回值的時候避免不了要呼叫拷貝構造。

傳參用左值拷貝和右值拷貝都一樣,但是返回值如果用右值參照效率會高,並且通常左值參照面臨著物件出了作用域銷燬的問題。所以這就是右值參照的一個比較厲害的用法。

返回物件若出了作用域不存在,則用左值參照返回和右值參照返回都是錯誤的。

std::move是將物件的狀態或者所有權從一個物件轉移到另一個物件,只是轉移,沒有記憶體的搬遷或者記憶體拷貝,所以可以提高利用效率,改善效能。所以當作函數返回值的時候如果物件不存在左值參照和右值參照都會報錯!

場景:返回的物件在區域性域中棧上存在,返回該物件必須用傳值返回,並且有返回物件接受,這個時候編譯器優化,將兩次拷貝構造優化成一次拷貝構造。

測試用的string類

#include<assert.h>
namespace ljh
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		// 拷貝構造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷貝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		// 賦值過載
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷貝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}
		// 移動構造
		/*string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移動語意" << endl;
			swap(s);
		}*/
		// 移動賦值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移動語意" << endl;
			swap(s);
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			++_size;
			_str[_size] = '';
		}
		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最後做標識的
	};
}

臨時變數如果是4/8位元組,通常在暫存器當中,但是如果是較大的記憶體,會在呼叫方的函數棧幀中開闢一塊空間用於接受,這就是臨時物件。

臨時物件存在的必要性
當我們不需要接受返回值,而是對返回的物件進行直接使用,這個時候被呼叫的函數中的物件出了函數棧幀就銷燬了,所以在棧幀銷燬前會將物件拷貝到呼叫方棧幀的一塊空間當中,我們可以用函數名對這個臨時物件直接進行操作的(通常不能修改這個記憶體空間,臨時變數具有常性)。

分析下面幾組圖片程式碼的效率

不可避免的,下面的這個過程必然要呼叫兩次拷貝構造,編譯器對於連續拷貝構造優化成不生成臨時物件,由func::ss直接拷貝給main的str,我們如果只有前面所學的左值參照,func中的string ss在出了func後銷燬,這個時候參照的空間被銷燬會出現問題,這個時候顯得特別無力。
在連續的構造+拷貝構造會被編譯器進行優化,這個優化要看平臺,但大部分平臺都會做這個處理。

結果:

即使下面這種情況,在main接受沒有參照的情況下,依舊會呼叫一次拷貝構造,跟上面並沒有起到一個優化的作用。

結果:

解決方案:新增移動構造,新增一個右值參照版本的建構函式,建構函式內部講s物件(將亡值)的內容直接跟要構造的物件交換,效率很高!!

string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移動語意" << endl;swap(s);}

有了移動構造,對於上面的案例就變成了一次拷貝構造加一次移動構造。**編譯器優化後將ss判斷為將亡值,直接移動構造str物件,不產生臨時物件了,就只是一次移動構造,效率升高!!**同理移動賦值!

結果:

下面情況是參照+移動構造,但是編譯器優化就會判斷ss出了作用域還存在,反而會拿ss拷貝構造str,這個時候起不到優化的作用!

結果:

以上採用將物件開闢在堆上或靜態區都能夠採用參照返回解決問題,但是有一個壞處?
引入多執行緒的概念,每個執行緒執行的函數當中若有大量的堆上的資料或者靜態區的資料,相當於臨界資源變多,要注意存取臨界資源要加鎖。而每個棧區私有棧,會相對好些

右值:
1、內建型別表示式的右值,純右值。
2、自定義型別表示式的右值,將亡值。

將亡值:

string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移動語意" << endl;
			swap(s);
		}
int main()
{
	ljh::string& str = func2();
	vector<ljh::string> v;
	v.push_back("1234656");//傳進去的就是右值,是用"1234656"構造一個string物件傳入,就是典型的將亡值
}

移動構造:
將亡值在出了生命週期就要銷燬了,構造的時候可以將資源轉移過要構造的物件,讓將亡的物件指向NULL,相當於替它掌管資源。移動構造不能延續物件的生命週期,而是轉移資源。且移動構造編譯器不優化本質是一次拷貝構造+一次移動構造(從將亡值(此時返回值還是一個左值)給到臨時變數),再有臨時變數給到返回值接受物件(移動構造);
編譯器優化做第一次優化,會將將亡值當作右值,此時要進行兩次移動構造,編譯器第二次優化,直接進行一次移動構造,去掉生成臨時物件的環節。
只有需要深拷貝的場景,移動構造才有意義,跟拷貝構造一樣,淺拷貝意義不大。

move的真正意義:
表示別人可以將這個資源進行轉移走。

int main()
{
//為了防止這種情況,也要新增移動賦值。
	ljh::string str1;
	str1 = "123456";
}

c++11的演演算法swap的效率跟容器提供的swap效率一樣了。

vector提供的插入的右值參照版本,就是優化了傳右值情況,如果C++98則需要拷貝放入,而有右值就可以直接移動構造。兩個介面的效率差不多。
大多數容器的插入介面都做了右值參照版本!!

完美轉發

模板函數或者模板類用的&&即萬能參照。
模板中的&&不代表右值參照,而是萬能參照,其既能接收左值又能接收右值。模板的萬能參照只是提供了能夠接收同時接收左值參照和右值參照的能力。而forward才能將這種右值特性保持下去。

但是參照型別的唯一作用就是限制了接收的型別,後續使用中都退化成了左值,

此時右值在萬能參照成為左值,可能會造成本身右值可以移動構造,卻變成左值只能拷貝構造了。
Fun(std::forward<T>(t));才能夠保證轉發的時候值的特性

void Fun(int &x){ cout << "左值參照" << endl; }
void Fun(const int &x){ cout << "const 左值參照" << endl; }
void Fun(int &&x){ cout << "右值參照" << endl; }
void Fun(const int &&x){ cout << "const 右值參照" << endl; }
void Func(int x) {
 // ......
}
template<typename T>
void PerfectForward(T&& t) {
 Fun(t);
}
int main()
{
 PerfectForward(10);           // 右值
 int a;
 PerfectForward(a);            // 左值
 PerfectForward(std::move(a)); // 右值
 const int b = 8;
 PerfectForward(b);      // const 左值
 PerfectForward(std::move(b)); // const 右值
 return 0; }

預設成員函數

C++11 新增了兩個:移動建構函式和移動賦值運運算元過載。

現在有8個:建構函式,解構函式,拷貝構造,拷貝賦值,取地址,const取地址移動構造,移動賦值。

移動建構函式的預設生成的要求比較嚴格:
如果你沒有自己實現移動建構函式,且沒有實現解構函式 、拷貝構造、拷貝賦值過載都沒有實現。那麼編譯器會自動生成一個預設移動構造。預設生成的移動建構函式,對於內建型別
成員會執行逐成員按位元組拷貝,自定義型別成員,則需要看這個成員是否實現移動構造,如
實現了移動構造就呼叫移動構造沒有實現就呼叫拷貝構造
如果你提供了移動構造或者移動賦值,編譯器不會自動提供拷貝構造和拷貝賦值。
同理移動賦值。

即對於深拷貝的類,最好所有的構造,解構,拷貝,賦值過載,移動拷貝,移動賦值都寫上。

總結

左值參照通常在傳參和傳返回值的過程中減少拷貝,這是利用左值參照的語法特性。一般做不到的部分,通常選擇傳參的時候傳參照也可以解決,不通過返回值接受。
右值參照,一般是在深拷貝的類,實現移動構造和移動賦值,能夠解決左值參照無法做到的傳返回值的效率問題。

到此這篇關於C++11語法之右值參照的文章就介紹到這了,更多相關C++11右值參照內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com