文章目錄
- 初始化和賦值的區別
- 什么是默認初始化?
- 列表初始化
- 列表初始化的使用場景
- 不適合使用列表初始化的場景
- 類內初始值
- 混用string對象和C風格字符串
- 數組與vector對象
- 關于vector對象
- 兩者間的初始化關系
- 直接初始化與拷貝初始化
初始化和賦值的區別
- 初始化的含義是創建變量時賦予其一個初始值
- 賦值的含義時把對象的當前值擦除,而已一個新值來替代。
什么是默認初始化?
如果定義變量時沒有指定初值,則變量被默認初始化,此時變量被賦予了 “默認值”。默認值到底是什么由變量類型決定,同時定義變量的位置也會對此有影響。
內置類型的默認值由定義的位置決定, 定義于任何函數體之外的變量被初始化為0;定義于函數體內部的內置類型將不被初始化。一個未被初始化的內置類型變量的值是未定義的,試圖拷貝或以其他形式訪問此類值將引發錯誤。
列表初始化
C++定義了初始化的好幾種不同形式,通常定義一個變量并初始化的方式有以下四種:
int x = 0;
int x = {0};
int x{0};
int x(0);
使用花括號來初始化變量在C++11新標準中得到了全面應用。這種初始化的形式被程為列表初始化(list initialization)。現在,無論是初始化對象,還是某些時候為對象賦新值,都可以使用列表初始化。
列表初始化的使用場景
- 列表初始化可被用于以下場景:
// Vector 接收了一個初始化列表。
vector<string> v{"foo", "bar"};// 不考慮細節上的微妙差別,大致上相同。
vector<string> v = {"foo", "bar"};// 可以配合 new 一起用。
auto p = new vector<string>{"foo", "bar"};// map 接收了一些 pair, 列表初始化大顯神威。
map<int, string> m = {{1, "one"}, {2, "2"}};// 初始化列表也可以用在返回類型上的隱式轉換。
vector<int> test_function() { return {1, 2, 3}; }// 初始化列表可迭代。
for (int i : {-1, -2, -3}) {}// 在函數調用里用列表初始化。
void TestFunction2(vector<int> v) {}
TestFunction2({1, 2, 3});
- 用戶自定義類型也可以定義接收
std::initializer_list<T>
的構造函數和賦值運算符,以自動列表初始化:
class MyType {public:// std::initializer_list 專門接收 init 列表。MyType(std::initializer_list<int> init_list) {for (int i : init_list) append(i);}MyType& operator=(std::initializer_list<int> init_list) {clear();for (int i : init_list) append(i);}
};
MyType m{2, 3, 5, 7};
- 最后,列表初始化也適用于常規數據類型的構造,哪怕沒有接收
std::initializer_list<T>
的構造函數。
// MyOtherType 沒有 std::initializer_list 構造函數,
// 直接上接收常規類型的構造函數。
class MyOtherType {public:explicit MyOtherType(string);MyOtherType(int, string);
};
MyOtherType m = {1, "b"};
// 不過如果構造函數是顯式的(explict),就不能用 `= {}` 了。
MyOtherType m{"b"};
不適合使用列表初始化的場景
值得注意的是,當用于內置類型的變量時,如果使用列表初始化且初始值存在丟失信息的風險,則編譯器將報錯:
long double ld = 3.1415926536;
int a{ld}, b = {ld}; // 錯誤:轉換未執行,因為存在丟失信息的危險
int c(ld), d = ld; // 正確:轉換執行,且確實丟失了部分值
使用 long double
的值初始化 int
變量時可能丟失數據,所以編譯器拒絕了 a
和 b
的初始化請求。其中,至少 ld
的小數部分會丟失掉,而且某些情況下 int
也可能存不下 ld
的整數部分。
同時,千萬別直接列表初始化 auto
變量,因為可讀性不高:
auto d = {1.23}; // d 類型是 std::initializer_list<double>
auto d = double{1.23}; // d 類型為 double, 并非 std::initializer_list.
類內初始值
C++11標準規定,可以為數據成員提供一個類內初始值(in-class initializer)。創建對象時,類內初始值將用于初始化數據成員。沒有初始值的成員將被默認初始化。
對類內初始值的限制如下:
- 放在花括號里
- 放在等號右邊
- 不能使用圓括號
因為我們無法避免這樣的情況,有時函數聲明也會用到圓括號:
class Widget
{
private: typedef int x;int z(x);
};
因此用圓括號為類內成員提供類內初始值容易產生二義性,編譯器會覺得該語句語義不明。
混用string對象和C風格字符串
我們都知道允許使用字符串字面值來初始化string對象:
string s("Hello World!");
C++規定,任何出現字符串字面值的地方都可以用以空字符結束的字符數組來替代:
- 允許使用以空字符結束的字符數組來初始化string對象或為string對象賦值。
- 在string對象的加法運算中允許使用以空字符結束的字符數組作為其中一個運算對象(不能兩個對象都是);在string對象的復合賦值運算中允許是用以空字符結束的字符數組作為右側的運算對象。
上述性質反過來并不成立:如果程序的某處需要一個C風格字符串,無法直接用string對象來替代它。
例如:不能使用string對象直接初始化指向字符的指針。為了實現這一功能,string專門提供了一個名為c_str的成員函數:
char *str = s; // 錯誤:不能用string對象初始化char*
const char *str = s.c_str; // 正確
函數返回結果使用一個指針,該指針指向一個以空字符結束的字符數組,而這個數組所存的數據恰好與哪個string對象的一樣。結果指針的類型是const char*,從而確保我們不會改變字符數組的內容。
PS:由于我們無法保證c_str函數返回的數組一直有效,如果后續的操作改變了s的值就可能讓之前返回的數組失去效用。因此,如果執行完c_str()函數后程序想一直都能使用其返回的數組,最好將該數組重新拷貝一份。
數組與vector對象
關于vector對象
vector是模板而非類型,由vector生成的類型必須包含vector中元素的類型,如:
vector<int>
兩者間的初始化關系
- 不允許使用一個數組為另一個內置類型的數組賦初值
- 不允許使用vector對象初始化數組
- 允許使用數組來初始化vector對象
實現第三點只需要指明要拷貝區域的首元素地址和尾后地址就可以了:
int int_arr[] = {0, 1, 2, 3, 4, 5};
vector<int> ivec(begin(int_arr), end(int_arr));
用于創建ivec 的兩個指針實際上指明了用來初始化的值在數組int_arr中的位置,分別用標準庫函數begin和end來計算int_arr的首指針和尾后指針。在最終結果中,ivec將包含6個元素,它們的次序和值都與數組int_arr完全一樣。
亦可使用數組的一部分來初始化vector對象:
vector<int> subVec(int_arr + 1, int_arr + 4);
// 拷貝三個元素:int_arr[1]、int_arr[2]、int_arr[3]
直接初始化與拷貝初始化
- 使用直接初始化時,編譯器進行函數匹配來選擇與我們提供的參數最匹配的構造函數。
- 使用拷貝初始化時,編譯器將右側運算對象拷貝到正在創建的對象中,按需選擇是否進行類型轉換。
拷貝初始化通常使用拷貝構造函數來完成。
拷貝初始化不僅在我們用 = 定義變量時會發生,也會在下列情況中發生:
- 將一個對象作為實參傳遞給一個非引用類型的形參
- 從一個返回類型為非引用類型的函數返回一個對象
- 用花括號列表初始化一個數組中的元素或一個聚合類中的成員
拷貝初始化受到explicit類型的構造函數的限制:
需要類型轉化的拷貝初始化的過程是這樣的:
- 先隱式調用給定類型的構造函數,將等號右邊的值作為實參傳遞給構造函數,從而生成一個臨時的給定類型的對象。(類型轉換通過本步完成,如果是無需類型轉換的拷貝初始化則沒有本步)
- 再讓想要初始化的對象隱式調用拷貝構造函數,并將臨時對象作為實參傳給拷貝構造函數,從而完成初始化。
這樣的操作對于explicit類型的構造函數是行不通的,因為無法隱式調用一個explicit的構造函數生成一個臨時對象。 換言之,explicit類型的構造函數是抑制隱式類型轉換的。
因此面對explicit類型的構造函數只能執行直接初始化、或者是無需類型轉換的拷貝初始化:
// error:vector接受大小參數的構造函數是explicit的
vector<int> vi = 10; vector<int> vi(10); // 正確:直接初始化string s(10, 'x'); // 正確:直接初始化
string s1 = s; // 正確:無需類型轉換的拷貝初始化
如果希望使用有給explicit的構造函數,必須顯式地使用:
void f(vector<int>); // f的參數進行拷貝初始化
f(10);
// error:不能隱式地使用explicit的構造函數構造一個臨時vector
// 因為無法執行從int(也就是10的類型)到vector的類型轉換
f(vector<int>(10));
// 正確:顯式地使用explicit的構造函數
// 為vectoc的構造函數傳入10作為實參,構造一個臨時的vector
// 用臨時的vector初始化f