Object Oriented Programming in C++

1. Basic Ideas (Legacy)

在课堂中,我们学过 OOP 的三大特征,即封装继承多态。但时过境迁,OOP 的编程范式随着各种新技术的出现而不断更新。在这个文档中,我们会概述现代的 OOP 编程(A best practice)。

1.1 Encapsulation

Separation from interface and implementation.

1.1.1 How Encapsulation is Implemented

我们先从最基本的封装性谈起。C++ 用 “类类型 (class types)” 来对数据进行封装。封装很好理解,即将类内成员变量和函数与外界分开。不像在 C 语言中,函数的实现光溜溜地暴露在全局范围  (global scope) 。在 C++ 中,我们可以用类类型将这些函数的实现封装起来,只暴露使用的接口。

C++提供三种类类型:classstructunion,而只有前两者提供对数据的有效封装性,我们忽略union类型。

1.2 Inheritance

Don’t inherit for code reuse. Inherit, when you want to express a logical structure.

1.2.1 The Inheriting Class ("is-a" principal)

继承是对一个类的继承。当我们继承一个类得到新的类时,我们需要明白,在不同的继承访问控制修饰符下,成员变量的可见性和访问权限是怎么样的。

class Account{
public:
  int pub{0};
protected:
  int prot{0};
private:
  int pri{0};
};
class PubAccount: public Account{
public:
  PubAccount(){
    pub + prot;  // public + protected
  }
};
class ProtAccount: protected Account{
public:
  ProtAccount(){
    pub + prot;  // protected + protected
  }
};
class PriAccount: private Account{
public:
  PriAccount(){
    pub + prot;  // private + private
  }
};

int main(){
  PubAccount pubAccount;
  ProtAccount proAccount;
  PriAccount priAccount;
  pubAccount.pub;
}

为了封装性,基类的私有成员是不可以被继承到派生类中的。上面的例子中,展示了基类的不同成员的访问权限在不同继承方式下的访问权限。

1.2.2 class and struct

在C++中,classstruct 的区别就是 struct 因为C兼容的缘故,默认的成员变量和成员函数是 public 的,而不像 class 中的 private。还有一个区别就是继承时,struct 默认的继承方式为 public ,而 class 默认的继承方式是 private 。这就是它们的区别。

1.2.3 A Guideline: Make Non-Leaf Class Abstract

Make every class in your hierarchy either a base-only or leaf only.

1.3 Polymorphism

The separation of the interface and its implementation is one of the crucial ideas of modern software design.

1.3.1 Poly-morph-ism (Many Shapes)

多-态,即一种物体的多种状态。多态分为编译时多态性运行时多态性。在OOP中,通常指后者。

1.3.2 Compile-Time Polymorphism

也称为静态多态性,主要通过函数的重载和模板实现。C语言并不支持相同函数名的重载,C++通过将参数类型也作为符号名的一部分来支持函数的重载。如plus(int i)的符号名可能是_Z4plusi,最后面的i就表示有一个int类型的参数。如果在类中可能是这样的_ZN5Class4plus

由于这种多态性在编译期就确定下来了,所以效率要高一点。由于这种在编译时就确定的多态性,有时并不将静态多态性看作是真正意义上的多态。我们用模板的代码简单演示一下。

#include <iostream>

template<typename T>
T plus(T x){
    return x+1;
}
int main(){
    auto y = plus(20);
    auto z = plus(3.14);
    auto e = plus('c');
    return 0;
}

由于模板只有在调用相应类型的时候才会实例化,所以在这段代码中,我们在编写代码时就能说出来编译后会相应地产生三个关于 plus 的函数符号。而且这三个符号是独立的,链接时并不将其看作一个函数看待。

编译完成后查看符号表,我们确实看到了三个不同的函数符号:

du@DVM:~/Desktop/DSA$ nm -n stat_poly
                 U __cxa_atexit
                 U __dso_handle
                 U _GLOBAL_OFFSET_TABLE_
                 U _ZNSt8ios_base4InitC1Ev
                 U _ZNSt8ios_base4InitD1Ev
0000000000000000 T main
0000000000000000 W _Z4plusIcET_S0_
0000000000000000 W _Z4plusIdET_S0_
0000000000000000 W _Z4plusIiET_S0_
0000000000000000 b _ZStL8__ioinit
0000000000000047 t _Z41__static_initialization_and_destruction_0ii
000000000000009d t _GLOBAL__sub_I_main

1.3.3 Run-Time Polymorphism

也称动态多态性,通过虚函数和继承来实现。由于虚多态只有在程序运行时才能确定实际调用的时哪个函数,所以效率较低。(存储虚表造成的空间复杂度和虚指针索引导致的时间复杂度增加)

相关请参阅Virtual Dispatch in C++

2. Polymorphism in OOP

OOP is a programming paradigm in C++ using polymorphism based on runtime function dispatch using virtual functions.

2.1 Objects as Libraries

在OOP范式中,由于派生类是基类派生而来的,所以它们的库所用的API是相同的。通过创建对象将派生类实例化,每个对象可以具有不同的状态和行为。这种特性就是由"virtual"所提供的动态多态性。即基类定义的API可以被派生类对象重写,从而在不同对象中表现出不同的实现。在编译期(compile time),确定派生对象的类型。在运行时(runtime),我们才能知道派生类的状态。