继承----C++的三大特性之一

一,为什么要引入继承?


       继承是一个非常自然的概念,现实世界中的许多事物也都是具有继承性的。例如,爸爸继承爷爷的特性,儿子又继承爸爸的特性等都属于继承的范畴。下面是一个简单的汽车分类图:



在这个分类图中建立了一种层次结构,最高层是最普遍,最一般的,每一次都比它上一层的更详细,更具体。其中把上一层的叫做基类(或父类),紧接着基类的下一层叫做

派生类(或子类)


      所谓继承,就是从先辈处得到属性和特征。类的继承就是新类从已有类得到已有的特性,新类被称为派生类,已有类被称为基类。可抽象为派生类是基类的具体化,而基类则是派生类的抽象。


下面通过一个例子说明为什么要引用继承?

class Person
{
private:
	int length;//身高
	int weight;//体重
	int borndate;//出生日期
};

class Student
{
private:
	int length;//身高
	int weight;//体重
	int borndate;//出生日期
	
	int score;//成绩
	int total;//总分
	char* school;//学校
};

上面定义了两个类,一个人类,一个学生类,仔细观察我们发现人类中具有的属性(即类的成员数据)在学生类中都具。换言之,学生也是人,反过来讲人不一定是学生。就像这样的代码,人类中的属性我们重复定义了两次,那么有没有一种方法能避免这种代码重复呢?是的,这就是我们的继承,继承机制是面向对象程序设计中避免代码重复的最重要的手段,它允许程序员对已有类进行扩展,增加功能


二,继承的定义格式



eg:

class Person
{
private:
	int length;//身高
	int weight;//体重
	int borndate;//出生日期
};

class Student:public Person
{
private:
	int score;//成绩
	int total;//总分
	char* school;//学校
};

创建一个学生对象试试吐舌头


基类的所以对象都被继承过来了,很棒吧!


注意:class如果没有显示的给出继承方式,系统默认为private继承,但是最好还是显示定义继承类型,即使是私有继承。struct的默认继承方式则是public。


三,继承方式&访问限定符


派生类可以继承基类中除了构造函数和析构函数之外的所有成员,但是这些成员的访问属性是由继承方式决定的。




不同的继承方式下基类成员在派生类中的访问属性:



举例说明:

(1)public继承


eg1:

#include <iostream>
using namespace std;

class Person
{
public://公有数据成员
	int length;//身高
	int weight;//体重
};

class Student:public Person
{
public:
	void test1()
	{
		length = 183;//子类中可访问父类的共有成员(包括属性和方法)
		weight = 70;
		total = 300;
	}
private:
	int total;//总分
	char* school;//学校
};

void Funtest()
{
	Student s1;
	s1.length = 160;//类外派生类的对象可访问基类的公有成员(包括属性和方法)
	s1.weight = 45;
}
int main()
{
	Funtest();
	return 0;
}

由上述例子可得出:公有继承中,派生类中可访问基类的公有成员(包括属性和方法),类外派生类的对象可访问基类的公有成员(包括属性和方法)


eg2:

<pre name="code" class="cpp">#include <iostream>
using namespace std;

class Person
{
protected://保护数据成员
	int length;//身高
	int weight;//体重
};

class Student:public Person
{
public:
	void test1()
	{
		length = 183;//子类中可访问父类的保护成员(包括属性和方法)
		weight = 70;
		total = 300;
	}
private:
	int total;//总分
	char* school;//学校
};

void Funtest()
{
	Student s1;
	s1.length = 160;//类外派生类的对象不可访问基类的保护成员(包括属性和方法)
	s1.weight = 45;
}
int main()
{
	Funtest();
	return 0;
}


 
 

编译结果:

由上述例子可得出:公有继承中,派生类中可访问基类的保护成员(包括属性和方法),类外派生类的对象不可以访问基类的保护成员(包括属性和方法)

eg3:

#include <iostream>
using namespace std;

class Person
{
	private://私有数据成员
	int length;//身高
};

class Student:public Person
{
public:
	void test1()
	{
		length = 183;//子类中不可以访问父类的私有成员(包括属性和方法)
	}
};

void Funtest()
{
	Student s1;
	s1.length = 160;//类外派生类的对象不可以访问基类的私有成员(包括属性和方法)
}
int main()
{
	Funtest();
	return 0;
}

编译结果:



由上述例子可得出:公有继承中,派生类中不可以访问基类的私有成员(包括属性和方法),类外派生类的对象不可以访问基类的私有成员(包括属性和方法)


(二)保护继承


eg1:

#include <iostream>
using namespace std;

class Person
{
	public://公有数据成员
	int length;//身高
};

class Student:protected Person
{
public:
	void test1()
	{
		length = 183;//子类中可以访问父类的公有私有成员(包括属性和方法)
	}
};

void Funtest()
{
	Student s1;
	s1.length = 160;//类外派生类的对象不可以访问基类的公有成员(包括属性和方法)
}
int main()
{
	Funtest();
	return 0;
}

编译结果:


结论: 保护继承中,派生类中可以访问基类的公有成员(包括属性和方法),类外派生类的对象不可以访问基类的私有成员(包括属性和方法)


eg2:

#include <iostream>
using namespace std;

class Person
{
	protected://保护数据成员
	int length;//身高
};

class Student:protected Person
{
public:
	void test1()
	{
		length = 183;//子类中可以访问父类的保护成员(包括属性和方法)
	}
};

void Funtest()
{
	Student s1;
	s1.length = 160;//类外派生类的对象不可以访问基类的保护成员(包括属性和方法)
}
int main()
{
	Funtest();
	return 0;
}
编译结果:


结论:保护继承中,派生类中可以访问基类的保护成员(包括属性和方法),类外派生类的对象不可以访问基类的保护成员(包括属性和方法)


eg3:

#include <iostream>
using namespace std;

class Person
{
	private://私有数据成员
	int length;//身高
};

class Student:protected Person
{
public:
	void test1()
	{
		length = 183;//子类中可以访问父类的私有成员(包括属性和方法)
	}
};

void Funtest()
{
	Student s1;
	s1.length = 160;//类外派生类的对象不可以访问基类的私有成员(包括属性和方法)
}
int main()
{
	Funtest();
	return 0;
}

编译结果:


结论: 保护继承中,派生类中不可以访问基类的私有成员(包括属性和方法),类外派生类的对象不可以访问基类的私有成员(包括属性和方法)


(三)私有继承


eg1:

#include <iostream>
using namespace std;

class Person
{
	public://公有数据成员
	int length;//身高
};

class Student:private Person
{
public:
	void test1()
	{
		length = 183;//子类中可以访问父类的公有成员(包括属性和方法)
	}
};

void Funtest()
{
	Student s1;
	s1.length = 160;//类外派生类的对象不可以访问基类的公有成员(包括属性和方法)
}
int main()
{
	Funtest();
	return 0;
}

运行结果:


结论:私有继承中,派生类中可以访问基类的公有成员(包括属性和方法),类外派生类的对象不可以访问基类的公有成员(包括属性和方法)



eg2:

#include <iostream>
using namespace std;

class Person
{
	protected://保护数据成员
	int length;//身高
};

class Student:private Person
{
public:
	void test1()
	{
		length = 183;//子类中可以访问父类的保护成员(包括属性和方法)
	}
};

void Funtest()
{
	Student s1;
	s1.length = 160;//类外派生类的对象不可以访问基类的保护成员(包括属性和方法)
}
int main()
{
	Funtest();
	return 0;
}
运行结果:


结论:私有继承中,派生类中可以访问基类的保护成员(包括属性和方法),类外派生类的对象不可以访问基类的保护成员(包括属性和方法)



eg3:

#include <iostream>
using namespace std;

class Person
{
	private://私有数据成员
	int length;//身高
};

class Student:private Person
{
public:
	void test1()
	{
		length = 183;//子类中不可以访问父类的私有成员(包括属性和方法)
	}
};

void Funtest()
{
	Student s1;
	s1.length = 160;//类外派生类的对象不可以访问基类的私有成员(包括属性和方法)
}
int main()
{
	Funtest();
	return 0;
}
运行结果:


结论:私有继承中,派生类中不可以访问基类的私有成员(包括属性和方法),类外派生类的对象不可以访问基类的私有成员(包括属性和方法)


总结:

    吐舌头   三种继承方式下,在派生类中均可访问基类的共有成员和保护成员,私有成员不能访问(因为基类和派生类不属于同一作用域)。    

    吐舌头 公有继承中, 在类外通过子类的对象可访问基类的公有成员(包括成员数据和成员函数),其他情况在类外均访问不了。

    吐舌头  保护继承中,派生类中基类的public成员降级为protected,protected成员仍为protected。

    吐舌头  私有继承中,派生类中基类的public成员降级为private,protected降级为private。

    吐舌头  派生类继承了除基类的构造函数和析构函数之外的所有成员,稍后讲解。

    吐舌头  public继承是一个接口继承,保持is-a原则。每个父类可用的成员对子类都可用,每个子类成员都可看做一个父类成员。

     吐舌头 private和protected是实现继承,保持has-a原则。类似一组合/聚合。但它比组合更低级,当一个派生类对象需要访问基类的保护成员或需要重新定义虚函数时它就是合理的,绝大多数情况下我们都会选择public继承。



四,派生类的六个默认成员函数



在继承关系里,如果我们没有显示的定义这六个成员函数,则编译系统会在适合场合为我们自动合成。


1、继承关系中构造函数和析构函数的调用顺序:


class B
{
public:
	B()
	{
		cout<<"B()"<<endl;
	}
	~B()
	{
		cout<<"~B()"<<endl;
	}
};

class D:public B
{
public:
	D()
	{
		cout<<"D()"<<endl;
	}
	~D()
	{
		cout<<"~D()"<<endl;
	}
};

void Funtest()
{
	D d;
}
int main()
{
	Funtest();
	return 0;
}

非常简单的一段代码,你觉得会打印什么呢?一起来看看




有人看到这里,肯定会说,那明摆着嘛,先调用B类的构造函数再调用D类的构造函数,根据栈空间先进后出的原则,接着先析构B类自己,再析构从基类那继承来的部分,可是,事实真的这么简单吗?当然不。

       你想想,你创建的是子类的对象,怎么可能先去调用基类的构造函数呢,既然这样,为什么打印结果显示的确是先调用父类的构造函数呢???这里牵扯到构造函数的调用次序和函数体的执行顺序的问题,注意不要混淆它们。

 

实际上,调用顺序是这样的:


你可能会有点疑惑,明明先调用了子类的构造函数,可是为什么会去先执行基类的构造函数体,这是因为我们显示定义了父类的缺省构造函数,你想想,既然显示定义了,就必须调用它对吧,如果我们不在子类的构造函数中调用它,那何时调用呢,但是由于我们创建的是子类对象,所以必须先调用子类的构造函数,思来想去,将基类的构造函数放在子类的初始化列表中调用似乎再合适不过了,所以,我们C++的设计者们就是这样做的,是不是很聪明呢?   

      

       而析构函数的的调用顺序与构造函数的调用顺序刚好相反,先调用子类的构造函数,再调用父类的构造函数


在这里,还需要注意一点,如果基类的显示的定义了缺省的构造函数,那么基类的构造函数即使不显示定义,编译器也会为我们合成默认的构造函数用来调用基类的构造函数,组合也是如此。


2、派生类的构造函数和析构函数

      我们知道,基类的构造函数和析构函数是不能被继承的,鉴于这一点,派生类的构造函数和析构函数定义时需要注意那些问题呢?

      

派生类的构造函数:

(1)当基类显示地定义了缺省构造函数(即构造函数没有参数或全缺省)时,派生类可以显示定义自己的构造函数,也可以不定义,如果没有显示定义,编译器会自动替我们合成默认构造函数去调基类的构造函数。

(2)当基类有带参的构造函数时,派生类必须定义构造函数(即便函数体为空也必须定义),以提供吧参数传给基类构造函数的途径

派生类构造函数的一般格式:

派生类名(参数总表):基类名(参数表)

{

      派生类新增数据成员的初始化语句;

}

(3)含有子对象的派生类的构造函数

派生类的数据成员中包含基类的对象时,此对象称为子对象,即对象中的对象。

当派生类中包含子对象时,其构造函数的一般格式:

派生类名(参数总表):基类名(参数表0),子对象名1(参数表1),...,子对象名n(参数表n)

{

      派生类新增数据成员的初始化语句;

}

在定义派生类对象时,构造函数的调用顺序:

先调用派生类的构造函数---->调用基类的构造函数并执行函数体---->调用子对象的构造函数并执行函数体---->调用派生类的构造函数体


五,继承体系中的作用域


1,继承体系中,子类的作用域和父类的作用域属于两个作用域。(在子类中不能访问父类的私有成员足以说明此点)


2,同名隐藏。如果子类中包含和父类相同名字的成员,则子类成员将屏蔽对父类成员的直接访问,如果想要在子类中访问父类的同名成员,就必须采用作用域限定符。

eg:

class B
{
protected:
	int _a;
};

class D:public B
{
public:
	void test()
	{
		_a = 10;
	}
private:
	int _a;
};

void Funtest()
{
	D d;
	d.test();
}
int main()
{
	Funtest();
	return 0;
}


运行结果:


从监视窗口中,我们清晰的看到了子类对象的_a被改为10,而父类的成员数据_a仍然是一个随机值。怎样做到在子类对象中改变的是父类对象的成员数据呢?


eg2:

class B
{
protected:
	int _a;
};

class D:public B
{
public:
	void test()
	{
		B::_a = 10;
	}
private:
	int _a;
};

void Funtest()
{
	D d;
	d.test();
}
int main()
{
	Funtest();
	return 0;
}

运行结果:



注意:尽量避免父类和子类使用同名成员,不要给自己挖坑哦大笑


3,访问声明

      前面有提到,对于公有继承,数据成员函数继承到派生类中的访问属性不发生降级,通过派生类的对象仍可以在类外访问基类的公有成员,但是对于私有继承却做不到,因为他们在继承过程中对基类成员的访问属性进行了不同程度的降级,但是我们仍想通过派生类的对象在类外访问基类的私有成员该怎么办呢?这就引入了我们的访问声明。

       访问声明的方法就是把基类的保护成员或公有成员直接写在私有派生类定义式中的同名段中,同时给成员名前冠以基类名和作用域标识符。利用该种方法,该成员就成为派生类的保护成员或公有成员了。


使用访问声明时需注意的问题:

(1)数据成员也可以使用访问声明。

(2)访问声明中只含不带参数和类型的函数名或变量名。

(3)访问声明不能改变成员在基类中的访问属性。

(4)对于基类中的重载函数名,访问声明对基类中所有同名函数起作用。


六,赋值与转换----赋值兼容规则


1,子类对象可以直接赋值给父类对象(切片/割片)。

2,父类对象不能直接赋值给子类对象。

3,父类对象的引用或指针可以直接指向子类对象。

4,子类对象的引用或指针不可以直接指向父类对象。(强制类型转换可完成)

对象赋值:


引用或指针:

class B
{
protected:
	int _b;
};

class D:public B
{
private:
	int _d;
};

void Funtest()
{
	D d;
	B *b;
	b = &d;//父类指针指向子类对象

	D *d1;
	B b1;
	d1 = (D*)&b1;//子类对象可通过强制类型转换指向父类对象(尽量避免)

	D &d2 = d;
	B b2;
	b2 = d2;//父类引用指向子类对象

	D d3;
	B &b3 = b1;
    d3 = (D&)b3;//父类引用指向子类对象
}
int main()
{
	Funtest();
	return 0;
}


 
七、单继承&多继承&菱形继承

1、 单继承 :一个子类仅有一个直接的父类。

单继承中类中成员数据的分布与成员变量在类中的定义顺序有关。
2、多继承:一个子类有两个或两个以上直接的父类。

多继承中派生类成员的分布与继承类的先后次序有关

3、菱形继承(钻石继承)

菱形继承中成员的分布与最底层类继承的先后次序有关。


上图中我们标出了菱形继承中各个类所占字节数,可是C1类和C2类中都继承了B类中的
数据成员_b,那么如果我们通过D类的对象对_b进行访问,必然会产生二义性。
class B
{
	int _b;
};
class C1:public B
{
	int _c1;
};
class C2:public B
{
	int _c2;
};
class D:public C1,public C2
{
	int _d;
};
void Funtest()
{
	D d;
	d._b = 10;//错误,访问不明确
	d.C1::_b = 10;//正确
	d.C2::_b = 10;//正确
}


如何避免这种访问不明确呢,是否可以将重复部分_b只在D类中保存一份呢,这将
引入虚拟继承的概念。




eg:

class B
{
	int _b;
};
class C1:virtual public B
{
	int _c1;
};
class C2:virtual public B
{
	int _c2;
};
class D:public C1,public C2
{
	int _d;
};
void Funtest()
{
	B b;
	C1 c1;
}

上面这段代码就是一个虚拟继承的例子,注意关键字virtual的位置不要写错哦吐舌头



当创建好C1类的变量c1时,编译器会为C1合成一个默认的构造函数,这个合成的默认

构造函数会做哪些事呢?

首先,如果基类有缺省构造函数,它会去调它,其次,它会将偏移量的地址指针

(虚指针)放在c1对象的前4个字节处



再来看一下如果是D类的对象,又是怎么存储的呢?



动脑筋想一下,如果B,C1,C2,D均为空类,每个类所占字节大小又是多少呢?




最后,需要注意的几点:

1、友元关系不能继承,因为友元关系不属于类的成员(就好比你朋友的女朋友并不是

你的女朋友)。

2、如果类中包含静态成员,无论继承了多少派生类,静态成员都只保存一份

3、析构函数和构造函数不能被继承下来。原因:派生类除了继承基类的成员外,还

可以添加只属于自己的新成员,如果用继承来的构造函数初始化,只能初始化从基类

继承来的那部分,而派生类本身新添加的那部分成员初始化不了。析构函数也是一样

的,初始化不到派生类新添加的成员,导致内存泄漏。
















评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值