文章目錄
C++11學習(上)
1.C++簡介
相比於C++98/03,C++11則帶來了數量可觀的變化,其中包含了約140個新特性,以及對C++03標準中約600個缺陷的修正,這使得C++11更像是從C++98/03中孕育出的一種新語言。相比較而言,C++11能更好地用於系統開發和庫開發、語法更加泛華和簡單化、更加穩定和安全,不僅功能更強大,而且能提升程序員的開發效率。
2. 列表初始化
2.1 C++98中{}的初始化問題
在C++98中,標準允許使用花括號{}對數組元素進行統一的列表初始值設定。比如
int array1[] = {1,2,3,4,5};
int array2[5] = {0};
對於一些自定義的類型,卻無法使用這樣的初始化。比如:
vector<int> v{1,2,3,4,5};
上面無法通過編譯,每次都會導致vector未定義,需要先添加vector頭文件,然後使用循環對其賦初始值,非常不方便。C++11擴大了用大括號括起的列表(初始化列表)的使用範圍,使其可用於所有的內置類型和用戶自定義的類型,使用初始化列表時,可添加等號(=),也可不添加。
2.2 內置類型的列表初始化
int main()
{
// 內置類型變量
int x1 = { 10 };
int x2{ 10 };
int x3 = 1 + 2;
int x4 = { 1 + 2 };
int x5{ 1 + 2 };
// 數組
int arr1[5]{ 1,2,3,4,5 };
int arr2[]{ 1,2,3,4,5 };
// 動態數組,在C++98中不支持
int* arr3 = new int[5]{ 1,2,3,4,5 };
// 標準容器
vector<int> v{ 1,2,3,4,5 };
map<int, int> m{ {1,1}, {2,2,},{3,3},{4,4} };
return 0;
}
注意: 列表初始化可以在{}之前使用等號,其效果與不使用=沒有什麼區別。
2.3 自定義類型的列表初始化
- 標準庫支持單個對象的列表初始化
class Point
{
public:
Point(int x = 0, int y = 0) : _x(x), _y(y)
{}
private:
int _x;
int _y;
};
int main()
{
Pointer p{ 1, 2 };
return 0;
}
- 多個對象的列表初始化
多個對象想要支持列表初始化,需給該類(模板類)添加一個帶有initializer_list類型參數的構造函數即可。注意:initializer_list是系統自定義的類模板,該類模板中主要有三個方法:begin()、end()迭代器以及獲取區間中元素個數的方法size()。
#include <initializer_list>
template<class T>
class Vector {
public:
// ...
Vector(initializer_list<T> l) : _capacity(l.size()), _size(0)
{
_array = new T[_capacity];
for (auto e : l)
_array[_size++] = e;
}
Vector<T>& operator=(initializer_list<T> l) {
delete[] _array;
size_t i = 0;
for (auto e : l)
_array[i++] = e;
return *this;
}
// ...
private:
T* _array;
size_t _capacity;
size_t _size;
};
3. 變量類型推導
3.1 爲什麼需要類型推導
在定義變量時,必須先給出變量的實際類型,編譯器才允許定義,但有些情況下可能不知道需要實際類型怎麼給,或者類型寫起來特別複雜,比如:
#include <map>
#include <string>
int main()
{
short a = 32670;
short b = 32670;
// c如果給成short,會造成數據丟失,如果能夠讓編譯器根據a+b的結果推導c的實際類型,就不會存在問題
short c = a + b;
std::map<std::string, std::string> m{ {"apple", "蘋果"}, {"banana","香蕉"} };
// 使用迭代器遍歷容器, 迭代器類型太繁瑣
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
cout << it->first << " " << it->second << endl;
++it;
}
return 0;
}
C++11中,可以使用auto來根據變量初始化表達式類型推導變量的實際類型,可以給程序的書寫提供許多方便。將程序中c與it的類型換成auto,程序可以通過編譯,而且更加簡潔。
3.2 decltype類型推導
1. 爲什麼需要decltype
auto使用的前提是:必須要對auto聲明的類型進行初始化,否則編譯器無法推導出auto的實際類型。但有時候可能需要根據表達式運行完成之後結果的類型進行推導,因爲編譯期間,代碼不會運行,此時auto也就無能爲力。
template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
如果能用加完之後結果的實際類型作爲函數的返回值類型就不會出錯,但這需要程序運行完才能知道結果的實際類型,即RTTI(Run-Time Type Identification 運行時類型識別)。
C++98中確實已經支持RTTI:
- typeid只能查看類型不能用其結果類定義類型
- dynamic_cast只能應用於含有虛函數的繼承體系中
運行時類型識別的缺陷是降低程序運行的效率。
2. decltype
decltype是根據表達式的實際類型推演出定義變量時所用的類型,比如:
1. 推演表達式類型作爲變量的定義類型
int main()
{
int a = 10;
int b = 20;
// 用decltype推演a+b的實際類型,作爲定義c的類型
decltype(a + b) c;
cout << typeid(c).name() << endl;
return 0;
}
2. 推演函數返回值的類型
void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
// 如果沒有帶參數,推導函數的類型
cout << typeid(decltype(GetMemory)).name() << endl;
// 如果帶參數列表,推導的是函數返回值的類型,注意:此處只是推演,不會執行函數
cout << typeid(decltype(GetMemory(0))).name() << endl;
return 0;
}
4. 範圍for循環
4.1範圍for的語法
在C++98中如果要遍歷一個數組,可以按照以下方式進行:
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
cout << *p << endl;
}
對於一個有範圍的集合而言,由程序員來說明循環的範圍是多餘的,有時候還會容易犯錯誤。因此C++11中引入了基於範圍的for循環。for循環後的括號由冒號“ :”分爲兩部分:第一部分是範圍內用於迭代的變量,第二部分則表示被迭代的範圍。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (auto& e : array)
e *= 2;
for (auto e : array)
cout << e << " ";
return 0;
}
注意: 與普通循環類似,可以用continue來結束本次循環,也可以用break來跳出整個循環。
4.2範圍for的使用條件
- for循環迭代的範圍必須是確定的對於數組而言,就是數組中第一個元素和最後一個元素的範圍;對於類而言,應該提供begin和end的方法,begin和end就是for循環迭代的範圍。
注意: 以下代碼就有問題,因爲for的範圍不確定
void TestFor(int array[])
{
for (auto& e : array)
cout << e << endl;
}
- 迭代的對象要實現++和==的操作。
5. final與override
1.禁止重寫 final:修飾虛函數,表示該虛函數不能再被繼承
- 禁止虛函數被重寫
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒適" << endl; }
};
此代碼編譯報錯,提示不能重寫Drive。雖然Drive是虛函數,但是因爲有final關鍵字,保證它不會被重寫。你可能會說,那不聲明virtual不就完了。但是如果Car本身也有基類,Drive是繼承下來的,那virtual就是隱含的了。
- 禁止類被繼承
class Car final {
};
class Benz : public Car {
};
此代碼編譯報錯,提示不能繼承Car。
常見面試題:實現一個不能被繼承的類
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
// C++11給出了新的關鍵字final禁止繼承
class NonInherit final
{};
2.顯式聲明重寫 override: 檢查派生類虛函數是否重寫了基類某個虛函數,如果沒有重寫編譯報錯。
class Car {
public:
virtual void Drive() const {}
};
class Benz :public Car {
virtual void Drive() {}
};
上面的代碼在重寫函數f1時不小心漏了const,但是編譯器不會報錯。因爲它不知道你是要重寫f1,而認爲你是定義了一個新的函數。這樣的情況也發生在基類的函數簽名變化時,子類如果沒有全部統一改過來,編譯器也不能發現問題。
C++ 11引入了override聲明,使重寫更安全。
class Car {
public:
virtual void Drive() {}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒適" << endl; }
};
此時編譯報錯,提示找不到重寫的函數。