《重学C++》7. C++高级语法(一)OOP、构造函数、运算符重载

主要内容

这一篇主要总结于第七章”C++高级语法”中的第1-6小节。下文第一节主要介绍面向对象编程(Object Oriented Programming,OOP);第二节简单介绍类的创建和成员的声明、定义;第三四节为主要内容,涉及各种构造函数和运算符重载。




1. 面向对象编程()

编程中的抽象和实际之间的关系:
并不是所有面向对象编程中的逻辑关系都是在物质世界中有迹可循的。例如常见的”is-a”的关系用来表述“继承”的概念并不完善;此外还有实际项目中,“缓冲区”类的存在也并没有现实中对应的原型。


2. 添加虚数类

在C++编程中,可以右键项目添加类,创建.h和.cpp文件。
之后在这两个文件中进行编程。析构函数需要声明为虚函数,具体原因后面会讲。

在对应.h文件中对类进行声明

在对应.cpp文件中类的方法进行实现

最后,在另外的.cpp文件(记得引入头文件)中的main函数中进行对象的创建。

类的成员函数定义有两种:类内定义和类外定义。
类内定义是在类的大括号内直接写类的定义,类内定义的成员函数会被优先编译为内联函数
类外定义包括同文件类外定义,和分文件类外定义。它们均是在类内只写成员函数的声明,但是在同文件的类外或者单独一个.cpp文件中编写类的定义。


3. 虚数类的实现——重载+=、拷贝构造和临时对象优化

(1) 运算符重载,实现加法和赋值

为了实现虚数的加法操作并对对象进行赋值(红框内部分),分别对运算符+和运算符=进行重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//重载运算符+
//返回Complex对象,输入不允许修改变量的引用作为参数,最后const指明该函数不允许修改成员变量
Complex Complex::operator+ (const Complex& x) const
{
Complex tmp;
tmp._real = _real + x._real;
tmp._image = _image + x._image;
return tmp;
//tmp是局部变量,函数体外它会消亡,但是为什么可以返回这个变量
}

//重载运算符=
//返回Complex对象的引用,输入不允许修改变量的引用作为参数
Complex& Complex::operator= (const Complex& x)
{
if (this != &x)
{
_real = x._real;
_image = x._image;
}
return *this;
}

运行结果:
4个构造函数却有5个析构函数

(2) 拷贝构造函数

但是从上面的图可以看出存在几个问题:程序中声明了4个对象(a、b、c和tmp),却有4个构造函数和5个析构函数的调用,哪里多出了一个Complex对象?并且为什么构造和析构数量不一致?最后,tmp是局部变量,函数体外它会消亡,但是为什么重载+的函数可以返回这个变量并将其作为重载=的函数参数?(问题1

进行调试,发现实参tmp和形参x地址不一致,但是返回值c和*this地址一致:

tmp和参数x地址不一致,c和*this地址一致

这说明并不是tmp直接作为重载=的函数的参数,而是返回值tmp到参数x中间发生了复制,复制产生的临时对象作为参数x。

系统将局部对象tmp进行拷贝构造,生成一个看不见的临时对象,然后作为参数传入重载=函数中(因此tmp和参数x地址不同)。这个临时对象的拷贝构造是系统通过自动生成的拷贝构造函数来实现的,所以构造函数的输出只有4个,而不是析构函数的5个。(答案1

应当自定义拷贝构造函数,避免系统为我们实现构造函数。因为系统实现的拷贝构造函数,容易带来深拷贝/浅拷贝的问题

c++primer学习笔记–为什么要自己写拷贝构造函数
https://blog.csdn.net/pursue_my_life/article/details/80462572

编写自定义拷贝构造函数,这样需要拷贝构造时,系统就会调用自己写的这个函数(优化1):

1
2
3
4
5
6
7
//拷贝构造函数是一种特殊的构造函数,它的参数必须是引用,一般会加上const
Complex::Complex(const Complex& x)
{
_real = x._real;
_image = x._image;
//cout << "Complex::Complex(const Complex& x)" << endl;
}

拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其形参必须是引用,但并不限制为const,一般普遍的会加上const限制。

再次运行:
5个Complex对象

5个构造和析构函数输出

可以看到多了一个构造函数输出,就是上面的拷贝构造函数。并且返回值tmp和参数x的地址是一致的

(3) 临时对象优化

考虑到临时变量优化(尽量避免产生临时对象):因为重载+的函数中tmp没有必要存在,因此重载运算符+就可以将相加结果作为参数,直接返回拷贝构造函数的结果(优化2):

1
2
3
4
5
6
7
8
9
10
Complex Complex::operator+ (const Complex& c) const
{
//Complex tmp;
//tmp._real = _real + x._real;
//tmp._image = _image + x._image;
//return tmp;

//将相加结果作为参数,返回构造函数Complex(double r, double i)的结果
return Complex(_real + c._real, _image + c._image);
}

修改之后,少了tmp变量的构造和析构函数

修改之后,相比于上次运行结果,少了tmp变量的构造和析构函数。并且构造函数Complex(double r, double i)替代了拷贝构造函数。

更好的减少临时对象的方法是:直接对对象c进行赋值时构造。(优化3

Complex c = a+b;   //而不是 Complex c; c= a+ b;   

这样的好处是,避免了“Complex c; ”语句中默认构造函数产生的对象,而是直接进行赋值。

最终优化到只有3个Complex对象

生成的几个Complex对象总结:

  1. 程序员生成的Complex对象a,由Complex(double r, double i)函数生成;
  2. 程序员生成的Complex对象b,由Complex(double r, double i)函数生成;
  3. 程序员生成的Complex对象c,由Complex()函数生成;
    第三次优化中,语句”Complex c= a+b; “避免了调用Complex()等构造函数,并且直接使用a+b的结果(来自重载+函数的返回值)作为c(没有使用重载=进行赋值)。
  4. 程序员生成的Complex对象tmp,在重载+函数中由Complex()函数生成;
    第二次优化中,不使用Complex tmp;等即可避免产生该对象。
  5. 中间变量,由重载+函数的返回值tmp被拷贝构造产生(然后被作为重载=函数的参数x);
    第二次优化中,语句return Complex(_real + c._real, _image + c._image); 替代Complex tmp; 等语句,用Complex(double r, double i)函数替代了拷贝构造函数。

第2、3小节知识点:

  • 利用VS“新建类”功能,实现类的分文件编写。
  • 重载运算符
  • 拷贝构造函数
  • 临时变量优化

4. 虚数类的实现——前置和后置运算符

以++运算符为例,要注意前置和后置的区分。

1
2
3
4
5
6
7
8
9
10
11
//前置++重载
Complex& Complex::operator++ () {
real++;
image++;
return *this;
}

//后置++重载
Complex Complex::operator++ (int) {
return Complex(real++, image++);
}

区分前置和后置运算符的方式是形参列表是否有Int、参数返回类型。
如果是前置,返回值类型应该为引用;
如果是后置,应该是值传递,并且形参列表中有int。