【C++私房菜】面向对象中的简单继承

小明 2025-05-03 11:57:50 6

文章目录

  • 一、 继承基本概念
  • 二、派生类对象及派生类向基类的类型转换
  • 三、继承中的公有、私有和受保护的访问控制规则
  • 四、派生类的作用域
  • 五、���承中的静态成员

    一、 继承基本概念

    通过继承(inheritance)联系在一起的类构成一种层次关系。通常在层次关系的根部都有一个基类(base class),其他类则直接或间接地从基类继承而来,这些继承得到的类称为派生类(derived class)。基类负责定义在层次关系中所有类所共同拥有的成员,而每个派生类定义自己特有的成员。

    这个层次结构是如何体现的呢?继承作为面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,也就是派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。上一篇文章中【 C++私房菜】模板的入门与进阶-CSDN博客的都是函数复用,继承是类设计层次的复用。

    继承的定义格式如下:

     class 派生类名 : 继承方式 基类1,继承方式 基类2{ //... }; 

    从上述格式可以看出C++是支持多继承的。派生类必须通过使用 类派生列表(class derivation list) 明确指出它是从哪个(哪些)基类继承而来的。类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有以下三种访问说明符中的一个:public、protected或者private。

    此处我们定义一个基类和派生类来做说明,我们可以看到Person是基类。Student是派生类:

     class Person{ //... }; class Student:public Person{ //... }; 

    派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明。我们观察下文代码:

     class Quote { public: Quote() = default; Quote(const string& book, double sales_price) :bookNo(book), price(sales_price) {} string isBn()const { return bookNo; } virtual double net_price(size_t n)const { return n * price; } virtual ~Quote() = default; protected: double price = 0; private: string bookNo; }; class Bulk_quote :public Quote { public: Bulk_quote() = default; Bulk_quote(const string&, double, size_t, double); double net_price(size_t) const override; private: size_t min_qty = 0; double discount = 0; }; 

    上述代码完成了哪些工作呢?Bulk_quote对象具有以下特征:

    派生类对象存储了积累的数据成员(派生类继承了基类的实现)。

    派生类对象可以使用基类的方法(派生类继承了基类的接口)。

    因此Bulk_quote 类中必须包含一个 net_price 成员。Bulk_quote 类从它的基类继承了 isBn 函数和 bookNo、 price 等数据成员,还定义了新的版本,同时用于两个新增加的数据成员 min_qty 和 discount。这两个成员分别用于说明享受折扣所需购买的最低数量以及一旦该数量达到后具体的折扣信息。

    需要在继承特性中添加什么呢?

    派生类需要自己的构造函数。

    派生类可以根据需要添加额外的数据成员和成员函数。

    上文中只继承自一个类的这种继承被称为“单继承“。

    现在需要记住的是作为继承关系中的根节点的类通常都会定义一个虚析构函数,即使该函数不执行任何实际操作。

    本文我们暂时忽略 virtual 关键字,我将在后续的文章中对此进行叙述。

    当然我们也可以防止继承的发生,有时我们可能会定义一些类且不希望其他类继承它,或者不想考虑它是否适合作为一个基类。

    为了这一目的,C++11提供了一种防止继承发生的方式,即在类名后跟一个关键字 final。 如class NoDerived final{ //... };。或者我们也可以将父类构造函数私有化,派生类实例化不出对象,也就不能被继承。被final修饰的类我们通常称为最终类。

    但是如果我们在派生类定义了一个函数与基类虚函数名字相同但形参列表不同的函数,这仍是合法的行为。编译器将认为这个新定义的函数与基类中的是相互独立的。这时派生类的函数并没有覆盖掉基类中的版本。就实际的编程习惯而言,这种声明往往意味着发生了错误,因为我们可能原本希望派生类能覆盖掉基类中的虚函数,但是一不小心把形参列表弄错了。 要想调试并发现这样的错误显然非常困难。在C++11新标准中我们可以使用 override 关键字来说明派生类中的虚函数。这么做的好处是在使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误,后者在编程实践中显得更加重要。如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错,下面我们举几个例子:

     class Base { public: virtual void f1(int)const; virtual void f2(); void f3(); }; class Derived1 :public Base{ void f1(int)const override; //正确: f1与基类中的 f1 匹配 void f2(int) override; //错误: Base没有形如f2(int)的函数 void f3()override; //错误: f3不是虚函数 void f4()override; //错误: Base没有名为f4的函数 }; 

    因为只有虚函数才会被覆盖,所以编译器会认为 Derived 中的 f3是错误的。相同的 f4 的声明也是错误的,Base中没有为 f4的虚函数。我们在此处将 override与final 一起讨论:

     class Derived2 :public Derived1 { void f1(int)const final; //不允许后续的其他函数覆盖 f1(int) }; class Derived3 :public Derived2 { void f2(); //正确: 覆盖从间接基类Base继承而来的 f void f1(int)const; //错误: Derived2已经将f2声明为final }; 

    ⚠️成员变量所有的都会被继承,无论公有私有。


    二、派生类对象及派生类向基类的类型转换

    理解基类和派生类之间的类型转换是理解C++语言面向对象编程的关键所在。

    一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。因此,一个 Bulk_quote对象将包含四个数据元素:它从基类 Quote 继承而来的 bookNo 和 price 数据成员,以及 Bulk_quote 自己定义的 min_qty 和 discount 成员。

    因为在派生类中含有与基类相对于的组成部分,所以我们可以把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象的基类部分上。

    通常情况下,如果我们想把引用或指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致,或者对象的类型含有一个可接受的 const 类型转换规则。存在继承关系的类是一个重要的例外:我们可以将基类的指针或引用绑定到派生类对象上。例如,我们可以用 Quote& 指向一个 Bulk_quote对象,也可以把一个 Bulk_quote 对象的地址赋给一个Quote*。

     Quote item; Bulk_quote bulk; // 子类对象可以赋值给父类对象/指针/引用 Quote *p = &item; p = &bulk; Quote &r = bulk; Quote obj=bulk; 

    上述代码均是合法的,我们通常把这种转换称为派生类到基类的类型转换。和其他类型转换一样,编译器会隐式地执行此种转换。

    这种隐式特性意味着我们可以把派生类对象或者派生类对象的引用用在需要基类引用的地方;同样的,我们也可以把派生类对象的指针用在需要基类指针的地方。

    ⚠️注意:

    派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。

    基类对象不能赋值给派生类对象。

    在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。

    但是在对象之间并不存在类型转换,基类向派生类的隐式类型转换也不存在。为什么呢?

    派生类向基类的自动类型转换只对指针或引用类型有效。在派生类类型和基类类型之间不存在这样的转换。很多时候,我们确实希望派生类对象转换成它的基类类型,但是这种转换的实际发生过程往往与我们期望的有所差别。

    请注意,当我们初始化或赋值一个类类型的对象时,实际上是在调用某个函数。当执行初始化时,我们调用构造函数;而当执行赋值操作时,我们调用赋值运算符。这些成员通常都包含一个参数,该参数的类型是类类型的 const 版本的引用。 因为这些成员接受引用作为参数,所以派生类向基类的转换允许我们给基类的传递一个派生类的对象。这些操作不是虚函数。当我们给基类的构造函数传递一个派生类对象时,实际运行的构造函数是基类中定义的那个,显然该构造函数只能处理基类自己的成员。类似的,如果我们将一个派生类对象赋值给一个基类对象,则实际运行的赋值运算符也是基类中定义的那个,该运算符同样只能处理基类自己的成员。

     Bulk_quote bulk; //派生类对象 Quote item(bulk); //使用Quote::Quote(const Quote&)构造函数 item =bulk; //调用Quote::operator=(const Quote&) 

    当构造 item 时,运行 Quote 的拷贝构造函数。该函数只能处理 bookNo 和 price 两个成员,它负责拷贝 bulk 中Quote部分的成员,同时忽略掉 bulk 中 Bulk_quote 部分的成员。类似的,对于将bulk赋值给item的操作来说,只有bulk中Quote部分的成员被赋值给 item。因为在上述过程中会忽略 Bulk_quote 部分,所以我们可以说 bulk 的 Bulk_quote 部分被切割掉了,这就是派生类向基类赋值的过程。

    之所以存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类部分上。一个基类的对象既可以以独立的形式存在,也可以作为派生类对象的一部分存在。如果基类对象不是派生类对象的一部分,则它只含有基类定义的成员,而不含有派生类定义的成员。因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换。

     Quote base; Bulk_quote* bulkP=&base; //错误,不能将基类转换成派生类 Bulk_quote& bulkRef= base; //错误,不能将基类转换成派生类 Bulk_quote bulk; Quote *itemP =&bulk; //正确,动态类型是 Bulk_quote Bulk_quote *bulkP=itemP; //错误,不能将基类转换成派生类 

    如若此方式合法,则我们可能会使用到 bulkP 或 bulkRef 访问 base 中不存在的成员。

    当我们使用存在继承关系的类型时,必须将一个变量或其他表达式的**静态类型(static type)与该表达式表示对象的动态类型(dynamic type)**区分开来。表达式的静态类型是编译时总是已知的,它是变量声明时的类型或表达式生成的类型:动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。此部分我们将在后文多态中详细介绍。


    三、继承中的公有、私有和受保护的访问控制规则

    每个类分别控制自己的成员初始化过程,与之类似,每个类还分别控制着其成员对于派生类来说是否是可访问的。继承方式有三种分别为 public继承、protected继承和private继承。访问限定符同样也是三种: public访问、protected访问和private访问。

    protected成员:如前所述,一个类使用protected关键字来声明那些它希望与派生类分享但是不想被其他公共访问使用的成员。protected说明符可以看做是public和private中和后的产物。

    和私有成员类似,受保护的成员对于类的用户来说是不可访问的。

    和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。

    派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。

    我们举个例子来说明:

     class Base { protected: int prot_mem; }; class Derived :public Base { friend void clobber(Derived&); friend void clobber(Base&); private: int j; }; //错误: clobber不能访问Base的对象的private和protected成员 void clobber(Base& b) { b.prot_mem = 0; } //正确: clobber可以访问Derived的对象的private和protected成员 void clobber(Derived& s) { s.j = s.prot_mem = 0; } 

    某个类对其继承而来的成员的访问权限受到两个因素影响:一是在基类中该成员的访问说明符,二是在派生类的派生列表中的访问说明符。举个例子,考虑如下的继承关系:

     class Base { public: void pub_mem(); //public成员 protected: int prot_mem; //protected成员 private: char priv_mem; //private成员 }; struct Pub_Derv :public Base { int f() { return prot_mem; } //正确:派生类能访问protected成员 char g() { return priv_mem; } //错误:private成员对于派生类来说是不可访问的 }; struct Priv_Derv :private Base { int fl()const { return prot_mem; } //private 不影响派生类的访问权限 }; 

    派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。对基类成员的访问权限只与基类中的访问说明符有关。PubDerv和PrivDerv都能访问受保护的成员protmem,同时它们都不能访问私有成员privmem。 派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限:

     Pub_Derv dl; //继承自Base的成员是public的 Priv_Derv d2; //继承自Base的成员是private的 d1.pub_mem(); //正确:pub mem在派生类中是public的 d2.pub_mem(); //错误:pub mem在派生类中是private的 

    Pub_Derv和Priv_Derv都继承了pub_mem函数。如果继承是公有的,则成员将遵循其原有的访问说明符,此时d1可以调用pub_mem。在Priv_Derv中,Base 的成员是私有的,因此类的用户不能调用pub_mem。 上述内容总结成如下内容:

    特征类成员/继承方式public继承protected继承private继承
    public成员变量派生类的public成员派生类的protected成员派生类的private成员
    protected成员变量派生类的protected成员派生类的protected成员派生类的private成员
    private成员变量在派生类中不可见,只能通过基类接口访问在派生类中不可见,只能通过基类接口访问在派生类中不可见,只能通过基类接口访问
    能否隐式向上转换是(但只能在派生类中)

    从上述表格我们可以观察到:

    基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。

    实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 = Min(成员在基类的访问限定符,继承方式),public > protected > private。

    使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public。

    派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的访问说明符也会由影响。假定 D 继承自 B:

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

     class A { public: virtual void print() { cout 
The End
微信