c++ 运算符重载(二)

1. 输出运算符重载

1.1 简介

输出运算符重载,实际上是 << 的重载。 << 实际上是位移运算符,但是在c++里面,可以使用它来配合cout在控制台打印输出。 cout 其实是ostream 的一个实例,而ostrem 是 类basic_ostream的一个别名, basic_ostream里面对 << 运算符进行了重载,能使用cout << 来输出内容

1.2 来源

cpp 复制代码
#include <iostream>

using namespace std;

class stu {
public :
    string name;
    int age;
    stu(string name , int age):name(name) , age(age){}
};


int main() {
    stu s("王腾" , 16);
    cout << s.name << " : " << s.age <<endl;

    cout << 18; // operator<<(int n);
    cout << "王腾"; // operator<<(char n);

    return 0;
}

coutbasic_ostream类的一个对象,它的后面跟上一个 << 符号,实际上就是调用了 basic_ostream 这个类里面的一个函数 , 函数是: operator<<()

bash 复制代码
using ostream       = basic_ostream<char, char_traits<char>>;
bash 复制代码
class basic_ostream;

cout << s; 也就是 operator<<(stu s);,直接输出对象,那么需要重载 << 符号,因为标准库里面没有 stu 类,同时不建议去修改cout对应的类的源码,所以应该使用全局的方式定义

1.3 全局定义

cpp 复制代码
#include <iostream>

using namespace std;

class stu {
public :
    string name;
    int age;
    stu(string name , int age):name(name) , age(age){}
};

ostream& operator<<(ostream &  os, stu & s){
    os << s.name << "  " <<s.age <<endl;
    return os;
}

int main() {
    
    stu s0("王腾" , 16);
    stu s1("华云飞" , 17);

    cout << s0 << s1 ;

    return 0;
}

全局的定义方式重载 << :

cpp 复制代码
void operator<<(ostream &  os, stu & s)

第一个参数是 cout 对象,第二个参数是要打印的对象,返回值 void,如果要链式调用,返回值是 cout 对象

第一个参数必须要加上 & 变成引用的类型,因为cout对象的类禁止拷贝

第二个参数加不加都可以,加上的话不会开辟新的空间。

返回值一定得是引用类型,因为cout对象禁止拷贝

2. 输入运算符重载

输入运算符重载,实际上就是 >> 的重载,用法和上面的输出运算符重载相似

cpp 复制代码
#include <iostream>

using namespace std;

class stu {
public :
    string name;
    int age;

    stu(){};
    stu(string name , int age):name(name) , age(age){}
};



ostream& operator<<(ostream &  os, stu & s){
    os << s.name << "  " <<s.age <<endl;
    return os;
}

istream& operator>> (istream& in, stu &s1) {
    in >> s1.name >> s1.age;

    return in;
}


int main() {

    stu s1;

    cout << "请输入学生的姓名和年龄:" << endl;
    cin >>  s1;

    //打印学生, 实际上是打印他的名字。
    cout << s1  <<endl ;
    return 0;

    return 0;
}

运行过程:

bash 复制代码
请输入学生的姓名和年龄:
王腾
12
王腾  12

3.赋值运算符重载

3.1 拷贝赋值

3.1.1 赋值运算符

cpp 复制代码
#include <iostream>

using namespace std;

class stu {
public :
    string name;
    int age;
    stu(){}
    stu(string name , int age):name(name) , age(age){}
};


int main() {

    int a = 30;  
    int b = 40;  
    a = b; // 把b拷贝放到区域a里面去

    //------------------------------------------------
    stu s1 ("李宝" , 17);
    stu s2 ("王威" , 19);
    s2 = s1; //执行 拷贝赋值运算符 函数

    stu s3 = s1; // 执行拷贝构造函数

    cout << "s1::" << s1.name << "=" <<s1.age <<endl;
    cout << "s2::" << s2.name << "=" <<s2.age <<endl;

    return 0;
}

3.1.2 拷贝赋值函数

拷贝赋值函数 (Copy Assignment Operator Function )是C++中的特殊成员函数,用于定义对象从另一个对象进行拷贝赋值操作时的行为。它使用赋值运算符 = 进行重载,允许对象之间的数据成员进行复制。拷贝赋值函数的声明形式为 ClassName& operator=(const ClassName& other),其中 ClassName 是类名

cpp 复制代码
#include <iostream>

using namespace std;

class stu {
public :
    string name;
    stu(string name):name(name) {}

    //类当中的特殊的成员函数之一: 拷贝赋值函数
    stu& operator=(const stu & s2){
        cout << "执行 拷贝赋值运算符函数...." <<endl;
        name = s2.name;
        return *this;
    }

};

int main() {

    int a = 30;
    int b = 40;
    int c = 50;
    a =  b = c;

    cout << "a: "  << a <<endl;
    cout << "b: " << b <<endl;
    cout << "c: " << c <<endl;


    stu s1("李宝");
    stu s2 ("王威");

    s1 = s2 ;
    cout << "s1::"  <<s1.name <<endl;
    cout << "s2::" << s2.name <<endl;

    return 0;
}

运行结果:

cpp 复制代码
a: 50
b: 50
c: 50
执行 拷贝赋值运算符函数....
s1::王威
s2::王威
3.1.2.1 特点
  1. 拷贝赋值函数被用于将一个已存在对象的值复制给另一个已经存在的对象。
  2. 会被隐式调用,例如在赋值语句中或者作为参数传递给函数时。执行的是浅拷贝(逐个成员变量的拷贝)。
  3. 但当类中包含动态分配内存、文件句柄或其他资源时,需要自定义拷贝赋值函数来避免浅拷贝带来的问题。
3.1.2.2 使用场景
  1. 避免默认的浅拷贝:当类中包含指针成员或资源需要特殊管理时,需要自定义拷贝赋值函数来确保正确的拷贝行为,避免默认的浅拷贝带来的问题。
cpp 复制代码
#include <iostream>

class MyClass {
public:
    int* data = nullptr;

    MyClass(int value):data(new int(value)) {}

    // 拷贝赋值函数
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {  // 避免自我赋值 &other->other对象的地址
            delete data;  // 释放原有资源
            data = new int(*other.data);  // 执行深拷贝
        }
        return *this;
    }

    ~MyClass() {
        if (data !=nullptr ){
            delete data;
            data = nullptr;
        }

    }
};

int main() {
    MyClass obj1(36);
    MyClass obj2(29);

    obj2 = obj1;  // 调用拷贝赋值函数
    std::cout << *obj2.data << std::endl; 

    return 0;
}

MyClass 类包含一个指针成员 data。为了避免默认的浅拷贝,自定义了拷贝赋值函数,执行了深拷贝操作。当进行赋值操作 obj2 = obj1 时,拷贝赋值函数被调用,obj2 的 data 成员被复制为 obj1 的 data 成员的副本。这样,每个对象都有独立的资源,避免了指针成员的共享。

  1. 资源管理类:在某些情况下,需要确保每个对象拥有独立的资源,而不是共享同一个资源。通过自定义拷贝赋值函数,可以执行深拷贝,从而避免多个对象共享同一资源。
cpp 复制代码
#include <iostream>
#include <cstring>

class String {
public:
    char* data;
    size_t size;

    // 有参构造函数
    String(const char* str) {
        size = strlen(str);
        data = new char[size + 1];
        strcpy(data, str);
    }

    // 拷贝构造函数
    String(const String& other) {
        size = other.size;
        data = new char[size + 1];
        strcpy(data, other.data);
    }

    // 拷贝赋值函数
    String& operator=(const String& other) {
        if (this != &other) {  // 避免自我赋值
            delete[] data;  // 释放原有资源
            size = other.size;
            data = new char[size + 1];
            strcpy(data, other.data);
        }
        return *this;
    }

    ~String() {
        delete[] data;
    }
};

int main() {
    String str1("Hello");
    String str2("World");

    str2 = str1;  // 调用拷贝赋值函数

    std::cout << str2.data << std::endl;  // 输出:Hello

    return 0;
}

String 类封装了一个动态分配的字符数组作为字符串的存储。自定义了拷贝赋值函数,执行了深拷贝操作,确保每个对象都有独立的字符串资源。当进行赋值操作 str2 =str1 时,拷贝赋值函数被调用,str2 的 data 成员被复制为 str1 的 data 成员的副本。

  1. 链式赋值:拷贝赋值函数的返回类型通常是类的引用,这使得可以实现链式赋值,即连续多次赋值操作。
cpp 复制代码
#include <iostream>

class Counter {
public:
    int count;

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

    // 拷贝赋值函数
    Counter& operator=(const Counter& other) {
        if (this != &other) {  // 避免自我赋值
            count = other.count;
        }
        return *this;
    }
    
    ~Counter(){}
};

int main() {
    Counter c1(5);
    Counter c2(10);
    Counter c3(15);

    c3 = c2 = c1;  // 链式赋值

    std::cout << c1.count << std::endl;  // 输出:5
    std::cout << c2.count << std::endl;  // 输出:5
    std::cout << c3.count << std::endl;  // 输出:5

    return 0;
}

3.2 移动赋值

3.2.1 简介

移动赋值函数(Move Assignment Operator Function )是一种特殊的成员函数,用于实现对象的移动赋值操作。它通常以右值引用作为参数,并返回一个对当前对象 的引用。移动赋值函数的主要目的是通过直接转移资源拥有权来提高效率,而不是进行深拷贝。

移动赋值函数的声明形式为 ClassName& operator=(ClassName&& other),其中 ClassName 是类名。

3.2.2 特点

  1. 参数为右值引用(例如 T&&)。
  2. 返回类型为类类型本身的引用。
  3. 通常会修改原始对象和被赋值对象,移动赋值函数将移动源对象的状态置为有效但未定义的状态,因此在移动后,移动源对象的值不能再被使用,必须谨慎操作。

3.2.3 使用场景

  1. 临时对象的转移:当有临时对象或右值对象需要赋值给另一个对象时,可以使用移动赋值函数,避免不必要的拷贝操作,提高性能。
cpp 复制代码
#include <iostream>

class String{
public:
    char* data;
    int size;

    String(const char* str) {
        size = strlen(str);
        data = new char[size + 1];
        strcpy(data, str);
        std::cout << "有参构造 ..." << std::endl;
    }

    // 移动赋值函数
    String& operator=(String&& other) {
        if (this != &other) {  // 避免自我赋值
            delete[] data;  // 释放原有资源
            data = other.data;  // 转移资源所有权
            size = other.size;
            other.data = nullptr;  // 将移动源对象的资源置为空指针
            other.size = 0;
            std::cout << "移动赋值 ..." << std::endl;
        }
        return *this;
    }

    ~String() {
        delete[] data;
    }
};

int main() {
    String str1("Hello");
    String str2("World");

    str2 = std::move(str1);  // 调用移动赋值函数

    return 0;
}

String 类表示一个字符串类,拥有字符指针成员 data 和大小成员 size。我们实现了移动赋值函数,通过转移资源所有权来实现高效的移动操作。在 main() 函数中,使用 std::move() 将 str1 转换为右值,然后将其赋值给 str2。这将调用移动赋值函数,并将 str1 的资源转移到 str2,避免了不必要的字符串拷贝。

  1. 容器类的优化:对于需要管理大型数据结构(如动态数组、链表等)的容器类,通过实现移动赋值函数,可以实现容器之间的高效资源转移。
cpp 复制代码
#include <iostream>
#include <vector>

class LargeData {
public:
    std::vector<int> data;

    LargeData(std::initializer_list<int> values) : data(values) {
        std::cout << "Constructor called" << std::endl;
    }

    // 移动赋值函数
    LargeData& operator=(LargeData&& other) {
        if (this != &other) {  // 避免自我赋值
            data = std::move(other.data);  // 转移资源所有权
            std::cout << "Move assignment operator called" << std::endl;
        }
        return *this;
    }
};

int main() {
    LargeData data1{1, 2, 3, 4, 5};
    LargeData data2{6, 7, 8, 9, 10};

    data2 = std::move(data1);  // 调用移动赋值函数

    return 0;
}

LargeData 类表示一个大型数据类,其中使用 std::vector 存储数据。实现了移动赋值函数,通过转移 std::vector 的资源所有权来实现高效的移动操作。在 main() 函数中,使用 std::move() 将 data1 转换为右值,并将其移动赋值给 data2。这将调用移动赋值函数,并将 data1 的资源转移到 data2,避免了不必要的数据拷贝。

3.2.4 注意点

  1. 避免自我赋值:在移动赋值函数的实现中,需要首先检查待移动的对象与当前对象是否是同一个对象,以避免在自我赋值的情况下出现错误的资源释放。
  2. 转移资源所有权:在移动赋值函数中,需要将待移动对象的资源的所有权转移到当前对象中,通常通过调用移动构造函数或移动赋值运算符来实现。对于动态分配的资源,需要在转移所有权之前释放当前对象的资源。

3.3 禁用拷贝赋值,只用移动赋值

cpp 复制代码
class ResourceHolder {
public:
    ResourceHolder() { /* 执行资源分配操作 */ }

    ResourceHolder(const ResourceHolder&) = delete; // 禁用拷贝构造函数
    ResourceHolder& operator=(const ResourceHolder&) = delete; // 禁用拷贝赋值运算符

    ResourceHolder(ResourceHolder&& other) noexcept {
        // 移动构造函数,将资源从other转移到当前对象
        this->resource_ = other.resource_;
        other.resource_ = nullptr;
    }

    ResourceHolder& operator=(ResourceHolder&& other) noexcept {
        // 移动赋值运算符,将资源从other转移到当前对象
        if (this != &other) {
            releaseResource(); // 释放当前对象的资源
            this->resource_ = other.resource_;
            other.resource_ = nullptr;
        }
        return *this;
    }

private:
    Resource* resource_; // 假设有一个指向某个资源的指针

    void releaseResource() {
        if (resource_) {
            /* 执行资源释放操作 */
            delete resource_;
            resource_ = nullptr;
        }
    }
};
相关推荐
咖啡里的茶i28 分钟前
C++之继承
c++
NormalConfidence_Man37 分钟前
C++新特性汇总
开发语言·c++
风清扬_jd1 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
冷白白1 小时前
【C++】C++对象初探及友元
c语言·开发语言·c++·算法
睡觉然后上课2 小时前
c基础面试题
c语言·开发语言·c++·面试
qing_0406032 小时前
C++——继承
开发语言·c++·继承
ya888g2 小时前
GESP C++四级样题卷
java·c++·算法
小叶学C++2 小时前
【C++】类与对象(下)
java·开发语言·c++
NuyoahC2 小时前
算法笔记(十一)——优先级队列(堆)
c++·笔记·算法·优先级队列
FL16238631293 小时前
[C++]使用纯opencv部署yolov11-pose姿态估计onnx模型
c++·opencv·yolo