目錄
- 1、Destructor(析構函數)
- 在堆和棧(函數作用域與內嵌作用域)上分別創建Employee對象,觀察析構函數的行為
- 2、Friend(友元)
- 1、為何需要友元
- 2、友元函數和友元類
- 3、關于友元的一些問題
- 3、Copy Constructor(拷貝構造函數)
- 拷貝構造
- 隱式聲明的拷貝構造函數
- 在堆和棧上分別拷貝創建Employee對象
- 4、深拷貝與淺拷貝
- 1、Customizing Copy Constructor(定制拷貝構造函數)
- 2、待解決的疑問
1、Destructor(析構函數)
析構函數與構造函數正好相反。
注意,重載函數以函數參數的個數以及順序來區分,析構函數沒有參數也就不可重載了。
在堆和棧(函數作用域與內嵌作用域)上分別創建Employee對象,觀察析構函數的行為
#include<iostream>
#include<string>
using namespace std;class Date {
private:int year = 2019, month = 1, day = 1;
public:int getYear() { return year; }int getMonth() { return month; }int getDay() { return day; }void setYear(int y) { year = y; }void setMonth(int m) { month = m; }void setDay(int d) { day = d; }Date() = default;Date(int y, int m, int d) :year(y), month(m), day(d) {std::cout << "Date" << toString() << std::endl;}std::string toString() {return (std::to_string(year) + '-' + std::to_string(month) + '-' + std::to_string(day));}
};enum class Gender {male,female,
};class Employee {
private:std::string name;Gender gender;Date* birthday;
public://靜態成員,用于計算雇員對象的數量static int numberOfObjects;void setName(std::string name) { this->name = name; }void setGender(Gender gender) { this->gender = gender; }void setBirthday(Date birthday) { *(this->birthday) = birthday; }std::string getName() { return name; }Gender getGender() { return gender; }Date getBirthday() { return *birthday; }std::string toString(){return (name +( (gender == Gender::male ? std::string(" male ") : std::string(" female ") )+ birthday->toString()));}//帶參構造函數Employee(std::string name,Gender gender,Date birthday):name{name},gender{gender}{//自增運算,完成每構造一次對象就數目+1numberOfObjects++;//注意,構造函數new出來的對象在析構函數要delete//在堆上構造了一個新的Date對象,然后存在數據成員里面,這樣就將new出來的新的data地址傳遞到了當前對象的birthday變量this->birthday = new Date(birthday);std::cout << "Now there are : " << numberOfObjects << " employees" << std::endl;}//默認構造函數Employee():Employee("Alan",Gender::male,Date(2000,4,1)){}//析構函數~Employee(){//當析構掉一個對象時,成員個數-1numberOfObjects--;//將在堆上面構造的變量釋放掉,由于這里沒有淺拷貝函數,不需要特別注意delete birthday;birthday = nullptr;std::cout << "析構掉一個->Now there are : " << numberOfObjects << " employees" << std::endl;}
};
int Employee::numberOfObjects = 0;
//在堆和棧(函數作用域與內嵌作用域)上分別創建Employee對象,觀察析構函數的行為
int main()
{Employee e1;std::cout << e1.toString() << std::endl;Employee* e2 = new Employee{"John",Gender::male,Date(1990,3,2) };std::cout << e2->toString() << std::endl;//e3是在內嵌作用域內定義的對象,出了這個作用域就被析構了。{Employee e3{ "Alice",Gender::female,{1989,2,14} };std::cout << e3.toString() << std::endl;}std::cout << "Now there are : " << Employee::numberOfObjects << " employees" << std::endl;return 0;
}
e3是在內嵌作用域內定義的對象,出了這個作用域就被析構了。
2、Friend(友元)
1、為何需要友元
1、私有成員無法從類外訪問
2、但有時又需要授權某些可信的函數和類訪問這些私有成員
2、友元函數和友元類
1、用friend關鍵字聲明友元函數或者友元類
2、友元的缺點:打破了封裝性
3、可以在類外面定義,但是必須在類里面聲明。
下面的例子中,Kid類和print函數都可以直接訪問Date類中的私有成員
class Date {
private:int year{ 2019 } , month{ 1 };int day{ 1 };
public:friend class Kid;friend void print(const Date& d);
};
void print(const Date& d) {cout << d.year << "/" << d.month << "/" << d.day << endl;
}
class Kid {
private:Date birthday;
public:Kid() { cout << "I was born in " << birthday.year << endl; }
};
int main() {print(Date());Kid k;cin.get();
}
3、關于友元的一些問題
1、兩個類可以互為友元類嗎?如果你能舉出例子就更好了
2、其它的面向對象編程語言中,有friend這種東西或者類似的東西嗎?
3、一個類可以有友元,友元能夠訪問這個類中的私有/保護成員;那么,一個函數是否可以有友元,通過友元訪問這個函數中的局部變量?
1、可以。
我們可以把Screen類聲明為Window類的友元類,同時把Window類也聲明為Screen類的友元類。這樣兩個類的成員函數就可以相互訪問對方的私有和保護成員了。
2、沒有
3、不可以
3、Copy Constructor(拷貝構造函數)
拷貝構造
拷貝構造:用一個對象初始化另一個同類對象
拷貝構造函數可以簡寫為 copy ctor,或者 cp ctor。
如何聲明拷貝構造函數(copy ctor)
Circle (Circle&);
Circle (const Circle&);
Circle c1( 5.0 );
Circle c2( c1 ); //c++03
Circle c3 = c1; //c++03
Circle c4 = { c1 }; //c++11
Circle c5{ c1 }; //c++11
帶有額外的默認參數的拷貝構造函數
class X { //來自C++11標準: 12.8節
// ...
public:X(const X&, int = 1);
};
X b(a, 0); // calls X(const X&, int);
X c = b; // calls X(const X&, int);
兩個對象obj1和obj2已經定義。然后這種形式的語句:
obj1 = obj2;
不是調用拷貝構造函數,而是對象賦值。
反之,如下語句:
AClass aObject = bObject; // bObject也是AClass類型的對象
雖然有“等號(=)”,但由于是在定義對象的時候“賦值”,此時的“等號(=)”被解釋為初始化,需要調用拷貝構造函數。
隱式聲明的拷貝構造函數
一般情況下,如果程序員不編寫拷貝構造函數,那么編譯器會自動生成一個。自動生成的拷貝構造函數叫做“隱式聲明/定義的拷貝構造函數”。
一般情況下,隱式聲明的copy ctor簡單地將作為參數的對象中的每個數據域復制到新對象中。
在堆和棧上分別拷貝創建Employee對象
#include<iostream>
#include<string>
using namespace std;class Date {
private:int year = 2019, month = 1, day = 1;
public:int getYear() { return year; }int getMonth() { return month; }int getDay() { return day; }void setYear(int y) { year = y; }void setMonth(int m) { month = m; }void setDay(int d) { day = d; }Date() = default;Date(int y, int m, int d) :year(y), month(m), day(d) {std::cout << "Date" << toString() << std::endl;}std::string toString() {return (std::to_string(year) + '-' + std::to_string(month) + '-' + std::to_string(day));}
};enum class Gender {male,female,
};class Employee {
private:std::string name;Gender gender;Date* birthday;
public://靜態成員,用于計算雇員對象的數量static int numberOfObjects;void setName(std::string name) { this->name = name; }void setGender(Gender gender) { this->gender = gender; }void setBirthday(Date birthday) { *(this->birthday) = birthday; }std::string getName() { return name; }Gender getGender() { return gender; }Date getBirthday() { return *birthday; }std::string toString(){return (name + ((gender == Gender::male ? std::string(" male ") : std::string(" female ")) + birthday->toString()));}//帶參構造函數Employee(std::string name, Gender gender, Date birthday) :name{ name }, gender{ gender }{//自增運算,完成每構造一次對象就數目+1numberOfObjects++;//注意,構造函數new出來的對象在析構函數要delete//在堆上構造了一個新的Date對象,然后存在數據成員里面,這樣就將new出來的新的data地址傳遞到了當前對象的birthday變量this->birthday = new Date(birthday);std::cout << "Now there are : " << numberOfObjects << " employees" << std::endl;}//默認構造函數Employee() :Employee("Alan", Gender::male, Date(2000, 4, 1)) {}//拷貝構造函數Employee(const Employee& e1) {this->birthday = e1.birthday;this->name = e1.name;this->gender = e1.gender;//個數也需要+1numberOfObjects++;std::cout << "Employee(const Employee&) is invoked" << std::endl;}//析構函數~Employee(){//當析構掉一個對象時,成員個數-1numberOfObjects--;//注意如果析構的是淺拷貝函數且被拷貝對象已經被delete了,則不需要delete這個數據//delete birthday;//birthday = nullptr;std::cout << "析構掉一個->Now there are : " << numberOfObjects << " employees" << std::endl;}
};
int Employee::numberOfObjects = 0;
//在堆和棧上分別拷貝創建Employee對象
int main()
{//默認構造Employee e1;std::cout << e1.toString() << std::endl;//拷貝構造Employee e2 = {e1};std::cout << e2.toString() << std::endl;//在堆上構造Employee* e3 = new Employee{ "John",Gender::male,Date(1990,3,2) };std::cout << e3->toString() << std::endl;std::cout << std::endl;std::cout << "Now there are : " << Employee::numberOfObjects << " employees" << std::endl;return 0;
}
4、深拷貝與淺拷貝
由于上面的拷貝函數,我們是將一個對象的所有數據成員否賦值給一個新的對象,所以會出現一個問題。
如果一個數據成員是指針類型(地址),那么我們新構造的對象的這個數據的地址也是這個。
對于非地址數據,則不會有這個問題。
我感覺,這也是拷貝函數的一個漏洞,一般來說我直觀理解的拷貝就是深拷貝而非淺拷貝。
淺拷貝:數據域是一個指針,只拷指針的地址,而非指針指向的內容
在兩種情況下會出現淺拷貝:
創建新對象時,調用類的隱式/默認構造函數
為已有對象賦值時,使用默認賦值運算符
深拷貝:拷貝指針指向的內容
解釋:
前提條件:類A中有個指針p,指向一個外掛對象b(b是B類型的對象);如果類A里面沒有指針成員p,那也就不要談深淺拷貝了。
現在有一個類A的對象a1(a1的指針p指向外掛對象b1)。以拷貝構造的方式,創建a1的一個拷貝a2。
(1) 如果僅僅將a1.p的值(這個值是個地址)拷貝給 a2.p,這就是淺拷貝。淺拷貝之后,a1.p和a2.p都指向外掛對象 b1
(2) 如果創建一個外掛對象b2,將 a2.p指向b2;并且將b1的值拷貝給b2,這就是深拷貝
Employee e1{"Jack", Date(1999, 5, 3), Gender::male};
Employee e2{"Anna", Date(2000, 11, 8), Gender:female};
Employee e3{ e1 }; //cp ctor,執行一對一成員拷貝
創建 e3 對象時,調用了Employee的拷貝構造函數。
上面的代碼執行之后,e3.birthday指針指向了 e1.birthday所指向的那個Date對象,這樣會導致修改e1,e2對象也會被修改。
1、Customizing Copy Constructor(定制拷貝構造函數)
如何深拷貝
(1) 自行編寫拷貝構造函數,不使用編譯器隱式生成的(默認)拷貝構造函數
(2) 重載賦值運算符,不使用編譯器隱式生成的(默認)賦值運算符函數
此時我們根據被拷貝對象來生成一個新的對象,然后把這個對象賦給拷貝對象。
class Employee {
public:// Employee(const Employee &e) = default; //淺拷貝ctorEmployee(const Employee& e){ //深拷貝ctorbirthdate = new Date{ e.birthdate };} // ...
}
Employee e1{"Jack", Date(1999, 5, 3), Gender::male};
Employee e2{"Anna", Date(2000, 11, 8),, Gender:female};
Employee e3{ e1 }; //cp ctor 深拷貝
2、待解決的疑問
有關淺拷貝對象以及它的析構的一個問題
如果我們使用淺拷貝構造函數:
Employee(const Employee& e1) {
this->birthday = e1.birthday;this->name = e1.name;this->gender = e1.gender;//個數也需要+1numberOfObjects++;std::cout << "Employee(const Employee&) is invoked" << std::endl;}
然后我們在主函數用到了這個淺拷貝構造函數,由于我們帶參構造函數是在堆new了一個新的數據對象
//帶參構造函數Employee(std::string name, Gender gender, Date birthday) :name{ name }, gender{ gender }{//自增運算,完成每構造一次對象就數目+1numberOfObjects++;//注意,構造函數new出來的對象在析構函數要delete//在堆上構造了一個新的Date對象,然后存在數據成員里面,這樣就將new出來的新的data地址傳遞到了當前對象的birthday變量this->birthday = new Date(birthday);std::cout << "Now there are : " << numberOfObjects << " employees" << std::endl;}
所以在析構函數中我們會delete這個數據
delete birthday;birthday = nullptr;
那么問題來了:
我們在delete拷貝構造出來的對象時,如果它指向對象已經被析構了,也就是說birthday 已經被delete了,這時候編譯器就會報錯,如何解決這個問題呢?
這個問題我已經在慕課上提問了,等老師回復再做更新。
解決回復:
一般來說,普通構造函數中有為類成員分配內存的操作,那么拷貝構造函數、重載的賦值運算符函數均需要執行深拷貝。