前言
在上一则教程中,我们讲述了重载运算符中前 ++和后++的重载函数的实现,阐述了在 C++中可以将运算符进行重载的方法,这种方法大大地便利了程序员编写代码,在接下来地叙述中,我们将着重讲述运算符重载时地一些更为细致地内容,其中就包括当重载地运算符返回值为引用和非引用两种状态时,代码执行效率地高低以及采用在类内实现运算符重载函数的方法。
返回值为引用和非引用的区别
在上述所示的类当中,增加一部分代码,加入析构函数以及拷贝构造函数,代码如下所示:
- class Point
- {
- private:
- int x;
- int y;
- public:
- Point()
- {
- cout<<"Point()"<<endl;
- }
- Point(int x, int y) : x(x), y(y)
- {
- cout<<"Point(int x, int y)"<<endl;
- }
- Point(const Point& p)
- {
- cout<<"Point(const Point& p)"<<endl;
- x = p.x;
- y = p.y;
- }
- ~Point()
- {
- cout<<"~Point()"<<endl;
- }
- friend Point operator++(Point &p);
- friend Point operator++(Point &p, int a);
- void printInfo()
- {
- cout<<"("<<x<<", "<<y<<")"<<endl;
- }
- };
在上述的代码中,我们在构造函数以及拷贝构造函数析构函数都加入了打印信息,其中,运算符重载函数前++和后++函数沿用之前的一样,返回值不是引用,与此同时,我们在前 ++和后 ++函数中也加入打印信息的代码,代码如下所示:
- /* ++p */
- Point operator++(Point &p)
- {
- cout << "++p" << endl;
- p.x += 1;
- p.y += 1;
- return p;
- }
- /* p++ */
- Point operator++(Point &p, int a)
- {
- cout << "p++" << endl;
- Point n;
- n = p;
- p.x += 1;
- p.y += 1;
- return n;
- }
上述便是前 ++和 后 ++的重载函数,紧接着,书写主函数的代码,观察当返回值为非引用的时候,代码的运行效果,主函数代码如下所示:
- int main(int argc, char **argv)
- {
- Point p1(1, 2);
- cout<<"begin"<<endl;
- ++p1;
- cout << "******************"<<endl;
- p1++;
- cout<<"end"<<endl;
- return 0;
- }
上述代码的运行结果如下所示:
lhp7d3H1crAE9u2
依据运行结果我们分析一下,第一条输出信息 Point(int x, int y)是因为执行了 Point p1(1,2);语句而调用的构造函数,++p这条输出信息同样也是因为执行了 ++p;而调用的构造函数,那紧接着的两条输出信息是如何产生的呢,我们回过头去看看++p的函数,可以看到 ++p的函数是一个返回值为 Point类型的函数,而上述中的输出语句 Point(const Point& p)和 ~Point()就是在创建这个返回值对象时调用的构造函数以及当返回值返回后调用的析构函数;而紧接着的输出信息是 p++和 Point()以及~Point(),p++这个输出信息自然是因为调用的后 ++重载运算符函数的构造函数而输出的打印信息,那紧接着的 Point()和 ~Point()是因为在后 ++重载运算符函数中,创建的局部变量 Point n,进而调用了 Point()函数,以及函数退出之后,局部变量销毁,调用了析构函数。
上述详细地分析了各个打印信息输出的原因,通过上述的打印信息我们可以清楚知道程序在什么地方调用了构造函数,在什么地方调用了析构函数,再次回顾上述的函数调用过程,可以看出来其实调用的Point(const Point& p)和~Point()是多余的,那要如何改进代码呢,我们只需要将前 ++运算符重载函数的返回值类型改为引用就行,这样就不会创建临时的变量,同时也就不会在调用构造函数和析构函数,改动之后的代码如下所示:
- Point& operator++(Point &p)
- {
- cout<<"++p"<<endl;
- p.x += 1;
- p.y += 1;
- return p;
- }
那么上述代码的运行结果是什么呢?在主函数不变的情况下,输出结果如下所示:
M4QzImA1uYxnBK9
可以看到上述结果中,之前在 ++p后输出的两条信息现在因为将返回值设置为引用之后就消失了,说明这样的方法避免了调用构造函数和析构函数,节省了程序运行的空间,那如果将后++重载函数设置为引用可不可行呢,很显然,如果返回的是 n的引用,那么这在语法中就是错误的,因为n是局部变量,局部变量在函数调用结束就销毁了,是不能作为引用对象的。如果返回的是 p呢,那么函数的运行结果将发生改变,换句话说就是不是实现的后 ++这个功能了。
最后,总结一下,对于一个函数来说,函数的返回结果如果作为值返回,那么代码的执行效率较低;如果作为引用返回,那么代码的执行效率较高,但是会存在一个问题,引用返回可能会导致函数运行出错,所以,在保证函数运行没有错误的前提下,为了提高效率应该使用的是引用返回。
紧接着,我们知道我们在使用 C++进行编码的时候,基本不会再采用 C语言中的语法 printf这个语句,随之替代的是 cout这个语句,我们也知道我们使用 cout进行输出的时候,往往采用的是下面这样的输出方式:
- cout << "m=" << m << endl; /* 此时 m 不是一个实例化对象 */
但是如果说此时 m 是一个实例化的对象,那么像上述这样输出就是存在问题的,这个时候,就需要对 <<运算符进行重载,重载的代码如下所示:
- ostream& operator<<(ostream &o, Point p)
- {
- cout<<"("<<p.x<<", "<<p.y<<")";
- return o;
- }
稍微对上述代码进行一下解释, 这里为什么返回值是ostream&呢,是因为对于 cout来说,它是ostream类的实例化对象,在使用 cout进行输出的时候,它所遵循的一个输出格式是 cout <<,因此,这里的返回值是 ostream。为什么返回值是引用呢,是为了满足下面所示代码的运行,同时输出了 m和 p1,结合上述代码,我们来编写主函数,主函数代码如下所示:
- int main(int argc, char **argv)
- {
- Point p1(1,2);
- Point m;
- m = p1++;
- cout << "m =" << m << "p1 =" << p1 << endl;
- }
上述代码的运行结果如下所示:
1cGujg7yqZSIfpK
可以看到在重载了运算符 <<之后,输出实例化的对象也是可行的。
类内实现运算符重载函数
在上述代码中我们实现的 +运算符重载函数以及前 ++运算符重载函数和后++运算符重载函数,都是在类外实现的,那么如果要在类内实现以上几个运算符重载函数,应该如何写呢,我们先回顾一下,在类外面实现的+运算符重载函数的函数声明如下所示:
- friend Point operator+(Point &p1, Point &p2); /* 因为在类外要能够访问类里面的数据成员,因此这里使用的是友元 */
上述是在类外实现运算符重载函数时的函数原型,那么如果函数的定义就是在类里面实现的,函数又该如何编写呢?首先,如果是在类里面实现,那么当前使用这个类进行实例化的对象本身就可以使用 *this来表征一个对象,这个时候,如果要重载 +运算符函数,那么就只需要一个Point类的形参就行,代码如下所示:
- class Point
- {
- private:
- int x;
- int y;
- public:
- /* 省略相关构造函数的代码,可以结合前文补全 */
- Point operator+(Point &p)
- {
- cout<<"operator+"<<endl;
- Point n;
- n.x = this->x + p.x;
- n.y = this->y + p.y;
- return n;
- }
- }
对比上述在类外面实现的代码,对于重载的运算符 +来说,只有一个形参了,而与其相加的另一个对象使用的是this来替代。依据这样的一种思路,我们继续将前 ++和后 ++重载的运算符函数进行改写,改写之后的代码如下所示:
- class Point
- {
- private:
- int x;
- int y;
- public:
- /* Point p(1,2); ++p */
- Point& operator++(void)
- {
- cout<<"operator++(void)"<<endl;
- this->x += 1;
- this->y += 1;
- return *this;
- }
- /* Point p(1,2); p++; */
- Point operator++(int a)
- {
- cout<<"operator++(int a)"<<endl;
- Point n;
- n = *this;
- this->x += 1;
- this->y += 1;
- return n;
- }
- };
结合上述的代码,我们再来编写主函数,主函数的代码如下所示:
- int main(int argc, char ** argv)
- {
- Point p1(1,2);
- Point p2(2,3);
- Point m;
- Point n;
- cout << "begin" << endl;
- m = ++p1; /* m = p1.operator++(); */
- cout << "m =" << m << "p1 =" << p1 << endl;
- cout << "*********************" << endl;
- n = p2++; /* n = p2.operator++(0); */
- cout << "n =" << n << "p2 =" << p2 << endl;
- return 0;
- }
上述代码中,注释掉的代码和没注释的代码前后是等价的,只是说注释掉的代码看起来更加直观,更加容易理解其背后的原理,而注释前的代码则更加简洁。这里额外说一点,<<的重载函数是不能够放到类内实现的,因为这个重载函数的形参不是 Point类的,所以其只能在类外才能实现。
上述中,叙述了在类内实现的重载运算符函数,接下来叙述一下 =运算符在类内实现的重载函数,我们以之前所说的 Person类来实现这个功能,Person类的代码实现如下所示:
- class Person
- {
- private:
- char *name;
- int age;
- char *work;
- public:
- Person()
- {
- name = NULL;
- work = NULL;
- }
- Person(char *name, int age, char *work)
- {
- this->age = age;
- this->name = new char[strlen(name) + 1];
- strcpy(this->name,name);
- this->work = new char[strlen(work) + 1];
- strcpy(this->work, work);
- }
- /* 拷贝构造函数 */
- Person(Person &p)
- {
- this->age = p.age;
- this->name = new char[strlen(p.name) + 1];
- strcpy(this->name,p.name);
- this->work = new char[strlen(p.work) + 1];
- strcpy(this->work, p.work);
- }
- ~Person()
- {
- if (this->name)
- delete this->name;
- if (this->work)
- delete this->work;
- }
- void PrintInfo(void)
- {
- cout << "name =" << name << "age =" << age << "work =" << work << endl;
- }
- }
基于上述的代码,我们可以书写如下的主函数代码:
- int main(int argc, char **argv)
- {
- Person p1("zhangsan", 18, "doctor");
- Person p2;
- p2 = p1;
- }
上述中,我们还没有将 =运算符进行重载,就使用了 =实现了实例化对象的运算,这样会存在一个什么问题呢,我们从源头来进行分析,=运算符执行的是值拷贝,那么在执行了上述语句之后,p2和p1之间的关系是这样的:
ywhv3zYKCaRjrXx
通过上述所示的图片可以看出,如果不将 =进行重载,那么会让 p1和 p2的name 和 work指向同一块内存,这会造成什么问题呢,如果此时已经将 p1的内存释放掉了,而这个时候又要释放 p2的内存,这种情形就会出错,同一块内存不能够释放两次。
因此,就需要对 =运算符进行重载,重载的代码如下所示:
- /* 注意此处的代码是在类里面实现的成员函数,这里省略的一部分代码 */
- Person& operator=(Person &p)
- {
- if (this == &p)
- return *this;
- this->age = p.age;
- if (this->name)
- delete this->name;
- if (this->work)
- delete this->work;
- this->name = new char[strlen(p.name) + 1];
- strcpy(this->name, p.name);
- this->work = new char[strlen(p.work) + 1];
- strcpy(this->work, p.work);
- }
这样子就会避免上述情况的出现,我们现在继续来书写主函数:
- int main(int argc, char **argv)
- {
- Person p1("zhangsan", 18, "doctor");
- cout<<"Person p2 = p1" <<endl;
- Person p2 = p1;
- Person p3;
- cout<<"p3=p1"<<endl;
- p3 = p1;
- cout<<"end"<<endl;
- p1.PrintInfo();
- p2.PrintInfo();
- p3.PrintInfo();
- return 0;
- }
上述主函数运行的结果如下所示:
2kiKb8NEfYynTdo
通过上述代码我们看到,实际上代码 Person p2 = p1的运行并不是调用的 = 的重载函数,而是调用的拷贝构造函数,只有 p3= p1才是调用的 =的重载函数。
在本章节的最后,额外补充一点,刚刚提到了拷贝构造函数,实际上拷贝构造函数的形参大多数都是加了const修饰符的,也就是像如下所示的这样子:
- Person& operator=(const Person &p)
而这个时候,如果我们定义的 Person p1也是 const的,也就是像这样:
- const Person p1("zhangsan", 18, "doctor");
那这个时候在使用 p1.PrintInfo()的时候就会出错,因为此时必须把该成员函数也表明为 const的才行,代码如下所示:
- /* 类内成员函数,省略部分代码 */
- void PrintInfo(void) const
- {
- cout << "name =" << name << "age =" << age << "work =" << work << endl;
- }
总结一下也就是说:const对象只能够调用const成员函数,而const表示的是此函数没有对当前对象进行修改
小结
上述就是本期教程分享的内容,到本期教程截至,C++相对于 C语言不同的一些语法特性就到此结束了。下期教程将介绍 C++如何实现面向对象的方法。本期教程所涉及到的代码可以通过百度云链接的方式获取到。
链接:https://pan.baidu.com/s/1BC55_QH-iV23-ON0v1OGSA
提取码:iyf7
本文转载自微信公众号「wenzi嵌入式软件」,可以通过以下二维码关注。转载本文请联系wenzi嵌入式软件公众号。