访问其操作数的一个成员。
运算符名 | 语法 | 可重载 | 原型示例(对于 class T ) | |
---|---|---|---|---|
类定义内 | 类定义外 | |||
数组下标 | a[b] | 是 | R& T::operator; | N/A |
间接寻址(由 a 所指向的变量) | a | 是 | R& T::operator(); | R& operator(T &a); |
取地址 | &a | 是 | R T::operator&(); | R operator&(T &a); |
对象的成员 | a.b | 否 | N/A | N/A |
指针的成员 | a->b | 是 | R T::operator->() | N/A |
对象的成员指针 | a.b | 否 | N/A | N/A |
指针的成员指针 | a->b | 是 | R T::operator->(R) | R T::operator->(T, R) |
- 注解 - 与大多数用户定义的重载相同,返回类型应当与内建运算符所提供的返回类型相匹配,以便用户定义的运算符可以和内建运算符以相同方式使用。不过,在用户定义的运算符重载中,任何类型都可以作为其返回类型(包括 void)。一个例外是 operator-> ,它必须返回一个指针或者另一个带有重载的 operator-> 的类以使其真正可用。 |
解释
内建的下标 (subscript)运算符提供对其指针或数组操作数所指向的对象的访问。
内建的间接寻址 (indirection)运算符提供对其指针操作数所指向的对象或函数的访问。
内建的取地址 (address of)运算符创建指向其对象或函数操作数的指针。
对象的成员和对象的成员指针运算符提供对其对象操作数的数据成员或成员函数的访问。
内建的指针的成员和指针的成员指针运算符提供对其指针操作数所指向的类的数据成员或成员函数的访问。
内建的下标运算符
下标运算符表达式的形式为
expr1 [ expr2 ] | (1) | |
expr1 [ { expr, … } ] | (2) | (C++11) |
1) 对于内建运算符,表达式之一(expr1 或 expr2)必须是“T 的数组”类型的泛左值或“T 的指针”类型的纯右值,而另一表达式(分别为 expr2 或 expr1)必须是无作用域枚举或整型类型的纯右值。此表达式的结果类型为 T
。
2) 方括号中有花括号环绕列表的形式仅用于调用重载的 operator[]
内建下标表达式 E1[E2],除了求值顺序之外 (C++17 起)与表达式 *(E1 + E2) 严格等同,就是说,它遵循指针算术的规则,将指针操作数(可以是数组到指针转换的结果,但它必须指向某数组的元素或末尾后一位置)调整为指向同数组的另一元素,然后再进行解引用。
应用到数组时,若数组为左值,则 (C++11 起)下标表达式为左值,否则为亡值 (C++11 起)。
应用到指针时,下标表达式始终是左值。
不允许类型 T
为不完整类型,即使始终不会使用 T
的大小或其内部结构也是如此,如 &x[0]。
以逗号表达式为下标表达式是被弃用的。 例如, a[b, c] 被弃用而 a[(b, c)] 未被弃用。 | (C++20 起) |
在对于用户定义运算符的重载决议中,对于每个对象类型 T
(可有 cv 限定),下列函数签名参与重载决议:
T& operator); | ||
T& operator, T*); | ||
运行此代码
- #include <iostream>
- int main()
- {
- int a[4] = {1, 2, 3, 4};
- int* p = &a[2];
- std::cout << p[1] << p[-1] << 1[p] << (-1)[p] << '\n';
- }
输出:
- 4242
内建的间接寻址运算符
间接寻址运算符表达式的形式为
* expr | ||
内建间接寻址运算符的操作数必须是对象指针或函数指针,其结果就是 expr 所指向的指针或函数。
指向(可有 cv 限定)的 void 的指针不能解引用。指向其他不完整类型的指针可以解引用,但产生的左值只能在允许不完整类型的语境中使用,例如初始化一个引用。
在对于用户定义运算符的重载决议中,对于每个要么为(可有 cv 限定的)对象类型要么为(未被 const 或引用限定的)函数类型的类型 T
,下列函数签名参与重载决议:
T& operator(T); | ||
运行此代码
- #include <iostream>
- int f() { return 42; }
- int main()
- {
- int n = 1;
- int* pn = &n;
- int& r = *pn; // 左值可以绑定到引用
- int m = *pn; // 间接寻址 + 左值到右值转换
- int (*fp)() = &f;
- int (&fr)() = *fp; // 函数左值可以绑定到引用
- }
内建的取地址运算符
取地址运算符表达式的形式为
& 表达式 | (1) | |
& class :: 成员 | (2) | |
1) 当其操作数是某个对象或函数类型 T
的左值表达式时,operator&
创建并返回一个有相同 cv 限定的 T*
类型的纯右值,并指向由该操作数所代表的对象或函数。当其操作数具有不完整类型时,可以构成指针,但若该不完整类型恰好为某个定义了其自身的 operator&
的类,则其行为未定义 (C++14 前)则使用内建还是重载运算符是未指明的 (C++14 起)。对于其类型带有用户定义的 operator&
的操作数,可以使用 std::addressof 来获取真正的指针。
当其操作数为重载函数的名字时,仅当根据其语境可以解析这个重载时才能取得其地址。详细说明参见重载函数的地址。
2) 当其操作数为非静态成员的限定名(比如 &C::member)时,其结果为 C
类中的 T
类型的成员函数指针或数据成员指针的纯右值。注意,&member、C::member,甚至 &(C::member) 都不能用于初始化成员指针。
在对于用户定义运算符的重载决议中,此运算符不引入任何额外函数签名:若存在作为可行函数的重载 operator&
,则内建的取址运算符不适用。
运行此代码
- void f(int) {}
- void f(double) {}
- struct A { int i; };
- struct B { void f(); };
- int main()
- {
- int n = 1;
- int* pn = &n; // 指针
- int* pn2 = &*pn; // pn2 == pn
- int A::* mp = &A::i; // 数据成员指针
- void (B::*mpf)() = &B::f; // 成员函数指针
- void (*pf)(int) = &f; // 根据初始化语境进行重载决议
- // auto pf2 = &f; // 错误:重载函数类型有歧义
- auto pf2 = static_cast<void (*)(int)>(&f); // 由于转型进行重载决议
- }
内建的成员访问运算符
成员访问运算符表达式的形式为
表达式 . template (可选) 标识表达式 | (1) | |
表达式 -> template (可选) 标识表达式 | (2) | |
表达式 . 伪析构函数 | (3) | |
表达式 -> 伪析构函数 | (4) | |
1) 第一操作数必须是完整类类型 T
的表达式。
2) 第一操作数必须是指向完整类类型的指针 T*
的表达式。
3,4) 第一操作数必须是标量类型表达式(见下文)。
两种运算符的第一个操作数都被求值,即便它并不是必须的(比如其第二个操作数指名的是静态成员)。
两个运算符的第二个操作数是 T
或 T
的某个无歧义且可访问的基类 B
的数据成员或成员函数的名字(正式的说法是标识表达式(id-expression))(如 E1.E2 或 E1->E2),并可选地有限定(如 E1.B::E2 或 E1->B::E2),可选地使用 template 歧义消解符(如 E1.template E2 或 E1->template E2)。
如果提供的是用户定义的 operator->
,则递归地对其所返回的值再次调用 operator->
,直到到达返回普通指针的 operator->
为止。之后再对这个指针采用内建语义。
对于内建类型,表达式 E1->E2 严格等价于 (*E1).E2;因此以下规则仅处理了 E1.E2 的情形。
在表达式 E1.E2 中:
1) 当 E2
为静态数据成员时:
- 如果
E2
具有引用类型T&
或T&&
,则其结果为T
类型的左值,代表E2
所代表的对象或函数, - 否则,其结果为代表该静态数据成员的左值。
- 基本上,这两种情况下
E1
均被求值随即被丢弃;
2) 当 E2
为非静态数据成员时:
- 如果
E2
具有引用类型T&
或T&&
,则其结果为T
类型的左值,代表E2
所代表的对象或函数, - 否则,如果
E1
为左值,则其结果为代表E1
的这个非静态数据成员的左值, - 否则(
E1
为右值 (C++17 前)亡值(可能是从纯右值实质化而来) (C++17 起)),其结果为代表E1
的这个非静态数据成员的右值 (C++11 前)亡值 (C++11 起)。
- 若
E2
不是 mutable 成员,则结果的 cv 限定性是E1
和E2
的 cv 限定性的合并,否则(E2
是 mutable 成员)为E1
和E2
的 volatile 限定性的合并;
3) 当 E2
为静态成员函数时,其结果为代表该静态成员函数的左值。基本上,这种情况下 E1
被求值随即被丢弃;
4) 当 E2
为(包括析构函数在内的)非静态成员函数时,其结果为代表 E1
的这个非静态成员函数的一种特殊的纯右值,它只能用作成员函数调用运算符的左操作数,而不能用于其他目的;
5) 当 E2
为成员枚举项时,其结果为等于 E1
的这个成员枚举项的纯右值;
6) 当 E2
为嵌套类型时,程序非良构(无法编译);
7) 当 E1
为标量类型而 E2
为一个 ~
之后跟着代表(移除 cv 限定后)相同类型的类型名或 decltype 说明符,可选地有限定时,其结果为一种特殊的纯右值,它只能用作函数调用运算符的左操作数,而不能用于其他目的。所构成的函数调用表达式被称为伪析构函数调用(pseudo destructor call)。它不接受任何实参,返回 void ,求值 E1
后不进行任何操作 (C++20 前)结束其结果对象的生存期 (C++20 起)。这是唯一使 operator.
的左操作数具有非类类型的情形。允许进行伪析构函数调用,使得编写代码时无须了解某个给定类型是否存在析构函数成为可能。
operator.
不能重载,而对于 operator->
来说,在对于用户定义运算符的重载决议中,内建运算符不引入任何额外函数签名:若存在作为可行函数的重载 operator&
,则不采用内建的 operator->
。
运行此代码
- #include <iostream>
- struct P
- {
- template<typename T>
- static T* ptr() { return new T; }
- };
- template<typename T>
- struct A
- {
- A(int n): n(n) {}
- int n;
- static int sn;
- int f() { return 10 + n; }
- static int sf() { return 4; }
- class B {};
- enum E {RED = 1, BLUE = 2};
- void g()
- {
- typedef int U;
- // 待决的模板成员需要关键词 template
- int* p = P().template ptr<U>();
- p->~U(); // U 为 int,调用 int 的伪析构函数
- delete p;
- }
- };
- template<> int A<P>::sn = 2;
- int main()
- {
- A<P> a(1);
- std::cout << a.n << ' '
- << a.sn << ' ' // A::sn 也可以工作
- << a.f() << ' '
- << a.sf() << ' ' // A::sf() 也可以工作
- // << a.B << ' ' // 错误:不允许嵌套类型
- << a.RED << ' '; // 枚举项
- }
输出:
- 1 2 11 4 1
内建的成员指针访问运算符
通过成员指针进行的成员访问运算符表达式的形式为
lhs . rhs | (1) | |
lhs -> rhs | (2) | |
1) lhs 必须是类类型 T
的表达式。
2) lhs 必须是指向类类型指针 T*
的表达式。
两个运算符的第二操作数都是类型为指向 T
的(数据或函数)成员指针类型,或为指向 T
的无歧义且可访问基类 B
成员指针类型的表达式。
对于内建类型,表达式 E1->E2 严格等价于 (E1).E2;因此以下规则仅处理了 E1.E2 的情形。
在表达式 E1.*E2 中:
1) 当 E2
为指向数据成员的指针时,
- 如果
E1
为左值,则其结果为代表这个成员的左值, - 否则(
E1
为右值 (C++17 前)亡值(可能是从纯右值实质化而来) (C++17 起)),其结果为代表这个数据成员的右值 (C++11 前)亡值 (C++11 起);
2) 当 E2
为指向成员函数的指针时,其结果为代表这个成员函数的一种特殊的纯右值,它只能用作成员函数调用运算符的左运算数,而不能用于其他目的;
3) cv 限定性的规则与对象的成员运算符相同,但有一条额外规则:指代 mutable 成员的成员指针不能用于改动 const 对象中的这个成员;
4) 当 E2
为空成员指针值时,其行为未定义;
5) 当 E1
的动态类型并不包含 E2
所指代的成员时,其行为未定义;
6) 当 E1
为右值而 E2
指向带有引用限定符 & 的成员函数时,程序非良构,除非该成员函数亦为 const
限定但非 volatile
限定 (C++20 起);
7) 当 E1
为左值而 E2
指向带有引用限定符 && 的成员函数时,程序非良构。
在对于用户定义运算符的重载决议中,对于每个类型 D
, B
, R
的组合,其中类类型 B
是与 D
相同的类或 D
的无歧义且可访问基类,而 R
是对象或函数类型,下列函数签名参与重载决议:
R& operator->(D, R B::*); | ||
其中两个操作数都可以是 cv 限定的,这种情况下返回类型的 cv 限定性是个操作数的 cv 限定性的合并。
运行此代码
- #include <iostream>
- struct S
- {
- S(int n): mi(n) {}
- mutable int mi;
- int f(int n) { return mi + n; }
- };
- struct D: public S
- {
- D(int n): S(n) {}
- };
- int main()
- {
- int S::* pmi = &S::mi;
- int (S::* pf)(int) = &S::f;
- const S s(7);
- // s.*pmi = 10; // 错误:无法通过 mutable 进行修改
- std::cout << s.*pmi << '\n';
- D d(7); // 基类的指针可以在派生类对象上工作
- D* pd = &d;
- std::cout << (d.*pf)(7) << ' '
- << (pd->*pf)(8) << '\n';
- }
输出:
- 7
- 14 15
标准库
许多标准容器类都重载了下标运算符
许多迭代器和智能指针类都重载了间接寻址和成员运算符
标准库中的类都没有重载 operator&
。最为人所知的重载 operator&
的例子是微软的 COM 类 CComPtr.aspx),但在如 boost.spirit 这样的 EDSL 中也会出现重载它的例子。
标准库中的类都没有重载 operator->*
。曾有建议将其作为智能指针接口的一部分,并在 boost.phoenix 中的 actor 上有实际应用,但它在如 cpp.react 这样的 EDSL 中更为常见。
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
DR | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 1213 | C++11 | 数组右值的下标操作导致左值 | 重分类为亡值 |
常见运算符 | ||||||
---|---|---|---|---|---|---|
赋值 | 自增自减 | 算术 | 逻辑 | 比较 | 成员访问 | 其他 |
a = ba += ba -= ba = ba /= ba %= ba &= ba |= ba ^= ba <<= ba >>= b | ++a—aa++a— | +a-aa + ba - ba ba / ba % b~aa & ba | ba ^ ba << ba >> b | !aa && ba || b | a == ba != ba < ba > ba <= ba >= ba <=> b | a[b]a&aa->ba.ba->ba.*b | a(…)a, b? : |
特殊运算符 | ||||||
static_cast 转换一个类型为另一相关类型dynamic_cast 在继承层级中转换const_cast 添加或移除 cv 限定符reinterpret_cast 转换类型到无关类型C 风格转型 以 static_cast 、 const_cast 及 reinterpret_cast 的混合转换一个类型到另一类型new 创建有动态存储期的对象delete 销毁先前由 new 表达式创建的对象,并释放其所拥有的内存区域sizeof 查询类型的大小sizeof… 查询形参包的大小(C++11 起)typeid 查询类型的类型信息noexcept 查询表达式是否能抛出异常(C++11 起)alignof 查询类型的对齐要求(C++11 起) |