一 拷贝构造函数的概念:
拷贝构造函数是一种特殊的构造函数,用于创建一个对象是另一个对象的副本。当需要用一个已存在的对象来初始化一个新对象时,或者将对象传递给函数或从函数返回对象时,会调用拷贝构造函数。
二 拷贝构造函数的特点:
1:拷贝构造函数是构造函数的一个重载形式。
2:拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
3:若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定 义类型是调用其拷贝构造函数完成拷贝的。
4:编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗? 当然像日期类这样的类是没必要的。
2.1 代码示例:
class Time
{
public:
// 普通构造函数
Time(int hour = 0, int minute = 0, int second = 0)
{
_hour = hour;
_minute = minute;
_second = second;
}
// 拷贝构造函数,使用引用传递
Time(const Time& other)
{
_hour = other._hour;
_minute = other._minute;
_second = other._second;
}
void Print() const
{
std::cout << _hour << ":" << _minute << ":" << _second << std::endl;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time t1(10, 20, 30); // 使用普通构造函数
//构造函数的重载
Time t2 = t1; // 使用拷贝构造函数
//Time t2(t1); // 拷贝构造的另一种写法
t1.Print();
t2.Print();
return 0;
}
输出:
2.2 为什么要使用引用呢?
我们在 increment 函数中改变x的值并没有间接性改变a,这是因为传过去的只是编译器创建实参的一个副本,而修改副本怎么可能可以改变a呢?
#include <iostream>
void increment(int x)
{
x = x + 1; // 修改的是副本,不影响实参
}
int main()
{
int a = 5;
increment(a); // 传递a的副本
std::cout << a << std::endl; // 输出5,原始值a未被修改
return 0;
}
知道传值传参的本质之后,再来想一想为什么要用引用?咱们先来说说如果没用用引用的后果会是怎么样,当把自定义类型传出去后且不用引用或者指针来接收,它会
调用 Time(const Time other)
,其中 other
是 t1
的按值传递副本。
为了按值传递,编译器需要创建 other
的副本。
创建 other
的副本时,再次调用 Time(const Time other)
。
这个新调用的 Time(const Time other)
又需要创建自己的 other
副本,再次调用 Time(const Time other)
。
如此反复,导致无限递归调用,最终导致栈溢出。
图:
C++规定,自定义类型的拷贝,都会调用拷贝构造
那为什么要引用呢?
首先我们来回顾一下引用 :
1:引用是现有变量的另一个名字。
2:它们不创建新对象,只是指向已有对象。
3:引用只是指向现有对象,不创建新副本
因为引用就是它本身,所以何来创建新副本这一说法,创建新副本是怕改变副本从而导致改变实参值
2.3 总结:
1:按值传递会递归:每次传递对象会复制对象,导致无限递归。
2:引用传递避免递归:引用只是指向对象本身,不会复制对象
三 默认拷贝构造:
当你没有显式定义拷贝构造函数时,编译器会为你自动生成一个默认的拷贝构造函数。这个默认拷贝构造函数会逐个拷贝对象的所有成员变量。
3.1 内置类型与自定义类型的拷贝:
内置类型:如 int
, char
, float
等,拷贝时直接按照字节方式进行复制,也就是直接复制其值。
自定义类型:如类和结构体,拷贝时会调用该类型的拷贝构造函数。
3.2 代码示例:
内置类型:
#include <iostream>
class MyClass
{
public:
int x; // 内置类型成员
};
int main()
{
MyClass obj1;
obj1.x = 10;
MyClass obj2 = obj1; // 使用编译器生成的默认拷贝构造函数
std::cout << "obj1.x: " << obj1.x << std::endl;
std::cout << "obj2.x: " << obj2.x << std::endl;
return 0;
}
输出:
对于一个类里面只有内置类型成员那编译器生成的默认拷贝构造会自动复制其值。
自定义类型:
#include <iostream>
class Time
{
public:
// 默认构造函数
Time()
{
_hour = 0;
_minute = 0;
_second = 0;
}
// 拷贝构造函数
Time(const Time& other)
{
_hour = other._hour;
_minute = other._minute;
_second = other._second;
std::cout << "Time::Time(const Time& other)" << std::endl;
}
private:
int _hour;
int _minute;
int _second;
};
class MyClass
{
public:
int x; // 内置类型成员
Time t; // 自定义类型成员
};
int main()
{
MyClass obj1;
obj1.x = 10;
MyClass obj2 = obj1; // 使用编译器生成的默认拷贝构造函数
std::cout << "obj1.x: " << obj1.x << std::endl;
std::cout << "obj2.x: " << obj2.x << std::endl;
return 0;
}
当执行MyClass obj2 = obj1; 因obj1类里面有自定义类型 t 所以编译器生成的默认拷贝构造会自动调用Time(const Time& other) 来完成
3.3 总结:
内置类型:编译器默认拷贝构造函数会直接复制其值。
自定义类型:编译器默认拷贝构造函数会调用该类型的拷贝构造函数来复制其内容。
四 内存分区:
要理解好深拷贝与浅拷贝那就得先了解内存是怎么样分区的。
计算机程序运行时,内存通常被分为四个主要区域:栈区、堆区、全局静态区和只读区(常量区和代码区)。
4.1 栈区:
局部变量:函数内部定义的变量。
形参(函数参数):函数定义时的参数。
返回地址:函数调用后的返回地址。
特点:
栈区中访问速度快且栈的内存连续分配。
因存储的都是 局部/形参/返回地址 所以栈区空间小,存储的生命周期短。
在我们局部变量所在的函数执行完成时,它会自动释放内存。
4.2 堆区:
动态分配的数据:通过 new
或 malloc
等动态分配函数分配的内存。
特点:
因存储的都是new 或者malloc开辟的空间所以堆区空间大,所以访问速度慢。
堆中的内存分配和释放是通过指针进行的,可能不是连续的。
堆区的内存需要程序员手动管理,必须手动释放动态分配的内存,否则会导致内存泄漏。
4.3 全区/静态区:
全局变量:在所有函数外部定义的变量。
静态变量:使用 static
关键字定义的变量。
特点:
全局变量和静态变量在程序的整个运行期间一直存在,直到程序结束。
全局变量可以在程序的所有函数中访问,静态变量在声明的作用域内共享。
4.4 只读常量区:
常量:程序中定义的常量。
代码:程序的指令代码。
特点:
常量区的数据在程序运行期间不能被修改,保证了数据的安全性和稳定性。
代码区存储程序的指令代码,在程序运行时被载入内存以执行。
五 浅拷贝:
首先我们来回顾C语言里面的基本类型和指针类型。
5.1 基本类型:
基本类型是C语言内置的数据类型,它们用于存储最基本的数值数据。常见的基本类型包括:int float char……
5.2 指针类型:
指针类型是存储内存地址的数据类型。指针用于指向其他变量或对象在内存中的位置。
5.3 基本类型代码示例:
#include <iostream>
class BasicType
{
public:
int value;
// 构造函数
BasicType(int v)
{
value = v;
}
// 拷贝构造函数
BasicType(const BasicType& other)
{
value = other.value;
}
};
int main()
{
BasicType obj1(10);
BasicType obj2 = obj1; // 浅拷贝,复制基本类型的值
std::cout << "改变前: " << std::endl;
std::cout << "obj1.value: " << obj1.value << std::endl;
std::cout << "obj2.value: " << obj2.value << std::endl;
obj2.value = 20; // 修改obj2的值
std::cout << "改变后: " << std::endl;
std::cout << "obj1.value: " << obj1.value << std::endl;
std::cout << "obj2.value: " << obj2.value << std::endl;
return 0;
}
输出:
值会被复制但修改新对象的值不会影响原对象。
5.3 指针类型代码示例:
#include <iostream>
class SimplePointer
{
public:
int* ptr; // 成员变量 ptr
// 构造函数
SimplePointer(int value)
{
ptr = (int*)malloc(sizeof(int)); // 动态分配内存并初始化
if (ptr != nullptr)
{
*ptr = value;
}
}
SimplePointer(const SimplePointer& other)
{
this->ptr = other.ptr; // 浅拷贝,复制内存地址
}
void print() const
{
std::cout << "Value: " << *ptr << std::endl;
}
};
int main()
{
SimplePointer obj1(10); // 创建第一个对象,并将值初始化为10
SimplePointer obj2(obj1); // 使用拷贝构造函数(浅拷贝)
// 打印初始值
std::cout << "Initial values:" << std::endl;
obj1.print();
obj2.print();
// 修改obj2的值
*obj2.ptr = 20;
// 打印修改后的值
std::cout << "After change:" << std::endl;
obj1.print();
obj2.print();
return 0;
}
输出:
复制内存地址,共享同一块内存,修改会互相影响
六 深拷贝:
#include <iostream>
#include <cstdlib>
#include <cstring>
class SimpleClass
{
public:
int* ptr;
// 默认构造函数
SimpleClass(int value)
{
ptr = (int*)malloc(sizeof(int)); // 动态分配内存并初始化
if (ptr != nullptr)
{
*ptr = value;
}
}
// 深拷贝构造函数
SimpleClass(const SimpleClass& other)
{
ptr = (int*)malloc(sizeof(int)); // 分配新内存
if (ptr != nullptr)
{
*ptr = *(other.ptr); // 复制内容
}
}
// 析构函数
~SimpleClass()
{
if (ptr != nullptr)
{
free(ptr); // 释放内存
}
}
void Print() const
{
if (ptr != nullptr)
{
std::cout << "Value: " << *ptr << std::endl;
}
}
};
int main()
{
SimpleClass obj1(10); // 创建对象,ptr 指向的值为 10
SimpleClass obj2 = obj1; // 使用深拷贝构造函数
obj1.Print();
obj2.Print();
// 修改 obj2 的值
if (obj2.ptr != nullptr)
{
*(obj2.ptr) = 20;
}
obj1.Print();
obj2.Print();
return 0;
}
输出:
深拷贝不仅复制对象的指针成员,还为指针指向的内容分配新的内存,并复制原对象的数据。这样,两个对象拥有独立的内存,修改一个不会影响另一个。