c++基础(六):派生类访问控制;继承中的构造和析构(对象模型、调用顺序、同名成员的处理方法、静态成员特性);多继承和虚继承

继承和派生

1.1 继承概述

1.1.1 为什么需要继承

网页类

class IndexPage{
public:
	//网页头部
	void Header(){
		cout << "网页头部!" << endl;
	}
	//网页左侧菜单
	void LeftNavigation(){
		cout << "左侧导航菜单!" << endl;
	}
	//网页主体部分
	void MainBody(){
		cout << "首页网页主题内容!" << endl;
	}
	//网页底部
	void Footer(){
		cout << "网页底部!" << endl;
	}
private:
	string mTitle; //网页标题
};

#if 0
//如果不使用继承,那么定义新闻页类,需要重新写一遍已经有的代码
class NewsPage{
public:
	//网页头部
	void Header(){
		cout << "网页头部!" << endl;
	}
	//网页左侧菜单
	void LeftNavigation(){
		cout << "左侧导航菜单!" << endl;
	}
	//网页主体部分
	void MainBody(){
		cout << "新闻网页主体内容!" << endl;
	}
	//网页底部
	void Footer(){
		cout << "网页底部!" << endl;
	}
private:
	string mTitle; //网页标题
};

void test(){
	NewsPage* newspage = new NewsPage;
	newspage->Header();
	newspage->MainBody();
	newspage->LeftNavigation();
	newspage->Footer();
}
#else
//使用继承,可以复用已有的代码,新闻业除了主体部分不一样,其他都是一样的
class NewsPage : public IndexPage{
public:
	//网页主体部分
	void MainBody(){
		cout << "新闻网页主主体内容!" << endl;
	}
};
void test(){
	NewsPage* newspage = new NewsPage;
	newspage->Header();
	newspage->MainBody();
	newspage->LeftNavigation();
	newspage->Footer();
}
#endif
int main(){

	test();

	return EXIT_SUCCESS;
}

1.2 继承基本概念

  • c++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。
  • 一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。
  • 派生类中的成员,包含两大部分:
    一类是从基类继承过来的,一类是自己增加的成员。
    从基类继承过过来的表现其共性,而新增的成员体现了其个性。

1.2.1 派生类定义

  • 派生类定义格式:
   Class 派生类名 :  继承方式 基类名{
         //派生类新增的数据成员和成员函数
   }
  • 三种继承方式:

public : 公有继承
private : 私有继承
protected : 保护继承

  • 从继承源上分:

单继承:指每个派生类只直接继承了一个基类的特征
多继承:指多个基类派生出一个派生类的继承关系,多继承的派生类直接继承了不止一个基类的特征

1.2.2 派生类访问控制

派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。
派生类的访问权限规则如下:
在这里插入图片描述
在这里插入图片描述

//基类
class A{
public:
	int mA;
protected:
	int mB;
private:
	int mC;
};

//1. 公有(public)继承
class B : public A{
public:
	void PrintB(){
		cout << mA << endl; //类内可访问基类public属性,子类中还是public属性
		cout << mB << endl; //类内可访问基类protected属性,子类中还是protected属性
		//cout << mC << endl; //类内不可访问基类private属性
	}
};
class SubB : public B{
	void PrintSubB(){
		cout << mA << endl; //类内可访问基类public属性,孙子类中还是public属性
		cout << mB << endl; //类内可访问基类protected属性,孙子类中还是public属性
		//cout << mC << endl; //类内不可访问基类private属性
	}
};
void test01(){

	B b;
	cout << b.mA << endl; //类外可访问基类public属性
	//cout << b.mB << endl; //类外不可访问基类protected属性
	//cout << b.mC << endl; //类外不可访问基类private属性
}

//2. 私有(private)继承
class C : private A{
public:
	void PrintC(){
		cout << mA << endl; //可访问基类public属性,子类中变为private属性
		cout << mB << endl; //可访问基类protected属性,子类中变为private属性
		//cout << mC << endl; //不可访问基类private属性
	}
};
class SubC : public C{
	void PrintSubC(){
		//cout << mA << endl; //不可访问基类public属性
		//cout << mB << endl; //不可访问基类protected属性
		//cout << mC << endl; //不可访问基类private属性
	}
};
void test02(){
	C c;
	//cout << c.mA << endl; //不可访问基类public属性
	//cout << c.mB << endl; //不可访问基类protected属性
	//cout << c.mC << endl; //不可访问基类private属性
}
//3. 保护(protected)继承
class D : protected A{
public:
	void PrintD(){
		cout << mA << endl; //类内可访问基类public属性,子类中变为protected属性
		cout << mB << endl; //类内可访问基类protected属性,子类中变为protected属性
		//cout << mC << endl; //不可访问基类private属性
	}
};
class SubD : public D{
	void PrintD(){
		cout << mA << endl; //类内可访问基类public属性,孙子类中变为protected属性
		cout << mB << endl; //类内可访问基类protected属性,孙子类中变为protected属性
		//cout << mC << endl; //不可访问基类private属性
	}
};
void test03(){
	D d;
	//cout << d.mA << endl; //不可访问基类public属性
	//cout << d.mB << endl; //不可访问基类protected属性
	//cout << d.mC << endl; //不可访问基类private属性
}

1.3 继承中的构造和析构

1.3.1 继承中的对象模型

子类会继承父类的所有内容,只是私有成员被编译器隐藏了
在C++编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成:

class A{
public:
	int mA;
	int mB;
};
class B : public A{
public:
	int mC;
};
class C : public B{
public:
	int mD;
};
void test(){
	cout << "A size:" << sizeof(A) << endl;
	cout << "B size:" << sizeof(B) << endl;
	cout << "C size:" << sizeof(C) << endl;
}

1.3.2 对象构造和析构的调用原则

在这里插入图片描述
子类不会继承父类的构造函数和析构函数
继承中的构造和析构

子类对象在创建时会首先调用父类的构造函数
父类构造函数执行完毕后,才会调用子类的构造函数
当父类构造函数有参数时,需要在子类初始化列表(参数列表)中显示调用父类构造函数
析构函数调用顺序和构造函数相反

class A{
public:
	A(){
		cout << "A类构造函数!" << endl;
	}
	~A(){
		cout << "A类析构函数!" << endl;
	}
};

class B : public A{
public:
	B(){
		cout << "B类构造函数!" << endl;
	}
	~B(){
		cout << "B类析构函数!" << endl;
	}
};

class C : public B{
public:
	C(){
		cout << "C类构造函数!" << endl;
	}
	~C(){
		cout << "C类析构函数!" << endl;
	}
};

void test(){
	C c;
}

如果父类中没有合适默认构造,那么子类可以利用初始化列表的方式显示的调用父类的其他构造

class A{
public:
	A(int a){
		this->a = a;
	}
	~A(){
		cout << "A类析构函数!" << endl;
	}
	int a;
};

class B : public A{
public:
	B(int a)A(a);
	~B(){
		cout << "B类析构函数!" << endl;
	}
};

继承与组合混搭的构造和析构

class D{
public:
	D(){
		cout << "D类构造函数!" << endl;
	}
	~D(){
		cout << "D类析构函数!" << endl;
	}
};
class A{
public:
	A(){
		cout << "A类构造函数!" << endl;
	}
	~A(){
		cout << "A类析构函数!" << endl;
	}
};
class B : public A{
public:
	B(){
		cout << "B类构造函数!" << endl;
	}
	~B(){
		cout << "B类析构函数!" << endl;
	}
};
class C : public B{
public:
	C(){
		cout << "C类构造函数!" << endl;
	}
	~C(){
		cout << "C类析构函数!" << endl;
	}
public:
	D c;
};
void test(){
	C c;
}

A B D C 构造函数  C D B A 析构函数

1.3.3 继承中同名成员的处理方法

当子类成员和父类成员同名时,子类依然从父类继承同名成员
如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域,就近原则)
在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显示使用类名限定符)

class Base{
public:
	Base():mParam(0){}
	void Print(){ cout << mParam << endl; }
public:
	int mParam;
};

class Derived : public Base{
public:
	Derived():mParam(10){}
	void Print(){
		//在派生类中使用和基类的同名成员,显示使用类名限定符
		cout << Base::mParam << endl;
		cout << mParam << endl;
	}
	//返回基类重名成员
	int& getBaseParam(){ return  Base::mParam; }
public:
	int mParam;
};

int main(){

	Derived derived;
	//派生类和基类成员属性重名,子类访问成员默认是子类成员
	cout << derived.mParam << endl; //10
	derived.Print();
	//类外如何获得基类重名成员属性
	derived.getBaseParam() = 100;
	cout << "Base:mParam:" << derived.getBaseParam() << endl;

	return EXIT_SUCCESS;
}

注意: 如果重新定义了基类中的重载函数,将会发生什么?
若子类通过改变函数参数列表的方式重新定义基类的重载函数,则全部被隐藏,
若子类通过改变函数返回值的方式重新定义的基类重载函数,则未被定义的基类重载函数将被隐藏

class Base{
public:
	void func1(){
		cout << "Base::void func1()" << endl;
	};
	void func1(int param){
		cout << "Base::void func1(int param)" << endl;
	}
	void myfunc(){
		cout << "Base::void myfunc()" << endl;
	}
};
class Derived1 : public Base{
public:
	void myfunc(){
		cout << "Derived1::void myfunc()" << endl;
	}
};
class Derived2 : public Base{
public:
	//改变成员函数的参数列表
	void func1(int param1, int param2){
		cout << "Derived2::void func1(int param1,int param2)" << endl;
	};
};
class Derived3 : public Base{
public:
	//改变成员函数的返回值
	int func1(int param){
		cout << "Derived3::int func1(int param)" << endl;
		return 0;
	}
};
int main(){

	Derived1 derived1;
	derived1.func1();
	derived1.func1(20);
	derived1.myfunc();
	cout << "-------------" << endl;
	Derived2 derived2;
	//derived2.func1();  //func1被隐藏
	//derived2.func1(20); //func2被隐藏
	derived2.func1(10,20); //重载func1之后,基类的函数被隐藏
	derived2.myfunc();
	cout << "-------------" << endl;
	Derived3 derived3;
	//derived3.func1();  没有重新定义的重载版本被隐藏
	derived3.func1(20);
	derived3.myfunc();

	return EXIT_SUCCESS;
}

Derive1 重定义了Base类的myfunc函数,derive1可访问func1及其重载版本的函数。
Derive2通过改变函数参数列表的方式重新定义了基类的func1函数,则从基类中继承来的其他重载版本被隐藏,不可访问
Derive3通过改变函数返回类型的方式重新定义了基类的func1函数,则从基类继承来的没有重新定义的重载版本的函数将被隐藏。

任何时候重新定义基类中的一个重载函数,在新类中所有的其他版本将被自动隐藏.

1.3.4 非自动继承的函数

不是所有的函数都能自动从基类继承到派生类中。构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。
另外operator=也不能被继承,因为它完成类似构造函数的行为。也就是说尽管我们知道如何由=右边的对象如何初始化=左边的对象的所有成员,但是这个并不意味着对其派生类依然有效。
在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。

1.3.5 继承中的静态成员特性

静态成员函数和非静态成员函数的共同点:
1.他们都可以被继承到派生类中。
2.如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏。
3.如果我们改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏
静态成员函数不能是虚函数(virtual function).

class Base{
public:
	static int getNum(){ return sNum; }
	static int getNum(int param){
		return sNum + param;
	}
public:
	static int sNum;
};
int Base::sNum = 10;

class Derived : public Base{
public:
	static int sNum; //基类静态成员属性将被隐藏
#if 0
	//重定义一个函数,基类中重载的函数被隐藏
	static int getNum(int param1, int param2){
		return sNum + param1 + param2;
	}
#else
	//改变基类函数的某个特征,返回值或者参数个数,将会隐藏基类重载的函数
	static void getNum(int param1, int param2){
		cout <<  sNum + param1 + param2 << endl;
	}
#endif
};
int Derived::sNum = 20;

int main(){
	cout<<Derived::sNum<<endl;			//20
	cout<<Derived::Base::sNum<<endl;	//10
}

1.4 多继承与虚继承

1.4.1 多继承概念

在这里插入图片描述
我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义

class Base1{
public:
	void func1(){ cout << "Base1::func1" << endl; }
};
class Base2{
public:
	void func1(){ cout << "Base2::func1" << endl; }
	void func2(){ cout << "Base2::func2" << endl; }
};
//派生类继承Base1、Base2
class Derived : public Base1, public Base2{};
int main(){

	Derived derived;
	//func1是从Base1继承来的还是从Base2继承来的?
	//derived.func1(); 
	derived.func2();

	//解决歧义:显示指定调用那个基类的func1
	derived.Base1::func1(); 
	derived.Base2::func1();

	return EXIT_SUCCESS;
}

多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本?
解决方法就是显示指定调用那个基类的版本

1.4.2 菱形继承和虚继承

两个派生类继承同一个基类而又有某个类同时继承两个派生类,这种继承被称为菱形继承,或者钻石型继承
在这里插入图片描述
这种继承所带来的问题:
1.羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性
2.草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

class BigBase{
public:
	BigBase(){ mParam = 0; }
	void func(){ cout << "BigBase::func" << endl; }
public:
	int mParam;
};

class Base1 : public BigBase{};
class Base2 : public BigBase{};
class Derived : public Base1, public Base2{};

int main(){

	Derived derived;
	//1. 对“func”的访问不明确
	//derived.func();
	//cout << derived.mParam << endl;
	cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl;
	cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;

	//2. 重复继承
	cout << "Derived size:" << sizeof(Derived) << endl; //8

	return EXIT_SUCCESS;
}

上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,那么重复继承怎么解决?
对于这种菱形继承所带来的两个问题,c++为我们提供了一种方式,采用虚基类。那么我们采用虚基类方式将代码修改如下:
Derived 内部结构

vbptr 虚基类指针
指向一张 虚基类表
通过表找到偏移量
找到共有的数据

class BigBase{											//虚基类
public:
	BigBase(){ mParam = 0; }
	void func(){ cout << "BigBase::func" << endl; }
public:
	int mParam;
};

class Base1 : virtual public BigBase{};					//虚继承			
class Base2 : virtual public BigBase{};					//虚继承
class Derived : public Base1, public Base2{};

int main(){

	Derived derived;
	//二义性问题解决
	derived.func();
	cout << derived.mParam << endl;
	//输出结果:12
	cout << "Derived size:" << sizeof(Derived) << endl;

	return EXIT_SUCCESS;
}

以上程序Base1 ,Base2采用虚继承方式继承BigBase,那么BigBase被称为虚基类。
通过虚继承解决了菱形继承所带来的二义性问题。
但是虚基类是如何解决二义性的呢?并且derived大小为12字节,这是怎么回事?

1.4.3 虚继承实现原理

class BigBase{
public:
	BigBase(){ mParam = 0; }
	void func(){ cout << "BigBase::func" << endl; }
public: int mParam;
};
#if 0 //虚继承
class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
#else //普通继承
class Base1 :  public BigBase{};
class Base2 :  public BigBase{};
#endif
class Derived : public Base1, public Base2{};

在这里插入图片描述

在这里插入图片描述
通过内存图,我们发现普通继承和虚继承的对象内存图是不一样的。我们也可以猜测到编译器肯定对我们编写的程序做了一些手脚。

BigBase 菱形最顶层的类,内存布局图没有发生改变。
Base1和Base2通过虚继承的方式派生自BigBase,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个vbptr (virtual base pointer),vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。

Derived派生于Base1和Base2,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量。

由此可知编译器帮我们做了一些幕后工作,使得这种菱形问题在继承时候能只继承一份数据,并且也解决了二义性的问题。现在模型就变成了Base1和 Base2 Derived三个类对象共享了一份BigBase数据。

当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这和多继承是完全不同的)。即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化

那到底谁应该负责完成初始化呢?C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。

class BigBase{
public:
	BigBase(int x){mParam = x;}
	void func(){cout << "BigBase::func" << endl;}
public:
	int mParam;
};
class Base1 : virtual public BigBase{
public:
	Base1() :BigBase(10){} //不调用BigBase构造
};
class Base2 : virtual public BigBase{
public:
	Base2() :BigBase(10){} //不调用BigBase构造
};

class Derived : public Base1, public Base2{
public:
	Derived() :BigBase(10){} //调用BigBase构造
};
//每一次继承子类中都必须书写初始化语句
int main(){
	Derived derived;
	return EXIT_SUCCESS;
}

注意:
== 虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承.==

工程开发中真正意义上的多继承是几乎不被使用,因为多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承代替。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一、实验目的 掌握根据实际需求设计Java类的方法; 掌握Java继承的使用方法; 掌握Javapackage的概念和使用方法; 掌握静态方法定义和使用方法; 掌握Object类equals方法和toString方法的覆盖方法。 二、实验内容 1、设计一个名为figure的图形软件包(package)。包包含三角形、矩形、圆三个类。要求:(1)每个类都要构造方法并为成员设置get和set方法;(2)每个类都要有计算周长和面积的成员方法;(3)完成该软件包后的编码后,在另一个包的含有main方法的类编写代码,分别使用图形软件包的三个类,生成三个对象,并打印出其周长和面积。 2、编写类Factorial,为其添加两个静态方法方法名自定义)。其一个使用递归计算n的阶乘,一个使用非递归计算n的阶乘。构造main方法进行测试。 3、按照要求使用Java进行编码。 设计一个教师类Teacher,属性有编号(no)、姓名(name)、年龄(age)、所属学院(seminary),为这些属性设置相应的get和set方法; 为Teacher类重写equals方法;(当两个教师对象的no相同时返回true) 重写Teacher类的toString方法,通过该方法可以返回“编号为**、姓名为**、年龄为**的**学院老师”形式的字符串。 构造main方法进行测试。 4、设计一个带表头的单链表(链表的元素属于同一类型对象,但对象的类型可以随意),提供以下操作:(1)insert:在某个位置插入对象;(2)delete:在某个位置删除对象;(3)delete:删除链表与x相同的元素;(4)size:返回当前链表对象的个数;(5)isEmpty:判断链表是否为空;(6)traverse:遍历链表,打印出所有的元素;(7)getData:取得某个位置的对象构造main函数进行测试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值