C++ 基础 - 面向对象

面向对象

面向对象程序设计(Object-oriented programming,OOP)是种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。

面向对象基本特征

面向对象三大特征 —— 封装、继承、多态

封装

把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

关键字:public, protected, private。不写默认为 private。

  • public 成员:可以被任意实体访问
  • protected 成员:只允许被子类及本类的成员函数访问
  • private 成员:只允许被本类的成员函数、友元类或友元函数访问

继承

  • 基类(父类)——> 派生类(子类)

多态

  • 多态,即多种状态(形态)。将多态定义为消息以多种形式显示的能力。
  • 多态是以封装和继承为基础的。
  • C++ 多态分类及实现:
    1. 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
    2. 子类型多态(Subtype Polymorphism,运行期):虚函数
    3. 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
    4. 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换

静态多态(编译期/早绑定)

函数重载

class A
{
public:
    void fun(int a);
    void fun(int a, int b);
};
1
2
3
4
5
6

动态多态(运行期期/晚绑定)

  • 虚函数:用 virtual 修饰成员函数,使其成为虚函数

注意

  • 普通函数(非类成员函数)不能是虚函数
  • 静态函数(static)不能是虚函数
  • 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针
  • 内联函数不能是表现多态性时的虚函数

动态多态使用

DETAILS
#include <iostream>

// 形状类
class Shape
{
public:
  virtual double calcArea()
  {
    std::cout << "Shape::calcArea" << std::endl;
  }
  virtual ~Shape() {}
};

// 圆形类
class Circle : public Shape
{
public:
  Circle(double r) : _r(r)
  {
  }
  ~Circle()
  {
  }
  virtual double calcArea()
  {
    std::cout << "Circle::calcArea:" << 3.14 * _r * _r << std::endl;
  }

private:
  double _r;
};

// 矩形类
class Rectangle : public Shape
{
public:
  Rectangle(double x, double y) : _x(x), _y(y)
  {
  }
  ~Rectangle()
  {
  }
  virtual double calcArea()
  {
    std::cout << "Rectangle::calcArea:" << _x * _y << std::endl;
  }

private:
  double _x;
  double _y;
};

int main()
{
  Shape *shape1 = new Circle(4.0);         // Circle::calcArea:50.24
  Shape *shape2 = new Rectangle(5.0, 6.0); // Rectangle::calcArea30
  shape1->calcArea();                      // 调用圆形类里面的方法
  shape2->calcArea();                      // 调用矩形类里面的方法
  delete shape1;
  shape1 = nullptr;
  delete shape2;
  shape2 = nullptr;
  return 0;
}
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

虚析构函数

虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象

虚析构函数使用

DETAILS
#include <iostream>

// 形状类
class Shape
{
public:
  virtual double calcArea()
  {
    std::cout << "Shape::calcArea" << std::endl;
  }
  Shape() { std::cout << "Shape::Shape" << std::endl; }
  virtual ~Shape() { std::cout << "Shape::~Shape" << std::endl; }
};

// 圆形类
class Circle : public Shape
{
public:
  Circle(double r) : _r(r)
  {
    std::cout << "Circle::Circle" << std::endl;
  }
  ~Circle()
  {
    std::cout << "Circle::~Circle" << std::endl;
  }
  virtual double calcArea()
  {
    std::cout << "Circle::calcArea:" << 3.14 * _r * _r << std::endl;
  }

private:
  double _r;
};

int main()
{
  Shape *shape1 = new Circle(4.0);
  shape1->calcArea();
  delete shape1; // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
  shape1 = nullptr;
  return 0;
}
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

运行结果:

DETAILS
[root@iZuf61kbf845xt6tz10abgZ c17]# ./virtual_shape
Shape::Shape
Circle::Circle
Circle::calcArea:50.24
Circle::~Circle
Shape::~Shape
1
2
3
4
5

纯虚函数

纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。

virtual int fun() = 0;
1

虚函数、纯虚函数

  • 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖(override),这样的话,编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
  • 虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。
  • 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
  • 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
  • 虚基类是虚继承中的基类,具体见下文虚继承。

虚函数指针、虚函数表

  • 虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。
  • 虚函数表:在程序只读数据段,存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。

虚继承

虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。

底层实现原理与编译器相关,一般通过虚基类指针虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

虚继承、虚函数

  • 相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
  • 不同之处:
    • 虚继承
      • 虚基类依旧存在继承类中,只占用存储空间
      • 虚基类表存储的是虚基类相对直接继承类的偏移
    • 虚函数
      • 虚函数不占用存储空间
      • 虚函数表存储的是虚函数地址

模板类、成员模板、虚函数

  • 模板类中可以使用虚函数
  • 一个类(无论是普通类还是类模板)的成员模板(本身是模板的成员函数)不能是虚函数

抽象类、接口类、聚合类

  • 抽象类:含有纯虚函数的类
  • 接口类:仅含有纯虚函数的抽象类
  • 聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:
    • 所有成员都是 public
    • 没有定义任何构造函数
    • 没有类内初始化
    • 没有基类,也没有 virtual 函数