类与对象(3)

上一章类和对象 (2)-优快云博客

详细介绍了类的默认成员函数中的构造函数、析构函数和拷贝函数 

1.赋值运算符重载

1.1运算符重载

运算符重载是C++中一种特殊的函数,它允许开发者为自定义的类类型对象赋予特定的运算符行为。通过运算符重载,类对象可以像内置类型一样使用运算符进行操作,从而提高代码的直观性和可读性。

  • 运算符重载的基本概念

    • 运算符重载是通过定义特定名称的函数来实现的,函数的名称由 operator 关键字和后面要重载的运算符组成。例如,operator+ 用于重载加法运算符。
    • 运算符重载函数与其他函数类似,它有返回类型、参数列表和函数体。
    • 运算符重载时,参数的数量与运算符操作对象的数量一致。通常:
      • 一元运算符有一个参数。
      • 二元运算符有两个参数。
      • 左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
  • 成员函数与非成员函数

    • 如果运算符重载函数是成员函数,第一个参数是隐式的 this 指针,指向调用运算符的对象,因此成员函数的参数个数比运算符对象的数量少一个。
    • 如果运算符重载函数是非成员函数(全局函数),则必须显式地传递所有的参数,包括左右操作数。
  • 运算符优先级与结合性

    • 运算符重载后,其优先级和结合性与内置类型的运算符保持一致。
  • 不可重载的运算符

    • 以下五个运算符不能被重载:
      • .* (成员指针访问运算符)
      • :: (作用域解析运算符)
      • sizeof (大小运算符)
      • ?: (三目运算符)
      • , (逗号运算符)
    • 这些运算符不能通过重载定义新的含义。
  • 运算符重载的意义

    • 运算符重载的目的是为了让特定的操作更符合类的语义。例如,对于 Date 类,重载 operator- 可能有意义(比如日期相减),但重载 operator+ 可能没有实际意义。
  • 前置和后置运算符重载

    • 在重载递增(++)运算符时,必须区分前置和后置版本:
      • 前置递增(++obj)和后置递增(obj++)共享一个函数名 operator++
      • 后置递增需要额外添加一个 int 类型的参数,以便与前置递增区分开来。
  • 重载 <<>> 运算符

    • 对于流插入(<<)和流提取(>>)运算符,应该将它们重载为全局函数,而不是成员函数。原因是:
      • 成员函数会将 this 指针作为第一个参数,这会导致运算符的语法与习惯不符,如 object << cout 会变成 cout << object,这不符合常规的可读性。
      • 重载为全局函数时,第一个参数是 ostreamistream,第二个参数才是类类型对象,这样符合使用习惯。

1.1.1如何实现运算符重载?

成员函数实现

  • 一元运算符通过成员函数实现时,无需参数。
  • 二元运算符通过成员函数实现时,右侧操作数是函数参数。
class MyClass {
public:
    int value;
    MyClass(int v) : value(v) {}

    // 重载一元运算符 -
    MyClass operator-() const {
        return MyClass(-value);
    }

    // 重载二元运算符 +
    MyClass operator+(const MyClass& other) const {
        return MyClass(value + other.value);
    }
};

全局函数实现

  • 使用友元函数可以访问类的私有成员。
  • 通常用于流插入和提取运算符 (<<>>)
class MyClass {
public:
    int value;
    MyClass(int v) : value(v) {}

    // 声明友元
    friend ostream& operator<<(ostream& os, const MyClass& obj);
};

// 全局函数实现
ostream& operator<<(ostream& os, const MyClass& obj) {
    os << obj.value;
    return os;
}

特殊情况

前置与后置递增/递减

  • 前置递增:operator++()
  • 后置递增:operator++(int),通过增加一个虚拟的 int 参数来区分后置版本。
class Counter {
public:
    int count;

    Counter(int c) : count(c) {}

    // 前置++
    Counter& operator++() {
        ++count;
        return *this;
    }

    // 后置++
    Counter operator++(int) {
        Counter temp = *this;
        ++count;
        return temp;
    }
};

流插入 (<<) 和流提取 (>>)

  • 必须使用全局函数重载,因为成员函数会占用左操作数的位置,无法实现标准用法。
class MyClass {
public:
    int value;

    MyClass(int v) : value(v) {}

    friend ostream& operator<<(ostream& os, const MyClass& obj);
};

ostream& operator<<(ostream& os, const MyClass& obj) {
    os << "Value: " << obj.value;
    return os;
}

总结

  • 运算符重载让类对象的操作更加直观和类似于内置类型,但需谨慎使用,以确保符合类的语义。
  • 常用的重载运算符包括 +-===<<>>
  • 注意不可重载的运算符及遵守C++规则,例如不能改变内置类型的运算行为。

 1.2 赋值运算符重载

赋值运算符重载是C++中的一个特殊运算符重载,用于实现两个已经存在对象之间的赋值操作(区别于拷贝构造函数,它用于新对象的初始化)。

  • 必须是成员函数

    • 赋值运算符重载(operator=)只能作为成员函数实现,不能是全局函数。
    • 参数建议使用 const 当前类的引用,避免传值传参时的额外拷贝。
  • 返回类型为当前类的引用

    • 赋值运算符重载通常返回当前类的引用(*this),以支持链式赋值
A a, b, c;
a = b = c; // 链式赋值
  • 默认赋值运算符的行为

    • 如果没有显式定义赋值运算符重载,编译器会生成一个默认版本:
      • 对于内置类型成员变量:执行值拷贝(浅拷贝)。
      • 对于自定义类型成员变量:调用其拷贝赋值运算符。
    • 这种默认行为可能无法正确处理动态资源。
  • 何时需要自定义赋值运算符重载

    • 对于像 Date 类这样的简单类(只有内置类型成员变量且不涉及资源管理),编译器生成的默认赋值运算符通常足够。
    • 对于涉及动态资源(如指针)的类(如 Stack 类),默认的赋值运算符只会完成浅拷贝,可能导致多个对象指向同一资源,进而引发问题。因此需要手动实现深拷贝。
    • 如果类实现了析构函数以释放资源,就应该显式定义赋值运算符重载以确保正确管理资源。
  • 自动调用链

    • MyQueue 类,其成员是自定义类型(如 Stack),编译器生成的赋值运算符会自动调用 Stack 的赋值运算符重载。如果 Stack 的赋值运算符正确实现,MyQueue 通常不需要额外实现赋值运算符重载。
  • 浅拷贝与深拷贝的需求

    • 浅拷贝:直接复制指针值,两个对象共享同一块资源。
    • 深拷贝:为每个对象分配独立的资源,复制内容而非指针。
#include <iostream>
#include <cstring>
using namespace std;

class Stack {
public:
    Stack(int n = 4) {
        _a = new int[n];
        _capacity = n;
        _top = 0;
    }

    // 赋值运算符重载
    Stack& operator=(const Stack& other) {
        if (this != &other) {  // 防止自赋值
            // 释放当前资源
            delete[] _a;

            // 分配新资源并拷贝数据
            _a = new int[other._capacity];
            memcpy(_a, other._a, other._top * sizeof(int));
            _capacity = other._capacity;
            _top = other._top;
        }
        return *this;
    }

    ~Stack() {
        delete[] _a;
    }

private:
    int* _a;
    size_t _capacity;
    size_t _top;
};

 注意

  • 赋值运算符重载是用于处理两个已存在对象之间的赋值,需显式实现以满足类的特定需求。
  • 默认行为:内置类型执行浅拷贝;自定义类型调用其赋值运算符。
  • 深拷贝:适用于管理动态资源的类。
  • 只要类中涉及动态资源,或者实现了析构函数释放资源,就应显式实现赋值运算符重载以防止资源管理问题。
#include <iostream>
using namespace std;

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

    // 拷贝构造函数
    Date(const Date& d) {
        cout << "Date(const Date& d)" << endl;
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    // 赋值运算符重载
    Date& operator=(const Date& d) {
        // 防止自赋值
        if (this != &d) {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        // 返回当前对象,支持链式赋值
        return *this;
    }

    // 打印日期
    void Print() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1(2024, 7, 5);
    Date d2(d1); // 拷贝构造
    Date d3(2024, 7, 6);
    d1 = d3; // 赋值运算符重载

    // 注意:这里是拷贝构造,不是赋值运算符重载
    // 拷贝构造用于一个对象拷贝初始化另一个新对象
    // 而赋值运算符用于两个已存在对象之间的拷贝赋值
    Date d4 = d1;

    return 0;
}

写到这里,就可以引入关键的日期类的实现了(太长所以放另一篇了)

2.取地址运算符重载

在 C++ 中,取地址运算符 & 是一个非常特殊的运算符,它用于返回变量或对象的内存地址。通常情况下,取地址运算符是不能被重载的,因为它是编译器内置的操作。但在 C++ 中,实际上可以 重载“取地址运算符”,让它的行为有所不同。

2.1 const成员函数

2.1.1 什么是 const 成员函数?

在 C++ 中,const 成员函数是指在函数声明的末尾加上 const 关键字,表明该成员函数不会修改对象的状态。也就是说,const 成员函数可以被 const 对象调用,因为它保证不会改变对象的任何数据成员。

2.1.2 const 成员函数的作用

const 成员函数的主要作用是保证不会修改对象的状态。这对于保持类的不变性(即对象一旦创建,其状态不会被改变)非常重要,尤其是当你有 const 类型的对象时,编译器需要确保你只能调用那些不改变对象的函数。

为什么需要 const 成员函数?
  1. 只读操作:有些成员函数只做查询操作,不需要修改对象的成员变量,这时你应该将它们声明为 const 成员函数,以明确表明不会修改对象。
  2. 适应 const 对象:如果你有一个 const 对象,只能调用 const 成员函数,否则会导致编译错误。const 成员函数保证不会修改对象,因此它可以安全地应用于 const 对象。

2.1.3 const 成员函数的声明和定义

const 成员函数的声明和定义非常简单。只需要在函数声明的末尾加上 const,告诉编译器该函数不会修改类的数据成员。

语法

return_type function_name(parameters) const;

这里,const 是放在成员函数声明的末尾,表示该函数在调用时不会修改成员变量。

2.1.4 代码示例:使用 const 成员函数

举个简单的例子

#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass(int value) : _value(value) {}

    // 非 const 成员函数:修改 _value
    void setValue(int value) {
        _value = value;
    }

    // const 成员函数:不修改 _value
    int getValue() const {
        return _value;
    }

private:
    int _value;
};

int main() {
    MyClass obj(42);

    // 调用非 const 成员函数,可以修改 _value
    obj.setValue(100);

    // 调用 const 成员函数,不能修改 _value
    cout << "Value: " << obj.getValue() << endl;

    return 0;
}
  • setValue 函数是一个普通成员函数,它修改了类的成员变量 _value
  • getValue 函数是一个 const 成员函数,它只是读取 _value 的值,不会修改它。
  • main 函数中,obj.getValue() 被调用时,它没有修改对象的状态,这使得它成为一个 const 函数,可以被 const 对象调用。

2.1.5 const 对象与 const 成员函数

const 对象只能调用 const 成员函数。这是为了确保 const 对象的状态不会被修改。

#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass(int value) : _value(value) {}

    // const 成员函数:不会修改 _value
    int getValue() const {
        return _value;
    }

    // 非 const 成员函数:修改 _value
    void setValue(int value) {
        _value = value;
    }

private:
    int _value;
};

int main() {
    const MyClass obj(42); // const 对象

    // 调用 const 成员函数
    cout << "Value: " << obj.getValue() << endl;

    // 错误!不能调用非 const 成员函数
    // obj.setValue(100);  // 编译错误:无法对 const 对象调用非 const 函数

    return 0;
}
  • obj 是一个 const 类型的对象。
  • getValue 是一个 const 成员函数,它可以被 const 对象 obj 调用,因为它不会修改对象的状态。
  • setValue 是一个非 const 成员函数,它试图修改 _value,因此不能被 const 对象调用。

 在高版本vs下无法演示,但想展示的是会输入

error: 'void MyClass::setValue(int)' cannot be called on a const object

vs的话会直接成功

但我们不能指望所以编译器都能自己解决这个错误不是(

2.1.6 const 成员函数访问 const 成员

const 成员函数只能访问 const 成员变量,并且不能修改它们。

#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass(int value) : _value(value) {}

    // const 成员函数:不能修改 _value
    int getValue() const {
        return _value;
    }

    // 这是一个普通成员函数,能够修改 _value
    void setValue(int value) {
        _value = value;
    }

private:
    mutable int _value; // 允许在 const 成员函数中修改该成员
};

int main() {
    MyClass obj(42);
    
    // 调用 const 成员函数,安全地访问成员
    cout << "Value: " << obj.getValue() << endl;

    // 调用非 const 成员函数,修改成员
    obj.setValue(100);

    // 调用 const 成员函数,再次打印
    cout << "Updated Value: " << obj.getValue() << endl;

    return 0;
}

mutable 关键字

  • mutable 关键字允许在 const 成员函数中修改该成员,即使该成员通常在 const 成员函数中是不可修改的。
  • 在这个示例中,我们使用 mutable 关键字使 _value 成员在 const 成员函数中仍然可以被修改。

2.2 取地址运算符(&

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动生成的就可以够我们用了,不需要去显示实现。除非一些很特殊的场景,比如我们不想让别⼈取到当前类对象的地址,就可以自己实现⼀份,胡乱返回⼀个地址。

2.2.1 取地址运算符(&)的默认行为

首先,你应该知道,C++ 中的取地址运算符 & 是一个内建的运算符,它用于获取一个对象的内存地址。

比如,你有一个变量 int x = 10;,你可以通过 &x 获取 x 变量的内存地址。

int x = 10;
cout << &x << endl;  // 输出 x 的内存地址

在这里,&x 返回的是 x 变量的内存地址,通常是一个指针类型。

2.2.2 为什么要重载取地址运算符?

通常情况下,取地址运算符 & 会返回对象的内存地址,但是你可以重载它,改变它的默认行为,让它返回你想要的内容。就像你可以在 operator+ 里定义两个数相加的规则一样,你也可以在 operator& 里定义如何取地址。

这就是取地址运算符重载的真正目的!你可以通过重载它,来改变对象“取地址”的行为。例如,你可能不想返回对象的直接地址,而是返回某个特殊的指针、内存位置,或者做一些特定的处理。

2.2.3 重载取地址运算符的基本语法

class MyClass {
public:
    int data;
    
    // 重载取地址运算符
    int* operator&() {
        cout << "调用了重载的&运算符!" << endl;
        return &data;  // 返回成员变量 data 的地址
    }
};

在这个例子中,operator&() 是我们重载的取地址运算符,它改变了取地址的行为。每当我们通过 & 操作符去获取 MyClass 类的对象的地址时,实际调用的是这个重载的 operator&() 方法,而不是默认的行为。

2.2.4 实际例子

比如说...智能指针,智能指针的目标是像普通指针一样使用,但是它会负责内存的管理,防止内存泄漏。我们可以通过重载取地址运算符,让智能指针更加灵活。

#include <iostream>
using namespace std;

class SmartPointer {
private:
    int* ptr; 

public:
    SmartPointer(int value) {
        ptr = new int(value);
    }

    ~SmartPointer() {
        delete ptr;
        cout << "内存已释放" << endl;
    }

    int* operator&() {
        cout << "访问智能指针的内存地址!" << endl;
        return ptr;  
    }

    int getValue() const {
        return *ptr;
    }
};

int main() {
    SmartPointer sp(10); 
    cout << "值: " << sp.getValue() << endl;

    int* p = &sp;  // 调用 operator&()
    cout << "指针地址: " << p << endl;
    cout << "值: " << *p << endl;

    return 0;
}

详细可见 智能指针讲解-优快云博客

2.2.5 为什么要重载取地址运算符?

通过重载取地址运算符,智能指针不仅能像普通指针一样访问对象,还能提供额外的功能,例如:

  • 打印信息:在取地址时输出调试信息(比如 cout << "访问智能指针的内存地址!"),方便我们调试程序。
  • 资源管理:控制如何访问和管理内存。通过重载 operator&,你可以灵活地控制指针的行为,而不仅仅是简单的返回内存地址。

2.2.6 注意事项

  • 滥用风险:重载运算符虽然很方便,但滥用可能会导致代码变得不易理解。例如,改变取地址的默认行为可能让其他程序员感到困惑,难以理解代码的原意。
  • 与引用运算符区分:在 C++ 中,& 运算符既有取地址的作用,也有作为引用的意义。取地址和引用运算符虽然使用相同的符号,但实际上是两个不同的概念。

下一章讲构造函数和静态成员,大概还有类型转换 类与对象(4)-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值