1. 再談構造函數(shù)
1.1. 引入
1.1.1問題引入
class Date
{
public:
private:
int _year;
int _month;
int _day;
};
通過前面所學的知識, 我們知道了_year
, _month
, _day
這三個變量都是一些聲明, 并沒有開辟空間, 不是定義.
Date d1;
這一個操作就是給 d1這個對象整體定義, 但是對象整體定義,并不代表著里面的三個成員變量定義了.
???那么問題來了: 成員變量是在什么時候定義的??
1.1.2 const引入
class Date
{
public:
private:
int _year;
const int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
當我們用const 來修飾一下其中的一個成員變量, 那么會怎么樣了?
???這樣的報錯是什么意思? — 具有未初始化的常量限定數(shù)據(jù)成員
-
其實我們可以發(fā)現(xiàn)一些端倪: 就是const 修飾的變量有一個特點就是 定義的時候一定要初始化, 那么有些老鐵就會說 在里面給 const修飾的變量一個缺省值試一試
???這些不是聲明嘛, 給一個缺省值為什么就可以了? 原理是什么啊? 還有就是傳一個缺省值是傳給誰啊? - 其實, 初始化列表可以看成成員變量定義的地方. 而我們給的缺省值也是給初始化列表用的. 缺省, 缺省, 說實話就是一個備胎, 如果我們在初始化列表中給了這個變量的值? 那么就不會用這個缺省值了, 而去用我們在初始化列表中的值.
1.2 正篇
在構造函數(shù)中,初始化成員變量有兩種方式:
構造函數(shù)體賦值
和初始化列表
其實, 這兩種方式是有所不同的, 通過下面的一些比較就能看出他們的不同
1.2.1 構造函數(shù)體賦值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_year = 2022;
_month = month;
_month = 5;
_day = day;
_day = 20;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 5, 16);
return 0;
}
???延續(xù)上面的問題, 這個是不是初始化?
-
其實答案很明顯, 這個是并不是初始化. 因為每一個變量只有一次初始化, 但是可以有多次賦值. 通過上面的代碼, 我們可以發(fā)現(xiàn):
在構造函數(shù)內(nèi)部可以進行多次的賦值, 而初始化只有一次, => 構造函數(shù)不是成員變量初始化的地方
1.2.2 初始化列表
1.2.3.1 淺淺認識
初始化列表: 以一個冒號開始
, 接著用一個逗號
去分隔
數(shù)據(jù)成員列表, 每一個 成員變量后面跟一個放在括號
中的初始值或者表達式
class Date
{
public:
// 構造函數(shù)
Date()
// 此部分是 初始化列表
:_year(2023)
// 此部分是 構造函數(shù)體賦值
{
_month = 5;
_day = 16;
}
private:
const int _year; // const 修飾的成員變量
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
- 注意:
- 每個成員變量在初始化列表中
最多出現(xiàn)一次
(因為只能初始化一次) - 類中包含以下的成員時, 必須放在初始化列表中進行初始化:
- const 修飾的成員變量
- 引用修飾的成員變量
- 沒有默認構造函數(shù)的自定義類型
- 每個成員變量在初始化列表中
1.2.3.2 構造函數(shù)的 行走順序
構造函數(shù)初始化成員變量有兩種形式:
構造函數(shù)體賦值
和初始化列表
一個是賦值
, 一個是初始化
? 由此不難看出, 編譯器先走的是 初始化列表然后才是構造函數(shù)體賦值
???如果沒有初始化列表, 編譯器會走初始化列表這一步嗎?
-
猛一看, 感覺這個問題是不是有問題; 仔細一想, 其實這個問題問的很有深度~~. 換一句話說, 其實這個問題是想問 ?
初始化列表是構造函數(shù)必走的一步嗎?
通過下面的代碼, 驗證一番:
class Date
{
public:
Date()
{
_month = 5;
_day = 16;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
const int _year = 5;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print(); // 5 5 16
return 0;
}
class Date
{
public:
Date()
:_year(2023)
{
_month = 5;
_day = 16;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
const int _year = 5;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print(); //2023 5 16
return 0;
}
通過上面的兩個例子:
- 補丁 — — 成員變量給個缺省值, 這個缺省值其實傳給初始化列表的. 如果初始化列表沒有對此變量進行操作, 那么就會使用那個缺省值
- 初始化列表是構造函數(shù)必走的一步 — — 因為初始化列表是成員變量定義的地方
1.2.3.3 引用修飾成員變量
class A
{
public:
A(int a, int& b)
{
_a1 = a;
_a2 = b;
}
private:
int _a1;
int& _a2;
};
int main()
{
int n = 10;
A a(10, n);
return 0;
}
*****
error C2530: “A::_a2”: 必須初始化引用
*****
???有一些老鐵, 就會有一些疑問: 為什么這里的傳引用不能傳遞一個常量
? 就比如 A a(10, 10);
-
首先這樣寫會有報錯的:
在這里, 我們只需要看一下紅色的報錯
(藍色的報錯是這一類 的錯誤 — — 不能用構造函數(shù)體賦值來初始化 用引用修飾的成員變量):
因為 _a2 是 b的引用, b 是 10的引用 ? _a2 是 10的引用, 由于 10是一個常量, 所以引用都要用常量引用來接收, 否則就是引用的權限放大造成錯誤!!!
如果我們這樣修改的話, 就必須把成員變量里面也用 const來修飾此變量才可以. 不過這樣多不方便? 傳一個常量, 一直用, 還不能改變? 這不符合我們的需求啊~
通過上面的例子, 我們發(fā)現(xiàn): 使用構造函數(shù)體賦值這種方式是不行的!!
那么, 我們就采用初始化列表:
- _a2 = 10 — — _a2 是 b的引用, b 是 n的引用 ? _a2 是 n的引用. 因為初始化列表是成員變量定義的地方, 所以可以在此處進行對 引用修飾的_a2 進行初始化
- _a1 是一個隨機值 — — 發(fā)現(xiàn)傳參的 10沒用? 想告訴各位讀者的是, 我們傳參是有自己的目的性 和 選擇性; 如果我們不用, 對于內(nèi)置類型成員變量(當然也沒有缺省值)就會被初始化為隨機值.
1.2.3.4 沒有默認構造的自定義類型
class B
{
public:
// 無參調(diào)用 ==> 默認構造
B()
{
}
private:
int _b;
int _tem;
};
class A
{
public:
private:
int _a;
B bb;
};
int main()
{
A a1;
return 0;
}
上面的代碼是正確的, 因為A 類中有一個自定義類型的成語變量 bb. 構造函數(shù)是完成成員變量的初始化的, 對于 A類來說, 要完成 對 _a 和 bb( _b _tem) 的初始化
. 而要完成對 bb的初始化, 就需要 B類的默認構造. 如果 B類存在默認構造, 那么就會對 bb 進行初始化, 如果 B類不存在默認構造, 那么就不會對 bb進行初始化.??????
class B
{
public:
// 有參 ==> 就不存在默認構造
B(int x, int y)
{
_b = x;
_tem = y;
}
private:
int _b;
int _tem;
};
class A
{
public:
private:
int _a;
B bb;
};
int main()
{
A a1;
return 0;
}
上面, 我們故意對 B類的構造函數(shù)寫成了有參調(diào)用? 那么 B類就會失去默認構造=> B(_b _ tem) 就會是隨機值, 就不能完成對 B(_b _ tem) 的初始化了=> A類中就不能完成初始化
- 那我們試一試初始化列表??????
- 對
自定義類型成員變量
的初始化總結:
1.2.3初始化列表的 '坑'
成員變量在類中
聲明次序
就是在初始化列表中的初始化順序
, 與其在初始化列表中的先后次序無關
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
private:
int _a2;
int _a1;
};
int main()
{
A a(1);
}
???上面代碼的運行結果是什么?
-
_a2 是隨機值, _a1 = 1
因為聲明中的順序是_a2 _a1
, 那么編譯器在初始化列表初始化的時候就會先初始化 _a2, 再初始化_a1
?
初始化 _a2 的時候: _a2( _a1), 這時候_a1還沒有被初始化, 所以是一個隨機值; 初始化 _a1的時候: _a1(1), 那么 _a1就是 1嘍
建議— — 盡量按照聲明的順序來安排初始化列表中變量的初始化順序
初始化列表中, 每一個成員變量最多只能出現(xiàn)一次
class A
{
public:
A()
:_a1(5)
,_a2(10)
,_a1(8)
{}
private:
int _a2;
int _a1;
};
int main()
{
A a;
return 0;
}
- 初始化列表是每個成員變量定義的地方, 也是初始化的地方
1.2.4 談談初始化列表 和 構造函數(shù)
其實, 每一個初學者在這個地方都會停留一段時間.
初始化列表 和 構造函數(shù), 一不留神就會混淆了概念和作用
總的來說, 初始化列表是構造函數(shù)的一部分.構造函數(shù)的功能就是完成對成員變量的初始化工作, 對自定義類型會調(diào)用它的默認構造, 而對于內(nèi)置類型, 就不會進行處理.
完成這一個初始化工作, 有兩個方式: 初始化列表
和 構造函數(shù)體賦值
. 見名知義: 一個是初始化
, 一個是賦值
.
如果成員變量中 沒有 const修飾, 引用修飾 或者 沒有默認構造的自定義類型, 寫不寫初始化列表都是ok的, 只要有其中的一個存在, 就要寫一下初始化列表. 其實const修飾的成員變量也可以不用寫初始化列表(上面有例子, 不清楚的上去看看)
比較來說, 初始化列表可以完成的工作 > 構造函數(shù)體賦值 ? 我們一般建議使用初始化列表
. 當然有些工作時需要構造函數(shù)體賦值, 也是要寫構造函數(shù)體賦值的.
1.3 explicit關鍵字
1.3.1 引入
前面C語言的學習, 我們知道存在一種
類型轉換
. 由于C++能夠兼容C語言, 我們就大膽實驗??????
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
int main()
{
A a = 23;
return 0;
}
上面的代碼是可行的.
???為什么可以直接從一個 int類型
直接轉換成 A類的對象a
呢?? 中間出現(xiàn)了什么過程呢??
- 構造函數(shù)不僅可以構造與初始化對象, 對于單個參數(shù)或者第一個參數(shù)無默認值其余有默認值的構造函數(shù), 還具有類型轉換的作用
1.3.2 正篇
1.3.2.1 賦值的含義
???先有一個問題: 在C語言的學習中, 以int a = 10; double b = a;
為例子, 講述一下 類型轉換的過程
-
類型轉換不是直接就 把 a 的值 賦值給 b. 會生成一個
類型為 double的臨時變量, 記作 tem
, 然后把tem 賦值給 b
. 這里多說一句, 后面就出現(xiàn)了引用 &
, 就不會有臨時對象的生成.
那么延續(xù)上面的思路, 我們就會知道上面代碼的一個原理:
用 23去構造
了一個 類型為A的臨時對象, 記作tem, 然后把 tem 拷貝構造
給 a
???為啥換到這里就是 構造 和 拷貝構造了??
-
生成一個類型是 A的臨時對象 tem, 用 23去初始化tem — — 構造
用一個已知的對象tem 去初始化另一個對象 a — — 拷貝構造
1.3.2.2 探尋 '隱式類型轉換'
的真相
我們已經(jīng)知道隱式類型轉換的一個原理, 那么用代碼來驗證一番:
class A
{
public:
A(int a)
:_a(a)
{
cout << "調(diào)用了構造函數(shù)" << endl;
}
A(const A& x)
{
cout << "調(diào)用了拷貝構造函數(shù)" << endl;
}
private:
int _a;
};
int main()
{
A a = 23;
return 0;
}
*****
調(diào)用了構造函數(shù)
*****
???嗯??, 跟我們想的不一樣, 難道我們想錯了??
-
其實不然, 這里編譯器把它給優(yōu)化了.
我們知道: 構造 和 拷貝構造 的功能都是初始化
, 如果在同一行, 我們同時調(diào)用構造 + 拷貝構造
? 編譯器就只會調(diào)用一個構造函數(shù), 直接完成賦值~~
???老陳, 你空口無憑, 給我們看一下證據(jù)??
-
ok, 這就安排
由于,臨時對象具有常性
, 所以我們想到了用引用 &
來進行驗證 <==因為權限可以縮小 或 平移, 但是不能放大
先看下面的代碼??????
class A
{
public:
A(int a)
:_a(a)
{
cout << "調(diào)用了構造函數(shù)" << endl;
}
A(const A& x)
{
cout << "調(diào)用了拷貝構造函數(shù)" << endl;
}
private:
int _a;
};
int main()
{
A& a = 23;
return 0;
}
*****
error C2440: “初始化”: 無法從“int”轉換為“A &”
*****
class A
{
public:
A(int a)
:_a(a)
{
cout << "調(diào)用了構造函數(shù)" << endl;
}
A(const A& x)
{
cout << "調(diào)用了拷貝構造函數(shù)" << endl;
}
private:
int _a;
};
int main()
{
const A& a = 23;
return 0;
}
*****
調(diào)用了構造函數(shù)
*****
這里就變相地證明了: 上面的隱式類型轉換 是經(jīng)歷過了構造 + 拷貝構造, 不過是編譯器有優(yōu)化而已
注意: 不同的編譯器有不同的優(yōu)化, 所以看到不同的結果不必大驚小怪的~~
1.3.2.3 explicit關鍵字
在一些場景下, 隱式類型轉換會很方便(后面會學到的 string, STL… …),
但在另一些場景下, 我們又不希望隱式類型轉換的出現(xiàn)(后面會學到的 智能指針… …)
那我們?nèi)绾?code>不讓 隱式類型轉換 發(fā)生呢?? — — 答案就是explicit關鍵字
把explicit 放在 構造函數(shù)的前面就會不讓 隱式類型轉換發(fā)生文章來源:http://www.zghlxwxcb.cn/news/detail-470098.html
class A
{
public:
explicit A(int a)
:_a(a)
{
cout << "調(diào)用了構造函數(shù)" << endl;
}
A(const A& x)
{
cout << "調(diào)用了拷貝構造函數(shù)" << endl;
}
private:
int _a;
};
int main()
{
A a = 23;
return 0;
}
*****
error C2440: “初始化”: 無法從“int”轉換為“A”
message : class“A”的構造函數(shù)聲明為“explicit”
*****
大鵬一日同風起,扶搖直上九萬里文章來源地址http://www.zghlxwxcb.cn/news/detail-470098.html
到了這里,關于C嘎嘎~~[構造函數(shù)提升篇]的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!