目录
前言
一、拷贝构造函数
1. 概念
2. 笔试题-拷贝构造的次数
3. 特征
1). 拷贝构造函数是构造函数的一个重载形式。
2). 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
那怎么理解无穷递归了?
那为什么最好要用const修饰了?
3). 若未显式定义,编译器会生成默认的拷贝构造函数。
3.1调试过程了解一下:
3.2如果没使用默认的拷贝构造,调试过程:
什么时候需要深拷贝?
总结
前言
C++类与对象(二)(中)拷贝构造函数
一、拷贝构造函数
1. 概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?可以,拷贝构造函数就能满足你的要求。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
2. 笔试题-拷贝构造的次数
以下代码共调用多少次拷贝构造函数: ( )
Widget f(Widget u)
{
Widget v(u);
Widget w=v;
return w;
}
main(){
Widget x;
Widget y=f(f(x));
}
A.9
B.3
C.5
D.7
1).f(x);//传值传参,用x去初始化u(void f(Weight u))//自定义变量传值传参就会调用拷贝构造函数,调用后在再进入void f(Weight u)
#include<iostream>
using namespace std;
class Weight
{
public:
Weight()
{
cout << "Weight" << endl;
}
Weight(const Weight& w)
{
cout << "Weight(consy Weight& w)" << endl;
}
};
void f(Weight u)
{
}
int main()
{
Weight x;
f(x);//传值传参,用x去初始化u(void f(Weight u))
//自定义变量传值传参就会调用拷贝构造函数
//调用后在再进入void f(Weight u)
return 0;
}
2). Weight w = v也调用拷贝构造
3)注意:一个表达式中,连续步骤的构造+拷贝构造,或者拷贝构造+拷贝构造,胆大的编译器可能就会进行优化合二为一,所以这里只输出了4次Weight(const Weight& w)
#include<iostream>
using namespace std;
class Weight
{
public:
Weight()
{
cout << "Weight" << endl;
}
Weight(const Weight& w)
{
cout << "Weight(consy Weight& w)" << endl;
}
~Weight()
{
cout << "~Weight()" << endl;
}
};
Weight f(Weight u)
{
Weight v(u);
Weight w = v;
return w;
}
int main()
{
Weight x;
//Weight();//匿名对象的生命周期只在这一行
Weight ret = f(x);
//注意:一个表达式中,连续步骤的构造+拷贝构造,或者拷贝构造+拷贝构造,胆大的编译器可能就会进行优化
//合二为一,所以这里只输出了4次Weight(const Weight& w)
return 0;
}
3. 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1). 拷贝构造函数是构造函数的一个重载形式。
2). 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
class Data
{
public:
Data(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2022,10,7);
Data d2(d1);
d2.Print();
d1.Print();
return 0;
}
对于以上代码:
Data d2(d1);
拷贝构造函数简单理解就是把d1给d2
注意拷贝函数的参数只有一个且必须是类类型对象的引用,且一般会要求加上const,以保证d1不可修改
回顾一下引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,
编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
注意:不会额外开辟空间,内存空间是共用的。
通过d2.Print();d1.Print();可以发现输出结果是一样的,进行了拷贝
看下图,提示非法的复制构造函数:即使用传值方式编译器直接报错,会引发无穷递归调用。
那怎么理解无穷递归了?
因为自定义类型变量d1的临时拷贝或者说是对于自定义类型变量,当同类型的参数(同类型的对象)进行传参时,需要先调用拷贝构造函数。
下面将用vs2019进行调试,显示一下调用拷贝构造函数过程;
1).设置断点,F10进入调试,F5进入断点,显示如下:
2)F11下一步,发现调用拷贝构造函数
3)d1给d2完成,见右边调试结果
因为C++对于自定义类型变量,进行传参时会调用拷贝构造函数,那么就会出现套娃现象:
我Data(Data d)这样写拷贝构造函数的时候,对于Data d2(d1),对于自定义变量的传参,就调用一下拷贝构造函数,而调用拷贝构造函数,又会进行传值传参,此时又要调用拷贝构造函数,依此循环......,而Data(const Data& d)这样就不会出现这个问题。
如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。
或者这样理解,我将d1传过去变成Data(d1),又是拷贝构造,之后又是Data(Data d1)又是拷贝构造,一直拷贝构造。。。。。
那为什么最好要用const修饰了?
首先先简单回顾一下const
对于const
const放*右边,表示指针变量本身不能被修改
const放在*左边表示指针所指向内容,不能通过指针改变(这里的左边可以int之前,也可以是int之后*之前,总之只要是*左边即可)
对于构造函数,存在写反的问题,如下:
对于两种情况
如果是02的时候:输出随机值。
在Data(Data& d)-02下,写反后,对于Data d2(d1); d是d1的别名,this是d2, 我们要拷贝出d2, 就是要把d1给d2,但是d._year = _year; 对于这句话,是将this给了d1(注意隐含的this指针是这样,this->_year),相当于变成了d2给了d1, 而d2是随机值,所以print都是随机值。
3). 若未显式定义,编译器会生成默认的拷贝构造函数。
class Data
{
public:
Data(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//使用默认的拷贝构造函数
// 1.若未显式定义,编译器会生成默认的拷贝构造函数。
// 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
//Data(const Data& d)
//{
// _year = d._year;
// _month = d._month;
// _day = d._day;
//}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Func(Data d)
{
d.Print();
}
int main()
{
Data d1(2022,10,13);
Data d2(d1);
Func(d1);//传参就会先调用拷贝构造函数
d2.Print();
return 0;
}
拷贝构造函数的特点是( )
A.该函数名同类名,也是一种构造函数,该函数返回自身引用
B.该函数只有一个参数,是对某个对象的引用
C.每个类都必须有一个拷贝初始化构造函数,如果类中没有说明拷贝构造函数,则编译器系统会自动生成一个缺省拷贝构造函数,作为该类的保护成员
D.拷贝初始化构造函数的作用是将一个已知对象的数据成员值拷贝给正在创建的另一个同类的对象
A.拷贝构造函数也是一构造函数,因此不能有返回值
B.该函数参数是自身类型的对象的引用
C.自动生成的缺省拷贝构造函数,作为该类的公有成员,否则无法进行默认的拷贝构造
D.用对象初始化对象这是拷贝构造函数的使命,故正确
3.1调试过程了解一下:
1)Data d1(2022,10,13);f11跳到自己写的全缺省的Data构造函数,可以看到d1初始化成功。
2)若使用默认的由编译器生成的拷贝构造函数,f11会直接到Func(d1),发现d2变成和d1一样,即进行了按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意区分:默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理,其中c++把变量分成两种,内置类型/基本类型:int/char/double/指针 ,自定义类型:class/struct去定义类型对象。
3)调用Print
3.2如果没使用默认的拷贝构造,调试过程:
1)当我运行到Data d2(d1)的时候就会调用自己所写的构造函数
2)拷贝成功后
3)当到了Func(d1)后继续按F11,发现又去调用了拷贝构造函数,再f11进入的func函数本体,是因为对于自定义类型变量,当同类型的参数(同类型的对象)进行传参时,需要先调用拷贝构造函数。
4)最后就是调用print的过程
注意:但这并不能说明我们就可以肆无忌惮的使用默认生成的拷贝构造函数,看如下代码:
因为:默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(int capacity = 10)//构造函数,函数名为类名Stack,无返回值
{
_a = (int*)malloc(sizeof(int*) * capacity);
if (_a == nullptr)
{
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;//可写可不写
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
void push(int x)
{
}
int pop()
{
}//注意:函数都在公共区
private:
Stack _st1;
Stack _st2;
};
int main()
{
Stack st1(10);
Stack st2(st1);
return 0;
}
调试:调用了两次析构函数,是因为默认的拷贝构造函数对象按内存存储按字节序完成拷贝,那么_a指向的是同一块空间。
思考一个问题:那如果不是动态开辟的这种类型,是数组可以吗?
_a是指针的时候,拷贝的是指针,但是数组是可以的,且它们所在的内存区域也不一样。
那什么是深拷贝,什么是浅拷贝了?
对于简单的类,默认的拷贝构造函数一般就够用了,我们也没有必要再显式地定义一个功能类似的拷贝构造函数。但是当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,默认的拷贝构造函数就不能拷贝这些资源了,我们必须显式地定义拷贝构造函数,以完整地拷贝对象的所有数据。
比如之前缩写的stack类,显式地定义了拷贝构造函数,它除了会将原有对象的所有成员变量拷贝给新对象,还会为新对象再分配一块内存,并将原有对象所持有的内存也拷贝过来。这样做的结果是,原有对象和新对象所持有的动态内存是相互独立的,更改一个对象的数据不会影响另外一个对象。 这种将对象所持有的其它资源一并拷贝的行为叫做深拷贝,我们必须显式地定义拷贝构造函数才能达到深拷贝的目的。
而默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。这种拷贝不会开辟一块新的空间,即把d1对象全拷给d2。
什么时候需要深拷贝?
如果一个类拥有指针类型的成员变量,那么绝大部分情况下就需要深拷贝,因为只有这样,才能将指针指向的内容再复制出一份来,让原有对象和新生对象相互独立,彼此之间不受影响。如果类的成员变量没有指针,一般浅拷贝足以。
那我们再看一段代码:
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(int capacity = 10)//构造函数,函数名为类名Stack,无返回值
{
_a = (int*)malloc(sizeof(int*)*capacity);
if (_a == nullptr)
{
exit(-1);
}
_top = 0;
_capacity = capacity;
}
Stack(const Stack& st)
{
_a = st._a;
_top = st._top;
_capacity = st._capacity;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;//可写可不写
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
void push(int x)
{
}
int pop()
{
}//注意:函数都在公共区
private:
Stack _st1;
Stack _st2;
};
int main()
{
Stack st1(10);
Stack st2(st1);
return 0;
}
为什么运行崩溃了?经过调试发现了一个这样的错误,为什么?
经过调试发现,这里就不粘贴图片了,调试方法同上即可,该代码调用了两次~stack,析构函数调用了两次,即进行了两次释放,但是他之前已经进行了拷贝,即已经将st1给了st2,那么会进行两次释放,多次释放导致运行崩溃,但是编译会通过,因为代码的语法没有问题。
总结
拷贝构造函数,一般的类,自己生成拷贝构造函数就够用了,只有STACK类。自己直接管理资源的,需要子实现深拷贝。
内置类型的值拷贝,浅拷贝;自定义类型的成员,去调用这个成员的拷贝构造函数。