由【C++的探索路4】面向对象编程与类的基本定义可知,面向对象的编程具备抽象、封装、多态与继承四个基本概念。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;
}
T operator ope(para...) {
...
}
#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;
}
T operator ope(para...){
...
}
重载赋值运算符"="
这里通过一个程序完善的题目,逐步引入重载赋值运算符与其他相应的概念,主程序如下:
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大段程序,有点眼花缭乱吧,让我们进行一下简单的程序编写总结: