c++复习笔记——继承

Posted 努力学习的少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++复习笔记——继承相关的知识,希望对你有一定的参考价值。

目录

1.继承的概念和定义

1.1继承的概念

​ 1.2继承的定义

1.3继承关系和访问限定符

2.父类和子类的对象赋值转化

3.继承中的作用域

4.子类的默认成员函数

4.1子类的构造函数

4.2子类的拷贝构造函数

4.3子类的赋值重载函数

4.4 子类的析构函数

5.继承与静态成员

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

7.虚继承实现的原理

 9.防止继承的发生

10.继承的总结和反思


1.继承的概念和定义

1.1继承的概念

继承 (inheritance) 机制是面向对象程序设计 使代码可以复用 的最重要的手段,它允许程序员 保持原有类特 性的基础上进行扩展 ,增加功能,这样产生新的类,称为 子类 或者 派生类
如果我们想要定义学生和老师这两种对象,由于我们 老师Teacher学生Student的某些属性是一样的,例如: 名字、性别和年龄等,所以我们将这些属性定义在一个Person类中就可以,然后在将Teacher和Student去继承这些属性,在各自扩展各自相对应的功能即可。
如下:
class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name ; // 姓名
	string _sex;//性别
	int _age; // 年龄
    ...
};

class Student :public Person
{

protected:
	int _stuid;//学号
    ....
};

class Teacher :public Person
{
protected:
	int _jobid;//工号
     ...
};

在上面的代码中,Student和Teacher都继承了Person中的成员(成员变量和成员函数),都会变成子类的一部分。

如下我们测试一下:

void test1()
{
	Student s;//创建一个student对象
	s._name = "张三";
	s._age = 15;
	Teacher t;//创建一个teacher对象
	t._name = "李四";
	t._age = 45;
	s.Print();
	t.Print();
}

输出:

name:张三
age:15
name:李四
age:45

在上面的继承关系中,Student和Teacher都继承了Person的成员,所以Student和Teacher称为Person的子类,有些书里面会说是派生类,而Person是Student和Tecaher的父类,有些书里面是说是基类。它们的关系如下:

 1.2继承的定义

1.3继承关系和访问限定符

对于子类对其继承而来成员的访问权限受到两个因素影响,一个是父类中的权限访问限定,另一个是继承的方式

我将子类的继承而来的成员访问权限做了一个表格,如下:

其实我们可以不必要一个一个去背每一个类的继承而来的成员访问权限:
我对类的继承而来的成员访问权限做了一个总结:

1.除了父类的private的成员的继承都是在子类中不可以见,其余子类的成员的访问限定=Min(将父类的成员访问限定和继承的方式进行比较出较小的限定符),public>protected>privated.

2.对于父类的private的成员,无论以什么方式继承都是不可见的,这里的不可见并不是private成员没有继承给子类,而是在语法上,无论在子类内部还是在子类外部,都不能访问到父类的private成员。

3.在实际运用中,一般使用的是public的方式继承,当要改子类的成员的访问限定符时,我们只需要修改父类中的成员的访问限定符就行。

4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式

class Person
{
public:
	void Print()
	{
		cout << _name << endl;
	}
protected:
	string _name; // 姓名
private:
	int _age; // 年龄
};

class Student : public Person
{
	void test()
	{
		cout << _name << endl;//正确,_name是父类的protected的成员
		cout << _age << endl;//错误,_age是父类的private的成员,不能被子类访问
	}


protected:
	int _stunum; // 学号
};

2.父类和子类的对象赋值转化

子类的对象可以赋值给父类的对象/父类的指针/父类的引用,在这过程中的赋值,只是将子类与父类相同的那部分切割赋值给父类,这里也可以叫做切片或者切割。但是父类不能给子类赋值。

 我们看一下下面这个例子:

class Person
{
public:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};

class Student : public Person
{
public:
	int _No; // 学号
};

上面中的Student继承了Person的成员,那么我们来测试一下它们两个赋值关系:

void test2()
{
	Student s;
	s._name = "张三";
	s._sex = "男";
	s._age = 20;
	s._No = 202100200;
	//正确,将s的_name,_sex,_age成员变量赋值给p
	Person p = s;
	Student s1 = p;//错误,父类的对象不能赋值给子类的对象
	Person* pi = &s;//正确
	Person& ps = s;//正确

}

 

 

3.继承中的作用域

1. 在继承体系中父类 子类 都有 独立的作用域
2. 子类和父类中有同名成员, 子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定 义。 (在子类成员函数中,可以 使用 父类 :: 父类 显示访问
class Person
{
public:
	string _name = "小李子"; // 姓名
	int _num = 111; // 身份证号
};

class Student : public Person
{

public:
 int _num = 999; // 学号
};

在上面中的Person和Student中都有一个_num,当Student继承Person时,则Student类会自动屏蔽掉Person类的_num, 如果我们想要显示父类的_num,则需要在_num前指定类域。

class Student : public Person
{
public:
	void Print()
	{
		cout << " 身份证号:" << Person::_num << endl;//输出111,打印的是父类的_num
		cout << " 学号:" << _num << endl;//输出999,打印的是子类的_num
	}
protected:
	int _num = 999; // 学号
};

那如果父类子类相同的成员函数,那么父类的成员函数也会被隐藏掉。这两个函数不构成函数重载,因为不在同一个类域。如下:

class Person
{
public:
	void Print()
	{
		cout << "I am a Person" << endl;
	}
};

class Student : public Person
{
public:
	void Print()
	{
		cout << "I am a Student" << endl;
	}
};

在上面中的PersonStudent中都有一个Print函数,当Student继承Person时,则Student类会自动屏蔽掉Person类的Print函数, 如果我们想要显示父类的Print则需要在Print前指定类域。


void main()
{
	Student s1;
	s1.Print();//调用的是子类的Print
	s1.Person::Print();//调用的是父类的Print
};

总结:

1.父类的成员名与子类的成员名。则符合隐藏条件,则会自动屏蔽掉父类同名字的成员,当要调用父类的成员时,则需要在成员前指定父类的类域。

2.在继承体系中基类派生类都有独立的作用域,父类的成员函数的名与子类的成员函数名相同不会构成重载,因为它们不属于同一个类域。

3.  注意在实际中在继承体系里面最好不要定义同名的成员因为同名的成员容易混淆。

        

4.子类的默认成员函数

子类继承父类的那部分成员在构造函数中,不能单个的进行初始化,假设有这一个父类:

class Person
{
public:
    //person的构造函数
	Person(const char* name,int age)
		:_name(name)
		,_age(age)
	{

	}
    
    //person的拷贝构造函数
    Person(const Person& p)
		: _name(p._name)
		,_age(p._age)
	{
		
	}

    //person的赋值重载
	Person& operator=(const Person& p)
	{
		if (this != &p)
		{
			_name = p._name;
			_age = p._age;
		}
		return *this;
	}
    
    //person的析构函数
	~Person()
	{
		cout << "~Person()" << endl;
	}
   
protected:
	const char* _name; // 姓名
	int _age;
};

4.1子类的构造函数

尽管在子类对象中含有从父类继承而来的成员,但是派生类并不能直接初始化这些成员,子类必须使用父类的构造函数来初始化它的父类部分。

               在子类的构造函数中,首先先初始化父类部分,然后按照声明的顺序依次

               初始化子类的成员。

子类对象的父类部分子类自己的数据成员都是在构造函数初始阶段执行初始化操作的,类似于我们初始化成员的过程,子类构造函数同样是通过构造函数初始化列表来将实参传递给父类的构造函数。

class Student : public Person
{
public:
	Student(const char* name, int age, int num)
		:_name(name)//错误,不能将父类那部分成员单独的初始化
		,_age(age)//错误
		,_num(num)
	{

	}


protected:
	int _num = 999; // 学号
};

子类正确的构造函数是,需要先调用父类的构造函数初始化继承父类的成员,在初始化自己的成员。子类调用父类的构造函数中,父类的构造函数需要是pubilcd的访问权限

	Student(const char* name,int age,int num)
		:Person(name,age)//直接调用父类的构造函数初始化父类成员
		,_num(num)
	{

	}

如果父类中有默认的构造函数,则子类的构造函数中可以不用显示父类的构造函数,它会自动调用父类的构造函数,默认的构造函数是我们可以不写参数,但会自动的调用构造函数。

	//父类的默认构造函数
    Person(const char* name="张三",int age=20)
		:_name(name)
		,_age(age)
	{

	}    


    //子类的构造函数
	Student(const char* name,int age,int num)
		_num(num)
	{

	}

4.2子类的拷贝构造函数

先将继承父类的成员赋值给子类,然后将自己的成员再赋值 ,其中s传递给Person的拷贝构造函数需要先切片,切片后只剩下与父类相同的成员,然乎在将这些成员赋值给父类。

	Student(const Student& s)
		:Person(s)//s传递给Person是一个切片行为
		,_num(s._num)
	{

	}

4.3子类的赋值重载函数

子类的赋值重载函数中也是需要先调用父类的赋值重载函数,再将自己的成员赋值。

	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
          //s传进去也是切片,调用父类的oerator=的重载将继承父类的成员进行赋值
			Person::operator=(s);
			//operator=(s) 错误的,如果不指定类域,则会一直调用自己的operator=,
            //会发生栈溢出的错误
			_num = s._num;
		}
		return *this;
	}

4.4 子类的析构函数

	~Student()
	{
		cout << "~student" << endl;
		Person::~Person();//正确
        //~Person();//错误 
        清理自己的成员
	}

在子类的析构函数中,需要调用父类的析构函数清理掉父类的成员,在清理掉自己的成员,

但是子类在调用父类的析构函数中需要在析构函数前指定父类的类域,因为多态的原因,任何析构函数都会被处理为destructor(),所以父类的析构函数和子类的析构函数就为相同名字,此时父类的析构函数就会被隐藏掉,所以子类要调用父类的析构函数就需要在析构函数前指定父类的类域。

我们来进行测试一下:

int main()
{
	Student s1;
	return 0;
}

结果:

 我们发现~Person调用了两次,这说明了编译器也为我们调用了一次父类的析构函数,其实编译器在子类析构完成后会自动调用父类的析构函数(这是为了保证先子后父的后进先出的顺序),在将子类的成员所以我们在写子类的析构函数中,不需要写父类的析构函数。

题目:设计一个类A,让这个类不能够被继承。
class A
{
private:
	A()
	{

	}
};


class B :public A
{

};

我们只需要将类A的构造函数设计为Private访问权限,因为子类在创建对象中,必须先要调用父类的构造函数,而A的构造函数时Private,子类B就不能访问到A的构造函数。所以B继承A后不能创建出对象。

总结;

1. 派生类的 构造函数 必须 调用基类的构造函数初始化基类的那一部分成员 。如果基类没有默认的构造函 数,则必须在派生类构造函数的 初始化列表阶段 显示调用。
2. 派生类的 拷贝构造函数 必须调用 基类的拷贝构造 完成基类的拷贝初始化。
3. 派生类的 operator=必须 要调用 基类的operator= 完成基类的复制。
4 . 派生类的析构函数 会在被调用完成后 自动调用基类的析构函数 清理基类成员。因为这样才能保证派生类 对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造
6. 派生类对象析构清理先调用 派生类析构 再调 基类的析构

 

5.继承与静态成员

如果在父类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义不论从父类派生多少个子类,对于每个静态成员来说都只存在一个static实例。

在下面中的类和Base和Derived1、Derived2中共用一个_count.

class Base
{
public:
	Base()
	{
		_count++;
	}

	static int _count;
};

int Base::_count = 0;

class Derived1:public Base
{

};

class Derived2 :public Base
{
};

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

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承 

菱形继承:菱形继承是多继承的一种特殊情况 

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在 Assistant 的对象中 Person 成员会有两份。

数据冗余:出现相同的成员变量。

在只有一个基类的情况下,派生类的作用域嵌套在直接父类和间接父类的作用域中,查找过程沿着继承体系自底向上进行,直到找到所需的名字为止,子类的名字将隐藏父类的同名成员。

二义性:访问Assistant的_named的时候,访问的_name有两个,这就使_name有两种意思。当一个类拥有多个父类时,有可能出现子类从两个或更多父类中继承同名成员的情况。此时,不加前缀限定符直接使用该名字将引发二义性

class Person
{
public:
	string _name; // 姓名
};

class Student : public Person
{
protected:
	int _num; //学号
};

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	//会存在二义性,无法明确要访问哪一个
	Assistant a;
	a._name = "张三";

	//需要显示指定访问父类的成员可以解决二义性的问题。
	//但还是存在数据冗余
	a.Teacher::_name = "张三";
	a.Student::_name = "张三";
}

为了解决菱形继承的问题,c++就有一个虚继承的解决上诉问题。

虚继承的是让某个类做出声明,承诺愿意共享它的父类,其中,共享的父类子对象称为虚父类,在这种机制下,不论虚父类在继承体系中出现多少次,在子类中都只包含唯一 一个共享的虚父类子对象。

 所以我们只需要在Student和Teacher的继承关系中加上virtual。修改如下:

class Student : virtual public Person
{
protected:
	int _num; //学号
};

class Teacher :virtual public Person
{
protected:
	int _id; // 职工编号
};

当我们用了虚继承以后,student类和teacher类中的person类中的变量再Assistant就变为同一份。这样我们在使用_name就不用指定哪个类域成员了,我们直接使用_name就不会存在二义性的问题,因为student类和teacher类中的person类中的_name为同一个成员。

7.虚继承实现的原理

为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助 内存窗口观察对象成员的模型。
当我们的B类和C类继承A的没有用虚继承时,我们来观看各成员的分布。
class A {
public:
	int _a;
};


class B :  public A {
public:
	int _b;
};

class C :  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;
}

 我们可以看到B类和C类中都有一个_a

接下来我们再来看一下当B类和C类进行虚继承的时候,我们再来看各成员的分布。

class A {
public:
	int _a;
};


class B : virtual public A {
public:
	int _b;
};

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最上面是有一个值, 这个值是一个指针 ,(注意,我的电脑是大端存储,所以我们将这个值取出来时,是按照大端取出的), 这里是通过了 B C 的两个指针,找到指向的一张表。这两个指 针叫虚基表指针,这两个表叫虚基表。虚基表是由两个变量组成,由下面变量的值减去上面变量的值就是一个偏移量。通过这个偏移量和B或C的起始地址就可以找到下面的 A
例如:我们通过B最上面的值找到B的虚基表

 

我们将虚基表下面的值减去上面的值得到的为14( 注意上面的值十六进制),转换为十进制就为20.由B的起始地址0x005EF75C加上20就是A的地址。

同样的,我们通过C最上面的值找到C的虚基表

 我们将虚基表下面的值减去上面的值得到的为0c( 注意上面的值十六进制),转换为十进制就是12,由B的起始地址0x005EF764加上12就是A的地址。

D d;

B b=d;

C c=d;

在虚基类的切片行为里,它不像之前直接将父类的成员直接赋值过去,例如上面的B中,它将d的_b成员切过去赋值给b,但是少了一个_a,所以就需要通过指针找到虚基表,计算出偏移量,然后再找到a的位置,再将_a赋值给b对象里。

下面这张图是Person的虚拟继承的原理解释:

        

 9.防止继承的发生

有时我们定义一个这样一种类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个父类。为了实现这一目的,C++11新标准规定提供了一种防止继承发生的方法,即在类名后面跟一个关键字final.

class A final{/* */ };//A不能作为基类
class B{/* */ };
class C final:B{/* */ };//C不能作为基类

10.继承的总结和反思

 

1.public继承是一种is-a(是什么)的关系,也就是说每个子类对象是一个父类的对象。

例如:奔驰,宝马是小汽车,所以这里就是is-a的关系。所以得用继承的方式

2.组合是一种has-a(有什么)的关系,假设B组合了A,每个B对象中都有一个A的对象。

例如:奔驰,宝马都有轮胎,而不能说奔驰,宝马都是轮胎,这里就是has-a的关系,所以得用组合的方式。

3.继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用 (white-box reuse)。术语白箱是相对可视性而言:在继承方式中,基类的内部细节子类可见 。 继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。当基类中如果添加或删除成员变量,那么在派生类中使用基类的成员的函数就有可能受到影响,这就是依赖关系强。

4.对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对 象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse), 因为对象的内部细节是不可见的。对象只以“黑箱的形式出现。 组合类之间没有很强的依赖关系, 耦合度低。优先使用对象组合有助于你保持每个类被封装。

5.类之间的关系既可以用继承,也可以用组合,就用组合。

好啦,今天的分享就到这里了,如果这篇文章对你有帮助的话,麻烦给我点赞关注加收藏,我会不断的分享知识给大家。

以上是关于c++复习笔记——继承的主要内容,如果未能解决你的问题,请参考以下文章

复习笔记——C++模板

C++ 类和对象期末复习笔记——多态性

C++类和对象期末复习笔记

《游戏引擎架构》笔记三

复习笔记——C++内联函数

Java复习笔记4--实现多重继承