C++类和对象 - 拷贝构造, 赋值重载

拷贝构造函数

拷贝构造作用:一个已经存在的对象去初始化另一个要创建的对象

日常写代码中会出现如下场景: 

class Data
{
public:
    Data(int year, int month, int day)    // 拷贝构造函数
    {
        this->_year = year;
        this->_month = month;
        this->_day = day;
    }
private:
    int _year;
    int _month;
    int _day;
}

int main()
{
    Data d1(2024.11.11);
    Data d2(d1);
}

 在上面的代码中, 我们想要使用一个已存在的对象去初始化另一个对象, 此时拷贝构造的作用就体现出来了.

class Data
{
public:
    Data(Data& data)    // 拷贝构造函数
    {
        this->_year = data._year;
        this->_month = data._month;
        this->_day = data._day;
    }
private:
    int _year;
    int _month;
    int _day;
}

拷贝构造函数特性:

  1. 是构造函数的一种重载形式.
  2. 参数只有一个,且必须是同类型对象的引用.

 推荐拷贝构造函数加上 const 修饰

class Data
{
public:
    Data(const Data& data)    // 拷贝构造函数
    {
        this->_year = data._year;
        this->_month = data._month;
        this->_day = data._day;
    }
private:
    int _year;
    int _month;
    int _day;
}

因为我们只是想要复制对象, 并不需要修改, 所以加上 const 修饰更加安全, 防止对象数据被修改.

参数必须是引用

上面说, 因为拷贝构造是不会修改对象属性的, 所以推荐加上 const 修改.

那如果我不传引用, 参数就是对象的一个副本, 这样即使修改了参数, 也不会影响外部的对象. 这样就可以不写 const.

class Data
{
public:
    Data(Data data)    // 拷贝构造函数
    {
        this->_year = data._year;
        this->_month = data._month;
        this->_day = data._day;
    }
private:
    int _year;
    int _month;
    int _day;
}

上面的代码是错误的. 拷贝构造函数的参数有且只有一个, 必须是同类型对象的引用.

首先, 我们知道拷贝构造函数的作用: 一个已经存在的对象去初始化另一个要创建的对象.
如果, 传参用的不是引用, 那么那个参数就是原对象的副本, 这个副本是怎么来的?
这个副本也是通过那个"已存在的对象创建出来的" , 这不就是拷贝构造函数的作用吗?
这个副本是通过拷贝构造函数创建出来的, 而拷贝构造函数又需要这个副本, 就形成了一个死循环.

这样最终就会造成无限死循环.

编译器默认生成的拷贝构造函数

拷贝构造也是C++中的默认成员函数.
如果我们没有显示的写构造函数, 编译器就会帮我们生成一个默认的拷贝构造函数.

编译器默认生成的拷贝构造内置类型会赋值(值拷贝),自定义类型会去调用自定义类的拷贝构造.

在涉及到动态内存管理时, 使用默认生成的拷贝构造函数可能会出现问题. (浅拷贝问题)

class Stack
{
public:
    Stack(int size)
    {
        _size = size;
        _array = (int*)malloc(sizeof(int) * size);
        if(_array == nullptr)
        {
            perror("malloc 申请空间失败");
        }
    }

    ~Stack()
    {
        if(_array)
        {
            free(_array);
        }
    }
private:
    int* _array;
    int _size;
}

int main()
{
    Stack s1(10);
    Stack s2(s1);
}

上面的代码中没有显示的写构造函数, 用的就是编译器生成的默认函数.

指针也属于C++的内置类型 (内置类型值拷贝), 所以通过 s1  去初始化 s2 时, s2 中的 _array 与 s1的 _array 指向的是同一块空间. 

\

可能会导致的问题:

当 s1 和 s2 生命周期结束时

它们都会调用析构函数释放空间

s1 的 _array 和 s2 的 _array 所指向的空间是同一个

s1 和 s2 一共两次析构, 就会对那块空间进行两次释放

对一快空间进行两次释放, 会导致程序出错

所以当涉及到动态内存管理时, 需要格外注意浅拷贝问题.

赋值重载

复制重载是运算符重载的一种.

复制重载就是对 "=" 实现运算符重载.

运算符重载

Data d1(2024.11.11);
Data d2 = d1 + 100;

在代码中有一个 d1 对象, 我们想得到一个新的日期类 d2, d2 日期类 = d1日期 加上100天.
我们直接使用 "+" 运算符是无法进行计算的. 

C++中, 普通的 "+" 运算符只能用于内置类型,
对于自定义类型是不适用的.
如果我们像是让 "+" 运算符支持自定义类型,
我们就需要去实现 "+" 运算符的重载.

为什么会出现运算符重载?

我们完全可以实现一个成员函数来完成这些功能

比如 "+" 运算符的功能, 我实现一个plus(int day) 成员函数

也是完全可以实现所需功能的.

使用运算符重载最主要好处是: 

便于代码的理解, 当我们在代码中看到一个成员函数

我们需要去看函数接口的文档/讲解, 而在代码中使用 "+"运算符

很容易就让人理解这是在完成 "数据相加" 的操作, + 的原始作用就是 "数据相加"

 所以使用运算符重载最重要的目的之一: 便于代码的理解.

// 返回值类型 operator运算符(参数列表)
Data operator+(Data d1, int x)
{}

 这样就完成了 "+" 运算符的重载.
返回一个 Data 类型的数据, 需要两个参数, 一个 Data 类型, 一个 int 类型数据. {} 中就是函数体

运算符重载都是针对自定义类型的
所以运算符重载一般都写在类内, 当作一个成员函数

class Data
{
public:
    Data(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    bool operator==(const Data& d)
    {
        return _year == d.year && _month == d.month && _day == d.day;
    }
private:
    int _year;
    int _month;
    int _day;
}

int main()
{
    Data d1(2024, 11, 11);
    Data d2(2024, 11, 11);
    if(d1 == d2)    // 调用了 d1的operator==, 所以隐藏的this指向的就是d1
    {
        cout << "两个日期相等" << endl;
    }
}

写在内类, 第一个类型参数就不用我们自己写了.
因为成员函数默认会又一个 this 指针. 代替了我们手动传递.

operator= 赋值重载

赋值重载也是C++的默认成员函数之一.
当我们不显示的写时, 编译器会自动生成一个.
自动生成的赋值运算符重载, 对于内置类型时直接复制,
对于自定义类型则会调用对应类型的赋值运算符重载函数.

赋值运算符的重载只能写在内类

如果显示的写在了类外

那么类内和类外的两个赋值重载就会冲突

class Data
{
public:
    Data(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    bool operator==(const Data& d)
    {
        return _year == d.year && _month == d.month && _day == d.day;
    }

    Data& operator=(const Data& d)    // 赋值重载
    {
        if(*this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this;
    }
private:
    int _year;
    int _month;
    int _day;
}

int main()
{
    Data d1(2024, 11, 11);
    Data d2 = d1;    // "=" 运算符重载
    if(d1 == d2)    // "==" 运算符重载
    {
        cout << "两个日期相等" << endl;
    }
    return 0;
}

this 是调用的对象的地址, 返回 *this 就是返回对象本身.

前置++和后置++

前置++ 和 后置++的参数相同, 返回值不同.
这是构不成重载的, 所以C++为了区分前置++ 和 后置++做了点特殊处理.

前置++:

Data& operator++();

后置++:

Data& operator++(int)

可以看到前置++没有什么改变.
在后置++ 中增加了一个参数.

后置++中的那个参数完全没有用处
只是为了区别前置++和后置++

 

class Data
{
public:
    Data& operator++()    // 前置++
    {}
    Data& operator++(int)    // 后置++
    {}
private:
    int _year;
    int _month;
    int _day;
}

int main()
{
    Data d1(2024, 11, 11);
    d1++;
    ++d1;
    return 0;
}

可以看到后置++多的那个参数不用关心, 只是为了区分前置++和后置++, 也无需为那个参数传参

五个不能重载的运算符

  1. "::"
  2. "." 
  3. "?:"
  4. ".*"
  5. "sizeof"

这五个运算符是不能被重载的.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值