拷贝构造函数:
概念:
构造函数的第一个参数,是类本身的const引用(一般情况下没有其他参数,少数情况:其他参数必须有默认值!)称此类构造函数为拷贝构造函数
特征:
1:拷贝构造函数类名与函数值一致,没有返回值(与构造函数一致)
2:每个类都有拷贝构造函数,未定义时系统自动生成(可能产生错误)
默认生成的拷贝构造函数:为每个成员分配内存空间、成员变量赋值(浅拷贝)
3:调用时只有一个实参,要求函数不能重载
调用时仅对象作为实参,后面若有参数都为默认值,无法区分函数导致无法重载
4:第一个参数是类本身的const引用,存在其他有默认值的参数
//格式:
类名(const 类名&对象名);
class CPY
{
public:
CPY();//默认构造函数;
CPY(const CPY& a);//拷贝构造函数
...
};
拷贝构造函数的应用:
- 用已经存在的对象初始化构造新的对象
- 以类本身对象作为函数传递的实参,在非引用情况下(程序需要在函数中新创建一个相同的对象并复制值
- 返回类型为非引用类型的函数返回一个对象
int main()
{
CPY a{...};//构造
CPY b(a);//拷贝构造
CPY c=a;//拷贝构造
MAX(a);//-> int MAX(const CPY a) //函数五引用对象传参:拷贝构造用于传参
MAX(a,b)//->CPY MAX(const CPY& a,const CPY& b) return a;
//有引用的传参不是拷贝构造函数
//函数返回值使用拷贝构造函数
}
注意:
拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用,且这个参数几乎总是const的引用。
拷贝构造函数作用即将一个已经存在的对象用于初始化构造新的对象,我们没有定义拷贝构造函数时,系统将自动构造拷贝构造函数:从给定对象中依次将每个非static成员拷贝到正在创建的新对象中。即分以下几种情况:
-
- 当该类的对象都是普通变量,默认拷贝构造函数可完成值的拷贝(浅拷贝),正确
- 当类的对象中出现指针、数组等变量,默认函数拷贝原来对象成员中的值到新创建的指针的值、数组的首地址(浅拷贝),错误❌
- 原因以下浅拷贝仅仅拷贝值,导致多处指针指向同一地址,当其中多对象生命周期结束时析构函数将指针同一地址多次释放导致系统崩溃
- 解决方法自定义拷贝构造函数使用深拷贝:创建一新指针、数组(指向不同地址),再将其对象成员的指针、数组值赋值给新创建的对象成员(strcpy()或memcpy()),即得到指向不同地址的同值对象成员,满足拷贝构造函数、析构函数要求;
- 拷贝构造函数的参数采用引用方式,若是非引用:为调用拷贝构造函数需复制实参到形参,由复制的需要又得重新调用拷贝构造函数,产生无限循环;而引用即调用对象本身,无需复制。
引例:
class Complex //复数类
{
public:
Complex(double r, double i);
Complex(const Complex& c);
Complex add(Complex c);//加法
private:
double real; //实部
double image; //虚部
};
Complex::Complex(double r, double i) :real(r), image(i)
{
cout << "构造函数,实部:"<<real<<",虚部:"<<image << endl;
}
Complex::Complex(const Complex & c)
{
real = c.real;
image = c.image;
cout << "拷贝构造函数,实部:" << real << ",虚部:" << image << endl;
}
Complex Complex::add(Complex c)
{
Complex y(real + c.real, image + c.image); //构造函数
return y;//返回值为类对象,会调用拷贝构造函数
}
void f(Complex n) //参数是类对象,会调用拷贝构造函数
{
cout << "f(Complex n)" << endl;
}
int main()
{
Complex a(3, 4); //调用构造函数
Complex b(6.5, 7.5);//调用构造函数
Complex c(a); //拷贝构造函数
Complex d = c;//拷贝构造函数,注意和下一节的赋值区分开
f(b); //拷贝构造函数
c = a.add(b); //拷贝构造函数
return 0;
}
程序分析:
- 调用构造函数,创建了两个复数类对象a和b
- 用已知对象初始化另一个对象,系统调用拷贝构造函数
- 利用c初始化对象d,这一句看似=赋值,其实还是调用拷贝构造函数,因为这里还是初始化过程。
- Complex d=c 等同 Complex d(c) 等同 Complex d{c},
- 将实参b传给形参n,因为形参是非引用的类对象,调用拷贝构造函数。
- 首先实参b传递给非引用形参c会调用拷贝构造函数,接着在add函数中定义了一个复数类对象y(24行),系统会调用构造函数。
- 最后,函数add的返回值是一个非引用对象,系统会创建一个临时对象,将局部对象y赋值给临时对象,这时也要调用拷贝构造函数。
赋值:
同类的对象之间可以互相赋值,即一个对象的值可以赋值给另一个对象。对象之间的赋值通过“=”进行。默认就是把一个对象所有非static数据成员的值依次赋值给另一个对象。
//格式
对象名1 = 对象名2;
说明:
(1)对象的赋值只对其中的数据成员赋值,不对成员函数赋值。
每个对象的数据成员占用独立的存储空间,不同对象的数据成员占有不同的存储空间,赋值的过程是将一个对象的数据成员在存储空间的值复制给另一个对象的数据成员的存储空间。
而不同对象的成员函数是同一个函数代码段,不需要、也无法对它们赋值。
(2)类的数据成员中不能包括动态分配的数据,否则在赋值时可能出现意想不到的严重后果,(不同指针指向同一地址后多次释放delete,崩溃)
如果类的数据成员有指针,则一定要实现如下函数
1、构造函数 (如果没有,会出现野指针)
2、拷贝构造函数(如果没有,会出现浅拷贝)
3、重载 = 符号 (如果没有,会出现浅拷贝)
2、析构函数(内存泄漏)
delete动态对象则自动调用析构函数,不delete不调用析构函数
而动态成员需系统调用析构函数delete
拷贝构造函数与赋值区分:拷贝构造定义初始化阶段,赋值在定义完成后进行。
运算符重载
在类中重新定义运算符,赋予运算符新的功能以适应类的运算,如果某个运算符重载了,那么在使用该运算符时,系统会自动调用。
格式如下:
返回值类型 operator 运算符(参数);
//注意 operator关键字必须写
//例如:
operator +()//表示:重载+运算符
operator *()//表示:重载*运算符。
其中,operator 是 C++的关键字,专门用于定义运算符重载函数。
运算符重载是一种形式的C++多态,它使得对象操作更直观,本质上也是属于函数重载。
假设有一个Student类,并为它定义了一个operator +()成员函数,以重载+运算符,如果有Student的对象s1,s2,s3。便可以编写这样的代码。
s1 = s2+s3; //简化
s1 = s2.operator+(s3);//复杂写法
已使用的引例:
1. 这是因为C++已经对string类重载了“+”运算符加法:
string s1="abc";string ;s2="xyz"; s1+s2;
2. C++对“<<”和“>>”进行了重载,用户在不同的场景下使用它们,作用是不同的。
对于位运算而言:“<<”运算符是左移运算符,“>>”运算符是右移运算符。
“<<”运算符在输出操作中与流对象cout配合使用,是流插入运算符;
“>>”运算符在输入操作中与流对象cin配合使用,是流提取运算符。
注意:运算符重载的赋值函数,要添加以下代码:避免同对象赋值判断
if(this==&s)
return;
// s 是实参地址
//防止同一地址相互赋值,先delete释放地址导致数据丢失