C++学习笔记
打个比方,拷贝构造造成无穷递归的情况,就像是:我们想要一面镜子里照出画面,采用的方法却是通过另一面镜子和它对照,如此这般,用无穷面镜子一个接一个的两两对照,指望通过镜子两两对照来照出画面。
当参数采用传值方式时,拷贝构造的参数就必须是自产的,为了获得这个自产的参数就要启动新的拷贝构造,可新的拷贝构造也需要一个自产的参数,为了这个自产参数就又要启动一个新的拷贝构造。。。。。。结果就是生成了无穷个连串的拷贝构造,它们都希望自己生成的下一个拷贝构造可以产生一个参数给自己用,然而永远不可能产生一个参数给它用,因此形成了无穷递归调用。
打个比方,赋值运算符重载就像是两个工程师拿同一份图纸盖房子,程序员就是工程师,程序员编写的类就是图纸,编译器就是工头,程序运行就是工人盖房子的过程。
两个工程师拿着相同的图纸(图纸内容相同,只有署名不同)去盖两个房子,他们两人雇佣了同一批工人,工人在盖第一个房子的时候没有问题,然而当工人在盖第二个房子的时候发现图纸和上一个一模一样,认为是工程事故于是拒绝施工,除非工程师到场确定造两次否则不开工,然而工程师偷懒没去,结果工人自动由此确定只需盖一座房子就可以完成任务了,于是对着一个房子拍了两张照片发给工头交差,工程师收到照片也认为两个房子造好了于是拿去售楼部卖;
(赋值运算符如果没有显示定义,就会自动生成赋值运算符重载来进行浅拷贝,浅拷贝只会直接拷贝地址和值,不会自动开辟空间存储内存信息)
后来,一家住户入住时没有发现问题,但当两家住户入住的时候发现只有一个房子可以住,就把前一家住户赶出来了;
(由于没有开辟新的空间,只是把地址赋给了两个指针,结果只能存一份后来的那个信息)
再后来,拆迁队拆房的时候发现一个房子要拆两次,自己还补了两份拆迁款,于是报警抓人;
(由于地址存了两份,当我们试图释放内存时会出现把同一份空间释放两次的情况,这是时候编译器会报错中止程序运行。)
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。(即,根据该单个形参来创建该对象的深拷贝)
拷贝构造函数也是特殊的成员函数,其特征如下:
(关键在于拷贝构造的参数从何而来。当参数采用传值方式时,拷贝构造的参数就必须是自产的,为了获得这个自产的参数就要启动新的拷贝构造,可新的拷贝构造也需要一个自产的参数,为了这个自产参数就又要启动一个新的拷贝构造。。。。。。结果就是生成了无穷个连串的拷贝构造,它们都希望自己生成的下一个拷贝构造可以产生一个参数给自己用,然而永远不可能产生一个参数给它用,因此形成了无穷递归调用。
打个比方,拷贝构造造成无穷递归的情况,就像是:我们想要一面镜子里照出画面,采用的方法却是通过另一面镜子和它对照,如此这般,用无穷面镜子一个接一个的两两对照,指望通过镜子两两对照来照出画面。
代码如下(示例):
#include
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) //正确写法
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//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
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;
}
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
如下代码,将**==**运算符重载为了可处理成员变量的运算符。
#include
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;
}
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
如下代码,我们将赋值运算符**=**重载为成员变量的赋值运算,d1的_day重载为d2的_day
#include
#include
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;
}
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
即,一个类可以给他的赋值运算符的功能做个性化定义,而在其他地方,赋值运算符会恢复它本来的作用。在全局域,运算符它本来的作用是不可以被更改的。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
如下:我们没有编写赋值运算符进行赋值操作,但我们的编译器自动生成了赋值操作符完成了赋值操作,对于比较简单的赋值操作,我们可以省略赋值操作符。
#include
using namespace std;
class Text
{
public:
Text(int m = 1, int n = 2)
{
_m = m;
_n = n;
}
void Print()
{
cout << _m<<" " << _n << endl;
}
private:
int _m;
int _n;
};
int main()
{
Text t;
t.Print();
Text t1(6, 6);
t1.Print();
return 0;
}
还有一种情况,可以通过构造函数间接调用其他类的赋值运算符重载。
如下代码,d1=d2执行时,自动使用了Time重载过的赋值运算符。
#include
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;
}
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
即,如果要实现的功能涉及到深拷贝,则必须显示定义,再开一片空间储存、拷贝。拷贝构造就相当于个性化复制粘贴,我们把两个对象给它,它把一个对象的内容拷贝给另一个对象,它可以自动进行简单拷贝工作,但为了避免内存泄露导致生产事故,它有意的不去进行内存信息的拷贝复制。
打个比方,程序员就是工程师,程序员编写的类就是图纸,编译器就是工头,程序运行就是工人盖房子的过程。
两个工程师拿着相同的图纸(图纸内容相同,只有署名不同)去盖两个房子,他们两人雇佣了同一批工人,工人在盖第一个房子的时候没有问题,然而当工人在盖第二个房子的时候发现图纸和上一个一模一样,认为是工程事故于是拒绝施工,除非工程师到场确定造两次否则不开工,然而工程师偷懒没去,结果工人自动由此确定只需盖一座房子就可以完成任务了,于是对着一个房子拍了两张照片发给工头交差,工程师收到照片也认为两个房子造好了于是拿去售楼部卖;
(赋值运算符如果没有显示定义,就会自动生成赋值运算符重载来进行浅拷贝,浅拷贝只会直接拷贝地址和值,不会自动开辟空间存储内存信息)
后来,一家住户入住时没有发现问题,但当两家住户入住的时候发现只有一个房子可以住,就把前一家住户赶出来了;
(由于没有开辟新的空间,只是把地址赋给了两个指针,结果只能存一份后来的那个信息)
再后来,拆迁队拆房的时候发现一个房子要拆两次,自己还补了两份拆迁款,于是报警抓人;
由于地址存了两份,当我们试图释放内存时会出现把同一份空间释放两次的情况,这是时候编译器会报错中止程序运行。