C++之继承

本文详细解释了面向对象编程中的继承机制,包括私有、保护和公共继承,以及派生类对象的赋值转换。讨论了继承中的作用域、默认成员函数和友元关系,强调了虚拟继承在解决菱形继承问题上的作用。同时还比较了继承和组合的区别,指导何时选择使用哪种复用方式。
摘要由CSDN通过智能技术生成

目录

1.继承的概念及定义

1.1继承的概念

 1.2 继承定义

1.2.1定义格式

1.2.2继承基类成员访问方式的变化

2.基类和派生类对象赋值转换

3.继承中的作用域

4.派生类的默认成员函数

5.继承与友元

6.继承与静态成员

7.复杂的菱形继承及菱形虚拟继承

8.继承和组合

9.笔试面试题


1.继承的概念及定义

1.1继承的概念

继承 (inheritance) 机制是面向对象程序设计 使代码可以复用 的最重要的手段,它允许程序员在 持原有类特性的基础上进行扩展 ,增加功能,这样产生新的类,称派生类。继承 呈现了面向对象 程序设计的层次结构 ,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用, 承是类设计层次的复用。

 1.2 继承定义

1.2.1定义格式

Person是父类,也称作基类。Student是子类,也称作派生类。

1.2.2继承基类成员访问方式的变化

红色框中为最常用的情况。

总结:

1. 基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的 不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它
2. 基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected 可以看出保护成员限定符是因继承才出现的
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min( 成员在基类的访问限定符,继承方式 ) public > protected
> private
4. 使用关键字 class 时默认的继承方式是 private ,使用 struct 时默认的继承方式是 public 不过
最好显示的写出继承方式
5. 在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承 ,也不提倡
使用 protetced/private 继承,因为 protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

2.基类和派生类对象赋值转换

1.派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
2.基类对象不能赋值给派生类对象。
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;
}

 

3.继承中的作用域

1. 在继承体系中 基类 派生类 都有 独立的作用域
2. 子类和父类中有同名成员, 子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。 (在子类成员函数中,可以 使用 基类 :: 基类成员 显示访问
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在 继承体系里 面最好 不要定义同名的成员

// B 中的 fun A 中的 fun 不是构成重载,因为不是在同一作用域
// B 中的 fun A 中的 fun 构成隐藏,成员函数满足函数名相同就构成隐藏。

4.派生类的默认成员函数

1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为一些场景析构函数需要构成重写,重写的条件之一是函数名相同 。那么编译器会对析构函数名进行特殊处理,处理成 destrutor() ,所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

5.继承与友元

友元关系不能继承 ,也就是说基类友元不能访问子类私有和保护成员。
class Student;
class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;
}
void main()
{
	Person p;
	Student s;
	Display(p, s);
}

6.继承与静态成员

基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员 。无论派生出多少个子
类,都只有一个 static 成员实例
class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};
void main()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;
}

7.复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
Assistant 的对象中 Person 成员会有两份。

 

虚拟继承 可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在 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 Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
void Test()
{
	Assistant a;
	a._name = "peter";
}

 

 虚拟继承解决数据冗余和二义性的原理:

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;
}
下图是菱形虚拟继承的内存对象成员模型:这里可以分析出 D 对象中将 A 放到的了对象组成的最下面,这个A 同时属于 B C ,那么 B C 如何去找到公共的 A 呢? 这里是通过了 B C 的两个指针,指 向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量 可以找到下面的 A

 

8.继承和组合

1.public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象。
2.组合是一种 has-a 的关系。假设 B 组合了 A ,每个 B 对象中都有一个 A 对象。
3.继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse) 。术语 白箱 是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
4.对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse) ,因为对象的内部细节是不可见的。对象只以 黑箱 的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
5.实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。 
// Car和BMW Car和Benz构成is-a的关系
class Car {
protected:
	string _colour = "白色"; // 颜色
	string _num = "陕ABIT00"; // 车牌号
};

class BMW : public Car {
public:
	void Drive() { cout << "好开-操控" << endl; }
};

class Benz : public Car {
public:
	void Drive() { cout << "好坐-舒适" << endl; }
};

// Tire和Car构成has-a的关系

class Tire {
protected:
	string _brand = "Michelin";  // 品牌
	size_t _size = 17;			 // 尺寸

};

class Car {
protected:
	string _colour = "白色"; // 颜色
	string _num = "陕ABIT00"; // 车牌号
	Tire _t; // 轮胎
};

9.笔试面试题

1. 什么是菱形继承?菱形继承的问题是什么?

菱形继承是指在一个类继承体系中出现了菱形的结构,即某个类同时继承自两个不同的父类,而这两个父类又最终共同地继承自同一个祖先类。这种情况下会导致一些问题,主要包括以下几点:

1. 数据冗余:由于子类直接或间接地从多个路径上获得相同基类的成员变量和方法,可能导致数据重复存储和功能重复实现。

2. 二义性:当子类调用从不同路径上来的相同名称函数时,在编译器无法确定应该使用哪一个版本。这种二义性使得代码难以理解并且容易引发错误。

3. 内存浪费:由于存在数据冗余,并且对象中包含了多份相似甚至完全一样的信息,在内存占用方面可能造成浪费。

为避免菱形继承带来的问题,在设计时可以考虑使用虚拟继承(virtual inheritance)来解决。通过将公共基类标记为`virtual`可以确保只有一份实例被创建,并消除了歧义和数据重复等问题。

2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的?

菱形虚拟继承是针对菱形继承问题提出的解决方案之一。在菱形虚拟继承中,通过使virtual关键字来标记基类,确保只有一个共享基类子对象被创建。这样可以避免数据冗余和二义性问题。

假设有以下类结构:

class A
{
public:
    int data;
};

class B : virtual public A {
};

class C : virtual public A {
};

class D : public B, public C {
};

在上述代码中,B 和 C 类都通过 virtual 关键字从 A 类派生而来,并且 D 类同时从 B 和 C 继承。这种设计方式消除了多次派生导致的数据重复存储。

当我们实例化 D 对象时,只会生成唯一的A子对象,并且D对象可以直接访问A中定义的成员变量data而不会发生歧义。

因此,在C++中使用虚拟继承能够有效地解决由于多层次、多路径导致的数据重复存储和函数调用二义性等问题。

 3. 继承和组合的区别?什么时候用继承?什么时候用组合?

继承(Inheritance)和组合(Composition)是面向对象编程中常用的两种关系建立方式,它们有以下区别:

1. 继承:
   - 继承是一种"is-a"关系,表示一个类从另一个类派生而来,并且拥有父类的属性和方法。
   - 子类可以重用父类的代码,可以访问父类中被声明为protected或public的成员。
   - 继承支持多态性,在运行时能够根据实际类型调用相应函数。

2. 组合:
   - 组合是一种"has-a"关系,表示一个对象包含另一个对象作为其部分。
   - 通过在新创建的类中嵌入其他已存在的对象来实现复用。这样做更加灵活,并且遵循了封装原则。


在选择使用继承还是组合时需要考虑以下因素:

1.使用继承:当子类型 is a 父类型并且需要共享基本功能、接口以及数据结构时适合使用。例如,在具体化通用概念上非常方便。

2.使用组合:当某个功能不属于当前定义好了层次结构内任何特定单元所需提供服务范围之内,则应该采取将其设计成独立模块并与其他模块进行协同工作。此外,在防止菱形问题等情况下也推荐采取组件化设计。

总体而言,如果要表达"is-a"关系,则倾向于使用继承;如果要表达“has-a”或者“uses-a” 关系,则倾向于使用组件。同时也可根据具体场景需求进行灵活选择和权衡。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值