模板的進階:
非類型模板參數
????????是C++模板中允許使用具體值(而非類型)作為模板參數的特性。它們必須是編譯時常量,且類型僅限于整型、枚舉、指針、引用。(char也行)
? ? ? ? STL標準庫里面也使用了非類型的模板參數。
// 非類型模板參數示例:固定大小的數組
template <typename T, int Size>
class FixedArray
{
private:T data[Size];
public:T& operator[](int index) {return data[index];}constexpr int getSize() const { return Size; }
};int main() {FixedArray<int, 5> arr; // Size=5在編譯時確定arr[2] = 42;static_assert(arr.getSize() == 5);
}
????????在 C++ 標準中,非類型模板參數不能直接使用 std::string
,但可以使用字符數組或字符指針的形式間接實現類似效果。
template<const char* str>
struct MyTemplate { /* ... */ };// 定義外部鏈接的字符數組(C++17 起可用)
extern const char my_str[] = "Hello";
MyTemplate<my_str> obj; // 合法
模板特化
????????是C++中針對特定類型或條件提供定制化模板實現的技術,就是模板的特殊化處理,特化不能單獨存在。它分為全特化和偏特化,兩種形式。
? ? ? ? 這里最后一次比較,其實變成了指針與指針比較,比較的是地址的大小。那么這里如果要讓指針里面的數據進行比較,那么就要用到模板的特化。
template<class T>
bool Greater(T left, T right)
{return left > right;
}template<>
bool Greater<Date*>(Date* left,Date* right)
{return *left > *right;
}
? ? ? ? 這樣走Date* 比較的時候,就會走第二個模板函數。
? ? ? ? 除了函數能特化,類也可以特化。
template<class T>
struct Less
{bool operator()(const T& x1,const T& x2) const{return x1 < x2;}
};template<>
struct Less<Date*>
{bool operator()(Date* x1,Date * x2) const {return *x1 < *x2;}
};
? ? ? ? 上面仿函數也可以使用class,但是要注意在public下面操作,不然調不到函數。
????????
? ? ? ? 這里使用STL庫里面的優先級隊列,用自己寫的模板特化,發現也是可以使用的。
偏特化
????????允許為模板參數的一部分或特定條件提供特殊實現。它適用于類模板,但不支持函數模板。
// 通用模板
template <class T, class U>
class MyClass
{
public:void print() { cout << "General template\n"; }
};// 偏特化:當第二個參數為 int 時
template <class T>
class MyClass<T, int>
{
public:void print() { cout << "Partial specialization (U = int)\n"; }
};// 偏特化:當兩個參數均為指針類型時
template <class T, class U>
class MyClass<T*, U*>
{
public:void print() { cout << "Partial specialization (both pointers)\n"; }
};// 使用示例
int main()
{MyClass<double, char> a; // 通用模板MyClass<float, int> b; // 偏特化(U = int)MyClass<int*, double*> c; // 偏特化(指針類型)a.print(); // 輸出: General templateb.print(); // 輸出: Partial specialization (U = int)c.print(); // 輸出: Partial specialization (both pointers)
}
template <class T, class U>
class MyClass<T&, U&>
{
public:void print() { cout << " T& , U& \n"; }
};
模板不支持分離編譯
????????聲明(.h),定義(.cpp)分離。
?PS:模板在同一文件下可以類外定義。
????????在類模板中使用typename
關鍵字加在內嵌類型(iterator)前是為了告訴編譯器該名稱是一個類型,而非靜態成員或變量。這種情況發生在模板參數未實例化時,當訪問嵌套的依賴類型時,必須使用typename
消除歧義。
????????zs : : vector<T> : : iterator ,這里要加 typename 。不然編譯器區分不清楚這里是類型還是變量。因為靜態函數、變量也可以指定類域就可以訪問。
? ? ? ??
? ? ? ? 這里把push_back() 分開定義后,使用出現鏈接錯誤。
? ? ? ? 因為構造函數、size()函數、operator[ ],在vector.h中有定義,所以vector 實例化 v 的時候,這些成員函數同時實例化,直接就有定義。編譯階段直接確定地址。
? ? ? ? 而 push_back()、insert()在 vector.h 中只有聲明,沒有定義。那么只有在鏈接階段去確認地址。????
? ? ? ? 但是這里 vector.cpp 中模板類型的 T 無法確定,所以沒有實例化,就無法進入符號表。進入不了符號表后面鏈接階段就會發生錯誤。
根本原理:
C++標準規定模板是編譯期多態機制,編譯器需要根據調用處的具體類型生成代碼。若模板實現不可見(如分離到.cpp文件),則無法完成實例化。
解決方案:
1.模板聲明和定義不要分離到 .h 和 .cpp (推薦)。
2.在cpp 顯示實例化。(不推薦,換個類型就要實例化一次,麻煩)
模板總結:
一、優點:
- 類型安全:模板在編譯期進行類型檢查,比宏更安全(如
std::vector<int>
只能存儲int類型)- 代碼重用:通過泛型編程減少重復代碼(如一個
max()
模板可處理int/double/string等類型)- 零運行時開銷:模板實例化在編譯期完成,無額外運行時成本
- 高性能泛型算法:STL算法(如sort)能針對不同類型生成優化代碼
二、缺點:
- 編譯錯誤晦澀:類型不匹配時錯誤信息冗長(如缺少某個成員函數的錯誤可能長達數十行)
- 編譯時間膨脹:每個模板實例化都會生成新代碼,大型項目編譯時間顯著增加
- 代碼膨脹風險:多個類型實例化可能導致二進制文件增大(如vector<int>和vector<string>會生成兩份代碼)
- 調試困難:調試器難以跟蹤模板實例化代碼
C++的繼承:
????????C++繼承是面向對象編程的核心機制之一,允許派生類復用基類的屬性和方法,同時擴展或修改其行為。
? ? ? ? 一個學校里面,人有很多種身份,比如學生、老師、校長、保潔工作人員等。他們有共同的特點也有不同的地方。那么如果對每個人單獨的來寫一份代碼以表明其全部特征,那么代碼會非常的冗余。
? ? ? ? 因為其作為人這個個體在很多的特征上是相似的,那么使用C++的繼承就可以很好的解決這方面的問題。
// 基類:個人
class Person
{
public:string name;int age;string gender;void display() const {cout << "姓名: " << name << "\n年齡: " << age<< "\n性別: " << gender << endl;}
};// 學生類
class Student : public Person
{
private:string studentID;
};// 教師類
class Teacher : public Person {
private:string employeeID;
};
????????繼承允許一個類(派生類,student,teacher )基于另一個類(基類,Person )來創建,從而獲得基類的屬性和方法,同時可以添加新的特性或覆蓋已有的方法。?
????????基本語法,派生類通過冒號后跟訪問說明符(如public、protected、private)和基類名稱來繼承基類。
????????
? ? ? ? 派生類既有基類的屬性 (name、age、?gender),也有自己拓展的屬性(?studentID、employeeID )。
代碼復用:繼承允許派生類直接使用基類的成員(變量和函數),避免重復編寫相同邏輯。
層次化建模:通過繼承表達現實世界的分類關系(如"動物→哺乳動物→狗"),使代碼結構更符合邏輯認知。
繼承方式:
????????public、protected和private繼承。public繼承是最常用的,它保持基類成員的訪問權限不變。protected繼承會將基類的public和protected成員變為派生類的protected成員。private繼承則將所有基類成員變為派生類的private成員。這些不同繼承方式會影響派生類及其后續派生類對基類成員的訪問權限。
????????
? ? ? ? 其實有規律,直接由權限更小的那個控制。實際上就只有public繼承用的比較多。
? ? ? ? protected\priveate:類外不能訪問,類里面可以訪問。
? ? ? ? 不可見:隱身,類里面外面都無法訪問。
私有成員的意義:不想被子類繼承的成員。
基類中想給子類復用,但是又不想暴露直接訪問的成員,就應該定義成保護
class Parent
{
public:string name = "Parent";
};class Child : public Parent
{
public:string name = "Child"; // 隱藏父類的namevoid printNames() {cout << "子類 name: " << name << endl; // 輸出 Childcout << "父類 name: " << Parent::name << endl; // 輸出 Parent}
};int main()
{Child obj;obj.printNames();return 0;
}
?
? ? ? ??當子類和父類都定義了同名成員變量name
時,子類會隱藏父類的同名成員。若需訪問父類成員,需通過作用域解析運算符顯式指明。
1.在繼承體系中基類和派生類都有獨立的作用域。
2.子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。(在子類成員函數中,可以使用 基類::基類成員? 顯示訪問)
?????????實際上的內存存儲中,子類對象包含了基類的數據(name)。.
????????若父類的name
是private
:
????????此時子類定義的name
是獨立的新成員,不會產生命名沖突(但父類成員仍然存在,只是不可直接訪問)。
class A
{
public:void func(){cout << "fucn()" << endl;}
};class B : public A
{
public:void func(int i){cout << "func(int i)->" << endl;}
};
? ? ? ??
? ? ? ? 有的地方就會提問這里兩個 func() 是不是構造函數重載?因為這里是兩個不同的類域,所以其構成隱藏。
????????當子類對象賦值給父類對象時,會發生對象切片(切割)。這個過程會自動截斷子類特有的成員,只保留父類部分。
????????子類對象可以賦值給父類對象/指針/引用。這里雖然是不同類型,但是不是隱式類型轉換。賦值兼容轉換,第三個就能看出來,這里是個特殊支持,語法天然支持。
????????子類切割對象賦值給父類,但是不能把父類對象反向賦值給子類對象。
? ? ? ? 指針或者引用,其指向父類那一部分的地址或者是那一部分的別名。
繼承關系中的默認函數表現:
子類編譯器默認生成的 構造函數:
????????1、自己的成員,跟類和對象一樣。內置類型不處理,自定義類型調用它自己的默認構造。
????????2、繼承父類成員,必須調用父類的構造函數初始化。
class Person
{
public:string _name;string _sex;int _age;Person(const char* name):_name(name),_sex("男"),_age(10){cout << "Person()" << endl;}
};// 學生類
class Student : public Person
{
public:int _num;Student(const char* name, int num):Person(name) /*顯式調用父類構造函數*/ , _num(num){cout << "Studet()" << endl;}};int main()
{Student s1("張三",001);return 0;
}
? ? ? ? 注意上面子類再初始化列表里,調用父類的構造函數,初始化繼承成員。
子類編譯器生成默認生成的 拷貝構造:
1、自己成員,跟類和對象一樣。內置類型值拷貝,自定義類型調用它的拷貝構造。
2、繼承的父類成員,必須調用父類拷貝構造初始化。
Person(const Person& p):_name(p._name),_sex(p._sex),_age(p._age){cout << "Person(const Person& )" << endl;}Student(const Student& s):Person(s) /*子類成員拷貝*/,_num(s._num){cout << "Studet(const Student& )" << endl;}int main()
{Student s1("張三",001);Student st2(s1);return 0;
}
?
????????這里子類拷貝構造函數(Student(const Student& s)
)初始化列表位置,傳入基類拷貝構造函數(Person(const Person& p)
)的參數,直接使用了子類對象(s)。這里其實是前面切片的應用。
子類編譯器默認生成的 賦值運算符:
1、自己成員,跟類和對象一樣。內置類型值拷貝,自定義類型調用它?operator=
的。
2、繼承的父類成員,必須調用父類的?operator=?
Person& operator=(const Person& p)
{cout << "Person& operator=(const Person& )" << endl;_name = p._name;_sex = p._sex;_age = p._age;return *this;
}Student& operator=(const Student& s)
{if (this != &s){Person::operator=(s); // 調用父類賦值_num = s._num;}cout << "Student& operator=(const Student& )" << endl;return *this;
}int main()
{Student s1("張三",001);Student st2(s1);Student st3("李四",002);st2 = st3;return 0;
}
?
? ? ? ? 這里有個注意的點,調用父類的賦值運算符時要使用類域指定,如果不指定類域,子類默認會去調用自己的賦值運算符,構成死循環。
子類編譯器默認生成的 析構函數:
1、自己的成員內置類型不處理,自定義類型調用它的析構函數。
2、繼承父類的成員,調用父類析構函數處理。
~Person()
{cout << "~Person" << endl;
}~Student()
{Person::~Person();cout << "~Student() " << endl;//...
}
???????子類的析構函數跟父類的析構函數構成隱藏。直接調用調不到,要指定類域。??
????????
? ? ? ? 這里會發現Peson的構造函數調用了三次但是,析構函數調用了六次。這里其實是因為析構函數很特殊。不需要去顯示的掉用基類的析構函數,編譯器會自己自動的去調用。
~Student()
{//Person::~Person(); //不用顯示調用cout << "~Student() " << endl;//...
}
? ? ? ? 這是因為其數據存儲結構,比如一個子類 (Student),它會先存儲父類(Person)的成員,然后在下面存儲自己的成員。因為其數據存在棧幀上的,要遵循后進先出規則,所以后構造的先析構(與構造順序相反,子類數據后構造),先調用派生類的析構函數,再調用基類的析構函數。
? ? ? ? 每個子類析構函數后面會自動調用父類析構函數,這樣才能保證先析構子類,再析構父類。(自己手寫編譯器無法保證順序)
繼承和友元:
? ? ? ? PS:友元關系不能被繼承。基類的友元不會自動成為派生類的友元。
? ? ? ? 如果想訪問子類的私有數據,設置為子類的友元就行。
繼承中靜態成員的作用與訪問規則
????????靜態成員(靜態變量、靜態方法)屬于類本身,而非類的實例。所有實例共享靜態成員,且通過類名直接訪問。
class Person
{
public:Person() { ++_count; }string _name;static int _count; //靜態成員變量
};int Person::_count = 0;class Student : public Person
{
protected:int _num;
};
????????共享性:父類的靜態成員會被子類繼承,但子類與父類共享同一份靜態成員。
class Parent
{
public:
static int value;
};class Child : public Parent
{
public:
static int value;
};Parent::value = 10; // 父類靜態成員
Child::value = 20; // 子類靜態成員(隱藏父類的同名成員)
????????隱藏:若子類定義了同名靜態成員,父類的靜態成員會被隱藏,但未被覆蓋(通過父類名仍可訪問)。
多繼承:
單繼承:單繼承指一個子類僅能繼承一個父類的屬性和方法。這是大多數面向對象編程語言的基礎特性,能簡化代碼結構并減少復雜性。
class Animal {
public:void eat() { cout << "Eating" << endl; }
};class Dog : public Animal {
public:void bark() { cout << "Barking" << endl; }
};
多繼承:多繼承允許一個子類同時繼承多個父類,增強了代碼復用能力,但可能引發命名沖突(如多個父類有同名成員)和復雜性。
class Base1 { public: int a = 100; };
class Base2 { public: int b = 200; };class Derived : public Base1, public Base2
{
public:int sum() { return a + b; }
};
Q:如何定義一個不能被繼承的類?
1.父類構造私有化。子類對象實例化不能調用構造函數。
2.?final?關鍵字?修飾 不能被繼承的類。 (C++11)
Q:下面代碼 p1、p2、p3的大小關系?
class Base1 { public: int _a; };
class Base2 { public: int _b; };class Derived : public Base1, public Base2
{
public:int _d;
};int main()
{Derived d;Base1* p1 = &d;Base2* p2 = &d;Derived* p3 = &d;return 0;
}
A:p1=p3!=p2
? ? ? ? 這里其實是看對切割的理解深不深。
? ? ? ? 這里p1指向的是base1,p2指向的是base2,都是指向對應數據的開頭。p3指向的是整體,指向的是整體數據的首地址。
class Derived : public Base2, public Base1 //先繼承 Base2
{
public:int _d;
};
PS:子類先繼承誰,誰的數據在前面(Base2)。
? ? ? ? 這里數據位置改變,p1、p2指向的位置也會改變。p2=p3!=p1。
Q:這里p1、p2誰的地址大(先繼承Base1,再繼承Base2)?
? ? ? ? 因為數據存儲在棧幀中,是先存低地址再存高地址(這里先存的_a、_b、_c)。所以 p2 > p1、p3。
? ? ? ??
?????????
菱形繼承:
????????菱形繼承是多繼承的特殊情況,指一個子類的多個父類繼承自同一個基類,導致基類的成員在子類中存在多份副本。
class Person
{
public:string _name;
};class Student : public Person
{
protected:int _num;
};class Teacher :public Person
{};class Assistant :public Student, public Teacher
{};
????????當多個父類繼承自同一個祖先類時,可能導致成員重復和調用歧義。菱形繼承有數據冗余和二義性的問題。在 Assistant 的對象中 Person 成員會有兩份。
? ? ? ? 指定類域調用就明確了。監視窗口能看到有多份的數據。
菱形虛擬繼承:
????????通過虛擬繼承(virtual
關鍵字),中間派生類(B和C)共享同一份基類A的實例,從而消除冗余和二義性。
class A
{
public: int _a;
};class B : virtual public A // 虛繼承
{
public: int _b;
}; class C : virtual public A // 虛繼承
{
public: int _c;
}; class D : public B, public C
{
public: int _d;
};
內存示意圖:
? ? ? ? 沒有進行虛繼承的情況:
? ? ? ? 數據順序排布,這里先繼承的B,所以B的成員數據在A的前面。D對象里面存儲了多個父類的成員數據( _a )。
???????虛繼承的情況:
? ? ? ? A 的成員數據在公共區域且只存儲了一份,紅杠上的地址存儲了一個偏移量,是離 A 的距離 ,用于對象B、C 查找到 A 的位置。(注意這里編譯器使用的X32位編譯的,方便查看)
? ? ? ? 這時候發生切片(切割)的時候就跟前面有些不一樣了。這里B對象(b)由兩個部分組成,一個是B自己,還有一個是A的數據。?
? ? ? ? A 在公共數據區域,切片后怎么去找這里的 A 呢(pb->_a)?這里直接指針加偏移量就是_a,同理 pc->_c 也是一樣的。
PS:這里偏移量是16進制,14是20,0C是12。
D對象內存布局:
+----------------+
| B的虛基表指針 |
+----------------+
| C的虛基表指針 |
+----------------+
| A::data | // 唯一副本
+----------------+