💐 🌸 🌷 🍀 🌹 🌻 🌺 🍁 🍃 🍂 🌿 🍄🍝 🍛 🍤
📃个人主页 :阿然成长日记 👈点击可跳转
📆 个人专栏: 🔹数据结构与算法🔹C语言进阶🔹C++🔹Liunx
🚩 不能则学,不知则问,耻于问人,决无长进
🍭 🍯 🍎 🍏 🍊 🍋 🍒 🍇 🍉 🍓 🍑 🍈 🍌 🍐 🍍
前言
最近看见这样一个问题,突然发现自己也回答的不是很清楚,写篇博客复习一下。
这个问题中的赋值构造的说法有误:C++中没有“赋值构造函数”,对应的是“赋值操作符重载函数”(operator=函数)
一.什么是拷贝构造函数
- 拷贝构造函数是一种特殊构造函数,具有
单个形参
,该形参(常用const修饰
)是对该类类型的引用
,
class MyClass {
public:
MyClass(); // 默认构造函数
MyClass(const MyClass& other); // 拷贝构造函数
};
-
注意一定是引用。否则会导致
无限递归的调用拷贝构造函数
。原因
:当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用拷贝构造函数。为啥形参必须是对该类型的引用呢?试想一下,假如形参是该类的一个实例,由于是传值参数,我们把形参复制到实参会调用拷贝构造函数,如果允许拷贝构造函数传值,就会在拷贝构造函数内调用拷贝构造函数,从而形成无休止的递归调用导致栈溢出。 -
作用:当定义一个
新对象
并用一个同类型的对象
对它进行初始化时,将显式使用拷贝构造函数。具体体现在如下情况
// 1.通过已有对象初始化新对象;
class MyClass {
public:
MyClass(); // 默认构造函数
MyClass(const MyClass& other); // 拷贝构造函数
};
MyClass obj1; // 调用默认构造函数
MyClass obj2 = obj1; // 调用拷贝构造函数
// 2.将对象作为参数传递给函数时,由于参数是按值传递的,因此需要使用拷贝构造函数创建副本;
void myFunc(MyClass obj);
MyClass obj1;
myFunc(obj1); // 调用拷贝构造函数
// 3.从函数返回对象时,同样需要使用拷贝构造函数创建副本
MyClass myFunc();
MyClass obj = myFunc(); // 调用拷贝构造函数
二.什么是赋值函数
- 在C++中应该叫做是
赋值操作符重载operator=
,因为赋值必须作为类成员
,那么它的第一个操作数隐式绑定到this
指针,也就是 this 绑定到指向左操作数的指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般作为const 引用传递。 - 在C++中,类可以通过重载赋值操作符(也称为赋值运算符或赋值函数)来自定义对象的赋值行为。默认情况下,C++提供的赋值操作符只是简单地将一个对象的值复制到另一个对象中,这在一些情况下并不符合实际需求。
class MyClass {
public:
MyClass(); // 默认构造函数
MyClass(const MyClass& other); // 拷贝构造函数
MyClass& operator=(const MyClass& other); // 赋值操作符重载函数
- 作用:当已有对象被赋值给另一个同类对象,双方已经是初始化过了,赋值操作只是去修改一个对象的属性,而不是去初始化。
- 还需要注意一个调用问题:这个出现
赋值“=”
的地方未必调用的都是赋值函数
(算术符重载函数),也有可能拷贝构造函数
. 那么什么时候是调用拷贝构造函数,什么时候是调用赋值函数你? 判断的标准其实很简单:如果this指向的对象还未被初始化,那么调用的只能是拷贝构造函数,,反之如果变量已经存在,那么调用的就是赋值函数,还需要注意,如果是自己赋值给自己,那么直接返回*this
。
✏️注意的问题
在定义赋值操作符重载函数时,需要注意以下几点:
- 1️⃣ 赋值操作符重载函数的返回值必须是当前类对象的引用,因为赋值操作完成后,需要返回当前对象的引用;
2️⃣ 赋值操作符重载函数不会改变传入对象的状态,因此应该将其声明为const;
3️⃣ 赋值操作符重载函数应该检查是否是自我赋值,如果是则直接返回当前对象的引用;
4️⃣ 赋值操作符重载函数需要执行深拷贝操作,否则会导致指针等资源共享问题。
需要注意的是,赋值操作符重载函数和拷贝构造函数有所区别。拷贝构造函数用于在创建对象时通过复制另一个对象来初始化该对象,而赋值操作符重载函数用于在已有对象被赋值给另一个同类对象时执行初始化操作。
三.深浅拷贝问题
接下来,我们又要引入一个新问题深浅拷贝。
我们上面所讲解的,都是调用的是默认的
拷贝构造函数和赋值构造函数。
-
我们还记得,拷贝构造函数和赋值函数并非每个对象都会使用,如果不主动编写的话,编译器将以“位拷贝”的方式自动生成缺省的函数。 而且默认的都是——
浅拷贝
. -
浅拷贝会造成什么问题?通常来说,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,指针确实被复制了一个份,但是指针指向的内容没有复制。则两类中的两个指针将指向同一个地址,当对象快结束时,会调用
两次析构函数
,而导致指针悬挂现象
。所以,这时,必须采用深拷贝。倘若类中含有指针变量,那么这两个缺省的函数就会发生错误
想要实现深拷贝需要自主实现:
- 就拿之前学习string类模拟时的代码来说说。
class String
{
public:
String(const char *str = NULL);
String(const String &rhs);
String& operator=(const String &rhs);
~String(void){
delete[] m_data;
}
private:
char *m_data;
};
类内声明 类外定义
构造函数
String::String(const char* str)
{
if (NULL == str)
{
m_data = new char[1];
*m_data = '\0';
}
else
{
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
}
拷贝构造函数,使用new进行深拷贝
String::String(const String &rhs)
{
m_data = new char[strlen(rhs.m_data) + 1];
strcpy(m_data, rhs.m_data);
}
赋值函数,同样使用new进行深拷贝,但是他会先释放掉原来对象的资源
String& String::operator=(const String &rhs)
{
if (this == &rhs)
return *this;
delete[] m_data; m_data = NULL;
m_data = new char[strlen(rhs.m_data) + 1];
strcpy(m_data, rhs.m_data);
return *this;
}
四.小结
拷贝构造函数和赋值函数重载符是两种不同的函数,它们有以下几个主要区别:
1.参数类型不同:拷贝构造函数的参数是一个同类对象的引用,用于指定要复制的对象,而赋值函数的参数也是一个同类对象的引用,但是用于赋值操作。
2.调用时机不同:拷贝构造函数是在创建新对象
时调用,通过复制另一个对象来初始化新对象;而赋值函数是在已有对象
被赋值给另一个同类对象时调用,执行初始化操作。
3.返回值不同:拷贝构造函数不需要返回任何值
,它会自动返回新对象的引用;而赋值函数的返回值是当前对象的引用,因为赋值操作完成后,需要返回当前对象的引用。
4.实现方法不同:拷贝构造函数通常会执行深拷贝操作,以避免资源共享问题;而赋值函数通常会在执行赋值操作之前释放当前对象的资源
,以避免内存泄漏问题。