目录
- 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了,这时候编译器就会报错,如何解决这个问题呢?
这个问题我已经在慕课上提问了,等老师回复再做更新。
解决回复:
一般来说,普通构造函数中有为类成员分配内存的操作,那么拷贝构造函数、重载的赋值运算符函数均需要执行深拷贝。