从C到Cpp——十三、运算符重载、友元函数和转换函数

一、什么是运算符重载

简而言之,运算符重载的作用是让同一个运算符依据不同的上下文联系发挥不同的作用。

事实上,在C++甚至C语言的基础语法中,就出现了很多运算符重载的现象。

int &a;
int *pa=&a;

第一个&的作用是声明一个引用类型的变量,而第二个&的作用是对a变量取地址。这便是重载的一个样例。

而在C++中,我们甚至可以人为地去设置这种运算符重载。这种设置是通过编写一种被称为运算符函数的特殊函数来实现的。也就是说,自定义的运算符函数重载实质上是对自定义函数的调用,不过调用的方式是使用已有的普通运算符

运算符函数的名称格式如下:
operateop(argument-list)

举例而言:

operate+(a)

这里+为被重载的运算符,a为参数。如果我们把函数命名为类似上述的形式,那么编译器就能够进行识别,编译器就会设置运算符的重载!

如此操作,在特定的情况下,可以使下面两种情况等价,假设b和a为某两个对象,且有关于a和b对象对应类型的重载:

a+b
a.operate+(b)

后面,会对这一特性进行具体说明。

二、使用运算符重载特性

1--定义重载函数

定义重载函数的方式和定义成员函数一样,只不过需要把函数名命名为operator+、operator-这样的形式,下面就为被定义为类的长度为10的int型数组设置了一个重载运算符:

#include <iostream>
#include <cstdlib>

class Array_10
{
	private:
		int array[10];
	public:
		void set(int num,int value)
		{
			array[num]=value;
	    } 
	    Array_10 operator+(const Array_10& tmp)
	    {
	    	Array_10 result;
			for (int i=0;i<10;i++)
			{
				result.array[i]=array[i]+tmp.array[i];
			}
			return result;
		}
		void show()
		{
			for (int i=0;i<10;i++)
			{
				std::cout<<array[i]<<"  ";
			}
			std::cout<<std::endl;
		}
};

int main(void)
{
	Array_10 m;
	Array_10 n;
	Array_10 r;
	for (int i=0;i<10;i++)
	{
		m.set(i,rand()%100);
		n.set(i,rand()%100);
	}
	
	m.show();
	n.show();
	
	r=m+n;
	r.show();
}

当然,也可以只在类定义中提供一个函数原型,然后把函数的定义放在外边。如此,就要加上Array_::前缀了:

Array_10 Array_10::operator+(const Array_10& tmp)
{
	Array_10 result;
	for (int i=0;i<10;i++)
	{
		result.array[i]=array[i]+tmp.array[i];
	}
	return result;
}

2--自定义的运算符重载一般多应用于类方法

3--运算符函数可以隐式和显式地调用

上述代码中,将r=m+n改为下列形式,程序仍旧可以正常运行:

r=m.operator+(n);

这意味着运算符函数保留了其作为类成员函数地性质,仍旧可以显式地调用。事实上,r=m+n就是显式调用地缩写,我们可以将其理解为对operator+类成员函数地隐式调用!

4--可以连续使用重载的运算符

如果修改上述代码的主函数为如下

int main(void)
{
	Array_10 m;
	Array_10 n;
	Array_10 p;
	Array_10 r;
	for (int i=0;i<10;i++)
	{
		m.set(i,rand()%100);
		n.set(i,rand()%100);
		p.set(i,rand()%100);
	}
	
	m.show();
	n.show();
	p.show();
	
	r=m+n+p;
	r.show();
}

程序仍然能实现其功能。

因为编译器会把下面这一句进行解释:

m+n+p

它将等价于

m.operator+(n+p)

三、重载运算符的限制

1--重载的运算符不一定是类成员函数,但必须至少有一个操作数(参数)是用于自定义的类型。

为什么有这样的限制,想编译器如何识别这个运算符是原有的还是重载的。还是通过联系上下文看看操作符作用的元素(比如+运算符必然连接两个变量)是不是符合运算符函数的定义。如果是,则使用运算符重载。所以,必须有一个操作数是自定义类型,才能进行区分。

类成员运算符函数已经在前面介绍,而非成员函数也能变为运算符函数。如下就是一个自定义的非成员运算符函数。由于非成员函数是在类定义外部定义的,且不带作用域解析运算符,那么编译器是能够识别并把运算符按照非成员函数的方式重载的,而不是按照成员函数的方式。

#include <iostream>
#include <cstdlib>

class Array_10
{
	private:
		int array[10];
	public:
		void set(int num,int value)
		{
			array[num]=value;
	    } 
	    int read(int num) const
	    {
	    	return array[num];
		}
		void show()
		{
			for (int i=0;i<10;i++)
			{
				std::cout<<array[i]<<"  ";
			}
			std::cout<<std::endl;
		}
};

int operator*(const Array_10& a,const Array_10& b);

int main(void)
{
	Array_10 m;
	Array_10 n;
	int r;
	for (int i=0;i<10;i++)
	{
		m.set(i,rand()%100);
		n.set(i,rand()%100);
	}
	
	m.show();
	n.show();
	//r=operator*(m,n);
	r=m*n;
	std::cout<<r;
}

int operator*(const Array_10& a,const Array_10& b)
{
	int sum=0;
	for (int i=0;i<10;i++)
	{
		sum+=(a.read(i))*(b.read(i));
	}
	return sum;
}

在这里,下面两种函数使用方法等价:

r=operator*(m,n);
r=m*n;

这与成员运算符函数的等价有差异,可见编译器非常聪明,能够因地制宜!

在上述代码中,我使用了成员函数read来辅助非成员函数访问成员对象。其实这里还可以使用友元函数进行访问,之后章节将给出具体的介绍!

2--结构体可以运算符重载吗?

由于C++中的结构体可以被当作默认成员属性为public的类(有一定差别,但操作上可以姑且这么认为),所以同样可以进行运算符重载。方式和类几乎一致!

#include <iostream>

struct Student
{
	int age;
	int grade;
	Student();
};
Student operator+(const Student& a,const Student& b);

int main(void)
{
	Student a,b;
	Student c;
	c=a+b;
	std::cout<<c.age<<std::endl;
	std::cout<<c.grade;
} 

Student::Student():age(20),grade(2)
{
	
}

Student operator+(const Student& a,const Student& b)
{
	Student result;
	result.age=a.age+b.age;
	result.grade=a.grade+b.grade;
	return result;
}

3--不能改变运算符原来的语法规则和优先级

很好理解,因为运算符重载先按照原来的规则进行读取,然后再判断是否运用操作符重载。

4--上面这一点也意味着,编译器会对运算符函数参数的数量进行检查。由于能重载的运算符只有一元运算符和二元运算符,所以对于二元函数,成员函数最多只有一个参数,非成员函数最多两个参数;对于一元函数,成员函数不能有参数,非成员函数最多一个参数。

参数数量不匹配,都会发生错误。所以,请按照标准严谨分配参数!

5--不能重载以下运算符

  1. sizeof
  2. . 成员运算符
  3. .* 成员指针运算符
  4. :: 作用域解析运算符
  5. ? 条件运算符

还有一些不常用,这里不予展示了

6--下列运算符只能通过成员函数重载

  1. = 赋值运算符
  2. () 函数调用运算符
  3. [] 下标运算符
  4. -> 间接成员运算符

四、部分运算符重载的示例

1-- ++(前置)运算符

#include <iostream>

class Student
{
	private:
		int age;
		int grade;
	public:
		Student(int p_age,int p_grade)
		{
			age=p_age;
			grade=p_grade;
		}
		Student operator++()
		{
			age++;
			grade++;
			return *this; 
		}
		void show()
		{
			std::cout<<age<<std::endl;
			std::cout<<grade<<std::endl;
		}
};

int main(void)
{
	Student a(19,1);
	++a;
	a.show();
	(++a).show();
}

可见成员一元运算符函数重载不用额外传参数

2--++(后置)运算符

约定形参处加一个int

Student operator++(int)
{
	age++;
	grade++;
	return *this; 
}

五、友元函数

1--简介

友元函数是一种能够访问类成员的非成员函数。它的原型需要被定义在类的public部分中。

2--创建友元函数

第一步是将其原型放在类声明中,并在原型的前面加上关键字freind:

friend void function_name(int param);

这个原型意味着:

  1. 虽然该函数在类声明中声明,但它不是成员函数,不能通过成员运算符.调用
  2. 这个函数与成员函数有相同访问权限(可以直接访问类成员,而不是使用成员运算符

第二步是编写定义,注意这里就不用加上关键字friend了:

​void function_name(int param)
{
  ......
}

3--示例——把上面的一段实现数组内积的代码用友元重新定义

#include <iostream>
#include <cstdlib>

class Array_10
{
	private:
		int array[10];
	public:
		void set(int num,int value)
		{
			array[num]=value;
	    } 
		void show()
		{
			for (int i=0;i<10;i++)
			{
				std::cout<<array[i]<<"  ";
			}
			std::cout<<std::endl;
		}
		friend int operator*(const Array_10& a,const Array_10& b);
};


int main(void)
{
	Array_10 m;
	Array_10 n;
	int r;
	for (int i=0;i<10;i++)
	{
		m.set(i,rand()%100);
		n.set(i,rand()%100);
	}
	
	m.show();
	n.show();
	//r=operator*(m,n);
	r=m*n;
	std::cout<<r;
}

int operator*(const Array_10& a,const Array_10& b)
{
	int sum=0;
	for (int i=0;i<10;i++)
	{
		sum+=a.array[i]*b.array[i];
	}
	return sum;
}

六、类的自动类型转换

1--

类在构造函数的加持下具有一些自动类型转化的性质,我们以Student类为例子进行说明。假设存在构造函数Student

Student::Student(double p_height)
{
    height=p_height;
}

其中height是Student类的一个成员对象。

那么这个构造函数将可以被用于自动类型转换,可以编写如下的代码:

Student a;
a=180.0

原理是什么呢?运行到a=180.0这个片段时,程序会通过构造函数在参数180.0的条件下创造一个临时的Student类对象,然后通过逐个成员复制的方法将临时对象的值复制给a。

2--

仅在构造函数只有一个参数,或构造函数除第一个参数外均包含默认参数的情形下可以使用上述的性质

Student(double height,int age);
Student(double height,int age=20);

上述两个函数,仅第二个支持自动类型转换。

3--

在下列情况下可以进行隐式的类型转换

  1. 将Student对象初始化为double时
  2. 将double值赋给Student对象时
  3. 将double值传递给接收Student对象的函数时
  4. 返回值被声明为Student的函数试图返回double时
  5. 上述任意一种情况下,使用可转换为double类型的任意内置类型。

如果要关闭这种自动的类型转换,在声明相应的Student构造函数时可以前置关键字explicit。

explicit Student(double height);

 当然此时仍然可以使用显式的类型转换(强制类型转换仍然时可以的)

4--当上述的性质具有二义性时,将被禁用。

比方说,同时定义了两个构造函数

Student(double height);
Student(long height);

编译器将会因为避免二义性的考虑禁用自动类型转换的性质。

七、转换函数

如上一节所示,将double等内置类型转换为Student这样的自定义类是可以自动的,而将Student这样的自定义类转换为内置类型则需要借助转换函数

转换函数是一种特殊的运算符函数,其原型必须满足以下形式:

operator typeName();

且必须满足

  1. 转换函数必须是类方法
  2. 转换函数不能有指定的返回类型(会自动返回typeName类型)
  3. 转换函数不能有参数

示例如下:

operator double();
Student::operator double()
{
    return height;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值