复制指的是我们把数据或者对象、原语从一个地方复制到另一个地方时,我们实际上有两个副本
,大多数时候当我们只是想读取或者修改一个已经存在的对象时,我们当然不想复制,因为复制需要时间,不必要的复制会浪费性能。
int main()
{
int a = 10;
int b = a;
b = 1;
}
上面代码b复制a,改变b的值,a不会受到影响,因为内存中有两个值。
现在我定义了一个结构体:
struct Vector2
{
float x,y;
};
int main()
{
Vector2 *a = new Vector2();
Vector2 *b = a;
b++;
b->x = 1;
}
现在我们有两个指针,但实际上指向相同的值,b++时a不会受到影响,但如果改变b指向的值则a也会受到影响。
也就是说每当我们编写一个变量被赋值另一个变量时,我们总是在复制,而在指针的情况下我们在复制指针,也就是内存地址,内存地址的数字,只是数字而已,不是指针指向的实际内存。
现在我要用非常原始的方法来写一个字符串类来真正的实现复制指针指向的内存。
class String
{
private:
char *m_Buffer;
unsigned int m_Size;
public:
String(const char *string)
{
m_Size = strlen(string);
m_Buffer = new char[m_Size + 1]; //+1是为了给终止符腾出空间
接下来把char *string拷贝到缓冲区m_Buffer,可以用for循环实现。
for(int i = 0; i < m_Size; i++)
m_Buffer[i] = string[i];
但我们有更简洁的方法:memcpy。
memcpy(m_Buffer, string, m_Size + 1);
}
friend std::ostream &oprator<<(std::ostream &stream, const String &string); //声明友元
};
现在我们想输出这个字符串,需要重载<< 运算符。
std::ostream &oprator<<(std::ostream &stream, const String &string)
{
stream << string.m_Buffer; //这里有个问题,需要访问String类的私有变量m_Buffer,可以在 String类中把这个方法声明成友元。
return stream;
}
int main()
{
String string = "test";
std::cout << string << std::endl;
}
这一切似乎看起来很好,但已经有了一个内存泄漏,
m_Buffer = new char[m_Size + 1]; 这条语句并没有手动释放,所以现在要写一个析构函数来释放掉它。
~String()
{
delete[] m_Buffer;
}
运行起来没有问题,现在在main里面复制一个对象
String string1 = string;
运行起来没问题,结果是想要的,但结束时程序崩溃了。
这是因为我们复制的这个指针实际上只是复制了内存地址,并没有复制内存,两个指针指向的内存是同一块(浅拷贝),而析构函数执行了两次,试图释放一个不存在的内存块当然会出问题。
我们需要做的是复制整个对象,创建一个新 char数组来实现指针指向不同的内存(深拷贝)。
需要用到的一个函数叫做“拷贝构造函数”,他是一个构造函数,当你复制相同类型的第二个字符串时,它会被调用。
String(const String& other)
{
m_Buffer = new char[m_Size + 1];
memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
}
这样处理后确实没问题了,实现了深拷贝。
现在我写一个一个方法来打印String,接着在拷贝构造函数加上一条输出语句,为了直白的看出执行了几次复制。
void PrintString(String string)
{
std::cout << string <<std::endl;
}
int main()
{
String string = "this is test";
String string1 = string;
PrintString(string);
PrintString(string1);
}
运行可以看到拷贝构造函数运行了三遍,这是不必要的。需要把PrintString的参数加上const和引用
void PrintString(const String& string)
{
std::cout << string <<std::endl;
}
这样PrintString接受的是一个引用而不是一个实际的字符串,运行可以看到只发生了一次复制。
所以在基础使用中,用const引用更好。
总是通过const引用传递对象。在函数本身我们可以决定是否复制,但在函数内部,不应该随意复制,这样会拖慢我们的程序。