目录
一、继承的概念和定义
1.1 - 概念
1.2 - 定义
二、继承时的对象内存模型
三、向上转型和向下转型
四、继承时的名字遮蔽问题
4.1 - 有成员变量遮蔽时的内存分布
4.2 - 重名的基类成员函数和派生类成员函数不构成重载
C++ 中的继承是类与类之间的关系,是一个很简单很直观的概念,与现实世界中的继承类似,例如儿子继承父亲的财产。
继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程,例如类 B 继承于类 A,那么 B 就拥有 A 的成员变量和成员函数。
被继承的类称为父类或基类,继承的类称为子类或派生类。"父类" 和 "子类" 通常放在一起称呼,"基类" 和 "派生类" 通常放在一起称呼。
#include
using namespace std;
// 基类 Person
class Person
{
public:
void SetName(const char* name = "张三") { _name = name; }
void SetAge(int age = 18) { _age = age; }
const char* GetName() const { return _name.c_str(); }
int GetAge() const { return _age; }
protected:
string _name; // 姓名
int _age; // 年龄
};
// 派生类 Student
class Student : public Person
{
public:
void SetStuId(int stu_id = 0) { _stu_id = stu_id; }
int GetStuId() const { return _stu_id; }
protected:
int _stu_id; // 学号
};
// 派生类 Teacher
class Teacher : public Person
{
public:
void SetJobId(int job_id) { _job_id = job_id; }
int GetJobId() const { return _job_id; }
protected:
int _job_id; // 工号
};
int main()
{
Student s;
s.SetName("李四");
s.SetAge(19);
s.SetStuId(1);
cout << s.GetName() << "的年龄是" << s.GetAge() <<
",学号是" << s.GetStuId() << "。" << endl;
// 李四的年龄是19,学号是1。
Teacher t;
t.SetName("王五");
t.SetAge(30);
t.SetJobId(10);
cout << t.GetName() << "的年龄是" << t.GetAge() <<
",工号是" << t.GetJobId() << "。" << endl;
// 王五的年龄是30, 工号是10。
return 0;
}
通过以上的例子我们就可以明白:继承机制是面向对象程序中使代码可以复用的重要手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。继承呈现了面向对象呈现设计的层次结构,体现了由简单到复杂的认知过程,以前我们接触的复用是模板,继承则是类设计层次的复用。
继承的定义格式为:
class 派生类名 : [继承方式] 基类名
{
派生类新增加的成员
}
不同的继承方式会影响基类成员在派生类中的访问权限:
基类成员/继承方式 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
基类的 public 成员 | 派生类的 public 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
基类的 protected 成员 | 派生类的 protected 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
基类的 private 成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结:
不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问和调用)。注意:我们说的是基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基类的 private 成员是能够被继承的,并且成员变量会占用派生类对象的内存,它只是在派生类中无法使用罢了,即不可见。
如果希望基类的成员既不向外暴露,即不能通过对象访问,还能在派生类中使用,那么只能声明为 protected,由此可以看出 protected 访问限定符是因为继承才出现的。
通过上面的表格我们可以发现,基类的 private 成员在派生类中不可见,基类的其他成员在派生类中的访问权限 = min(成员在基类中的访问权限,继承方式),比较规则是:public > protected > private。
使用 class 关键字时,默认是 private 继承,使用 struct 关键字时,默认是 public 继承,不过最好显示地写出继承方式。
在实际运用中一般都是使用 public 继承,几乎很少使用 protected/ private 继承,也不提倡使用 protected/ private 继承,因为 protected/ private 继承下来的成员只能在派生类的类里面使用,实际中扩展维护性不强。
没有继承时,对象内存模型很简单,对象的内存中只包含成员变量,存储在栈区或堆区(使用 new 创建对象),成员函数与对象内存分离,存储在代码区。
当有继承关系时,派生类对象的内存模型可以看成是基类成员变量和新增成员变量的总和,而所有成员函数仍然存储在另外一个区域——代码区,由所有对象共享。
#include
using namespace std;
class A
{
public:
int _i;
int _j;
};
class B : public A
{
public:
int _k;
};
class C : public B
{
public:
int _l;
};
int main()
{
A a;
cout << &a << endl;
cout << &a._i << " " << &a._j << endl;
cout << sizeof(A) << endl;
B b;
cout << &b << endl;
cout << &b._i << " " << &b._j << " " << &b._k << endl;
cout << sizeof(B) << endl;
C c;
cout << &c << endl;
cout << &c._i << " " << &c._j << " " << &c._k << " " << &c._l << endl;
cout << sizeof(C) << endl;
return 0;
}
即:
可以发现,基类的成员变量排在前面,新增的成员变量排在后面。
可以用 "派生类对象/派生类指针/派生类引用" 给 "基类对象/基类指针/基类引用" 初始化或赋值,这在 C++ 中称为向上转型(Upcasting
),还有一个形象的说法叫切片或切割。向上转型非常安全,可以由编译器自动完成。
相应地,用基类给派生类初始化或赋值称为向下转型(Downcasting
)。向下转型则有风险,需要程序员手动干预。
不能用 "基类对象" 给 "派生类对象" 初始化或赋值,因为基类不包含派生类的成员变量,所以无法对派生类的成员变量赋值。
"基类指针或者引用" 可以通过强制类型转换的方式给 "派生类指针或引用" 初始化或赋值,但是必须是 "基类指针" 指向 "派生类对象" 时才是安全的。这里基类如果是多态类型,可以使用 RTTI(Run-Time Type Information)的 dynamic_cast 识别后进行安全转换(后面再详细讲解)。
#include
using namespace std;
class Person
{
protected:
string _name;
int _age;
};
class Student : public Person
{
public:
int _stu_id;
};
int main()
{
Student s;
Person p = s;
Person* pp = &s;
Person& rp = s; // 由此可以发现,向上转型的过程中没有发生隐式类型转换
// s = p; // error
Student* ps = (Student*)pp;
ps->_stu_id = 1;
// 基类指针 pp 指向的是派生类对象 s,所以转换是安全的
// pp = &p;
// ps = (Student*)pp;
// ps->_stu_id = 0; // 越界访问
// 这种情况下,强制类型转换是可行的,但不安全
return 0;
}
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用,以及通过派生对象访问该成员时,实际上使用的是派生类新增的成员,而不是从基类继承来的。
#include
using namespace std;
class A
{
public:
void Print() { cout << _i << " " << _j << endl; }
public:
int _i = 1;
int _j = 2;
};
class B : public A
{
public:
// 遮蔽基类的成员函数
void Print() { cout << _i << " " << _j << " " << _k << endl; }
public:
int _j = 3; // 遮蔽基类的成员变量
int _k = 4;
};
int main()
{
B b;
// 使用的是派生类新增的成员,而不是从基类继承来的
cout << b._j << endl; // 3
b.Print(); // 1 3 4
// 使用的是基类继承来的成员
cout << b.A::_j << endl; // 2
b.A::Print(); // 1 2
return 0;
}
cout << &b._i << " " << &b.A::_j << " " << &b._j << " " << &b._k << endl;
cout << sizeof(B) << endl;
即:
当基类 A 的成员变量被遮蔽时,仍然会留在派生类对象 b 的内存中,B 类新增的成员变量也始终排在基类 A 的成员变量的后面。
类其实是一种作用域,每个类都会定义它自己的作用域,在这个作用域类我们再定义类的成员。当存在继承关系时,派生类的作用域嵌套在基类的作用域之内,如果一个名字在派生类的作用域内无法找到,编译器会继续到外层的基类作用域中查找该名字的定义。
在上面的例子中,B 继承自 A,它们作用域嵌套的嵌套关系如下:
函数重载指的是在同一个作用域内有多个名称相同但参数列表不同的函数,所以重名的基类成员函数和派生类成员函数不会构成重载,即便参数一样,也不会报错。