Skip to content

运算符重载

本章大纲

  • 堆内存分配
  • 运算符重载简介
  • 运算符重载的限制
  • 类成员函数和全局函数的运算符函数比较
  • 重载++--运算符
  • 重载<<>>运算符
  • 类型转换

堆内存分配

new

语法:指针变量 = new <数据类型>[长度];

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* 申请一个整型的空间 */
int *ip;
ip = new int;

/* 申请一个整型空间并同时初始化 */
ip = new int(5);

/* 申请长度为10 的一维数组 */
int *ap;
ap = new int[10]; /* 不能对动态数组初始化 */
  1. 如果分配的空间长度为1个单位,那么:
C++
1
2
float *pNum=new float; //与
float *pNum=new float[1];//等价
  1. 使用new分配内存时,其空间长度可以是**变量**或**数值表达式**
C++
1
2
int nSize=5;
int *nPInt=new int[nSize+5];//分配一个可以容纳10个int型数据的空间
  1. new分配的内存空间是连续的
C++
1
2
3
int*nPInt=new int[10];
nPInt[5]=100;//或
*(nPInt+5)=100;
  1. 如果当前存储器无足够的内存空间可分配,则new运算符返回0(NULL)

delete

语法:delete mem_ptr_allocated_by_new;

C++
1
2
delete  ip;
delete  [ ]ap;//表示归还一组空间
  • new运算符获得的内存空间,只许使用一次delete
  • 不允许对同一块空间进行**多次释放**,否则将会产生严重错误
  • delete只能用来释放由new运算符分配的动态内存空间,对于程序中的变量、数组的存储空间,不得使用delete运算符去释放

运算符重载

  • 运算符重载的实质:函数重载,只不过它重载的是类似+ - * / =这样的操作符
  • C++不但提供了**函数重载**,而且提供了**运算符重载**
  • 注意: 运算符被重载后,其原有的功能仍然保留

运算符重载语法:

C++
1
2
3
数据类型 operator 运算符名称 (形参表列)  { 
    //对运算符的重载处理 
}

实例:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Complex {  // 虚数类,a+bi
 public:
  Complex() {
    real = 0;
    imag = 0;
  }
  Complex(double r, double i) {
    real = r;
    imag = i;
  }
  Complex operator+(Complex &c2);  // 声明重载运算符的函数
  void display();

 private:
  double real;  // 实部
  double imag;  // 虚部
};

Complex Complexoperator + (Complex & c2)  // 定义重载运算符的函数
{
  Complex c;
  c.real = real + c2.real;
  c.imag = imag + c2.imag;
  return c;
}

注意事项

1. 能否重载

1、可以重载的运算符

C++
1
2
3
4
5
+  -  *  /  %  ^  &  |  ~
!  =  <  >  +=  -=  *=  /=  %
^=  &=  |=  <<  >>  >>=  <<=  ==  !=
<=  >=  &&  ||  ++  --  ->*  `  ->
[]  ()  new  delete  new[]  delete[]

2、不可以重载的运算符

C++
1
2
3
4
5
6
.           类属关系运算符
.*          成员指针运算符
::          作用域运算符
?:          条件运算符
#           编译预处理符号
sizeof()    取数据类型的长度

2. 重载原则

  • **优先级**不变;

  • **结合性**不变;(运算符应用的顺序)

  • 操作数个数不变;(元数)

  • **语法结构**不变。

3. 实质:函数重载

编译器选择重载的运算符是**遵循函数重载的选择原则**,即按不同类型或个数的参数来选择不同的重载运算符

4、运算符重载应符合使用习惯,便于理解

5、运算符重载**不能创造新的运算符号**

6、用于类对象的运算符一般必须重载,但有两个例外,赋值运算符=和取地址运算符&不必用户重载

①赋值运算符(=),可以用于每一个类对象,可以利用它在同类对象之间相互赋值。

② 地址运算符&,也不必重载,它能返回类对象在内存中的起始地址。

类成员函数和全局函数的运算符函数对比

  • 在C++中,运算符重载是通过**运算符重载函数**实现的,运算符重载函数一般采用下述两种形式之一。

  • 成员函数的形式

  • 友元函数的形式

  • **运算符重载函数**是一种特殊的成员函数或友员函数(全局函数通常被指定为友元函数)

  • 区别

  • 成员函数具有 this 指针,友员函数没有this指针

  • 不管是成员函数还是友员函数重载,运算符的使用方法相同

  • 传递参数的方式不同,实现代码不同,应用场合也不同

运算符重载函数是类成员函数

重载语法:

C++
1
2
3
4
5
6
返回值类型   类名::operator 运算符 (参数表) {
     // 相对于该类定义的操作
}
operator是关键字
operator 运算符是函数名
一个运算符被重载后原有意义没有失去只是定义了相对某一特定类的一个新运算符

参数表:

  1. 单目运算参数表中无参数,调用该函数的对象为操作数。
  2. 双目运算参数表中有一个参数,调用该函数的对象为第一操作数,参数表中的参数为第二操作数。
  3. 当一元运算符的操作数,或者二元运算符的**左操作数**是类的对象时,定义重载算符函数为成员函数 ,例如+、-、*、/、% 、=、+=、-=(二元操作)
  4. C++不能用友元重载的运算符: ()[]->或者任何赋值运算符

显式调用和隐式调用

  • 当运算符的重载为**成员函数形式**时可采用**隐式**和**显式**两种方式调用
C++
1
2
3
class Complex {};
C3 = C1 + C2;  // 编译器解释为: C1.operator+(C2);
C3 = C1 - C2;  // 编译器解释为: C1.operator-(C2);
  • 一元运算符(@表示已重载运算符)
  • 隐式调用:@对象名 或者 对象名@
  • 显式调用:对象.operator@( )
  • 二元运算符
  • 隐式调用:对象名1@对象名2
  • 显式调用:对象名1.operator@(对象名2)

重载++和--

重载函数只能从形式参数上加以区别

  • 运算符前置自增运算符:用成员函数实现时,没有形式参数。

  • 运算符后置自增运算符:另外增加一个形式上的形式参数,类型定为int。这个参数只是用来区别两种自增算符,并不参加实际的运算

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Weight {
 public:
  Weight(int v = 0) : value(v) {}

  // 运算符前置自增
  Weight& operator++();

  // 运算符后置自增
  Weight operator++(int);

  void print() { cout << value << endl; }

 private:
  int value;
};

// 前增量
Weight& Weight::operator++() {
  value++;       // 先加一
  return *this;  // 再返回
}
// 后增量
Weight Weight::operator++(int) {
  Weight temp(*this);  // 操作数保存为临时对象
  value++;             // 操作数加1
  return temp;         // 返回没有加1的临时对象
}

运算符重载函数是友元函数

重载语法:

C++
1
2
3
返回值类型 operator 运算符 (参数表) {
    //相对于该类而定义的操作(运算符重载函数体)
}

参数表:

  • 单目运算符重载,参数表中只有一个形参数;
  • 双目运算符重载,参数表中有两个形参数。
  • 运算符重载为成员函数和友元函数形式的主要区别在于前者有this 指针,后者无this 指针。

显式调用和隐式调用

  • 二元运算符
  • 显式:operator@(对象名1,对象名2)
  • 隐式:对象名1@对象名2

  • 一元运算符

  • 显式:operator@(对象名)
  • 隐式:@对象名 或者 对象名@

  • 重载为友元函数,比如“+”,操作数都由形参给出,通过运算符重载的函数进行传递。并且运算结果的类型与操作数的类型一致。

  • 重载运算符的操作中,无论重载为成员函数还是友元函数,其**形参多为引用类型**,目的是增加可读性,提高程序的运行效率,因为使用引用类型,在进行参数传递的过程中,不需要复制临时对象。
  • 如果左操作数必须是一个不同类的对象,那么该运算符函数必须作为全局函数来实现(友元函数)。例如运算符<<>>

重载<<和>>

  • istream 和 ostream 是 C++ 的预定义流类,cin 是 istream 的对象,cout 是 ostream 的对象
  • 运算符 << 由ostream 重载为插入操作,用于输出基本类型数据
  • 运算符 >> 由 istream 重载为提取操作,用于输入基本类型数据
  • 用友员函数重载 << 和 >> ,输出和输入用户自定义的数据类型
C++
1
2
3
//重载函数的声明形式如下:
istream & operator >> (istream &,自定义类 &);
ostream & operator << (ostream &,自定义类 &);

数据类型转换

在程序编译时或在程序运行实现:

  • 基本类型<---->基本类型
  • 基本类型<---->类类型
  • 类类型<---->类类型

C++数据类型转换方式:

  • 隐式数据类转换
  • 显式数据类型转换,也叫强制类型转换

对于自定义类型和类类型,类型转换操作是没有定义的。

**转换构造函数**的作用是将一个其他类型的数据转换成一个类的对象。转换构造函数只有一个形参

用户可以根据需要定义转换构造函数,在函数体中告诉编译系统怎样去进行转换。

使用转换构造函数将一个指定的数据转换为类对象的方法如下:

  • ① 先声明一个类
  • ② 在这个类中定义一个**只有一个参数的构造函数**,参数的类型是需要转换的类型,在函数体中指定转换的方法
  • ③ 在该类的作用域内可以用以下形式将指定类型的数据转换为此类的对象:
  • 类名(指定类型的数据);
C++
1
2
3
4
Complex(double r) {//将一个浮点数转换为虚数
    real=r;
    imag=0;
}

不仅可以将一个标准类型数据转换成类对象,也可以将另一个类的对象转换成转换构造函数所在的类对象。

类型转换函数(类独有)

  • 用转换构造函数可以将一个指定类型的数据转换为类的对象;不能反过来将一个类的对象转换为一个其他类型的数据(例如将一个Complex类对象转换成double类型数据)。
  • C++提供类型转换函数(type conversion function)来解决这个问题。类型转换函数的作用是将一个类的对象转换成另一类型的数据
  • 声明语法:operator 类型名 () ;
  • 特点:
  • 无返回值
  • 功能类似强制转换
  • 只能是成员函数,不能是友元函数或普通函数,因为转换的主体是本类的对象
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class RMB {
 public:
  RMB(double value = 0.0) { setRMB(value); }
  void setRMB(double value) {
    yuan = value;
    fen = (value - yuan) * 100;
  }
  void ShowRMB() { cout << yuan << "元" << fen << "分" << endl; }
  operator double() {  // 类型转换函数
    return yuan + fen / 100.0;
  }

 private:
  int yuan, fen;
};

void main() {
  RMB r1(1.01), r2(2.20);
  RMB r3;
  // 显式转换类型
  r3.setRMB((double)r1 + (double)r2);
  r3.ShowRMB();

  // 自动转换类型
  r3 = r1 + 2.40;
  r3.ShowRMB();

  // 自动转换类型
  r3 = 2.0 - r1;
  r3.ShowRMB();
}

对于r3=r1+2.40;的系统工作

1、寻找重载的成员函数+运算符

2、寻找重载的友元函数+运算符

3、寻找转换运算符

4、验证转换后的类型是否支持+运算。

转换运算符重载一般建议尽量少使用。

explicit构造函数

  • 放在单参数的构造函数中,防止隐式转换, 导致函数的入口参数, 出现歧义.
  • 如果可以使用A构造B, 未加explicit的构造函数, 当使用B进行参数处理时, 就可以使用A, 使得接口混乱.
  • 为了避免这种情况, 使用explicit避免隐式构造, 只能通过显式(explicit)构造.
  • 按照默认规定,只有一个参数的构造函数也定义了一个隐式转换,将该构造函数对应数据类型的数据转换为该类对象
C++
1
2
3
4
5
class String {
    String ( const char* p ); // 用C风格的字符串p作为初始化值
    //…
}
String s1 = "hello"; //OK 隐式转换,等价于String s1 = String("hello");

有的时候可能会不需要这种隐式转换,如下:

C++
1
2
3
4
5
class String {
     String ( int n ); //本意是预先分配n个字节给字符串
     String ( const char* p ); // 用C风格的字符串p作为初始化值
    //…
}

下面两种写法比较正常:

C++
1
2
String s2 ( 10 );   //OK 分配10个字节的空字符串
String s3 = String ( 10 ); //OK 分配10个字节的空字符串

下面两种写法就比较疑惑了:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
String s4 = 10; //编译通过,也是分配10个字节的空字符串
String s5 = 'a'; //编译通过,分配int('a')个字节的空字符串
//s4 和s5 分别把一个int型和char型,隐式转换成了分配若干字节的空字符串,容易令人误解。
//为了避免这种错误的发生,我们可以声明显示的转换,使用explicit 关键字:

class String {
   explicit String ( int n ); //本意是预先分配n个字节给字符串
   String ( const char* p ); // 用C风格的字符串p作为初始化值
//…
}
//加上explicit,就抑制了String ( int n )的隐式转换

下面两种写法仍然正确:

C++
1
2
String s2 ( 10 );   //OK 分配10个字节的空字符串
String s3 = String ( 10 ); //OK 分配10个字节的空字符串

下面两种写法就不允许了:

C++
1
2
String s4 = 10; //编译不通过,不允许隐式的转换
String s5 = 'a'; //编译不通过,不允许隐式的转换

explicit 可以有效得防止构造函数的隐式转换带来的错误或者误解.

总结:explicit 只对单参构造函数起作用,用来抑制隐式转换

本章总结

  • 注意运算符重载的规则和限制
  • 重载运算符的时候要注意函数的返回类型
  • 前增量和后增量运算符的重载区别
  • 赋值运算符重载要注意内存空间的释放和重新申请。
  • 转换运算符重载与构造函数、析构函数一样没有返回值,通过转换运算符重载可以在表达式中使用不同类型的对象,但要注意转换运算符重载不可滥用。

Comments