《重学C++》7. C++高级语法(三) 其他问题:头文件、深浅拷贝、多态和虚函数
1. 其他工程问题
头文件重复包含问题
为了防止同一个文件被include多次,有两种方式避免:
可以使用宏定义方式进行避免:
1
2
3
4
5
6#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
//to do
#end if这种宏定义的方式优点:可移植性好;缺点:无法防止宏名重复(宏名重复时会出错),难以排错。
使用编译器的方式
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 | // 抽象类 |
下面是负责调用各个类的main函数:
1 | int main() |
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语言编写的操作系统;其次,面向对象不一定适用于所有场合。它最大的特点是提升了软件更迭变化过程中的效率,对于每次变化不大的程序,面向对象未必有优势。