目錄
1. 類型轉換
非explicit的單參數構造函數
示例?
explicit的單參數構造函數
示例
不同版本的行為?
示例 (單參數)
示例(多參數且其余參數有默認值 )
?示例(多參數且無默認值)
2. static成員變量
例題1
例題2?
3. 友元
4. 內部類
例題
5. 匿名對象
示例?
1. 類型轉換
C++支持將內置類型隱式轉換為類類型對象,需要非explicit的單參數構造函數或者多參數但其余參數有默認值的構造函數。
非explicit的單參數構造函數
示例?
假設定義一個 Length ?類來表示長度,單位是米,它有一個接受 int ?類型參數的構造函數用于設置長度數值:
#include <iostream> using namespace std;class Length { public:// 接受int類型參數的構造函數Length(int meter) : length(meter){}void show() const {cout << "長度為: " << length << " 米" << endl;} private:int length; };int main() { // 復制初始化,將int類型的5轉換為Length類型的對象lenLength len = 5; //想要創建一個表示長度為5米的length對象len len.show();return 0; }
這里 int ?類型的 5 ?可以通過 Length ?類的構造函數隱式轉換為 Length ?類型的對象,使用 Length len = 5; ?這種復制初始化的方式,代碼看起來就像是把一個整數值賦給了 Length ?類型的對象,比較符合常規的賦值思維習慣,簡單易懂。相比之下,直接初始化 Length len(5); ?雖然功能一樣,但復制初始化在這種類型轉換很自然的場景里,從寫法上會給人一種更直觀的感覺。
explicit的單參數構造函數
示例
如果構造函數被聲明為explicit ,就不再支持隱式類型轉換,僅允許直接初始化。
#include <iostream> using namespace std;class MyClass { public: explicit MyClass(int value) // explicit 修飾構造函數,禁止隱式轉換: data(value) {} private:int data; };int main() {MyClass obj(10);// 正確,顯式調用構造函數 // MyClass obj = 20; 錯誤! 因為 MyClass 的構造函數是 explicit,禁止隱式轉換return 0; }
不同版本的行為?
- C++98/03,Length len = 5; 復制初始化的處理方式是:先使用5調用構造函數 Length(int meter) 生成一個臨時的Length類型的對象,然后再通過拷貝構造函數將臨時對象復制到 len。允許編譯器優化省略。
- C++11,優化為直接構造。編譯器必須省略臨時對象和拷貝構造函數的調用,Length len = 5; 編譯器會直接調用構造函數初始化 len,提高了程序效率。
示例 (單參數)
class MyClass
{
public:MyClass(int value) // 非 explicit,允許隱式轉換: data(value){} // 無拷貝構造函數(編譯器會默認生成)
private:int data;
};void func(MyClass obj)
{/* ... */
}int main()
{MyClass obj = 10; // 隱式轉換:int → MyClass//C++98/03行為:先調用構造函數生成臨時的MyClass類型對象,再通過拷貝構造函數(若用戶未定義,編譯器默認生成一個公有的)將臨時對象賦值給obj(這里臨時對象生命周期僅用于初始化obj,拷貝完成后銷毀)//C++11及以后行為:允許直接構造obj而不生成臨時對象,不調用拷貝構造函數func(20); // 隱式轉換:int → MyClass//C++98/03行為:(兩次開銷)先生成臨時的MyClass類型對象,再通過拷貝構造(若用戶未定義,編譯器默認生成一個公有的)函數將臨時對象傳遞給func函數的形參obj//C++11及以后行為:(一次開銷)允許直接在函數func的形參obj的存儲位置(函數棧幀)構造MyClass對象,而不生成臨時對象return 0;
}
class MyClass
{
public:MyClass(int value) // 非 explicit,允許隱式轉換: data(value){}// 無拷貝構造函數(編譯器會默認生成)
private:int data;
};void func(const MyClass& a)//參數為引用類型
{/* ... */
}int main()
{MyClass obj = 10; func(20); //不管是C++98/03還是C++11及以后,當參數是引用類型時,必須創建臨時對象,然后綁定到引用上,臨時對象生命周期延長至函數結束return 0;
}
示例(多參數且其余參數有默認值 )
#include <iostream>
using namespace std;
class A
{
public:// 多參數但其余參數有默認值的構造函數A(int a, int b = 4) : value(a + b) {cout << value << endl;}private:int value;
};int main()
{ A obj2 = 10; //這里 10 是int類型,隱式轉換為A類型,調用多參數(有默認值)構造函數return 0;
}
?示例(多參數且無默認值)
#include <iostream>
using namespace std;
class B
{
public:// 多參數且無默認值的構造函數B(int a, int b) : num(a + b) {cout << num << endl;}private:int num;
};int main()
{// 不能隱式轉換,顯式調用構造函數進行類型轉換B obj = B(3, 5);//理論上B(3, 5)復制初始化 先調用構造函數創建臨時對象,再調用拷貝構造函數將臨時對象賦值給obj,實際上進行了優化//不產生臨時對象,也不調用拷貝構造函數,等價于直接初始化 B obj(3,5),二者在效果和性能上完全一致return 0;
}
#include <iostream>
using namespace std;
class Point
{
public:Point(int x, int y) : m_x(x), m_y(y) {}void print() const {std::cout << "(" << m_x << ", " << m_y << ")";}private:int m_x;int m_y;
};void displayPoint(const Point& p)
{p.print();std::cout << std::endl;
}int main()
{// 1.顯式構造Point對象后傳遞displayPoint(Point(10, 20));//Point(10, 20)調用構造函數創建臨時對象,這個臨時對象直接綁定到函數的參數p上// 2.使用列表初始化語法,編譯器隱式調用構造函數displayPoint({ 30, 40 });//編譯器看到這種形式,知道接受一個Point類型引用參數,Point類有構造函數,此時編譯器會隱式地使用{10,20}作為參數調用構造函數,創建臨時對象//3.直接初始化然后再傳Point p(5, 5);displayPoint(p);return 0;
}
2. static成員變量
- 用static修飾的成員變量,稱之為靜態成員變量,靜態成員變量必須在在類外進行初始化。
- 所有對象共享同一份靜態成員變量,它存儲在全局數據區,而不是在每個對象的內存空間中。
- 用static修飾的成員函數,稱之為靜態成員函數,靜態成員函數沒有this指針。
- 靜態成員函數中可以訪問其他的靜態成員,但是不能訪問非靜態成員,因為沒有this指針。
- 非靜態的成員函數,可以訪問任意的靜態成員變量和靜態成員函數。
- 突破類域就可以訪問靜態成員,可以通過類名::靜態成員或者對象.靜態成員來訪問靜態成員變量和靜態成員函數。
- 靜態成員也是類的成員,受public、protected、private訪問限定符的限制。
- 靜態成員變量不能在聲明位置給缺省值初始化,因為缺省值是給構造函數初始化列表的,靜態成員變量不屬于某個對象,不走構造函數初始化列表。
示例:?
//實現一個類,計算程序中創建出了多少個類對象?
#include<iostream>
using namespace std;
class A
{
public:A(){++_scount;}A(const A& t){++_scount;}~A(){--_scount;} //靜態成員函數static int GetACount(){//_a++; error! 靜態成員函數不能訪問非靜態成員,因為沒有this指針return _scount;}void func(){cout << _scount << endl;cout << GetACount() << endl;}
private:int _a = 4;//類里面聲明static int _scount;
};//類外?初始化
int A::_scount = 0;int main()
{cout << A::GetACount() << endl;A a1, a2;A a3(a1);cout << A::GetACount() << endl;cout << a1.GetACount() << endl;//cout << A::_scount << endl; 編譯報錯:error C2248 : “A::_scount” : ?法訪問private成員(在“A”類中聲明) 受訪問限定符限制!return 0;
}
例題1
?鏈接[?求1+2+3+……n]
?
class Sum
{
public:Sum(){_ret+=_i;++_i;}static int Getret(){return _ret;}
private:static int _i;static int _ret;};int Sum:: _i=1;int Sum:: _ret=0;class Solution {
public:int Sum_Solution(int n) {// Sum a[n]; 變長數組Sum*p=new Sum[n];return Sum::Getret();}
};
例題2?
設已經有A,B,C,D 4個類的定義,程序中A,B,C,D構造函數調用順序為(C->A->B->D)
設已經有A,B,C,D 4個類的定義,程序中A,B,C,D析構函數調用順序為(B->A->D->C)
C c; int main() {A a;B b;static D d;return 0; }
解析:
變量c是全局對象,其構造函數最先調用;在main函數中先聲明A a; 然后聲明B b; 所以A的構造函數先于B調用;D d;是靜態局部對象,其構造函數在main函數首次執行到該聲明處調用,在A和B之后。所以順序為:C->A->B->D
非靜態局部對象a、b 析構時機是離開main函數作用域時,順序為構造的反序(B->A);靜態對象(全局c和靜態局部d)析構時機是程序結束時,順序為構造的反序(全局C先構造,靜態局部D后構造,故析構順序為D->C)。所以順序為B->A->D->C
3. 友元
一般來說,類的私有成員外部是不能訪問的,而友元函數在一些特殊場景下很有用,比如需要在類外部的函數中訪問類的私有數據進行特定操作,又不想把這些數據設為公有成員破壞封裝性,就可以將該函數聲明為友元函數。
- 友元提供了一種突破類訪問限定符封裝的方式,友元分為:友元函數和友元類,在函數聲明或者類聲明的前面加 friend,并且把友元聲明放到一個類的里面。
- 外部友元函數可訪問類的私有和保護成員,友元函數僅僅是一種聲明,他不是類的成員函數。
- 友元函數不屬于類的成員函數,沒有 this ?指針。調用時和普通函數一樣,直接使用函數名調用。
- 友元函數可以在類定義的任何地方聲明,不受類訪問限定符限制。
- 友元類的關系是單向的,不具有交換性,比如A類是B類的友元,但是B類不是A類的友元。
- 友元類關系不能傳遞,如果A是B的友元,B是C的友元,但是A不是C的友元。
- 一個函數可以是多個類的友元函數。
- 友元提供了便利,但是友元會增加耦合度,破壞了類的封裝性,所以友元不宜多用,使用時要謹慎。
#include<iostream>
using namespace std;class B; //前置聲明,否則編譯器在A類中編譯時會因為不知道B是什么類型而報錯,告訴編譯器B是一個類
class A
{//友元聲明
friend void func(const A& aa, const B& bb);
private:int _a1 = 1;int _a2 = 2;
};class B
{
//友元聲明
friend void func(const A& aa, const B& bb);
private:int _b1 = 3;int _b2 = 4;
};void func(const A& aa, const B& bb)//func函數可以是多個類的友元函數
{cout << aa._a1 << endl;cout << bb._b1 << endl;
}int main()
{A aa;B bb;func(aa, bb);return 0;
}
當一個類被聲明為另一個類的友元類時,那么這個類的所有成員函數都可以訪問另一個類的私有和保護成員,無需為每個成員函數單獨聲明friend關鍵字。?
#include<iostream>
using namespace std;class A
{//友元聲明
friend class B;// B整體是A的友元類,友元類的聲明是對整個類的授權,B類的所有成員函數都可訪問A的私有成員private:int _a1 = 1;int _a2 = 2;
};class B
{
public:void func1(const A& aa){cout << aa._a1 << endl;cout << _b1 << endl;}void func2(const A& aa){cout << aa._a2 << endl;cout << _b2 << endl;}private:int _b1 = 3;int _b2 = 4;
};int main()
{A aa;B bb;bb.func1(aa);bb.func2(aa);return 0;
}
4. 內部類
如果一個類定義在另一個類的內部,這個內部類就叫做內部類。內部類是一個獨立的類,跟定義在 全局相比,它只是受外部類類域限制和訪問限定符限制,所以外部類定義的對象中不包含內部類。內部類默認是外部類的友元類。
#include<iostream>
using namespace std;class A
{
private:static int _k;int _h = 1;
public:class B //B默認就是A的友元{public:void foo(const A& a){cout << _k << endl; cout << a._h << endl; }private:int _b = 1;};
};int A::_k = 1; //初始化靜態成員變量int main()
{cout << sizeof(A) << endl; //4 非靜態成員_hA::B b;//B類的作用域在A類內部,在外部使用B類時需要指定其所屬的外部類域A aa;b.foo(aa);return 0;
}
內部類本質也是一種封裝,當A類跟B類緊密關聯,A類實現出來主要就是給B類使用,那么可以考慮把A類設計為B的內部類,如果放到private/protected位置,那么A類就是B類的專屬內部類,其他地方都用不了。?
例題
?鏈接[?求1+2+3+……n]?
class Solution
{
private:static int _i;static int _ret;class Sum //內部類{public:Sum(){_ret+=_i;++_i;} };public:int Sum_Solution(int n) {Sum a[n];//變長數組// Sum*p=new Sum[n];// delete []p;return _ret;}};int Solution:: _i=1;int Solution:: _ret=0;
5. 匿名對象
用類型 (實參)定義出來的對象叫做匿名對象,相比之前定義的類型 對象名(實參)定義出來的叫有名對象。
匿名對象通常是臨時對象,它們在表達式結束后會被自動銷毀。
示例?
#include<iostream>
using namespace std;class A
{
public:A(int a = 0):_a(a){cout << "A(int a)" << endl;}~A(){cout << "~A()" << endl;}
private:int _a;
};class Solution
{
public:int Sum_Solution(int n) {//...return n;}
};int main()
{//有名對象//A aa1(); 不能這么定義對象,因為編譯器無法識別這是是?個函數聲明,還是對象定義 A aa1;cout << endl;A aa2(2);cout << endl; //匿名對象,匿名對象的特點不?取名字,但是它的?命周期只有這一行,我們可以看到下一行他就會自動調用析構函數A();cout << endl;A(1);cout << endl;//匿名對象在這樣場景下就很好?:/*Solution sl;cout << sl.Sum_Solution(10) << endl;*///為了更方便cout << Solution().Sum_Solution(10) << endl;return 0;
}
運行結果: