[C++]关于重载运算符的一些建议

本文探讨了C++中的运算符重载实践,包括如何谨慎定义类型转换函数以避免意外转换,解释了自增和自减运算符的不同用法,并讨论了避免重载逻辑运算符的原因。此外,还介绍了如何理解和使用new和delete的不同形式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

运算符

1. 谨慎定义类型转换函数

有两种函数允许编译器进行这些的转换:单参数构造函数(single-argument constructors)和隐式类型转换运算符。单参数构造函数是指只用一个参数即可以调用的构 造函数。该函数可以是只定义了一个参数,也可以是虽定义了多个参数但第一个参数以后的 所有参数都有缺省值。

第一个例子:

class Name {
public:
  Name(const string& s);
... }; 
class Rational {
public:
  Rational(int numerator = 0,
           int denominator = 1);
... 
// for names of things
// 转换 string 到 
// Name 
// 有理数类 
// 转换 int 到 
// 有理数类 
};

第二个例子:隐式类型转换运算符只是一个样子奇怪的成员函数:operator 关键字,其后跟一个类
型符号。

class Rational { 
public: ... 
  operator double() const;
};
// 在下面这种情况下,这个函数会被自动调用: Rational r(1, 2); 
double d = 0.5 * r;
// 转换 Rational 类成 
// double 类型 
// r 的值是 1/2 
// 转换 r 到double, 
// 然后做乘法 

隐式类型转换可能出现的问题:

#include <iostream>
using namespace std;
class rational {
public:
    rational(double a, double b) {
        val = a / b;
    }
    operator double() {
        return val;
    }
private:
    double val;
};
int main() {
    rational test(3, 4);
    cout << test << endl;
    return 0;
}

我们本以为没有定义operator <<,所以编译器会报错,但实际上编译器会把test隐式类型转换为double类型。这看起来很不错,实际上回出现很多不可预计的问题。它表明了隐式类型转换的缺点: ==它们的存在将导致错误的发生==。

解决方法是用不使用语法关键字的等同的函数来替代转换运算符。例如为了把 Rational 对象转换为 double,用 asDouble 函数代替 operator double 函数:


class Rational {
public: 
... 
  double asDouble() const;
};
// 这个成员函数能被显式调用: Rational r(1, 2); 
cout << r;
cout << r.asDouble();
//转变 Rational // 成 double 
// 错误! Rationa 对象没有 // operator<< 
// 正确, 用 double 类型 //打印 r 

在多数情况下,这种显式转换函数的使用虽然不方便,但是函数被悄悄调用的情况不再 会发生,这点损失是值得的。就好像在编写库时,string没有给出隐式类型转换为char*的操作符,而是给出了c_str()来转换就是这个道理。

以下讨论单参数构造函数进行隐式类型转换的问题。

template<class T>
class Array {
public: 
  Array(int lowBound, int highBound);
  Array(int size);
  T& operator[](int index);
  ...
};

第一个构造函数允许调用者确定数组索引的范围,例如从 10 到 20。它是一个两参数构造函数,所以不能做为类型转换函数。第二个构造函数让调用者仅仅定义数组元素的个数(使 用方法与内置数组的使用相似),不过不同的是它能做为类型转换函数使用,能导致无穷的痛苦。

例如比较 Array对象,部分代码如下:

bool operator==( const Array<int>& lhs, 
Array<int> a(10);
const Array<int>& rhs);
Array<int> b(10);
...
for (int i = 0; i < 10; ++i)
  if (a == b[i]) {
    do something for when
    a[i] and b[i] are equal;
} else { 
// 哎呦! "a" 应该是 "a[i]" 
    do something for when they're not;
  }

我们想用 a 的每个元素与 b 的每个元素相比较,但是当录入 a 时,我们偶然忘记了数组 下标。当然我们希望编译器能报出各种各样的警告信息,但是它根本没有。因为它把这个调 用看成用 Array参数(对于 a)和 int(对于 b[i])参数调用 operator==函数,然而没有 operator==函数是这样的参数类型,我们的编译器注意到它能通过调用 Array构造函 数能转换 int 类型到 Array类型,这个构造函数只有一个 int 类型的参数。然后编译器如此去编译,生成的代码就像这样:

for (int i = 0; i < 10; ++i)
  if (a == static_cast< Array<int> >(b[i]))   ...

每一次循环都把 a 的内容与一个大小为 b[i]的临时数组(内容是未定义的)比较。这 不仅不可能以正确的方法运行,而且还是效率低下的。因为每一次循环我们都必须建立和释 放 Array对象。

解决的方法是利用一个最新编译器的特性,explicit 关键字。为了解决隐式类型转换 而特别引入的这个特性,它的使用方法很好理解。构造函数用 explicit 声明,如果这样做, 编译器会拒绝为了隐式类型转换而调用构造函数。显式类型转换依然合法:

template<class T> 
class Array {
public:
...
explicit Array(int size); // 注意使用"explicit" 
... }; 
Array<int> a(10);
Array<int> b(10);
if (a == b[i]) ...
if (a == Array<int>(b[i])) ...
// 正确, explicit 构造函数 // 在建立对象时能正常使用 
// 也正确
// 错误! 没有办法 
// 隐式转换
// int 到 Array<int> 
// 正确,显式从 int 到 // Array<int>转换 
// (但是代码的逻辑 
// 不合理) 
if (a == static_cast< Array<int> >(b[i])) ... 
// 同样正确,同样 
// 不合理

if (a == (Array<int>)b[i]) ... //C 风格的转换也正确, 
// 但是逻辑 
// 依旧不合理

关于explicit:(不允许参数隐式类型转换!)

class Test1
{
public:
    Test1(int n)
    {
        num=n;
    }//普通构造函数
private:
    int num;
};
class Test2
{
public:
    explicit Test2(int n)
    {
        num=n;
    }//explicit(显式)构造函数
private:
    int num;
};
int main()
{
    Test1 t1=12;//隐式调用其构造函数,成功
    Test2 t2=12;//编译错误,不能隐式调用其构造函数
    Test2 t2(12);//显式调用成功
    return 0;
}

2. 自增和自减

重载函数间的区别决定于它们的参数类型上的差异,但是不 论是 increment 或 decrement 的前缀还是后缀都只有一个参数。为了解决这个语言问题,C++ 规定后缀形式有一个int类型参数,当函数被调用时,编译器传递一个0做为int参数的值 给该函数:


class UPInt {
public: 
  UPInt& operator++();
  const UPInt operator++(int);
  UPInt& operator--();
  const UPInt operator--(int);
  UPInt& operator+=(int);
... }; 
UPInt i;
++i;
i++;
--i;
i--;

值得注意的是,==前缀返回的是引用,后缀返回的是const对象==。(很容易通过前缀自增和后缀自增的区别来判读合理性。)

UPInt& UPInt::operator++() {
  *this += 1;
  return *this;
}
const UPInt UPInt::operator++(int) {
  UPInt oldValue = *this;
++(*this); // 增加 return oldValue;
} 

如果后缀的increment不是const对象,那么以下代码就是正确的:

UPInt i;
i++++; // 两次 increment 后缀 这组代码与下面的代码相同: 
i.operator++(0).operator++(0);

3. 不要重载&&, ||或 “,”

C++使用==布尔表达式短路求值法==(short-circuit evaluation)。这表示一旦 确定了布尔表达式的真假值,即使还有部分表达式没有被测试,布尔表达式也停止运算。

char *p;
...if ((p != 0) && (strlen(p) > 10)) ...
// 这里不用担心当 p 为空时 strlen 无法正确运行,因为如果 p 不等于 0 的测试失败,strlen 不会被调用。同样:

int rangeCheck(int index)
{
if ((index < lowerBound) || (index > upperBound)) ......
}

C++允许根据用户定义的类型,来定制&&和||操作符。方法是重载函数 operator&& 和 operator||,你能在全局重载或每个类里重载。但是你就失去了短路求值的特性。

if (expression1 && expression2) ... 
// 对于编译器来说,等同于下面代码之一: 
if (expression1.operator&&(expression2)) ...
                              // when operator&& is a
                              // member function
if (operator&&(expression1, expression2)) ...
                              // when operator&& is a
// global function 

这好像没有什么不同,但是函数调用法与短路求值法是绝对不同的。首先当函数被调用时,需要运算其所有参数,所以调用函数 functions operator&& 和 operator||时,两个 参数都需要计算,换言之,没有采用短路计算法。第二是 C++语言规范没有定义函数参数的 计算顺序,所以没有办法知道表达式1与表达式2哪一个先计算。完全可能与具有从左参数 到右参数计算顺序的短路计算法相反。

不能重载的部分:

. .* :: ?:
new delete sizeof typeid 
static_cast dynamic_cast const_cast reinterpret_cast 

能重载的部分:

operator new operator deleteoperator new[] operator delete[] +-*/%^&|~ 
! =<>+=-=*=/=%= ^=&=|=<<>> >>=<<=== != <=>=&&||++ -- , ->*-> () [] 

操作符重载的目的是使程序更容易阅读,书 写和理解,而不是用你的知识去迷惑其他人。如果你没有一个好理由重载操作符,就不要重 载。在遇到&&, ||, 和 ,时,找到一个好理由是困难的,因为无论你怎么努力,也不能让它 们的行为特性与所期望的一样。

4. 理解各种不同含义的new和delete

string *ps = new string(“Memory Management”);

你使用的 new 是 new 操作符。这个操作符就象 sizeof 一样是语言内置的,你不能改变它的 含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便 容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new 操作符总是做 这两件事情,你不能以任何方式改变它的行为。

new operator

你所能改变的是如何为对象分配内存。new 操作符调用一个函数来完成必需的内存分 配,你能够重写或重载这个函数来改变它的行为。new 操作符为分配内存所调用函数的名字 是 operator new。
函数 operator new 通常这样声明:

void * operator new(size_t size); 

void *rawMemory = operator new(sizeof(string));
操作符 operator new 将返回一个指针,指向一块足够容纳一个 string 类型对象的内存。
就象 malloc 一样,operator new 的职责只是分配内存。它对构造函数一无所知。 operator new 所了解的是内存分配。把 operator new 返回的未经处理的指针传递给一个对 象是 new 操作符的工作。


placement new

但是有时你有一些已经被分配但是尚未处理的(raw)内存,你需要在这些内存中构造一个对象。你可以 使用一个特殊的 operator new ,它被称为 placement new。

void * operator new(size_t, void *location)
{ 
  return location;
}

operator new 的目的是为对象分配内存然后返回指向该内存的指针。在使用 placement new 的情况下,调 用者已经获得了指向内存的指针,因为调用者知道对象应该放在哪里。placement new 必须 做的就是返回转递给它的指针。(没有用的(但是强制的)参数 size_t 没有名字,以防止编 译器发出警告说它没有被使用。)

delete memory deallocation

Operator delete 用来释放内存,它被这样声明:

void operator delete(void *memoryToBeDeallocated); 

因此,

delete ps; 

导致编译器生成类似于这样的代码:


ps->~string(); // call the object's dtor operator 
delete(ps); // deallocate the memory 
// the object occupied 

这有一个隐含的意思是如果你只想处理未被初始化的内存,你应该绕过 new 和 delete
操作符,而调用 operator new 获得内存和 operator delete 释放内存给系统:

void *buffer =
  operator new(50*sizeof(char));
  // 分配足够的// 内存以容纳 50 个 char 
...
operator delete(buffer);
//没有调用构造函数 
// 释放内存 // 没有调用析构函数 

如果你用 placement new 在内存中建立对象,你应该避免在该内存中用 delete 操作符。
因为 delete 操作符调用 operator delete 来释放内存,但是包含对象的内存最初不是被 operator new 分配的,placement new 只是返回转递给它的指针。谁知道这个指针来自何方? 而你应该显式调用对象的析构函数来解除构造函数的影响:


// 在共享内存中分配和释放内存的函数 
void * mallocShared(size_t size);
void freeShared(void *memory);
void *sharedMemory = mallocShared(sizeof(Widget));
Widget *pw = // 如上所示, 
constructWidgetInBuffer(sharedMemory, 10); // 使用 
...
delete pw;
pw->~Widget();
freeShared(pw);
// 结果不确定! 共享内存来自// mallocShared, 而不是 operator new 
// 正确。 析构 pw 指向的 Widget,// 但是没有释放//包含 Widget 的内存// 正确。 释放 pw 指向的共享内存// 但是没有调用析构函数 

new 和 delete 操作符是内置的,其行为不受你的控制,凡是它们调用的内存分配和释放函数则可以控制。当你想定制 new 和 delete 操作符的行为时,请记住你不能真的做到这 一点。你只能改变它们为完成它们的功能所采取的方法,而它们所完成的功能则被语言固定 下来,不能改变。(You can modify how they do what they do, but what they do is fixed by the language)

<think>好的,我需要回答用户关于C++运算符重载的使用与示例的问题。首先,我需要回忆一下运算符重载的基本概念和相关规则。根据用户提供的引用内容,运算符重载允许用户自定义数据类型使用C++内置的运算符,比如关系运算符、算术运算符等。引文[^2]提到,重载运算符可以使操作更自然直观,这是关键点之一。 接下来,我应该确定回答的结构。用户希望了解如何实现和使用重载运算符,所以需要分步骤说明,可能包括基本语法、注意事项以及示例代码。根据引用[^3],C++编译器会为类生成默认的拷贝构造函数和赋值运算符,但用户可能需要自定义这些运算符。 我需要先解释运算符重载的基本语法,包括成员函数和友元函数两种方式。比如,重载+运算符作为成员函数时,参数数量比非成员少一个,因为隐含了this指针。同时,要注意能创建新的运算符,只能重载已有的,并且能改变运算符的优先级和结合性,这些是重要的注意事项,引文中有提到。 然后,给出几个常见的运算符重载示例。比如算术运算符+,关系运算符==,输入输出运算符>>和<<,以及赋值运算符=。每个示例需要展示代码,并解释其作用。比如,在Vector类中重载+运算符,实现向量相加;重载==来比较两个对象是否相等。引用[^1]提到关系运算符重载,可以用于比较类的对象,这需要体现在示例中。 另外,输入输出运算符通常需要重载为友元函数,因为它们的左操作数是流对象,而是类的实例。引文[^2]中的示例部分应该包含这样的例子,所以需要确保代码正确,比如使用友元函数来重载<<和>>。 在注意事项部分,需要提到哪些运算符能被重载,比如.和::,以及能改变运算符的优先级。同时,要强调保持运算符的语义一致性,避免让+运算符执行减法的操作,这会让代码难以理解。 最后,生成相关问题,可能涉及运算符重载的应用场景、与内置类型的区别、深拷贝与浅拷贝的问题,以及如何实现特定运算符如自增或下标运算符。这些问题可以帮助用户进一步学习相关内容。 现在,我需要将这些内容组织成结构清晰的回答,使用用户要求的格式,确保所有数学表达式用$...$,独立公式用$$...$$,并正确引用提供的参考文献。同时,确保代码示例正确,符合C++语法,并解释每个示例的作用和结果。</think>### C++运算符重载使用与示例 #### 一、基本概念 运算符重载允许用户为自定义数据类型定义运算符的行为,使其像内置类型一样直观操作。例如,对`Vector`类重载`+`运算符实现向量加法: $$ \vec{v_1} + \vec{v_2} = (x_1+x_2, y_1+y_2) $$ #### 二、实现步骤 1. **语法形式** - **成员函数重载** ```cpp ReturnType operator运算符(参数) { ... } ``` - **全局函数重载(需声明为友元)** ```cpp friend ReturnType operator运算符(参数1, 参数2) { ... } ``` 2. **核心规则** - 只能重载C++已有的运算符(如`+`, `==`, `<<`),能创建新符号 - 至少一个操作数为用户定义类型 - 改变运算符的优先级和结合性 --- #### 三、常用示例 1. **算术运算符重载(`+`)** ```cpp class Vector { public: Vector operator+(const Vector& v) { return Vector(x + v.x, y + v.y); } private: double x, y; }; ``` - 使用:`Vector v3 = v1 + v2;` 2. **关系运算符重载(`==`)** ```cpp bool operator==(const Student& s1, const Student& s2) { return s1.id == s2.id; } ``` - 使用:`if (stu1 == stu2) ...` 3. **输入/输出运算符重载(`<<`, `>>`)** ```cpp friend ostream& operator<<(ostream& os, const Point& p) { os << "(" << p.x << "," << p.y << ")"; return os; } ``` - 使用:`cout << point;` 输出格式:`(3,5)` 4. **赋值运算符重载(`=`)** ```cpp Array& operator=(const Array& other) { if (this != &other) { // 防止自赋值 delete[] data; // 释放原有资源 data = new int[other.size]; size = other.size; std::copy(other.data, other.data + size, data); } return *this; } ``` - 必须返回引用以实现链式赋值:`a = b = c;` --- #### 四、注意事项 1. **重载运算符** `.`(成员访问符)、`::`(作用域解析符)、`?:`(条件运算符)等 2. **保持语义一致性** 重载运算符应保持原有含义(例如`+`应实现减法逻辑) 3. **特殊运算符要求** - 赋值运算符`=`、下标运算符`[]`、函数调用运算符`()`必须定义为成员函数 - 流运算符`<<`/`>>`必须定义为友元全局函数 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值