继承和多态C++

这里写目录标题

    • 继承
      • public、protected、private 修饰类的成员
      • public、protected、private 指定继承方式
        • 改变访问权限
      • C++继承时的名字遮蔽问题
      • 基类成员函数和派生类成员函数不构成重载
      • C++基类和派生类的构造函数
        • 构造函数的调用顺序
        • 基类构造函数调用规则
      • C++基类和派生类的析构函数
      • C++多继承(多重继承)
      • 多继承下的构造函数
      • 多继承造成的命名冲突
      • C++将派生类赋值给基类(向上转型)⭐
        • 将派生类对象赋值给基类对象
        • 将派生类指针赋值给基类指针
        • 将派生类引用赋值给基类引用
    • 多态与虚函数
      • 多态的用途
      • C++虚函数注意事项
      • 纯虚函数

搬运自C语言中文网

继承

继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类 B 继承于类 A,那么 B 就拥有 A 的成员变量和成员函数。
在C++中,派生(Derive)和继承是一个概念,只是站的角度不同。继承是儿子接收父亲的产业,派生是父亲把产业传承给儿子。

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

派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能。

以下是两种典型的使用继承的场景:

  1. 当你创建的新类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承,这样不但会减少代码量,而且新类会拥有基类的所有功能。

  2. 当你需要创建多个类,它们拥有很多相似的成员变量或成员函数时,也可以使用继承。可以将这些类的共同成员提取出来,定义为基类,然后从基类继承,既可以节省代码,也方便后续修改成员。

#include<iostream>
using namespace std;//基类 Pelple
class People{
public:void setname(char *name);void setage(int age);char *getname();int getage();
private:char *m_name;int m_age;
};
void People::setname(char *name){ m_name = name; }
void People::setage(int age){ m_age = age; }
char* People::getname(){ return m_name; }
int People::getage(){ return m_age;}//派生类 Student
class Student: public People{
public:void setscore(float score);float getscore();
private:float m_score;
};
void Student::setscore(float score){ m_score = score; }
float Student::getscore(){ return m_score; }int main(){Student stu;stu.setname("小明");stu.setage(16);stu.setscore(95.5f);cout<<stu.getname()<<"的年龄是 "<<stu.getage()<<",成绩是 "<<stu.getscore()<<endl;return 0;
}
//派生类 Student
class Student: public People{
……
}

由此总结出继承的一般语法为:
class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};

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

public、protected、private 三个关键字除了可以修饰类的成员,还可以指定继承方式。

public、protected、private 修饰类的成员

类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。

在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员在类外也不能通过对象访问 private、protected 属性的成员。

声明为 protected 的成员在类外也不能通过对象访问,但是在它的派生类内部可以访问
protected 成员和 private 成员类似,也不能通过对象访问。但是当存在继承关系时,protected 和 private 就不一样了:基类中protected 成员可以在派生类中使用,而基类中的 private 成员不能在派生类中使用

public、protected、private 指定继承方式

不同的继承方式会影响基类成员在派生类中的访问权限。
在这里插入图片描述
通过上面的分析可以发现:

  1. 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如,当继承方式为 protected 时,那么基类成员在派生类中的访问权限最高也为 protected,高于 protected 的会降级为 protected,但低于 protected 不会升级。再如,当继承方式为 public 时,那么基类成员在派生类中的访问权限将保持不变。

也就是说,继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的。

  1. 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。

  2. 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。

  3. 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。

注意,我们这里说的是基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。
在这里插入图片描述
由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public。

在派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问。(因为 m_hobby 是 private 属性的,在派生类中不可见,所以只能借助基类的 public 成员函数 sethobby()、gethobby() 来访问。)

改变访问权限

使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。

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

//派生类Student
class Student : public People {
public:void learning();
public:using People::m_name;  //将protected改为publicusing People::m_age;  //将protected改为publicfloat m_score;
private:using People::show;  //将public改为private
};
void Student::learning() {cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
}

C++继承时的名字遮蔽问题

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

#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;
}//派生类Student
class Student: public People{
public:Student(char *name, int age, float score);
public:void show();  //遮蔽基类的show()
private:float m_score;
};
Student::Student(char *name, int age, float score){m_name = name;m_age = age;m_score = score;
}
void Student::show(){cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}int main(){Student stu("小明", 16, 90.5);//使用的是派生类新增的成员函数,而不是从基类继承的stu.show();//使用的是从基类继承来的成员函数stu.People::show();return 0;
}

运行结果:
小明的年龄是16,成绩是90.5
嗨,大家好,我叫小明,今年16岁

本例中,基类 People 和派生类 Student 都定义了成员函数 show(),它们的名字一样,会造成遮蔽。第 37 行代码中,stu 是 Student 类的对象,默认使用 Student 类的 show() 函数。

但是,基类 People 中的 show() 函数仍然可以访问,不过要加上类名和域解析符.People:: stu.People::show();

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

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

#include<iostream>
using namespace std;//基类Base
class Base{
public:void func();void func(int);
};
void Base::func(){ cout<<"Base::func()"<<endl; }
void Base::func(int a){ cout<<"Base::func(int)"<<endl; }//派生类Derived
class Derived: public Base{
public:void func(char *);void func(bool);
};
void Derived::func(char *str){ cout<<"Derived::func(char *)"<<endl; }
void Derived::func(bool is){ cout<<"Derived::func(bool)"<<endl; }int main(){Derived d;d.func("c.biancheng.net");d.func(true);d.func();  //compile errord.func(10);  //compile errord.Base::func();d.Base::func(100);return 0;
}

如果说有重载关系,那么也是 Base 类的两个 func 构成重载,而 Derive 类的两个 func 构成另外的重载。

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

前面我们说基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。
这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数

//People(name, age)就是调用基类的构造函数
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }

People(name, age)就是调用基类的构造函数,并将 name 和 age 作为实参传递给它,m_score(score)是派生类的参数初始化表,它们之间以逗号,隔开。

也可以将基类构造函数的调用放在参数初始化表后面:

Student::Student(char *name, int age, float score): m_score(score), People(name, age){ }

函数头部是对基类构造函数的调用 People("小明", 16),而不是声明,所以括号里的参数是实参,它们不但可以是派生类构造函数参数列表中的参数,还可以是局部变量、常量等,例如:

Student::Student(char *name, int age, float score): People("小明", 16), m_score(score){ }

构造函数的调用顺序

在这里插入图片描述
还有一点要注意,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。以上面的 A、B、C 类为例,C 是最终的派生类,B 就是 C 的直接基类,A 就是 C 的间接基类。

C++ 这样规定是有道理的,因为我们在 C 中调用了 B 的构造函数,B 又调用了 A 的构造函数,相当于 C 间接地(或者说隐式地)调用了 A 的构造函数,如果再在 C 中显式地调用 A 的构造函数,那么 A 的构造函数就被调用了两次,相应地,初始化工作也做了两次,这不仅是多余的,还会浪费CPU时间以及内存,毫无益处,所以 C++ 禁止在 C 中显式地调用 A 的构造函数。

基类构造函数调用规则

通过派生类创建对象时必须调用基类的构造函数,这是语法规定。换句话说,定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。请看下面的例子:

People::People(): m_name("xxx"), m_age(0){ }
People::People(char *name, int age): m_name(name), m_age(age){}
Student::Student(): m_score(0.0){ }  //派生类默认构造函数
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
int main(){Student stu1;stu1.display();Student stu2("小明", 16, 90.5);stu2.display();return 0;
}

创建对象 stu1 时,执行派生类的构造函数Student::Student(),它并没有指明要调用基类的哪一个构造函数,从运行结果可以很明显地看出来,系统默认调用了不带参数的构造函数,也就是People::People()

创建对象 stu2 时,执行派生类的构造函数Student::Student(char *name, int age, float score),它指明了基类的构造函数。

在第 27 行代码中,如果将People(name, age)去掉,也会调用默认构造函数,第 37 行的输出结果将变为:
xxx的年龄是0,成绩是90.5。

如果将基类 People 中不带参数的构造函数删除,那么会发生编译错误,因为创建对象 stu1 时需要调用 People 类的默认构造函数, 而 People 类中已经显式定义了构造函数,编译器不会再生成默认的构造函数。(显式定义了构造函数 就不会生成默认的构造函数,所以要自己写一个构造函数)

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

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

#include <iostream>
using namespace std;class A{
public:A(){cout<<"A constructor"<<endl;}~A(){cout<<"A destructor"<<endl;}
};class B: public A{
public:B(){cout<<"B constructor"<<endl;}~B(){cout<<"B destructor"<<endl;}
};class C: public B{
public:C(){cout<<"C constructor"<<endl;}~C(){cout<<"C destructor"<<endl;}
};int main(){C test;//就是创建了一个C类的对象return 0;
}

运行结果:
A constructor
B constructor
C constructor
C destructor
B destructor
A destructor

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

在前面的例子中,派生类都只有一个基类,称为单继承(Single Inheritance)。除此之外,C++也支持多继承(MultipleInheritance),即一个派生类可以有两个或多个基类。
多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D:

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

D 是多继承形式的派生类,它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。D 根据不同的继承方式获取 A、B、C 中的成员,确定它们在派生类中的访问权限。

多继承下的构造函数

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

D(形参列表): A(实参列表), B(实参列表), C(实参列表){//其他操作
}

基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。仍然以上面的 A、B、C、D 类为例,即使将 D 类构造函数写作下面的形式:

D(形参列表): B(实参列表), C(实参列表), A(实参列表){//其他操作
}

那么也是先调用 A 类的构造函数,再调用 B 类构造函数,最后调用 C 类构造函数。 因为是这样声明的

class D: public A, private B, protected C{//类D新增加的成员
}
#include <iostream>
using namespace std;//基类
class BaseA{
public:BaseA(int a, int b);~BaseA();
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;
}//基类
class BaseB{
public:BaseB(int c, int d);~BaseB();
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;
}//派生类
class Derived: public BaseA, public BaseB{
public:Derived(int a, int b, int c, int d, int e);~Derived();
public:void show();
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::show(){cout<<m_a<<", "<<m_b<<", "<<m_c<<", "<<m_d<<", "<<m_e<<endl;
}int main(){Derived obj(1, 2, 3, 4, 5);obj.show();return 0;
}

多继承造成的命名冲突

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

void BaseA::show(){cout<<"m_a = "<<m_a<<endl;cout<<"m_b = "<<m_b<<endl;
}
void BaseB::show(){cout<<"m_c = "<<m_c<<endl;cout<<"m_d = "<<m_d<<endl;`#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;
}int main(){Derived obj(1, 2, 3, 4, 5);obj.display();return 0;
}`
}

我们显式的指明了要调用哪个基类的 show() 函数。

void Derived::display(){BaseA::show();  //调用BaseA类的show()函数BaseB::show();  //调用BaseB类的show()函数cout<<"m_e = "<<m_e<<endl;
}

完整代码

#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;
}int main(){Derived obj(1, 2, 3, 4, 5);obj.display();return 0;
}

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

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

#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;
}

本例中 A 是基类, B 是派生类,a、b 分别是它们的对象,由于派生类 B 包含了从基类 A 继承来的成员,因此可以将派生类对象 b 赋值给基类对象 a: a = b;。通过运行结果也可以发现,赋值后 a 所包含的成员变量的值已经发生了变化。

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

#include <iostream>
using namespace std;//基类A
class A{
public:A(int a);
public:void display();
protected:int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){cout<<"Class A: m_a="<<m_a<<endl;
}//中间派生类B
class B: public A{
public:B(int a, int b);
public:void display();
protected: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;
}//基类C
class C{
public:C(int c);
public:void display();
protected:int m_c;
};
C::C(int c): m_c(c){ }
void C::display(){cout<<"Class C: 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): B(a, b), C(c), m_d(d){ }
void D::display(){cout<<"Class D: m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
}int main(){A *pa = new A(1);B *pb = new B(2, 20);C *pc = new C(3);D *pd = new D(4, 40, 400, 4000);pa = pd;pa -> display();pb = pd;pb -> display();pc = pd;pc -> display();cout<<"-----------------------"<<endl;cout<<"pa="<<pa<<endl;cout<<"pb="<<pb<<endl;cout<<"pc="<<pc<<endl;cout<<"pd="<<pd<<endl;return 0;
}

本例中定义了多个对象指针,并尝试将派生类指针赋值给基类指针。与对象变量之间的赋值不同的是,对象指针之间的赋值并没有拷贝对象的成员,也没有修改对象本身的数据,仅仅是改变了指针的指向
将派生类指针赋值给基类指针时,通过基类指针只能使用派生类的成员变量,但不能使用派生类的成员函数(有待细讲)

将派生类引用赋值给基类引用

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

向上转型后通过基类的对象、指针、引用 只能访问从基类继承过去的成员(包括成员变量和成员函数),不能访问派生类新增的成员

多态与虚函数

在上一节讲到,基类的指针也可以指向派生类对象
将派生类指针赋值给基类指针时,通过基类指针只能使用派生类的成员变量,但不能使用派生类的成员函数(有待细讲)

#include <iostream>
using namespace std;//基类People
class People{
public:People(char *name, int age);void display();
protected:char *m_name;int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}//派生类Teacher
class Teacher: public People{
public:Teacher(char *name, int age, int salary);void display();
private:int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}int main(){People *p = new People("王志刚", 23);p -> display();p = new Teacher("赵宏佳", 45, 8200);p -> display();return 0;
}

运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是个无业游民。

当基类指针 p 指向派生类 Teacher 的对象时,虽然使用了 Teacher 的成员变量,但是却没有使用它的成员函数,导致输出结果不伦不类(赵宏佳本来是一名老师,输出结果却显示人家是个无业游民),不符合我们的预期。

换句话说,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。

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

更改上面的代码,将 display() 声明为虚函数:

#include <iostream>
using namespace std;//基类People
class People{
public:People(char *name, int age);virtual void display();  //声明为虚函数
protected:char *m_name;int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}//派生类Teacher
class Teacher: public People{
public:Teacher(char *name, int age, int salary);virtual void display();  //声明为虚函数
private:int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}int main(){People *p = new People("王志刚", 23);p -> display();p = new Teacher("赵宏佳", 45, 8200);p -> display();return 0;
}

运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是一名教师,每月有8200元的收入。

和前面的例子相比,本例仅仅是在 display() 函数声明前加了一个virtual关键字,将成员函数声明为了虚函数(Virtual Function),这样就可以通过 p 指针调用 Teacher 类的成员函数了,运行结果也证明了这一点(赵宏佳已经是一名老师了,不再是无业游民了)。

有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员(在使用虚函数之前,指向派生类对象只能使用派生类成员变量而不能使用成员函数)

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

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

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

引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力,所以以后我们再谈及多态时一般是说指针。本例的主要目的是让读者知道,除了指针,引用也可以实现多态。

多态的用途

多态的用途
通过上面的例子读者可能还未发现多态的用途,不过确实也是,多态在小项目中鲜有有用武之地。

接下来的例子中,我们假设你正在玩一款军事游戏,敌人突然发动了地面战争,于是你命令陆军、空军及其所有现役装备进入作战状态。具体的代码如下所示:

#include <iostream>
using namespace std;//军队
class Troops{
public:virtual void fight(){ cout<<"Strike back!"<<endl; }
};//陆军
class Army: public Troops{
public:void fight(){ cout<<"--Army is fighting!"<<endl; }
};
//99A主战坦克
class _99A: public Army{
public:void fight(){ cout<<"----99A(Tank) is fighting!"<<endl; }
};
//武直10武装直升机
class WZ_10: public Army{
public:void fight(){ cout<<"----WZ-10(Helicopter) is fighting!"<<endl; }
};
//长剑10巡航导弹
class CJ_10: public Army{
public:void fight(){ cout<<"----CJ-10(Missile) is fighting!"<<endl; }
};//空军
class AirForce: public Troops{
public:void fight(){ cout<<"--AirForce is fighting!"<<endl; }
};
//J-20隐形歼击机
class J_20: public AirForce{
public:void fight(){ cout<<"----J-20(Fighter Plane) is fighting!"<<endl; }
};
//CH5无人机
class CH_5: public AirForce{
public:void fight(){ cout<<"----CH-5(UAV) is fighting!"<<endl; }
};
//轰6K轰炸机
class H_6K: public AirForce{
public:void fight(){ cout<<"----H-6K(Bomber) is fighting!"<<endl; }
};int main(){Troops *p = new Troops;p ->fight();//陆军p = new Army;p ->fight();p = new _99A;p -> fight();p = new WZ_10;p -> fight();p = new CJ_10;p -> fight();//空军p = new AirForce;p -> fight();p = new J_20;p -> fight();p = new CH_5;p -> fight();p = new H_6K;p -> fight();return 0;
}

这个例子中的派生类比较多,如果不使用多态,那么就需要定义多个指针变量,很容易造成混乱;而有了多态,只需要一个指针变量 p 就可以调用所有派生类的虚函数。

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

C++虚函数注意事项

    1. 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
  1. 为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数。关于名字遮蔽已在《C++继承时的名字遮蔽》一节中进行了讲解。
    所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。
    不理解 ,按照 遮蔽 的说法,即使基类中的函数不申明为虚函数,也会被派生类中的同名函数 遮蔽啊

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

  3. 只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为virtual void func();,派生类虚函数的原型为virtual void func(int);,那么当基类指针 p 指向派生类对象时,语句p -> func(100);将会出错,而语句p -> func();将调用基类的函数。

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

纯虚函数

在C++中,可以将虚函数声明为纯虚函数,语法格式为:

virtual 返回值类型 函数名 (函数参数) = 0;

纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。
最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。
包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

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

纯虚函数使用举例:

#include <iostream>
using namespace std;
//线
class Line{
public:Line(float len);virtual float area() = 0;virtual float volume() = 0;
protected:float m_len;
};
Line::Line(float len): m_len(len){ }
//矩形
class Rec: public Line{
public:Rec(float len, float width);float area();
protected:float m_width;
};
Rec::Rec(float len, float width): Line(len), m_width(width){ }
float Rec::area(){ return m_len * m_width; }
//长方体
class Cuboid: public Rec{
public:Cuboid(float len, float width, float height);float area();float volume();
protected:float m_height;
};
Cuboid::Cuboid(float len, float width, float height): Rec(len, width), m_height(height){ }
float Cuboid::area(){ return 2 * ( m_len*m_width + m_len*m_height + m_width*m_height); }
float Cuboid::volume(){ return m_len * m_width * m_height; }
//正方体
class Cube: public Cuboid{
public:Cube(float len);float area();float volume();
};
Cube::Cube(float len): Cuboid(len, len, len){ }
float Cube::area(){ return 6 * m_len * m_len; }
float Cube::volume(){ return m_len * m_len * m_len; }
int main(){Line *p = new Cuboid(10, 20, 30);cout<<"The area of Cuboid is "<<p->area()<<endl;cout<<"The volume of Cuboid is "<<p->volume()<<endl;p = new Cube(15);cout<<"The area of Cube is "<<p->area()<<endl;cout<<"The volume of Cube is "<<p->volume()<<endl;return 0;
}

运行结果:
The area of Cuboid is 2200
The volume of Cuboid is 6000
The area of Cube is 1350
The volume of Cube is 3375

本例中定义了四个类,它们的继承关系为:Line --> Rec --> Cuboid --> Cube。

Line 是一个抽象类,也是最顶层的基类,在 Line 类中定义了两个纯虚函数 area() 和 volume()。

在 Rec 类中,实现了 area() 函数;所谓实现,就是定义了纯虚函数的函数体。但这时 Rec 仍不能被实例化,因为它没有实现继承来的 volume() 函数,volume() 仍然是纯虚函数,所以 Rec 也仍然是抽象类。

直到 Cuboid 类,才实现了 volume() 函数,才是一个完整的类,才可以被实例化。

可以发现,Line 类表示“线”,没有面积和体积,但它仍然定义了 area() 和 volume() 两个纯虚函数。这样的用意很明显:Line 类不需要被实例化,但是它为派生类提供了“约束条件”,派生类必须要实现这两个函数,完成计算面积和体积的功能,否则就不能实例化。

在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。

抽象基类除了约束派生类的功能,还可以实现多态。请注意第 51 行代码 Line *p = new Cuboid(10, 20, 30);指针 p 的类型是 Line,但是它却可以访问派生类中的 area() 和 volume() 函数,正是由于在 Line 类中将这两个函数定义为纯虚函数;如果不这样做,51 行后面的代码都是错误的???(回忆一下多态概念的引入,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数,至于基类被派生类同名函数遮蔽,是在通过派生类对象访问成员函数的情况下发生的)。我想,这或许才是C++提供纯虚函数的主要目的。
关于纯虚函数的几点说明

  1. 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。(可以有纯虚函数已经被实现,只要还剩一个没有

  2. 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。如下例所示:

//顶层函数不能被声明为纯虚函数
void fun() = 0;   //compile error
class base{
public ://普通成员函数不能被声明为纯虚函数void display() = 0;  //compile error
};

摘自C语言中文网

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/92205.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android app专项测试之耗电量测试

前言 耗电量指标 待机时间成关注目标 提升用户体验 通过不同的测试场景&#xff0c;找出app高耗电的场景并解决 01、需要的环境准备 1、python2.7(必须是2.7&#xff0c;3.X版本是不支持的) 2、golang语言的开发环境 3、Android SDK 此三个的环境搭建这里就不详细说了&am…

无涯教程-Perl - send函数

描述 此函数在SOCKET上发送消息(与recv相反)。如果Socket未连接,则必须提供一个目标以与TO参数进行通信。在这种情况下,将使用sendto系统功能代替系统发送功能。 FLAGS参数由按位或0以及MSG_OOB和MSG_DONTROUTEoptions中的一个或多个形成。 MSG_OOB允许您在支持此概念的Socke…

炬芯科技发布全新第二代智能手表芯片,引领腕上新趋势!

2023年7月&#xff0c;炬芯科技宣布全新第二代智能手表芯片正式发布。自2021年底炬芯科技推出第一代的智能手表芯片开始便快速获得了市场广泛认可和品牌客户的普遍好评。随着技术的不断创新和突破&#xff0c;为了更加精准地满足市场多元化的变幻和用户日益增长的体验需求&…

C语言入门 Day_5 四则运算

目录 前言 1.四则运算 2.其他运算 3.易错点 4.思维导图 前言 图为世界上第一台通用计算机ENIAC,于1946年2月14日在美国宾夕法尼亚大学诞生。发明人是美国人莫克利&#xff08;JohnW.Mauchly&#xff09;和艾克特&#xff08;J.PresperEckert&#xff09;。 计算机的最开始…

轻量级 Spring Task 任务调度可视化管理

Spring Task/Spring Scheduler 傻傻分不清 首先做一下“名词解释”&#xff0c;分清楚这两者的区别&#xff1a; Spring Task Spring Task 是 Spring 框架自带的一个任务调度模块&#xff0c;提供了基本的任务调度功能。它是通过 Java 的 Timer 和 TimerTask 类来实现的&…

ReactDOM模块react-dom/client没有默认导出报错解决办法

import ReactDOM 模块“"E:/Dpandata/Shbank/rt-pro/node_modules/.pnpm/registry.npmmirror.comtypesreact-dom18.2.7/node_modules/types/react-dom/client"”没有默认导出。 解决办法 只需要在tsconfig.json里面添加配置 "esModuleInterop": true 即…

数据结构介绍

1、什么是数据结构呢&#xff1f; 计算机底层存储、组织数据的方式。是指数据相互之间是以什么方式排列在一起的。数据结构是为了更方便的管理和使用数据&#xff0c;需要结合具体的业务来进行选择。一般情况下&#xff0c;精心选择的数据结构可以带来更高的运行或者存储效率。…

6.利用matlab完成 符号矩阵的秩和 符号方阵的逆矩阵和行列式 (matlab程序)

1.简述 利用M文件建立矩阵 对于比较大且比较复杂的矩阵&#xff0c;可以为它专门建立一个M文件。下面通过一个简单例子来说明如何利用M文件创建矩阵。 例2-2 利用M文件建立MYMAT矩阵。(1) 启动有关编辑程序或MATLAB文本编辑器&#xff0c;并输入待建矩阵&#xff1a;(2) 把…

【2023年11月第四版教材】《第5章-信息系统工程之软件工程(第一部分)》

《第5章-信息系统工程&#xff08;第一部分&#xff09;》 章节说明1 软件工程1.1 架构设计1.2 需求分析 章节说明 65%为新增内容, 预计选择题考5分&#xff0c;案例和论文不考&#xff1b;本章与第三版教材一样的内容以楷体字进行标注! 1 软件工程 1.1 架构设计 1、软件架…

Linux文件权限一共10位长度,分成四段

Linux文件权限一共10位长度,分成四段 Linux文件权限 1、 文件aaa的访问权限为rw-r--r--,现要增加所有用户的执行权限和同组用户的写权限&#xff0c;下列哪些命令是正确的&#xff1f; a) chmod ax gw aaa √ b) chmod 764 aaa c) chmod 775 aaa √ d)…

vue3+vite+pinia

目录 一、项目准备 1.1、Vite搭建项目 1.2、vue_cli创建项目 二、组合式API(基于setup) 2.1、ref 2.2、reactive 2.3、toRefs 2.4、watch和watchEffect 2.5、computed 2.6、生命周期钩子函数 2.7、setup(子组件)的第一个参数-props 2.8、setup(子组件)的第二个参数…

贴吧照片和酷狗音乐简单爬取

爬取的基本步骤 很简单&#xff0c;主要是两大步 向url发起请求 这里注意找准对应资源的url&#xff0c;如果对应资源不让程序代码访问&#xff0c;这里可以伪装成浏览器发起请求。 解析上一步返回的源代码&#xff0c;从中提取想要的资源 这里解析看具体情况&#xff0c;一…

什么是前端框架?怎么学习? - 易智编译EaseEditing

前端框架是一种用于开发Web应用程序界面的工具集合&#xff0c;它提供了一系列预定义的代码和结构&#xff0c;以简化开发过程并提高效率。 前端框架通常包括HTML、CSS和JavaScript的库和工具&#xff0c;用于构建交互式、动态和响应式的用户界面。 学习前端框架可以让您更高效…

python爬虫——爬虫伪装和反“反爬”

前言 爬虫伪装和反“反爬”是在爬虫领域中非常重要的话题。伪装可以让你的爬虫看起来更像普通的浏览器或者应用程序&#xff0c;从而减少被服务器封禁的风险&#xff1b;反“反爬”则是应对服务器加强的反爬虫机制。下面将详细介绍一些常见的伪装和反反爬技巧&#xff0c;并提…

加杠杆的股票类型是什么?

加杠杆的股票类型在投资领域有不同的称呼&#xff0c;包括杠杆股票、倍增股票、奇特股票等。这些股票类型都具有共同的特点&#xff0c;即提供给投资者以杠杆交易的机会&#xff0c;可以放大投资的回报。以下是对加杠杆的股票类型的介绍。 1. 杠杆型基金&#xff1a;杠杆型基金…

Java+Excel+POI+testNG基于数据驱动做一个简单的接口测试【杭州多测师_王sir】

一、创建一个apicases.xlsx放入到eclipse的resource里面&#xff0c;然后refresh刷新一下 二、在pom.xml文件中加入poi和testng的mvn repository、然后在eclipse的对应目录下放入features和plugins&#xff0c;重启eclipse就可以看到testNG了 <!--poi excel解析 --><d…

ChatGPT​保密吗?它有哪些潜在风险?如何规避?

自2022年11月公开发布以来&#xff0c;ChatGPT已成为许多企业和个人的必备工具&#xff0c;但随着该技术越来越多地融入我们的日常生活&#xff0c;人们很自然地想知道&#xff1a;ChatGPT是否是保密的。 问&#xff1a;ChatGPT保密吗&#xff1f; 答&#xff1a;否&#xff0…

Unity 工具 之 Azure 微软SSML语音合成TTS流式获取音频数据的简单整理

Unity 工具 之 Azure 微软SSML语音合成TTS流式获取音频数据的简单整理 目录 Unity 工具 之 Azure 微软SSML语音合成TTS流式获取音频数据的简单整理 一、简单介绍 二、实现原理 三、实现步骤 四、关键代码 一、简单介绍 Unity 工具类&#xff0c;自己整理的一些游戏开发可…

企事业数字培训及知识库平台

前言 随着信息化的进一步推进&#xff0c;目前各行各业都在进行数字化转型&#xff0c;本人从事过医疗、政务等系统的研发&#xff0c;和客户深入交流过日常办公中“知识”的重要性&#xff0c;再加上现在倡导的互联互通、数据安全、无纸化办公等概念&#xff0c;所以无论是企业…

jvs-rules API数据源配置说明(含配置APIdemo视频)

在JVS中&#xff0c;多数据源支持多种形态的数据接入&#xff0c;其中API是企业生产过程中常见的数据形态。使用数据源的集成配置&#xff0c;以统一的方式管理和集成多个API的数据。这些平台通常提供各种数据转换和处理功能&#xff0c;使得从不同数据源获取和处理数据变得更加…