【C++的面向對象】C++的構造函數和析構函數詳解


一,典型的C++面向對象編程


1、三要素
(1)頭文件hpp中類的定義
(2)源文件cpp中類的實現(構造函數、析構函數、方法)
(3)主程序

2、案例
(1)用C++來編程“人一天的生活”
(2)“人”的屬性:name、age、male
(3)“人”的方法:eat、work(coding/shopping)、sleep
(4)人的生活:eat->work->sleep
實戰中,一般一個cpp和一個hpp文件配對,描述一個class,class的名字和文件名相同的。

person.hpp

#ifndef __PERSON_H__
#define __PERSON_H__

#include <string>
using namespace std;

// 聲明這個類
class person
{
// 訪問權限
public:
	// 屬性
	string name;			// 名字
	int age;				// 年齡
	bool male;				// 性別,男爲true,女爲false
	
	// 方法
	void eat(void);
	void work(void);
	void sleep(void);
		
private:

};
#endif

person.cpp

#include "person.hpp"
#include <iostream>
using namespace std;

// class的成員函數中可以引用class的成員變量,但是要考慮public和private這些訪問限制
void person::eat(void)
{
	cout << name << " eat" << endl;
}

void person::work(void)
{
	if (this->male)
	{
		cout << this->name << " coding" << endl;
	}
	else
	{
		cout << this->name << " shopping" << endl;
	}
}

void person::sleep(void)
{
	cout << this->name << " sleep" << endl;
}

main.cpp

#include "person.hpp"

int main(void)
{
	// 人的一天的生活
	person zhangsan;			// 創建了一個person的對象,分配在棧上
	
	zhangsan.name = "zhangsan";
	zhangsan.age = 23;
	zhangsan.male = 1;
	
	zhangsan.eat();
	zhangsan.work();
	zhangsan.sleep();
		
	return 0;
}

makefile

all:
	g++ person.cpp main.cpp -o app

3、C++面向對象式編程總結
(1)整個工作分爲2大塊:一個是建模和編寫類庫,一個是使用類庫來編寫主程序完成任務。
(2)有些人只負責建模和編寫類庫,譬如開發opencv的人。
(3)有些人直接調用現成類庫來編寫自己的主任務程序,譬如使用opencv分析一張圖片中有沒有電動車。
(4)難度上不確定,2個都可能很難或者很簡單。

4、C++學習的三重境界
(1)學習C++第一重境界就是語法層面,先學會如何利用C++來建模、來編程,學習語法時先別解決難度大的問題。
(2)學習C++第二重境界是解決問題層面,學習如何理解並調用現成類庫來編寫主程序解決問題。
(3)學習C++第三重境界是編寫類庫和sample給別人用,需要基礎好且有一定架構思維。


二,C++的構造函數和析構函數


1.構造函數和析構函數的引入


1、什麼是構造函數
(1)constructor,字面意思是用來構造對象的函數;destructor,字面意思是用來析構對象的函數
(2)可以理解爲語言自帶的一種hook函數(回調函數)。
用一個例子說明一下到底說明是回調函數:

你到一個商店買東西,剛好你要的東西沒有貨,於是你在店員那裏留下了你的電話,過了幾天店裏有貨了,店員就打了你的電話,然後你接到電話後就到店裏去取了貨。

在這個例子裏,你的電話號碼就叫回調函數你把電話留給店員就叫登記回調函數店裏後來有貨了叫做 觸發回調事件店員給你打電話叫做 調用回調函數你到店裏去取貨叫做 響應回調事件

(3)當對象產生時constructor會自動被調用,一般用於初始化class的屬性、分配class內部需要的動態內存
(4)對對象消亡時destructor會自動被調用,一般用於回收constructor中分配的動態內存,避免內存丟失

2、構造和析構一般用法
(1)不寫時C++會自動提供默認的構造和析構,也可以顯式提供默認構造和析構函數

/*-----person.hpp------*/
	person();				// 默認構造函數
	person(string name);	// 自定義構造函數
	~person();				// 默認析構函數
/*-----person.cpp------*/
person::person()
{
	// 默認構造函數是空的
	cout << "default constructor" << endl;
}

person::person(string name)
{
	// 自定義構造函數
	this->name = name;			// 構造對象後,同時對對象中的name屬性進行初始化
	cout << "userdefined constructor" << endl;
}

person::~person()
{
	// 默認析構函數是空的
}

(2)構造和析構函數不需要返回值類型,構造函數可以帶參或不帶參,析構函數不帶參
(3)構造函數可以重載(overload),析構函數不需要重載

3、爲什麼需要構造函數和析構函數

(1)構造函數可以看作是對象的初始化式,注意對比對象和變量的初始化區別。
(2)構造函數可以爲對象完成動態內存申請,同時在析構函數中再釋放,形成動態內存的完整使用循環。
(3)C語言中struct無構造函數概念,所以struct中需要用到動態內存時必須在定義struct變量後再次單獨申請和釋放,而這些操作都需要程序員手工完成。
(4)C++ class的構造和析構特性,是C++支持面向對象編程的一大語言特性。


2.在構造和析構函數中使用動態內存


1、析構函數的使用
析構函數在對象對銷燬時自動調用,一般有下面幾種情況

()分配在棧上的對象,當棧釋放時自動析構

/*-----person.cpp------*/
person::person()
{
	cout << "默認構造函數" << endl;
}
person::person(string name)
{
	this->name = name;			// 構造對象後,同時對對象中的name屬性進行初始化
	cout << "自定義構造函數" << endl;
}

person::~person()
{
	cout << "析構函數" << endl;
}

/*-----main.cpp------*/
    person Person;      //創建一個對象在棧上
	Person.age = 23;
	Person.male = 1;

	Person.eat();
	Person.work();

在這裏插入圖片描述
(2)用new分配的對象,用delete顯式析構

/*-----main.cpp------*/
	string s1 = "linux";
	person *pPerson = new person(s1);	

	pPerson->age = 23;
	pPerson->male = 1;
	pPerson->eat();
	pPerson->work();
	
// 用完對象後就銷燬它
	delete pPerson;

在這裏插入圖片描述
(3)普通情況下析構函數都是空的,因爲不必做什麼特別的事情

2、在class中使用動態內存變量
(1)什麼情況下用動態內存?需要大塊內存,且需要按需靈活的申請和釋放,用棧怕爆、用全局怕浪費和死板時
(2)在class person中增加一個int *指針,用於指向一個int類型元素的內存空間,將動態內存從int變量升級到int數組變量
(3)在構造函數中分配動態內存
(4)在析構函數中回收動態內存
(5)實戰中C++常用的動態內存往往是容器vector那些,後面會講到

/*-----person.cpp------*/
person::person(string name)
{
	this->name = name;			// 構造對象後,同時對對象中的name屬性進行初始化
	
	 //在構造函數中對class中需要分配動態內存的指針進行動態分配
	this->pInt = new int(55);        //分配了一個int型元素的4個字節的內容,並把它初始化爲55
	this->pInt = new int[100];		// 分配了100個int元素的數組
	
	cout << "自定義構造函數" << endl;
}

person::~person()
{
	delete this->pInt;				// 釋放單個內存
	delete[] this->pInt;			// 釋放數組內存
	cout << "析構函數" << endl;
}

/*-------------申請到的內容可以在方法裏面使用--------------*/
void person::sleep(void)
{
	cout << "value of this->pInt = " << *(this->pInt) << endl;
	
	for (int i = 0; i<100; i++)
	{
		this->pInt[i] = i;
	}
	
	for (int i = 0; i<100; i++)
	{
		cout << "pInt[" << i << "] = " << pInt[i] << endl;
	}
	
	cout << this->name << " sleep" << endl;
}

3、用valgrind工具查看內存泄漏
(1)valgrind工具介紹:參考:valgrind工具的使用
(2)安裝:sudo apt-get install valgrind(ubuntu16.04 X64)
(3)編譯程序:主要是添加-g參數便於調試時有行號 g++ person.cpp main.cpp -g -o app
(4)使用:valgrind --tool=memcheck --leak-check=full --show-reachable=yes --trace-children=yes ./app

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


3.構造函數與類的成員初始化


1、構造函數一大功能就是初始化成員變量
(1)默認構造函數不帶參,無初始化功能
(2)若無其他構造函數,則默認構造函數可以省略。但若有哪怕1個其他構造函數,則默認構造函數不能省,必須寫上,否則創建默認對象的時候就無法調用默認構造函數。
(3)棧上分配對象時,若使用默認構造函數,則對象變量後面不加空的(),若用帶參構造才需要加(初始化參數)。

/*-----main.cpp------*/

string s1 = "zhu";
person person;   //在棧上創建默認的不帶參數的對象,調用的是默認的構造參數
person Person();  //報錯,編譯器會以爲person()是一個函數
person person(s1);	//在棧上創建帶參數的對象,調用的是自定義的構造函數	

2、C++的成員初始化列表
(1)一般用於帶參構造函數中,用來給屬性傳參賦值
(2)成員初始化列表和構造函數之間用冒號間隔,多個列表項之間用逗號間隔。
(3)初始化列表可以替代構造函數內的賦值語句,達到同樣效果

/*-----person.hpp------*/
class person
{
public:
 //成員變量
	string name;			// 名字
	int age;				// 年齡
	bool male;				// 性別,男爲true,女爲false

	person(){};				// 默認構造函數
	person(string myname, int myage, bool mymale);
	~person();				// 默認析構函數
	
	// 方法
	void print(void);	
private:
};

/*-----person.cpp------*/
                                                 //:成員變量(形參) ,成員變量(形參),成員變量(形參)
person::person(string myname, int myage, bool mymale):name(myname),age(myage),male(mymale)
{
	cout << "userdefined constructor" << endl;
}

// 打印出對象中所有成員的值
void person::print(void)
{
	cout << "name = " << name << endl;
	cout << "age = " << age << endl;
	cout << "male = " << male << endl;
}

:person::~person()
{
	cout << "userdefined destructor" << endl;
}

/*-----main.cpp------*/

	string s1 = "zhu";
	person pPerson(s1, 35, true);			// 創建了一個person的對象,並傳參
	pPerson.print();

person::person(string myname, int myage, bool mymale):name(myname),age(myage),male(mymale)
{
}

等價於
person::person(string myname, int myage, bool mymale)
{
	this->name = myname;
	this->age = myage;
	this->male = mymale;
}

3、構造函數使用參數默認值
(1)class聲明時可以給函數形參賦值一個默認值,實際調用時若不傳參就使用默認值

person(string myname, int myage , bool mymale = false);   
person(string myname , int myage = 33, bool mymale = false);
person(string myname = "aston", int myage = 33, bool mymale = false);

 //賦值默認參數時只能從後面往前面賦值
person(string myname = "aston", int myage , bool mymale);   //錯誤

(2)方法實現時形參可以不寫默認值,但是實際是按照聲明時的默認值規定的
(3)有默認值情況,要注意實際調用不能有重載歧義否則編譯不能通過

/*-----person.hpp------*/
person(){};				// 默認構造函數
person(string myname = "aston", int myage = 33, bool mymale = false);

/*-----main.cpp------*/
person person;       //編譯器報錯,有歧義,編譯器不知道你是要調用默認構造函數還是調用有三個默認值的構造函數

(4)所有參數都帶默認值的構造函數,1個可以頂多個構造函數(舉例說明)

/*-----person.hpp------*/
//只用定義一個全部帶默認參數的構造函數
person(string myname = "aston", int myage = 33, bool mymale = false);

/*-----main.cpp------*/
person person;       
person person("aston");
person person("aston",35);
person person("aston",35,false);   //都可以調用

三,拷貝構造函數的引入


1、初始化變量和對象的本質
(1)簡單變量定義時,可以直接初始化,也可以用另一個同類型變量來初始化

int a = 4;		// 直接初始化,用一個值來直接對新定義的變量初始化
int b = a;		// 間接初始化,也就是用另一個變量來初始化這個剛新定義的變量

原理:變量的直接初始化,是變量在被分配內存之後直接用初始化值去填充賦值完成初始化;變量用另一個變量來初始化,是給變量分配了內存後執行了一個內存複製操作來完成的初始化

(2)用class來定義對象時,可以直接初始化,也可以用另一個對象來初始化

	// 方式1:直接初始化
	person p1("aston", 35, true);
	
	// 方式2:用另一個對象來初始化新定義的對象
	person p2(p1);		// p2是新對象,並且值已經被用p1來初始化了
	person p2 = p1;		// 和上面的寫法本質上是一樣的

原理:對象的直接初始化,是對象在分配內存之後調用了相應構造函數來完成的初始化;對象的用另一個對象來初始化,是對象在分配之後調用了相應的拷貝構造函數來完成初始化。

2、拷貝構造函數
(1)拷貝構造函數是構造函數的一種,符合構造函數的一般性規則
(2)拷貝構造函數的引入是爲了讓對象在初始化時能夠像簡單變量一樣的被直接用=來賦值
(3)拷貝構造函數不需要重載,他的參數列表固定爲const classname& xx
(4)拷貝構造函數很合適用初始化列表來實現

person(const person& pn);		// 默認拷貝構造函數聲明

person::person(const person& pn):name(pn.name),age(pn.age),male(pn.male)    //默認拷貝構造函數的定義
{
	/*
	this->name = pn.name;
	this->age = pn.age;
	this->male = pn.male;
	*/
	cout << "copy constructor" << endl;
}

1.淺拷貝與深拷貝


1、淺拷貝的缺陷

(1)上節講的只有普通成員變量初始化(沒有動態分配內存)的拷貝構造函數,就是淺拷貝
(2)如果不顯式提供,C++會自動提供一個全部普通成員被淺拷貝的默認拷貝構造函數
(3)淺拷貝在遇到有動態內存分配時就會出問題

/*-----person.hpp------*/
class person
{
public:
	
	string name;			
	int age;				
	bool male;				
	int *pInt;				// 只是分配了p本身的4字節內存,並沒有分配p指向的空間內存
	
	person(string myname, int myage, bool mymale);    //帶參數構造函數
	person(const person& pn);		// 拷貝構造函數
	~person();				     // 默認析構函數

private}

/*-----person.cpp------*/
person::person(string myname, int myage, bool mymale):name(myname),age(myage),male(mymale)    //自定義構造函數
{
	this->pInt = new int(5);		// 分配了1個int元素
}

// 默認拷貝構造函數
person::person(const person& pn):name(pn.name),age(pn.age),male(pn.male)
{
}
person::~person()
{
	delete this->pInt;				// 釋放單個內存
}

/*-----main.cpp------*/
person p1("aston", 35, true);
person p2 = p1;			
*p1.pInt = 44;
cout << *p2.pInt << endl;     //發生段錯誤,原因是解引用野指針

2、如何解決
(1)不要用默認拷貝構造函數,自己顯式提供一個拷貝構造函數,並且在其內部再次分配動態內存
(2)這就叫深拷貝,深的意思就是不止給指針變量本身分配內存一份,也給指針指向的空間再分配內存(如果有需要還要複製內存內的值)一份。

// 提供深拷貝構造函數
MAN::person::person(const person& pn):name(pn.name),age(pn.age),male(pn.male)
{
	pInt = new int(*pn.pInt);			// 深拷貝
}

(3)一般如果不需要深拷貝,根本就不用顯式提供拷貝構造函數,所以提供了的基本都是需要深拷貝的。
(4)拷貝構造函數不需要額外的析構函數來對應,用的還是原來的析構函數

3、如何深度理解淺拷貝和深拷貝
(1)這個問題不是C++特有的,Java等語言也會遇到,只是語言給封起來了,而C++需要類作者自己精心處理。
(2)從編程語言學角度講,本質上是值語義和引用語義的差別。值語義(value symatics):例如int a = 1;定義a的時候同時給a分配4字節的空間引用語義(reference symatics)例如:double *p ;定義時只給p本身分配了4字節的內存,而P所指向的內存地址要程序員分配

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章