複習 C++ 類(二)拷貝構造,賦值運算子,析構(1)
拷貝,賦值,銷燬
在類的基本概念一文中,有講過這三種操作
如果我們定義一個空類
class Empty
{
};
如果我們自己沒有宣告拷貝構造,拷貝賦值,解構函式,編譯器在處理之後會自動新增此三函式的預設版本
(當然如果沒有宣告任何建構函式,也會宣告一個default建構函式)
以上編譯器生成的操作都是inline和public的
上面的空類就如同:
class Empty
{
Empty(){}
Empty(const &Empty){}
Empty& operator=(Empty&){}
~Empty(){}
};
且只有被呼叫時,這些函式才被建立
//呼叫方法:
Empty p1;//default建構函式
Empty p2(p1);//copy建構函式
p2 = p1;//拷貝賦值操作符
拷貝建構函式
帶有預設引數的也可以是拷貝建構函式
class X
{
public:
X(const x&,int i = 1);
};
int main()
{
X b(a,0);
X c = b;
}
一般合成的拷貝建構函式會將給定物件的每個非static成員一次拷貝到建立的物件中
成員的型別決定拷貝的方式:
(1)類型別:使用其拷貝建構函式
(2)內建型別直接拷貝
(3)不能拷貝一個數組,但是可以逐個元素的拷貝陣列中的成員(如果陣列元素是類型別,則還會用到其拷貝建構函式)
呼叫拷貝建構函式
Circle c1(5,0);
//c++03:
Circle c2(c2);
//雖然使用=運算子,但是因為是定義時,所以也是呼叫拷貝建構函式
Circle c3 = c1;
//c++11:
Circle c4{c1};
Circle c5 = c1;
注意注意,=只有定義物件時,才是拷貝構造
//拷貝建構函式使用和定義例項 #pragma once #include<iostream> using namespace std; class Square { private: double side{1.0}; static int numberOfObjects;//建立物件的個數 public: Square():Square(1.0){}//代理構造(委託構造) Square(double side) { this->side = side; numberOfObjects++; } double getSide(){ return side; } void setSide(double side){ this->side = side; } double getArea() { return side*side; } static int getObjectNum() { return numberOfObjects; } //拷貝建構函式,此處將引數宣告為const的,防止在函式體中實參被意外修改 Square(const Square&rhs) { this->side = rhs.side; numberOfObjects++; cout<<"Squre(const Squre&)is invoked"<<endl; } ~Square() { numberOfObjects--; } };
#include"Square.h"
#include<iostream>
using namespace std;
int Square::numberOfObjects = 0;//靜態成員初始化
int main()
{
//一次輸出每次的物件個數
Square s1(10.0);
cout<<Square::getObjectNum()<<endl;
Square s2{s1};
cout<<Square::getObjectNum()<<endl;
//將s1傳遞給拷貝建構函式,建立一個匿名物件,放在堆區,並將匿名物件的地址存在指標中
Square *s3 = new Square{s1};
cout<<Square::getObjectNum()<<endl;
system("pause");
return 0;
}
拷貝建構函式也可以使用委託構造方式初始化成員
拷貝建構函式的引數數量可以是1-n個
淺拷貝 (shallow copy)
類的資料域是一個指標,只拷貝指標的地址,而非指向的內容
發生的兩種情況:
(1)使用隱式的建構函式
(2)使用=,為已有物件賦值的預設賦值運算子
e3使用預設拷貝建構函式建立,所以是淺拷貝,將e1的date型別指標拷貝給e3的date,這樣這物件種的date指標成員就會指向同一個Date物件(如上圖右側所示),如果此時修改e3,date指向的內容,那麼e1的內容也會改變
淺拷貝會導致物件的互相之間的干擾
深拷貝(deep copy)行為像值的類
要拷貝指標指向的內容
發生情況:過載拷貝賦值運算子,或編寫拷貝建構函式去拷貝指標指向的內容
Employee{
Employee(const Employee&e) = default;//淺拷貝
Employee(const Employee&e)//深拷貝,不是拷貝指標,而是拷貝指標指向的值
{
birthday = new Date(*e.birthday);
}
};
//這樣呼叫,就是深拷貝
Employee e3{e1};
深淺拷貝的問題是由類中的指標型別引起的
拷貝賦值運算子
如果一個運算子是成員函式,其左側運算物件就會被繫結到隱式的this引數
拷貝賦值運算子返回一個指向左側運算物件的引用
因為引用返回左值,其他型別返回右值
合成的版本將右側物件的每個非static成員賦予左側運算物件的相應成員
Sales_data& Sales_data&::operator=(const Sales_data&ths)
{
bookNo = rhs.bookNo;
units_sold = rhs.units_sold;
revenue = rhs.revenue;
return *this;
}
拒絕合成拷貝賦值運算子的情況
1.C++不允許引用更改指向的物件
2.且更改const成員也不合法,所以如果成員為引用或const的,則編譯器拒絕合成,我們只能自定義此操作符
3.如果本類的基類將=運算子置為private,派生類的合成拷貝運算子應該可以處理基類部分,但是如果無權呼叫基類=運算子,則拒絕合成
過載=,改變其預設工作方式
(需要深拷貝時)
如果想要使用a = b = c形式,就要返回引用型別
預設使用合成的版本時的淺拷貝
Employee e1{"Jack",Date{1999,5,3},Gender::male};
Employee e2{"Anna",Date{2000,11,8},Gender::female};
e2 = e1;
class Emplotee{
public:
Employee& operator=(const Employee&e)//過載的是Employee型別的賦值運算子
{
name = e.name;
this->gender = e.gender;
*this->birthday = *e.birthday;//拷貝的是所指的物件,而不是指標
}
};
解構函式
解構函式負責釋放物件使用的資源,銷燬物件的非static資料成員
解構函式不接收引數,所以不能過載,一個類只有一個
解構函式首先執行函式體,然後按初始化的逆序銷燬成員
成員銷燬時,依賴自身的型別,如果是類型別,則使用其解構函式,如果是內建型別,則什麼也不需要做
解構函式在以下情況自動呼叫:
![image-20211023154643151](C:\Users\god\AppData\Roaming\Typora\typora-user-images\image-
.png)
指向一個物件的引用或指標離開作用域時,解構函式不會執行
合成的解構函式
如果為空,則成員都會被自動銷燬(注意:不是函式體銷燬成員,而是在函式體之後隱式的析構階段中銷燬)
如果不為空,則可能是對於某些類,合成解構函式用於阻止該型別的物件被銷燬
//如果有以下宣告,則不允許編譯器合成解構函式
~C() = delete;
13.9
什麼時候會生成合成解構函式:
在一個類未定義自己的解構函式時,編譯器會定義一個合成解構函式
#pragma once
#include<iostream>
#include<string>
#include"Date.h"
enum class Gender
{
male ,
famale ,
};
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 } {
numberOfObjects++;
this->birthday = new Date(birthday);//在堆上建立一個Date物件,呼叫了Date的合成版本的拷貝建構函式
std::cout<<"Employee create"<<" now have"<<numberOfObjects<<" objects"<<std::endl;
}
Employee() : Employee("Alan" , Gender::male , Date(2000 , 4 , 1)) {}
~Employee()
{
numberOfObjects--;
//歸還記憶體
delete birthday;
birthday = nullptr;
std::cout<<"Employee delete"<<" now have"<<numberOfObjects<<" objects"<<std::endl;
}
};
#pragma once
#include<iostream>
#include<string>
class Date
{
private:
int year = 2019,month = 10,day = 5;//給初始值,因為預設建構函式沒有將成員初始化
public:
int getYear(){ return year; }
int getMonth(){ return month; }
int getDay(){ return day; }
void SetYear(int year){ this->year = year; }
void SetMonth(int month){ this->month = month; }
void SetDay(int day){ this->day = day; }
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));
}
};
#include<iostream>
#include"Date.h"
#include"Employee.h"
using namespace std;
int Employee::numberOfObjects = 0;
int main()
{
Employee e1;
std::cout<<e1.toString()<<endl;
Employee* e2 = new Employee("John",Gender::male,Date(1990,3,2));
std::cout<<e2->toString()<<endl;
//內嵌作用域
{
Employee e3{"Alice",Gender::famale,{1989,2,14}};
std::cout<<e3.toString()<<endl;//由於e3在內嵌作用域中定義,所以出了這個作用域,e3就被銷燬
}
//由於這裡有輸入的阻塞,所以程式未結束,所以e1,e2還沒有銷燬
cin.get();
return 0;
}
= default
顯式的要求編譯器生成合成版本的函式(也只能對具有合成版本的函式使用)
使用=default,合成的函式將會隱式的宣告為內聯的
如果不想內聯,則只在類外定義時使用 = default
class Sales_data
{
Sales_data() = default;
Sales_data(const Sales_data&) = default;
~Sales_data() = default;
Sales_data& operator=(const Sales_data&);
};
//如果不想宣告為內聯的:
Sales_data& Sales_data::operator=(const Sales_data&) = default;
學習的時候喜歡用Markdown做記錄,存貨已經堆滿檔案夾了