C++封装继承与多态

 

与其他语言的区别

与C区别

区别 C C++
头文件 .h .h .hpp
命名空间 不支持 支持
空间分配 malloc、free、calloc、realloc new、delete、malloc、free
引用 不支持 支持
struct 仅为变量;为private;无法初始化、定义别名与使用模板 支持函数;为public;支持初始化、定义别名与使用模板
auto、explicit 不支持 支持
重载与虚函数 不支持 支持
dynamic_cast 不支持 支持
模板 不支持 支持、STL

与Java区别

区别 Java C++
指针 无法访问 支持访问
继承 不支持多重继承、但支持多个接口 支持多重继承,接口类似虚函数概念
函数与变量 除基本类型外,都是类的函数 支持类外函数与变量
struct与union 不支持 支持
内存管理 均为new出来的,回收自动 支持malloc、free、new、delete
操作符重载 不支持 支持
预编译 不支持 支持
字符串 类对象实现 结尾以null为终止符
异常处理机制 try-catch-finally、类型确定、无返回码 try-catch、类型不确定、有返回码

类的封装、继承与多态

类的封装

封装原因

  • 结合:将属性与方法结合;
  • 接口:利用接口机制隐藏内部实现细节,只留下接口供外部调用;
  • 复用:实现代码复用

类的大小

  class A{}; sizeof(A) = 1; 
  class A{virtual Fun(){} }; sizeof(A) = 4(32bit)/8(64bit) 
  class A{static int a; }; sizeof(A) = 1;
  class A{int a; }; sizeof(A) = 4;
  class A{static int a; int b; }; sizeof(A) = 4;

类的构造函数

构造类型

  • 默认构造:无参构造
  • 一般构造:包含各种参数,参数顺序个数不同,可以有不同构造
  • 拷贝构造:
    • 出现环境:对象以值传递方式进入函数体、以值传递的方式从函数返回、一个对象需要另外对象初始化
    • 函数参数必须为引用
    • 如果是值传递,作为参数需要构造生成副本,再次调用拷贝构造,形成无限递归
    • 浅拷贝,存在问题(当有指针的时候,会有两个指针指向相同的位置),进行重写(申请空间存储数据)
  • 移动构造
    • 避免分配新空间,将原来的对象直接拿过来使用
  • 赋值运算符的重载
    • 类似构造函数,将右边的对象赋值给左边的对象,但不属于构造函数
    • 要求两边对象创建
  • 类型转换构造
    • 根据指定类型的对象,构造一个本类的对象。
    • 如果不想默认转换,需要使用explicit阻止隐式转换

构造中构造与析构函数是否可抛出异常

构造函数

  • C++ 只会析构已经完成的对象,对象只有在其构造函数执⾏完毕才算是完全构造妥当。在构造函数中发⽣异常,控制权转出构造函数之外。
  • 因此,在对象 b 的构造函数中发⽣异常,对象b的析构函数不会被调⽤。因此会造成内存泄漏
  • ⽤ auto_ptr 对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时发⽣资源泄漏的危机,不再需要在析构函数中⼿动释放资源;

析构函数

  • 如果控制权基于异常的因素离开析构函数,⽽此时正有另⼀个异常处于作⽤状态,C++ 会调⽤ terminate 函数让程序结束
  • 如果异常从析构函数抛出,⽽且没有在当地进⾏捕捉,那个析构函数便是执⾏不全的。如果析构函数执⾏不全,就是没有完成他应该执⾏的每⼀件事情

构造中的问题

构造中的内存泄漏问题

  • 原因:c++只会析构已经完成的对象。
  • 出现:如果构造函数中发生异常,不会调用析构函数。如果在构造函数中申请了内存操作,则会造成内存泄漏。
  • 派生类有问题:如果有继承关系,派生类中的构造函数抛出异常,那么基类的构造函数和析构函数可以照常执行的。
  • 解决办法:用智能指针来管理内存

拷贝构造中的浅拷贝和深拷贝

使用深拷贝的场景

  • 在copy构造中,copy的对象是否存在指针,如果有需要重写copy构造,因为浅拷贝不会存储数据,相同指针指向同一对象,当数据成员中有指针时,必须要用深拷贝。

系统默认

系统默认的拷贝函数——即浅拷贝。当数据成员中没有指针时,浅拷贝是可行的;

原因 如果没有自定义拷贝构造函数,会调用默认拷贝构造函数,这样就会调用两次析构函数。第一次析构函数delete了内存,第二次的就指针悬挂了。所以,此时,必须采用深拷贝。

操作

  • 深拷贝在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。 简而言之,当数据成员中有指针时,必须要用深拷贝

类的析构函数

析构与构造不同的是,构造只用于初始化参数,析构除了撤销对象外,一般还用于解决对象分配的内存空间等。 析构中的问题

  • 析构函数不能、也不应该抛出异常
    • 析构函数抛出异常,则异常点之后的程序不会执行,造成资源泄露
    • 异常发生时,异的传播过程中会进行栈展开。调用已经在栈构造好的对象的析构函数来释放资源,此时若其他析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃。
      • 解决办法:把异常完全封装在析构函数内部,决不让异常抛出函数之外

继承

继承原因

  • 接口:使用所有非私有方法
  • 复用:继承属性与方法,减少代码的书写
  • 重写:重写父类的方法以增加子类的功能

继承类型

  • 单一继承:继承一个父类,最多使用
  • 多重继承:一个类有多个基类,类之间使用逗号隔开
  • 菱形继承:BC继承自A,D继承自BC

继承缺点

  • 耦合:耦合性太大
  • 封装:破坏了类的封装性
  • 解决:使用抽象方法的继承和接口

class与struct的默认继承方式

  • class默认private继承
  • struct默认public继承

不能继承的类或函数

  • final
  • 赋值运算符=
  • 友元类/友元函数不能继承
  • 虚继承
  • 构造函数
    • 列表初始化与默认构造:子类构造函数使用成员列表初始化来调用父类构造函数以创建派生类中的基类部分。如果子类没有使用成员列表初始化,默认使用默认基类构造函数,如果没有默认构造函数会报错。
    • 父类属性:在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有private属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。
  • 析构函数
  • 单例模式中将自身构造和析构放在private作用域中不能继承

类与类之间关系

  • has:包含关系
  • use:friend关系
  • is:继承关系

继承控制方式对属性的影响

  • public继承 -> 不改变基类的访问权限
  • protected继承 -> 将父类public成员变为子类protected成员,其他保持不变
  • private继承 -> 不受继承方式的影响,子类永远无权访问

组合

  • 定义:⼀个类⾥⾯的数据成员是另⼀个类的对象,即内嵌其他类的对象作为⾃⼰的成员
  • 组合对象的构建:⾸先创建各个内嵌对象,难点在于构造函数的设计。创建对象时既要对基本类型的成员进⾏初始化,⼜要对内嵌对象进⾏初始化
  • 组合对象的构建顺序:先调⽤内嵌对象的构造函数,然后按照内嵌对象成员在组合类中的定义顺序,与组合类构造函数的初始化列表顺序⽆关。然后执⾏组合类构造函数的函数体,析构函数调⽤顺序相反。

多态

多态原因

  • 运行时的调用函数需要根据编译时类型决定程序最终执行过程的真实调用方法

多态种类

  • 静态多态:编译期间确定。不需要基类,只需要在各个具体实现中要求相同的接口名称
    • 重载、模板函数
  • 动态多态:继承+虚函数实现。程序运行时确定。确定子父类共同功能,在父类中,将功能声明为虚函数,子类重写虚函数,完成具体功能
    • 虚函数、基类引用指向子类对象

多态实现

  • 重载:编译期实现
  • 类、函数模板:编译期
  • 虚函数:运行期

动态多态的实现

动态多态通过继承+虚函数实现

原理

  • 对象中有虚函数的指向的指向(编译期间创建对象或运行时创建对象时创建)和虚函数表(每个类的虚函数入口地址,为编译期创建)

  • 管理对象的空间中有vptr地址(随对象创建而创建),vptr指针对应的vtable(在编译期确定,是针对类的)中保存该对象的虚函数成员,其保存函数的入口地址

多继承

  • 在多继承中,vtable会有多个vptr地址,对应不同基函数的vptr

运行时virtual

  • 为了多态,编译器会给每个包含虚函数或继承了虚函数的类自动建立一个虚函数表,当子类继承父类的虚函数时,子类会有自己的vtable
    • 如果存在大量的子类继承,且重写父类的虚函数接口只占总数的一小部分的情况下,会造成大量地址空间浪费

多态的实现原理(为什么构造和析构需要按顺序)

虚表的写入时机、多态的实现原理、构造析构顺序的原因

构造函数与virtual

构造函数是否可以为virtual

  • 不能
  • 虚函数调用需要对象构建得到虚表调用,而对象还没有构造。

构造函数中调用virtual

  • 首先创建派生类的基类部分,执行基类构造,由于派生类没有初始化,所以c++当作不存在,仅仅将其认为是基类的对象。

析构函数与virtual

析构函数的子类应该声明为virtual

  • 为了确保析构的时候,释放派生类对象,需要基类析构函数声明为虚函数,否则只会析构对应的父类对象,而不会析构子类对象。 析构函数中调用virtual
  • 仅仅将其认为是基类的对象。

哪些函数不能是虚函数

  • 构造函数
  • 某些析构函数
  • 友元函数(原因不是类成员)
  • 静态成员函数(原因:不属于任何对象或实例)
  • 内联函数(原因:需要在编译期间展开,同时需要类对象有vptr,但没有地址)
  • 成员函数模板(原因:成员模板函数需要在调用的时候才能确定,而虚函数需要解析时候确定vtable大小)

动态多态中的纯虚函数

区别

  • 纯虚函数用于如果生成基类对象则不合理的场景
  • 其使得纯虚函数的类为抽象基类,本身成为了接口

使用 virtual void exit()=0=0表示为纯虚函数

重载、重写和重定义

  • 重载:同一可访问区内被声明的几个具有不同参数列表的同名函数,参数类型、个数、顺序可以不同
  • 重写:重新定义父类中除函数体外完全相同的虚函数,访问修饰符可以不同,virtual中是private,派生类可以是public
  • 重定义:派生类重新定义父类相同名字的非virtual函数,参数列表和返回类型可以不同。此时,父类函数被隐藏,如果不想被隐藏,需要将其定义为virtual并且完全同名