文章目錄
- 構造函數再探
- 以下代碼共調用多少次拷貝構造函數
- 委托構造函數
- 概念
- 形式
- 匿名對象
- 友元
- 友元的聲明
- 友元類
- 令成員函數作為友元
- 函數重載和友元
- 注意
- 內部類
- 特性
- 類的const成員
- 可變數據成員
- 類的static成員
- 概念
- 關于static
- 靜態成員的類內初始化
- 靜態成員能用于某些普通成員不能的場景
構造函數再探
以下代碼共調用多少次拷貝構造函數
Widget f(Widget u)
{ Widget v(u);Widget w=v;return w;
}int main(){Widget x;Widget y=f(f(x));}
這道題我第一眼看,在調用f的時候,會用實參來構造對象u,在用對象u構建v,然后用v構建w,最后因為是傳值返回,會用w再構造一個臨時量,所以調用一次f會使用4次拷貝構造函數,最后再用這兩次f的返回值來構造y,應該是9次。
但是答案卻是7次,讓我十分不解,所以我去論壇搜索了一下,發現這里涉及到了編譯器的優化。
我們可以看到,在第一次調用f的結束的時候,會返回一個由w構造的臨時量,再將這個臨時量作為實參來初始化第二個f的形參u,(第二個f調用到return這一步時,返回一個由w構造的臨時量,再將這個臨時量作為實參來初始化拷貝構造函數的形參)編譯器覺得這一步有點多余,會將其優化為一步(第一次直接用w構造u,第二次直接用w構造y),所以每次調用f的時候其實只經過了3次的拷貝構造,最后再加上y的拷貝構造,一共是7次。
委托構造函數
概念
使用它所屬類的其他構造函數執行它自己的初始化過程,換言之,它把它自己的一些(或者全部)指責委托給了其他構造函數。
形式
有一個成員初始值的列表和一個函數體。成員初始值列表只有一個唯一的入口,即類名本身。
class Date
{
public:// 非委托構造函數Date(int year, int month, int day):_year(year), _month(month), _day(day) { }// 其余構造函數全部委托給上面的構造函數Date():Date(0,1,1){}Date(int i):Date(i,0,0){}Date(std::istream &is):Date(){is >> this->_year >> this->_month >> this->_day;}void pr(){cout << this->_year << ends << this->_month << ends << this->_day << endl;}
private:int _year;int _month;int _day;
};
本例中,共有四個構造函數。
第一個構造函數接受三個實參,使用這些實參初始化數據成員,然后結束工作。
第二個默認構造函數使用三參數的構造函數完成初始化過程,因為函數體為空可知無需再執行其他任務。
第三個構造函數接受一個實參,同樣委托給了三參數的構造函數。
第四個構造函數先委托給了默認構造函數,默認構造函數又接著委托給了三參數構造函數。當這些受委托的構造函數執行完后,接著執行std::istream &is構造函數體的內容。
ps:當一個構造函數委托給另一個構造函數時,受委托的構造函數的初始值列表和函數體依次被執行,然后控制權才會交還給委托者的函數體。
匿名對象
當我們想要調用對象中的一個方法,或者只是想在這一句使用這個對象,其他地方不再使用該對象的時候。如果我們直接構造一個對象使用,這無疑是一種很大的浪費,因為這個對象我們用了一次就扔了,不再需要了,而一個對象的生命周期是整個棧幀。
這時就需要用到匿名對象,匿名對象是一種臨時對象,它的生命周期只有使用它的那一行語句,執行完則立即銷毀
class Date
{int _year;int _month;int _day;
public:Date(int year = 2020, int month = 4, int day = 24){_year = year;_month = month;_day = day;cout << "gouzao" << this << endl;}void print(){cout << this->_year << endl;}~Date(){cout << "xigou" << this << endl;}
};int main()
{Date d1; //創建一個對象,生命周期為整個函數棧幀d1.print();Date().print();//創建一個匿名對象,生命周期只有這一行語句,實行完則立即調用析構函數Date d2; //創建一個對象,生命周期為整個函數棧幀d2.print();return 0;
}
運行結果:
友元
類中的private部分非類的成員是無法訪問的。但類可以允許其他類或者函數訪問它的非公有成員,方法是令其他類或者函數成為它的友元(friend)。
- 友元函數可訪問類的私有和保護成員,但不是類的成員函數
- 友元函數不能用const修飾
- 一個函數可以是多個類的友元函數
- 友元函數的調用與普通函數的調用和原理相同
友元的聲明
友元聲明只能出現在類定義的內部,但是在類內出現的具體位置不限。友元不是類的成員也不受它所在區域訪問控制級別的約束。一般來說,最好在類定義開始或結束前的位置集中聲明友元。
友元的聲明僅僅指定了訪問的權限,而非一個通常意義上的函數聲明。如果我們希望類的用戶能夠調用某個友元函數,那么我們就必須在友元聲明之外再專門對函數進行一次聲明。
ps:上述代碼想講的還是要知道友元聲明的作用是規定訪問權限,不能起到普通意義聲明的作用。
友元類
class A
{friend class Date;//將Date聲明為友元類,Date可以訪問A的所有成員變量
private:string str;
};
class Date
{
public:void Print(){cout << a.str << endl;//訪問a的私有成員}
private:int _year;int _month;int _day;A a;
};
令成員函數作為友元
如果不想讓整個類作為自己的友元,也可以只為那個需要訪問自己的private對象的函數提供訪問權限。當把一個成員函數聲明成友元時,必須明確指出該成員函數屬于哪個類:
class A;
class Date
{
public:void Print(A);private:int _year;int _month;int _day;
};class A
{friend void Date::Print(A);//將Date的成員函數Print聲明為友元,Print可以訪問A的所有成員變量
private:string str;
};void Date::Print(A a){cout << a.str << endl;//訪問a的私有成員
}
要想令某個成員函數作為友元,我們必須仔細組織程序的結構以滿足聲明和定義的彼此依賴關系。該例中,我們必須按照如下方式設計程序:
- 首先前向聲明A類(因為Print的聲明會用到)
- 定義Date類,聲明Print函數,但不能定義它(因為要訪問A的私有成員,A還沒有被定義好,無法訪問其私有成員)
- 定義A類,包括對Print的友元聲明
- 定義Print,此時它才可以使用A的成員
函數重載和友元
盡管重載函數的名字相同,但它們仍然是不同的函數。因此,如果一個類想把一組重載函數聲明成它的友元,需要對這組函數中的每一個分別聲明:
在A中并沒有關于Print(A, A)的友元聲明,因此其不能訪問A的私有成員。
注意
1.友元關系是單向的,不具有交換性。
Date為A的友元,可以訪問A的私有成員,但是A并不能訪問Date的
2.友元關系不能傳遞。
如果B是A的友元,C是B的友元,則不能說明C時A的友元。
內部類
有沒有想過,既然類的成員變量可以是自定義類型,那能不能在類中再構建一個類呢?
class Date
{
public:void Print(){cout << _year << endl;}private:class A{public:void Print(){cout << _data << endl;}private:int _data;};int _year;int _month;int _day;};
可以看到,這是可以的,但是這兩個類有什么關系嗎?
這個內部類其實是一個獨立的類,它不屬于外部類,同時外部類對它也沒有任何特權,但是它同時還是外部類的友元類,可以通過外部類的對象參數來訪問外部類的所有成員。
特性
- 內部類可以定義在外部類的public、protected、private都是可以的。
- 注意內部類可以直接訪問外部類中的static、枚舉成員,不需要外部類的對象/類名。
- sizeof(外部類)=外部類,和內部類沒有任何關系
類的const成員
因為對于類和對象,封裝性是一個很重要的東西,但是訪問限定符只對外部有影響,對自身的成員函數沒有影響,如果我們不想讓一個成員函數對類的成員進行修改,這時,就需要用const來修飾成員函數。
class Date
{
public://等價于 void print(const Date* this)void Print() const{cout << _year << '-' << _month << '-' << _day << endl;}
private:int _year;int _month;int _day;};
對于用const修飾的成員函數,需要將const放在最后面,來區分開const參數和const返回值。這里的const其實修飾的是該成員函數的this指針,所以該成員函數就無法對類的成員進行修改。
-
const對象可以調用非const成員函數嗎?
答案:不行,因為const對象的只能讀不能寫,而非const的成員函數則可讀可寫,使權限放大了,不行。 -
非const對象可以調用const成員函數嗎?
答案:可以,因為非const對象的可讀可寫,const成員函數只可讀,使權限縮小,可行。
-
const成員函數內可以調用其它的非const成員函數嗎?
答案:不行,因為const成員函數的this指針是常量,只可讀,而非const的成員函數則可讀可寫,使權限放大了,不行。 -
非const成員函數內可以調用其它的const成員函數嗎?
答案:可以,因為非const成員函數的this指針可讀可寫,const成員函數只可讀,使權限縮小,可行。
可變數據成員
有時會發生這樣一種情況,我們希望能修改類的某個數據成員,即使是在一個const成員函數內。可以通過在變量的聲明中加入mutable關鍵字實現這一目的。
一個可變數據成員永遠不會是const,即使它是const對象的成員。因此,一個const成員函數可以改變成員的值。
舉個例子,我們需要一個可變成員num來追蹤每個成員函數被調用了多少次:
class Date
{
public:void Print() const{num++;cout << num << endl;}private:mutable size_t num = 0;
};int main(int argc, char const *argv[]) {Date d;d.Print();d.Print();d.Print();return 0;
}
運行結果:
盡管Print是一個const成員函數,它仍然能夠改變num的值。因為num是個可變成員,因此任何成員函數,包括const函數在內都能改變它的值。
類的static成員
概念
有的時候類需要它的一些成員與類本身直接相關,而不是與類的各個對象保持關聯。這樣的成員叫做類的靜態成員。
關于static
我們通過在成員的聲明前加上關鍵字static使得其與類關聯在一起。對于用static修飾的成員函數,稱為靜態成員函數,成員變量稱為靜態成員變量。因為靜態的成員的生命域不在類中,在靜態區,所以靜態的成員只能在類外初始化。
- 靜態成員為所有類對象所共享,不屬于某個具體的實例
- 靜態成員變量必須在類外定義,定義時不添加static關鍵字,一個靜態數據成員只能定義一次
- 靜態成員函數可以在類內部也可以在類外部定義,在類外部定義時,不能添加siatic關鍵字(外部有static關鍵字表示這個函數在文件內有效)
- 類靜態成員亦可用 類名::靜態成員 或者 對象.靜態成員 或者 指向對象的指針->靜態成員 來訪問
- 靜態成員函數不與任何對象綁定在一起,它們不包含this指針(static成員函數也就不能被聲明成const,上章提到const是為了將this指針賦予底層const),也不能在static函數體內使用this指針,不包含this指針也就不能訪問任何非靜態成員
- 靜態成員和類的普通成員一樣,也有public、protected、private3種訪問類別,也可以具有返回值
- 靜態成員和全局變量雖然都存儲在靜態區,但是靜態成員的生命周期只在本文件中,而全局變量不是
因為靜態成員函數不屬于某個對象,所以它沒有this指針,無法訪問任何非靜態的成員,但是非靜態的成員函數具有this指針,可以不用通過作用域運算符直接使用靜態成員。
關于第二點和第三點的代碼示例:
class Date
{int db = 666;static int num;static double init();
public:static int print();void test(){db += db * num;// 成員函數不用通過作用域運算符就能直接使用靜態成員}
};
int Date::num = init(); // 定義并初始化一個靜態成員int main(int argc, char const *argv[]) {int r;r = Date::print(); // 使用作用域運算符訪問靜態成員Date d1;Date *d2 = &d1;r = d1.print(); // 通過Date的對象或引用r = d2->print(); // 通過指向Date對象的指針return 0;
}
關于第二點:
從類名開始,這條定義語句的剩余部分就都位于類的作用域之內了。因此,我們可以直接使用init函數,雖然其是私有的。
靜態成員的類內初始化
通常情況下,類的靜態成員不應該在類的內部初始化。然而,我們可以為靜態成員提供const整數類型的類內初始值,不過要求靜態成員必須是字面值常量類型的constexpr,初始值必須是常量表達式。
class Date
{constexpr static int num = 2*3; // num是常量表達式double db[num];
public:
};
如果某個靜態成員的應用場景僅限于編譯器可以替換他的值的情況,則一個初始化的const或者static constexpr不需要分別定義。相反,如果我們將它用于值不能替換的場景中,該成員必須有一條定義語句。
如果在類的內部提供了一個初始值,則在類外部成員的定義不能再指定一個初始值了:
即使一個常量靜態數據成員在類內部被初始化了,通常情況下也應該在類的外部定義一下該成員。
靜態成員能用于某些普通成員不能的場景
- 靜態數據成員可以是不完全類型(聲明之后定義之前)
- 靜態數據成員的類型可以就是他所屬的類類型。而非靜態數據成員只能聲明成他所屬類的指針或引用
- 可以使用靜態成員作為默認實參
非靜態數據成員不可以,因為它的值本身屬于對象的一部分這么做的結果是無法真正提供一個對象以便從中獲取成員的值,最終將引發錯誤。
(圖中error:非靜態數據成員“Date::test”的使用無效)