天天看点

面向对象程序设计一、基础知识二、定义基类和派生类三、虚函数四、抽象基类五、访问控制与继承六、继承中的类作用域七、构造函数与拷贝控制

一、基础知识

  • 继承,表示has-a(是一种),即派生类也是一种基类。
  • 继承关系下,派生类继承了基类的成员变量,继承了基类成员函数的调用权。
  • 派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。
  • 类派生列表的形式是:首先是一冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面有访问说明符。

二、定义基类和派生类

(一)定义基类

  • 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。

(1)成员函数与继承

  • 对于成员函数,如果基类不希望派生类自定义,则声明为non-virtual函数,即普通函数;如果基类希望派生类自定义,且派生类有默认定义,则声明为virtual函数;如果基类要求派生类自定义,则声明为纯虚函数。

(2)访问控制与继承

  • 派生类能访问公有成员,不能访问私有成员。
  • 如果基类希望它的派生类有权访问某成员,同时禁止其他用户访问,可以将访问权限设置为protected。

(二)定义派生类

(1)派生类中的虚函数

  • 派生类可以在它覆盖的函数前使用virtual关键字,但不是非得这样做。
  • C++11新标准允许向派生类显式地注明它使用某个成员函数覆盖了它继承的虚函数。具体做法是在形参列表后面、或者在const成员函数的const关键字的后面、或者在引用成员函数的引用限定符后面添加一个关键字override。

(2)派生类对象及派生类向基类的类型转换

  • 派生类也是一种基类,派生类继承了基类的成员变量和成员函数的调用权。
  • 因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当作基类来使用,而且能将基类的指针或引用绑定到派生类对象中的基类部分上。这种转换通常称为派生类到基类的类型转换,编译器会隐式地执行这种转换。

(3)派生类构造函数

  • 派生类必须使用基类的构造函数来初始化它的基类部分。
  • 除非我们特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化。
  • 先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

(4)派生类使用基类的成员

  • 派生类可以访问基类的公有成员和受保护成员。

(5)继承与静态成员

  • 如果基类定义了一静态成员,则在整个继承体系中只存在该成员的唯一定义。

(6)派生类的声明

  • 派生类的声明与其他类差别不大,声明中包含类名但是不包含它的派生列表。
class Base{…};
class Derived :public Base; //错误:派生列表不能出现在这里
class Derived;              //正确
           

(7)被用作基类的类

  • 如果我们将某个类用作基类,则该类必须已经定义而非仅仅声明。
  • 因为派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类要知道它们时什么。

(8)防止继承的发生

  • C++11新标准提供了一种防止继承发生的方法——在类名后跟一个关键字final。

(三)类型转换与继承

  • 我们可以将基类的指针或引用绑定到派生类的对象上。
  • 当使用基类的引用(或指针)时,实际上我们并不清楚该引用(或指针)所绑定对象的真实类型。

(1)静态类型和动态类型

  • 表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型。
  • 动态类型则是变量或表达式表示的内存中的对象的类型,动态类型直到运行时才可知。

(2)类型转换

  • 不存在基类向派生类的类型转换。
  • 在对象之间不存在类型转换。派生类向基类的自动类型转换只对指针或引用有效,在派生类类型和基类类型之间不存在这样的转换。
  • 当用派生类对象初始化基类对象时,实际上用的是派生类对象中的基类部分去初始化基类对象。

三、虚函数

  • 当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。
  • 因为我们直到运行时才能知道到底调用了哪个版本的虚函数,所以虚函数必须都有定义。

(一)对虚函数的调用可能在运行时才被解析

  • 当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。
  • 当用引用或者指针类型调用虚函数且是up-cast时,在程序运行时才能确定下来。

(二)派生类中的虚函数

  • 一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
  • 派生类中虚函数的返回类型也必须与基类函数匹配。但当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。

(三)final和override说明符

  • 如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器会报错。
  • 如果我们把某个函数指定为final,则之后任何尝试覆盖该函数的操作都将引发错误。
  • final和override说明符出现在形参列表(包括任何const或引用修饰符)以及尾置返回类型之后。

(四)虚函数与默认实参

  • 虚函数也可以拥有默认实参,如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。
  • 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。

(五)回避虚函数的机制

  • 在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本,使用作用域运算符可以实现这一目的。

四、抽象基类

  • 在虚函数声明语句的分号之前书写=0就可以将一个虚函数声明为纯虚函数,且=0只能出现在类内部的虚函数声明语句处。
  • 我们可以为纯虚函数提供定义,不过函数体必须定义在类的外部。

(一)含有纯虚函数的类是抽象基类

  • 抽象基类负责定义接口,后续的其他类可以覆盖该接口。
  • 不能直接创建抽象基类的对象。

(二)派生类构造函数只初始化它的直接基类

五、访问控制与继承

(一)受保护的成员

  • 受保护的成员对于类的用户来说是不可访问的。
  • 受保护的对于派生类的成员和友元是可以访问的。
  • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。

(二)派生类向基类转换的可访问性

假定D继承自B:

  • 只有当D公有地继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是受保护的或者私有的,则用户代码不能使用该转换。
#include<iostream>

using namespace std;

class A {
public:
    virtual void print() { cout << "I'm A"; }
};

class B : public A {
public:
    void print() override { cout << "I'm B"; }
};

class C : private A {
public:
    void print() override { cout << "I'm C"; }
};

int main() {
    A *p1, *p2;
    B b;
    C c;

    p1 = &b;
    p2 = &c; //Cannot cast 'C' to its private base class 'A'

    return 0;
}

           
  • 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其基类的类型转换对于派生类的成员和友元来说永远是可访问的。
#include<iostream>

using namespace std;

class A {
public:
    void Print() { print(); }

    virtual void print() { cout << "I'm A"; }
};

class B : public A {
public:
    void print() override { cout << "I'm B"; }
};

class C : private A {
public:
    void Print_c() { Print(); }
    void print() override { cout << "I'm C"; }
};

int main() {
    C c;
    c.Print_c(); //C会调用A中的Print();
                 //Print实现up-cast,Print会调用print虚函数;
                 //从而实现动态绑定    

    return 0;
}

           
  • 如果D继承B的方式是公有的或者受保护的,则D的派生类的成员和友元可以使用D向B的类型转换;反之,如果D继承B的方式是私有的,则不能使用。

(三)友元与继承

  • 友元关系不能继承,基类的友元在访问派生类成员时不具有特殊性。
  • 类似的,派生类的友元也不能随意访问基类的成员。

(四)改变个别成员的可访问性

  • 可以通过using声明改变派生类继承的某个名字访问级别。
#include<iostream>

using namespace std;

class Base {
public:
    size_t size() const { return n; }
protected:
    size_t n;
};

class Derived : private Base {
public:
    using Base::size;
protected:
    using Base::n;
};

int main() {


    system("pause");
    return 0;
}
           
  • using声明语句中名字的访问权限由该using声明语句之前的访问说明符决定。
  • 派生类只能为那些它能访问的名字提供using声明。

(五)默认的继承保护级别

  • 默认情况下,使用class关键字定义的派生类是私有继承的;而使用struct关键字定义的派生类是公有继承的。
class Base {};
struct D1:Base{}; //默认public继承
class  D2:Base{}; //默认private继承
           

六、继承中的类作用域

  • 每个类定义自己的作用域,在这个作用域内我们定义类的成员。
  • 当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
  • 如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在基类的作用域内继续寻找。

(一)名字冲突与继承

  • 派生类能重用定义在其直接基类或间接基类中名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在其外层作用域(即基类)的名字。
  • 我们可以通过作用域运算符来使用一个被隐藏的基类成员。
  • 声明在内层作用域的函数并不会重载声明在外层作用域的函数,因此,定义派生类中的函数也不会重载其基类中的成员,即使派生类成员和基类成员的形参列表不一致,基类成员仍然会被隐藏的。

七、构造函数与拷贝控制

(一)虚析构函数

(二)合成拷贝控制与继承

(三)派生类的拷贝控制成员

(四)继承的构造函数

继续阅读