1. 程式人生 > 其它 >複習 C++ 類(二)拷貝構造,賦值運算子,析構(1)

複習 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做記錄,存貨已經堆滿檔案夾了