1. 程式人生 > 其它 >C++ 第四章 類和物件

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.菱形繼承(鑽石繼承)

兩個子類繼承同一個父類,又有某個類同時繼承兩個子類,這種繼承被稱為菱形繼承(鑽石繼承)

問題

  1. 羊繼承動物的資料,駝同樣繼承了動物的資料,當草泥馬使用資料時,會產生二義性
  2. 菱形繼承導致資料有兩份,資源浪費

解決方案: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.動態多型的滿足條件

  1. 有繼承關係
  2. 子類重寫父類的虛擬函式

3.動態多型的使用

父類的指標或引用 執行子類的物件

4.多型的原理剖析

vfptr:虛擬函式(表)指標(virtual function pointer)
vftable:虛擬函式表(virtual function table)

5.多型的好處

  1. 組織結構清晰
  2. 可讀性強
  3. 前期和後期擴充套件及維護性高

6.純虛擬函式和抽象類

在多型中,通常父類中虛擬函式的實現是毫無意義的,主要是呼叫子類重寫的內容
因此,可以將虛擬函式改為純虛擬函式

語法

virtual 返回值型別 函式名 (引數列表) = 0;

當類中有了純虛擬函式,此類也叫抽象類

抽象類特點:

  1. 無法例項化物件
  2. 子類必須重寫抽象類中的純虛擬函式,否則也屬於抽象類

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;
	}
}

總結

如果子類中沒有堆區資料,可以不寫虛析構或純虛析構