C++ 继承详解:从基础到深入
继承是面向对象编程中最强大的功能之一,它不仅促进了代码的重用,还帮助我们构建复杂的系统。在C++中,通过继承,我们可以创建一个新的类(称为派生类)来扩展现有类(基类)的功能。本文将全面探讨C++中的继承机制,从基础定义到复杂的菱形继承问题。
1. 继承的基本概念
在C++中,继承允许派生类继承基类的属性和方法。例如,如果我们有一个Person
类,它具有name
和age
属性,我们可以创建一个Student
类继承Person
,并添加特有的studentID
属性。
class Person {
protected:
string name;
int age;
};
class Student : public Person {
private:
int studentID;
};
在这个例子中,Student
自动获得Person
类的属性,展示了如何通过继承复用代码。
2. 继承类型与成员访问修饰符
C++支持三种继承方式:public、protected和private。这些修饰符影响派生类中基类成员的访问级别。
- Public继承:基类的public成员在派生类中也是public的,而基类的protected成员在派生类中保持protected。
- Protected继承:基类的public和protected成员在派生类中均变为protected。
- Private继承:基类的public和protected成员在派生类中均变为private。
2.1继承基类成员访问方式的变化
3.基类和派生类对象赋值转换
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类 的指针是指向派生类对象时才是安全的。
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
void Test ()
{
Student sobj ;
// 1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj ;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派生类对象
sobj = pobj;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj
Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
ps1->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问
题
ps2->_No = 10;
}
4.继承中的作用域
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected :
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;
cout<<" 身份证号:"<<Person::_num<< endl;
cout<<" 学号:"<<_num<<endl;
}
protected:
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" <<i<<endl;
}
};
void Test(){
B b;
b.fun(10);
};
5.派生类的默认成员函数
6 个默认成员函数, “ 默认 ” 的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
6.复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
下图是菱形继承的内存对象成员模型:这里可以看到数据冗余
7.继承的总结和反思
8.笔试面试题
1. 菱形继承,又称为“钻石继承”,是指有一个类继承自两个具有共同基类的类。这种继承结构形成了一个类图的菱形结构。例如:
A
/ \
B C
\ /
D
在这个例子中,类D继承自类B和C,而类B和C都继承自类A。这种结构在不使用特殊措施的情况下,会引起以下问题:
- 数据冗余:类D的实例会包含两个独立的类A的实例(一个通过类B继承来的,另一个通过类C继承来的),这导致数据冗余。
- 二义性:如果类A有一个成员变量或方法,类D在访问这个成员时会遇到不明确的问题,编译器不知道是应该通过B的A部分还是C的A部分来访问它,因此会产生编译错误。
2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的问题?
菱形虚拟继承是解决菱形继承问题的一种方法,通过在继承时使用virtual
关键字来声明。继续使用上述的例子,可以这样声明:
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
使用虚拟继承后,C++确保在继承层次中只有一个A的实例被包括在内。这解决了数据冗余问题,因为现在类D中只包含一个A的实例。同时,这也消除了二义性,因为无论通过B还是C访问A的成员,都是访问的同一个实例。
3. 继承和组合的区别?什么时候用继承?什么时候用组合?
继承和组合是面向对象设计中两种基本的代码复用方式。
- 继承表达的是“是一个(is-a)”的关系。比如,“学生是一个人”,这里学生(Student)类可以继承人(Person)类。继承通常用于表示一种类型和子类型的关系,它可以实现接口的一致性和多态性。
- 组合表达的是“有一个(has-a)”的关系。例如,“汽车有一个引擎”,这里汽车(Car)类可以包含引擎(Engine)类的实例作为其成员。组合用于构建复杂的类型,由多个部分以明确的界面组成。
何时使用继承:
- 当子类真正代表超类的子类型,并且可以替代超类时。
- 当你希望在多个派生类中共享代码时。
- 当你希望利用多态性质。
何时使用组合:
- 当你希望某个类包含某些功能,但又不通过接口继承它们时。
- 当你需要复用多个类的实现,但它们不构成一个真正的子类型关系时。
- 当你希望避免继承带来的过度耦合和脆弱的基类问题时。
原文地址:https://blog.csdn.net/2301_77754590/article/details/140508681
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!