网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
\color{red}{2361038962@qq.com}
2361038962@qq.com,我会尽量帮大家进行解答!
4. 拷贝构造函数
4.1 概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰,防止在拷贝构造函数中将被拷贝的对象修改),在用已存在的类类型对象创建新对象时由编译器自动调用。
问:这个地方为什么要传引用呢?
答:因为函数调用时的传值调用的拷贝本身也是一种拷贝,此时也调用了构造函数!如果我们在构造函数中不使用引用,将会无穷无尽的调用拷贝构造函数。当然,其实我们也可以传指针,但是传了指针之后,编译器会将它当作是普通构造函数,而不是拷贝构造函数了。
例如:
int main() { Date d1; Func(d1);//Func函数调用的过程中就调用了构造函数 }
问:拷贝构造和构造函数的区别是什么?
答:拷贝构造也是构造函数的一种,只不过前面的构造函数是普通构造,拷贝构造是特殊的构造函数。
4.2 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参(使用引用传参后将不会自动调用拷贝构造函数),使用传值方式会引发无穷递归调用。
//Date拷贝构造函数的定义
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
如果我们不使用引用:
//Date拷贝构造函数的定义
Date(const Date d)//使用d1来初始化d(相当于const Date d(d1)),同类型的对象初始化就要调用拷贝构造函数,非自定义类型的对象直接拷贝即可
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int main()
{
Date d1;
Date d2(d1);
Date d2 = d1;//这种写法和上面的那行是一样的效果,因为都是通过已经创建的对象来对未创建的对象进行初始化,其
return 0;
}
画图理解:
3. 若未显式定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝(类似memcpy函数),这种拷贝我们叫做浅拷贝,或者值拷贝。
下面拿日期类代码举例:
class Date
{
public:
//构造函数
Date(int year = 1,int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//打印函数
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
d1.Print();
d2.Print();
//注意:下面的写法是错误的
//d2(d1);
//拷贝构造函数只能用来初始化要创建的对象,而不能用于已经创建的对象
return 0;
}
在上面的代码中,我们并没有写拷贝构造函数,使用的是编译器默认的拷贝构造函数,但是程序依然输出了正确的结果,这是因为默认的构造函数对于内置类型进行了浅拷贝(逐字节拷贝,类似memcpy),即d1和d2两块内存空间中存储的值一模一样,所以能够输出正确的结果:
4. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
代码:
class F
{
public:
//构造函数
F()
{
_arr = (int\*)malloc(sizeof(int) \* 10);
}
//析构函数
~F()
{
free(_arr);
}
private:
int\* _arr;
};
int main()
{
F f1;
F f2(f1);
return 0;
}
上述代码执行将会报错,为什么?因为我们此时没有定义拷贝构造函数,所以使用的是默认的拷贝构造函数,所以执行的是浅拷贝,或者说是值拷贝,结果如图所示:
我们在途中看到,f1和f2的_arr指向的都是同一块空间,但是f1和f2作为两个独立的变量会执行两次析构函数,即将malloc开辟在堆上的空间释放两次,所以程序会出现崩溃的现象。同时不仅仅这个地方会出现问题,在修改数据的时候也会互相影响。
那么正确的拷贝构造函数应该怎么写呢?
{
public:
//构造函数
F()
{
_arr = (int\*)malloc(sizeof(int) \* 10);
}
//拷贝构造函数
F(F& f)
{
_arr = (int\*)malloc(sizeof(int) \* 10);
}
//析构函数
~F()
{
free(_arr);
}
private:
int\* _arr;
};
int main()
{
F f1;
F f2(f1);
return 0;
}
此处会涉及到深拷贝问题,后面会进行学习。
浅拷贝的问题:
1、指向一块空间,修改数据会互相影响
2、这块空间析构时会释放两次,程序会崩溃。
问:对于内置类型的数组类型,浅拷贝是否可以达成我们的目的?
答:可以的,因为数组是开辟在栈区上的,而不是开辟在堆区上的。下面是代码验证:
class F
{
public:
//构造函数
F()
{
for (int i = 0; i < 10; i++)
{
_arr[i] = i;
}
}
private:
int _arr[10];
};
int main()
{
F f1;
F f2(f1);
return 0;
}
调试截图:
我们可以看到默认的拷贝构造函数能够实现我们的目标,完成数组的拷贝。所以内置类型的数组类型,也算是内置类型。
5. 默认拷贝构造函数对于内置类型和自定义类型有不同的处理方法:
* 内置类型:内置类型的成员会完成值拷贝(浅拷贝)。
* 自定义类型:自定义类型的成员,去调用这个成员的拷贝构造。
代码:
```
class Time
{
public:
Time(int hour = 0, int minute = 0,int second = 0)
{
_hour = hour;
_minute = minute;
_second = second;
}
//没写Time的拷贝构造函数,所以调用的就是默认拷贝构造函数
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1,int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
Time _time;
};
int main()
{
Date d1;
Date d2(d1);//对于Time类型的成员变量,调用了其默认的构造函数(浅拷贝),对其进行拷贝
return 0;
}
```
调试截图:
![image-20220517110838771](https://img-blog.csdnimg.cn/img_convert/556160c9ae5ad939e478074efee4180d.png)
5. 赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义,我们就不能将其改为-
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的 操作符有一个默认的形参this,限定为第一个形参
.*
、::
、sizeof
、?:
、.
注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
第一种:全局的operator
//全局的operator==
class Date
{
public:
Date(int year = 1,int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1_day == d2._day;
}
int main()
{
Date d1;
Date d2(d1);
//1.第一种调用方式
if (operator== (d1, d2))
{
cout << "==" << endl;
}
//2.第二种调用方式
if (d1 == d2)//编译器会处理成对应重载运算符调用if(operator== (d1, d2))
{
cout << "==" << endl;
}
return 0;
}
我们运行之后上面的程序会报错,因为我们在类外访问了private成员变量,这是非法的。有三种修改方式:
- 将private改为public。但是这种方法是非常不推荐的,因为我们一般会将数据设为私有的,不想被使用者知道或者进行修改。
- 采用接口的方式。
class Date
{
public:
Date(int year = 1,int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
int GetYear()
{
return _year;
}
int GetMonth()
{
return _year;
}
int GetDay()
{
return _year;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1.GetYear() == d2.GetYear()
&& d1.GetMonth() == d2.GetMonth()
&& d1.GetDay() == d2.GetDay();
}
//在上面的这种写法中会出现错误,为什么?
//下面来进行分析:
//此时d1和d2是const Date& d1类型的,调用的时候会将其地址传过去,所以传过去的类型应该是const Date\*
//而this指针的类型是Date \* const,所以不能发生上述类型转换,属于权限的放大,应该像下面这样写:
bool operator==(Date& d1, Date& d2)//将const去掉就好
{
return d1.GetYear() == d2.GetYear()
&& d1.GetMonth() == d2.GetMonth()
&& d1.GetDay() == d2.GetDay();
}
//或者像下面这样进行修改
void Print()const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
int GetYear()const
{
return _year;
}
int GetMonth()const
{
return _year;
}
int GetDay()const
{
return _year;
}
//上面的代码相当于下面的代码
void Print(const Date\* const this)
{
cout << _year << "-" << _month << "-" << _day << endl;
}
int GetYear(const Date\* const this)
{
return _year;
}
int GetMonth(const Date\* const this)
{
return _year;
}
int GetDay(const Date\* const this)
{
return _year;
}
//这样进行修改之后,再执行上面报错的操作就是权限的缩小了,这是被编译器所允许的
总结:建议成员对象函数中不修改成员变量的函数,都建议在成员函数的后面加上一个const,进而使this指针的类型变成
const Date *
。
- 采用友元的方式(此处不作详解,会破坏封装)。
第二种:类中的operator。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator==(const Date& d)//因为类中的函数自动传了this指针
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
//1.第一种调用方式
if (d1.operator== (d2))//
{
cout << "==" << endl;
}
//2.第二种调用方式
if (d1 == d2)//编译器会处理成if(d1.operator(&d1, d2))
{
cout << "==" << endl;
}
return 0;
}
问:如果类里面的和全局的operator都同时存在,那么优先调用哪一个?
答:优先调用类里面的!
练习:写日期类<
的重载。
bool operator<(const Date& d)
{
if (_year < d._year
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day))
{
return true;
}
else
return false;
}
5.2 赋值运算符重载
首先先对拷贝构造和赋值运算符进行区分。
Date d1(2022, 5, 17);
Date d2(d1);//拷贝构造--一个存在的对象去初始化另一个要创建的对象
Date d3 = d1;//这是拷贝构造--一个已经存在的对象去初始化另一个已经存在的对象
Date d3;//这条语句执行之后,d3已经存在
d3 = d1;//赋值重载--两个已经存在的对象之间赋值
对于日期类赋值重载的实现:
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
我们此处写的并不够全面,为什么?因为按照正常的赋值语法来说,a = b = c
这样的操作是合法的,赋值表达式是有返回值的,但是我们上面实现的返回值类型为void
,所以不支持连续赋值,所以我们还要进行改进:
Date& operator=(const Date& d)//传引用返回是为了提高效率,避免拷贝
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
此时我们还要考虑一种情况,就是自己给自己进行赋值,这样的行为是没有什么意义的,所以我们要避免这样的操作。
下面就是改进后的赋值运算符重载(标准版)
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return \*this;
}
注意:当我们没有写赋值重载函数的时候,我们在代码中写的赋值操作就会自动执行拷贝构造函数,来完成赋值操作。
赋值运算符主要有以下五点:
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
问:那么编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?
答:答案是有必要的,比如涉及到管理非栈区空间的时候,就会涉及到深拷贝的问题,此时就需要我们自己来实现赋值重载函数。
6. const成员
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改。
我们知道this指针的类型是Date* const
,在成员函数的后面加上const之后,this指针的类型就变成了const Date* const
。
下面有四个问题:
- const对象可以调用非const成员函数吗?
不行,这属于权限的放大! - 非const对象可以调用const成员函数吗?
可以,这属于权限的缩小! - const成员函数内可以调用其它的非const成员函数吗?
这个是什么意思呢?我们用代码来举例:
bool Date::operator==(const Date& d)
{
Print();//这就是在成员函数内调用其它的成员函数
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
上面的例子就可以演示在成员函数内调用其它的成员函数,本质上其实就是通过this指针进行调用的,即this->Print
。
所以上面的问题是不可以的,因为const成员函数内的this指针的类型是const Date* const
,而非const成员函数内的this指针的类型是Date *const
,属于权限的放大,所以是不可以的!
4. 非const成员函数内可以调用其它的const成员函数吗?
可以,属于权限的缩小。
7.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
te& d)
{
Print();//这就是在成员函数内调用其它的成员函数
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
上面的例子就可以演示在成员函数内调用其它的成员函数,本质上其实就是通过this指针进行调用的,即`this->Print`。
所以上面的问题是不可以的,因为const成员函数内的this指针的类型是`const Date* const` ,而非const成员函数内的this指针的类型是`Date *const`,属于权限的放大,所以是不可以的!
4. 非const成员函数内可以调用其它的const成员函数吗?
可以,属于权限的缩小。
## 7.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
[外链图片转存中...(img-XVydF5mi-1715617878155)]
[外链图片转存中...(img-HssqD3gb-1715617878155)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**