C++学习 day6

发布于 25 天前  29 次阅读


Day 6:拷贝构造、深浅拷贝和类型转换

今天目标:理解对象复制时会发生什么,掌握拷贝构造函数、深拷贝与浅拷贝、delete/default,并认识 C++ 的四种显式类型转换。

对应代码:

2-代码/day6-/29-拷贝构造函数示例.cpp
2-代码/day6-/30-拷贝构造函数示例2.cpp
2-代码/day6-/31-delete与default示例.cpp
2-代码/day6-/32-显示类型转换示例.cpp

1、拷贝构造函数

拷贝构造函数用于用一个已经存在的对象,初始化另一个同类型的新对象。

基本格式:

class Demo
{
public:
    Demo(const Demo& rhs);
};

说明:

Demo(const Demo& rhs)

表示参数是本类类型的 const 引用,这种构造函数就是拷贝构造函数。

示例:

class Demo
{
public:
    Demo(int x, int y) : _x(x), _y(y) {}

    Demo(const Demo& rhs)
    {
        _x = rhs._x;
        _y = rhs._y;
        cout << "Demo(const Demo&)" << endl;
    }

private:
    int _x;
    int _y;
};

如果没有自己写拷贝构造函数,编译器会自动生成一个,默认按成员变量逐个复制。


2、拷贝构造函数的调用时机

常见调用场景有三种。

2.1、用已有对象创建新对象

Demo d1(100, 200);

Demo d2(d1);
Demo d3{d1};
Demo d4 = d1;

这些都是用 d1 初始化一个新的 Demo 对象,会调用拷贝构造函数。

2.2、对象作为函数参数按值传递

void foo(Demo d)
{
}

Demo d1(100, 200);
foo(d1);

调用 foo(d1) 时,形参 d 是一个新对象,需要用 d1 拷贝初始化。

2.3、函数按值返回对象

Demo foo()
{
    Demo d(1, 1);
    return d;
}

返回对象时,可能调用拷贝构造函数。

注意:

现代编译器可能会进行返回值优化,省略某些拷贝构造函数调用。
所以实际运行时,不一定每次都能看到拷贝构造函数输出。

3、浅拷贝和深拷贝

3.1、浅拷贝

浅拷贝就是简单复制成员变量的值。

如果成员变量是普通类型,例如 intdouble,浅拷贝通常没有问题。

但如果成员变量是指针,浅拷贝只会复制指针地址:

两个对象的指针成员指向同一块堆内存。

这样会产生问题:

1. 修改一个对象的数据,可能影响另一个对象。
2. 两个对象析构时,会对同一块内存 delete 两次。
3. 程序可能崩溃。

3.2、深拷贝

深拷贝是在拷贝对象时,重新申请一块新的内存,再把原对象中的数据复制过去。

示例:

class Demo
{
public:
    Demo(int x, int y)
    {
        _x = new int(x);
        _y = new int(y);
    }

    ~Demo()
    {
        delete _x;
        delete _y;
    }

    Demo(const Demo& rhs)
    {
        _x = new int(*rhs._x);
        _y = new int(*rhs._y);
    }

private:
    int* _x;
    int* _y;
};

这样拷贝后:

d1._x 和 d2._x 指向不同的内存。
d1._y 和 d2._y 指向不同的内存。
两个对象互不影响,析构时也不会重复释放同一块内存。

一句话理解:

浅拷贝:只复制地址。
深拷贝:重新申请内存,再复制内容。

4、delete 和 default 关键字

这里的 delete 和释放堆内存的 delete p; 不是同一种用法。

4.1、= delete

= delete 用来删除某些函数,禁止别人调用。

常见用途:禁止对象拷贝。

class Demo
{
public:
    Demo() = default;
    Demo(const Demo&) = delete;
};

int main()
{
    Demo d1;
    // Demo d2 = d1;  // 错误,拷贝构造函数被删除
}

适合场景:

对象管理独占资源,不希望被复制。
例如文件描述符、锁、某些硬件资源等。

4.2、= default

= default 表示要求编译器生成默认版本的函数。

class Demo
{
public:
    Demo() = default;
};

常见用途:

1. 明确保留默认构造函数。
2. 让代码意图更清楚。
3. 配合 = delete 控制类能做什么、不能做什么。

小结:

= delete   禁止编译器生成或禁止调用某个函数。
= default  要求编译器生成默认版本的函数。

5、C++ 显式类型转换

C 语言风格转换:

int a = (int)3.14;

C++ 更推荐使用下面四种显式类型转换,因为语义更清楚。


6、static_cast

static_cast 用于比较正常、风险较低的类型转换。

常见用途:

1. 基础类型之间转换。
2. void* 和其他类型指针之间转换。
3. 有继承关系的类指针之间转换。

示例:

double d = 3.14;
int a = static_cast<int>(d);

说明:

3.14 转成 int 后,小数部分会被丢掉,结果是 3。

7、reinterpret_cast

reinterpret_cast 用于非常底层、风险较高的强制解释。

常见用途:

1. 不同类型指针之间强转。
2. 整数和指针之间强转。

示例:

double d = 3.14;
int* p = reinterpret_cast<int*>(&d);

注意:

reinterpret_cast 只是换一种类型去解释同一块内存。
它不保证转换后的结果合理,也不保证访问是安全的。

普通业务代码中应尽量少用。


8、const_cast

const_cast 用来去掉指针或引用上的 const 属性。

示例:

int a = 100;
const int* p = &a;

int* q = const_cast<int*>(p);
*q = 200;

这种情况中,a 本身不是常量,只是通过 const int* 不能修改,所以去掉 const 后修改通常可以。

但如果原对象本身就是 const,再强行修改就是未定义行为:

const int a = 100;
const int* p = &a;

int* q = const_cast<int*>(p);
*q = 200;  // 未定义行为

注意:

const_cast 只能去掉访问路径上的 const。
如果对象本身真的是 const,不应该修改它。

9、dynamic_cast

dynamic_cast 主要用于多态类型之间的安全向下转换。

一般用于继承和虚函数相关场景:

Base* p = new Derived;
Derived* q = dynamic_cast<Derived*>(p);

当前阶段先记住:

dynamic_cast 常用于有继承关系、有虚函数的类之间转换。
后面学习继承和多态时再重点掌握。

10、Day6 总结

1. 拷贝构造函数用于用已有对象初始化新对象。
2. 拷贝构造函数参数通常写成 const 类名&。
3. 对象按值传参、按值返回、用已有对象创建新对象时,可能调用拷贝构造。
4. 类中有指针成员并管理堆内存时,要注意深拷贝。
5. 浅拷贝只复制地址,深拷贝会重新申请内存并复制内容。
6. = delete 可以禁止某些函数被调用,例如禁止拷贝。
7. = default 可以要求编译器生成默认版本的函数。
8. static_cast 用于常规转换。
9. reinterpret_cast 用于底层重新解释,风险较高。
10. const_cast 用于去掉 const,但不能修改真正的 const 对象。
11. dynamic_cast 用于多态类型转换,后面继承多态再深入。

一句话记忆:

Day6 的重点是对象复制:有资源就想深拷贝,不想被复制就 delete;类型转换要选语义明确的 C++ 写法。

"When faced with uncertainty, ask the spring breeze."