C++继承和多态

C++继承和多态

继承和派生

C++ 中的继承是类与类之间的关系,继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。

派生(Derive)和继承是一个概念,

被继承的类称为父类或基类,继承的类称为子类或派生类。“子类”和“父类”通常放在一起称呼,“基类”和“派生类”通常放在一起称呼

  • 当你创建的新类与现有的类相似
  • 当你需要创建多个类,它们拥有很多相似的成员变量或成员函数时,使用继承。可以将这些类的共同成员提取出来,定义为基类,然后从基类继承
class Student: public People

继承的一般语法为:

class 派生类名:[继承方式] 基类名{
    派生类新增加的成员
};

继承方式: 包括 public(公有的)、private(私有的)和 protected(受保护的),此项是可选的,如果不写,那么默认为 private。

C++三种继承方式

继承方式限定了基类成员在派生类中的访问权限,包括 public(公有的)、private(私有的)和 protected(受保护的)。此项是可选项,如果不写,默认为 private(成员变量和成员函数默认也是 private)

public、protected、private 指定继承方式

1) public继承方式

  • 基类中所有 public 成员在派生类中为 public 属性;
  • 基类中所有 protected 成员在派生类中为 protected 属性;
  • 基类中所有 private 成员在派生类中不能使用。

2) protected继承方式

  • 基类中的所有 public 成员在派生类中为 protected 属性;
  • 基类中的所有 protected 成员在派生类中为 protected 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

3) private继承方式

  • 基类中的所有 public 成员在派生类中均为 private 属性;
  • 基类中的所有 protected 成员在派生类中均为 private 属性;
  • 基类中的所有 private 成员在派生类中不能使用。
  • 继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的。
  • 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)
  • 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
  • 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。

类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。

在派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问

#include<iostream>
using namespace std;
//基类People
class People{
public:
    void setname(char *name);
    void setage(int age);
    void sethobby(char *hobby);
    char *gethobby();
protected:
    char *m_name;
    int m_age;
private:
    char *m_hobby;
};
void People::setname(char *name){ m_name = name; }
void People::setage(int age){ m_age = age; }
void People::sethobby(char *hobby){ m_hobby = hobby; }
char *People::gethobby(){ return m_hobby; }
//派生类Student
class Student: public People{
public:
    void setscore(float score);
protected:
    float m_score;
};
void Student::setscore(float score){
    m_score=score;
}
//派生类Pupil
class Pupil: public Student{
public:
    void display();
    void setranking(int ranking);
private:
    int m_ranking;
};
void Pupil::display(){
    cout<<m_name<<m_age<<m_score<<m_ranking<<gethobby()<<endl;//hobby
}//派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问
void Pupil::setranking(int ranking){
    m_ranking=ranking;
}

改变访问权限

使用 using 关键字可以改变基类成员在派生类中的访问权限

using 只能改变基类中 public 和 protected 成员的访问权限, private 成员不能改变,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。

#include<iostream>
using namespace std;

//基类People
class People{
public:
    void show();
protected:
    char *m_name;
    int m_age;
};
void People::show(){
    cout<<m_name<<m_age<<endl;
}

class Student:public People{
public:
    void learning();
public:
    using People::m_name;
    using People::m_age;
    float m_score;
private:
    using People::show;//public 改为private, 函数不加()
};
void Student::learning(){
    cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
}
//使用 using 改变了它们的默认访问权限,如代码第 21~25 行所示,将 show() 函数修改为 private 属性的,是降低访问权限,将 name、age 变量修改为 public 属性的,是提高访问权限。

C++继承时的名字遮蔽

如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的

基类成员函数和派生类成员函数不构成重载

对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样

C++基类和派生类的构造函数

基类的成员函数可以被继承,类的构造函数不能被继承。

对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,派生类中无法访问,

解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数

示例代码:

#include<iostream>
using namespace std;
class People{
protected:
    char *m_name;
    int m_age;
public:
    People(char*, int);
};
People::People(char *name, int age): m_name(name), m_age(age){}
//派生类Student
class Student: public People{
private:
    float m_score;
public:
    Student(char *name, int age, float score);
    void display();
};
//People(name,age)就是调用基类的构造函数
Student::Student(char *name, int age, float score):People(name,age),m_score(score){
    
}
//也可以将基类构造函数的调用放在参数初始化表后面:
//Student::Student(char *name, int age, float score): m_score(score), //People(name, age){ }

//但不能放在函数体内
//Student::Student(char *name, int age, float score){
//    People(name, age); 因为People构造函数没有被继承。
//    m_score = score;
//}

void Student::display(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<"。"<<endl;
}

构造函数的调用顺序

A --> B --> C

A类构造函数 --> B类构造函数 --> C类构造函数

构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。

因为在调用B构造函数时,A已经调用了,C再调用就是重复调用,因此C++禁止在 C 中显式地调用 A 的构造函数

基类构造函数调用规则

定义派生类构造函数时最好指明基类构造函数;如果不指明(在派生类构造函数的初始化列表前未调用),就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。

C++基类和派生类的析构函数

析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉
析构函数的执行顺序和构造函数的执行顺序刚好相反:

  • 创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
  • 而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。

C++多继承(多重继承)详解

派生类都只有一个基类,称为单继承(Single Inheritance)

一个派生类可以有两个或多个基类,称为多继承(Multiple Inheritance)

class D: public A, private B, protected C{
    //类D新增加的成员
}

多继承下的构造函数:

多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。以上面的 A、B、C、D 类为例,D 类构造函数的写法为:

D(形参列表): A(实参列表), B(实参列表), C(实参列表){///和声明派生类时基类出现的顺序相同
    //其他操作
}

命名冲突:

当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::以显式地指明到底使用哪个类的成员,消除二义性

#include <iostream>
using namespace std;
//基类
class BaseA{
public:
    BaseA(int a, int b);
    ~BaseA();
public:
    void show();
protected:
    int m_a;
    int m_b;
};
BaseA::BaseA(int a, int b): m_a(a), m_b(b){
    cout<<"BaseA constructor"<<endl;
}
BaseA::~BaseA(){
    cout<<"BaseA destructor"<<endl;
}
void BaseA::show(){
    cout<<"m_a = "<<m_a<<endl;
    cout<<"m_b = "<<m_b<<endl;
}
//基类
class BaseB{
public:
    BaseB(int c, int d);
    ~BaseB();
    void show();
protected:
    int m_c;
    int m_d;
};
BaseB::BaseB(int c, int d): m_c(c), m_d(d){
    cout<<"BaseB constructor"<<endl;
}
BaseB::~BaseB(){
    cout<<"BaseB destructor"<<endl;
}
void BaseB::show(){
    cout<<"m_c = "<<m_c<<endl;
    cout<<"m_d = "<<m_d<<endl;
}
//派生类
class Derived: public BaseA, public BaseB{
public:
    Derived(int a, int b, int c, int d, int e);
    ~Derived();
public:
    void display();
private:
    int m_e;
};
Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){
    cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){
    cout<<"Derived destructor"<<endl;
}
void Derived::display(){
    BaseA::show();  //调用BaseA类的show()函数
    BaseB::show();  //调用BaseB类的show()函数
    cout<<"m_e = "<<m_e<<endl;
}

C++虚继承和虚基类详解

多继承(Multiple Inheritance), 指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。

//间接基类A
class A{
protected:
    int m_a;
};
//直接基类B
class B: public A{
protected:
    int m_b;
};
//直接基类C
class C: public A{
protected:
    int m_c;
};
//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //命名冲突
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};
int main(){
    D d;
    return 0;
}

类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。

void seta(int a){ B::m_a = a; }//使用 B 类的 m_a
void seta(int a){ C::m_a = a; }//使用 C 类的

虚继承(Virtual Inheritance)

为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。

在继承方式前面加上 virtual 关键字就是虚继承

class A{
protected:
    int m_a;
};
class B: virtual public A{
protected:
    int m_b;
};
class C: virtual public B{
protected:
    int m_c;
};
class D: public B, public C{
protected:
    int m_d;
public:
    void set_a(int a){m_a=a};
    void set_b(int b){m_b=b};
    void set_c(int c){m_c=c};
    void set_d(int d){m_d=d};
};

virtual class

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。

虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身

虚基类成员的可见性

该成员被两条或多条路径覆盖了,不能直接访问了,此时必须指明该成员属于哪个类。(::)

以上图中的菱形继承为例,假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:

  • 如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 B 的成员,此时不存在二义性。
  • 如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高
  • 如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。

C++虚继承时的构造函数

#include <iostream>
using namespace std;
class A{
public:
    A(int a);
protected:
    int m_a;
};
A::A(int a): m_a(a){ }
//直接派生类B
class B: virtual public A{
public:
    B(int a, int b);
public:
    void display();
protected:
    int m_b;
};
//直接派生类C
class C: virtual public A{
public:
    C(int a, int c);
public:
    void display();
protected:
    int m_c;
};
C::C(int a, int c): A(a), m_c(c){ }
void C::display(){
    cout<<"m_a="<<m_a<<", m_c="<<m_c<<endl;
}

//间接派生类D
class D:public B, public C{
public:
    D(int a, int b, int c, int d);
public:
    void display();
private:
    int m_d;
};
D::D(int a, int b, int c, int d):A(a), B(90,b), C(100,c), m_d(d){ }
void D::display(){
    cout<<"m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
}

D 不但要负责初始化直接基类 B 和 C,还要负责初始化间接基类 A。而在以往的普通继承中,派生类的构造函数只负责初始化它的直接基类,再由直接基类的构造函数初始化间接基类

虚基类 A 在最终派生类 D 中只保留了一份成员变量 m_a,如果由 B 和 C 初始化 m_a,那么 B 和 C 在调用 A 的构造函数时很有可能给出不同的实参

虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;

C++将派生类赋值给基类(向上转型)

类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型(Upcasting)

向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预。

将派生类对象赋值给基类对象

#include <iostream>
using namespace std;
//基类
class A{
public:
    A(int a);
public:
    void display();
public:
    int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){
    cout<<"Class A: m_a="<<m_a<<endl;
}
//派生类
class B: public A{
public:
    B(int a, int b);
public:
    void display();
public:
    int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
    cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
}
int main(){
    A a(10);
    B b(66, 99);
    //赋值前
    a.display();
    b.display();
    cout<<"--------------"<<endl;
    //赋值后
    a = b; //派生类赋值给基类
    a.display();
    b.display();
    return 0;
}

赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。

对象之间的赋值不会影响成员函数,也不会影响 this 指针。

只能用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。理由很简单,基类不包含派生类的成员变量,无法对派生类的成员变量赋值。同理,同一基类的不同派生类对象之间也不能赋值

将派生类指针赋值给基类指针

对象指针之间的赋值并没有拷贝对象的成员,也没有修改对象本身的数据,仅仅是改变了指针的指向。

  • 通过基类指针访问派生类的成员

    将派生类指针赋值给基类指针时,通过基类指针只能使用派生类的成员变量,但不能使用派生类的成员函数

    编译器通过指针来访问成员变量,指针指向哪个对象就使用哪个对象的数据;编译器通过指针的类型来访问成员函数,指针属于哪个类的类型就使用哪个类的函数。

  • 赋值后值不一致的情况
  • 将派生类引用赋值给基类引用

引用变量在功能上等于一个指针常量,本质上是通过指针的方式实现的

基类的引用也可以指向派生类的对象,并且它的表现和指针是类似的

int main(){
	D d(4,40,400,4000);
    A &ra=d;
    B &rb=d;
    C &rc=d;
}

C++多态和虚函数

当基类指针 p 指向派生类,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数

为了消除这种尴尬,让基类指针能够访问派生类的成员函数,C++ 增加了虚函数(Virtual Function)。使用虚函数非常简单,只需要在基类函数声明前面增加 virtual 关键字。

虚函数使得基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员

基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态

多态是面向对象编程的主要特征之一,C++中虚函数的唯一用处就是构成多态。

C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。

虚函数是根据指针的指向来调用的,指针指向哪个类的对象就调用哪个类的虚函数。

不是根据指针的类型(通过哪个类定义的指针)来判断调用哪个类的成员函数

借助引用也可以实现多态

People p("王志刚", 23);
Teacher t("赵宏佳", 45, 8200);
People &rt = t;
rt.display();

引用只能指代固定的对象,在多态性方面缺乏表现力

对于具有复杂继承关系的大中型程序,多态可以增加其灵活性,让代码更具有表现力

为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数

当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。

只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)

构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。

析构函数可以声明为虚函数,而且有时候必须要声明为虚函数

下面是构成多态的条件:

  • 必须存在继承关系;
  • 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
  • 存在基类的指针,通过该指针调用虚函数。

成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。

纯虚函数

纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。

包含纯虚函数的类称为抽象类(Abstract Class)。

抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现,

虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。

关于纯虚函数的几点说明

  1. 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
  2. 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。

C++ typeid运算符:获取类型信息

typeid 会把获取到的类型信息保存到一个 type_info 类型的对象里面,并返回该对象的常引用;

typeid 的使用非常灵活,它的操作数可以是普通变量、对象、内置类型(int、float等)、自定义类型(结构体和类),还可以是一个表达式

类型比较 结果 类型比较 结果
typeid(int) == typeid(int) true typeid(int) == typeid(char) false
typeid(char*) == typeid(char) false typeid(str) == typeid(char*) true
typeid(a) == typeid(int) true typeid(b) == typeid(int) true
typeid(a) == typeid(a) true typeid(a) == typeid(b) true
typeid(a) == typeid(f) false typeid(a/b) == typeid(int) true

表达式typeid(*p1) == typeid(Base)typeid(p1) == typeid(Base*)的结果为 true 可以说明:即使将派生类指针 p2 赋值给基类指针 p1,p1 的类型仍然为 Base*。

指针的类型声明后就不变了

C++运算符重载基础教程

重载,就是赋予新的含义。函数重载(Function Overloading)可以让一个函数名有多种功能,在不同情况下进行不同的操作。运算符重载(Operator Overloading)也是一个道理,同一个运算符可以有不同的功能。

#include <iostream>
using namespace std;
class complex{
public:
    complex();
    complex(double real, double imag);
public:
    //声明运算符重载
    complex operator+(const complex &A) const;
    void display() const;
private:
    double m_real;
    double m_imag;
};

complex complex::operator+(const complex &A) const{
    complex B;
    B.m_real=this.m_real+A.m_real;
    B.m_imag=this.m_imag+A.m_imag;
    return B;
}

运算符重载格式:

返回值类型 operator 运算符名称 (形参表列){
    //TODO:
}


operator是关键字,专门用于定义重载运算符的函数。我们可以将operator 运算符名称这一部分看做函数名

c3 = c1.operator+(c2);

原文链接: https://www.cnblogs.com/Zak-NoS/p/11673942.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    C++继承和多态

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/304126

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月16日 上午1:35
下一篇 2023年2月16日 上午1:36

相关推荐