【C++的探索路7】运算符重载的基本概念与赋值运算符重载

【C++的探索路4】面向对象编程与类的基本定义可知,面向对象的编程具备抽象、封装、多态与继承四个基本概念。C++通过类的概念实现了面向对象编程中的抽象与封装;而部分通过运算符重载实现了多态的性质。

这一部分将对运算符重载进行回忆和学习,第一部分先是个人对书(清华大学出版社--新标准C++程序设计教程 郭炜老师)中这部分章节进行回忆,整理思维导图图如下,(可能会有部分错误,后续将不断的对这一部分进行修正)!


运算符重载的概念和原理

C++非常注重类型匹配,如果语句中出现类型不匹配,则很有可能导致出现程序报错的现象。
#include<iostream>
using namespace std;
class Complex {
private:
	int imag, real;
public:
	Complex() :real(0), imag(0) {
	};
};
int main()
{
	int a  = 5;
	Complex c;
	c + 5;
    return 0;
}

上述程序定义了一个Complex的复数类,我们将这段程序敲入VS2017中,发现编译器自动在c+5下面标注下划波浪线。显然,在没有提供相应的接口情况下,直接将Complex的对象与5相加,将导致类型不匹配的错误发生。

运算符重载的目的就是为了解决无法直接将简单类型的变量与对象进行计算操作的问题。正是出于这个原因,运算符重载又可称为运算符函数;运算符重载实质上就是提供一种对外的接口。
运算符重载的基本书写格式为:
T operator ope(para...) {
	...
}
其中,T为返回值类型,ope为需要重载的运算符名称,para..则为参数表,运算符重载有两种形式,其一是重载为全局函数,其二是重载为成员函数。
而重载也有两种形式,形式一重载为全局函数,形式二:重载 为成员函数 ,方便内部操作。

这里通过一个Complex类的例子说明运算符重载的用法以及运算符重载的两种基本形式。
使得Complex类的对象能够直接与整型变量进行运算,注:Complex为复数的意思,这里编写了一个复数类
#include<iostream>  
using namespace std;
class Complex {
public:
	int imag, real;
	Complex() :real(0), imag(0) {
	};
	void operator - (int a) {
		cout << real - a << "+" << imag  << "i" << endl;
	}
};
void  operator + (Complex c, int a) {
	cout  << c.real  + a  << "+" << c.imag  << "i" << endl;
}
int main()
{
	int a  = 5;
	Complex c;
	c  + 5;
	cout  << "---这是一条美丽的分界线---" << endl;
	c  - 6;
	return 0;
}

现在对这段程序进行分析,该段程序实现了整型变量a直接利用+,-运算符对对象进行操作。
从构造形式上+与-运算符都具备
T operator ope(para...){
...
}
的基本形式;不同的是参数表。

从语法结构上来看,+运算符重载为全局函数,其参数表为两个参数;编译过程中c+5被处理为 operator+(c,5)
而与之对应的-运算符则重载为成员函数,只需要提供一个参数表即可;在运行过程中c-6被编译器处理成: c.operator-(6)

重载赋值运算符"="

我们在平时的日常计算中,用的最频繁的就是=,这个与那个相等,那个等于这个。作为日常运算的扛把子,运算符重载较复杂的内容,我们在介绍完运算符重载的基本概念后就要对赋值运算符重载这一部分进行讲解。
整体来说,复制运算符重载在运算符重载这一章中比较复杂,除了单纯的重载之外,还经常伴随着深浅拷贝的问题发生(本质是指针重复指向)。


这里通过一个程序完善的题目,逐步引入重载赋值运算符与其他相应的概念,主程序如下:

int main() {
	String s;
	s = "Good Luck";
	cout << s.c_str() << endl;
	s = "ShenZhou 8!";
	cout << s.c_str() << endl;
	return 0;
}

现在我们对这个类进行实现:

==========================================================================================
第一步:定义String类

这一部分我们对主程序进行逐级解剖:

第一行有一个孤零零的String对象s。良好的编程风格告诉我们,它可以孤独,但不能让真的就在那什么都不做,孤零零的戳在那里:一定要编写对应的构造函数完成这个对象的初始化工作。以免后续在不知情的情况下调用这个对象进行运算,比如调用s.c_str(),引发对应的内存问题。

——————————————————————————————————————————————————————————

主程序内的第二行与第四行将不定长度的字符串赋值给String对象s,这两行较为简单的语句肩负了较多的编程使命:需要较多的代码进行实现。

首先,字符串与String类类型不匹配,故需实现运算符重载:String operator=(char*str)

此外,字符串长度是可变而且未知的,只有char*的指针可以完成这一任务,因此String类内需要定义char*类型的成员变量str

——————————————————————————————————————————————————————————

程序第三和第五行分别调用了s.c_str()进行字符串输出,所以类内部一定需要定义c_str()成员函数。

最后,在程序结束的时候,一定要析构函数打扫战场。

—————————————————————————————————————————————————————————————————

综合上述需求,String类可以初步定义如下:

class String {
	char*str;
public:
	String() :str(NULL) {};
	String&operator=(char*s);
	const char*c_str()const;
	~String();
};

代码解释:

1,String构造函数可以什么都不做,但一定需要给内部变量进行地址分配赋值,所以这里给str赋值为NULL;

——————————————————————————————————————————————————————————————————

2,赋值运算符的类型问题

一般来说,可以令返回值为void,String与String&,但选择String&是为了保持良好的编程习惯以及减少内存消耗。


为什么不选择void?

如果有String的三个对象a,b,c

String a,b,c;
a=b=c;<==>a.operator=(b.operator=(c))

当存在a=b=c的赋值表达式时,其等价于上面表达式。

如果返回值为void,则在执行完b.operator=(c)后,将给a.operator传递一个void值,将使得程序错误。


为什么不选择String?

原因是基于较少内存消耗的考虑

当选择String返回类型时,程序在运行过程中将调用复制构造函数(参见前面关于复制构造函数的调用定义)做额外的事,从而导致额外的花销。


综上,我们选择String&作为返回值类型。

———————————————————————————————————————————————————————————————————

3,c_str()设置为常量成员函数的原因是为了防止外部对它进行修改。

================================================================================================

第二步:填充String类

经编写可填充得到整个String类

class String {
	char*str;
public:
	const char*c_str()const {
		return str;
	}
	String() :str(NULL) {};
	String&operator=(const char*s);
	~String() {
		if (str)
			delete[]str;
	}
};
String&String::operator=(const char*s) {
	if (str)
		delete[]str;
	if (s)
	{
		str = new char[strlen(s) + 1];
		strcpy_s(str, strlen(s) + 1, s);
	}
	else
		str = NULL;
	return*this;
}

c_str()与析构函数比较简单,都是简单的操作,这里着重讲解赋值运算符重载的编写问题

首先,挪窝肯定需要先清地盘:先把String类对象的成员变量delete掉。

接着就是进行复制的操作

如果被拷贝的对象(此对象不是OO中那个对象的意思)存在,则可以进行操作:先new一块地址,然后再调用strcpy_s函数进行copy操作

如果被拷贝对象不存在,则str=NULL;


到了这里程序也还没结束,因为只是完成了我们需要的工作,而赋值运算符重载函数的返回值为String&,我们需要给他返回值,就需要加个*this

返回给当前作用的对象。


至此,赋值运算符重载的工作就算基本完成了;贴一下完整的代码:

#include<iostream>
class String {
	char*str;
public:
	const char*c_str()const {
		return str;
	}
	String() :str(NULL) {};
	String&operator=(const char*s);
	~String() {
		if (str)
			delete[]str;
	}
};
String&String::operator=(const char*s) {
	if (str)
		delete[]str;
	if (s)
	{
		str = new char[strlen(s) + 1];
		strcpy_s(str, strlen(s) + 1, s);
	}
	else
		str = NULL;
	return*this;
}

using namespace std;
int main() {
	String s;
	s = "Good Luck";
	cout << s.c_str() << endl;
	s = "ShenZhou 8!";
	cout << s.c_str() << endl;
	return 0;
}

深浅拷贝(指针重复指向的问题)


有一句话叫做:做好人,就要做到底。上面的赋值运算符重载似乎工作是做到底了:很好的实现了字符串的赋值操作。

但实际上却远远不够;如果我们另外加上两行代码:

String s2; s2=s;
String s3(s);

Ctrl+F5后,我们会看到程序发生崩溃的现象,这是因为通常意义的浅拷贝现象发生了

其实严格来说C++并没有浅拷贝这个概念,这个问题的本质是指针的重复指向问题(再度印证了前面:用指针需谨慎的思想)。

由于指针重复指向同一块内存区域,当程序消亡、析构时,同一块内存空间被释放两次,从而引发了一连串的内存问题。

下面依次来对上面两行代码发生错误的原因进行陈述,并给出对应的解决方案

这一行代码在操作过程中,调用了默认的复制构造函数,将s.str赋值给str;而str将与s.str共同指向一块内存区域。在程序消亡的过程中,这一块内存将被delete掉两次,从而引发内存问题。如果s2本身就有赋值,则将多出一块内存无法进行回收。

对应的解决方案为:在String内部再编写一个赋值运算符重载函数。

	String&operator=(const String&s);

程序实现细节如下

String&String::operator=(const String&s) {
	if (s.str == str)
		return*this;
	if (str) {
		delete[]str;
		str = new char[strlen(s.str) + 1];
		strcpy_s(str, strlen(s.str) + 1, s.str);
	}
	else
		str = NULL;
	return*this;
}
程序Highlights解析:

1,在清理前可以先判断是否相等,如果相等可以直接返回,避免内存消耗

2,如果不相等,则与上面基本相同操作


程序第二行

String s3(s);
解决方案如下:

String::String(const String&s) {
	if (s.str)
	{
		str = new char[strlen(s.str) + 1];
		strcpy_s(str, strlen(s.str) + 1, s.str);
	}
	else
		str = NULL;
}

这一段程序与上面的赋值运算符重载略有区别:没有编写判断相等的语句;其不写的理由:因为这是初始化阶段,因为本来就不存在,所以就不需要判断。

整段程序整理如下:

#include<iostream>
class String {
	char*str;
public:
	const char*c_str()const {
		return str;
	}
	String() :str(NULL) {};
	String&operator=(const char*s);
	String&operator=(const String&s);
	String(const String&s);
	~String() {
		if (str)
			delete[]str;
	}
};
String&String::operator=(const char*s) {
	if (str)
		delete[]str;
	if (s)
	{
		str = new char[strlen(s) + 1];
		strcpy_s(str, strlen(s) + 1, s);
	}
	else
		str = NULL;
	return*this;
}
String&String::operator=(const String&s) {
	if (s.str == str)
		return*this;
	if (str) {
		delete[]str;
		str = new char[strlen(s.str) + 1];
		strcpy_s(str, strlen(s.str) + 1, s.str);
	}
	else
		str = NULL;
	return*this;
}
String::String(const String&s) {
	if (s.str)
	{
		str = new char[strlen(s.str) + 1];
		strcpy_s(str, strlen(s.str) + 1, s.str);
	}
	else
		str = NULL;
}
using namespace std;
int main() {
	String s;
	s = "Good Luck";
	cout << s.c_str() << endl;
	s = "ShenZhou 8!";
	cout << s.c_str() << endl;
	String s2;
	s2 = s;
	cout << s.c_str() << endl;
	String s3(s);
	String s4 = s;
	return 0;
}

般情况,上面两种情况都被视为浅拷贝。但浅拷贝的本质为:指针重复指向同一块区域,从而在析构过程中引发内存错误。

对于这种现象,其对应的解决方案为:重新编写对应的赋值运算符重载函数。


再往后我想了想,又补充了几段语句,使得赋值运算符重载这一部分内容更为完整,由于基本套路就在那里,感兴趣的同学可以分析一下这么写的原因

#include<iostream>
using namespace std;
class String {
	char *str;
public:
	const char* c_str()const {
		return str;
	}
	String():str(NULL){}
	~String();

	String&operator=(const char*s);
	String&operator=(const String&s);
	String(const char*s);
	String(const String&s);
};
String::~String() {
	if (str)
		delete[]str;
}
String&String::operator=(const char*s) {
	if (str)
		delete[]str;
	if (s) {
		str = new char[strlen(s) + 1];
		strcpy_s(str, strlen(s) + 1, s);
	}
	else
		str = NULL;
	return*this;
}
String&String::operator=(const String&s) {
	if (str == s.str)
		return *this;
	if (str)
		delete[]str;
	if (s.str) {
		str = new char[strlen(s.str) + 1];
		strcpy_s(str, strlen(s.str) + 1, s.str);
	}
	else
		str = NULL;
	return*this;
}
String::String(const char*s) {
	str = new char[strlen(s) + 1];
	strcpy_s(str, strlen(s) + 1, s);
}
String::String(const String&s) {
	if (s.str)
	{
		str = new char[strlen(s.str) + 1];
		strcpy_s(str, strlen(s.str) + 1, s.str);
	}
	else
		str = NULL;
	return;
}
int main() {
	String s;//定义String类
	s = "Welcome to our group";
	cout << s.c_str() << endl;
	String s2 = "this";//属于构造函数
	cout << s2.c_str() << endl;
	s2 = s;//定义形参为String&的重载
	cout << s2.c_str() << endl;

	String s3(s2);//定义相关构造函数
	cout << s3.c_str() << endl;
	return 0;
}

总结

这一部分又是N大段程序,有点眼花缭乱吧,让我们进行一下简单的程序编写总结:





  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值