C++ 第四章 類和物件
一、深拷貝與淺拷貝
淺拷貝:簡單的賦值操作,會導致指標指向同一記憶體地址
如果利用編譯器提供的拷貝建構函式,會做淺拷貝操作
淺拷貝帶來的問題是:堆區記憶體重複釋放,引發崩潰
深拷貝:在堆區重新申請空間,進行拷貝操作
public: int age; string name; int *height; person(string name, int age,int height) { this->age = age; this->name = name; this->height = new int(height); } person(const person& p) //深拷貝 { cout << "Person 拷貝建構函式呼叫" << endl; age = p.age; //height = p.height;編譯器預設淺拷貝時進行的操作 height = new int (*p.height); } ~person()//解構函式:將堆區開闢的資料進行釋放 { if (height != NULL) delete(height); cout << "析構" << endl; } };
總結:如果屬性有在堆區開闢的,一定要自己提供拷貝建構函式,防止淺拷貝帶來的問題
二、初始化列表
傳統賦值初始化相當於:先宣告類,再進行屬性的賦值操作
初始化列表相當於:直接宣告一個有初始值的型別,在建構函式語句前,省略了賦值操作
在大型專案中,class類中成員變數極多的情況下,初始化列表效率更高
person(string name, int age,int h) :age(age), name(name), height(new int(h))
{
//注意:指標成員變數初始化時需用new 型別名(變數)來進行
}
三、類物件作為類成員
構造順序:構造時先構造類的成員物件,再構造類自身
析構順序:析構時先析構自身
class Phone { public: string p_Brand; Phone() { cout << "Phone created!" << endl; } Phone(string brand) { this->p_Brand = brand; cout << "Phone created!" << endl; } ~Phone() { cout << "Phone deleted!" << endl; } }; class person { public: int age; string name; int *height; Phone phone; } /* Phone created! Person created! 18 yxc 180 iphone person deleted! Phone deleted! */
四、靜態成員
1.靜態成員函式
兩種訪問方式:
person::func();//1.通過類名訪問
person p1;
p1.func();//2.通過物件訪問
特點:
1.程式共享一個函式
2.靜態成員函式只能訪問靜態成員變數
3.靜態成員函式也有訪問許可權
2.靜態成員變數
特點:
1.所有物件共享同一份資料,在記憶體中只有一份
2.在編譯階段分配記憶體
3.類內宣告,類外初始化
class Animal
{
public:
static const int head = 1;
};
五、C++物件型別和this指標
1.成員變數和函式分開儲存
在C++中,類內的成員變數和成員函式分開儲存,只有非靜態成員變數才屬於類的物件上
空變數佔用記憶體空間為1位元組,因為編譯器會給每個空物件分配一個位元組空間,以區分空物件佔用記憶體的位置
一個含int成員變數的物件佔用記憶體空間為4位元組
class Person
{
int m_A; //非靜態成員變數 屬於類的物件
static int m_B; //靜態成員變數 不屬於類的物件
void func() //非靜態成員函式 不屬於類的物件
{
}
static void func() //靜態成員函式 屬於類的物件
{
}
};
2.this指標概念
this指標指向被呼叫的成員函式所屬的物件
this指標是隱含每一個非靜態成員函式內的一種指標,無需定義,直接使用
class Person
{
public:
int age;
Person(int age)
{
this->age = age;
}
Person& AddAge(const Person p)
{
this->age += p.age;
return *this;
}
};
int main()
{
Person p1(10);
Person p2(20);
p1.AddAge(p2).AddAge(p2).AddAge(p2);
cout << p1.age << endl;
return 0;
}
3.空指標訪問成員函式
C++中空指標也是可以呼叫成員函式的,但也要注意有沒有用到this指標
如果用到this指標,需要加以判斷程式碼的健壯性
class Person
{
void ShowAge()
{
if(this)
{
cout<<this->age<<endl;
}
}
}
4.const修飾成員函式
常函式
成員函式後加const後叫常函式
常函式不可修改成員屬性
但是成員屬性宣告時加關鍵字mutable後,在常函式、常物件中仍可修改
常物件
宣告物件前加const稱為常物件
常物件只能呼叫常函式
this指標的本質是指標常量,指標的指向是不可修改的
class Person
{
public:
int age;
mutable int height; //特殊變數,在常函式、常物件中也可修改
void ShowAge() const //常函式
{
if (this)
{
cout << this->age << endl;
this->height = 185;
}
}
void ShowHeight()
{
if (this)
{
cout << this->height << endl;
}
}
};
int main()
{
Person p1(10);
const Person p2(10);
p1.ShowAge();
p1.ShowHeight();
//p2.ShowHeight(); //錯誤:常物件只能呼叫常函式
return 0;
}
五、友元
在程式裡,有些私有屬性,也想讓類外特殊的一些函式或者類進行訪問,友元的目的就是讓一個函式或者類 訪問另一個類中似有成員
友元的關鍵字:friend
友元的三種實現方法:
- 全域性函式做友元
- 類做友元
- 成員函式做友元
1.全域性函式做友元
class House
{
friend void GF(House* house);//告訴編譯器:全域性函式GF是類House的好朋友,可訪問private內容
public:
string Living_room;
private:
string Bed_room;
public:
House()
{
Living_room = "客廳";
Bed_room = "臥室";
}
};
void GF(House* house)
{
cout << "GF is visiting " << house->Living_room << endl;
cout << "GF is visiting " << house->Bed_room << endl;
}
2.類做友元
class House
{
friend class GF; //類GF是House的好朋友,可以訪問private內容
//無許可權修飾,不是House的成員
public:
House(); //類內宣告函式,類外實現
public:
string Living_room;
private:
string Bed_room;
};
class GF
{
public:
string name;
House *house;
public:
GF(string name);//類內宣告函式,類外實現
void visit(House* house);//類內宣告函式,類外實現
};
House::House() //類外實現,注意明確名稱空間
{
Living_room = "客廳";
Bed_room = "臥室";
}
void GF::visit(House* house)//注意宣告名稱空間的位置
{
cout << name << " is visiting " << house->Living_room << endl;
cout << name << " is visiting " << house->Bed_room << endl;
}
GF::GF(string name)//類外實現
{
house = new House();
this->name = name;
}
3.成員函式做友元
class House
{
friend void GF::visit();
public:
House(); //類內宣告函式,類外實現
public:
string Living_room;
private:
string Bed_room;
};
六、運算子過載
概念:對已有的運算子重新進行定義,賦予其另一種功能,以適應不同的資料型別
1.加號運算子過載
通過成員函式過載+號
class Person
{
public:
int age;
Person operator+(const Person& p)
{
Person temp(0);
temp.age = this->age + p.age;
return temp;
}
Person()
{
}
Person(int age)
{
this->age = age;
}
};
本質
Person p3 = p1.operator+(p2);
通過全域性函式過載+號
Person operator+ (const Person& p1, const Person& p2)
{
Person temp;
temp.age = p1.age + p2.age;
return temp;
}
本質
Person p3 = operator+(p1,p2);
2.左移運算子<<過載
只能利用全域性函式過載左移運算子
因為利用成員運算子 左移運算子 p.operator<<(cout) 簡化版本:p<<cout 無法實現p在左側
ostream &operator<<(ostream& cout, const Person& p) //ostream是靜態的,記憶體中只有一份
//本質:operator<<(cout,p) 簡化:cout<<p p為引用型別,可以防止有開闢在堆區屬性的物件崩潰
{
cout << "姓名:" << p.name << " 年齡:" << p.age << endl;
return cout;
}
若需要輸出類的私有屬性,可以將過載<<的函式做類的友元
3.遞增運算子++過載
前置遞增
MyInteger& operator++()
{
this->val ++; //先加法運算
return *this;//再返回結果
}
注意:返回引用
後置遞增
MyInteger operator++(int) //int代表佔位引數,可以用於區分前置和後置遞增
{
MyInteger t = this->val;//儲存原先結果
this->val++;//加法運算
return t;//返回原先結果,返回型別不是引用,因為要返回後置原先結果
}
理解:後置遞增較為耗時,因為內部發生值傳遞與拷貝操作
4.賦值運算子=過載
背景知識
C++編譯器至少給一個類新增4個函式
1.預設建構函式
2.預設解構函式
3.預設拷貝函式
4.賦值運算子operator=,對屬性進行值拷貝(若類存在到堆區的屬性,則涉及到深淺拷貝問題)
預設=運算子存在的問題:淺拷貝
若物件存在開闢在堆區的屬性,用預設=運算子賦值後,在析構時會導致堆區內容重複釋放,程式崩潰
解決方案:利用深拷貝
class Person
{
public:
int* age;
Person(int age)
{
this->age = new int(age);
}
~Person()
{
if (age != NULL)
{
delete age;
}
}
Person& operator=(const Person& p) //過載賦值運算子= 深拷貝
{
if (this->age != NULL) //判斷在堆區是否有記憶體,很重要:防止記憶體洩漏
{
delete this->age;
this->age = NULL;
}
this->age = new int(*p.age); //在堆區開闢新空間,拷貝值
return *this;//返回引用型別,鏈式程式設計思想
}
};
ostream& operator<<(ostream& cout, Person& p)
{
cout << *p.age << endl;
return cout;
}
int main()
{
Person p1(18),p2(19),p3(20);
p2 = p1 = p3;
cout << p1<<p2<<p3;
return 0;
}
5.關係運算符(<,==,>)過載
演算法題中常用
bool operator<(const Person& p)const
{
return age < p.age;
}
6.函式呼叫運算子()過載
函式呼叫運算子()也可以過載
由於過載後使用的方式非常像函式的呼叫,因此稱為仿函式
仿函式沒有固定寫法,非常靈活
class Myprint
{
public:
void operator()(string str)
{
cout << str << endl;
}
Myprint()
{
}
};
int main()
{
Myprint()("12345"); //匿名物件
Myprint p1;
p1("54321");
return 0;
}
七、繼承(OOP三大特徵之一)
有些類與類之間存在特殊的關係,下級別的成員除了擁有上一級的共性,還有自己的特性
此時需要考慮利用繼承的技術,減少重複程式碼
1.基本語法
class 子類:繼承方式 父類
{
};
2.概念
子類也稱派生類,父類也稱為基類
派生類中的成員包含從基類繼承而來的,以及自己特有的成員
從基類繼承而來的表現其共性,特有的成員表現其個性
3.繼承方式
一共有三種繼承方式:
- 公共繼承
- 保護繼承
- 私有繼承
父類中私有的成員只是被隱藏了,但還是會繼承下去
公共繼承
除父類中私有成員外,其他所有成員將會被顯式繼承,其訪問許可權保持不變
保護繼承
除父類中私有成員外,其他所有成員將會被顯式繼承,其訪問許可權變為protected
私有繼承
除父類中私有成員外,其他所有成員將會被顯式繼承,其訪問許可權變為private
4.繼承中構造和析構的順序
先構造父類,再構造子類
析構順序一般與構造順序相反
class Sub : public Base {
private:
int z;
public:
Sub(int x, int y, int z):Base(x,y){ //構造子類時,對父類建構函式寫法
this->z = z;
}
int getZ() {
return z;
}
int calculate() {
return Base::getX() * Base::getY() * this->getZ();
}
};
5.繼承同名成員處理方式
當子類中出現與父類同名的屬性或函式時,
訪問子類同名成員,直接訪問即可
訪問父類同名成員,需要加作用域
當子類與父類擁有同名的成員函式,子類會隱藏父類中同名成員函式,加作用域可以訪問到父類中同名函式
6.繼承同名靜態成員處理方式
靜態成員和非靜態成員出現同名,處理方式一致
- 訪問子類同名成員,直接訪問即可
- 訪問父類同名成員,需要加作用域
通過物件訪問
cout<<s.m_A<<endl;
cout<<s.Base::m_A<<endl;
通過類名訪問
cout<<Son::m_A<<endl;
cout<<Son::Base::m_A<<endl;
7.多繼承語法
C++允許一個類繼承多個類
實際開發中不建議使用,當父類中出現同名成員,需要加作用域區分
語法
class 子類:繼承方式1 父類1,繼承方式2 父類2…
{
};
8.菱形繼承(鑽石繼承)
兩個子類繼承同一個父類,又有某個類同時繼承兩個子類,這種繼承被稱為菱形繼承(鑽石繼承)
問題
- 羊繼承動物的資料,駝同樣繼承了動物的資料,當草泥馬使用資料時,會產生二義性
- 菱形繼承導致資料有兩份,資源浪費
解決方案:virtual
利用虛繼承virtual
class Animal
{
public:
int age;
};
class Sheep :virtual public Animal
{
};
class Tuo : virtual public Animal
{
};
class SheepTuo :public Sheep, public Tuo
{
};
int main()
{
SheepTuo st;
st.Sheep::age = 20;
st.Tuo::age = 15;
st.age = 21;
cout << st.age << endl;
return 0;
}
八、多型(OOP三大特性之一)
1.分類
靜態多型:函式過載和運算子過載屬於靜態多型,複用函式名
動態多型:派生類和虛擬函式實現執行時多型
區別
靜態多型的函式地址早繫結——編譯階段確定函式地址
動態多型的函式地址晚繫結——執行階段確定函式地址
class Animal
{
public:
int age;
void speak()
{
cout << "動物在說話" << endl;
}
};
class Cat:public Animal
{
void speak()
{
cout << "喵~" << endl;
}
};
//靜態多型:地址早繫結,在編譯階段確定函式地址
void DoSpeak(Animal &a)
{
a.speak();
}
int main()
{
Cat c1;
DoSpeak(c1);//動物在說話
return 0;
}
若想實現子類呼叫函式,那麼函式地址不能提前繫結,需要在執行階段進行繫結,地址晚繫結
class Animal
{
public:
int age;
virtual void speak() //改為虛繼承即可
{
cout << "動物在說話" << endl;
}
};
重寫:函式返回值 函式名 形參列表 完全相同
2.動態多型的滿足條件
- 有繼承關係
- 子類重寫父類的虛擬函式
3.動態多型的使用
父類的指標或引用 執行子類的物件
4.多型的原理剖析
vfptr:虛擬函式(表)指標(virtual function pointer)
vftable:虛擬函式表(virtual function table)
5.多型的好處
- 組織結構清晰
- 可讀性強
- 前期和後期擴充套件及維護性高
6.純虛擬函式和抽象類
在多型中,通常父類中虛擬函式的實現是毫無意義的,主要是呼叫子類重寫的內容
因此,可以將虛擬函式改為純虛擬函式
語法
virtual 返回值型別 函式名 (引數列表) = 0;
當類中有了純虛擬函式,此類也叫抽象類
抽象類特點:
- 無法例項化物件
- 子類必須重寫抽象類中的純虛擬函式,否則也屬於抽象類
7.虛析構和純虛析構
多型使用時,如果子類中有屬性開闢到堆區,那麼父類指標在釋放時無法呼叫到子類的析構程式碼
解決方法:將父類中的解構函式改為虛析構和純虛析構
共性
都需要具體的函式實現
可以解決父類指標釋放子類物件
不同
若是純虛析構,則該類屬於抽象類,無法例項化物件
語法
class Base
{
public:
virtual Base()
{
}
virtual ~Base() = 0;//純虛析構
virtual ~Base() //虛析構
{
}
}
Base::~Base()//純虛析構需要有宣告,也需要實現
{
}
例項分析
class Animal
{
public:
string* name;
Animal(string name)
{
cout << "Animal created!" <<endl;
this->name = new string(name);
}
virtual ~Animal() //虛析構
{
cout << "Animal deleted!" << endl;
if (name != NULL)
{
delete(name);
name = NULL;
}
}
};
class Cat :public Animal
{
public:
Cat(string name):Animal(name)
{
cout << "Cat created!" << endl;
this->name = new string(name);
}
~Cat()
{
cout << "Cat deleted!" << endl;
if (name != NULL)
{
delete(name);
name = NULL;
}
}
};
void doSpeak(Animal* a)
{
cout << *a->name << " is speaking!" << endl;
delete(a);
}
int main()
{
doSpeak(new Cat("Tom"));
return 0;
}
class Animal
{
public:
string* name;
Animal(string name)
{
cout << "Animal created!" <<endl;
this->name = new string(name);
}
virtual ~Animal() = 0;
};
Animal::~Animal() //純虛析構
{
cout << "Animal deleted!" << endl;
if (name != NULL)
{
delete(name);
name = NULL;
}
}
總結
如果子類中沒有堆區資料,可以不寫虛析構或純虛析構