19. 特殊运算符

特殊运算符

在 C++ 中,有一些特殊运算符,如 ++​、--​、new​、delete ​等,它们与传统的一元或二元运算符(例如 +​、<​、~​)有所不同。这些特殊运算符在使用时,从代码中的使用到程序员定义的映射与传统运算符有轻微的差别。其中,取下标([]​)和函数调用(()​)是两种最重要的用户自定义运算符。

取下标运算符([]​)

通过 operator[] ​函数,我们可以为类对象的下标赋予新的含义。operator[] ​函数的第二个参数(下标)可以是任意类型,因此常被用于定义类似 vector​、关联数组等类型。

operator[]() ​必须是非 static 成员函数。

#include <iostream>
#include <map>
#include <string>

class StringIntMap {
private:
    std::map<std::string, int> data;

public:
    // 重载下标运算符
    int& operator[](const std::string& key) {
        return data[key];
    }

    // 为了完整性,我们也提供一个const版本的operator[]
    const int& operator[](const std::string& key) const {
        return data.at(key);
    }
};

int main() {
    StringIntMap map;

    // 使用下标运算符设置值
    map["apple"] = 1;
    map["banana"] = 2;
    map["cherry"] = 3;

    // 使用下标运算符获取值
    std::cout << "apple: " << map["apple"] << std::endl;
    std::cout << "banana: " << map["banana"] << std::endl;
    std::cout << "cherry: " << map["cherry"] << std::endl;

    return 0;
}

函数调用运算符(()​)

在 C++ 中,函数调用运算符 () ​可以像其他运算符一样被重载,使得对象能够像函数一样被调用。这种运算符重载对于创建类似函数的对象(函数对象)特别有用,它们可以保存数据并在调用时执行操作。

以下是一个名为 Add ​的类,它是一个函数对象,用于将一个复数加到另一个复数上:

class Add {
    complex val;
public:
    Add(complex c) : val(c) {}
    Add(double r, double i) : val(r, i) {}
    void operator()(complex& c) const {
        c += val;
    }
};

使用 Add ​类的示例:

void h(vector<complex>& vec, list<complex>& lst, complex z) {
    for_each(vec.begin(), vec.end(), Add{2, 3});
    for_each(lst.begin(), lst.end(), Add{z});
}

在这个例子中,Add{2, 3} ​创建了一个对象,它将复数 {2, 3} ​加到 vector ​的每个元素上。Add{z} ​创建了另一个对象,它将变量 z ​加到 list ​的每个元素上。

Lambda 表达式本质上是定义函数对象的一种方式。

operator() ​必须是非 static ​成员函数。

解引用运算符(->​)

解引用运算符 ->​,也称为箭头运算符,可以被定义为一个一元后置运算符。重载 -> ​的主要目的是创建“智能指针”,即行为类似于指针的对象,并且在访问对象时执行某些操作。标准库中的“智能指针”如 unique_ptr ​和 shared_ptr ​提供了 -> ​运算符。

#include <iostream>

class MyClass {
public:
    void Display() {
        std::cout << "Hello from MyClass!" << std::endl;
    }
};

class SmartPointer {
private:
    MyClass* ptr;
public:
    SmartPointer(MyClass* p = nullptr) : ptr(p) {}

    ~SmartPointer() {
        delete ptr;
    }

    // 重载箭头运算符,使其可以像普通指针一样访问 MyClass 的成员
    MyClass* operator->() {
        return ptr;
    }

};

int main() {
    SmartPointer sp(new MyClass());
    sp->Display();  // 使用重载的箭头运算符访问 MyClass 的成员
	sp.operator->()->Display();  // 等价于上一行代码
    return 0;
}

返回值最好是指针,或者支持再次使用 -> ​的类对象,因为在调用 -> ​运算符后,还会再次使用 ->​。

运算符 -> ​必须是非 static ​成员函数。

递增和递减

在 C++ 中,递增运算符 ++​ 和递减运算符 --​ 可以用作前置运算符(如 ++i​ 和 --i​)以及后置运算符(如 i++​ 和 i--​)。

#include <iostream>
using namespace std;

class Counter {
private:
    int value;

public:
    // 构造函数
    Counter(int v = 0) : value(v) {}

    // 前置递增运算符 ++
    Counter& operator++() {
        ++value;
        return *this;
    }

    // 后置递增运算符 ++ (注意参数 int 用来区分后置)
    Counter operator++(int) {
        Counter temp = *this;  // 保存当前值
        value++;               // 增加当前对象的值
        return temp;           // 返回保存的旧值
    }

    // 前置递减运算符 --
    Counter& operator--() {
        --value;
        return *this;
    }

    // 后置递减运算符 -- (注意参数 int 用来区分后置)
    Counter operator--(int) {
        Counter temp = *this;  // 保存当前值
        value--;               // 减少当前对象的值
        return temp;           // 返回保存的旧值
    }

    // 打印当前计数值
    void print() const {
        cout << "Counter value: " << value << endl;
    }
};

这里的 int ​参数用于区分前置和后置版本,后置版本中的 int ​参数是一个哑参数,用于表示这是一个后置运算符。

在设计中,可以考虑去掉后置的 ++ ​和 -- ​运算符,因为它们在语法上有些奇怪,实现起来较难,效率较低,且应用范围较小。

分配和释放内存

new ​和 delete ​运算符分别用于分配和释放内存。用户可以重新定义全局的 operator new() ​和 operator delete()​,也可以为特定类定义这些运算符。

全局版本的 operator new() ​和 operator delete()

void* operator new(size_t);  // 用于单个对象
void* operator new[](size_t); // 用于数组
void operator delete(void*, size_t); // 用于单个对象
void operator delete[](void*, size_t); // 用于数组

new ​需要为类型 X ​的对象分配内存时,它调用 operator new(sizeof(X))​。类似地,当 new ​需要为包含 N ​个 X ​对象的数组分配内存时,它调用 operator new[](N*sizeof(X))​。new ​表达式实际分配的空间可能比 N*sizeof(X) ​多一些,超出的部分可以容纳若干字符。

特定类的 operator new() ​和 operator delete()

更好的做法是为特定的类提供这些操作,其中,这个特定的类可以作为很多派生类的基类。例如,为 Employee ​类及其派生类提供特定的分配和释放函数:

#include <iostream>
#include <cstdlib>

class Base {
public:
    // 重载 operator new
    void* operator new(std::size_t size) {
        std::cout << "Base::operator new called, size = " << size << std::endl;
        void* p = std::malloc(size);  // 分配内存
        if (!p) throw std::bad_alloc();  // 如果分配失败,抛出异常
        return p;
    }

    // 重载 operator delete
    void operator delete(void* p, std::size_t size) {
        std::cout << "Base::operator delete called, size = " << size << std::endl;
        std::free(p);  // 释放内存
    }

    virtual ~Base() = default;  // 虚析构函数以确保正确删除派生类对象
};

class Derived : public Base {
public:
    int data;
    Derived(int value) : data(value) {
        std::cout << "Derived constructor, data = " << data << std::endl;
    }
};

int main() {
    Base* b = new Derived(10);  // 使用 Base::operator new 进行分配
    delete b;  // 使用 Base::operator delete 释放
    return 0;
}

成员函数 operator new() ​和 operator delete() ​是隐式的 static ​成员,因此它们无法使用 this ​指针,也不能修改对象的值。它们提供了一块可供构造函数初始化并由析构函数释放的存储空间。

std::size_t size​ 参数等于实际类的大小,比如上面的输出是:

Base::operator new called, size = 16
Derived constructor, data = 10
Base::operator delete called, size = 16

用户自定义字面值常量

用户自定义字面值常量(user-defined literal​)是通过字面值常量运算符(literal operator​)定义的,这类运算符负责把带后缀的字面值常量映射到目标类型。字面值常量运算符的名字由 operator""​ 加上后缀组成,例如:operator"" _cm​。

#include <iostream>

// 定义一个字面值常量运算符
long double operator"" _cm(long double x) {
    return x * 10;
}

int main() {
    long double height = 3.4_cm;
    std::cout << "Height in mm: " << height << std::endl;
    return 0;
}

使用 constexpr​ 可以确保在编译时求值。

可添加后缀的字面值常量类型:

  1. 整型字面值常量:可以用于接受 unsigned long long ​或者 const char* ​参数的字面值常量运算符,也可用于模板字面值常量运算符。例如,123m ​和 12345678901234567890X​。
  2. 浮点型字面值常量:可以用于接受 long double ​或者 const char* ​参数的字面值常量运算符,也可用于模板字面值常量运算符。例如,12345678901234567890.976543210x ​和 3.99s​。
  3. 字符串字面值常量:可以用于接受 (const char*, size_t) ​参数对的字面值常量运算符。例如,"string"s ​和 R"(Foolbar)"_path​。
  4. 字符字面值常量:可以用于接受 char​、wchar_t​、char16_t​、char32_t ​等字符类型参数的字面值常量运算符。例如,'f'_runic ​和 u'BEEF"_W​。

以下是一个定义字面值常量运算符的例子,用于存放任意内置整数类型都无法表示的整型值:

Bignum operator""x(const char* p) {
    return Bignum(p);
}

void f(Bignum);
f(123456789012345678901234567890123456789012345x);

在这个例子中,operator""x ​是一个用户自定义的字面值常量运算符,它接受一个 const char* ​参数,并返回一个 Bignum ​对象。然后,这个运算符被用来创建一个非常大的整数字面值。注意,在代码中,并未在数字串两端使用双引号,编译器负责把数字转换成所需的类型。

不以下划线开始的后缀都是为标准库预留的,因此程序员最好把自己的后缀设计为以下划线开始,以避免未来因标准库的扩充而失效。

友元

友元函数和友元类是 C++ 中的特殊机制,它们允许非成员函数或类访问另一个类的私有成员。这种机制在需要外部函数或类访问类的内部数据时非常有用,尤其是在类的封装性和数据隐藏需要被严格控制的情况下。

友元函数

假设我们有两个类 Matrix ​和 Vector​,我们希望定义一个计算矩阵与向量乘积的运算符 operator*​。由于乘法运算不应属于 Matrix ​或 Vector ​中的任何一个,我们可以将 operator* ​声明为这两个类的友元函数。

constexpr int rc_max = 4; // 行列的尺寸

class Matrix;

class Vector {
    float v[rc_max];
public:
    friend Vector operator*(const Matrix&, const Vector&);
};

class Matrix {
    Vector v[rc_max];
public:
    friend Vector operator*(const Matrix&, const Vector&);
};

Vector operator*(const Matrix& m, const Vector& v) {
    Vector r;
    for (int i = 0; i != rc_max; i++) {
        r.v[i] = 0;
        for (int j = 0; j != rc_max; j++)
            r.v[i] += m.v[i].v[j] * v.v[j];
    }
    return r;
}

在这个例子中,operator* ​可以访问 Vector ​和 Matrix ​的私有成员,但用户不能直接访问这些类的内部表示。

友元类

友元类的所有成员函数都可以访问另一个类的私有成员。这种声明方式允许两个类之间有更紧密的联系,但同时也意味着对类的内部细节的访问权限更加开放。

假设我们有一个 List ​类和一个 List_iterator ​类,我们希望 List_iterator ​类的所有成员函数都能访问 List ​类的私有成员。

class List_iterator {
    // ...
};

class List {
    friend class List_iterator;
    // ...
};

在这个例子中,List_iterator ​类的所有成员函数都被声明为 List ​类的友元,因此它们可以访问 List ​类的私有成员。

此外,模板参数也可以设置为友元。

template<typename T>
class X {
	friend T;
	friend class T; // 多余的 "class"
};

友元发现

在 C++ 中,友元声明必须在类的外层作用域中提前声明,或者定义在直接外层非类作用域中。友元的声明对于嵌套名字空间作用域内的首次声明是敏感的,其友元性在更外层的作用域中可能会失效。

  • 友元类:必须在类的外层作用域中提前声明。
  • 友元函数:必须在类的外层作用域中提前声明,或者定义在直接外层非类作用域中。
// 全局作用域
class C1; // 将成为N::C的友元
void f1(); // 将成为N::C的友元

namespace N {
    class C2; // 将成为C的友元
    void f2() {} // 将成为C的友元

    class C {
        int x;
    public:
        friend class C1; // OK(已经预先定义)
        friend void f1(); // OK
        friend class C3; // OK(已经在外层作用域中定义)
        friend void f3(); // 需要在N作用域中声明
        friend class C4; // 需要在N作用域中声明
        friend void f4(); // 需要在N作用域中声明
    };
}

class C4 {}; // 不是N::C的友元
void f4() { N::C x; x.x = 1; } // 错误:x是私有的并且f4()不是N::C的友元

即使友元函数不是在直接外层作用域中声明,也可以通过其参数找到它。友元函数应该显式地声明在外层作用域中,或者接受一个数据类型为该类或者其派生类的参数;否则,我们无法调用该友元函数。

class X {
    friend void f(); // 没用,因为f没有在X的外层作用域中声明
    friend void h(const X&); // 可以通过参数找到
};

void g(const X& x) {
    f(); // 作用域内找不到f()
    h(x); // X的友元h()
}

在这个例子中,f ​没有在 X ​的外层作用域中声明,因此即使 f ​被声明为 X ​的友元,也无法在 g ​函数中调用 f​。而 h ​函数通过参数 const X& ​找到了 X ​的友元,因此可以在 g ​函数中调用 h​。

友元与成员函数

何时使用成员函数

  • 需要修改对象状态的操作:成员函数可以直接修改对象的状态,因为它们可以直接访问对象的成员变量。
  • 构造函数、析构函数和虚函数:这些函数通常必须作为成员函数出现。
  • 直接访问类的表示:如果函数需要直接访问类的内部数据,它们应该被定义为成员函数。

何时使用友元函数

  • 不需要直接访问类的表示:如果函数不需要修改对象的状态,或者不需要直接访问类的私有成员,它们可以被定义为友元函数。
  • 二元运算符:如果运算符的所有操作数都能进行隐式类型转换,那么这些运算符函数通常被定义为接受 const ​引用参数或非引用参数的非成员函数。(因为非 const ​引用参数只接受左值,不接受类型转换)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇