C++中的继承与多态

本文详细探讨了C++中的继承概念,包括继承的种类、派生类的构造函数和析构函数、多重继承、虚基类、虚函数、纯虚函数以及抽象类。此外,还讲解了派生类如何初始化基类成员、如何处理同名成员的二义性,以及虚函数在多态中的作用。通过对这些概念的深入理解,读者能够更好地掌握C++的面向对象编程技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是继承、如何实现继承

        继承是面向程序设计的一个重要特性,它允许从一个或者几个基类中继承它们的数据成员和成员函数,并再次基础上增加自己的成员,通过继承, 可以吸收现有类的数据和函数来创建新类, 并增添新的成员增强此类, 这样可以节约程序开发的时间。C++中的继承其实就是在一个已存在类的基础上构建一个新的类,新的类称为派生类原来的类称为基类,这种派生类从基类上获得特性的现象称为继承。

        派生可以分为单级派生和多级派生,也可从继承的角度分为单一继承成多重继承,他们的关系如下:

 

         派生类的定义形式如下:

//定义派生类的一般形式
class 派生类名:派生类列表{
    成员列表
}
//举例子
#include <iostream>
using namespace std;
class Base //基类
{
private:
int b_number;
public:
Base( ){} //基类不带参数的构造函数
Base(int i) : b_number (i) { } //基类带参数的构造函数
int get_number( ) {return b_number;}
void print( ) {cout << b_number << endl;}
};
class Derived : public Base //派生类
{
private:
int d_number; //派生类增加的数据成员
public:
Derived(int i,int j):Base(i),d_number(j){};//派生类构造函数
void print( )//派生类增加的成员函数
{
cout << get_number() << " "; //派生类继承的成员函数
cout << d_number << endl;
}
};

 派生类的构成与访问

        我们在学习派生类的时候应该时刻把握住派生类的本质,及派生类也是一个类,所以类中涉及到的问题派生类中也会存在,但是不同的是派生类中还要考虑与基类的关系,比如对成员的访问,派生类能否访问基类成员,构造函数是怎样的调用的,派生类的构造函数中如何初始化基类的数据成员等等问题。本小节我们先介绍派生类怎么的一些基本说明和访问权限。

        派生类实在基类的基础上对他进行扩展,所以在定义派生类的时候基类必须是已经定义的,派生类继承了基类的数据成员和函数,但是有哪些是不能继承的呢?首先是友元关系,基类的友元只能访问基类的成员而不能访问派生类。其次是静态成员,静态成员在整个继承关系中只有一个成员,无论多少个派生类,静态成员只能有一个实例;第三是构造函数和析构函数,他们也不会被继承。

         类一共有三种继承方式,分别是公有,保护和私有继承(实际使用中多实用公有继承,剩下两种意义不大),他们所控制的就是派生类中对基类成员的访问。类的保护成员和私有成员对于类用户没有什么差异,都是不可访问的,但是保护成员可以被派生类访问,而私有成员是不可以被派生类访问的,类之间的访问关系如下图:

         那么不同的继承方式是如何影响派生类对基类成员的访问呢?

  • 公有继承(public inheritance)

        基类的公有成员和保护成员在派生类中保持原有访问属性, 私有成员仍为基类私有。

  • 私有继承(private inheritance)

        基类的所有成员在派生类中为私有成员。

  • 保护继承(protected inheritance)

         基类的公有成员和保护成员在派生类中成了保护成员, 私有成员仍为基类私有。

         简单总结,不管采用哪种继承方式,派生类都不能访问基类的私有成员,所以不同的继承方式改变的只是公有和保护类型的访问权限私有继承把这两者都变为私有,保护继承把这两者都变成保护,公有继承不改变两者访问权限。

 派生类的构造函数和析构函数

 构造函数

        前边已经提到过,派生类并不会继承基类的构造函数和析构函数,这就会产生一个问题,如果对一个派生类进行初始化,而他又没有基类的构造函数,那么派生类中的基类成员应该怎么赋值?答案是基类的成员初始化的工作要由派生类的构造函数承担, 同样基类的析构函数也要被派生类的析构函数来调用。

        我们先来看一下派生类的构造函数是如何承担基类对象的初始化的,其实从派生类构造函数的定义就能找到答案:

/*****************************************************************
*派生类名(形式参数列表) : 基类名(基类构造函数实参列表),派生类初始化列表
*{
*派生类初始化函数体
*}
*****************************************************************/
class Point { 
    int x,y;
    public: Point(int a,int b):x(a),y(b) { } //构造函数
};
class Rect : public Point { 
    int h,w;
    //派生类构造函数
    public: Rect(int a,int b,int c,int d):Point(a,b),h(c),w(d) { }
};

        代码注释中的 “基类名(基类构造函数实参列表)”即是调用基类构造函数, 而派生类新增加的数据成员可以在“派生类初始化列表” (尽量在此) 初始化, 也可以在“派生类初始化函数体” 中初始化。当基类中有无参数的或者参数全部默认的构造函数,那么也可不调用基类的构造函数,因为这是会默认调用构造函数。

class A { public: A(int) { } }; //有参数构造函数
class B { public: B(int) { } }; //有参数构造函数
class D: public B {
D(int x) : a(x),B(x) { } //显式调用基类或子对象构造函数
A a;
};

        所以当我们调用一个派生类的构造函数时,它的调用顺序是基类构造函数-->派生类的初始化列表-->执行派生类初始化的函数体。

 析构函数

        派生类中可以定义自己的析构函数对自己所增加的成员进行清理工作,但是基类的清理工作仍然有基类的析构函数自己完成,需要通过派生类的析构函数去调用基类的析构函数。执行派生类的析构函数时, 系统会自动调用基类的析构函数和子对象的析构函数, 对基类和子对象进行清理。析构函数的执行顺序和那个构造函数相反:派生类的析构函数-->子对象的析构函数-->基类的析构函数。

多重继承和派生

        之前的小结介绍了多重继承,下边我们来定义一个多重继承

/************************************************************
*多重继承的定义
*class 派生类名:访问标号1 基类名1,访问标号2 基类名2,... { //类体
*成员列表
*};
*************************************************************/
class A { };
class B : public A { }; //A→B
class C : public A { }; //A→C
class D : public B,public C { }; //B→D,C→D

 多重继承的构造函数和单一继承的基本相同,只是在构造函数初始化列表中需要调用多个基类的构造函数。

派生类名(形式参数列表) : 基类名1(基类1构造函数实参列表),
基类名2(基类2构造函数实参列表),
…,
子对象名1(子对象1属类构造函数实参列表),
…,
派生类初始化列表
{
派生类初始化函数体
}

      在多重继承时可能会产生一个问题,如果多个基类有同名的成员,在派生类中访问这种重名成员的时候,就会产生二义性。我们看下下边的例子。

class A {
public:
    void fun() { cout<<"a.fun"<<endl; }
};
class B {
public:
    void fun() { cout<<"b.fun"<<endl; }
    void gun() { cout<<"b.gun"<<endl; }
};
class C:public A,public B {
public:
    void gun() { cout<<"c.gun"<<endl; } //重写gun()
    void hun() { fun(); } //出现二义性
};
int main()
{
    C c,*p=&c;
    return 0;
}

         我们可以使用成员函数限定名消除二义性,例如:

 c.A::fun(); //成员名限定消除二义性
c.B::fun(); //成员名限定消除二义性
p->A::fun(); //成员名限定消除二义性
p->B::fun(); //成员名限定消除二义性

         名字支配规则:如果存在两个包含的作用于,那么一个名字在外边已经声明过,则内部没有声明相同的名字,则外部名字内部可用,如果内部也声明了,外部名字被内部覆盖。在类的派生层次结构中, 基类的成员和派生类新增的成员都具有类作用域, 二者的作用域是不同的: 基类在外层, 派生类在内层如果派生类声明了一个和基类成员同名的新成员, 派生的新成员就覆盖了基类同名成员, 直接使用成员名只能访问到派生类的成员。如果派生类中声明了与基类成员函数同名的新函数, 即使函数的参数不同, 从基类继承的同名函数的所有重载形式也都会被覆盖。

虚基类

        虚基类使得在继承间接共同基类的时候只保留一份成员,有如下的一个继承关系

        可以看出,D继承了C1中的A也继承了C2中的A,D中有两份A成员,所以这事如果声明A为虚基类,那么D中只会保留一份A中的成员。它的定义方式如下

class 派生类名: virtual 访问标号 虚基类名,... { //类体
成员列表
};

        为了保证虚基类在派生类中只继承一次, 应当在该基类的所有直接派生类中声明为虚基类。 否则仍然会出现对基类的多次继承

        在最后的派生类中不仅要负责对其直接基类进行初始化, 还要负责对虚基类初始化。且在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。

class A { public: A(int) {} }; //定义基类
class B : virtual public A { public: B(int a):A(a) {} };
//对基类A初始化
class C : virtual public A { public: C(int a):A(a) {} };
//对基类A初始化
class D : public B,public C
{ public: D(int a):A(a),B(a),C(a) {} };

赋值兼容规则

        赋值兼容规则是C++多态的重要基础之一,它是指在任何需要基类对象的地方,都可以使用公有的派生类对象来代替。

        上述替代包括:

  • 派生类的对象可以赋值给基类对象;
  • 派生类的对象可以初始化基类的引用;
  • 派生类对象的地址可以赋给指向基类的指针;
class Base { }; //基类
class Derive : public Base { }; //公有派生类
Base b,*pb; //定义基类对象、 指针
Derive d; //定义派生类对象

这时, 支持下面三种操作:
b=d; //派生类对象赋值给基类, 复制基类继承部分
Base &rb=d; //基类引用到派生类对象
pb=&d; //基类指针指向派生类对象

动态联编和静态联编

        所谓的联编就是将模块或者函数合并在一起生成可执行代码的处理过程。
        在编译阶段就将函数实现和函数调用绑定起来称为静态联编,在程序运行的时候才进行函数实现和函数调用的绑定称为动态联编(dynamic binding),如在编译“Point *pc=&c”时, 只根据兼容性规则检查它的合理性, 即检查它是否符合派生类对象的地址可以赋给基类的指针的条件。 至于“pc->area()”调用哪个函数, 等到程序运行到这里再决定。

虚函数

        在介绍虚函数之前我们先来说一下多态,多态是指同样的消息被不同类型的对象接收时导致不同的行为。 所谓消息是指对类成员函数的调用, 不同的行为是指不同的实现, 也就是调用了不同的函数。在C++中, 这种多态性可以通过重载多态(函数和运算符重载) 、强制多态(类型强制转换) 、 类型参数化多态(模板) 、 包含多态(继承及虚函数) 四种形式来实现。

        被virtual关键字修饰的成员函数, 就是虚函数。 虚函数的作用就是实现多态性——以共同的方法, 对不同的对象采取不同的策略。在成员函数定义或声明前面加上关键字virtual, 即定义了虚函数,虚函数只在类体中使用,如果派生类中成员函数和基类中的同名成员函数参数个数、 参数类型以及返回类型一样,那么系统默认这个函数是虚函数。

        虚函数可在基类和派生类中使用相同的函数名定义函数的不同实现, 从而实现“一个接口, 多种方式” 。 当用基类指针或引用对虚函数进行访问时, 系统将根据运行时指针或引用所指向或引用的实际对象来确定调用对象所在类的虚函数版本。

        虚函数实现多态的条件

  1. 类之间的继承关系满足赋值兼容性规则;
  2. 改写了同名的虚函数;
  3. 根据赋值兼容性规则使用指针(或引用) 。

虚函数的实现

        当编译器编译含有虚函数的类时, 将为它建立一个虚函数表VTABLE(virtual table) , 它相当于一个指针数组, 存放每个虚函数的入口地址。 编译器为该类增加一个额外的数据成员, 这个数据成员是一个指向虚函数表的指针, 通常称为vptr。
        Point类只有一个虚函数area, 所以虚函数表里只有一项。 如下图是Point对象UML示意

         如果派生类Circle没有重写这个虚函数area, 则派生类的虚函数表里的元素所指向的地址就是基类Point的虚函数area的地址。 如果派生类Circle重写这个虚函数area,这时编译器将派生类虚函数表里的vptr指向Circle::area(), 即指向派生类area虚函数的地址。

         当调用虚函数时, 先通过vptr找到虚函数表, 然后再找出虚函数的真正地址, 再调用它。

        总之派生类能继承基类的虚函数表, 而且只要是和基类同名(参数也相同) 的成员函数, 无论是否使用virtual声明, 它们都自动成为虚函数。 如果派生类没有改写继承基类的虚函数, 则函数指针调用基类的虚函数。 如果派生类改写了基类的虚函数, 编译器将重新为派生类的虚函数建立地址, 函数指针会调用改写以后的虚函数。

纯虚函数和抽象类

虚析构函数

        派生类的对象从内存中撤销时一般先调用派生类的析构函数, 然后再调用基类的析构函数。但是, 如果用new运算符建立了派生类对象 且定义了一个基类的指针指向这个对象, 那么当用delete运算符撤销对象时, 系统会只执行基类的析构函数, 而不执行派生类的析构函数, 因而也无法对派生类对象进行真正的撤销清理操作。为了解决这种问题,我们要对基类的析构函数声明为虚函数。如果将基类的析构函数声明为虚函数, 由该基类所派生的所有派生类的析构函数也都自动成为虚函数, 即使派生类的析构函数与基类的析构函数名字不相同。

纯虚函数

        在许多情况下, 不能在基类中为虚函数给出一个有意义的定义, 这时可以将它说明为纯虚函数(pure virtual function) , 将具体定义留给派生类去做。 纯虚函数的定义形式为:

virtual 返回类型 函数名(形式参数列表)=0;

        纯虚函数的作用是在基类中为其派生类保留一个函数的名字, 以便派生类根据需要对它进行定义。 如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义, 则该虚函数在派生类中仍然为纯虚函数。
        包含有纯虚函数的类称为抽象类(abstract class) 。 一个抽象类只能作为基类来派生新类, 所以又称为抽象基类(abstract baseclass) 。 抽象类不能定义对象。

        如果在派生类中给出了抽象类的纯虚函数的实现, 则该派生类不再是抽象类。 否则只要派生类仍然有纯虚函数, 则派生类依然是抽象类。 抽象类至少含有一个虚函数, 而且至少有一个虚函数是纯虚函数。

操作符重载

        操作符重载的形式如下:

/*************************************************
*返回类型 operator运算符号(形式参数列表)
*{
*函数体
*}
*************************************************/
  • 运算符重载函数不能使用默认参数。
  • 重载运算符必须具有一个类对象(或类对象的引用) 的参数,不能全部是C++的内置数据类型。
  • 一般若运算结果作为左值则返回类型为引用类型; 若运算结果要作为右值, 则返回对象。
  • 重载运算符的功能应该与原来的功能一致。

运算符重载为类的成员函数

class 类名{ //类体
……
返回类型 operator 运算符号(形式参数列表);
……
};
返回类型 类名::operator 运算符号(形式参数列表)
{
函数体
}

        当运算符重载为成员函数时, 运算符函数的形式参数的个数比运算符规定的运算对象个数要少一个。原因是类的非静态成员函数都有一个隐含的this指针, 运算符函数可以用this指针隐式地访问类对象的成员, 因此这个对象自身的数据可以直接访问, 不需要放到形参列表中进行传递, 少了的运算对象就是该对象本身。

class complex //复数类
{
public:
complex(double r=0.0,double i=0.0){real=r; imag=i;}//构造函数
complex operator +(complex &c2); //重载+运算符
complex operator -(complex &c2); //重载-运算符
void display()
{
cout<<real<<"+"<<imag<<"i"<<endl;
}
private:
double real; //实部
double imag; //虚部
};
complex complex::operator +(complex &c2)
{ complex c;
c.real=c2.real+real;
c.imag=c2.imag+imag;
return c;
}
complex complex::operator -(complex &c2)
{ complex c;
c.real=real-c2.real; //顺序不能颠倒
c.imag=imag-c2.imag;
return c;
}
int main()
{ complex c1(1,2),c2(3,4),c3;
c3=c2+c1; c3.display(); //输出4+6i
c3=c2-c1; c3.display(); //输出2+2i
return 0;
}

        前置单目运算符号

返回类型 类名::operator op()
{
…… //this指针对应obj运算对象
}

         后置单目运算符(后边加个参数,表示后置)

返回类型 类名::operator op(int)
{
…… //this指针对应obj运算对象
}

运算符重载为友元函数

        当运算符重载为友元函数时, 运算符函数的形式参数的个数和运算符规定的运算对象个数一致。

class 类名{ //类体
……
//友元声明
friend 返回类型 operator 运算符号(形式参数列表);
};
返回类型 operator 运算符号(形式参数列表)
{
函数体
}


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值