來自面向對象思考的個人blog:https://blog.csdn.net/craftsman1970/article/details/79717944
搞了挺長時間rust,C++的語法知識忘得差不多了,是時候撿一撿了。
Table of Contents
4:const, const expression和constexpr
50:正則表達式庫(regular-expression library)
53:有作用域的enum(scoped enumeration)
56:標準庫mem_fn類模板和std::function()
1:longlong
數據類型long long是在C++11中重新定義的,標準規定它最小是64bit
在這之前爲了提供超過32bit的整數,各個開發環境(編譯器)分別定義了各自的64bit整數類型。結果當然就是影響了代碼地兼容性。
現在好了。C++11直接定義了long long類型。
我猜許多人應該使用過這個類型,當然在C++11之前,這種嘗試會被編譯器無情拒絕,自C++11之後就不會在發生這樣地情況了。因此我認爲:在C++11新特性中,long long一定是最容易被接受的一個。多數程序員看到它時甚至不會意識到這是一個新特性。
相應地,C++11規定:在指定long long字面值類型時,使用ll或LL。這也可以從long的l或L推斷出來。
2:列表初始化
C++11中擴展了使用花括號初始化變量的應用範圍,稱這種初始化方式爲列表初始化。
例如:
可以像下面這樣初始化vector:
vector<int> int_vector = {5, 4, 3, 2, 1};
可以像下面這樣初始化list:
list<int> int_list = {5, 4, 3, 2, 1};
甚至可以像下面這樣初始化map
map<int, const char*> id2Name = {{1,"Zhang"},{2, "Wang"},{3, "Li"}};
另一種形式
下面和寫法也合法,和上面的幾種寫法等價。
vector<int> int_vector{5, 4, 3, 2, 1};
list<int> int_list {5, 4, 3, 2, 1};
map<int, const char*> id2Name{{1,"Zhang"},{2, "Wang"},{3, "Li"}};
3:空指針(nullptr)
假設我們有下面的代碼:
void output(int value){
std::cout << "value=" << value << std::endl;
}
void output(char* msg){
if(msg != 0){
std::cout << "msg=" << msg << std::endl;
}else{
std::cout << "msg=NULL" << std::endl;
}
}
這是兩個重載的函數。如果我們希望調用output(char*)函數,下面的那種方式都不行:
output(0);
output(NULL);
原因很簡單,0是整數,NULL實際上也是整數0。當然也不是沒有辦法,代碼可以改成這樣:
outpu((char*)0);
output((char*)NULL);
其實我們需要的就是一個字面值來表示空指針,而不是借用數字0。C++11中定義了這個字面值:nullptr。有個它,再調用output函數是就清晰多了。
output(0); //int參數版本
output(nullptr); //char* 參數版本。
當然,下面的代碼也會發生編譯錯誤。
int i = nullptr;
4:const, const expression和constexpr
C++11允許將變量聲明爲constexpr類型以便由編譯器驗證變量的值是否是一個常量表達式。
變量聲明爲constexpr類型,就意味着一方面變量本身是常量,也意味着它必須用常量表達式來初始化。
constexpr int mf = 20;
constexpr int limit = mf + 1;
如果初始值不是常量表達式,那麼就會發生編譯錯誤。
constexpr函數
除了能用常量表達式初始化constexpr變量以外,還可以使用constexpr函數。它是指能用於常量表達式的函數,也就是說它的計算結果可以在編譯 時確定。定義的方法就是在返回值類型前加constexpr關鍵字。但是爲了保證計算結果可以在編譯是確定,必須滿足以下條件:
返回值和形參必須都是字面值類型。
函數中只能有一條return語句。
例如下面的計算階乘的constexpr函數。
constexpr long long factorial(int n){
return n <= 1? 1 : (n * factorial(n - 1));
}
constexpr long long f18 = factorial(20);
可以用它來初始化constexpr變量。所有計算都在編譯時完成。比較有趣的是像溢出這樣錯誤也會在編譯是被檢出。
簡單地說
constexpr可以
加強初始值的檢查
計算的時機從運行時提前到編譯時,比宏定義效率更高。
5:類型別名
假設有一個二維圖形計算的程序,定義了一個point結構體。
struct point
{
int x;
int y;
};
在有些系統中,int類型的精度,範圍都足夠,在其他的系統中可能就不能滿足需求,可能需要擴大字長,或者需要提高精度等等。
方法有多種,其中之一就是定義別名。在C++11中定義別名的方法如下:
using dtype = int;
它的含義是爲int指定一個別名,dtype。指定別名以後,point結構體變成下面這樣:
struct point
{
dtype x;
dtype y;
};
這樣一來,只要改變dtype所對應的數據類型,所有使用point的代碼都會適應這種變化。
類型別名和typedef有什麼區別?
typedef也能提相同的功能,但是形式略有不同。
typedef int dtype; //等價於using dtype = int;
typedef vector<point> PointVector; //等價於using PointVector = vector<point>
typedef void(*PtoF)(int); //等價於using PtoF=void(*)(int);
C++11的別名定義方式似乎更容易理解一些。除此以外區別似乎不大。
6:auto類型修飾符
看下面的代碼:
vector<point> v = {{1, 2}, {3, 4}};
vector<point>::iterator it = v.begin();
while(it != v.end()){
cout << (*it).x << "," << (*it).y << endl;
it++;
}
如果實際編過這樣的程序,就一定有過這樣的經驗:不知道如何定義it的類型。C++11中可以使用auto,就像下面這樣:
vector<point> v = {{1, 2}, {3, 4}};
auto it = v.begin();
while(it != v.end()){
cout << (*it).x << "," << (*it).y << endl;
it++;
}
7:decltype修飾符
在存在初始化代碼的情況下,可以使用auto來自動決定變量的類型。還存在另外一種情況,我們希望變量的類型通過初始化代碼以外的表達式推斷得到。
假設有下面的結構體:
struct Point{
int x;
int y;
};
在其他地方,可能這樣定義point類型的變量:
Point point;
同樣我們也可以定義指向point的指針:
Point* p1 = nullptr;
在C++11中提供了另一種方式來決定變量的類型:decltype修飾符。利用它可以通過表達式的類型來決定變量的類型:
decltype(point)* p2 = nullptr;
這兩種方式有什麼不同呢?當point的類型發生變化時,p1的類型需要一起修改,p2的類型就不需要修改。
8:類內初始化
C++11中引入了類內初始化器,以減少構造函數和初始化代碼的數量。說起來挺玄,其實就是下面代碼中背景加亮的部分。
class Rect
{
public:
Rect(int l, int t, int r, int b)
:left{l}, top{t}, right{r}, bottom{b}
{
}
Rect(int l, int t, int r, int b, LineStyle ls)
:left{l}, top{t}, right{r}, bottom{b}
,style{ls}
{
}
private:
int top{0};
int left{0};
int right{0};
int bottom{0};
LineStyle style{lsSolid};
};
類內初始化之後,構造函數只需要負責和缺省值不同的部分就好,代碼精煉很多了。
9:範圍for語句
C++11引入了範圍for語句:
int array[]{1, 2, 3, 4};
int sum = 0;
for(int a : array){
sum += a;
}
vector<int> vect{1, 2, 3, 4};
int sum = 0;
for(int v: vect){
sum += v;
}
for(int v : vect)讀作“對於vect中的每一個v”。應該說,程序簡練了不少。
運用條件
是不是所有地數據集合可以交給範圍for遍歷呢?答案是否定的。
數據v被範圍for遍歷的條件是,該數據支持v.begin()/v.end()或者是begin(v)/end(v)並返回一個迭代器。STL中的容器都滿足上述條件。對於內置類型的數組來講C++編譯器提供了等同於上述接口的機制,因此也可以在範圍for中使用。
10:容器的cbegin和cend函數
vector本身是const類型,生成的迭代器就必須是const類型。這樣,在編譯層次就避免了可能發生的對vector數據的修改。
還有另外一種情況,數據本身不是const類型,但是從設計的角度來講有些處理不應該修改該數據。這時也應該要求const類型的迭代器,以避免數據被意外修改。
C++11爲此提供了cbegin和cend方法。
vector<int> v{1, 2, 3, 4, 5, 6};、
auto ait = v.cbegin();
while(ait != v.cend()){
sum += *ait;
*ait = sum; //編譯錯誤
ait++;
}
cbegin()/cend()決定了返回的迭代器類型爲const。這時即使vector的類型不是const,也可以防止對該數據的誤操作。
11:標準庫函數begin和end
C++11引入了begin和end函數。使用begin和end的代碼如下:
for(int* p1 = begin(a1); p1 != end(a1); ++p1){
cout << *p1 << endl;
}
12:initializer_list形參
C++11中的可變參數
C++11在標準庫中提供了initializer_list類,用於處理參數數量可變但類型相同的情況。使用initializer_list最常用的方式是通過大括號包圍的值列表對其進行初始化:
initializer_list<int> vlist{9, 8, 7, 6};
除了不能修改vlist中的值以外,可以像一般的list一樣使用。
繼續看下面的函數:
template<typename T>
void output(initializer_list<T> lst)
{
for(auto &a : lst){
cout << a << endl;
}
}
這個函數很簡單,就是輸出list中的內容,它有幾個特點:
通過模版,auto的使用,是它可以自動適應參數的類型
通過initializer_list的使用,自動適應參數的個數。
函數弄好以後,怎麼使用就可以看心情了。
initializer_list<int> vlist{9, 8, 7, 6};
output(vlist);
output({1, 3, 4, 5});
output({"How", "are", "you", "!"});
13:返回類型後置
除了構造函數和析構函數以外,函數聲明都需要明確函數的返回類型,在傳統的C或者C++中,函數聲明大致是這個樣子:
int getSum(int a, int b);
第一個int就是函數的返回類型,它表明函數的返回值類型爲整數。在新的C++11以後,我們也可以這樣聲明:
auto getSum(int a, int b)->int;
在原來放返回值類型的位置寫auto,在函數聲明結束以後接一個'->'再跟着寫函數的返回值類型。兩種方式的效果是一樣的。
14:使用=default生成默認構造函數
先看下面代碼:
struct Point{
int x;
int y;
};
一旦因爲其他原因添加了其他有參數的構造函數,編譯器就不再生成缺省的構造函數了。
C++11允許我們使用=default來要求編譯器生成一個默認構造函數:
struct Point{
Point()=default;
Point(int _x, int _y):x(_x),y(_y){}
int x = 0;
int y = 0;
};
如果是自己編寫的無參構造函數的話,就需要指定成員的構造方式。默認構造函數會對數據成員進行默認初始化,所以就不需要另外指定了。這樣可以省去一些麻煩。
15:array容器
C++11中引入了array容器,基本上解決了內置數組的問題:
std::array<int, 5> c11ary;
c11ary.fill(0);
unsigned int i = 0;
while(i<c11ary.size()){
c11ary.at(i) = i;
i++;
}
這段代碼中,
使用fill方法實現了數據填充。
使用size方法取得數組的大小。
雖然at(i)方法實現帶有越界檢查的讀寫。
16:右值引用
代碼如下
void merge(Image&& img){
//接管img中的數據。
img.height = 0;
img.width = 0;
img.data = nullptr;
}
我們將參數聲明爲右值引用,要求像一個臨時變量一樣任性地使用數據。使用這個函數的方法如下:
Image img1(100, 100);
Image img2(100, 200);
img1.merge(std::move(img2));
注意代碼中的std::move,這是標準庫中提供的方法,它可以將左值顯式轉換爲右值引用類型,從而告訴編譯器,可以像右值(臨時變量)一樣處理它。同時也意味着接下來除了對img2賦值或銷燬以外,不再使用它。
C++11通過使用右值引用提供了一種接管數據的標準方法。
17:更快的swap
C++11之前的swap
先看swap的實現:
template<classT>voidswap ( T& a, T& b )
{
T c(a); a=b; b=c;
}
下面結合示例下面的代碼看看發生了什麼。
當swap調用了T C(a)的時候,實際上是調用了拷貝構造函數,當swap代碼調用了賦值操作時,實際上是調用了賦值運算符。
由於拷貝構造函數和賦值運算符包含內存拷貝操作,而這樣的操作共執行了三次,所以在一個swap中一共存在三次內存拷貝的操作。這種不必要的內存操作很多情況下都會影響C++的執行效率。
C++11之後的swap
引入了右值引用和數據移動的概念之後,代碼變成下面的樣子:
template<classT>voidswap (T& a, T& b)
{
T c(std::move(a)); a=std::move(b); b=std::move(c);
}
由於std::move將變量類型轉換爲右值引用,TestData有機會提供下面針對右值引用的構造函數和賦值運算符。
TestData(TestData&& d)
:size(d.size)
,data(d.data)
{
d.size = 0;
d.data = nullptr;
}
TestData& operator=(const TestData&& d)
{
size = d.size;
data = d.data;
return *this;
}
由於代碼中使用內存移管代替了不必要的內存拷貝,因此效率會大大提高。
如果觀察C++11的標準庫,會發現很多類都增加了右值引用的參數,這實際上就是對數據移動的支持,也就是對高效率的支持。
18:容器的insert成員
填充多個元素
iterator insert (const_iterator position,
size_type n,
const value_type& val);
這個方法可以在指定位置填充n個val。除了參數以外,方法的返回值從void變爲iterator,返回最後一個添加的元素的位置。有了這個返回值,在同一個位置填充元素就會很方便。例如下面的代碼:
std::list<int> demo{1, 2, 3, 4, 5, 6};
auto position = std::find(demo.begin(), demo.end(), 3);
for(int i = 0; i < 5; ++i){
position = demo.insert(position, 2 , i);
}
for (int var: demo) {
std::cout << var << ",";
}
std::cout << endl;
在3的前面連續添加0,1,2,3,4。代碼輸出如下:
1,2,4,4,3,3,2,2,1,1,0,0,3,4,5,6,
以移動方式插入數據
iterator insert (const_iterator position,
value_type&& val);
這個方法是C++11中追加的新方法,提供了對數據移動的支持。實例代碼如下:
std::list<string> strlist{"are", "you"};
std::string str("How");
strlist.insert(strlist.begin(), std::move(str));
for (auto var: strlist) {
std::cout << var << ",";
}
std::cout << endl;
std::cout << "str=" << str << endl;
輸出結果爲:
How,are,you,
str=
支持initializer_list
這個方法也是C++11中新追加的,提供對initializer_list的支持。示例代碼如下:
strlist.insert(strlist.begin(), {"C++", "11"});
for (auto var: strlist) {
std::cout << var << ",";
}
std::cout << endl;
在前面示例的基礎上添加再次在list的開頭插和“C++”和“11”兩個字符串。執行結果如下:
C++,11,How,are,you,
19:容器的emplace成員
考慮下面的Rect類:
struct Rect
{
Rect(int l, int t, int r, int b)
:left{l}, top{t}
,right{r}, bottom{b}
{}
int left;
int top;
int right;
int bottom;
};
當需要向容器添加Rect對象時,代碼大致是這樣的:
std::list<Rect> rlist;
rlist.push_front(Rect(10, 10, 20, 20));
在調用push_front時,首先構造一個臨時的Rect對象傳遞給push_front方法,然後在push_front的內部,在複製一個Rect對象添加到容器中。全過程會發生一次創建動作和一次拷貝動作,才能將對象的內容添加到list當中去。
爲了減少拷貝動作的次數,當然可以使用右值引用參數的成員函數。除此之外,C++11還提供了另一種方法:emplace成員。使用這個成員可以直接傳遞用於生成對象的參數,對象的創建過程交給容器去執行:
std::list<Rect> rlist;
rlist.emplace_front(10, 10, 20, 20);
用法非常簡單,只要保證參數和元素構造函數的參數相同即可。
除了emplace_front以外,C++11還提供了emplace和emplace_back方法,分別對應insert和push_back方法。
20:管理容器的容量
capacity和size
理解capacity和size的區別非常重要,容器的size是指已經保存在容器中的數據的個數,而容量是指在不再重新分配內存的前提下容器最大可以包含的數據的個數。舉個例子:容量爲2升的瓶子裝了1升水。2升是capacity,1升是size。
管理容器的容量
在絕大多數情況下,程序員不必關注容器類內存管理的細節,把這些工作完全交給C++標準庫。但是有時也會有例外:
要求操作的響應非常快,快到不能忽略從堆中申請內存的時間。
使用的空間非常大,大到不希望容器保持多餘的內存空間。
這時就需要主動干預內存的取得和釋放動作。C++標準庫爲此提供了相應的成員函數。
capacity:取得容器的容量
size:取得已經保存在容器中數據的個數。
reserve:分配至少可以容納指定數量元素的內存空間。
shrink_to_fit:釋放多餘的內存空間,只保留可以容納容器中數據的最小內存。
21:string的數值轉換函數
string是標準庫中最常用的類,說活躍在字符串處理的各種場景中。但是長期以來string和數值之間的轉換一直比較繁瑣。這種情況到C++11以後有了很大的改觀,因爲標準庫中爲string和數值的相互轉換提供了一套函數。
數值到string
函 數非常簡單明快,只需要一個函數:to_string。但是實際上它是一組重載的函數,分別對應從int,long,long long,unsigned int,unsigned long,unsigned long long,float,double,long double到string的轉換。
使用起來也非常簡單:
string value = to_string(2.5);
string到數值
針對基本的數值類型,C++11提供了相應的轉換方法。
stoi:string 到 int
stol: string 到 long
stoll:string 到 long long
stoul:sting 到 unsigned long
stoull:string 到 unsigned long long.
stof:string 到 float
stod:string 到 double
stold:string 到 long double.
以下是示例代碼:
string value = to_string(2.5);
int iv = stoi(value);
cout << "iv=" << iv << endl;
double dv = stod(value);
cout << "dv=" << dv << endl;
內容爲"2.5"的字符串,轉換爲整數以後結果是2,轉換爲double以後的結果是2.5。
string values("1.0,2.4,3.5,4.6,5.7");
while(values.size()>0){
string::size_type sz;
cout << stod(values, &sz) << endl;
if(sz < values.size()){
values = values.substr(sz + 1);
}
else{
values.clear();
}
}
22:lambda表達式
可以認爲lambda表達式取得信息有兩種方式,或者說兩個時機:一個是參數列表,其內容是在表達式被調用時決定;另一個捕獲列表,其內容是在是表達式被創建的時候決定,本文討論捕獲列表。
值捕獲
先看如下代碼:
int factor = 2;
auto multiply = [factor](int value)
{return factor * value;};
factor = 4;
cout << multiply(2) << endl;
代碼中首先爲factor賦值2,創建lambda表達式以後,再次賦值4。由於lambda表達式的捕獲是在該表達式創建是進行的,而第二次賦值在lambda表達式創建之後,所以muliply(2)的執行結果爲4。
引用捕獲
還是這段代碼,只要在捕獲列表中變量的前面多了一個&,就變成了引用捕獲。
int factor = 2;
auto multiply = [&factor](int value)
{return factor * value;};
factor = 4;
cout << multiply(2) << endl;
捕獲的時機並沒有變化,只是捕獲的是factor的引用而不是factor的值,所以定義lambda之後,對factor再次賦值4依然會影響multiply(2)的結果。此時的輸出爲8。
隱式捕獲
前面例子中使用捕獲列表時,具體指定了變量名,屬於顯式捕獲。另外還有隱式捕獲,由lambda表達式推斷需要捕獲的變量。具體方法是:
當需要隱式值捕獲時,使用[=];
當需要隱式引用捕獲時,使用[&];
在上面例子中使用隱式捕獲以後,結果不會發生變化。
可變lambda
假設有如下vector,保存的內容是學生的考試成績:
vector<int> score{45, 70, 56, 86, 28, 60, 90};
可以用以下代碼來尋找第一個及格成績:
find_if(score.begin(), score.end(),
[](int v){return (v >=60);});
如果需要找到第n個及格成績,很自然地會考慮使用下面的代碼:
int counter = 2;
find_if(score.begin(), score.end(),
[counter](int v){
return (v >=60)&&(--counter == 0);
});
但 是很遺憾,這時會出現編譯錯誤,告訴你counter是隻讀的。其原因是因爲在lambda表達式中很少有需要修改捕獲值的場景,因此默認捕獲值具有 const屬性。如果出現本例這樣,確實希望修改捕獲值的情況,C++11使用mutable關鍵字來解決這個問題。來看完整代碼:
int counter = 2;
auto iter find_if(score.begin(), score.end(),
[counter](int v)mutable{
return (v >=60)&&(--counter == 0);
});
cout << *iter << endl;
當然了,由於是值捕獲,處於lambda表達式外面的counter值依然不會改變。
如果希望連外面的counter一起修改,使用引用捕獲即可。
23:參數綁定
lambda表達式也有缺點:在類似功能多次使用的時候,每次定義lambada表達式也會比較麻煩。本文介紹另一種方式:參數綁定。
標準庫bind函數
繼續用lambda表達式中用過的例子,如果希望找到第一個長度小於2的string,可以使用以下代碼:
bool istarget(const string& s){
return s.size() < 2;
}
vector<string> v{"This","is", "a", "predicate", "."};
auto found = find_if(v.begin(), v.end(), istarget);
cout << *found << endl;
如果我們希望在istarget中選擇string時使用變量而不是固定的2的時候,一般的函數就不能滿足需求了(雖然使用全局變量算是一個選項)。除了和函數對象和lambda表達式以外,還可以使用標準庫bind函數來實現,其步驟如下:
根據需求定義比較函數
在本例中,就是定義一個接受選擇對象string對象和最小長度參數的istarget函數:
bool istarget(const string& s, int sz){
return s.size() < sz;
}
使用參數綁定定義新的可調用對象
C++11標準庫提供了一個bind函數,按照C++ Primer的說法,可以將bind函數看作一個通用的函數適配器,它接受一個可調用對象,生成一個新的可調用對象來“適應”原對象的參數列表。調應bind的一般形式爲:
auto newCallable = bind(callable, arg_list);
具體到本例,可以這樣定義
auto isTarget = bind(istarget, _1, 2);
istarget:bind適配的對象,就是第一步中定義具有兩個參數的istarget函數
接下來是傳遞給istarget的參數。參數的順序和istarget參數列表中規定的一致。
_1:佔位符,_1代表isTarget被調用時的接受的第一個實參,這個_1處在bind參數列表的第一個位置表明isTarget的第一個實參會在調用istarget時作爲istarget的第一個實參使用。
2:比較長度信息,形式和佔位符不同,處在參數列表的第二個位置,這個值會在調用istarget時作爲istarget的第二個實參使用。
istarget函數定義一次之後,可以使用bind函數適應各種算法的要求,從而實現了實現一次定義,多次使用的目標。
#include "../include/hello.h"
using namespace std;
int main(){
vector<int> v{11,22,33,44,55,66};
unsigned int count =2;
auto fun1 = [&count](int a, int b)mutable{
cout<<b<<endl;
return (a>=44)&&(--count == 0);};
auto fun_new = std::bind(fun1,std::placeholders::_1,888);
auto iter = find_if(v.begin(),v.end(),fun_new);
cout<<*iter<<endl;
cout<<count<<endl;
return 0;
}
24:無序關聯容器
C++11另外引入了4種無序關聯容器(unordered associative container)。這些容器將存儲組織爲一組桶,根據哈希值將數據映射到桶。與有序關聯容器類似,無序關聯容器也可以用同樣的標準分類:
除了哈希管理操作以外,無序容器還提供了與有序容器相同的操作。也就是說有序容器和無序容器可以互換
25:智能指針shared_ptr
基本原理
shared_ptr在內部維護一個相當於引用計數的機制,允許多個指針同時指向一個對象。某個指針被銷燬之後,引用計數同時較少,當所有指針都被銷燬之後,自動釋放管理的對象。
聲明和初始化
shared_ptr是一個模版類,在聲明時必須聲明指向對象的類型:
上面的代碼只是定義了一個空的shared_ptr,如果需要同時初始化,最安全的方式是使用make_shared標準庫函數:
之所以說這種方式安全,我想是因爲make_shared雖然也生成了MyString對象,但是這個對象直接裝配到shared_ptr上,利用側根本摸不着。
賦值
我們也可以把一個shared_ptr的值賦值給另一個shared_ptr:
使用shared_ptr
可以像普通指針一樣使用shared_ptr:
代碼全貌
輸出結果:
shared_prt的本身是一個類,所以它的初始化實際上就是調用shared_ptr類的構造函數。通過分析shared_ptr的構造函數,就可以準確把握shared_ptr初始化的方法。
https://blog.csdn.net/craftsman1970/article/details/80765625
修飾符說明
explicit:保證該構造函數不會被隱式調用
noexcept:該函數不會拋出異常,
constexpr:該函數可以在編譯期間求值
26:智能指針unique_ptr
可以自動管理內存,只是這塊內存不和其他的unique_ptr分享
由於unique_ptr對於內存的獨佔特性,unique_ptr不支持直接的賦值操作,而只能支持右值引用的賦值,基本形式如下:
必須是先前的持有者明確放棄權利之後,才能賦值給新的持有者。實際的程序中,上面的代碼並沒有太大的意義,真正常見的應該是下面的代碼:
getvalue函數返回的是一個右值,所以也會執行右值引用賦值
從函數返回unique_ptr的時候涉及到一個例外:即將銷燬的unique_ptr可以被拷貝或賦值。
27:智能指針weak_ptr
weak_ptr看起來更像shared_ptr的附屬品,它從shared_ptr衍生,但不會控制所指向對象的生命週期。weak_ptr的弱就弱在這裏。
https://blog.csdn.net/craftsman1970/article/details/80834199
在使用上,C++11標準庫沒有提供通過weak_ptr直接訪問對象的方法,而是調用weak_ptr的lock方法生成一個shared_ptr,再通過shared_ptr訪問對象。
輸出結果爲 127.
使用shared_ptr以後,代碼不再需要顯式釋放申請的內存,使內存的管理更加簡單。
使用weak_ptr之後,可以通過lock方法來確認對象是否有效,使得內存的相互參照的管理更加容易。
28:將=default用於拷貝控制成員
作爲C++編譯器,一旦程序員定義了構造函數,就可以認爲默認初始化已經不能滿足程序員的需求而不再生成默認的構造函數了。這種處理方式在大多數情況下會更安全。
C++11以後,如果程序員還是希望編譯器生成默認構造函數,可以通過=default來實現。
和自動生成默認構造函數的規則類似,如果程序員定義了某個拷貝控制成員,編譯器就不再自動生成其他的。
當然也存在像淺拷貝那樣,編譯器生成的拷貝控制成員就可以滿足需求的情況,這時可以對拷貝控制成員使用=default要求編譯器生成某些拷貝控制成員。
29:使用=delete 阻止拷貝類對象
如果編譯器沒有生成默認構造函數或拷貝控制函數,可以使用=default要求編譯器生成;同樣地,有時我們也會希望某些函數函數不要被調用,這時可以使用=delete修飾該函數。
舉個例子:
例如在Singleton設計模式中就希望類的對象只能通過getInstance靜態方法得到。在C++11發佈之前,類是通過將其拷貝構造函數和賦值運算符私有化來實現的。
C++11增加了=delete修飾符,明確表達雖然聲明瞭某函數,但是又禁止它們被使用的意思。本例中的拷貝構造函數和賦值運算符可以如下聲明:
這樣做最直接的效果就是,test方法本身就會發生編譯錯誤(編譯時),而不需要等到test方法真正被使用時(運行時)。
30:用移動類對象代替拷貝類對象
31:移動構造函數通常應該是noexcept
https://blog.csdn.net/craftsman1970/article/details/81087262
移動構造函數通常應該是noexcept,即,不會拋出異常的移動構造函數
拷貝構造函數通常伴隨着內存分配操作,因此很可能會拋出異常;移動構造函數一般是移動內存的所有權,所以一般不會拋出異常。
C++11中新引入了一個noexcept關鍵字,用來向程序員,編譯器來表明這種情況。
對於永遠不會拋出異常的函數,可以聲明爲noexcept的。這一方面有助於程序員推斷程序邏輯,另一方面編譯器可以更好地優化代碼。
32:移動迭代器
https://blog.csdn.net/craftsman1970/article/details/81122149
2 using namespace std;
3
4 class Test{
5 public:
6 Test()
7 {
8 cout<<"default construction"<<endl;
9 };
10 ~Test(){};
11 Test(const Test& t){
12 cout<<"copy construction"<<endl;
13 }
14
15 Test& operator=(const Test&t){
16 cout<<"equal construction"<<endl;
17 return *this;
18 }
19 Test(const Test&& t){
20 cout<<"move copy construction"<<endl;
21 }
22
23 Test& operator=(const Test&& t)
24 {
25 cout<<"move equal construction"<<endl;
26 return *this;
27 }
28
29
30 };
31
32
33 int main(){
34 Test ts[4];
35 Test dts[4];
36 copy(begin(ts),end(ts),begin(dts));
構造函數,默認構造函數,拷貝構造函數分別被執行4次
至此,引出C++11的概念
int main(){
34 Test ts[4];
35 allocator<Test> alloc;
36 auto dts = alloc.allocate(4);
37
38 uninitialized_copy(make_move_iterator(begin(ts)),make_move_iterator(end(ts)),dts);
代碼首先使用allocator預先取得保存對象的內存空間而不調用初始化函數。
然後使用unitialize_copy來迭代調用每個對象的構造函數。這裏又存在兩種情況:如果只是簡單地使用通常的迭代器,那麼被調用的將是拷貝構造函 數;本例中使用的make_move_iterator適配器告訴編譯器迭代對象是可以移動的,因此調用的是移動構造函數。
這種可以生成右值引用的迭代器就是移動迭代器。
可以看出,實現了和單個實例同樣的高效率。
33:引用限定成員函數
引用限定符(reference qualifier)
目的很簡單,就是希望加一個限制,使得右值對象不能調用setText方法。手段也同樣簡單,只要在方法簽名的後面添加一個“&“,就可以通知編譯器,這個函數只對左值(引用)有效。就像下面這樣:
添加了引用限定以後,下面的代碼就會產生編譯錯誤。
添加引用限定符的位置也是添加const, noexcept的位置,如果需要同時添加的話,引用限定符需要在const之後,noexcept之前。
34:function類模板
代碼首先是function<int(int,int)>定義了返回值爲int,參數爲兩個int的可調用對象類型,然後定義了從string到該類型的映射。
接下來就是初始化映射和使用映射了。
在本例中可以看到:雖然每個操作的類型並不相同,但是由於它們擁有相同的調用形式,通過引入function類模版,可以像同一種類型一樣使用它們。
35:explicit類型轉換運算符
36:override說明符
在實際的開發中隨着開發規模的擴大,類的繼承關係會變得越來越深,成員函數的參數也會越來越多,經常會遇到派生類中定義的成員函數的簽名和覆蓋對象的簽名不一致的而導致覆蓋失敗的情況。
而且要命的是,這種錯誤不會產生編譯錯誤,不容易被發現。
爲了解決這個問題,C++11中引入了一個方法:在聲明、定義派生類中的覆蓋函數時使用override說明符:
由於明確的函數的用意,所以當編譯器無法在基類中找到相同簽名的虛函數的時候,就會產生編譯錯誤
37:final說明符
38:delete說明符
39:繼承的構造函數
直接上代碼:
使用前:
1 #include "../include/hello.h"
2 using namespace std;
3
4 class Test{
5 public:
6 Test(double a,double b):m_a(a),m_b(b)
7 {
8 cout<<"default construction"<<endl;
9 }
10
11 ~Test(){};
12
13 double m_a = 0;
14 double m_b = 0;
15
16
17 };
18
19
20 class Test1 : public Test{
21 public:
22 Test1(double a, double b ):Test(a,b)
23 {
24 }
25 ~Test1(){};
26
27
28 };
29
30 int main(){
31
32 Test c1(1,1);
33 Test1 c2(1,1);
34
35 return 0;
36 }
使用後:
1 #include "../include/hello.h"
2 using namespace std;
3
4 class Test{
5 public:
6 Test(double a,double b):m_a(a),m_b(b)
7 {
8 cout<<"default construction"<<endl;
9 }
10 ~Test(){};
11 double m_a = 0;
12 double m_b = 0;
13 };
14
15 class Test1 : public Test{
16 public:
17 using Test::Test;
18 ~Test1(){};
19 };
20
21 int main(){
22
23 Test c1(1,1);
24 Test1 c2(1,1);
25
26 return 0;
27 }
注意Test1類的構造函數,是不是肥腸的方便。
40:模板類型別名
還是廢話少說,上代碼:
1 #include "../include/hello.h"
2 using namespace std;
3
4 class Test{
5 public:
6 Test()=default;
7 Test(double a,double b):m_a(a),m_b(b)
8 {
9 cout<<"default construction"<<endl;
10 }
11 ~Test(){};
12 double m_a = 0;
13 double m_b = 0;
14 };
15
16 class Test1 {
17 public:
18 Test1(){};
19 ~Test1(){};
20 };
21
22 typedef map<int,int> IntMap;
23 template<typename T> using TemMap = map<int,T>;
24
25 int main(){
26 //=============================
27 map<int,int> map1;
28 map<int,int>map2;
29 IntMap map3;
30 //=============================
31 map<int,Test> map4;
32 map<int,Test1> map5;
33 TemMap<Test> map6;
34 //====================================
35 return 0;
36 }
41:模板函數的默認模板參數
template<typename T, typename F=less<T>>
int compare(const T &v1, const T &v2, F f=F())
{
if(f(v1,v2)) return -1;
if(f(v2,v1)) return 1;
return 0;
}
int main(){
cout<<compare(1,2)<<endl;
cout<<compare(1,2,greater<int>())<<endl;
}
除了F=less<T>部分以外,就是一個普通的模板比較函數。而高亮的部分就是本文的主題:模板函數的模板參數。這種寫法的含義就是如果程序員沒有指定第二個模板參 數,編譯器就默認使用less<T>;如果程序員另外指定了模板參數,例如greater<T>,那麼就使用指定的那個模板參 數。
第一行沒有指定第三個模板參數,並且1<2,所以返回-1;第二行另外指定了greater<int>,執行的是相反的比較邏輯,返回1。
模板函數的默認模板參數很像函數的默認參數,無論是用法還是注意事項。
42:模板函數與返回類型後置
漸進式說明
最簡單的情況
先考慮我們有一個函數,功能是從一個整數數組中取得其中一個元素。
代碼很簡單,但這只是一個引子。本文的所有示例代碼都不考慮下標越界的情況,這樣可以更加突出主題。
適用於其他類型
如果希望這個函數可以適應更多類型的數組,只要引入模板即可。
也沒難多少。
更加通用
如果除了數據類型可以擴展之外,還希望可以將其應用於vector的話,就沒有那麼容易了。例如下面的代碼是不能通過編譯的。
不能通過編譯是由於解引用不是數據類型,而是操作。如果模板變量爲T,而返回值爲T*的話是可以正常編譯的。
解決這個問題的方法是使用前面講到過的C++11新特性:返回值類型後置和decltype。代碼如下:
由於decltype需要取得it解引用的類型,所以取得返回值類型的操作必須在it出現之後,即所謂的返回值類型後置。有了這個模板函數之後,下面的3中情況,代碼都能夠正確無誤地執行:
43:引用合併和完美轉發
C++11的另一個新特性,當引用和右值引用同時出現時,遵循下面的原則:
左值引用 + 左值引用 = 左值引用
左值引用 + 右值引用 = 左值引用
右值引用 + 左值引用 = 左值引用
右值引用 + 右值引用 = 右值引用
上代碼:
6 void inc (int& a, int& b)
7 {
8 b = a+99;
9 cout << b <<endl;
10 }
11
12 template <typename F, typename T1, typename T2>
13 void exe(F f, T1&& t1, T2&& t2)
14 {
15 f(t1,t2);
16 }
17 int main(){
18 int a = 0;
19 exe(inc,1,a);
20 cout<<a<<endl;
21 return 0;
22 }
可以發現,左值引用a +右值引用t2 = 左值引用b
歸納起來原則很簡單:永遠是左值優先。這種現象稱爲引用合併(reference collapse)。
完美轉發是爲了解決引用合
並而引入的機制:
上代碼:
void inc (int&& a, int&& b)
{
>> cout << a + b <<endl;
}
template <typename F, typename T1, typename T2>
void exe(F f, T1&& t1, T2&& t2)
{
>> f(t1,t2);
}
int main(){
exe(inc,1,1);
return 0;
}
6 void inc (int&& a, int&& b)
7 {
8 cout << a + b <<endl;
9 }
10
11 template <typename F, typename T1, typename T2>
12 void exe(F f, T1&& t1, T2&& t2)
13 {
14 f(std::forward<T1>(t1),std::forward<T2>(t2));
15 }
16 int main(){
17
18 exe(inc,1,1);
19 return 0;
20 }
如此修改,就可以正常運行了。
std::forward是爲了在使用右值引用參數的函數模板中解決參數的完美轉發問題
std::move和std::forward只是執行轉換的函數(確切的說應該是函數模板)。std::move無條件的將它的參數轉換成一個右值,而std::forward當特定的條件滿足時,纔會執行它的轉換。
std::move表現爲無條件的右值轉換,就其本身而已,它不會移動任何東西。 std::forward僅當參數被右值綁定時,纔會把參數轉換爲右值。 std::move和std::forward在運行時不做任何事情。
move屬於強轉,forward對於左值還是會轉換成左值,對於右值轉換成右值。一般在模板元編程裏面,對於forward需求比較多,因爲可以處理各種不同場景。而一般的代碼裏面,由於可以確認傳入的是左值還是右值,所以一般直接就調用std::move了。
44:可變參數模板
C++11增加了可變參數模板,可以接受可變數目,類型的參數。我們通過開發中常用的輸出調試信息的例子介紹紹可變參數模板的使用方法。首先是聲明可變參數模板。
在模板參數定義的部分,通過typename...定義可變模板參數。三個點的含義和C語言中的可變參數定義類似,不同的是後面接着指定了可變參數列表的名稱Args。
在參數定義的部分,使用可變模板參數定義的Args以相同的格式定義函數的可變參數列表。
這個參數列表可以在模板函數的實現中直接使用,這樣在定義可變參數模板時就解決了可變參數函數的第二個難點。
可變參數模板的實現
可變參數模板的實現通常需要一些小技巧:遞歸和重載。還是先看代碼。
template<typename T>
void write(const T& t)
{
cout<<t<<endl;
}
template<typename T, typename... Args>
void write(const T& t, const Args... rest)
{
write(t);
write(rest...);
}
int main(){
write(1,2,3,4,5,6);
return 0;
}
代碼中定義了兩個重載的writeLog函數,一個只接受類型爲T的參數t,另一個除了t之外,還接受可變參數rest。當使用一個參數調用writeLog的時候,實際調用上面的函數;當使用多個參數調用writeLog的時候調用下面的writeLog。
下面的writeLog首先使用第一個參數t調用上面的writeLog之後,使用rest遞歸調用writeLog(嚴格講是rest中有多於一個參數的 時候)。從調用者來看,每次處理一個參數之後,使用其餘的參數再次調用writeLog,直到最後調用一個參數的writeLog
例如log輸出中經常需要的時間信息,就可以這樣實現:
using namespace std;
class Logtime{
public :
Logtime(){
time(&t);
}
void output(){
tm * p = localtime(&t);
cout<<p->tm_hour<<":"<<p->tm_min<<":"<<p->tm_sec;
}
time_t t;
};
void write(Logtime t)
{
t.output();
}
template<typename T>
void write(const T& t)
{
cout<<t<<endl;
}
template<typename T, typename... Args>
void write(const T& t, const Args... rest)
{
write(t);
write(rest...);
}
int main(){
Logtime t;
write(t,1,2,3,4,5,6);
return 0;
}
45:sizeof...運算符
假設有一個程序,需要接受文字信息並生成學生檔案,信息的形式爲:
"Name:ABC", "Age:20", "Wight:73","Address:Dalian", "Interest:football"
程序解析上述信息後,形成以下形式的數據:
根據本應用的要求,姓名,年齡和體重三項爲必填項,地址和興趣爲可選項。
參考前一篇文章的做法,代碼可以這樣實現:
但是存在一個問題,就是參數數目可能會少於3個,也可能會多於5個。無論哪種情況都不可能生成正確的數據,於是希望在遞歸處理之前將這些情況排掉。而取得實際參數個數的方法就是sizeof...。參考下面的代碼:
46:包擴展
光從形式上來看,兩個函數的簽名完全不同,但是程序可以正常執行。其原因就是發生了包擴展,編譯器根據add的需求將arg包進行了擴展。包擴展的格式就是在包名後面加上三個小點。
template <typename T>
T add(T a,T b, T c){
return a+b+c;
}
template<typename T, typename... Args>
T sum(T t, Args... arg)
{
return add(t,arg...);
}
int main(){
cout<<sum(1,2,3)<<endl;
return 0;
}
第一個實參1賦值給了形參t,arg則包含了另外兩個實參2和3。arg展開的結果就是2,3。也就是說,add(t, arg...)等價於add(1, 2, 3)。
47:可變參數模板的參數轉發
參考:
https://blog.csdn.net/craftsman1970/article/details/82594888
using namespace std;
class Segment{
public:
Segment()=default;
virtual ~Segment()=default;
virtual ostream& output(ostream& os)const =0;
};
ostream& operator<<(ostream& os, const Segment& seg)
{
return seg.output(os);
}
class StringSegment: public Segment{
public:
StringSegment(string&& str):m_msg(std::move(str)){}
StringSegment(string& str):m_msg(str){}
virtual ~StringSegment()=default;
ostream& output(ostream& os)const {
os<<m_msg;
return os;
}
private:
string m_msg;
};
class MsgHolder{
public:
MsgHolder()=default;
~MsgHolder()=default;
template<typename T>
void add(T&& msg)
{
m_segs.push_back(make_shared<StringSegment>(std::forward<T>(msg)));
}
template<typename T, typename... Args>
void add (T&& t, Args&&... rest){
add(std::forward<T>(t));
add(std::forward<Args>(rest)...);
}
ostream& output (ostream& os)const{
for(auto seg:m_segs){
os<<(*seg);
}
return os;
}
private:
vector<shared_ptr<Segment>> m_segs;
};
ostream& operator<<(ostream& os, MsgHolder& holder)
{
return holder.output(os);
}
int main(){
MsgHolder mh;
string ltest("lvalue test");
string rtest("rvalue test");
mh.add("testof ", ltest,"and", std::move(rtest));
cout<<"mg="<<mh<<endl;
return 0;
}
48:標準庫tuple模板
初始化
tuple初始化有幾種方式,首先是默認初始化。這種情況下,tuple的每個成員都被默認初始化。
也可以在初始化時指定各個成員的值。下面代碼中第一種方式使用的是tuple的構造函數,第二種方式使用的是初始化列表。
使用make_tuple函數配合auto類型指示符,還可以更簡潔地初始化tuple。
訪問tuple成員
取得成員的數量和類型
如果tuple數據的生成者和使用者不在一個模塊中,可能就需要對數據進行某種檢查,這時就很可能希望知道成員的數量或者類型。直接上代碼。
49:新的bitset運算
https://blog.csdn.net/craftsman1970/article/details/82667464
50:正則表達式庫(regular-expression library)
正則表達式(regular expression)是一種描述字符序列的方法,從C++11起,C++正則表達式庫(regular-expression library)成爲新標準庫的一部分。
這塊內容,需要的話自行查找學習就行,關鍵內容還在正則表達式上。
https://blog.csdn.net/craftsman1970/article/details/82748045
51:noexcept異常指示符
軟件開發的規模越來越大,函數庫/類庫的調用層級也越來越多,這時確定一個調用是否會拋出異常也 變得越來越困難。作爲解決手段之一,C++11中通過noexcept說明符來對外宣稱處理不會拋出異常。這樣程序員就不必深入所有的調用層級自己去確認 了。
明明聲明瞭某個處理是noexcept,實際的內部處理還是拋出了異常。在這種情況下,即使進行了錯誤捕捉,也不會正常工作。
52:內聯命名空間(常用於類庫升級)
命名空間簡介
隨着軟件開發規模的擴大,類名,函數名重複的可能性也越來越大。最樸素的解決辦法就是改名,這種方法在向已經存在的類庫中添加代碼時問題不大,但是如果是將兩個從未謀面的代碼庫結合在一起時就不再適用了。
C++解決這個問題的辦法就是引入命名空間。假設有下面兩個命名空間:
代碼中分別定義了std_namespace1和lstd_namespace2兩個命名空間。在兩個命名空間內分別定義了相同名稱的Normal類。這兩個類的成員函數是否一樣其實不重要,這裏姑且使用了相同的名稱。
有了命名空間以後,使用【命名空間名稱::類名】的方式,就可以對這兩個同名的Normal類加以區分了。
如果使用using語句,可以讓某個命名空間的內容釋放出來,就好像它們都存在與外層命名空間一樣。
內聯命名空間
C++11中引入了內聯命名空間(inline namespace),它的特點就是不需要使用using語句就可以直接在外層命名空間使用該命名空間內部的內容,而且無需使用命名空間前綴。先看代碼:
內聯命名空間的聲明方法就是在原來的聲明語法前面增加inline關鍵字。除此之外上面代碼還有以下特點:
兩處聲明的命名空間同名,它們同屬一個命名空間。這是C++命名空間從來就有的特性。
第一次聲明命名空間時使用了inline關鍵字,這叫顯式內聯;第二次沒有使用inline關鍵字,但是由於第一次已經聲明瞭inline,這裏聲明的還是內聯命名空間。這種情況成爲隱式內聯。
內聯命名空間聲明之後,就可以在外層命名空間不適用前綴而直接使用它們了。
上述代碼中test_inline_namespace處在linline_namespace1的外層,所以可以直接使用Inline1和Inline2。test_inline_namespace2處在更外層,這時也只是需要使用外層命名空間inline_test前綴即可。
看起來inline_namespace就像不存在一樣。
前面提到內聯命名空間就像不存在一樣,那麼就產生了一個嚴肅的問題:它有什麼用?爲了回答這個問題,我們舉一個更加接近實際開發的例子。假設有如下類庫代碼:
接下來纔是重點:假設我們隊類庫進行了升級,同時又希望:
-
使用者代碼不受影響,除非使用者自己想改。
-
可以自由使用新類庫的功能
-
如果有需要仍然可以使用原來的類庫
解決方法當然是使用內聯命名空間。首先是對類庫進行處理:
代碼中爲每個版本的類庫定義了命名空間,同時將最新版本定義爲內聯命名空間。有了這樣的準備之後,使用者代碼可以是下面這樣:
使用最新類庫的時候,就好像沒有定義過命名空間一樣;如果實在是需要原來的類庫,可以通用版本前綴加類名的方式。
還有一點很重要:由於隱式內聯語法的存在,將來出現ver3的時候,只要將唯一的一個inline關鍵字移動到第一次出現的ver3定義之前就可以了!
53:有作用域的enum(scoped enumeration)
傳統解決方法,將枚舉類型的定義放到不同的一個作用域(類或命名空間)中。例如:
這樣兩個枚舉定義就不會發生衝突了。可以用如下方式使用這兩個枚舉類型:
C++11中引入了限定作用域的枚舉類型的概念。其用法如下:
和前面的方式進行比較可以發現:只是在標準的枚舉類型定義格式中增加了class關鍵字。它的效果就是爲枚舉值同時定義了一個和枚舉類型同名的作用域。定義了限定作用域的枚舉類型之後,可以以如下方式使用:
54:指定enum類型的大小
C++11新標準中,允許使用enum類型名後接冒號加類型的方式來指定枚舉類型的大小。例如我們可以將scope_enum_2的大小指定位8個字節:
55:enum前置聲明(包括C++前置聲明的複習)
https://blog.csdn.net/craftsman1970/article/details/83063500
代 碼中的變化有兩點,一個是數據ImportantClass成員的類型變成了指針類型;另一個是include語句變成了class聲明語句。兩個變化缺 一不可,第二個變化是第一個變化的前提條件。因爲這裏只是聲明瞭指針類型,所以只要告訴編譯器ImportantClass一個類就夠了。真正的類型信息 在userclass.cpp中提供。這種方式就是前置聲明。
56:標準庫mem_fn類模板和std::function()
C++中的可調用對象包括函數,函數對象,lambada表達式,參數綁定等,它們都可以作爲算法的參數。
使用function生成可調用對象
我們希望可以直接使用Test的成員函數compare成員函數。這個需求可以使用C++11中引入的function來解決:
function<bool(const Test&, const Test&)> tmp = &Test::compare;
通過這種方式,我們可以將類的成員函數轉換爲可調用對象
使用mem_fn生成可調用對象
使用function的方法,還是有點麻煩:雖然&Test::compare的簽名已經決定了可調用對象的形式,程序員還是需要另外指定。解決這個問題的方法是使用mem_fn(注意不是mem_fun)來生成可調用對象,mem_fn會根據成員函數指針推斷可調用函數的類型,就省去了另外指定的步驟。
struct Test{
Test(int i):a(i){}
bool compare (const Test& t)const{
return (a < t.a);
}
int a;
};
//bool compare(const Test& t1, const Test& t2){
// return t1.compare(t2);
//}
//std::function<bool(const Test&, const Test&)> tmp = &Test::compare;
int main(){
Test t1(1);
Test t2(2);
function<bool(const Test&, const Test&)> tmp = &Test::compare;
cout<<"error!!"<<mem_fn(&Test::compare)(t1,t2)<<endl;
return 0;
}
57:類類型的union成員