C++複習筆記——初始化列表順序
參考測試程式碼1:
初始化列表中,用被賦值3的i來賦值j,j打印出來也是3.#include<iostream> using namespace std; class A { public: A(int iniI,int iniJ,int iniK):i(iniI),j(i),k(iniK){} int i; int j; int k; }; int main(){ A a = A(3,4,5); cout << a.i << endl; cout << a.j << endl; cout << a.k << endl; }
參考測試程式碼2:
初始化列表中先初始化j,再用j初始化i,這次就不能遂願了。#include<iostream> using namespace std; class A { public: A(int iniI,int iniJ,int iniK):j(iniJ),i(j),k(iniK){} int i; int j; int k; }; int main(){ A a = A(3,4,5); cout << a.i << endl; cout << a.j << endl; cout << a.k << endl; }
因為初始化列表對映(請允許我這樣稱呼它)形參和成員的順序並不是真實的賦值變數的順序,可以理解為只是簡單的無順序羅列。
這個大家都知道,我今天想知道的是,為什麼會這樣,什麼機制決定這個初始化列表中成員的初始化是按成員宣告的先後順序執行的。
其實本文想探討的問題就是——C++的初始化列表這個功能,是怎樣整個對映到編譯、執行過程的。
提起過程,我突然聯想到了scanf,或者說是cin,這些是可以給變數賦值的,但是這個是什麼時候完成的?當然是執行時,這個是動態的賦值了。
所以,假設初始化列表的初始化也是執行時,不用假設,其實就是,類物件就可以執行後動態生成(但是也應該有編譯時就定義好的吧?沒有嗎?main之外的全域性物件?)
那麼假如初始化列表都是執行時的事了,而類成員的順序是編譯時就確定好的,所以初始化列表對變數的初始化順序要遵循事先定義好的順序,也就是變數宣告順序!
但是,為什麼一定要遵頊這個順序呢?舉個反例:
同樣動態,我先宣告個i,再宣告個j,先cin給j賦值,再cin給i賦值行不行?順序不還是可以人為改變麼?所以動態這個理由說不通!
或許,就只是一種巧合,一種約定罷了。
“因為記憶體地址是順序來的,所以我們初始化它也順便按這個順序來吧?”——編者蒙
補充:
經過彙編除錯,因為地址順序是這樣的吧,也許,總之編譯器就先給i賦值,另一現象是,用j給i賦值,i和k初始值都不是0,j偏偏是0!!
這是一個很費解的問題,一個面試題就是這樣的,本來你認為初始化之前,j的值是亂的,一如i與k,可是它就是0,然後給i賦值0。。。
他們打了一個馬虎眼,寫了個0,但是我們都知道這個j(0)裡的0給不到i,所以說答案i是亂的。。。
但是面試官顯然對編譯器摳的過於細了,也許只是基於gcc或者g++?總之,自己實際操作後,j的野值它就是0。面試官期望的結果是這個。假如你是個傻子,答個0,也對了吧?瞎貓碰死耗子?
但是現在要探討這個機理,為什麼j就剛好是0,如果用k來初始化i,k就不是0啊,見第三種。
class A
{
public:
//兩種初始化結果一樣
// A(int iniI,int iniJ,int iniK):j(iniJ),i(j),k(iniK){}
//別看前者沒有j(0),因為j的預設值就是0,所以這個題的結果是i為0
// A(int iniI,int iniJ,int iniK):j(0),i(j),k(iniK){}
//換個k試試,看是巧合還是編譯器有意優化
A(int iniI,int iniJ,int iniK):j(iniJ),i(k),k(iniK){}
int i;
int j;
int k;
};
有網友說那就是無規律可循的巧合,我就不信邪,肯定是有規律的,當時忘了這道題是怎麼說的,算沒算我錯,我就假設這個題有更高明的解釋。一定要找出來!
換個方向探索:
class A
{
public:
//額外加個變數看看有無變化
A(int iniI,int iniJ,int iniK):j(iniJ),i(l),k(iniK),l(2){}
int i;
int j;
int k;
int l;
};
看到一個可能是規律的現象,k變0了,k是倒數第2個。
(gdb) print a.i
$1 = 3097936
(gdb) print a.j
$2 = 134513646
(gdb) print a.k
$3 = 0
(gdb) print a.l
$4 = 5636084
可能和物件有關,可能和地址有關,因為現在k的地址剛好是之前j的地址,0xbffff5f8。是這個地址特殊嗎?(gdb) print &a.l
$8 = (int *) 0xbffff5fc
(gdb) print &a.k
$9 = (int *) 0xbffff5f8
(gdb) print $ebp
$10 = (void *) 0xbffff618
棧底都是0xbffff618。
物件是保證高地址不超過0xbffff5fc,這個l的地址是之前k的地址,棧底對齊的。但是這也不是棧的底部,只是變數能達到的位置吧,可能被其他東西(我在main之外定義了一個非靜態的物件,這佔用了空間)佔用了更高地址(棧底地址高)。總之,就是這個0xbffff5fc地址的內容是0。還是什麼?
清除多餘物件變數的宣告,再編譯除錯:
(gdb) print &a
$1 = (A *) 0xbffff600
(gdb) print $ebp
$2 = (void *) 0xbffff618
(gdb) print &a.l
$3 = (int *) 0xbffff60c
(gdb) print &a.k
$4 = (int *) 0xbffff608
(gdb) print a.l
$5 = 5636084
(gdb) print a.k
$6 = 134514699
(gdb) print a.j
$7 = 134514160
(gdb) print a.i
$8 = 134514688
打印發現,因為沒存到地址0xbffff5fc了,所以沒“0”值了。那個地址0xbffff5fc也沒規定非得給物件的倒數第二個引數。完全是巧合,野值就是野值,你被忽悠後,自己又剛好遇到巧合,就更質疑裡邊有什麼特殊機制。其實是沒有的!!!!
好吧,我被面試的忽悠了,東方X信的C++題就是鬧著玩的!!!!!!!!!你們不是吹毛求疵,是雞蛋裡挑骨頭!!!
=====================================================================================================
還沒完:關於類物件的初始化到底算哪個階段的問題,或者說裡邊的成員變數初始化到底算哪個階段,發現一個比較有意思的事情。
同樣用上邊那種”錯誤“程式碼:
#include<iostream>
using namespace std;
class A
{
public:
A(int iniI,int iniJ,int iniK):j(iniJ),i(j),k(iniK){}
int i;
int j;
int k;
};
int main(){
cout<<"hello"<<endl;
A a = A(3,4,5);
A a2 = A(3,4,5);
cout << a.i << endl;
cout << a2.i << endl;
}
執行:
[[email protected] cpp]# ./a.out
hello
134514699
5636084
[[email protected] cpp]# ./a.out
hello
134514699
3817460
[[email protected] cpp]# ./a.out
hello
134514699
5636084
可以看到,每次執行,用j上的“野”值給i賦值,結果都是固定的,想要不一樣必須重新編譯一次才行。(也不一定,也許是巧合,見下邊的int例子)所以說,至少這些初始化之前的值是固定的。
仔細一想,也沒什麼不可理解的,物件新申請了一片記憶體,又沒初始化,趕上什麼是什麼唄!
只是為什麼每次執行值都固定,是因為這塊區域的申請與劃分什麼的,都定在編譯階段了嗎?可以這樣理解嗎?
其實編譯器和記憶體申請這學得還是不精,這裡邊的記憶體指的是虛存麼?你不可能這個程式老能分配到固定的記憶體區域,並且那塊的值也固定吧。
這個問題是通用的,比如隨便宣告定義一個int變數,不定義,每次執行值也是一樣的。
int i不賦值一定固定嗎?
g++ initializeList.cpp
[[email protected] cpp]# ./a.out
i:3817460
[[email protected] cpp]# ./a.out
i:3817460
[[email protected] cpp]# ./a.out
i:5636084
[[email protected] cpp]# ./a.out
i:3817460
[[email protected] cpp]# ./a.out
i:3817460
其實有例外:執行幾次,有一次值就不一樣了。多執行幾次,還有。但是類物件的成員值執行多次就不變化,這也是巧合,或者類和普通型別不一樣?
這樣一來,對編譯原理和記憶體分配這塊就更費解了。
====================================================================================================
結論:也許某些書有參考答案,有進一步的說明,比如網友推薦的Effective C++,還有編譯原理,或者自己用編譯器分解編譯過程來研究,待考察。
相關推薦
C++複習筆記——初始化列表順序
參考測試程式碼1: #include<iostream> using namespace std; class A { public: A(int iniI,int iniJ,int iniK):i(iniI),j(i),k(iniK){}
C++ 建構函式初始化呼叫順序及類函式內部巢狀函式情況
C++建構函式初始化順序 C++建構函式按下列順序被呼叫:(1、2、3、4是按照優先順序順序來的!) (1)任何虛擬基類的建構函式按照它們被繼承的順序構造; (2)任何非虛擬基類的建構函式按照它們被繼承的順序構造; (3)任何成員物件的建構函式按照它們宣告的順序呼叫;(如果成員物件有前面出現
c++11之初始化列表
一、前言 C++的學習中,我想每個人都被變數定義和申明折磨過,比如我在大學筆試過的幾家公司,都考察了const和變數,型別的不同排列組合,讓你區別有啥不同。反正在學習C++過程中已經被折磨慣了,今天再來看看重溫下那段“輝煌的歷史”。先來看一段程式碼: Playe
C++建構函式初始化列表與建構函式中的賦值的區別
C++類中成員變數的初始化有兩種方式: 建構函式初始化列表和建構函式體內賦值。下面看看兩種方式有何不同。 成員變數初始化的順序是按照在那種定義的順序。 1、內部資料型別(char,int……指標等) class Animal { publ
< C++ > initializer list 初始化列表(建構函式後面加個冒號的解釋)
Keypoint : 呼叫父類的建構函式(一般為有參建構函式),初始化類中的成員。 C++ primer 5th edition: Remember When creating an object of a derived class, a program first
c++ 用引數初始化列表對資料成員初始化
除了使用建構函式可以對類中的成員變數進行初始化,還可以使用引數初始化列表。這種方法不在函式體內對資料成員初始化,而是在函式首部實現。這樣可以減少函式體的長度。 舉例如下: #include<string> using namespace std; class S
淺析C++中的初始化列表(區別賦值和初始化)
派生類不能直接訪問基類的私有成員,而必須通過基類方法進行訪問。 具體來說,派生類建構函式必須使用基類建構函式。 建立派生類物件時,程式首先建立基類物件。C++使用初始化列表完成這項工作。 RatedPlayer::RatedPlayer(int r, co
c++中初始化列表的初始化變量順序問題
bsp 變量 結果 請問 iostream 類的成員 sin vat 並不是 例題來看:請問下面程序打印出的結果是什麽? 1 #include <iostream> 2 #include <string> 3 4 using namesp
C++筆記 第二十課 初始化列表的使用---狄泰學院
如果在閱讀過程中發現有錯誤,望評論指正,希望大家一起學習,一起進步。 學習C++編譯環境:Linux 第二十課 初始化列表的使用 問題:類中是否可以定義const成員?可以 小實驗 下面的類定義是否合法?不合法 如果合法,ci的值是什麼,儲存在哪裡? 20-1 類中的c
C++:建構函式的初始化列表,以及初始化的順序
建構函式的初始化值列表: 對於物件的資料成員而言,初始化和賦值是有區別的。 當資料成員是 const 、引用,或者屬於某種未提供預設建構函式的類型別的話,就必須通過建構函式的初始值列表為這些成員提供初始值,否則就會引發錯誤。 // 錯誤:ci 和 ri 必須初始化 CobstRef::C
C++解析(12):初始化列表與物件構造順序
0.目錄 1.類成員的初始化 2.類中的const成員 3.物件的構造順序 3.1 區域性物件的構造順序 3.2 堆物件的構造順序 3.3 全域性物件的構造順序 4.小結 1.類成員的初始化 類中是否可以定義const成員? 下面的類定義是否合法?如果合法,ci的值是什麼,儲
《深度探索C++物件模型》筆記(三)建構函式、拷貝構造和初始化列表
歡迎檢視系列部落格: -------------------------------------------------------------------------------------------------------------- 看了這一章
C++構造函數對類成員變量初始化,使用初始化列表和構造函數內部直接賦值 的差別
初始化列表 不能 構造 調用 ron 二次 art size strong 初始化和賦值對內置類型的成員沒有什麽大的差別,像任一個構造函數都能夠。但有的時候必須用帶有初始化列表的構造函數: (1) 成員類型是沒有默認構造函數的類。若沒有提供顯式初始化時,則編譯器隱式
c++類 用冒號初始化對象(成員初始化列表)
函數 ace end -1 box 技術分享 對象 mage stream c++類 用冒號初始化對象(成員初始化列表) 成員初始化的順序不同於它們在構造函數初始化列表中的順序,而與它們在類定義中的順序相同 #include<iostream> int n=0
轉:C++類構造函數初始化列表
關於 elf 引用類型 類類型 聲明 編譯器 分隔 賦值操作符 失敗 構造函數初始化列表以一個冒號開始,接著是以逗號分隔的數據成員列表,每個數據成員後面跟一個放在括號中的初始化式。例如: class CExample { public: int a; fl
C++11 初始化列表(initializer_list)
clu amp space return ret 列表 stl容器 int stat C++11對原有的初始化列表(用花括號圍住的若幹個值)進行了大幅的擴展。以下寫法在C++11中都是被允許的: 1 int static_arr[5] = {1, 2, 3, 4};
C++中的初始化參數列表
圖片 .com 包含 自然 cout csharp pan 初始化列表 arp c++中以下幾種情況的變量的初始化不可以寫在構造函數裏,而是要寫在初始化參數列表中 1.const常量 class AA { public : const int num; publi
【校招面試 之 C++】第1題 為什麽優先使用構造函數的初始化列表
初始化 校招 操作 struct st2 使用 mage div 賦值運算 1.首先看一個例子: #include<iostream> using namespace std; class Test1 { public: Test1() // 無參
C++類建構函式為什麼要使用初始化列表?
class Class { int a; double b; string s; void execute(); }; \\第一種寫法 Class::Class(){ a = 0; b = 1.0; s = "stirng"; } \\第二種寫法 Class::Clas
C++中只能使用初始化列表(只能是初始化)不能使用賦值的情況
(1)當類中含有const(常量)、reference(引用)成員變數時,只能初始化,不能對它們進行賦值; 常量不能被賦值,只能被初始化,所以必須在初始化列表中完成,C++引用也一定要初始化,所以必須在初始化列表中完成。 (2)基類的建