1.继承
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
//name:peter
//age:18
//name:peter
//age:18
2 继承的定义
2.1格式
Person是父类,也称作基类。Teacher是子类,也称作派生类。
2.2继承关系和访问限定符
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的protected 成员 | 派生类的protected成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的private成 员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
-
基类
private
成员的访问:- 基类中的
private
成员在派生类中确实是不可见的。无论继承方式如何,这些成员对派生类都是不可访问的。 - 如果需要在派生类中访问这些成员,可以通过基类提供
protected
或public
方法间接访问,或者在基类中提供友元类或友元函数。
- 基类中的
-
基类
protected
成员的访问:- 基类中的
protected
成员在派生类中是可访问的,允许派生类访问和修改这些成员,但它们在派生类外部仍然不可见。 protected
成员的访问权限是为了支持派生类访问,限制了直接外部访问。
- 基类中的
-
继承方式与访问控制:
- 基类的
private
成员在子类中是不可见的,protected
成员在子类中可见。实际访问方式取决于基类成员的访问修饰符和继承方式的组合。 - 继承方式(
public
、protected
、private
)会影响基类成员在派生类中的访问级别。通常,public
继承使基类的public
成员保持public
,protected
继承使基类的public
和protected
成员都变为protected
,private
继承则使基类的所有public
和protected
成员都变为private
。
- 基类的
-
默认继承方式:
class
默认使用private
继承。struct
默认使用public
继承。- 明确指定继承方式可以提高代码的可读性和维护性,避免由于默认继承方式带来的混淆。
-
继承的使用建议:
- 在实际编程中,
public
继承是最常用的,因为它能够支持广泛的代码复用和接口设计。 protected
和private
继承通常用于更特殊的场景,比如在实现某些设计模式时。protected
继承可以防止外部代码直接使用基类的接口,而private
继承则主要用于实现内部细节的封装。
- 在实际编程中,
2.3实例演示三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
public :
void Print ()
{
cout<<_name <<endl;
}
protected :
string _name ; // 姓名
private :
int _age ; // 年龄
};
public
继承
class Student : public Person
{
protected:
int _stunum; // 学号
};
Student s;
s.Print(); // 可以访问
// s._name; // 不可访问,编译错误
// s._age; // 不可访问,编译错误
protected
继承
class Student : protected Person
{
protected:
int _stunum; // 学号
};
Student s;
// s.Print(); // 不可访问,编译错误
// s._name; // 不可访问,编译错误
// s._age; // 不可访问,编译错误
private
继承-
class Student : private Person { protected: int _stunum; // 学号 }; Student s; // s.Print(); // 不可访问,编译错误 // s._name; // 不可访问,编译错误 // s._age; // 不可访问,编译错误
3.基类和派生类对象赋值转换
- 派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。
3.1. 对象切片(Object Slicing)
- 当派生类对象赋值给基类对象时,只有派生类中的基类部分会被复制,这个过程称为“切片”。
- 切片后的基类对象不再包含派生类独有的数据和行为,只保留基类部分的内容。
class Base {
public:
int baseVar;
};
class Derived : public Base {
public:
int derivedVar;
};
Derived d;
d.baseVar = 1;
d.derivedVar = 2;
Base b = d; // 切片,Base 对象只保留 baseVar,丢失 derivedVar
3.2. 基类对象不能赋值给派生类对象
基类对象不包含派生类的所有信息,因此无法赋值给派生类对象。这会导致派生类独有的成员无法初始化或被错误地初始化。
3.3指针和引用的类型转换
向上转换(Upcasting):派生类的指针或引用可以赋值给基类的指针或引用。这是安全的,因为派生类总是包含基类的部分。
Derived* d = new Derived();
Base* b = d; // 向上转换
向下转换(Downcasting):基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用,但前提是这个基类指针或引用实际指向的是派生类的对象,否则会导致未定义行为。
- 在多态情况下,可以使用
dynamic_cast
进行向下转换,并通过 RTTI 确保转换的安全性:
Base* b = new Derived(); //这个基类指针或引用实际指向的是派生类的对象
Derived* d = dynamic_cast<Derived*>(b); // 安全的向下转换
if (d != nullptr) {
// 转换成功,可以安全使用 d
}
- RTTI(Run-Time Type Information):C++ 提供了
dynamic_cast
来在运行时检查和转换类型。dynamic_cast
只能用于多态类型(即至少有一个虚函数的类),它会在类型不匹配时返回nullptr
。
4.继承中的作用域
4.1. 独立的作用域
- 在 C++ 中,基类和派生类各自拥有独立的作用域。这意味着即使在派生类中定义了与基类同名的成员,它们也属于不同的作用域。
- 由于作用域不同,在派生类中定义的同名成员会“隐藏”基类的成员,这种隐藏可以通过显式指定作用域来解决。
4.2. 成员隐藏(Name Hiding)
- 当子类和父类有同名成员时,子类的成员会隐藏父类的同名成员。这种隐藏机制适用于成员变量和成员函数。
- 访问隐藏成员:如果需要访问被隐藏的基类成员,可以使用
基类名::成员名
的形式来访问。class Base { public: int value = 10; void display() { cout << "Base display" << endl; } }; class Derived : public Base { public: int value = 20; // 隐藏了 Base::value void display() { cout << "Derived display" << endl; } // 隐藏了 Base::display() }; Derived d; cout << d.value << endl; // 输出 20,访问的是 Derived::value cout << d.Base::value << endl; // 输出 10,访问的是 Base::value d.display(); // 输出 "Derived display" d.Base::display(); // 输出 "Base display"
4.3. 成员函数的隐藏
- 成员函数的隐藏仅仅依赖于函数名是否相同,而不考虑参数列表(即使参数列表不同,基类的函数依然会被隐藏)。
- 这种情况在函数重载和继承结合时容易导致混淆,因此在派生类中重新定义基类函数时需要特别小心。
4.4. 避免同名成员
- 在实际开发中,尽量避免在继承体系中定义同名成员,尤其是函数和变量。这不仅可以减少隐藏问题,也可以提升代码的可读性和维护性。
5.派生类的默认成员函数
5.1. 构造函数
- 在派生类的构造函数中,必须先调用基类的构造函数来初始化基类部分。如果基类没有默认构造函数,必须在派生类的初始化列表中显式调用基类的构造函数。
class Base {
public:
Base(int x) : value(x) {}
protected:
int value;
};
class Derived : public Base {
public:
Derived(int x, int y) : Base(x), derivedValue(y) {}
protected:
int derivedValue;
};
5.2. 拷贝构造函数
- 派生类的拷贝构造函数会调用基类的拷贝构造函数,以确保基类部分被正确复制。
Derived(const Derived& other) : Base(other), derivedValue(other.derivedValue) {}
5.3. 赋值运算符 operator=
- 派生类的赋值运算符必须调用基类的赋值运算符来处理基类部分的赋值。通常是在派生类的赋值操作中先调用基类的赋值运算符,然后再处理派生类自身的数据成员。
Derived& operator=(const Derived& other) {
if (this != &other) {
Base::operator=(other); // 调用基类的赋值运算符
derivedValue = other.derivedValue;
}
return *this;
}
5.4. 析构函数
- 派生类的析构函数在执行完派生类的清理工作后,自动调用基类的析构函数,确保基类部分也被正确清理。这种顺序确保资源的释放顺序与分配顺序相反。
~Derived() {
// 派生类的清理工作
// 自动调用基类的析构函数
}
5.5. 构造和析构顺序
- 构造函数的调用顺序是:先调用基类构造函数,再调用派生类构造函数。
- 析构函数的调用顺序则相反:先调用派生类析构函数,再调用基类析构函数。
5.6. 析构函数的隐藏与重写
- 如果基类析构函数没有声明为
virtual
,派生类的析构函数会隐藏基类的析构函数。这意味着通过基类指针删除派生类对象时,只有基类部分会被释放,可能导致资源泄露。因此,如果类可能作为基类使用,析构函数通常应声明为virtual
。
class Base {
public:
virtual ~Base() {} // 推荐用法
};
class Derived : public Base {
public:
~Derived() {} // 派生类析构函数
};
6.继承与友元
- 友元关系不受继承方式的影响,友元函数和友元类可以访问被友元声明类的所有成员。基类的友元函数或友元类不能访问派生类的私有或受保护成员,除非在派生类中重新声明它们为友元。友元关系在继承层次中是不可传递的。
#include <iostream>
using namespace std;
class Student; // 前向声明
class Person {
public:
Person(const string& name = "") : _name(name) {}
friend void Display(const Person& p, const Student& s); // 友元函数声明
protected:
string _name; // 姓名
};
class Student : public Person {
public:
Student(const string& name = "", int stuNum = 0) : Person(name), _stuNum(stuNum) {}
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s) {
cout << "Name: " << p._name << endl;
cout << "Student Number: " << s._stuNum << endl;
}
int main() {
Person p("Alice");
Student s("Bob", 12345);
Display(p, s);
return 0;
}
7.继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例 。
#include <iostream>
using namespace std;
class Person {
public:
Person() { ++_count; } // 构造函数中增加计数
static int _count; // 静态数据成员声明
protected:
string _name; // 姓名
};
int Person::_count = 0; // 静态数据成员定义
class Student : public Person {
protected:
int _stuNum; // 学号
};
class Graduate : public Student {
protected:
string _seminarCourse; // 研究科目
};
void TestPerson() {
Student s1;
Student s2;
Student s3;
Graduate s4;
// 输出人数
cout << "人数: " << Person::_count << endl; //4
// 注意: 直接访问和修改 Person::_count 是有效的,因为静态数据成员是共享的
// Student::_count = 0; // 这行代码实际上不影响 Person::_count,因为静态数据成员是共享的
// 重置静态数据成员 (这行代码将 Person::_count 设置为0)
Person::_count = 0;
// 输出人数
cout << "人数: " << Person::_count << endl; //0
}
int main() {
TestPerson();
return 0;
}
8.复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在Father的对象中Person成员会有两份。
class Person
{
public :
string _name ; // 姓名
};
class Student : public Person
{
protected :
int _num ; //学号
};
class Teacher : public Person
{
protected :
int _id ; // 职工编号
};
class Father : public Student, public Teacher
{
protected :
string _childname ; // 孩子名字
};
void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个
Father a ;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class father : public Student, public Teacher
{
protected :
string _childname ;
};
void Test ()
{
Assistant a ;
a._name = "peter";
}
8.1虚拟继承解决数据冗余和二义性的原理
在内存中,A的内存地址会在D后面,这里B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
9.继承vs组合
-
继承(is-a)每个派生类对象都是一个基类对象。:
- 特点:派生类是基类的一种类型,继承基类的接口和实现。继承实现了类的复用,但可能会破坏基类的封装。
- 优点:简单直接,适合实现明确的“是一个”关系。例如,
Cat
是Animal
的一种。 - 缺点:耦合度高,基类的变化可能影响到所有派生类。继承可能导致复杂的继承层次结构(如菱形继承)。
#include <iostream>
#include <string>
using namespace std;
class Shape {
public:
Shape(const string& color) : _color(color) {}
virtual void draw() const = 0; // 纯虚函数,强制子类实现
protected:
string _color; // 颜色属性
};
class Circle : public Shape {
public:
Circle(const string& color, double radius)
: Shape(color), _radius(radius) {}
void draw() const override {
cout << "Drawing a " << _color << " circle with radius " << _radius << endl;
}
private:
double _radius;
};
class Rectangle : public Shape {
public:
Rectangle(const string& color, double width, double height)
: Shape(color), _width(width), _height(height) {}
void draw() const override {
cout << "Drawing a " << _color << " rectangle with width " << _width << " and height " << _height << endl;
}
private:
double _width, _height;
};
int main() {
Circle c("red", 5.0);
Rectangle r("blue", 4.0, 6.0);
c.draw();
r.draw();
return 0;
}
-
组合(has-a)假设B组合了A,每个B对象中都有一个A对象:
- 特点:类通过包含其他类的对象来实现功能,强调“有一个”关系。组合对象的内部细节对外部是隐藏的。
- 优点:低耦合度,组合对象的变化不会影响到其他类。组合支持更灵活的设计,符合“黑箱复用”原则。
- 缺点:组合可能需要更多的代码来实现对象之间的协调和交互。
#include <iostream>
#include <string>
using namespace std;
class Color {
public:
Color(const string& color) : _color(color) {}
string getColor() const { return _color; }
private:
string _color;
};
class Shape {
public:
Shape(const Color& color) : _color(color) {}
virtual void draw() const = 0; // 纯虚函数,强制子类实现
protected:
Color _color; // 通过组合来管理颜色
};
class Circle : public Shape {
public:
Circle(const Color& color, double radius)
: Shape(color), _radius(radius) {}
void draw() const override {
cout << "Drawing a " << _color.getColor() << " circle with radius " << _radius << endl;
}
private:
double _radius;
};
class Rectangle : public Shape {
public:
Rectangle(const Color& color, double width, double height)
: Shape(color), _width(width), _height(height) {}
void draw() const override {
cout << "Drawing a " << _color.getColor() << " rectangle with width " << _width << " and height " << _height << endl;
}
private:
double _width, _height;
};
int main() {
Color red("red");
Color blue("blue");
Circle c(red, 5.0);
Rectangle r(blue, 4.0, 6.0);
c.draw();
r.draw();
return 0;
}
- 则设计原
- 优先使用组合:组合提供了更多的灵活性和更低的耦合度。通过组合,你可以创建更复杂的对象而不依赖于继承体系的复杂性。
- 合理使用继承:当类之间存在明确的“is-a”关系,并且继承不会导致过度的耦合和复杂性时,继承是合适的选择。继承在实现多态和共享行为时是必要的。
- 理解封装:组合支持更好的封装,因为对象的内部实现对外部是隐藏的,而继承可能暴露了基类的内部实现。