類和對象------2

目錄

  • 一. C++面向對象模型初探
    • 1 .成員變量和函數的存儲
  • 二 this指針
      • 1 )this指針工作原理
      • 2 )this指針的使用
      • 3) const修飾成員函數
      • 4 )const修飾對象(常對象)
  • 3.友元
      • 1 )友元語法
      • 2) 課堂練習
    • 4 強化訓練(數組類封裝)
  • 四 運算符重載(沒整理完)
      • 1 )運算符重載基本概念
      • 2 運算符重載碰上友元函數
      • 3 可重載的運算符
      • 4 前置和后置(++/--)運算符重載
      • 5 賦值(=)運算符重載
      • 6 指針運算符(*、->)重載
      • 7 不要重載&&、||
      • 8 強化訓練_字符串類封裝
      • 9 附錄:運算符和結合性
  • 五. 繼承和派生
    • 1 繼承概述
      • 1.1 為什么需要繼承
      • 1.2 繼承基本概念
      • 1.3 派生類定義
    • 2 派生類訪問控制
    • 3 繼承中的構造和析構
      • 3.1 繼承中的對象模型
      • 3.2 對象構造和析構的調用原則
    • 4.繼承中同名成員的處理方法
    • 5. 非自動繼承的函數
    • 6. 繼承中的靜態成員特性
    • 7. 多繼承
      • 1 多繼承概念
      • 2 菱形繼承和虛繼承
      • 3 虛繼承實現原理
  • 六 多態
    • 1 多態基本概念
    • 2 向上類型轉換及問題
      • 2.1 問題拋出
      • 2.2 問題解決思路
      • 2.3 問題解決方案(虛函數,vitual function)
    • 3 C++如何實現動態綁定
    • 4 抽象基類和純虛函數
    • 5 純虛函數和多繼承
    • 6 虛析構函數
      • 6.1 虛析構函數作用
      • 6.2 純虛析構函數
  • 七. 重寫 重載 重定義

一. C++面向對象模型初探

1 .成員變量和函數的存儲

在c語言中, 變量和函數“分開來聲明的,也就是說,語言本身并沒有支持“數據”和“函數”之間的關聯性我們把這種程序方法稱為“程序性的”,由一組“分布在各個以功能為導航的函數中”的算法驅動,它們處理的是共同的外部數據。

c++實現了“封裝”,那么數據(成員屬性)和操作(成員函數)是什么樣的呢?
“數據”和“處理數據的操作(函數)”是分開存儲的。

  • ?c++中的非靜態數據成員直接內含在類對象中,就像c struct一樣。
  • ?成員函數(member function)雖然內含在class聲明之內,卻不出現在對象中。
  • ?每一個非內聯成員函數(non-inline member function)只會誕生一份函數實例.
class MyClass01{
public:int mA;
};class MyClass02{
public:int mA;static int sB;
};class MyClass03{
public:void printMyClass(){cout << "hello world!" << endl;}
public:int mA;static int sB;
};class MyClass04{
public:void printMyClass(){cout << "hello world!" << endl;}static void ShowMyClass(){cout << "hello world!" << endl;}
public:int mA;static int sB;
};int main(){MyClass01 mclass01;MyClass02 mclass02;MyClass03 mclass03;MyClass04 mclass04;cout << "MyClass01:" << sizeof(mclass01) << endl; //4//靜態數據成員并不保存在類對象中cout << "MyClass02:" << sizeof(mclass02) << endl; //4//非靜態成員函數不保存在類對象中cout << "MyClass03:" << sizeof(mclass03) << endl; //4//靜態成員函數也不保存在類對象中cout << "MyClass04:" << sizeof(mclass04) << endl; //4return EXIT_SUCCESS;
}
MyClass01:4
MyClass02:4
MyClass03:4
MyClass04:4

通過上面的案例,我們可以的得出:C++類對象中的變量和函數是分開存儲。

二 this指針

1 )this指針工作原理

通過上例我們知道,c++的數據和操作也是分開存儲,并且每一個非內聯成員函數(non-inline member function)只會誕生一份函數實例,也就是說多個同類型對象共用一塊代碼

那么問題是:這一塊代碼是如何區分那個對象調用自己的呢?

在這里插入圖片描述

c++通過提供特殊的對象指針,this指針,解決上述問題。This指針指向被調用的成員函數所屬的對象。
  c++規定,this指針是隱含在對象成員函數內的一種指針。當一個對象被創建后,它的每一個成員函數都含有一個系統自動生成的隱含指針this,用以保存這個對象的地址,也就是說雖然我們沒有寫上this指針,編譯器在編譯的時候也是會加上的。因此this也稱為“指向本對象的指針”,this指針并不是對象的一部分,不會影響sizeof(對象)的結果。
  this指針是C++實現封裝的一種機制,它將對象和該對象調用的成員函數連接在一起,在外部看來,每一個對象都擁有自己的函數成員。一般情況下,并不寫this,而是讓系統進行默認設置。
this指針永遠指向當前對象

成員函數通過this指針即可知道操作的是那個對象的數據。This指針是一種隱含指針,它隱含于每個類的非靜態成員函數中。This指針無需定義,直接使用即可。
注意:靜態成員函數內部沒有this指針,靜態成員函數不能操作非靜態成員變量

         c++編譯器對普通成員函數的內部處理

在這里插入圖片描述

2 )this指針的使用

  • ?當形參和成員變量同名時,可用this指針來區分
  • ?在類的非靜態成員函數中返回對象本身,可使用return *this.
class Person{
public://1. 當形參名和成員變量名一樣時,this指針可用來區分Person(string name,int age){//name = name;//age = age; //輸出錯誤this->name = name;this->age = age;}//2. 返回對象本身的引用//重載賦值操作符//其實也是兩個參數,其中隱藏了一個this指針Person PersonPlusPerson(Person& person){string newname = this->name + person.name;int newage = this->age + person.age;Person newperson(newname, newage);return newperson;}void ShowPerson(){cout << "Name:" << name << " Age:" << age << endl;}
public:string name;int age;
};//3. 成員函數和全局函數(Perosn對象相加)
Person PersonPlusPerson(Person& p1,Person& p2){string newname = p1.name + p2.name;int newage = p1.age + p2.age;Person newperson(newname,newage);return newperson;
}int main(){Person person("John",100);person.ShowPerson();cout << "---------" << endl;Person person1("John",20);Person person2("001", 10);//1.全局函數實現兩個對象相加Person person3 = PersonPlusPerson(person1, person2);person1.ShowPerson();person2.ShowPerson();person3.ShowPerson();//2. 成員函數實現兩個對象相加Person person4 = person1.PersonPlusPerson(person2);person4.ShowPerson();system("pause");return EXIT_SUCCESS;
}

3) const修飾成員函數

  • ?用const修飾的成員函數時,const修飾this指針指向的內存區域,成員函數體內不可以修改本類中的任何普通成員變量
  • ?當成員變量類型符前用mutable修飾時例外。
//const修飾成員函數
class Person{
public:Person(){this->mAge = 0;this->mID = 0;}//在函數括號后面加上const,修飾成員變量不可修改,除了mutable變量void sonmeOperate() const{//this->mAge = 200; //mAge不可修改this->mID = 10; //const Person* const tihs;}void ShowPerson(){cout << "ID:" << mID << " mAge:" << mAge << endl;}
private:int mAge;mutable int mID;
};int main(){Person person;person.sonmeOperate();person.ShowPerson();system("pause");return EXIT_SUCCESS;
}

4 )const修飾對象(常對象)

  • ?常對象只能調用const的成員函數
  • ?常對象可訪問 const 或非 const 數據成員,不能修改,除非成員用mutable修飾
class Person{
public:Person(){this->mAge = 0;this->mID = 0;}void ChangePerson() const{//mAge = 100; //mAge不可修改mID = 100;}void ShowPerson(){cout << "ID:" << this->mID << " Age:" << this->mAge << endl;}public:int mAge;mutable int mID;
};void test(){	const Person person;//1. 可訪問數據成員cout << "Age:" << person.mAge << endl;//person.mAge = 300; //不可修改person.mID = 1001; //但是可以修改mutable修飾的成員變量//2. 只能訪問const修飾的函數//person.ShowPerson();person.ChangePerson();
}

3.友元

類的主要特點之一是數據隱藏,即類的私有成員無法在類的外部(作用域之外)訪問。但是,有時候需要在類的外部訪問類的私有成員,怎么辦?
解決方法是使用友元函數,友元函數是一種特權函數c++允許這個特權函數訪問私有成員。這一點從現實生活中也可以很好的理解:
比如你的家,有客廳,有你的臥室,那么你的客廳是Public的,所有來的客人都可以進去,但是你的臥室是私有的,也就是說只有你能進去,但是呢,你也可以允許你的閨蜜好基友進去。
程序員可以把一個全局函數、某個類中的成員函數、甚至整個類聲明為友元

1 )友元語法

  • ?friend關鍵字只出現在聲明處
  • ?其他類、類成員函數、全局函數都可聲明為友元
  • ?友元函數不是類的成員,不帶this指針
  • ?友元函數可訪問對象任意成員屬性,包括私有屬性
class Building;
//友元類
class MyFriend{
public://友元成員函數void LookAtBedRoom(Building& building);void PlayInBedRoom(Building& building);
};
class Building{//全局函數做友元函數friend void CleanBedRoom(Building& building);
#if 0//成員函數做友元函數friend void MyFriend::LookAtBedRoom(Building& building);friend void MyFriend::PlayInBedRoom(Building& building);
#else	//友元類friend class MyFriend;
#endif
public:Building();
public:string mSittingRoom;
private:string mBedroom;
};void MyFriend::LookAtBedRoom(Building& building){cout << "我的朋友參觀" << building.mBedroom << endl;
}
void MyFriend::PlayInBedRoom(Building& building){cout << "我的朋友玩耍在" << building.mBedroom << endl;
}//友元全局函數
void CleanBedRoom(Building& building){cout << "友元全局函數訪問" << building.mBedroom << endl;
}Building::Building(){this->mSittingRoom = "客廳";this->mBedroom = "臥室";
}int main(){Building building;MyFriend myfriend;CleanBedRoom(building);myfriend.LookAtBedRoom(building);myfriend.PlayInBedRoom(building);system("pause");return EXIT_SUCCESS;
}

[友元類注意]

  • 1.友元關系不能被繼承。
  • 2.友元關系是單向的,類A是類B的朋友,但類B不一定是類A的朋友。
  • 3.友元關系不具有傳遞性。類B是類A的朋友,類C是類B的朋友,但類C不一定是類A的朋友。

思考: c++是純面向對象的嗎?
如果一個類被聲明為friend,意味著它不是這個類的成員函數,卻可以修改這個類的私有成員,而且必須列在類的定義中,因此他是一個特權函數。c++不是完全的面向對象語言,而只是一個混合產品。增加friend關鍵字只是用來解決一些實際問題,這也說明這種語言是不純的。畢竟c++設計的目的是為了實用性,而不是追求理想的抽象。
— Thinking in C++

2) 課堂練習

請編寫電視機類,電視機有開機和關機狀態,有音量,有頻道,提供音量操作的方法,頻道操作的方法。由于電視機只能逐一調整頻道,不能指定頻道,增加遙控類,遙控類除了擁有電視機已有的功能,再增加根據輸入調臺功能。

提示:遙控器類可作為電視機類的友元類。

class Remote;class Television{friend class Remote;
public:enum{ On,Off }; //電視狀態enum{ minVol,maxVol = 100 }; //音量從0到100enum{ minChannel = 1,maxChannel = 255 }; //頻道從1到255Television(){mState = Off;mVolume = minVol;mChannel = minChannel;}//打開電視機void OnOrOff(){this->mState = (this->mState == On ? Off : On);}//調高音量void VolumeUp(){if (this->mVolume >= maxVol){return;}this->mVolume++;}//調低音量void VolumeDown(){if (this->mVolume <= minVol){return;}this->mVolume--;}//更換電視頻道void ChannelUp(){if (this->mChannel >= maxChannel){return;}this->mChannel++;}void ChannelDown(){if (this->mChannel <= minChannel){return;}this->mChannel--;}//展示當前電視狀態信息void ShowTeleState(){cout << "開機狀態:" << (mState == On ? "已開機" : "已關機") << endl;if (mState == On){cout << "當前音量:" << mVolume << endl;cout << "當前頻道:" << mChannel << endl;}cout << "-------------" << endl;}
private:int mState; //電視狀態,開機,還是關機int mVolume; //電視機音量int mChannel; //電視頻道
};//電視機調臺只能一個一個的調,遙控可以指定頻道
//電視遙控器
class Remote{
public:Remote(Television* television){pTelevision = television;}
public:void OnOrOff(){pTelevision->OnOrOff();}//調高音量void VolumeUp(){pTelevision->VolumeUp();}//調低音量void VolumeDown(){pTelevision->VolumeDown();}//更換電視頻道void ChannelUp(){pTelevision->ChannelUp();}void ChannelDown(){pTelevision->ChannelDown();}//設置頻道 遙控新增功能void SetChannel(int channel){if (channel < Television::minChannel || channel > Television::maxChannel){return;}pTelevision->mChannel = channel;}//顯示電視當前信息void ShowTeleState(){pTelevision->ShowTeleState();}
private:Television* pTelevision;
};//直接操作電視
void test01(){Television television;television.ShowTeleState();television.OnOrOff(); //開機television.VolumeUp(); //增加音量+1television.VolumeUp(); //增加音量+1television.VolumeUp(); //增加音量+1television.VolumeUp(); //增加音量+1television.ChannelUp(); //頻道+1television.ChannelUp(); //頻道+1television.ShowTeleState();
}//通過遙控操作電視
void test02(){//創建電視Television television;//創建遙控Remote remote(&television);remote.OnOrOff();remote.ChannelUp();//頻道+1remote.ChannelUp();//頻道+1remote.ChannelUp();//頻道+1remote.VolumeUp();//音量+1remote.VolumeUp();//音量+1remote.VolumeUp();//音量+1remote.VolumeUp();//音量+1remote.ShowTeleState();
}

4 強化訓練(數組類封裝)

MyArray.h

#ifndef MYARRAY_H
#define MYARRAY_Hclass MyArray{
public://無參構造函數,用戶沒有指定容量,則初始化為100MyArray();//有參構造函數,用戶指定容量初始化explicit MyArray(int capacity);//用戶操作接口//根據位置添加元素void SetData(int pos, int val);//獲得指定位置數據int GetData(int pos);//尾插法void PushBack(int val);//獲得長度int GetLength();//析構函數,釋放數組空間~MyArray();
private:int mCapacity; //數組一共可容納多少個元素int mSize; //當前有多少個元素int* pAdress; //指向存儲數據的空間
};#endif

MyArray.cpp

#include"MyArray.h"MyArray::MyArray(){this->mCapacity = 100;this->mSize = 0;//在堆開辟空間this->pAdress = new int[this->mCapacity];
}
//有參構造函數,用戶指定容量初始化
MyArray::MyArray(int capacity){this->mCapacity = capacity;this->mSize = 0;//在堆開辟空間this->pAdress = new int[capacity];
}
//根據位置添加元素
void MyArray::SetData(int pos, int val){if (pos < 0 || pos > mCapacity - 1){return;}pAdress[pos] = val;
}
//獲得指定位置數據
int MyArray::GetData(int pos){return pAdress[pos];
}
//尾插法
void MyArray::PushBack(int val){if (mSize >= mCapacity){return;}this->pAdress[mSize] = val;this->mSize++;
}
//獲得長度
int MyArray::GetLength(){return this->mSize;
}
//析構函數,釋放數組空間
MyArray::~MyArray(){if (this->pAdress != nullptr){delete[] this->pAdress;}
}

TestMyArray.cpp

#include"MyArray.h"void test(){//創建數組MyArray myarray(50);//數組中插入元素for (int i = 0; i < 50; i++){//尾插法myarray.PushBack(i);//myarray.SetData(i, i);}//打印數組中元素for (int i = 0; i < myarray.GetLength(); i++){cout << myarray.GetData(i) << " ";}cout << endl;
}

四 運算符重載(沒整理完)

1 )運算符重載基本概念

運算符重載,就是對已有的運算符重新進行定義,賦予其另一種功能,以適應不同的數據類型。(運算符重載不能改變本來寓意,不能改變基礎類型寓意)

運算符重載(operator overloading)只是一種”語法上的方便”,也就是它只是另一種函數調用的方式。

在c++中,可以定義一個處理類的新運算符。這種定義很像一個普通的函數定義,只是函數的名字由關鍵字operator及其緊跟的運算符組成。差別僅此而已。它像任何其他函數一樣也是一個函數,當編譯器遇到適當的模式時,就會調用這個函數。

語法:
定義重載的運算符就像定義函數,只是該函數的名字是operator@,這里的@代表了被重載的運算符。函數的參數中參數個數取決于兩個因素。

  • ?運算符是一元(一個參數)的還是二元(兩個參數);
  • ?運算符被定義為全局函數(對于一元是一個參數,對于二元是兩個參數)還是成員函數(對于一元沒有參數,對于二元是一個參數-此時該類的對象用作左耳參數)

代碼:示例代碼\49 []運算符重載

[兩個極端]

    • 有些人很容易濫用運算符重載。它確實是一個有趣的工具。但是應該注意,它僅僅是一種語法上的方便而已,是另外一種函數調用的方式。從這個角度來看,只有在能使涉及類的代碼更易寫,尤其是更易讀時(請記住,讀代碼的機會比我們寫代碼多多了)才有理由重載運算符。如果不是這樣,就改用其他更易用,更易讀的方式。
    • 對于運算符重載,另外一個常見的反應是恐慌:突然之間,C運算符的含義變得不同尋常了,一切都變了,所有C代碼的功能都要改變!并非如此,對于內置的數據類型的表示總的所有運算符是不可能改變的。我們不能重載如下的運算符改變其行為。
1 + 4;

2 運算符重載碰上友元函數

友元函數是一個全局函數,和我們上例寫的全局函數類似,只是友元函數可以訪問某個類私有數據。

案例: 重載左移操作符(<<),使得cout可以輸出對象。

class Person{friend ostream& operator<<(ostream& os, Person& person);
public:Person(int id,int age){mID = id;mAge = age;}
private:int mID;int mAge;
};ostream& operator<<(ostream& os, Person& person){os << "ID:" << person.mID << " Age:" << person.mAge;return os;
}int main(){Person person(1001, 30);//cout << person; //cout.operator+(person)cout << person << " | " << endl;return EXIT_SUCCESS;
}

3 可重載的運算符

幾乎C中所有的運算符都可以重載,但運算符重載的使用時相當受限制的。特別是不能使用C中當前沒有意義的運算符(例如用**求冪)不能改變運算符優先級不能改變運算符的參數個數。這樣的限制有意義,否則,所有這些行為產生的運算符只會混淆而不是澄清寓語意。

在這里插入圖片描述
在這里插入圖片描述

除了賦值號(=)外,基類中被重載的操作符都將被派生類繼承。

特殊運算符

  • ?=, [], () 和 -> 操作符只能通過成員函數進行重載
  • ?<< 和 >> 操作符最好通過友元函數進行重載
  • ?不要重載 && 和 || 操作符,因為無法實現短路規則

常規建議

在這里插入圖片描述

4 前置和后置(++/–)運算符重載

重載的++和–運算符有點讓人不知所措,因為我們總是希望能根據它們出現在所作用對象的前面還是后面來調用不同的函數。解決辦法很簡單,例如當編譯器看到++a(前置++),它就調用operator++(a),當編譯器看到a++(后置++),它就會去調用operator++(int).

class Complex{friend ostream& operator<<(ostream& os,Complex& complex){os << "A:" << complex.mA << " B:" << complex.mB << endl;return os;}
public:Complex(){mA = 0;mB = 0;}//重載前置++Complex& operator++(){mA++;mB++;return *this;}//重載后置++Complex operator++(int){	Complex temp;temp.mA = this->mA;temp.mB = this->mB;mA++;mB++;return temp;}//前置--Complex& operator--(){mA--;mB--;return *this;}//后置--Complex operator--(int){Complex temp;temp.mA = mA;temp.mB = mB;mA--;mB--;return temp;}void ShowComplex(){cout << "A:" << mA << " B:" << mB << endl;}
private:int mA;int mB;
};void test(){Complex complex;complex++;cout << complex;++complex;cout << complex;Complex ret = complex++;cout << ret;cout << complex;cout << "------" << endl;ret--;--ret;cout << "ret:" << ret;complex--;--complex;cout << "complex:" << complex;
}
    • 優先使用++和–的標準形式,優先調用前置++。
      如果定義了++c,也要定義c++,遞增操作符比較麻煩,因為他們都有前綴和后綴形式,而兩種語義略有不同。重載operator++和operator–時應該模仿他們對應的內置操作符。
      對于++和–而言,后置形式是先返回,然后對象++或者–,返回的是對象的原值。前置形式,對象先++或–,返回當前對象,返回的是新對象。其標準形式為:

調用代碼時候,要優先使用前綴形式,除非確實需要后綴形式返回的原值,前綴和后綴形式語義上是等價的,輸入工作量也相當,只是效率經常會略高一些,由于前綴形式少創建了一個臨時對象。

5 賦值(=)運算符重載

賦值符常常初學者的混淆。這是毫無疑問的,因為’=’在編程中是最基本的運算符,可以進行賦值操作,也能引起拷貝構造函數的調用。
class Person{
friend ostream& operator<<(ostream& os,const Person& person){
os << “ID:” << person.mID << " Age:" << person.mAge << endl;
return os;
}
public:
Person(int id,int age){
this->mID = id;
this->mAge = age;
}
//重載賦值運算符
Person& operator=(const Person& person){
this->mID = person.mID;
this->mAge = person.mAge;
return *this;
}
private:
int mID;
int mAge;
};

//1. =號混淆的地方
void test01(){
Person person1(10, 20);
Person person2 = person1; //調用拷貝構造
//如果一個對象還沒有被創建,則必須初始化,也就是調用構造函數
//上述例子由于person2還沒有初始化,所以會調用構造函數
//由于person2是從已有的person1來創建的,所以只有一個選擇
//就是調用拷貝構造函數
person2 = person1; //調用operator=函數
//由于person2已經創建,不需要再調用構造函數,這時候調用的是重載的賦值運算符
}
//2. 賦值重載案例
void test02(){
Person person1(20, 20);
Person person2(30, 30);
cout << “person1:” << person1;
cout << “person2:” << person2;
person2 = person1;
cout << “person2:” << person2;
}
//常見錯誤,當準備給兩個相同對象賦值時,應該首先檢查一下這個對象是否對自身賦值了
//對于本例來講,無論如何執行這些賦值運算都是無害的,但如果對類的實現進行修改,那么將會出現差異;
//3. 類中指針
class Person2{
friend ostream& operator<<(ostream& os, const Person2& person){
os << “Name:” << person.pName << " ID:" << person.mID << " Age:" << person.mAge << endl;
return os;
}
public:
Person2(char* name,int id, int age){
this->pName = new char[strlen(name) + 1];
strcpy(this->pName, name);
this->mID = id;
this->mAge = age;
}
#if 1
//重載賦值運算符
Person2& operator=(const Person2& person){

	//注意:由于當前對象已經創建完畢,那么就有可能pName指向堆內存//這個時候如果直接賦值,會導致內存沒有及時釋放if (this->pName != NULL){delete[] this->pName;}this->pName = new char[strlen(person.pName) + 1];strcpy(this->pName,person.pName);this->mID = person.mID;this->mAge = person.mAge;return *this;
}

#endif
//析構函數
~Person2(){
if (this->pName != NULL){
delete[] this->pName;
}
}
private:
char* pName;
int mID;
int mAge;
};

void test03(){
Person2 person1(“John”,20, 20);
Person2 person2(“Edward”,30, 30);
cout << “person1:” << person1;
cout << “person2:” << person2;
person2 = person1;
cout << “person2:” << person2;
}

為什么operator=返回一個reference to this ?
為了實現連續賦值,賦值操作符必須返回一個引用指向操作符的左側實參。這是你為class實現賦值操作符必須遵循的協議。這個協議不僅適用于標準的賦值形式,也適用于+=、-=、
=等等。
注意,這只是一個協議,并無請執行。如果不遵循它,可能代碼一樣通過編譯。然而這份協議被所有內置類型和標準程序庫所提供的類型如string、vector等所遵守。因此除非你有一個標新立異的好理由,不然還是隨眾吧。

class Person{
friend ostream& operator<<(ostream& os, const Person& person){
os << “ID:” << person.mID << " Age:" << person.mAge << endl;
return os;
}
public:
Person(int id, int age){
this->mID = id;
this->mAge = age;
}
//重載賦值運算符
Person operator=(const Person& person){
this->mID = person.mID;
this->mAge = person.mAge;
return *this;
}
//重載賦值運算符
Person operator=(int x){
this->mID = x;
this->mAge = x;
return *this;
}
private:
int mID;
int mAge;
};

void test(){
Person person1(20, 20);
Person person2(30, 30);
cout << “person1:” << person1;
cout << “person2:” << person2;
//由于person2 = person1返回的是臨時對象,所以賦值為10并沒有改變person2對象
(person2 = person1) = 10;
cout << “person2:” << person2;
}

如果沒有重載賦值運算符,編譯器會自動創建默認的賦值運算符重載函數。行為類似默認拷貝構造,進行簡單值拷貝。

6 指針運算符(*、->)重載

class Person{
public:
Person(int param){
this->mParam = param;
}
void PrintPerson(){
cout << “Param:” << mParam << endl;
}
private:
int mParam;
};

class SmartPointer{
public:
SmartPointer(Person* person){
this->pPerson = person;
}
//重載指針的->、操作符
Person
operator->(){
return pPerson;
}
Person& operator*(){
return pPerson;
}
~SmartPointer(){
if (pPerson != NULL){
delete pPerson;
}
}
public:
Person
pPerson;
};

void test01(){

//Person* person = new Person(100);
//如果忘記釋放,那么就會造成內存泄漏SmartPointer pointer(new Person(100));
pointer->PrintPerson();

}

7 不要重載&&、||

不能重載operator&& 和 operator|| 的原因是,無法在這兩種情況下實現內置操作符的完整語義。說得更具體一些,內置版本版本特殊之處在于:內置版本的&&和||首先計算左邊的表達式,如果這完全能夠決定結果,就無需計算右邊的表達式了–而且能夠保證不需要。我們都已經習慣這種方便的特性了。
我們說操作符重載其實是另一種形式的函數調用而已,對于函數調用總是在函數執行之前對所有參數進行求值。
class Complex{
public:
Complex(int flag){
this->flag = flag;
}
Complex& operator+=(Complex& complex){
this->flag = this->flag + complex.flag;
return *this;
}
bool operator&&(Complex& complex){
return this->flag && complex.flag;
}
public:
int flag;
};
int main(){

Complex complex1(0);
Complex complex2(1);//原來情況,應該從左往右運算,左邊為假,則退出運算,結果為假
//這邊卻是,先運算(complex1+complex2),導致,complex1的flag變為complex1+complex2的值, complex1.a = 1
// 1 && 1
//complex1.operator&&(complex1.operator+=(complex2))
if (complex1 && (complex1 += complex2)){   cout << "真!" << endl;
}
else{cout << "假!" << endl;
}return EXIT_SUCCESS;

}

根據內置&&的執行順序,我們發現這個案例中執行順序并不是從左向右,而是先右猴左,這就是不滿足我們習慣的特性了。由于complex1 += complex2先執行,導致complex1 本身發生了變化,初始值是0,現在經過+=運算變成1,1 && 1輸出了真。

8 強化訓練_字符串類封裝

MyString.h
#ifndef MYSTRING_H
#define MYSTRING_H
#include

class MyString{
public:
//構造和析構
MyString();
MyString(const char* p);
MyString(const MyString& obj);
~MyString();
//普通成員函數
const char* c_str() const;
int length(); //獲取字符串長度
//運算符重載<< 重載,友元函數
friend ostream& operator<<(ostream& cout, MyString& obj);
friend void operator>>(const char* str, MyString& obj);
// =, [] 重載
MyString& operator=(const MyString& obj);
MyString& operator=(const char* p);
char& operator[](int index);
//重載+=、+
MyString& operator+=(MyString& str);
MyString& operator+=(const char* s);
MyString operator+(MyString& str);
MyString operator+(const char* str);
// , != 重載
bool operator
(const char* p);
bool operator!=(const char* p);
bool operator==(MyString& obj);
bool operator!=(MyString& obj);
private:
char* pAddress;
int mLength;
};
#endif

MyString.cpp
#include “MyString.h”

//構造和析構
MyString::MyString(){
this->mLength = 0;
this->pAddress = new char[1];
this->pAddress[0] = ‘\0’;
}
MyString::MyString(const char* p){
this->mLength = strlen§;
this->pAddress = new char[this->mLength + 1];
strcpy(this->pAddress,p);
}
MyString::MyString(const MyString& obj){
this->mLength = obj.mLength;
this->pAddress = new char[this->mLength + 1];
strcpy(this->pAddress, obj.pAddress);
}
MyString::~MyString(){
if (this->pAddress != NULL){
delete[] this->pAddress;
}
}

const char* MyString::c_str() const{
return this->pAddress;
}
int MyString::length(){
return this->mLength;
}

//運算符重載<< 重載,友元函數
ostream& operator<<(ostream& out, MyString& obj){
out << obj.pAddress;
return out;
}

void operator>>(const char* str, MyString& obj){
if (obj.pAddress != NULL){
delete[] obj.pAddress;
}
obj.pAddress = new char[strlen(str) + 1];
for (int i = 0; i < strlen(str) + 1;i ++){
obj.pAddress[i] = ‘\0’;
}
strcpy(obj.pAddress,str);
}

// =,[] 重載
MyString& MyString::operator=(const MyString& obj){
if (this->pAddress != NULL){
delete[] this->pAddress;
this->pAddress = NULL;
}
this->mLength = obj.mLength;
this->pAddress = new char[this->mLength + 1];
strcpy(this->pAddress,obj.pAddress);
return this;
}
MyString& MyString::operator=(const char
p){
if (this->pAddress != NULL){
delete[] this->pAddress;
this->pAddress = NULL;
}
this->mLength = strlen§;
this->pAddress = new char[this->mLength + 1];
strcpy(this->pAddress, p);
return *this;
}
char& MyString::operator[](int index){
return this->pAddress[index];
}

//重載+=、+
MyString& MyString::operator+=(MyString& str){
//判斷追加的字符串是否為空
if (str.mLength == 0){
return this;
}
//計算兩個字符串總長
this->mLength = this->mLength + str.mLength;
//申請兩個字符串長度的空間
char
pTemp = new char[this->mLength + 1];
//初始化數組
for (int i = 0; i < this->mLength + 1;i++){
pTemp[i] = ‘\0’;
}
//拷貝兩個字符串到新空間中
char* p = pTemp;
strcat(p, this->pAddress);
strcat(p, str.pAddress);
//釋放舊空間
if (this->pAddress != NULL){
delete[] this->pAddress;
this->pAddress = NULL;
}
//更新pAddress指針
this->pAddress = pTemp;

return *this;

}
MyString& MyString::operator+=(const char* s){
//判斷追加的字符串是否為空
if (s == NULL || strlen(s) == 0){
return *this;
}

//計算兩個字符串總長
this->mLength = this->mLength + strlen(s);
//申請兩個字符串長度的空間
char* pTemp = new char[this->mLength + 1];
//初始化數組
for (int i = 0; i < this->mLength + 1;i++){pTemp[0] = '\0';
}
//拷貝兩個字符串到新空間中
strcat(pTemp, this->pAddress);
strcat(pTemp, s);
//釋放舊空間
if (this->pAddress != NULL){delete[] this->pAddress;this->pAddress = NULL;
}
//更新指針
this->pAddress = pTemp;return *this;

}
MyString MyString::operator+(MyString& str){

if (str.mLength == 0){return *this;
}MyString tempString;tempString.mLength = this->mLength + str.mLength;
tempString.pAddress = new char[tempString.mLength + 1];
//初始化數組
for (int i = 0; i < tempString.mLength + 1;i++){tempString.pAddress[i] = '\0';
}
strcat(tempString.pAddress,this->pAddress);
strcat(tempString.pAddress, str.pAddress);return tempString;

}
MyString MyString::operator+(const char* str){
if (str == NULL || strlen(str) == 0){
return *this;
}

MyString tempString;tempString.mLength = this->mLength + strlen(str);
tempString.pAddress = new char[tempString.mLength + 1];
for (int i = 0; i < tempString.mLength + 1;i ++){tempString.pAddress[i] = '\0';
}
strcat(tempString.pAddress, this->pAddress);
strcat(tempString.pAddress, str);return tempString;

}

// , != 重載
bool MyString::operator
(const char* p){
if (p == NULL){
return false;
}
if (strcmp(this->pAddress,p) == 0){
return true;
}
return false;
}
bool MyString::operator!=(const char* p){
if (p == NULL){
return false;
}
if (strcmp(this->pAddress, p) != 0){
return true;
}
return false;
}
bool MyString::operator==(MyString& obj){

if (strcmp(this->pAddress, obj.pAddress) == 0){return true;
}
return false;

}
bool MyString::operator!=(MyString& obj){
if (strcmp(this->pAddress, obj.pAddress) != 0){
return true;
}
return false;
}
TestMyString.cpp
//1. 測試+=
void test01(){
MyString str1(“bbb”);
MyString str2(“aaa”);

str1 += str2;
cout << "str1:" << str1 << endl;
str1 += "hello world!";
cout << "str1:" << str1 << endl;

}

//2. 測試+
void test02(){

MyString str1("bbb");
MyString str2("aaa");MyString str3 = str1 + str2; //有問題
cout << "str1:" << str1 << endl;
cout << "str2:" << str2 << endl;
cout << "str3:" << str3 << endl;
cout << "-----------------" << endl;
MyString str4 = str1 + "hello world!";
cout << "str1:" << str1 << endl;
cout << "str4:" << str4 << endl;

}

//3. 測試=、[]
void test03(){
MyString str1(“bbb”);
MyString str2(“aaa”);
cout << “str1:” << str1 << endl;
cout << “str2:” << str2 << endl;
cout << “---------------” << endl;
str1 = str2;
cout << “str1:” << str1 << endl;
cout << “str2:” << str2 << endl;
cout << “---------------” << endl;
cout << “[]:”;
for (int i = 0; i < str1.length(); i++){
cout << str1[i];
}
cout << endl;
}

//4. 測試==、!=
void test04(){

MyString str1("bbb");
MyString str2("aaa");
if (str1 != str2){cout << "不相等!" << endl;
}
if (str1 != "ccc"){cout << "不相等!" << endl;
}str2 = str1;
if (str1 == str2){cout << "相等!" << endl;
}
if (str1 == "bbb"){cout << "相等!" << endl;
}

}

//5. 拷貝構造、=
void test05(){
MyString str1(“bbb”);
MyString str2 = str1;
MyString str3(str1);

cout << "str1:" << str1 << endl;
cout << "str2:" << str2 << endl;
cout << "str3:" << str3 << endl;

}

//6. 右移運算符
void test06(){

MyString str;
"hello world" >> str;cout << "str:" << str << endl;

}

9 附錄:運算符和結合性

優先級 運算符 名稱或含義 使用形式 結合方向 說明
1 [] 數組下標 數組名[常量表達式] 左到右 –
() 圓括號 (表達式)/函數名(形參表) –
. 成員選擇(對象) 對象.成員名 –
-> 成員選擇(指針) 對象指針->成員名 –

2 - 負號運算符 -表達式 右到左 單目運算符
按位取反運算符 ~表達式
++ 自增運算符 ++變量名/變量名++
– 自減運算符 --變量名/變量名–
*	取值運算符	*指針變量		
&	取地址運算符	&變量名		
!	邏輯非運算符	!表達式		
(類型)	強制類型轉換	(數據類型)表達式		--
sizeof	長度運算符	sizeof(表達式)		--

3 / 除 表達式/表達式 左到右 雙目運算符
* 乘 表達式*表達式
% 余數(取模) 整型表達式%整型表達式
4 + 加 表達式+表達式 左到右 雙目運算符
- 減 表達式-表達式
5 << 左移 變量<<表達式 左到右 雙目運算符
>> 右移 變量>>表達式

6 > 大于 表達式>表達式 左到右 雙目運算符
>= 大于等于 表達式>=表達式
< 小于 表達式<表達式
<= 小于等于 表達式<=表達式
7 == 等于 表達式==表達式 左到右 雙目運算符
!= 不等于 表達式!= 表達式

8 & 按位與 表達式&表達式 左到右 雙目運算符
9 ^ 按位異或 表達式^表達式 左到右 雙目運算符
10 | 按位或 表達式|表達式 左到右 雙目運算符
11 && 邏輯與 表達式&&表達式 左到右 雙目運算符
12 || 邏輯或 表達式||表達式 左到右 雙目運算符

13 ?: 條件運算符 表達式1?
表達式2: 表達式3 右到左 三目運算符

14 = 賦值運算符 變量=表達式 右到左 –
/= 除后賦值 變量/=表達式 –
= 乘后賦值 變量=表達式 –
%= 取模后賦值 變量%=表達式 –
+= 加后賦值 變量+=表達式 –
-= 減后賦值 變量-=表達式 –
<<= 左移后賦值 變量<<=表達式 –
>>= 右移后賦值 變量>>=表達式 –
&= 按位與后賦值 變量&=表達式 –
^= 按位異或后賦值 變量^=表達式 –
|= 按位或后賦值 變量|=表達式 –

15 , 逗號運算符 表達式,表達式,… 左到右 –

五. 繼承和派生

1 繼承概述

繼承是面向對象編程(OOP)的三大特性之一(封裝、繼承、多態),它允許創建一個新類(派生類)基于一個已存在的類(基類),從而獲得基類的屬性和方法。

1.1 為什么需要繼承

在這里插入圖片描述

網頁類
class IndexPage{
public://網頁頭部void Header(){cout << "網頁頭部!" << endl;}//網頁左側菜單void LeftNavigation(){cout << "左側導航菜單!" << endl;}//網頁主體部分void MainBody(){cout << "首頁網頁主題內容!" << endl;}//網頁底部void Footer(){cout << "網頁底部!" << endl;}
private:string mTitle; //網頁標題
};#if 0
//如果不使用繼承,那么定義新聞頁類,需要重新寫一遍已經有的代碼
class NewsPage{
public://網頁頭部void Header(){cout << "網頁頭部!" << endl;}//網頁左側菜單void LeftNavigation(){cout << "左側導航菜單!" << endl;}//網頁主體部分void MainBody(){cout << "新聞網頁主體內容!" << endl;}//網頁底部void Footer(){cout << "網頁底部!" << endl;}
private:string mTitle; //網頁標題
};void test(){NewsPage* newspage = new NewsPage;newspage->Header();newspage->MainBody();newspage->LeftNavigation();newspage->Footer();
}
#else
//使用繼承,可以復用已有的代碼,新聞業除了主體部分不一樣,其他都是一樣的
class NewsPage : public IndexPage{
public://網頁主體部分void MainBody(){cout << "新聞網頁主主體內容!" << endl;}
};
void test(){NewsPage* newspage = new NewsPage;newspage->Header();newspage->MainBody();newspage->LeftNavigation();newspage->Footer();
}
#endif
int main(){test();return EXIT_SUCCESS;
}

1.2 繼承基本概念

c++最重要的特征是代碼重用,通過繼承機制可以利用已有的數據類型來定義新的數據類型,新的類不僅擁有舊類的成員,還擁有新定義的成員
一個B類繼承于A類,或稱從類A派生類B。這樣的話,類A成為基類(父類), 類B成為派生類(子類)。
派生類中的成員,包含兩大部分:

  • ?一類是從基類繼承過來的,一類是自己增加的成員。
  • ?從基類繼承過過來的表現其共性,而新增的成員體現了其個性。
    在這里插入圖片描述

1.3 派生類定義

派生類定義格式:

   Class 派生類名 :  繼承方式 基類名{//派生類新增的數據成員和成員函數}

三種繼承方式:

  • ?public : 公有繼承
  • ?private : 私有繼承
  • ?protected : 保護繼承

從繼承源上分:

  • ?單繼承:指每個派生類只直接繼承了一個基類的特征
  • ?多繼承:指多個基類 派生出一個派生類的繼承關系,多繼承的派生類直接繼承了不止一 個基類的特征

2 派生類訪問控制

派生類繼承基類派生類擁有基類中全部成員變量和成員方法(除了構造和析構之外的成員方法),但是在派生類中,繼承的成員并不一定能直接訪問,不同的繼承方式會導致不同的訪問權限。
派生類的訪問權限規則如下:
在這里插入圖片描述

在這里插入圖片描述

//基類
class A{
public:int mA;
protected:int mB;
private:int mC;
};//1. 公有(public)繼承
class B : public A{
public:void PrintB(){cout << mA << endl; //可訪問基類public屬性cout << mB << endl; //可訪問基類protected屬性//cout << mC << endl; //不可訪問基類private屬性}
};
class SubB : public B{void PrintSubB(){cout << mA << endl; //可訪問基類public屬性cout << mB << endl; //可訪問基類protected屬性//cout << mC << endl; //不可訪問基類private屬性}
};
void test01(){B b;cout << b.mA << endl; //可訪問基類public屬性//cout << b.mB << endl; //不可訪問基類protected屬性//cout << b.mC << endl; //不可訪問基類private屬性
}//2. 私有(private)繼承
class C : private A{
public:void PrintC(){cout << mA << endl; //可訪問基類public屬性cout << mB << endl; //可訪問基類protected屬性//cout << mC << endl; //不可訪問基類private屬性}
};
class SubC : public C{void PrintSubC(){//cout << mA << endl; //不可訪問基類public屬性//cout << mB << endl; //不可訪問基類protected屬性//cout << mC << endl; //不可訪問基類private屬性}
};
void test02(){C c;//cout << c.mA << endl; //不可訪問基類public屬性//cout << c.mB << endl; //不可訪問基類protected屬性//cout << c.mC << endl; //不可訪問基類private屬性
}
//3. 保護(protected)繼承
class D : protected A{
public:void PrintD(){cout << mA << endl; //可訪問基類public屬性cout << mB << endl; //可訪問基類protected屬性//cout << mC << endl; //不可訪問基類private屬性}
};
class SubD : public D{void PrintD(){cout << mA << endl; //可訪問基類public屬性cout << mB << endl; //可訪問基類protected屬性//cout << mC << endl; //不可訪問基類private屬性}
};
void test03(){D d;//cout << d.mA << endl; //不可訪問基類public屬性//cout << d.mB << endl; //不可訪問基類protected屬性//cout << d.mC << endl; //不可訪問基類private屬性
}

3 繼承中的構造和析構

3.1 繼承中的對象模型

在C++編譯器的內部可以理解為結構體,子類是由父類成員疊加子類新成員而成:

class Aclass{
public:int mA;int mB;
};
class Bclass : public Aclass{
public:int mC;
};
class Cclass : public Bclass{
public:int mD;
};
void test(){cout << "A size:" << sizeof(Aclass) << endl;cout << "B size:" << sizeof(Bclass) << endl;cout << "C size:" << sizeof(Cclass) << endl;
}

查看類繼承的內部模型:
找到VS2013開發人員命令提示程序(一般在:C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\Shortcuts),打開,然后復制你工程路徑,命令:cd 路徑,進入你工程文件夾中(如果工程不在C盤在E盤的話,要再E:下),然后命令:cl /d1 reportSingleClassLayout類名 文件名全稱
如:cl /d1 reportSingleClassLayoutSon test.cpp

3.2 對象構造和析構的調用原則

繼承中的構造和析構

  • ?子類對象在創建時會首先調用父類的構造函數
  • ?父類構造函數執行完畢后,才會調用子類的構造函數
  • ?當父類構造函數有參數時,需要在子類初始化列表(參數列表)中顯示調用父類構造函數
  • ?析構函數調用順序和構造函數相反

在這里插入圖片描述

class A{
public:A(){cout << "A類構造函數!" << endl;}~A(){cout << "A類析構函數!" << endl;}
};class B : public A{
public:B(){cout << "B類構造函數!" << endl;}~B(){cout << "B類析構函數!" << endl;}
};class C : public B{
public:C(){cout << "C類構造函數!" << endl;}~C(){cout << "C類析構函數!" << endl;}
};void test(){C c;
}

?繼承與組合混搭的構造和析構

在這里插入圖片描述

class D{
public:D(){cout << "D類構造函數!" << endl;}~D(){cout << "D類析構函數!" << endl;}
};
class A{
public:A(){cout << "A類構造函數!" << endl;}~A(){cout << "A類析構函數!" << endl;}
};
class B : public A{
public:B(){cout << "B類構造函數!" << endl;}~B(){cout << "B類析構函數!" << endl;}
};
class C : public B{
public:C(){cout << "C類構造函數!" << endl;}~C(){cout << "C類析構函數!" << endl;}
public:D c;
};
void test(){C c;
}

4.繼承中同名成員的處理方法

  • ?當子類成員和父類成員同名時,子類依然從父類繼承同名成員
  • ?如果子類有成員和父類同名,子類訪問其成員默認訪問子類的成員(本作用域,就近原則)
  • ?在子類通過作用域::進行同名成員區分(在派生類中使用基類的同名成員,顯示使用類名限定符)
class Base{
public:Base():mParam(0){}void Print(){ cout << mParam << endl; }
public:int mParam;
};class Derived : public Base{
public:Derived():mParam(10){}void Print(){//在派生類中使用和基類的同名成員,顯示使用類名限定符cout << Base::mParam << endl;cout << mParam << endl;}//返回基類重名成員int& getBaseParam(){ return  Base::mParam; }
public:int mParam;
};int main(){Derived derived;//派生類和基類成員屬性重名,子類訪問成員默認是子類成員cout << derived.mParam << endl; //10derived.Print();//類外如何獲得基類重名成員屬性derived.getBaseParam() = 100;cout << "Base:mParam:" << derived.getBaseParam() << endl;return EXIT_SUCCESS;
}

注意: 如果重新定義了基類中的重載函數,將會發生什么?

class Base{
public://重載函數void func1(){cout << "Base::void func1()" << endl;};void func1(int param){cout << "Base::void func1(int param)" << endl;}//非重載函數void myfunc(){cout << "Base::void myfunc()" << endl;}
};class Derived1 : public Base{};
class Derived2 : public Base{
public:void myfunc(){//基類myfunc被隱藏,可通過類作用域運算符指定調用基類myfunc函數//Base::myfunc();cout << "Derived2::void myfunc()" << endl;}
};
class Derived3 : public Base{
public://改變成員函數的參數列表void func1(int param1, int param2){//Base::func1(10);  //類的內部可通過類作用域運算符訪問基類重載版本的函數cout << "Derived3::void func1(int param1,int param2)" << endl;};
};
class Derived4 : public Base{
public://改變成員函數的返回值int func1(int param){Base::func1(10);cout << "Derived4::int func1(int param)" << endl;return 0;}
};//和基類非重載函數重名
void test01(){Derived1 derived1;derived1.myfunc();//和基類函數重名Derived2 derived2;derived2.myfunc();
}//和基類重載函數重名
void test02(){Derived3 derived3;//derived3.func1();  //基類重載版本的函數fun1被全部隱藏,子類外部不可訪問//derived3.func1(10);derived3.func1(10,20);Derived4 derived4;//derived4.func1(); //基類重載版本的函數fun1被全部隱藏,子類外部不可訪問derived4.func1(10);
}

//結論:任何時候重新定義基類中的任何一個函數,子類中這種函數的任何版本都會被隱藏(非覆蓋,可通過類作用域運算符調用)

任何時候重新定義基類中的一個重載函數,在新類中所有的其他版本將被自動隱藏.

5. 非自動繼承的函數

不是所有的函數都能自動從基類繼承到派生類中。構造函數和析構函數用來處理對象的創建和析構操作,構造和析構函數只知道對它們的特定層次的對象做什么,也就是說構造函數和析構函數不能被繼承必須為每一個特定的派生類分別創建
另外operator=也不能被繼承,因為它完成類似構造函數的行為。也就是說盡管我們知道如何由=右邊的對象如何初始化=左邊的對象的所有成員,但是這個并不意味著對其派生類依然有效。
在繼承的過程中,如果沒有創建這些函數,編譯器會自動生成它們。

6. 繼承中的靜態成員特性

靜態成員函數和非靜態成員函數的共同點:

  • 1.他們都可以被繼承到派生類中。
  • 2.如果重新定義一個靜態成員函數,所有在基類中的其他重載函數會被隱藏。
  • 3.如果我們改變基類中一個函數的特征,所有使用該函數名的基類版本都會被隱藏。

靜態成員函數不能是虛函數(virtual function).

class Base{
public:static int getNum(){ return sNum; }static int getNum(int param){return sNum + param;}
public:static int sNum;
};
int Base::sNum = 10;class Derived : public Base{
public:static int sNum; //基類靜態成員屬性將被隱藏
#if 0//重定義一個函數,基類中重載的函數被隱藏static int getNum(int param1, int param2){return sNum + param1 + param2;}
#else//改變基類函數的某個特征,返回值或者參數個數,將會隱藏基類重載的函數static void getNum(int param1, int param2){cout <<  sNum + param1 + param2 << endl;}
#endif
};
int Derived::sNum = 20;

7. 多繼承

1 多繼承概念

我們可以從一個類繼承,我們也可以能同時從多個類繼承,這就是多繼承。但是由于多繼承是非常受爭議的,從多個類繼承可能會導致函數、變量等同名導致較多的歧義。
在這里插入圖片描述

class Base1{
public:void func1(){ cout << "Base1::func1" << endl; }
};
class Base2{
public:void func1(){ cout << "Base2::func1" << endl; }void func2(){ cout << "Base2::func2" << endl; }
};
//派生類繼承Base1、Base2
class Derived : public Base1, public Base2{};
int main(){Derived derived;//func1是從Base1繼承來的還是從Base2繼承來的?//derived.func1(); derived.func2();//解決歧義:顯示指定調用那個基類的func1derived.Base1::func1(); derived.Base2::func1();return EXIT_SUCCESS;
}

多繼承會帶來一些二義性的問題, 如果兩個基類中有同名的函數或者變量,那么通過派生類對象去訪問這個函數或變量時就不能明確到底調用從基類1繼承的版本還是從基類2繼承的版本?
解決方法就是顯示指定調用那個基類的版本。

2 菱形繼承和虛繼承

兩個派生類繼承同一個基類而又有某個類同時繼承者兩個派生類,這種繼承被稱為菱形繼承,或者鉆石型繼承
在這里插入圖片描述

這種繼承所帶來的問題:
1.羊繼承了動物的數據和函數,鴕同樣繼承了動物的數據和函數,當草泥馬調用函數或者數據時,就會產生二義性。
2.草泥馬繼承自動物的函數和數據繼承了兩份,其實我們應該清楚,這份數據我們只需要一份就可以。

class BigBase{
public:BigBase(){ mParam = 0; }void func(){ cout << "BigBase::func" << endl; }
public:int mParam;
};class Base1 : public BigBase{};
class Base2 : public BigBase{};
class Derived : public Base1, public Base2{};int main(){Derived derived;//1. 對“func”的訪問不明確//derived.func();//cout << derived.mParam << endl;cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl;cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;//2. 重復繼承cout << "Derived size:" << sizeof(Derived) << endl; //8return EXIT_SUCCESS;
}

上述問題如何解決?對于調用二義性,那么可通過指定調用那個基類的方式來解決,

那么重復繼承怎么解決?
對于這種菱形繼承所帶來的兩個問題,c++為我們提供了一種方式,采用虛基類。那么我們采用虛基類方式將代碼修改如下:

class BigBase{
public:BigBase(){ mParam = 0; }void func(){ cout << "BigBase::func" << endl; }
public:int mParam;
};class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
class Derived : public Base1, public Base2{};int main(){Derived derived;//二義性問題解決derived.func();cout << derived.mParam << endl;//輸出結果:12cout << "Derived size:" << sizeof(Derived) << endl;return EXIT_SUCCESS;
}

以上程序Base1 ,Base2采用虛繼承方式繼承BigBase,那么BigBase被稱為虛基類。=
通過虛繼承解決了菱形繼承所帶來的二義性問題。
但是虛基類是如何解決二義性的呢?并且derived大小為12字節,這是怎么回事?

3 虛繼承實現原理

class BigBase{
public:BigBase(){ mParam = 0; }void func(){ cout << "BigBase::func" << endl; }
public: int mParam;
};
#if 0 //虛繼承
class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
#else //普通繼承
class Base1 :  public BigBase{};
class Base2 :  public BigBase{};
#endif
class Derived : public Base1, public Base2{};
普通繼承	                                        		虛繼承

在這里插入圖片描述

在這里插入圖片描述

通過對象布局圖,我們發現普通繼承和虛繼承的對象內存圖是不一樣的。我們也可以猜測到編譯器肯定對我們編寫的程序做了一些手腳。

  • ?BigBase 菱形最頂層的類,內存布局圖沒有發生改變。
  • ?Base1和Base2通過虛繼承的方式派生自BigBase,這兩個對象的布局圖中可以看出編譯器為我們的對象中增加了一個vbptr (virtual base pointer),vbptr指向了一張表,這張表保存了當前的虛指針相對于虛基類的首地址的偏移量。
  • ?Derived派生于Base1和Base2,繼承了兩個基類的vbptr指針,并調整了vbptr與虛基類的首地址的偏移量。

由此可知編譯器幫我們做了一些幕后工作,使得這種菱形問題在繼承時候能只繼承一份數據,并且也解決了二義性的問題。現在模型就變成了Base1和 Base2 、Derived三個類對象共享了一份BigBase數據。

當使用虛繼承時虛基類是被共享的,也就是在繼承體系中無論被繼承多少次,對象內存模型中均只會出現一個虛基類的子對象(這和多繼承是完全不同的)。即使共享虛基類,但是必須要有一個類來完成基類的初始化(因為所有的對象都必須被初始化,哪怕是默認的),同時還不能夠重復進行初始化,那到底誰應該負責完成初始化呢?C++標準中選擇在每一次繼承子類中都必須書寫初始化語句(因為每一次繼承子類可能都會用來定義對象),但是虛基類的初始化是由最后的子類完成,其他的初始化語句都不會調用。

class BigBase{
public:BigBase(int x){mParam = x;}void func(){cout << "BigBase::func" << endl;}
public:int mParam;
};
class Base1 : virtual public BigBase{
public:Base1() :BigBase(10){} //不調用BigBase構造
};
class Base2 : virtual public BigBase{
public:Base2() :BigBase(10){} //不調用BigBase構造
};class Derived : public Base1, public Base2{
public:Derived() :BigBase(10){} //調用BigBase構造
};
//每一次繼承子類中都必須書寫初始化語句
int main(){Derived derived;return EXIT_SUCCESS;
}

注意:
虛繼承只能解決具備公共祖先的多繼承所帶來的二義性問題不能解決沒有公共祖先的多繼承的.

工程開發中真正意義上的多繼承是幾乎不被使用,因為多重繼承帶來的代碼復雜性遠多于其帶來的便利,多重繼承對代碼維護性上的影響是災難性的,在設計方法上,任何多繼承都可以用單繼承代替。

Jerry Schwarz,輸入輸出流(iostream)的作者,曾在個別場合表示如何他重新設計iostream的話,很可能從iostream中去除多重繼承。

六 多態

1 多態基本概念

多態是面向對象程序設計語言中數據抽象和繼承之外的第三個基本特征。
多態性(polymorphism)提供接口與具體實現之間的另一層隔離,從而將”what”和”how”分離開來。多態性改善了代碼的可讀性和組織性,同時也使創建的程序具有可擴展性,項目不僅在最初創建時期可以擴展,而且當項目在需要有新的功能時也能擴展。

c++支持編譯時多態(靜態多態)運行時多態(動態多態)運算符重載和函數重載就是編譯時多態,而派生類和虛函數實現運行時多態
靜態多態和動態多態的區別就是函數地址是早綁定(靜態聯編)還是晚綁定(動態聯編)。如果函數的調用,在編譯階段就可以確定函數的調用地址,并產生代碼,就是靜態多態(編譯時多態),就是說地址是早綁定的。而如果函數的調用地址不能編譯不能在編譯期間確定,而需要在運行時才能決定,這這就屬于晚綁定(動態多態,運行時多態)。

//計算器

class Caculator{
public:void setA(int a){this->mA = a;}void setB(int b){this->mB = b;}void setOperator(string oper){this->mOperator = oper;}int getResult(){if (this->mOperator == "+"){return mA + mB;}else if (this->mOperator == "-"){return mA - mB;}else if (this->mOperator == "*"){return mA * mB;}else if (this->mOperator == "/"){return mA / mB;}}
private:int mA;int mB;string mOperator;
};

//這種程序不利于擴展,維護困難,如果修改功能或者擴展功能需要在源代碼基礎上修改
//面向對象程序設計一個基本原則:開閉原則(對修改關閉,對擴展開放)

//抽象基類

class AbstractCaculator{
public:void setA(int a){this->mA = a;}virtual void setB(int b){this->mB = b;}virtual int getResult() = 0;
protected:int mA;int mB;
};//加法計算器
class PlusCaculator : public AbstractCaculator{
public:virtual int getResult(){return mA + mB;}
};//減法計算器
class MinusCaculator : public AbstractCaculator{
public:virtual int getResult(){return mA - mB;}
};//乘法計算器
class MultipliesCaculator : public AbstractCaculator{
public:virtual int getResult(){return mA * mB;}
};void DoBussiness(AbstractCaculator* caculator){int a = 10;int b = 20;caculator->setA(a);caculator->setB(b);cout << "計算結果:" << caculator->getResult() << endl;delete caculator;
}

2 向上類型轉換及問題

2.1 問題拋出

對象可以作為自己的類或者作為它的基類的對象來使用。還能通過基類的地址來操作它。取一個對象的地址(指針或引用),并將其作為基類的地址來處理,這種稱為向上類型轉換。
也就是說:父類引用或指針可以指向子類對象,通過父類指針或引用來操作子類對象。

class Animal{
public:void speak(){cout << "動物在唱歌..." << endl;}
};class Dog : public Animal{
public:void speak(){cout << "小狗在唱歌..." << endl;}
};void DoBussiness(Animal& animal){animal.speak();
}void test(){Dog dog;DoBussiness(dog);
}

運行結果: 動物在唱歌

問題拋出: 我們給DoBussiness傳入的對象是dog,而不是animal對象,輸出的結果應該是Dog::speak。

2.2 問題解決思路

解決這個問題,我們需要了解下綁定(捆綁,binding)概念。

  • 把函數體與函數調用相聯系稱為綁定(捆綁,binding)

當綁定在程序運行之前(由編譯器和連接器)完成時,稱為早綁定(early binding).C語言中只有一種函數調用方式,就是早綁定。
上面的問題就是由于早綁定引起的,因為編譯器在只有Animal地址時并不知道要調用的正確函數。編譯是根據指向對象的指針或引用的類型來選擇函數調用。這個時候由于DoBussiness的參數類型是Animal&,編譯器確定了應該調用的speak是Animal::speak的,而不是真正傳入的對象Dog::speak。
解決方法就是遲綁定(遲捆綁,動態綁定,運行時綁定,late binding),意味著綁定要根據對象的實際類型,發生在運行。

C++語言要實現這種動態綁定,必須有某種機制來確定運行時對象的類型并調用合適的成員函數。

2.3 問題解決方案(虛函數,vitual function)

C++動態多態性是通過虛函數來實現的,虛函數允許子類(派生類)重新定義父類(基類)成員函數,而子類(派生類)重新定義父類(基類)虛函數的做法稱為覆蓋(override),或者稱為重寫

對于特定的函數進行動態綁定,c++要求在基類中聲明這個函數的時候使用virtual關鍵字,動態綁定也就對virtual函數起作用.

  • ?為創建一個需要動態綁定的虛成員函數,可以簡單在這個函數聲明前面加上virtual關鍵字,定義時候不需要.
  • ?如果一個函數在基類中被聲明為virtual,那么在所有派生類中它都是virtual的.
  • ?在派生類中virtual函數的重定義稱為重寫(override).
  • ?Virtual關鍵字只能修飾成員函數.
  • ?構造函數不能為虛函數

注意:

僅需要在基類中聲明一個函數為virtual.調用所有匹配基類聲明行為的派生類函數都將使用虛機制。

class Animal{
public:virtual void speak(){cout << "動物在唱歌..." << endl;}
};
class Dog : public Animal{
public:virtual void speak(){cout << "小狗在唱歌..." << endl;}
};
void DoBussiness(Animal& animal){animal.speak();
}
void test(){Dog dog;DoBussiness(dog);
}

3 C++如何實現動態綁定

動態綁定什么時候發生?

所有的工作都是由編譯器在幕后完成。當我們告訴通過創建一個virtual函數來告訴編譯器要進行動態綁定,那么編譯器就會根據動態綁定機制來實現我們的要求,不會再執行早綁定。

問題:C++的動態捆綁機制是怎么樣的?

首先,我們看看編譯器如何處理虛函數。當編譯器發現我們的類中有虛函數的時候,編譯器會創建一張虛函數表,把虛函數的函數入口地址放到虛函數表中,并且在類中秘密增加一個指針,這個指針就是vpointer(縮寫vptr),這個指針是指向對象的虛函數表。在多態調用的時候,根據vptr指針,找到虛函數表來實現動態綁定。

驗證對象中的虛指針:

class A{
public:virtual void func1(){}virtual void func2(){}
};//B類為空,那么大小應該是1字節,實際情況是這樣嗎?
class B : public A{};void test(){cout << "A size:" << sizeof(A) << endl;cout << "B size:" << sizeof(B) << endl;
}

在編譯階段,編譯器秘密增加了一個vptr指針,但是此時vptr指針并沒有初始化指向虛函數表(vtable),什么時候vptr才會指向虛函數表?在對象構建的時候,也就是在對象初始化調用構造函數的時候。 編譯器首先默認會在我們所編寫的每一個構造函數中,增加一些vptr指針初始化的代碼。如果沒有提供構造函數,編譯器會提供默認的構造函數,那么就會在默認構造函數里做此項工作,初始化vptr指針,使之指向本對象的虛函數表。
起初,子類繼承基類,子類繼承了基類的vptr指針,這個vptr指針是指向基類虛函數表,當子類調用構造函數,使得子類的vptr指針指向了子類的虛函數表。

當子類無重寫基類虛函數時:

在這里插入圖片描述

在這里插入圖片描述

過程分析:Animal* animal = new Dog;animal->fun1();當程序執行到這里,會去animal指向的空間中尋找vptr指針,通過vptr指針找到func1函數,
此時由于子類并沒有重寫也就是覆蓋基類的func1函數,所以調用func1時,仍然調用的是
基類的func1.
代碼驗證:示例代碼\71 驗證子類無重寫基類函數
執行結果: 我是基類的func1
測試結論: 無重寫基類的虛函數,無意義

當子類重寫基類虛函數時:

在這里插入圖片描述

在這里插入圖片描述

過程分析:Animal* animal = new Dog;animal->fun1();當程序執行到這里,會去animal指向的空間中尋找vptr指針,通過vptr指針找到func1函數,
由于子類重寫基類的func1函數,所以調用func1時,調用的是子類的func1.
代碼驗證: 示例代碼\72 驗證子類重寫基類函數
執行結果: 我是子類的func1
測試結論: 無重寫基類的虛函數,無意義

多態的成立條件:

  • 有繼承
  • 子類重寫父類虛函數函數
    • a) 返回值,函數名字,函數參數,必須和父類完全一致(析構函數除外)
      
    • b) 子類中virtual關鍵字可寫可不寫,建議寫
      
  • 類型兼容,父類指針,父類引用 指向 子類對象

4 抽象基類和純虛函數

在設計時,常常希望基類僅僅作為其派生類的一個接口。這就是說,僅想對基類進行向上類型轉換,使用它的接口,而不希望用戶實際的創建一個基類的對象。同時創建一個純虛函數允許接口中放置成員原函數,而不一定要提供一段可能對這個函數毫無意義的代碼。

做到這點,可以在基類中加入至少一個純虛函數(pure virtual function), 使得基類稱為抽象類(abstract class).

  • 純虛函數使用關鍵字virtual,并在其后面加上=0。如果試圖去實例化一個抽象類,編譯器則會阻止這種操作。
  • 當繼承一個抽象類的時候,必須實現所有的純虛函數,否則由抽象類派生的類也是一個抽象類。
  • Virtual void fun() = 0;告訴編譯器在vtable中為函數保留一個位置,但在這個特定位置不放地址。

建立公共接口目的是為了將子類公共的操作抽象出來,可以通過一個公共接口來操縱一組類,且這個公共接口不需要事先(或者不需要完全實現)。可以創建一個公共類.

案例: 模板方法模式

在這里插入圖片描述

//抽象制作飲品
class AbstractDrinking{
public://燒水virtual void Boil() = 0;//沖泡virtual void Brew() = 0;//倒入杯中virtual void PourInCup() = 0;//加入輔料virtual void PutSomething() = 0;//規定流程void MakeDrink(){Boil();Brew();PourInCup();PutSomething();}
};//制作咖啡
class Coffee : public AbstractDrinking{
public://燒水virtual void Boil(){cout << "煮農夫山泉!" << endl;}//沖泡virtual void Brew(){cout << "沖泡咖啡!" << endl;}//倒入杯中virtual void PourInCup(){cout << "將咖啡倒入杯中!" << endl;}//加入輔料virtual void PutSomething(){cout << "加入牛奶!" << endl;}
};//制作茶水
class Tea : public AbstractDrinking{
public://燒水virtual void Boil(){cout << "煮自來水!" << endl;}//沖泡virtual void Brew(){cout << "沖泡茶葉!" << endl;}//倒入杯中virtual void PourInCup(){cout << "將茶水倒入杯中!" << endl;}//加入輔料virtual void PutSomething(){cout << "加入食鹽!" << endl;}
};//業務函數
void DoBussiness(AbstractDrinking* drink){drink->MakeDrink();delete drink;
}void test(){DoBussiness(new Coffee);cout << "--------------" << endl;DoBussiness(new Tea);
}

5 純虛函數和多繼承

多繼承帶來了一些爭議,但是接口繼承可以說一種毫無爭議的運用了。
絕大數面向對象語言都不支持多繼承,但是絕大數面向對象對象語言都支持接口的概念,c++中沒有接口的概念,但是可以通過純虛函數實現接口。

接口類中只有函數原型定義,沒有任何數據定義。

多重繼承接口不會帶來二義性和復雜性問題。接口類只是一個功能聲明,并不是功能實現,子類需要根據功能說明定義功能實現。
注意:除了析構函數外,其他聲明都是純虛函數。

6 虛析構函數

6.1 虛析構函數作用

虛析構函數是為了解決基類的指針指向派生類對象,并用基類的指針刪除派生類對象。

class People{
public:People(){cout << "構造函數 People!" << endl;}virtual void showName() = 0;virtual ~People(){cout << "析構函數 People!" << endl;}
};class Worker : public People{
public:Worker(){cout << "構造函數 Worker!" << endl;pName = new char[10];}virtual void showName(){cout << "打印子類的名字!" << endl;}~Worker(){cout << "析構函數 Worker!" << endl;if (pName != NULL){delete pName;}}
private:char* pName;
};void test(){People* people = new Worker;people->~People();
}

6.2 純虛析構函數

純虛析構函數在c++中是合法的,但是在使用的時候有一個額外的限制:必須為純虛析構函數提供一個函數體。
那么問題是:如果給虛析構函數提供函數體了,那怎么還能稱作純虛析構函數呢?
純虛析構函數和非純析構函數之間唯一的不同之處在于純虛析構函數使得基類是抽象類,不能創建基類的對象。

//非純虛析構函數

class A{
public:virtual ~A();
};A::~A(){}

//純析構函數

class B{
public:virtual ~B() = 0;
};B::~B(){}
void test(){A a; //A類不是抽象類,可以實例化對象B b; //B類是抽象類,不可以實例化對象
}

如果類的目的不是為了實現多態,作為基類來使用,就不要聲明虛析構函數,反之,則應該為類聲明虛析構函數。

七. 重寫 重載 重定義

?重載,同一作用域的同名函數

	1.同一個作用域2.參數個數,參數順序,參數類型不同3.和函數返回值,沒有關系4.const也可以作為重載條件  //do(const Teacher& t){}  do(Teacher& t)

?重定義(隱藏)

	1.有繼承2.子類(派生類)重新定義父類(基類)的同名成員(非virtual函數)

?重寫(覆蓋)

	1.有繼承2.子類(派生類)重寫父類(基類)的virtual函數3.函數返回值,函數名字,函數參數,必須和基類中的虛函數一致
class A{
public://同一作用域下,func1函數重載void func1(){}void func1(int a){}void func1(int a,int b){}void func2(){}virtual void func3(){}
};
class B : public A{
public://重定義基類的func2,隱藏了基類的func2方法void func2(){}//重寫基類的func3函數,也可以覆蓋基類func3virtual void func3(){}
};

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/83860.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/83860.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/83860.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

量子計算在金融科技中的應用前景

隨著量子計算技術的飛速發展&#xff0c;其在各行業的應用潛力逐漸顯現&#xff0c;金融科技領域更是備受關注。量子計算的強大計算能力有望為金融行業帶來前所未有的變革&#xff0c;從風險評估到投資組合優化&#xff0c;從高頻交易到加密技術&#xff0c;量子計算都可能成為…

Redisson 四大核心機制實現原理詳解

一、可重入鎖&#xff08;Reentrant Lock&#xff09; 可重入鎖是什么&#xff1f; 通俗定義 可重入鎖類似于一把“智能鎖”&#xff0c;它能識別當前的鎖持有者是否是當前線程&#xff1a; 如果是&#xff0c;則允許線程重復獲取鎖&#xff08;重入&#xff09;&#xff0c;并…

srs-7.0 支持obs推webrtc流

demo演示 官方教程: https://ossrs.net/lts/zh-cn/blog/Experience-Ultra-Low-Latency-Live-Streaming-with-OBS-WHIP 實現原理就是通過WHIP協議來傳輸 SDP信息 1、運行 ./objs/srs -c conf/rtc.conf 2、obs推流 3、web端播放webrtc流 打開web:ht

面試題——JDBC|Maven|Spring的IOC思想|DI思想|SpringMVC

目錄 一、JDBC 1、jdbc連接數據庫的基本步驟&#xff08;掌握**&#xff09; 2、Statement和PreparedStatement的區別 &#xff08;掌握***&#xff09; 二、Maven 1、maven的作用 2、maven 如何排除依賴 3、maven scope作用域有哪些&#xff1f; 三、Spring的IOC思想 …

從代碼學習數學優化算法 - 拉格朗日松弛 Python版

文章目錄 前言1. 問題定義 (Problem Definition)2. 拉格朗日松弛 (Lagrangian Relaxation)3. 拉格朗日對偶問題 (Lagrangian Dual)4. 次梯度優化 (Subgradient Optimization)5. Python 代碼實現導入庫和問題定義輔助函數:求解拉格朗日松弛子問題次梯度優化主循環結果展示與繪圖…

密碼學實驗

密碼學實驗二 一、實驗目的&#xff08;本次實驗所涉及并要求掌握的知識點&#xff09; 掌握RSA算法的基本原理并根據給出的RSA算法簡單的實現代碼源程序,以及能夠使用RSA對文件進行加密。掌握素性測試的基本原理&#xff0c;并且會使用Python進行簡單的素性測試以及初步理解…

力扣面試150題-- 從中序與后序遍歷序列構造二叉樹

Day 44 題目描述 思路 這題類似與昨天那題&#xff0c;首先來復習一下&#xff0c;后序遍歷&#xff0c;對于后序遍歷每一個元素都滿足以下規律&#xff1a; &#xff08;左子樹&#xff09;&#xff08;右子樹&#xff09;&#xff08;根&#xff09;&#xff0c;那么我們直…

2區組的2水平析因實驗的混區設計

本文是實驗設計與分析&#xff08;第6版&#xff0c;Montgomery著傅玨生譯)第7章2k析因的區組化和混區設計第7.4節的python解決方案。本文盡量避免重復書中的理論&#xff0c;著于提供python解決方案&#xff0c;并與原書的運算結果進行對比。您可以從Detail 下載實驗設計與分析…

反向傳播算法——矩陣形式遞推公式——ReLU傳遞函數

總結反向傳播算法。 來源于https://udlbook.github.io/udlbook/&#xff0c;我不明白初始不從 x 0 \boldsymbol{x}_0 x0?開始&#xff0c;而是從 z 0 \boldsymbol{z}_0 z0?開始&#xff0c;不知道怎么想的。 考慮一個深度神經網絡 g [ x i , ? ] g[\boldsymbol{x}_i, \bold…

2025年PMP 學習二十三 16章 高級項目管理

2025年PMP 學習二十三 16章 高級項目管理 文章目錄 2025年PMP 學習二十三 16章 高級項目管理高級項目管理戰略管理戰略管理的組成要素&#xff1a;企業戰略轉化為戰略行動的階段&#xff1a; 組織戰略類型戰略組織類型組織級項目管理OPM&#xff08;公司項目管理&#xff09; 組…

Journal of Real-Time Image Processing 投稿過程

投稿要求雙欄12頁以內(包括參考文獻)&#xff0c;這個排版要求感覺不是很嚴格&#xff0c;我當時就是用普通的雙欄的格式去拍的版&#xff0c;然后就提交了&#xff0c;也沒單獨去下載模版。 投稿過程 12.12 Submission received 12.12 Submission is under technical check 1…

t檢驗詳解:原理、類型與應用指南

t檢驗詳解&#xff1a;原理、類型與應用指南 t檢驗&#xff08;t-test&#xff09;是一種用于比較兩組數據均值是否存在顯著差異的統計方法&#xff0c;適用于數據近似正態分布且滿足方差齊性的場景。以下從核心原理、檢驗類型、實施步驟到實際應用進行系統解析。 一、t檢驗的…

Web4X·AI實業未來家庭普及產品矩陣

Web4XAI實業未來家庭普及產品矩陣 > 打造一個“AI能干活、人更自由”的超級生活系統&#xff08;web4-web4.0&#xff09; 一、AI生活服務類 1、代表產品&#xff1a; ? AI語音助手&#xff08;對話、提醒、天氣、家庭調度&#xff09; ? AI陪護機器人&#xff08;老…

Centos上搭建 OpenResty

一、OpenResty簡介 OpenResty 是基于 Nginx 的擴展平臺&#xff0c;完全兼容 Nginx 的核心功能&#xff08;如 HTTP 服務和反向代理&#xff09;&#xff0c;同時通過內嵌 LuaJIT 支持&#xff0c;允許開發者用 Lua 腳本靈活擴展業務邏輯。它簡化了動態邏輯的實現。 二、安裝…

項目管理進階:基于IPD流程的項目管理部分問題及建議書【附全文閱讀】

該文檔主要探討了研發項目管理中存在的問題及改進建議。指出項目組織、項目計劃、項目監控等方面存在的問題&#xff0c;并給出了相應的設計要點。建議建立跨部門、全流程的項目計劃體系&#xff0c;加強風險管理&#xff0c;引入科學的估計方法&#xff0c;建立項目歷史數據積…

JVM之GC常見的垃圾回收器

收集器適用區域特點適用場景Serial新生代單線程&#xff0c;STW&#xff08;Stop-The-World&#xff09;客戶端小應用Parallel Scavenge新生代多線程&#xff0c;吞吐量優先后臺計算任務ParNew新生代Serial 的多線程版配合 CMS 使用CMS老年代并發標記&#xff0c;低延遲響應優先…

免費私有化部署! PawSQL社區版,超越EverSQL的企業級SQL優化工具面向個人開發者開放使用了

1. 概覽 1.1 快速了解 PawSQL PawSQL是專注于數據庫性能優化的企業級工具&#xff0c;解決方案覆蓋SQL開發、測試、運維的整個流程&#xff0c;提供智能SQL審核、查詢重寫優化及自動化巡檢功能&#xff0c;支持MySQL、PostgreSQL、Oracle、SQL Server等主流數據庫及達夢、金倉…

HTTP/HTTPS與SOCKS5協議在隧道代理中的兼容性設計解析

目錄 引言 一、協議特性深度對比 1.1 協議工作模型差異 1.2 隧道代理適配難點 二、兼容性架構設計 2.1 雙協議接入層設計 2.2 統一隧道內核 三、關鍵技術實現 3.1 協議轉換引擎 3.1.1 HTTP→SOCKS5轉換 3.1.2 SOCKS5→HTTP轉換 3.2 連接管理策略 3.2.1 智能連接池 …

3DGS——基礎知識學習筆記

1.什么是3D高斯潑濺&#xff08;3D Gaussian Splatting&#xff09;&#xff1f; 目標&#xff1a;從一組稀疏的3D點&#xff08;比如通過相機或激光雷達采集的點云&#xff09;重建出高質量的3D場景&#xff0c;并支持實時渲染。 核心思想&#xff1a;用許多“3D高斯分布”&…

【C++】不推薦使用的std::allocator<void>

文章目錄 不推薦使用的std::allocator<void>1. 核心區別2. 成員函數對比(1) allocate 和 deallocate(2) construct 和 destroy 3. 設計動機(1) std::allocator<T>(2) std::allocator<void> 4. 使用場景示例(1) std::allocator<int>(2) std::allocator&…