访问继承成员(C++)
访问继承成员
- 一、访问从基类继承的成员
- 1.1 继承的基本概念
- 1.2 如何访问继承的成员
- 二、为什么要用指针去访问成员
- 2.1 指针的基本用途
- 2.2 多态性的实现
- 2.3 使用指针的其它优势
- 三、总结
- 底层了解
- 背景知识:什么是 vtable 和 vptr?
- 代码示例:虚函数、多态、vtable
- 三、底层原理剖析
- 3.1 编译器的处理方式(伪代码/概念图)
- 四、对象切片问题
- 五、vtable 可视化演示(示意图)
- 六、验证 vtable 的存在(高级)
- 七、总结:指针 + 虚函数的意义
一、访问从基类继承的成员
1.1 继承的基本概念
当一个类(子类 / 派生类)从另一个类(父类 / 基类)继承时,它可以获取基类的成员变量和成员函数。继承方式分为三种:
class Base {
public:int a;
protected:int b;
private:int c;
};class Derived : public Base {// public继承方式下,a是public,b是protected,c不可访问
};
继承方式 | 基类 public 成员在派生类中 | 基类 protected 成员在派生类中 | 基类 private 成员在派生类中 |
---|---|---|---|
public | 仍是 public | 仍是 protected | 不可访问 |
protected | 变成 protected | 变成 protected | 不可访问 |
private | 不可访问 | 不可访问 | 不可访问 |
1.2 如何访问继承的成员
-
在子类内部,可以直接访问可见的基类成员;
-
在子类对象中,也可以通过对象或指针访问。
class Base {
public:int x = 5;void show() { std::cout << "Base::show()" << std::endl; }
};class Derived : public Base {
public:void print() {std::cout << "x = " << x << std::endl; // 直接访问show(); // 调用基类函数}
};int main() {Derived d;d.print(); // 间接访问 Base 成员d.show(); // 直接访问 Base 的函数std::cout << d.x << std::endl; // 访问 Base 的变量
}
二、为什么要用指针去访问成员
使用指针(特别是基类指针指向派生类对象)是 C++ 多态性的核心。
2.1 指针的基本用途
-
可以延迟绑定(多态):运行时决定调用哪个函数。
-
可以动态分配和释放内存。
-
可以通过统一接口操作不同对象。
2.2 多态性的实现
使用指针的主要好处体现在虚函数的多态调用中:
class Base {
public:virtual void show() { std::cout << "Base" << std::endl; }
};class Derived : public Base {
public:void show() override { std::cout << "Derived" << std::endl; }
};int main() {Base* ptr = new Derived(); // 基类指针指向派生类对象ptr->show(); // 输出 "Derived":多态行为delete ptr;
}
如果不用指针,而是直接用对象,多态将不会生效:
Base obj = Derived(); // 对象切片(object slicing)
obj.show(); // 输出 "Base" 而不是 "Derived"
2.3 使用指针的其它优势
-
可以轻松地传递对象到函数中(避免对象复制);
-
能够管理对象生命周期;
-
在容器中统一管理不同类型的对象指针(如抽象基类指针);
三、总结
场景 | 描述 |
---|---|
派生类访问基类成员 | 直接在派生类中使用继承来的成员或方法 |
为什么用指针访问 | 实现多态(虚函数),避免对象切片,统一接口管理对象 |
推荐用法: |
-
如果你需要多态,请使用基类指针或引用;
-
如果只是继承成员、没有虚函数需求,可以直接使用对象访问继承来的成员;
底层了解
背景知识:什么是 vtable 和 vptr?
在 C++ 中,为了实现运行时多态(即虚函数机制),编译器通常采用以下技术:
名称 | 含义 |
---|---|
vtable(虚函数表) | 每个类都有一张表,存储着该类的虚函数地址 |
vptr(虚函数指针) | 每个对象中都有一个隐藏的指针,指向其类的 vtable |
代码示例:虚函数、多态、vtable
#include <iostream>
using namespace std;class Base {
public:virtual void func1() { cout << "Base::func1()" << endl; }virtual void func2() { cout << "Base::func2()" << endl; }void normal() { cout << "Base::normal()" << endl; }
};class Derived : public Base {
public:void func1() override { cout << "Derived::func1()" << endl; }void func2() override { cout << "Derived::func2()" << endl; }void extra() { cout << "Derived::extra()" << endl; }
};int main() {Base* ptr = new Derived(); // 基类指针指向派生类对象ptr->func1(); // 调用 Derived::func1() —— 多态!ptr->func2(); // 调用 Derived::func2()ptr->normal(); // 调用 Base::normal() —— 非虚函数// ptr->extra(); // 错误:Base 类型中没有 extra()delete ptr;return 0;
}
输出:
Derived::func1()
Derived::func2()
Base::normal()
三、底层原理剖析
3.1 编译器的处理方式(伪代码/概念图)
步骤 1:编译器为每个类建立 vtable
Base_vtable:[0] => &Base::func1[1] => &Base::func2Derived_vtable:[0] => &Derived::func1[1] => &Derived::func2
步骤 2:每个对象内部添加一个隐藏指针 vptr
Base 对象:
+-----------+
| vptr ---> Base_vtable
+-----------+Derived 对象:
+-----------+
| vptr ---> Derived_vtable
+-----------+
步骤 3:调用虚函数时,编译器生成类似这样的代码:
// ptr->func1(); 实际代码类似于:
(*(ptr->vptr)[0])(ptr); // 从 vtable 取出第一个函数指针调用
四、对象切片问题
Derived d;
Base b = d; // 对象切片,Derived 部分被丢弃b.func1(); // 调用 Base::func1(),不是 Derived::func1()
因为 vptr 属于对象,当你把一个 Derived 对象赋值给 Base,只拷贝了 Base 的那部分数据(包括其 vptr),所以不会保留 Derived 的虚函数表指针。
五、vtable 可视化演示(示意图)
+----------------+
Base* → | vptr ----------+----> Base_vtable(原指向)| | ↓+----------------+ [0] → Base::func1[1] → Base::func2如果 ptr = new Derived();+----------------+
Base* → | vptr ----------+----> Derived_vtable(多态关键)| | ↓+----------------+ [0] → Derived::func1[1] → Derived::func2
六、验证 vtable 的存在(高级)
在某些平台下你可以用强制类型转换,**“手动调用虚函数表中的函数”**来验证虚函数表存在(仅供理解用途):
typedef void(*Fun)();int main() {Derived d;Fun* vtable = (Fun*)*(long long*)&d; // 获取 vptr,再转为函数指针数组vtable[0](); // 调用 Derived::func1vtable[1](); // 调用 Derived::func2
}
七、总结:指针 + 虚函数的意义
优点 | 原因 |
---|---|
实现运行时多态 | vtable + vptr |
支持接口编程 | 可定义抽象类,派生类实现后用基类指针操作 |
避免对象切片 | 保留派生类的完整信息 |
动态绑定灵活 | 程序运行时选择函数行为 |