系列文章目录
C++学习笔记
- C++学习笔记(1)——C++的诞生
- C++学习笔记(3)——缺省函数与函数重载
- C++学习笔记(4)——引用
- C++学习笔记(5)——inline内联
- C++学习笔记(6)——auto关键字
- C++学习笔记(7)——类与对象(上)
- C++学习笔记(8)——类与对象(中)
- C++学习笔记(9)——类与对象(中上)类的6个默认成员函数
文章目录
前言
比喻与理解
1、拷贝构造
打个比方,拷贝构造造成无穷递归的情况,就像是:我们想要一面镜子里照出画面,采用的方法却是通过另一面镜子和它对照,如此这般,用无穷面镜子一个接一个的两两对照,指望通过镜子两两对照来照出画面。
拷贝构造被调用就需要一个对象当参数,如果我们没有给一个对象当参数,那么编译器就会自产一个对象当参数,为了获得这个自产的对象当参数,编译器就会启动新的拷贝构造去试图产生这个对象,可新的拷贝构造也需要一个对象当参数,为了启动这个新的拷贝构造就需要再次自产对象当参数,而这又要启动一个新的拷贝构造。。。。。。结果就是启动了无穷个拷贝构造,它们都希望自己生成的下一个拷贝构造可以产生一个对象参数给自己当参数用,然而永远不可能产生一个参数给它用,因此形成了无穷递归调用。
2、赋值运算符重载
打个比方,赋值运算符重载就像是两个工程师拿同一份图纸盖房子,程序员就是工程师,程序员编写的类就是图纸,编译器就是工头,程序运行就是工人盖房子的过程。
两个工程师拿着相同的图纸(图纸内容相同,只有署名不同)去盖两个房子,他们两人雇佣了同一批工人,工人在盖第一个房子的时候没有问题,然而当工人在盖第二个房子的时候发现图纸和上一个一模一样,认为是工程事故于是拒绝施工,除非工程师到场确定造两次否则不开工,然而工程师偷懒没去,结果工人自动由此确定只需盖一座房子就可以完成任务了,于是对着一个房子拍了两张照片发给工头交差,工程师收到照片也认为两个房子造好了于是拿去售楼部卖;
(赋值运算符如果没有显示定义,就会自动生成赋值运算符重载来进行浅拷贝,浅拷贝只会直接拷贝地址和值,不会自动开辟空间存储内存信息)
后来,一家住户入住时没有发现问题,但当两家住户入住的时候发现只有一个房子可以住,就把前一家住户赶出来了;
(由于没有开辟新的空间,只是把地址赋给了两个指针,结果只能存一份后来的那个信息)
再后来,拆迁队拆房的时候发现一个房子要拆两次,自己还补了两份拆迁款,于是报警抓人;
(由于地址存了两份,当我们试图释放内存时会出现把同一份空间释放两次的情况,这是时候编译器会报错中止程序运行。)
一、拷贝构造函数——从真实存在的对象里面提取内容填充到我们假设的对象内
1、概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。(即,根据该单个形参来创建该对象的深拷贝)
2、特征
二、使用步骤
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
代码如下(示例):
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d) //正确写法,这里“&”符号确定了把d1作为被拷贝的对象
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//错误写法,没有显示定义被拷贝的对象(即,没说清楚拷贝谁),编译器会自动去生成d,
//然而生成的d并不是在main里定义的一个实际存在的对象,而是编译器提出的一个假设存在的对象,为了使这个假设的对象成真,编译器就会调用拷贝对象试图拷贝真实的内容填充进这个假设对象里面,但编译器没有一个真实存在的对象给它拷贝,所以又调用拷贝构造,结果无限循环。
//Date(const Date d)
//{
// _year = d._year;
// _month = d._month;
// _day = d._day;
//}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1); //调用拷贝构造
return 0;
}
这个错误就是个无底洞,走不通,编译器会自动提示这种错误。
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象只考虑内存存储不调用成员函数,内存拷贝以字节为单位进行拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
#include<iostream>
using namespace std;
class Time
{
public:
void Printf()
{
cout << "Time" << endl;
}
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
Date d2(d1);
return 0;
}
可以看到,当我们没有写显式定义时编译器只进行了值拷贝,没有调用成员函数。
- 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了。我们只需要对类似于栈这些需要深拷贝的类编写拷贝函数。
注意:类中如果没有涉及资源申请时,拷贝构造函数可以不写;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
- 拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用。
二、赋值运算符重载
1、运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this
.*
::sizeof
?:
.
注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
如下代码,将==运算符重载为了可处理成员变量的运算符。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1 == d2) << endl;
return 0;
}
2、赋值运算符重载
1. 赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
如下代码,我们将赋值运算符=重载为成员变量的赋值运算,d1的_day重载为d2的_day
#include<iostream>
#include<stdio.h>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1) //构造函数
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d) //赋值运算符重载
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
d1 = d2;
return 0;
}
2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
即,一个类可以给他的赋值运算符的功能做个性化定义,而在其他地方,赋值运算符会恢复它本来的作用。在全局域,运算符它本来的作用是不可以被更改的。
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
如果我们没有编写赋值运算符进行赋值操作,但我们的编译器自动生成了赋值操作符完成了赋值操作,对于比较简单的赋值操作,我们可以省略赋值操作符。
还有一种情况,可以通过构造函数间接调用其他类的赋值运算符重载。
如下代码,d1=d2执行时,自动使用了Time重载过的赋值运算符。
#include<iostream>
using namespace std;
class Time
{
public:
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
}
return *this;
}
private:
int _hour;
int _minute;
};
class Date
{
public:
Date(int m = 1,int n=2)
{
_year = m;
_month = n;
}
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2(6,6);
d1 = d2;
return 0;
}
注意:如果类中未涉及到资源管理,赋值运算符重载可以不写;一旦涉及到资源管理则必须要写。
即,如果要实现的功能涉及到深拷贝,则必须显示定义,再开一片空间储存、拷贝。拷贝构造就相当于个性化复制粘贴,我们把两个对象给它,它把一个对象的内容拷贝给另一个对象,它可以自动进行简单拷贝工作,但为了避免内存泄露导致生产事故,它有意的不去进行内存信息的拷贝复制。
打个比方,程序员就是工程师,程序员编写的类就是图纸,编译器就是工头,程序运行就是工人盖房子的过程。
两个工程师拿着相同的图纸(图纸内容相同,只有署名不同)去盖两个房子,他们两人雇佣了同一批工人,工人在盖第一个房子的时候没有问题,然而当工人在盖第二个房子的时候发现图纸和上一个一模一样,认为是工程事故于是拒绝施工,除非工程师到场确定造两次否则不开工,然而工程师偷懒没去,结果工人自动由此确定只需盖一座房子就可以完成任务了,于是对着一个房子拍了两张照片发给工头交差,工程师收到照片也认为两个房子造好了于是拿去售楼部卖;
(赋值运算符如果没有显示定义,就会自动生成赋值运算符重载来进行浅拷贝,浅拷贝只会直接拷贝地址和值,不会自动开辟空间存储内存信息)
后来,一家住户入住时没有发现问题,但当两家住户入住的时候发现只有一个房子可以住,就把前一家住户赶出来了;
(由于没有开辟新的空间,只是把地址赋给了两个指针,结果只能存一份后来的那个信息)
再后来,拆迁队拆房的时候发现一个房子要拆两次,自己还补了两份拆迁款,于是报警抓人;
由于地址存了两份,当我们试图释放内存时会出现把同一份空间释放两次的情况,这是时候编译器会报错中止程序运行。