目录
一、基本介绍
二、为什么需要拷贝构造函数
三、拷贝构造函数
四、传参时的问题
五、完整代码
一、基本介绍
拷贝构造函数是C++中一个特殊的构造函数,用于创建一个类的对象作为另一个同类对象的副本。当一个对象以值的形式被传递给函数、从函数返回,或者用另一个同类对象直接初始化一个新对象时。例如,ClassName obj1 = obj2;
这行代码就会调用obj2
的拷贝构造函数来初始化obj1
。
拷贝构造函数的主要作用是确保对象的正确复制,特别是当类成员包含指针或动态分配的资源时。在这种情况下,默认的拷贝构造函数只会进行浅拷贝(即复制指针的值而不是它指向的数据),这可能导致诸如资源共享或双重释放等问题。因此,如果类中有指针成员或需要特殊的复制逻辑,通常需要自定义拷贝构造函数以实现深拷贝,确保每个对象拥有自己独立的资源副本。(下面会详细的讨论这个问题)
此外,根据C++的“规则三原则”,如果定义了拷贝构造函数,通常也应该定义赋值运算符和析构函数,以确保类在复制和资源管理方面的行为是一致和安全的。
基本语法
拷贝构造函数的典型声明形式是ClassName(const ClassName& other),其中ClassName代表类名,而other是对另一个同类型对象的引用,即:
class Entity
{
public:
Entity(...) //构造函数
Entity(const Entity& other) //拷贝构造函数
~Entity() //析构函数
};
使用引用 (ClassName& other) 而不是按值传递 (ClassName other) 避免了在函数调用过程中创建参数的额外副本。如果不使用引用,每次调用拷贝构造函数时,都会需要通过拷贝构造函数本身来创建一个临时对象,这会导致无限递归和程序崩溃。另一方面,使用 const 修饰符 (const ClassName& other) 确保了在拷贝构造过程中不会修改被复制的对象。这是一种良好的编程实践,因为拷贝构造的目的是创建一个新的副本,而不是修改现有对象。const 修饰符还允许拷贝构造函数接受临时对象或那些仅能以只读方式访问的对象作为参数。
此外的一个建议是,在进行对象传递的时候,最好使用const &进行,下面也会谈到这个问题。
二、为什么需要拷贝构造函数
上面也谈到过,在进行类对象复制的时候,默认情况下是进行浅拷贝,因此当类成员中有指针的时候会出现问题。下面举一个简单的例子:
#pragma once
#include<iostream>
class String
{
private:
char* m_String; //字符串
unsigned int m_Size; //记录字符串的大小
public:
String(const char* input)
{
m_Size = strlen(input); //计算字符串的大小
m_String = new char[m_Size + 1]; //开辟空间,这里+1是因为需要考虑'\n'停止符。这里需要理解一个字符串的工作原理
memcpy(m_String, input, m_Size + 1); //将输入的input字符串拷贝到m_String中
m_String[m_Size] = 0; //在最后的末尾加上 停止符
}
~String()
{
delete[] m_String; //释放内存
}
friend std::ostream& operator<<(std::ostream& stream, const String& input)
{
stream << input.m_String;
return stream;
}
};
void run()
{
String string("window");
String second = string;
std::cout << string << std::endl;
return;
}
int main()
{
run();
return 0;
}
这是一个自定义string类,使用的是最原始的方式进行的。当然你可以使用智能指针进行自动内存的管理,但是这样是为了更好的进行说明和展示,方便知道C++究竟干了什么。
首先这样创建一个string对象是没有问题的。下面再创建一个对象second进行复制,再将它们打印,这个时候就回出问题了。
void run()
{
String string("window");
String second = string;
std::cout << string << std::endl;
std::cout << second << std::endl;
return;
}
程序就直接奔溃。出现这个问题的原因就是因为对同一个地址进行了两次delete释放。在second进行拷贝的时候,默认情况下进行的是浅拷贝。在程序结束的时候,第一个string类调用了析构函数对m_String进行的释放,但是由于string中的一个成员是指针,因此second在进行拷贝的时候实际上是对string中的每一个成员进行了复制,因此这个时候second中的m_String指针实际上和string中m_String指向的是同一个地址,所以second在调用析构函数的时候会报错。
我们想要的效果是直接复制一整个string,这个就是拷贝构造函数存在的价值。
三、拷贝构造函数
下面是对代码的改进
class String
{
private:
char* m_String; //字符串
unsigned int m_Size; //记录字符串的大小
public:
String(const char* input)
{
m_Size = strlen(input); //计算字符串的大小
m_String = new char[m_Size + 1]; //开辟空间,这里+1是因为需要考虑'\n'停止符。这里需要理解一个字符串的工作原理
memcpy(m_String, input, m_Size + 1); //将输入的input字符串拷贝到m_String中
m_String[m_Size] = 0; //在最后的末尾加上 停止符
}
//拷贝构造函数
String(const String& copy)
:m_Size(copy.m_Size)
{
m_String = new char[m_Size + 1];
memcpy(m_String, copy.m_String, m_Size + 1);
}
~String()
{
delete[] m_String; //释放内存
}
friend std::ostream& operator<<(std::ostream& stream, const String& input)
{
stream << input.m_String;
return stream;
}
};
首先先对m_Size进行初始化,然后重新分配m_String的内存。当调用这个拷贝构造函数的时候,会对新的second中的m_String进行内存的分配。现在再执行就没有什么问题了。
四、传参时的问题
在定义了拷贝构造函数后,如果进行传参,直接值传递也会发生问题。
#pragma once
#include<iostream>
class String
{
private:
char* m_String; //字符串
unsigned int m_Size; //记录字符串的大小
public:
String(const char* input)
{
m_Size = strlen(input); //计算字符串的大小
m_String = new char[m_Size + 1]; //开辟空间,这里+1是因为需要考虑'\n'停止符。这里需要理解一个字符串的工作原理
memcpy(m_String, input, m_Size + 1); //将输入的input字符串拷贝到m_String中
m_String[m_Size] = 0; //在最后的末尾加上 停止符
}
//拷贝构造函数
String(const String& copy)
:m_Size(copy.m_Size)
{
std::cout << "Start Copy Function" << std::endl;
m_String = new char[m_Size + 1];
memcpy(m_String, copy.m_String, m_Size + 1);
std::cout << "End Copy Function" << std::endl;
}
~String()
{
delete[] m_String; //释放内存
}
friend std::ostream& operator<<(std::ostream& stream, const String& input)
{
stream << input.m_String;
return stream;
}
};
//进行String类的打印
void StringPrint(String input)
{
std::cout << input << std::endl;
}
//shejing
void run()
{
String string("window");
String second = string;
StringPrint(string);
StringPrint(second);
return;
}
我们使用一个函数对String的进行打印,在拷贝函数中进行输出标记,发现输出结果为:
拷贝构造函数被多次的调用了。因为在进行传参的时候进行的值传参,是一个复制的过程,因此拷贝构造函数被反复的调用,要解决这个问题就是使用const & 进行传参。
这样的话,拷贝构造函数就只会在second进行复制的时候被调用。
五、完整代码
#include<iostream>
class String
{
private:
char* m_String; //字符串
unsigned int m_Size; //记录字符串的大小
public:
String(const char* input)
{
m_Size = strlen(input); //计算字符串的大小
m_String = new char[m_Size + 1]; //开辟空间,这里+1是因为需要考虑'\n'停止符。这里需要理解一个字符串的工作原理
memcpy(m_String, input, m_Size + 1); //将输入的input字符串拷贝到m_String中
m_String[m_Size] = 0; //在最后的末尾加上 停止符
}
//拷贝构造函数
String(const String& copy)
:m_Size(copy.m_Size)
{
std::cout << "Start Copy Function" << std::endl;
m_String = new char[m_Size + 1];
memcpy(m_String, copy.m_String, m_Size + 1);
std::cout << "End Copy Function" << std::endl;
}
~String()
{
delete[] m_String; //释放内存
}
friend std::ostream& operator<<(std::ostream& stream, const String& input)
{
stream << input.m_String;
return stream;
}
};
//进行String类的打印
void StringPrint(const String& input)
{
std::cout << input << std::endl;
}
void run()
{
String string("window");
String second = string;
StringPrint(string);
StringPrint(second);
return;
}