bingc++(继承)

Posted 月屯

tags:

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

上一篇

目录标题

继承

例子

继承方式

  • public继承方式,
    基类中public修饰成员在子类中的访问权限仍旧是public,
    基类中protected修饰成员在子类中的访问权限仍旧是protected,
    基类中private修饰成员在子类中不可见—不能被直接访问
  • protected继承方式
    基类中public修饰的成员在子类中的权限变为protected
    基类中protected修饰的成员在子类中的权限没有发生变化,即protected
    基类中private修饰的成员在子类中不可见,即不能直接被访问
  • private继承方式
    基类中public修饰成员在子类中的访问权限private
    基类中protected修饰成员在子类中的访问权限仍旧是private
    基类中private修饰成员在子类中不可见—不能被直接访问
  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public

基类和派生类对象的赋值转换----赋值兼容规则----

一定是在public的继承方式下才满足

  1. 可以直接用子类对象给基类对象赋值,但是反过来不可以(不能使用基类对象给子类对象赋值)
  2. 以使基类的指针指向子类的对象,但是反过来不可以(不能直接使用子类的指针指向基类对象),如果一定要指向必须进行强转
  3. 可以使用基类的引用去引用子类对象,但是反过来不行(不能使用子类的引用去引用基类对象)

继承中的作用域

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

派生类的默认成员函数

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

public:
	Base(int b)
		: _b(b)
	

	Base(const Base& b)
		: _b(b._b)
	

	Base& operator=(const Base& b)
	
		if (this != &b)
		
			_b = b._b;
		

		return *this;
	

	~Base()
	
		cout << "Base::~Base()" << endl;
	
protected:
	int _b;
;

class Derived : public Base

public:
	Derived(int b = 10, int d = 20)
		: Base(b)
		, _d(d)
	

	Derived(const Derived& d)
		: Base(d)
		, _d(d._d)
	

	Derived& operator=(const Derived& d)
	
		if (this != &d)
		
			// 1. 先调用基类的赋值运算符重载给基类部分成员赋值
			Base::operator=(d);

			// 2. 再给子类自己新增加的成员赋值
			_d = d._d;
		

		return *this;
	

	~Derived()
	
		cout << "Derived::~Derived()" << endl;

		// 编译器将子类的析构函数编译完成之后
		// 会在子类析构函数最后一条语句之后插入一条调用基类析构函数的汇编语句
		// call ~Base();
	
protected:
	int _d;
;
  1. 派生类对象初始化先调用基类构造再调派生类构造。
  2. 派生类对象析构清理先调用派

基类哪些成员被子类继承了

  1. 成员变量
    普通成员变量:基类所有的成员普通成员变量都被子类继承了
    静态成员变量:披继承了多个子类中拥有的是同一份静态成员变量,在整个继承体系中,静态成员只有一份
  2. 成员函数
    普通成员函数----基类所有的成员函数都被子类继承了
    静态成员函数::被继承
    默认成员函数:构造拷贝构造赋值运算符重载析构函数(二义)
  3. 友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

多继承

基类部分在上,子类部分在下,多个基类部分在子类对象中的次序和继承列表中基类出现的先后次序一致注意:继承列表中每个基类前必须要加继承权限,否则就是默认的继承权限

class D : public C1, public C2

public:
	int _d;
;

菱形继承


存在二义性问题

解决方案:

1. 明确化

2. 虚拟继承(用在菱形继承中解决菱形继承中存在的二义性问题)(重)(windows vs2013)
class B

public:
	int _b;
;

class D : virtual public B

public:
	D()
	

public:
	int _d;
;

虚拟继承和普通的单继承区别?
1.对象模型是倒立的(先子类变量,在基类变量)
2.对象多了4个字节(称偏移量表格地址或虚基表指针)
3.如果D没有显式定义构造函数,则编译器一定会生成,如果D定义了构造函数,则编译器一定会对构造函数进行修改
生成||修改目的:往对象的前4个字节中填弃数据

步骤:
1.取对象前4个字节中的内容,即:偏移量表格2取该地址空间中往后偏移4字节之后的内容—偏移量使用
3.结合偏移量给_b赋值
图解:

多态

概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

分类

  • 静态多态(静态绑定||早绑定):程序在编译期间已经确定了函数的行为,典型代表:函数重载、模板函数
  • 动态多态(动态绑定||晚绑定):在程序运行时,才可以确定函数的行为,即在编译阶段无法确定到底要调用那个函数

动态多态条件

  • 必须要处于继承的体系下
  • 基类中必须要有虚函数(被virtual关键字修饰的成员函数称为虚函数),在子类中必须要对基类中的虚函数进行重写
  • 虚函数调用:必须要通过基类的指针或者引用来调用

多态体现

在程序运行时,根据基类的指针或者引用指向不同类的对象,选择合适的虚函数进行调用

重写

a.必须在继承的体系中
b.基类的成员函数必须是虚函数
c.子类和基类虚函数的原型(返回值类型函数名字(参数列表))必须一模一样(例外:

  1. 返回值类型可以不同—基类1虚函数必须要返回基类2对象的指针或者引用;子类1虚函数必须返回子类2对象的指针或引用(基类1和基类2可以不同,但必须与子类配对)
  2. 函数名字不同,但必须是析构函数

    d.与访问权限没有关系

重写与同名隐藏对比

c++11新特性

  1. override
// override: C++11中新增加的关键字,目的:在编译阶段,来检测被override
// 修饰的函数是否对其基类对应的虚函数进行重写
// override只能修饰虚函数,只能修饰子类的虚函数
// 如果重写成功--编译通过
// 否则编译报错
class Base

public:
    virtual void Test1Func()
    
        cout << "Base::TestFunc()" << endl;
    
    
;

class Derived : public Base

public:
    virtual void Test1Func()override
    
        cout << "Base::TestFunc()" << endl;
    
;

  1. final
    修饰函数
// final:修饰函数
// 只能修饰虚函数
// 一般是用来修饰子类虚函数,目的:该虚函数不能被子类的派生类重写
class B

public:
	virtual void f()
	
		cout << "B::f()" << endl;
	

	// 编译报错
	//void f1()final
	//

	// 不推荐
	virtual void f2()final
	
;

// 需求:在D之后的子类中,不想让f虚函数再被重写了
class D : public B

public:
	virtual void f()final
	
		cout << "D::f()" << endl;
	
;


class E : public D

public:
	/*
	// 编译失败,因为f在D中被final修饰了,即:E中不能对基类中的f进行重写
	virtual void f()
	
		cout << "E::f()" << endl;
	*/
;

修饰类

// final:也可以修饰类
// 作用:该类不能被继承
class B final

public:
	int func()
	
		cout << "B::func()" << endl;
	
;

class D : public B
;

抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数_

class Shape

public:
	// 纯虚函数
	virtual double GetArea() = 0;
	virtual double GetCircumference() = 0;
;

例子

实现原理

以此为例

// 结论:如果类中包含有虚函数,不论虚函数有多少个,类对象大小都多了4个字节
class Base

public:
	Base(int b = 10)
		: _b(b)
	
		cout << "Base()" << endl;
	

	virtual void f1()
	
		cout << "Base::f1()" << endl;
	

	virtual void f3()
	
		cout << "Base::f3()" << endl;
	

	virtual void f2()
	
		cout << "Base::f2()" << endl;
	

	int _b;
;

包含虚函数的类对象模型


如果类中包含有虚函数(和个数无关),编译器会给对象多增加4个字节而且多增加的4个字节在对象的起始位置-----内存存放的是虚表地址,虚表地址是在构造对象时进行填充的----->在构造函数中填充的(构造函数如果没有显式实现:则编译器会给类生成一份默认的构造函数;构造函数如果显式实现:编译器会对用户实现的构造函数进行修改----增加给对象前4个字节存放虚表地址的语句)

虚表构建原理—重写(覆盖)

在编译阶段生成,本质是函数指针数组
基类虚函数表构建规则:

  1. 按照虚函数在类中声明的先后次序依次填充虚表

子类虚表的构建规则:
2. 将基类虚表中内容拷贝一份放到子类的虚表中了(此时基类和子类使用的不是同一张虚表)
3. 如果子类重写了基类的虚函数,就用子类自己的虚函数替换虚表中相同偏移量位置的基类虚函数的入口地址
4. 子类新增加的虚函数按照其在类中声明的先后次序依次放在虚表的最后
验证

构造方法产生

结论:如果编译器感觉自己需要的时候,就会生成
1.类和对象︰如果B类中包含有A类的对象,B没有显式定义构造函数,A定义了无参或者全缺省的构造方法,则编译器一定会给B类生成默认的构造方法
⒉.继承:如果B继承自A,A中定义了无参或者全缺省的构造方法,B没有显式定义任何构造函数,则编译器会给B生成无参的构造方法
3.虚拟继承:如果B虚拟继承自A,B没有显式定义任何构造方法,则编译器会给B类生成默认的构造方法,目的:为了在对象的前4个字节中填充虚基表的地址
4.类中包含有虚函数:如果类没有显式定义任何构造函数,则编译器会给该类生成一份默认的构造函数,目的:为了在对象前4个字节中填充虚表的地址

多态父子类反汇编

多继承虚表

多继承

虚函数表解析

对象的内存布局

常见题

1.以下关于纯虚函数的说法,正确的是( a)
A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数

2.关于虚函数的描述正确的是( b)
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数

3.假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则(d )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

#include<iostream>
using namespace std;
class A
public:
 A(char *s)  cout<<s<<endl; 
 ~A()
;
class B:virtual public A 
public:
 B(char *s1,char*s2):A(s1)  cout<<s2<<endl; 
;
class C:virtual public A 
public:
 C(char *s1,char*s2):A(s1)  cout<<s2<<endl; 
;
class D:public B,public C 
public:
 D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
  cout<<s4<<endl;
;
int main() 
 D *p=new D("class A","class B","class C","class D");
 delete p;
 return 0; 


A

A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D

class A 
public:
 virtual void func(int val = 1) std::cout<<"A->"<< val <<std::endl;
 virtual void test() func();
;
class B : public A 
public:
 void func(int val=0) std::cout<<"B->"<< val <<std::endl; 
;
int main(int argc ,char* argv[])

 B*p = new B;
 p->test();
 return 0; 

B

A: A->0 B: B->1 C: A->1 D: B->0

6.静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式,无法访问虚函数表,所以静态成员函数无法放进虚函数表。
7. 构造函数可以是虚函数吗
假设构造函数可以是虚函数,则构造函数也在虚表中,即构造函数只能通过虚表调用,构造方式中才给对象前4个字节中放虚表的地址,构造函数没有调用,对象前4个字节没有虚表指针—>无法找到虚表—>无法找到构造函数的入口地址
8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
10.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。

下一篇

以上是关于bingc++(继承)的主要内容,如果未能解决你的问题,请参考以下文章

bingc++(继承)

wpf datagrid表头SortDirection的升序(SortDirection)降序(Ascending)图标怎样放进去的?

类的加载次序与继承

bingc++动态内存管理

bingc++类

bingc++类