《重学C++》7. C++高级语法(三) 其他问题:头文件、深浅拷贝、多态和虚函数

1. 其他工程问题

头文件重复包含问题

为了防止同一个文件被include多次,有两种方式避免:

  1. 可以使用宏定义方式进行避免:

    1
    2
    3
    4
    5
    6
    #ifndef __SOMEFILE_H__
    #define __SOMEFILE_H__

    //to do

    #end if

    这种宏定义的方式优点:可移植性好;缺点:无法防止宏名重复(宏名重复时会出错),难以排错。

  2. 使用编译器的方式

    1
    #pragma once

    优点:可以防止宏名重复,容易排错;缺点:可移植性查,只能在win平台使用。

引用头文件

项目引用的各类头文件统一放到stdafx.h中,避免分散

2. 深拷贝、浅拷贝

浅拷贝:拷贝对象时,只拷贝指针地址。C++默认拷贝构造和赋值运算符重载都是浅拷贝。节省空间,但容易引发多次释放。
深拷贝:重新分配堆内存,不只拷贝指针,还会对指针指向的内容进行拷贝。浪费空间,但不会导致多次释放。

实现深拷贝,需要在拷贝构造赋值运算符重载时,主动复制指针指向的内存空间。此外,还需要在析构函数中考虑到将这块内存释放,防止发生内存泄露。
如果要兼顾深拷贝和浅拷贝的优点,有两种方法:一是采用引用计数,当没有指针指向该内存时才进行内存销毁(类似智能指针);其次是C++新标准的move方法(智能指针中也有类似的实现),指针b直接拿来使用指针a指向的空间,相当于所有权的转让。

3. 虚函数

继承

  • 派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。(即在类名后加上冒号,后跟以逗号分割的基类列表)。
  • 如果不希望一个类(成员函数)被继承(覆盖),需要在类名(函数声明的最后)后添加final关键字。
  • 可以指定派生类中一个成员函数是对基类中成员函数的覆盖,在const、引用限定符后显式注明override
  • 关于访问限定符,派生类不能使用基类private的成员,但可以使用protected成员,无论是谁都可以用public成员。
  • 友元函数可以访问该类private的成员,但友元不能被传递、不能被继承

如下代码,定义了一个抽象的基类Shape,然后两个具体的形状类Square和Circle均继承了Shape类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 抽象类
class Shape
{
public:
// 子类方法实现不一致加上virtual
//抽象函数后面加上=0,表示不能被自己实现,只能被派生类实现
virtual double Area() const = 0;
virtual void Show() = 0;

void Display()
{
cout << Area() << endl;
}
};

//正方形的对象,继承Shape
class Square: public Shape
{
public:
Square(double len) :_len(len) { }
void Show() { cout << "Square" << endl; }
double Area() const
{
return _len*_len;
}
private:
double _len;
};


//圆的对象,继承Shape
class Circle : public Shape
{
public:
Circle(double radius) :_radius(radius) {}
void Show() { cout << "Circle" << endl; }
double Area() const
{
return 3.1415926*_radius*_radius;
}

private:
double _radius;
};

下面是负责调用各个类的main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main()
{
// 面对变化,尽可能少修改原有的逻辑,要扩充逻辑
const int shapeNum = 2;

Square s1(2.0);
Circle c1(2.0);

//基类的数组存放派生类的对象
Shape* shapes[shapeNum];
shapes[0] = &s1;
shapes[1] = &c1;

for (unsigned int index = 0; index < shapeNum; index++)
{
shapes[index]->Display();
}

cout << endl;
cout << sizeof(s1) << endl;//s1对象的大小为16

return 0;
}

1. 虚函数和多态

虚函数

对于某些函数,基类希望它的派生类各自定义适合自己的版本,此时基类就将这些函数声明为虚函数。 (《C++Primer》P.526)

如果基类有虚函数, 这样运行的时候,就可以(只)允许基类指针/引用指向派生类对象(为什么可以这样?因为派生类中含有从基类继承的所有内容),同时可以调用相应的派生类成员函数,也就是实现多态(这种是运行时多态,也叫做动态绑定、运行时绑定)。任何构造函数之外的非静态函数都可以是虚函数,但仅限于类内成员函数。

C++语言中,当且仅当我们使用一个基类的指针(或引用)调用一个虚函数时将发生动态绑定。

多态

多态:C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。多态分为编译时多态(静态多态)和运行时多态(动态多态)。编译时多态通过重载函数实现,运行时多态通过虚函数实现。
如果不希望动态绑定,也可以通过作用域运算符强迫执行某个特定的虚函数。例如:
shapes[0]->Shape::Display();

纯虚函数

此外,上面的代码通过virtual function() = 0;的方式定义了基类的纯虚函数。含有纯虚函数的类是抽象基类,而抽象基类不能被实例化生成对象。因此上面不能声明一个Shape对象。
抽象基类的作用是声明一个接口,其派生类必须对这个接口进行具体实现。

C++多态(知乎)
https://zhuanlan.zhihu.com/p/37340242
C++多态(菜鸟教程)
https://www.runoob.com/cplusplus/cpp-polymorphism.html
虚函数与纯虚函数
https://blog.csdn.net/JerryGou/article/details/79051547

2. 对象的大小

虽然Square类的s1对象中数据成员只有一个double,但输出的s1对象的大小为16。
这是因为Square类继承了基类Shape,而Shape类含有两个纯虚函数:
virtual double Area() const = 0;
virtual void Show() = 0;

在运行时,程序将会确定虚函数(纯虚函数)的地址,并将其记录在该对象内。两个地址一共是8字节,加上Square类自己的浮点数数据成员是8字节,因此s1对象一共是16字节。
类对象除了数据成员,还会(也只会)保存一个虚函数表,其中会存放所有虚函数的地址(每个地址4字节),从而便于在调用虚函数时找到其地址。继承于同一个基类的不同派生类,其虚函数表中的地址也不同。
也就是说:对象的大小=基类对象大小+自身数据成员大小+所有虚函数地址的字节数(单位统一为字节),一般成员函数、静态成员(存在全局变量区)不会对对象的大小造成影响。

C++ 类对象大小计算(一)常规情况
https://blog.csdn.net/JerryGou/article/details/79052130
C++ 类对象大小计算(二)含有虚函数类
https://blog.csdn.net/JerryGou/article/details/79052335?spm=1001.2014.3001.5501




总结

面向对象的三大特性解决的问题:

  • 封装性:把问题简化、便于抽象;
  • 继承:减少代码重复,避免重复造轮子,便于复用;
  • 多态:可以实现功能的灵活扩充,提升开发效率。

首先面向对象不是万能的,即使没有面向对象也可以做,但可能效率低一些,例如早期用C语言编写的操作系统;其次,面向对象不一定适用于所有场合。它最大的特点是提升了软件更迭变化过程中的效率,对于每次变化不大的程序,面向对象未必有优势。