【C++grammar】析构、友元、拷贝构造函数、深浅拷贝

233 阅读6分钟

目录

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}{
        //自增运算,完成每构造一次对象就数目+1
        numberOfObjects++;
        //注意,构造函数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()
    {
        //当析构掉一个对象时,成员个数-1
        numberOfObjects--;
        //将在堆上面构造的变量释放掉,由于这里没有浅拷贝函数,不需要特别注意
        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 }{
        //自增运算,完成每构造一次对象就数目+1
        numberOfObjects++;
        //注意,构造函数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;
        //个数也需要+1
        numberOfObjects++;
        std::cout << "Employee(const Employee&) is invoked" << std::endl;
    }
    //析构函数
    ~Employee()
    {
        //当析构掉一个对象时,成员个数-1
        numberOfObjects--;
        //注意如果析构的是浅拷贝函数且被拷贝对象已经被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; //浅拷贝ctor
  Employee(const Employee& e){    //深拷贝ctor
    birthdate = 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;

    //个数也需要+1

    numberOfObjects++;

    std::cout << "Employee(const Employee&) is invoked" << std::endl;

}

然后我们在主函数用到了这个浅拷贝构造函数,由于我们带参构造函数是在堆new了一个新的数据对象

//带参构造函数

Employee(std::string name, Gender gender, Date birthday) :name{ name }, gender{ gender }{

    //自增运算,完成每构造一次对象就数目+1

    numberOfObjects++;

    //注意,构造函数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了,这时候编译器就会报错,如何解决这个问题呢?
这个问题我已经在慕课上提问了,等老师回复再做更新。

解决回复:

一般来说,普通构造函数中有为类成员分配内存的操作,那么拷贝构造函数、重载的赋值运算符函数均需要执行深拷贝。