【学习QT必备的C++基础】C++继承、派生与多态

2023-05-16

文章目录

  • C++继承和派生简明教程
  • C++三种继承方式
    • public、protected、private 修饰类的成员
    • public、protected、private 指定继承方式
    • 改变访问权限
  • C++继承时的名字遮蔽问题
    • 基类成员函数和派生类成员函数不构成重载
  • C++类继承时的作用域嵌套,破解C++继承的一切秘密!
  • C++继承时的对象内存模型
    • 继承时的内存模型
    • 有成员变量遮蔽时的内存分布
  • 借助指针突破访问权限的限制,访问private、protected属性的成员变量
    • 使用偏移
    • 突破访问权限的限制
  • C++将派生类赋值给基类(向上转型)
    • 将派生类对象赋值给基类对象
    • 将派生类指针赋值给基类指针
        • 1) 通过基类指针访问派生类的成员
        • 2) 赋值后值不一致的情况
    • 将派生类引用赋值给基类引用
  • 将派生类指针赋值给基类指针时到底发生了什么?
  • C++多态和虚函数快速入门教程
    • 借助引用也可以实现多态
    • 多态的用途
  • C++虚函数注意事项以及构成多态的条件
    • 构成多态的条件
    • 什么时候声明虚函数

C++继承和派生简明教程

已剪辑自: http://c.biancheng.net/view/2264.html

C++ 中的继承是类与类之间的关系,是一个很简单很直观的概念,与现实世界中的继承类似,例如儿子继承父亲的财产。

**继承(Inheritance)**可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类 B 继承于类 A,那么 B 就拥有 A 的成员变量和成员函数。

在C++中,**派生(Derive)**和继承是一个概念,只是站的角度不同。继承是儿子接收父亲的产业,派生是父亲把产业传承给儿子。

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

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

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

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

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

下面我们定义一个基类 People,然后由此派生出 Student 类:

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

运行结果:
小明的年龄是 16,成绩是 95.5

本例中,People 是基类,Student 是派生类。Student 类继承了 People 类的成员,同时还新增了自己的成员变量 score 和成员函数 setscore()、getscore()。这些继承过来的成员,可以通过子类对象访问,就像自己的一样。

请认真观察代码第21行:

class Student: public People

这就是声明派生类的语法。class 后面的“Student”是新声明的派生类,冒号后面的“People”是已经存在的基类。在“People”之前有一关键宇 public,用来表示是公有继承。

由此总结出继承的一般语法为:

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

继承方式包括 public(公有的)、private(私有的)和 protected(受保护的),此项是可选的,如果不写,那么默认为 private。我们将在下节详细讲解这些不同的继承方式。

C++三种继承方式

已剪辑自: http://c.biancheng.net/view/2269.html

C++继承的一般语法为:

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

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

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

public、protected、private 修饰类的成员

类成员的访问权限由高到低依次为 public --> protected --> private,我们在《C++类成员的访问权限以及类的封装》一节中讲解了 public 和 private:public 成员可以通过对象来访问,private 成员不能通过对象访问。

现在再来补充一下 protected。protected 成员和 private 成员类似,也不能通过对象访问。但是当存在继承关系时,protected 和 private 就不一样了:基类中的 protected 成员可以在派生类中使用,而基类中的 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 成员在派生类中不能使用。

通过上面的分析可以发现:

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

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

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

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

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

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

下表汇总了不同继承方式对不同属性的成员的影响结果 继承方式/基类成员 public成员 protected成员 private成员 public继承 protected继承 private继承

publicprotected不可见
protectedprotected不可见
privateprivate不可见

由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public。

【示例】演示类的继承关系。

#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 setranking(int ranking);
    void display();
private:
    int m_ranking;
};
void Pupil::setranking(int ranking){ m_ranking = ranking; }
void Pupil::display(){
    cout<<m_name<<"的年龄是"<<m_age<<",考试成绩为"<<m_score<<"分,班级排名第"<<m_ranking<<",TA喜欢"<<gethobby()<<"。"<<endl;
}
int main(){
    Pupil pup;
    pup.setname("小明");
    pup.setage(15);
    pup.setscore(92.5f);
    pup.setranking(4);
    pup.sethobby("乒乓球");
    pup.display();
    return 0;
}

运行结果:
小明的年龄是15,考试成绩为92.5分,班级排名第4,TA喜欢乒乓球。

这是一个多级继承的例子,Student 继承自 People,Pupil 又继承自 Student,它们的继承关系为 People --> Student --> Pupil。Pupil 是最终的派生类,它拥有基类的 m_name、m_age、m_score、m_hobby 成员变量以及 setname()、setage()、sethobby()、gethobby()、setscore() 成员函数。

注意,在派生类 Pupil 的成员函数 display() 中,我们借助基类的 public 成员函数 gethobby() 来访问基类的 private 成员变量 m_hobby,因为 m_hobby 是 private 属性的,在派生类中不可见,所以只能借助基类的 public 成员函数 sethobby()、gethobby() 来访问。

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

改变访问权限

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

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

using 关键字使用示例:

#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:
    void learning();
public:
    using People::m_name;  //将protected改为public
    using People::m_age;  //将protected改为public
    float m_score;
private:
    using People::show;  //将public改为private
};
void Student::learning() {
    cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
}
int main() {
    Student stu;
    stu.m_name = "小明";
    stu.m_age = 16;
    stu.m_score = 99.5f;
    stu.show();  //compile error
    stu.learning();
    return 0;
}

代码中首先定义了基类 People,它包含两个 protected 属性的成员变量和一个 public 属性的成员函数。定义 Student 类时采用 public 继承方式,People 类中的成员在 Student 类中的访问权限默认是不变的。

不过,我们使用 using 改变了它们的默认访问权限,如代码第 21~25 行所示,将 show() 函数修改为 private 属性的,是降低访问权限,将 name、age 变量修改为 public 属性的,是提高访问权限。

因为 show() 函数是 private 属性的,所以代码第 36 行会报错。把该行注释掉,程序输出结果为:
我是小明,今年16岁,这次考了99.5分!

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

已剪辑自: http://c.biancheng.net/view/2271.html

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

下面是一个成员函数的名字遮蔽的例子:

#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() 函数仍然可以访问,不过要加上类名和域解析符,如第 39 行代码所示。

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

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

下面的例子很好的说明了这一点:

#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 error
    d.func(10);  //compile error
    d.Base::func();
    d.Base::func(100);
    return 0;
}

本例中,Base 类的func()func(int)和 Derived 类的func(char *)func(bool)四个成员函数的名字相同,参数列表不同,它们看似构成了重载,能够通过对象 d 访问所有的函数,实则不然,Derive 类的 func 遮蔽了 Base 类的 func,导致第 26、27 行代码没有匹配的函数,所以调用失败。

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

至于 Base 类和 Derived 类的 4 个 func 为什么不会构成重载,我们将在VIP教程《C++类继承时的作用域嵌套,破解C++继承的一切秘密!》一节中讲解,届时读者必将有所顿悟。

C++类继承时的作用域嵌套,破解C++继承的一切秘密!

已剪辑自: http://c.biancheng.net/view/vip_2272.html

类其实也是一种作用域,每个类都会定义它自己的作用域,在这个作用域内我们再定义类的成员,这一点已在《类其实也是一种作用域》中讲到。当存在继承关系时,派生类的作用域嵌套在基类的作用域之内,如果一个名字在派生类的作用域内无法找到,编译器会继续到外层的基类作用域中查找该名字的定义。

换句话说,作用域能够彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域(inner scope),包含着别的作用域的作用域称为外层作用域(outer scope)。一旦在外层作用域中声明(或者定义)了某个名字,那么它所嵌套着的所有内层作用域中都能访问这个名字。同时,允许在内层作用域中重新定义外层作用域中已有的名字。

假设 Base 是基类,Derived 是派生类,那么它们的作用域的嵌套关系如下图所示:

img

派生类的作用域位于基类作用域之内这一事实可能有点出人意料,毕竟在我们的代码中派生类和基类的定义是相互分离的。不过也恰恰因为类作用域有这种继承嵌套的关系,所以派生类才能像使用自己的成员一样来使用基类的成员。

一个类作用域嵌套的例子:

#include<iostream>
using namespace std;
class A{
public:
    void func();
public:
    int n;
};
void A::func(){ cout<<"c.biancheng.net"<<endl; }
class B: public A{
public:
    int n;
    int m;
};
class C: public B{
public:
    int n;
    int x;
};
int main(){
    C obj;
    obj.n;
    obj.func();
    cout<<sizeof(C)<<endl;
    return 0;
}

运行结果:
c.biancheng.net
20

本例中,B 继承自 A,C继承自 B,它们作用域的嵌套关系如下图所示:

img

obj 是 C 类的对象,通过 obj 访问成员变量 n 时,在 C 类的作用域中就能够找到了 n 这个名字。虽然 A 类和 B 类都有名字 n,但编译器不会到它们的作用域中查找,所以是不可见的,也即派生类中的 n 遮蔽了基类中的 n。

通过 obj 访问成员函数 func() 时,在 C 类的作用域中没有找到 func 这个名字,编译器继续到 B 类的作用域(外层作用域)中查找,仍然没有找到,再继续到 A 类的作用域中查找,结果就发现了 func 这个名字,于是查找结束,编译器决定调用 A 类作用域中的 func() 函数。

这个过程叫做名字查找(name lookup),也就是在作用域链中寻找与所用名字最匹配的声明(或定义)的过程。

对于成员变量这个过程很好理解,对于成员函数要引起注意,编译器仅仅是根据函数的名字来查找的,不会理会函数的参数。换句话说,一旦内层作用域有同名的函数,不管有几个,编译器都不会再到外层作用域中查找,编译器仅把内层作用域中的这些同名函数作为一组候选函数;这组候选函数就是一组重载函数。

说白了,只有一个作用域内的同名函数才具有重载关系,不同作用域内的同名函数是会造成遮蔽,使得外层函数无效。派生类和基类拥有不同的作用域,所以它们的同名函数不具有重载关系。

我们不妨再来回顾一下上节的例子:

#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 error
    d.func(10);  //compile error
    d.Base::func();
    d.Base::func(100);
    return 0;
}

虽然 Derived 类和 Base 类都有同名的 func 函数,但它们位于不同的作用域,Derived 类的 func 会遮蔽 Base 类的 func。d 是 Derived 类的对象,调用 func 函数时,编译器会先在 Derived 类中查找“func”这个名字,发现有两个,也即void func(char*)和void func(bool),这就是一组候选函数。

执行到第 26、27 行代码时,在候选函数中没有找到匹配的函数,所以调用失败,这时编译器会抛出错误信息,而不是再到 Base 类中查找同名函数。

C++继承时的对象内存模型

已剪辑自: http://c.biancheng.net/view/vip_2273.html

在《C++对象的内存模型》一节中我们讲解了没有继承时对象内存的分布情况。这时的内存模型很简单,成员变量和成员函数会分开存储:

  • 对象的内存中只包含成员变量,存储在栈区或堆区(使用 new 创建对象);
  • 成员函数与对象内存分离,存储在代码区。

当存在继承关系时,内存模型会稍微复杂一些。

继承时的内存模型

有继承关系时,派生类的内存模型可以看成是基类成员变量和新增成员变量的总和,而所有成员函数仍然存储在另外一个区域——代码区,由所有对象共享。请看下面的代码:

#include <cstdio>
using namespace std;
//基类A
class A{
public:
    A(int a, int b);
public:
    void display();
protected:
    int m_a;
    int m_b;
};
A::A(int a, int b): m_a(a), m_b(b){}
void A::display(){
    printf("m_a=%d, m_b=%d\n", m_a, m_b);
}
//派生类B
class B: public A{
public:
    B(int a, int b, int c);
    void display();
protected:
    int m_c;
};
B::B(int a, int b, int c): A(a, b), m_c(c){ }
void B::display(){
    printf("m_a=%d, m_b=%d, m_c=%d\n", m_a, m_b, m_c);
}
int main(){
    A obj_a(99, 10);
    B obj_b(84, 23, 95);
    obj_a.display();
    obj_b.display();
    return 0;
}

obj_a 是基类对象,obj_b 是派生类对象。假设 obj_a 的起始地址为 0X1000,那么它的内存分布如下图所示:

img

假设 obj_b 的起始地址为 0X1100,那么它的内存分布如下图所示:

img

可以发现,基类的成员变量排在前面,派生类的排在后面。

为了让大家理解更加透彻,我们不妨再由 B 类派生出一个 C 类:

//声明并定义派生类C
class C: public B{
public:
    C(char a, int b, int c, int d);
public:
    void display();
private:
    int m_d;
};
C::C(char a, int b, int c, int d): B(a, b, c), m_d(d){ }
void C::display(){
    printf("m_a=%d, m_b=%d, m_c=%d, m_d=%d\n", m_a, m_b, m_c, m_d);
}
//创建C类对象obj_c
C obj_c(84, 23, 95, 60);
obj_c.display();

假设 obj_c 的起始地址为 0X1200,那么它的内存分布如下图所示:

img

成员变量按照派生的层级依次排列,新增成员变量始终在最后。

有成员变量遮蔽时的内存分布

更改上面的 C 类,让它的成员变量遮蔽 A 类和 B 类的成员变量:

//声明并定义派生类C
class C: public B{
public:
    C(char a, int b, int c, int d);
public:
    void display();
private:
    int m_b;  //遮蔽A类的成员变量
    int m_c;  //遮蔽B类的成员变量
    int m_d;  //新增成员变量
};
C::C(char a, int b, int c, int d): B(a, b, c), m_b(b), m_c(c), m_d(d){ }
void C::display(){
    printf("A::m_a=%d, A::m_b=%d, B::m_c=%d\n", m_a, A::m_b, B::m_c);
    printf("C::m_b=%d, C::m_c=%d, C::m_d=%d\n", m_b, m_c, m_d);
}
//创建C类对象obj_c
C obj_c(84, 23, 95, 60);
obj_c.display();

假设 obj_c 的起始地址为 0X1300,那么它的内存分布如下图所示:

img

当基类 A、B 的成员变量被遮蔽时,仍然会留在派生类对象 obj_c 的内存中,C 类新增的成员变量始终排在基类 A、B 的后面。

总结:在派生类的对象模型中,会包含所有基类的成员变量。这种设计方案的优点是访问效率高,能够在派生类对象中直接访问基类变量,无需经过好几层间接计算。

借助指针突破访问权限的限制,访问private、protected属性的成员变量

已剪辑自: http://c.biancheng.net/view/vip_2279.html

我们都知道,C++ 不允许通过对象来访问 private、protected 属性的成员变量,例如:

#include <iostream>
using namespace std;
class A{
public:
    A(int a, int b, int c);
private:
    int m_a;
    int m_b;
    int m_c;
};
A::A(int a, int b, int c): m_a(a), m_b(b), m_c(c){ }
int main(){
    A obj(10, 20, 30);
    int a = obj.m_a;  //Compile Error
    A *p = new A(40, 50, 60);
    int b = p->m_b;  //Compile Error
    return 0;
}

这段代码说明了,无论通过对象变量还是对象指针,都不能访问 private 属性的成员变量。

不过 C++ 的这种限制仅仅是语法层面的,通过某种“蹩脚”的方法,我们能够突破访问权限的限制,访问到 private、protected 属性的成员变量,赋予我们这种“特异功能”的,正是强大而又灵活的指针(Pointer)。

使用偏移

在对象的内存模型中,成员变量和对象的开头位置会有一定的距离。以上面的 obj 为例,它的内存模型为:

img

图中假设 obj 对象的起始地址为 0X1000,m_a、m_b、m_c 与对象开头分别相距 0、4、8 个字节,我们将这段距离称为偏移(Offset)。一旦知道了对象的起始地址,再加上偏移就能够求得成员变量的地址,知道了成员变量的地址和类型,也就能够轻而易举地知道它的值。

当通过对象指针访问成员变量时,编译器实际上也是使用这种方式来取得它的值。为了说明问题,我们不妨将上例中成员变量的访问权限改为 public,再来执行第 18 行的语句:

int b = p->m_b;

此时编译器内部会发生类似下面的转换:

int b = (int)( (int)p + sizeof(int) );

p 是对象 obj 的指针,(int)p将指针转换为一个整数,这样才能进行加法运算;sizeof(int)用来计算 m_b 的偏移;(int)p + sizeof(int)得到的就是 m_b 的地址,不过因为此时是int类型,所以还需要强制转换为int *类型;开头的*用来获取地址上的数据。

img

如果通过 p 指针访问 m_a:

int a = p -> m_a;

那么将被转换为下面的形式:

int a = * (int*) ( (int)p + 0 );

经过简化以后为:

int a = (int)p;

突破访问权限的限制

上述的转换过程是编译器自动完成的,当成员变量的访问权限为 private 时,我们也可以手动转换,只要能正确计算偏移即可,这样就突破了访问权限的限制。

修改上例中的代码,借助偏移来访问 private 属性的成员变量:

#include <iostream>
using namespace std;
class A{
public:
    A(int a, int b, int c);
private:
    int m_a;
    int m_b;
    int m_c;
};
A::A(int a, int b, int c): m_a(a), m_b(b), m_c(c){ }
int main(){
    A obj(10, 20, 30);
    int a1 = *(int*)&obj;
    int b = *(int*)( (int)&obj + sizeof(int) );
    A *p = new A(40, 50, 60);
    int a2 = *(int*)p;
    int c = *(int*)( (int)p + sizeof(int)*2 );
   
    cout<<"a1="<<a1<<", a2="<<a2<<", b="<<b<<", c="<<c<<endl;
    return 0;
}

运行结果:
a1=10, a2=40, b=20, c=60

前面我们说 C++ 的成员访问权限仅仅是语法层面上的,是指访问权限仅对取成员运算符.->起作用,而无法防止直接通过指针来访问。你可以认为这是指针的强大,也可以认为是 C++ 语言设计的瑕疵。

本节的目的不是为了访问到 private、protected 属性的成员变量,这种“花拳绣腿”没有什么现实的意义,本节主要是让大家明白编译器内部的工作原理,以及指针的灵活运用。

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

已剪辑自: http://c.biancheng.net/view/2284.html

在 C/C++ 中经常会发生数据类型的转换,例如将 int 类型的数据赋值给 float 类型的变量时,编译器会先把 int 类型的数据转换为 float 类型再赋值;反过来,float 类型的数据在经过类型转换后也可以赋值给 int 类型的变量。

数据类型转换的前提是,编译器知道如何对数据进行取舍。例如:

int a = 10.9;
printf("%d\n", a);

输出结果为 10,编译器会将小数部分直接丢掉(不是四舍五入)。再如:

float b = 10;
printf("%f\n", b);

输出结果为 10.000000,编译器会自动添加小数部分。

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

向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预。本节只介绍向上转型,向下转型将在后续章节介绍。 向上转型和向下转型是面向对象编程的一种通用概念,它们也存在于 Java、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;
}

运行结果:
Class A: m_a=10
Class B: m_a=66, m_b=99
----------------------------
Class A: m_a=66
Class B: m_a=66, m_b=99

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

赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。运行结果也有力地证明了这一点,虽然有a=b;这样的赋值过程,但是 a.display() 始终调用的都是 A 类的 display() 函数。换句话说,对象之间的赋值不会影响成员函数,也不会影响 this 指针。

将派生类对象赋值给基类对象时,会舍弃派生类新增的成员,也就是“大材小用”,如下图所示:

img

可以发现,即使将派生类对象赋值给基类对象,基类对象也不会包含派生类的成员,所以依然不同通过基类对象来访问派生类的成员。对于上面的例子,a.m_a 是正确的,但 a.m_b 就是错误的,因为 a 不包含成员 m_b。

这种转换关系是不可逆的,只能用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。理由很简单,基类不包含派生类的成员变量,无法对派生类的成员变量赋值。同理,同一基类的不同派生类对象之间也不能赋值。

要理解这个问题,还得从赋值的本质入手。赋值实际上是向内存填充数据,当数据较多时很好处理,舍弃即可;本例中将 b 赋值给 a 时(执行a=b;语句),成员 m_b 是多余的,会被直接丢掉,所以不会发生赋值错误。但当数据较少时,问题就很棘手,编译器不知道如何填充剩下的内存;如果本例中有b= a;这样的语句,编译器就不知道该如何给变量 m_b 赋值,所以会发生错误。

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

除了可以将派生类对象赋值给基类对象(对象变量之间的赋值),还可以将派生类指针赋值给基类指针(对象指针之间的赋值)。我们先来看一个多继承的例子,继承关系为:

img

下面的代码实现了这种继承关系:

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

运行结果:
Class A: m_a=4
Class B: m_a=4, m_b=40
Class C: m_c=400
-----------------------
pa=0x9b17f8
pb=0x9b17f8
pc=0x9b1800
pd=0x9b17f8

本例中定义了多个对象指针,并尝试将派生类指针赋值给基类指针。与对象变量之间的赋值不同的是,对象指针之间的赋值并没有拷贝对象的成员,也没有修改对象本身的数据,仅仅是改变了指针的指向。

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

请读者先关注第 68 行代码,我们将派生类指针 pd 赋值给了基类指针 pa,从运行结果可以看出,调用 display() 函数时虽然使用了派生类的成员变量,但是 display() 函数本身却是基类的。也就是说,将派生类指针赋值给基类指针时,通过基类指针只能使用派生类的成员变量,但不能使用派生类的成员函数,这看起来有点不伦不类,究竟是为什么呢?第 71、74 行代码也是类似的情况。

pa 本来是基类 A 的指针,现在指向了派生类 D 的对象,这使得隐式指针 this 发生了变化,也指向了 D 类的对象,所以最终在 display() 内部使用的是 D 类对象的成员变量,相信这一点不难理解。

编译器虽然通过指针的指向来访问成员变量,但是却不通过指针的指向来访问成员函数:编译器通过指针的类型来访问成员函数。对于 pa,它的类型是 A,不管它指向哪个对象,使用的都是 A 类的成员函数,具体原因已在《C++函数编译原理和成员函数的实现》中做了详细讲解。

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

2) 赋值后值不一致的情况

本例中我们将最终派生类的指针 pd 分别赋值给了基类指针 pa、pb、pc,按理说它们的值应该相等,都指向同一块内存,但是运行结果却有力地反驳了这种推论,只有 pa、pb、pd 三个指针的值相等,pc 的值比它们都大。也就是说,执行pc = pd;语句后,pc 和 pd 的值并不相等。

这非常出乎我们的意料,按照我们通常的理解,赋值就是将一个变量的值交给另外一个变量,不会出现不相等的情况,究竟是什么导致了 pc 和 pd 不相等呢?我们将在《将派生类指针赋值给基类指针时到底发生了什么?》一节中解开谜底。

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

引用在本质上是通过指针的方式实现的,这一点已在《引用在本质上是什么,它和指针到底有什么区别》中进行了讲解,既然基类的指针可以指向派生类的对象,那么我们就有理由推断:基类的引用也可以指向派生类的对象,并且它的表现和指针是类似的。

修改上例中 main() 函数内部的代码,用引用取代指针:

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

运行结果:
Class A: m_a=4
Class B: m_a=4, m_b=40
Class C: m_c=400

ra、rb、rc 是基类的引用,它们都引用了派生类对象 d,并调用了 display() 函数,从运行结果可以发现,虽然使用了派生类对象的成员变量,但是却没有使用派生类的成员函数,这和指针的表现是一样的。

引用和指针的表现之所以如此类似,是因为引用和指针并没有本质上的区别,引用仅仅是对指针进行了简单封装,读者可以猛击《引用在本质上是什么,它和指针到底有什么区别》一文深入了解。

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

将派生类指针赋值给基类指针时到底发生了什么?

已剪辑自: http://c.biancheng.net/view/2285.html

通过上节最后一个例子我们发现,将派生类的指针赋值给基类的指针后,它们的值有可能相等,也有可能不相等。例如执行pc = pd;语句后,pc 的值为 0x9b1800,pd 的值为 0x9b17f8,它们不相等。

我们通常认为,赋值就是将一个变量的值交给另外一个变量,这种想法虽然没错,但是有一点要注意,就是赋值以前编译器可能会对现有的值进行处理。例如将 double 类型的值赋给 int 类型的变量,编译器会直接抹掉小数部分,导致赋值运算符两边变量的值不相等。请看下面的例子:

#include <iostream>
using namespace std;
int main(){
    double pi = 3.14159;
    int n = pi;
    cout<<pi<<", "<<n<<endl;
    return 0;
}

运行结果:
3.14159, 3

pi 的值是 3.14159,执行int n = pi;后 n 的值变为 3,虽然是赋值,但是 pi 和 n 的值并不相等。

将派生类的指针赋值给基类的指针时也是类似的道理,编译器也可能会在赋值前进行处理。要理解这个问题,首先要清楚 D 类对象的内存模型,如下图所示:

img

首先要明确的一点是,对象的指针必须要指向对象的起始位置。对于 A 类和 B 类来说,它们的子对象的起始地址和 D 类对象一样,所以将 pd 赋值给 pa、pb 时不需要做任何调整,直接传递现有的值即可;而 C 类子对象距离 D 类对象的开头有一定的偏移,将 pd 赋值给 pc 时要加上这个偏移,这样 pc 才能指向 C 类子对象的起始位置。也就是说,执行pc = pd;语句时编译器对 pd 的值进行了调整,才导致 pc、pd 的值不同。

下面的代码演示了将 pd 赋值给 pc 时编译器的调整过程:

pc = (C*)( (int)pd + sizeof(B) );

如果我们把 B、C 类的继承顺序调整一下,让 C 在 B 前面,如下所示:

class D: public C, public B

那么输出结果就会变为:
pa=0x317fc
pb=0x317fc
pc=0x317f8
pd=0x317f8
相信聪明的你能够自行分析出来。

C++多态和虚函数快速入门教程

已剪辑自: http://c.biancheng.net/view/2294.html

在《C++将派生类赋值给基类(向上转型)》一节中讲到,基类的指针也可以指向派生类对象,请看下面的例子:

#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)

上面的代码中,同样是p->display();这条语句,当 p 指向不同的对象时,它执行的操作是不一样的。同一条语句可以执行不同的操作,看起来有不同表现方式,这就是多态。

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

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

前面我们说过,通过指针调用普通的成员函数时会根据指针的类型(通过哪个类定义的指针)来判断调用哪个类的成员函数,但是通过本节的分析可以发现,这种说法并不适用于虚函数,虚函数是根据指针的指向来调用的,指针指向哪个类的对象就调用哪个类的虚函数。

但是话又说回来,对象的内存模型是非常干净的,没有包含任何成员函数的信息,编译器究竟是根据什么找到了成员函数呢?我们将在《C++虚函数表精讲教程,直戳多态的实现机制》一节中给出答案。

借助引用也可以实现多态

引用在本质上是通过指针的方式实现的,这一点已在《C++引用在本质上是什么,它和指针到底有什么区别?》中进行了讲解,既然借助指针可以实现多态,那么我们就有理由推断:借助引用也可以实现多态。

修改上例中 main() 函数内部的代码,用引用取代指针:

int main(){
    People p("王志刚", 23);
    Teacher t("赵宏佳", 45, 8200);
   
    People &rp = p;
    People &rt = t;
   
    rp.display();
    rt.display();
    return 0;
}

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

由于引用类似于常量,只能在定义的同时初始化,并且以后也要从一而终,不能再引用其他数据,所以本例中必须要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。从运行结果可以看出,当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。

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

多态的用途

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

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

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

运行结果:
Strike back!
–Army is fighting!
----99A(Tank) is fighting!
----WZ-10(Helicopter) is fighting!
----CJ-10(Missile) is fighting!
–AirForce is fighting!
----J-20(Fighter Plane) is fighting!
----CH-5(UAV) is fighting!
----H-6K(Bomber) is fighting!

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

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

C++虚函数注意事项以及构成多态的条件

已剪辑自: http://c.biancheng.net/view/2296.html

C++ 虚函数对于多态具有决定性的作用,有虚函数才能构成多态。上节《C++多态和虚函数快速入门教程》我们已经介绍了虚函数的概念,这节我们来重点说一下虚函数的注意事项。

  1. 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。

  2. 为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数。关于名字遮蔽已在《C++继承时的名字遮蔽》一节中进行了讲解。

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

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

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

  6. 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数,这点我们将在下节中讲解。

构成多态的条件

站在“学院派”的角度讲,封装、继承和多态是面向对象的三大特征,封装、继承分别在《C++类成员的访问权限以及类的封装》《C++继承和派生简明教程》中进行了讲解,而多态是指通过基类的指针既可以访问基类的成员,也可以访问派生类的成员。

下面是构成多态的条件:

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

下面的例子对各种混乱情形进行了演示:

#include <iostream>
using namespace std;
//基类Base
class Base{
public:
    virtual void func();
    virtual void func(int);
};
void Base::func(){
    cout<<"void Base::func()"<<endl;
}
void Base::func(int n){
    cout<<"void Base::func(int)"<<endl;
}
//派生类Derived
class Derived: public Base{
public:
    void func();
    void func(char *);
};
void Derived::func(){
    cout<<"void Derived::func()"<<endl;
}
void Derived::func(char *str){
    cout<<"void Derived::func(char *)"<<endl;
}
int main(){
    Base *p = new Derived();
    p -> func();  //输出void Derived::func()
    p -> func(10);  //输出void Base::func(int)
    p -> func("http://c.biancheng.net");  //compile error
    return 0;
}

在基类 Base 中我们将void func()声明为虚函数,这样派生类 Derived 中的void func()就会自动成为虚函数。p 是基类 Base 的指针,但是指向了派生类 Derived 的对象。

语句p -> func();调用的是派生类的虚函数,构成了多态。

语句p -> func(10);调用的是基类的虚函数,因为派生类中没有函数覆盖它。

语句p -> func("http://c.biancheng.net");出现编译错误,因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员。

什么时候声明虚函数

首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【学习QT必备的C++基础】C++继承、派生与多态 的相关文章

  • 嵌入式状态机的几种骚操作

    已剪辑自 https mp weixin qq com s tulMJ7S7oOqV01J2E 9Xg 1 状态机基本术语 现态 xff1a 是指当前所处的状态 条件 xff1a 又称为 事件 xff0c 当一个条件被满足 xff0c 将会
  • 玩RTOS这么久,一问原子操作,蒙了~

    已剪辑自 https mp weixin qq com s kvxcOHT xHtMAjQqJu7Y2g 外链图片转存失败 源站可能有防盗链机制 建议将图片保存下来直接上传 img C3f9Rrei 1668695258073 https
  • 瑞利信道:从原理到实现

    瑞利信道模型 瑞利信道模型是无线通信信道最重要 最基础的的仿真模型 无线信道中的平坦衰落信道基本上都是在瑞利信道模型的基础上修改而成 xff0c 比如应用同样广泛的莱斯信道就可以通过在瑞利信道的基础上简单的添加直流分量实现 xff0c 而频
  • 分享一个通用的嵌入式驱动层

    https mp weixin qq com s bzPg5SremHDeIiguzvUVFA
  • 这次把怎么做好一个PPT讲清-总体篇

    文章目录 一 背景二 图表化 图示化三 关键词设计四 版式层级五 逻辑关系图 1 xff09 常用逻辑 2 xff09 如何让逻辑关系图好看 六 对齐 分组和对比 对齐 分组 分组就是将同类得信息放在一起 xff0c 靠的更近一点 那么 x
  • 这次把怎么做好一个PPT讲清-画图篇

    文章目录 概述布尔运算PPT幻灯片中如何设置形状对象格式每一个图形既是一个形状 xff0c 又是一个文本框 如何用PPT来实现三维3D效果 xff0c 附参数设置详解怎么用ppt画三维立体图 PPT做3D可动样机 PPT做3D 动态图标 P
  • 这次把怎么做好一个PPT讲清-其他技巧篇

    文章目录 如何统一批量设置PPT的中文字体和英文字体ppt如何插入页码和时间工具 原料插入页码和时间的步骤 xff1a 注意事项 在Powerpoint幻灯片里显示总页数在Powerpoint 2010中添加幻灯片编号在Powerpoint
  • 这次把怎么做好一个PPT讲清-审美篇

    要提高审美 xff0c 主要是靠不断的看优秀的作品来知道什么是美的 xff0c 这个短时间很难速成 xff0c 只能靠不断的积累 如何做出具有高级感的PPT xff1f 已剪辑自 https zhuanlan zhihu com p 386
  • 这样做时间轴,让你的PPT更出彩!

    文章目录 方法一 xff1a 美化时间节点 方法二 xff1a 利用图片中的 轴 方法三 xff1a 时间轴不一定需要 轴 方法四 xff1a 把时间轴拆成数页 总结 已剪辑自 https zhuanlan zhihu com p 5667
  • PPT文字很多的排版,PPT图片很多的排版,PPT图文排版

    文章目录 专业设计师是如何把一个word变成PPT的 xff1f 搭建 骨架 xff0c 填充 血肉 内页的排版 对页面的可视化处理 PPT文字巨多 xff01 领导还不让删 xff0c 怎么排版才高大上 xff1f 排版技巧一 xff1a
  • PPT目录页怎么设计才高大上?告诉你一个万能排版法!

    已剪辑自 https zhuanlan zhihu com p 64526891 嗨 xff0c 各位木友们好呀 xff0c 我是小木 帅的人都知道 xff0c 一个完整的PPT xff0c 一般应该要有 封面 43 目录 43 过渡页 4
  • 这样做框架结构图,让你的PPT更有创意!

    已剪辑自 https zhuanlan zhihu com p 58834710 嗨 xff0c 各位木友们好呀 xff0c 我是小木 昨天 xff0c 有个跟我一样鸟人的鸟人让我帮忙做个框架结构图 xff1a 可惜当时我不在办公室 xff
  • 如何画架构图?

    平时做过一些系统设计 xff0c 也写过一些系统分析文章 xff0c 从组件 关系 交互等方面提供一些建议 xff0c 并用我之前写文章画的一些图举些例子 构成系统的组件通过形状 颜色 名称来逼近其概念 LevelDB 主要构件如上面 Le
  • 主定理的证明及应用举例

    主定理 主定理最早出现在 算法导论 中 xff0c 提供了分治方法带来的递归表达式的渐近复杂度分析 规模为n的问题通过分治 xff0c 得到a个规模为n b的问题 xff0c 每次递归带来的额外计算为c n d T n lt 61 aT n
  • 程序员怎样才能写出一篇好的博客或者技术文章?

    文章目录 来分享下鹅厂多位技术同学关于如何写好技术文章的经验 1 为什么要写文章 1 1 对作者的好处 1 1 1 复盘学习成果 xff0c 巩固知识理解 1 1 2 提升思考能力 1 1 3 传播技术知识 xff0c 积累技术资产 1 1
  • 数字孪生技术有没有真正的实用价值?

    作为一个数字孪生领域的技术公司负责人 xff0c 我尽可能用比较直白的话来描述一下我对数字孪生行业以及数字孪生价值的理解 纵观数字孪生相关的公司 xff0c 主要有两个流派 xff0c 一派是具有互联网基因的数字孪生创业公司 xff0c 一
  • 你在编程过程中养成了哪些好习惯?

    写工作日志 我一直有大量写笔记的习惯 编程的时候 xff0c 也经常遇到一些麻烦的问题 xff0c 思路转瞬即逝 xff0c 于是把所有这些思路记录下来 xff0c 会在以后的搜索中成为重要的灵感来源 我的工作日志里通常以项目为单位 xff
  • 如何让 PPT 中的表格更美观?

    这个不难 xff0c 不信你看 做PPT表格 xff0c 千万不要直接把Excel截个图粘贴到PPT里 或者网上找到相应的表格图片 xff0c 也直接粘贴到PPT中 这样做的PPT表格肯定不好看呀 今天和大家分享几个表格美化的小技巧 xff
  • 这次把怎么做好一个PPT讲清-演讲篇

    商务演讲与汇报 一 目标 xff1a 演讲必须有清晰的目标 演讲 xff1a 影响他人发生积极的 改变 注意 xff0c 目标就要设定的影响听众在听完你的演讲后发生积极的改变 xff1b 例 xff1a 5月初向领导做月度工作汇报 让领导在

随机推荐

  • 这次把怎么做好一个PPT讲清-动画篇

    干货预警 xff01 作为一位PPT发烧友看过诸多PPT案例 xff0c 分享几个高大上的动画效果 文末有福利 xff01 废话不多说 xff0c 直接上重点 xff0c 本文主要讲八个动画技巧 xff0c 我们来看先目录 xff1a 收藏
  • 重新认知发明,全网保姆级入门说明

    已剪辑自 https mp weixin qq com s IDQXYXpWQlaW1NyX36H2vQ 关注 林外的日课 公众号 xff1a 每日思考 xff0c 每周更新 发明 xff0c 是指对产品 方法或者其改进所提出的新的技术方案
  • 软件测试需要掌握哪些技术?

    文章目录 1 黑盒测试 白盒测试 灰盒测试1 1 黑盒测试1 2 白盒测试1 3 灰盒测试 2 自顶向下集成和自底向上集成各自的优缺点 2 1 自顶向下集成2 2 自底向上集成 3 按照开发阶段划分 xff0c 软件测试可以分为哪几个流程
  • 画时序图的四个好用的工具~

    已剪辑自 https mp weixin qq com s xvCOLaGARp15vCRq6w8h2Q 分享几个画时序图的软件 xff0c 一些通信协议 xff0c 如I2C SPI UART MIPI等 xff0c 都会涉及到时序 Ti
  • Google软件工程:什么是软件工程

    文章目录 编程 软件工程软件工程的3个特性时间与变化海勒姆定律 xff08 Hyrum s Law xff09 目标不是 没有变化 规模和效率左移思维 权衡和成本最后 已剪辑自 https mp weixin qq com s GhYfH3
  • 多普勒失真信号重采样的Matlab仿真分析

    多普勒失真信号重采样的Matlab仿真分析 应用场景 水声通信指的是使用声信号在水中传输数据 相对而言 xff0c 电磁信号在水中吸收严重衰减过快 xff0c 光信号受水中悬浮颗粒的影响 xff0c 也无法完成远距离传输 这两种信号的传播距
  • 嵌入式为何钟爱SourceInsight,主要因为这个功能~

    已剪辑自 https mp weixin qq com s F gafwbZswpnY8EaCz8HxQ 不管是玩单片机还是嵌入式linux xff0c 只要是与硬件结合比较紧密的部分目前基本上还是C语言 xff0c 当然了 xff0c 不
  • 华科师兄最近的六条感悟

    已剪辑自 https mp weixin qq com s P6vz2zDTnCNli0GdyKX98Q 孤独之前是迷茫 孤独之后便是成长 曾经我是一个无法承受孤独的人 xff0c 无法和自己独处 xff0c 喜欢喧嚣的感觉 慢慢我发现这种
  • 一个优秀的程序员应该养成哪些好的习惯?

    文章目录 一 写代码前先想好思路 xff0c 先规划框架 xff0c 再到局部实现二 注重代码风格三 注重代码执行效率四 掌握一些编码原则五 解决问题时 xff0c 对于原理性的问题 xff0c 不要面向搜索引擎编程 六 注重基础知识的学习
  • 嵌入式开发中的防御性C语言编程

    嵌入式产品的可靠性自然与硬件密不可分 xff0c 但在硬件确定 并且没有第三方测试的前提下 xff0c 使用防御性编程思想写出的代码 xff0c 往往具有更高的稳定性 防御性编程首先需要认清C语言的种种缺陷和陷阱 xff0c C语言对于运行
  • PPT 最后一页写什么结束语既得体又能瞬间提升格调?

    谢邀 xff01 我只分享一个现下最流行的方法 xff0c 绝对让尾页逼格满满 xff01 罗永浩雷军都在用的 金句法 提到这份方法 xff0c 你可能会觉得很陌生 xff0c 但你一定见过这样的页面 xff1a 这样的页面还有很多 xff
  • Qt控件和事件

    文章目录 什么是 Qt 控件什么是Qt事件总结 已剪辑自 http c biancheng net view vip 9651 html Qt 是一个著名的 GUI 框架 xff0c 用来开发和用户交互的图形界面 作为 GUI 框架 xff
  • Qt信号和槽机制详解

    文章目录 信号和槽connect 函数实现信号和槽实例演示信号和槽机制 已剪辑自 http c biancheng net view vip 9652 html 信号和槽是 Qt 特有的消息传输机制 xff0c 它能将相互独立的控件关联起来
  • Qt QLabel文本框的使用

    文章目录 QLabel文本框的使用QLabel文本框的信号和槽实例演示QLabel文本框的用法 已剪辑自 http c biancheng net view vip 9653 html QLabel 是 Qt 帮我们写好的一个控件类 xff
  • Qt QPushButton按钮用法详解

    文章目录 QPushButton按钮的创建QPushButton按钮的使用QPushButton按钮的信号和槽实例演示QPushButton按钮用法 已剪辑自 http c biancheng net view vip 9654 html
  • Qt QLineEdit单行输入框用法详解

    已剪辑自 http c biancheng net view vip 9655 html QLineEdit 是 Qt 提供的一个控件类 xff0c 它直接继承自 QWdiget 类 xff0c 专门用来创建单行输入框 xff0c 如下图所
  • 使用python开发json、csv数据格式转换工具

    使用python开发json csv数据格式转换工具 json和xml是业界常用的数据格式 xff0c 而游戏行业经常使用csv配表 xff0c 包括本地化文本和数值 本文介绍csv和json序列化 逆序列化相关的python库 xff0c
  • 【学习QT必备的C++基础】C++类和对象

    文章目录 C 43 43 类的定义和对象的创建详解类的定义创建对象访问类的成员使用对象 指针 http c biancheng net c 80 总结 C 43 43 类的成员变量和成员函数详解在类体中和类体外定义成员函数的区别 C 43
  • 【学习QT必备的C++基础】C++引用精讲,C++ &用法全面剖析

    文章目录 C 43 43 引用10分钟入门教程C 43 43 引用作为函数参数C 43 43 引用作为函数返回值 C 43 43 引用在本质上是什么 xff0c 它和指针到底有什么区别 xff1f 引用和指针的其他区别 C 43 43 引用
  • 【学习QT必备的C++基础】C++继承、派生与多态

    文章目录 C 43 43 继承和派生简明教程C 43 43 三种继承方式public protected private 修饰类的成员public protected private 指定继承方式改变访问权限 C 43 43 继承时的名字遮