转换构造
一个类中构造函数可以重载,允许多个存在。如果构造函数只有一个参数且非当前类对时,可以将其他类型自动转换为当前类类型,这个过程为隐式类型转换。如下示例中的三个构造函数都可以发生隐式类型转换,在使用等号初始化或赋值时自动转换。
这样的可以发生隐式类型转换的构造函数可以称之为:转换构造函数。
cpp
class CTest {
CTest(int a) {} // 转换构造函数
/*
CTest(int a, int b = 0) {} // 转换构造函数
CTest(int a = 0, int b = 0) {} // 转换构造函数
explicit CTest(int a = 0, int b = 0) {} // 禁止发生隐式类型转换
CTest(int a, int b) {} // 不是转换构造函数
*/
};
CTest tst(10); // 调用带参数的构造
CTest tst2 = 20; // 合法操作 发生隐式类型转换,将int类型转换为CTest类型
tst2 = 30; // 合法操作 发生隐式类型转换
- 转换构造函数 :只定义了一个接受一个
int
参数的构造函数,允许隐式类型转换。- 取消了其他构造函数的注释后,下面是解释:
- **
CTest(int a, int b = 0)
:**可以通过一个参数隐式转换,第二个参数使用默认值。- **
CTest(int a = 0, int b = 0)
:**同样可以进行隐式转换,但两个参数都有默认值。- **
explicit CTest(int a = 0, int b = 0)
:**禁止隐式类型转换。只有在显式调用构造函数时才能构造对象。- **
CTest(int a, int b)
:**此构造函数需要两个参数,因此不能用来进行隐式转换。CTest tst(10);
: 这是合法的,调用了CTest(int a)
构造函数,创建一个CTest
对象tst
。CTest tst2 = 20;
: 这里发生了隐式类型转换,20
被转换为CTest
类型,调用了CTest(int a)
构造函数来初始化tst2
。这是合法的,因为你定义了一个可以接受单个int
参数的构造函数。tst2 = 30;
: 这是合法的,30
被隐式转换为CTest
类型,赋值给tst2
。同样使用了CTest(int a)
构造函数。
注意:如果是多个参数且无默认值时,则不能自动隐式类型转换。如果想要避免隐式类型转换,在构造函数前加上 关键字:explicit。
拷贝构造函数
拷贝构造函数: 是众多构造函数中的一种,空类中编译期会默认提供的一个函数,它
与默认的无参构造并存,函数名为当前类名,参数为当前类对象的引用(const 类名&
类对象),函数体代码不为空,代码为形参对象中的成员属性依次给this对象成员属性
初始化,一旦我们手动重构了拷贝构造,编译期就不会提供默认的了。当然也不会存
在默认的无参构造了。当用一个类对象给类的另一个对象初始化时,会调用拷贝构造函数。
对上述话以此拆分讲述
参数为当前类对象的引用。与默认无参构造不同,其函数体代码一般不为空,操作为:参数中对象成员依次给this对象成员进行初始化。
cpp
class CTest {
int m_a;
//默认的拷贝构造函数一般长这个样子,对于初始化代码下面两种写法都可以
CTest(const CTest & tst):m_a(tst.m_a) {//初始化参数列表
//this->m_a = tst.m_a;//或者再构造体内
}
};
上述代码解析: 拷贝构造函数的主要作用是通过已存在的对象来初始化一个新对象。在这个例子中,定义的拷贝构造函数接受一个
CTest
类型的常量引用参数tst
,并使用这个对象的m_a
成员变量来初始化新对象的m_a
成员变量。
当我们手动重构拷贝构造函数时,编译器就不会提供默认的拷贝构造函数了,当然也不会存在默认的无参构造了。
当用一个类对象给类的另一个对象初始化时,会调用拷贝构造函数。
cpp
class CTest {
CTest();
CTest(const CTest &tst);
};
CTest tst1; //调用无参构造
CTest tst2(tst1); //调用拷贝构造
默认的拷贝构造函数:是一个浅拷贝 。
浅拷贝需注意的问题:当类中存在指针成员且指向了一个具体的空间,拷贝构造函数
只是将两个指针里存储的地址进行一个值传递,并不会处理指针指向的空间。这样就
导致了多个对象里的指针指向了同一个空间,那么会导致以下两个问题:
数据一致性问题:
- 当一个对象通过指针修改了其指向的内存空间的内容时,其他对象访问同一内存空间时会得到已修改的值。这种情况下,修改的意图和结果可能不符合预期。如果多个对象共享同一数据,可能会导致意外的副作用。
内存管理问题:
- 如果对象的析构函数试图释放new出来的空间,释放指针所指向的内存空间,而多个对象指向同一地址,将导致后续的对象尝试释放已经被释放的内存,从而引发未定义行为(比如崩溃或内存泄漏)。

解决浅拷贝的问题
解决办法: 深拷贝,它并不是一个固定的写法,而是一个解决的办法:即在拷贝构造时,如果参数对象中的指针成员指向了一个内存空间,那么在重构拷贝构造时,需要为当前this对象中指针成员额外开辟新的内存空间,并初始化对应的值。
即:深拷贝会为每个对象的指针成员分配新的内存空间,并复制指针指向的内容。
cpp
class CTest {
int *m_p;
CTest(const CTest & tst) {
//深拷贝
if (tst.m_p)
m_p = new int(*tst.m_p);
else
m_p = nullptr;
}
};
在某些情况下,可以使用指针或引用可以避免对象的值传递,也避免了浅拷贝问题。
cpp
void fun(CTest tst); //避免值传递
void fun(CTest & tst);// or void fun(CTest *ptst);
例子:
cpp
class MyClass {
private:
int* data;
public:
// 构造函数
MyClass(int value) {
data = new int(value);
}
// 拷贝构造函数
MyClass(const MyClass& other) {
data = new int(*other.data); // 深拷贝
}
// 析构函数
~MyClass() {
delete data; // 释放内存
}
// 赋值操作符重载(如果需要)
MyClass& operator=(const MyClass& other) {
if (this != &other) {
delete data; // 先释放原有内存
data = new int(*other.data); // 深拷贝
}
return *this;
}
};
赋值函数operator=
operator=与拷贝构造的区别:
拷贝构造发生在构造对象的时候,operator=是赋值。
空类中会默认提供一个 operator=函数,函数和返回值 为当前类对象的引用,函数体代码不为空,形参对象中的成员属性依次给this对象成员属性赋值,用当前类对象给另一个类对象进行赋值操作。如果手动重构了operator=,编译器就不会提供默认的了。
cpp
class CTest {
public:
int m_a;
CTest& operator=(const CTest& tst) {
this->m_a = tst.m_a;
}
};
CTest tst1;
CTest tst2;
tst2 = tst1; //匹配operator =
operator=函数 默认也是一个浅拷贝,也会出现浅拷贝问题。解决办法:深拷贝 。
cpp
int * m_p;
CTest& operator=(const CTest& tst) {
if(this != &tst) { //判断是否自己给自己赋值
this->m_a = tst.m_a;
if (tst.m_p) {
if (this->m_p) {
*this->m_p = *tst.m_p;
} else {
this->m_p = new int(*tst.m_p);
}
} else {
if (this->m_p) {
delete this->m_p;
}
this->m_p = nullptr;
}
}
}
空类中存在的默认的函数4个:
- 默认无参数构造
- 默认的拷贝构造
- 默认的operator=
- 默认析构函数