RaymondHuang
RaymondHuang
发布于 2022-12-18 / 63 阅读
0
0

类中的运算符重载

普通运算符重载:

通过运算符重载,可以定义对象执行相应运算符之后的行为,比如定义两个对象相加的行为:

class Person {
public:
    int age;

    Person() : age(18) {}

    Person operator+(const Person& p) { // 重载+运算符,格式一定要这么写
        Person temp;
        temp.age = this->age + p.age;
        return temp;
    }
};

// 写在类外也可以,都一样
Person operator+(const Person& p1, const Person& p2) {
    Person temp;
    temp.age = p1.age + p2.age;
    return temp;
}

int main() {

    Person p1, p2;
    Person p3 = p1 + p2;
    
    cout << p3.age << endl; // 输出36
    return 0;
}

关系运算符重载:

通过定义==与!=运算符,来自定义两个对象的比较关系,默认的==和!=会比较二者的内存地址,很明显永远都不相等。

class Person {
public:
    int id;
    int age;

    Person(int _id, int _age) : id(_id), age(_age) {}

    bool operator==(const Person& p) {
        return (id==p.id && age==p.age);
    }

    bool operator!=(const Person& p) {
        return (id != p.id || age != p.age);
    }
};

int main() {
    Person p1(0, 18);
    Person p2(1, 28);

    cout << (p1 == p2) << endl; // 0
    cout << (p1 != p2) << endl; // 1

    return 0;
}

赋值运算符重载:

通过重载赋值运算符=,我们可以自定义对象的赋值行为,默认的赋值是完全拷贝过去,那我们考虑以下情况:

class Person {
public:
    int* age; // 注意age是一个指针

    Person(int _age) : age(new int(_age)) {} // 注意age在堆区被创建
    ~Person() {
        delete age;
    }
};

void test() {
    Person p1(18);
    Person p2(28);
    p2 = p1;
}

int main() {
    while (true) {
        test();
    }
}

上述的代码会造成野指针内存泄漏两个错误!首先,在执行外赋值语句之后,test函数结束了,那p1会被首先释放,执行了析构函数,age也会被从堆区删除,而轮到p2析构时,因为它的age和p1指向的age是一样的,此时age已经被p1的析构删掉了,p2没东西可以删,所以就会造成野指针。然后p2创建时的age,也就是28,因为没人管,会一直留在堆区,随着循环进行,内存泄漏便发生了!

要解决野指针的问题,我们只需要使用一个深拷贝即可:

class Person {
public:
    int* age;

    Person(int _age) : age(new int(_age)) {}
    ~Person() {
        delete age;
    }
    
    Person& operator=(Person& p) { // 重载=运算符
        age = new int(*p.age); // 在堆区再创建一个age
        return *this;
    }
};

void test() {
    Person p1(18);
    Person p2(28);
    p2 = p1;
}

int main() {
    while (true) {
        test();
    }
}

嗯,此时野指针的问题被解决了,可是内存泄漏的问题依然存在,为此,我们再优化一下:

class Person {
public:
    int* age;

    Person(int _age) : age(new int(_age)) {}
    ~Person() {
        delete age;
    }
    
    Person& operator=(Person& p) {
        if (age != NULL) {
            delete age; // 如果原来存在指向堆区的指针,那么先把堆区里面的东西删掉
        }

        age = new int(*p.age); // 然后在创建一个新的堆区的变量,重新让age指向它
        return *this;
    }
};

void test() {
    Person p1(18);
    Person p2(28);
    p2 = p1;
}

int main() {
    while (true) {
        test();
    }
}

左移运算符重载:

在使用cout输出的时候,我们希望直接输出对象的某些值,这个时候就需要重载左移运算符:

class Person {
public:
    int id;
    int age;

    Person() : id(0), age(18) {}

    void operator<<(ostream& cout) { // 注意传入的是ostream(output stream)的引用
      cout << id << "," << age;
    }
};

int main() {
    Person p;

    p.operator<<(cout);
    p << cout;
    // cout << p // 报错,没有找到对应的方法

    return 0;
}

值得注意的是,上面的重载是写在类体内的,不可以实现cout << p这种常用的写法,为了实现,我们需要把它写到类体外面去,这非常特殊,需要格外记住:

class Person {
public:
    int id;
    int age;

    Person() : id(0), age(18) {}
};

//值得注意的是,传入的是cout和p,和前面的不同,前面的传入的都是一样的两个形参
void operator<<(ostream& cout, const Person& p) { 
    cout << p.id << "," << p.age;
}

int main() {
    Person p;

    operator<<(cout, p); // 成功输出
    cout << p; // 成功输出

    return 0;
}

但现在还不够优雅,我们希望实现连锁调用,于是我们稍作修改:

class Person {
public:
    int id;
    int age;

    Person() : id(0), age(18) {}
};

// 让函数返回cout的引用即可,注意不是返回p的引用,因为要连锁调用的是cout而不是p!
ostream& operator<<(ostream& cout, const Person& p) {
    cout << p.id << "," << p.age << endl;
    return cout;
}

int main() {
    Person p;
    Person p1;
    
    operator<<(operator<<(cout, p), p1);
    cout << p << p1;
    cout << p << p1 << endl;

    return 0;
}

自增(自减)运算符重载:

以下以自增运算符为例,自减是一样的。

首先我们实现“先++,再返回”,即“++a”的形式:

class Person {
public:
    int age;

    Person() : age(18) {}

    // 注意此处返回的是Person&而不是Person,返回引用才能实现连锁调用!
    // 如果返回一个Person对象,那么无论加多少次,最终只是加了1次!原理和之前连锁调用一致。
    Person& operator++() {
        ++age;
        return *this;
    }
};

ostream& operator<<(ostream& cout, const Person& p) {
    cout << p.age;
    return cout;
}

int main() {
    Person p;
    cout << ++p << endl; // 19
    cout << p << endl; // 19
    cout << ++(++p) << endl; // 21
    cout << p << endl; // 21
    return 0;
}

接下来我们实现“先返回,再++”,即“a++”的形式。值得注意的是,C++原生不支持此类型的连锁调用,即“(a++)++”的行为是违法的,因为从原理上无法实现!

class Person {
public:
    int age;

    Person() : age(18) {}
    
    // 首先注意到,为了区别++在前还是在后,此处传参的地方写了一个int,表示在后
    // 然后,返回值是Person而不是Person&,如果返回的是引用,则会导致野指针!
    // 因为引用指向的对象已经在函数结束的时候被销毁了!
    Person operator++(int) {
        Person temp = *this;
        ++age;
        return temp;
    }
};

ostream& operator<<(ostream& cout, const Person& p) {
    cout << p.age;
    return cout;
}

int main() {
    Person p;
    cout << p << endl; // 18
    cout << p++ << endl; // 18
    cout << p << endl; // 19
    cout << (p++)++ << endl; // 19
    cout << p << endl; // 20,虽然上面连锁调用了,但和前面一样,那么无论调用多少次,都只是加了一次!
    return 0;
}

为了防止连锁调用,我们在修改一下函数即可:

class Person {
public:
    int age;

    Person() : age(18) {}
    
    // 在函数返回值加上一个const,返回一个常量,那就杜绝了连锁调用了!
    const Person operator++(int) {
        Person temp = *this;
        ++age;
        return temp;
    }
};

函子(functor):

这个东西比较简单,用处不大,但很好玩:

class Adder {
public:
    int sum;
    Adder(): sum(0) {}
    int operator()(int a, int b) { // 其实本质上就是重载了“括号”运算符
        this->sum += a + b;
        return a + b;
    }
};

int main() {
    Adder adder;
    cout << adder(2,3) << endl; // 输出5
    cout << adder.sum << endl; // 输出5
    cout << adder(2,3) << endl; //输出5
    cout << adder.sum << endl; // 输出10
    return 0;
}

其实上述例子,函子只是重载了括号运算符,比较有用的是,可以记录sum了。


评论