C++学习笔记(2)——继承与派生

开始之前,我们先说一句,我们打算用三篇笔记搞定C++面向对象基础,后面开始就要写泛型编程和STL了。节奏有点快是不是?

一、基本概念

从一个类派生出另一个类的格式如下:

class A//基类
{
 	public:
		int n1;
}; 
class B:public A//派生类,继承方式一般用public,当然也有其他方式 
{
	public:
		int n2;
};

此时类A公有派生出了类B,类A称为基类,类B称为派生类,派生类对象也是基类对象。

公有派生的情况下,存在着三条赋值兼容规则(这个和C++三种传递方式(值传递、指针传递和引用传递)相对应):

  • 派生类对象可以赋值给基类对象;
  • 派生类对象可以用来初始化基类引用;
  • 派生类对象的地址(或派生类的指针)可以赋值给基类的指针。

注意,这三条规则仅限于公有派生,如果是私有派生和保护派生则不成立。

这个规则很好理解。基类没有派生类的全部成员,但派生类有基类的全部成员,因此派生类>基类,基类对象当然可以接收派生类的值、址和引用了,但是反过来却不行。

下面这个例子(续上面的程序)展示了这三条规则:

	A a;
	B b;
	a = b;//派生类对象赋值给基类对象
	A &r = b;//派生类对象初始化基类引用
	A *pa = &b;//派生类对象的地址赋值给基类的指针
	B *pb = &b;
	pa = pb;//派生类的指针赋值给基类的指针

如果这三条反过来是不成立的。一般来讲,基类的指针不能赋值给派生类的指针。但是通过强制类型转换,也可以将基类指针强制转换成派生类指针,然后再赋值给派生类指针。只是这样做是有风险的。比如下面这个例子(续上面的程序):

	A *pa = &b; //基类指针能指向派生类对象 
	B *pb = &a; //出错,派生类指针不能指向基类对象 
	B *pb = (B*) &a; //强制转换类型,慎用 
	pa -> n1 = 1;
	pa -> n2 = 2; //出错,pa是基类指针,编译器只会去找基类的成员,不会去找派生类的成员 
	pb -> n1 = 3; //能编译成功,但运行时可能会出现意外的结果

另外,还有多层次的派生。下面这个例子展示了多重派生:

class A//基类
{
	int n1;
}; 
class B:public A//派生类
{
	int n2;
};
class C:public B//派生类
{
	int n3;
};

在本例中,类A派生类B,类B派生类C;类A是类B的直接基类,类B是类C的直接基类;类A是类C的间接基类

二、覆盖

派生类和基类可能有同名成员变量同名成员函数(或二者兼有),这种现象叫做覆盖。看看下面我的程序是如何解决这个问题的:

#include<iostream>
using namespace std;

class A //基类 
{
	public:
		int n;
		void test()
		{
			cout<<"class A!"<<endl;
		}
}; 

class B:public A //派生类 
{
	public:
		int n;//与类A的成员变量发生重名,一般不要这样做 
		void test()//与类A的成员函数发生重名,这种情况反而很常见 
		{
			cout<<"class B!"<<endl;
		}
		void func(); 
};

void B::func()
{
	n = 1;//默认是派生类(B)的 
	A::n = 2;//这个肯定是基类(A)的 
	cout<<n<<" "<<A::n<<endl;
	test();//默认是派生类(B)的 
	A::test();//这个肯定是基类(A)的 
}

int main()
{
	B b;//注意!对象b既是类A又是类B! 
	//因此,对象b也包含类A应有的成员变量 
	b.func();
	return 0; 
}

程序输出结果:

1 2
class B!
class A!

对于多重派生来说也是一样的,对于重名的成员变量或成员函数,你不写类名::,那么系统默认是属于本类的。为防止困惑,遇到重名的还是最好都写上类名::吧。

三、保护(protected)

访问范围说明符protected,是用来修饰成员变量或成员函数的。protected一般用来修饰基类的成员变量或成员函数,这样派生类也可以去访问基类的成员变量或成员函数了,但是在类定义外部是访问不了的(三种范围说明符的关系如下图,可能不太严谨)。
在这里插入图片描述
因此,保护成员的可被访问范围(自己的和我派生的,都能访问,其他类不能访问)大于私有成员(只能自己类内部访问,派生的也不行),而小于公有成员(谁都能访问)。

举一个下面程序的例子:

#include<iostream>
using namespace std;

class A{
	private:
		int pri;//私有成员
	public:
		int pub;//公有成员
	protected:
		int pro;//保护成员
}; 

class B:public A
{
	void test()
	{
		pri = 1;//出错!不能访问基类私有成员
		pub = 2;
		pro = 3;//可以访问基类保护成员
	}
};

int main()
{
	B b;
	b.pri = 1;//出错!类定义外部不能访问私有成员
	b.pub = 2;
	b.pro = 3;//出错!类定义外部不能访问保护成员
	return 0;
}

注意,在基类中,一般都将需要隐藏的成员说明为保护成员而非私有成员。

四、派生类的构造函数&析构函数

派生类对象创建时,也是要调用构造函数进行初始化的,这个过程跟封闭类对象比较类似,但又不尽相同。在派生类内部定义构造函数的格式如下:

构造函数名(形参表):基类名(基类构造函数实参表)
{
	函数体;
}

构造函数和析构函数在基类对象和派生类对象里有不同的生存期,下面我们用一个程序来说明它们是怎么运作的:

#include<iostream>
using namespace std;

class A{ //基类 
	private:
		int n1, n2;
	public:
		A(int _n1, int _n2):n1(_n1), n2(_n2)
		{
			cout<<"A被建造!"<<endl;
		}
		~A()
		{
			cout<<"A被拆毁!"<<endl;
		}
		void Print()
		{
			cout<<n1<<","<<n2<<endl;
		}
};

class B:public A{ //派生类 
	private:
		int m1, m2;
	public:
		//该构造函数说明前两个参数要用基类的构造函数初始化处理 
		//这样派生类对象b的n1、n2也有初始值了
		B(int _n1, int _n2, int _m1, int _m2):A(_n1, _n2), m1(_m1), m2(_m2)
		{
			cout<<"B被建造!"<<endl;
		}
		~B()
		{
			cout<<"B被拆毁!"<<endl;
		}
		void Print()
		{
			cout<<m1<<","<<m2<<endl;
		}
};

int main()
{
	B b(1, 2, 3, 4);
	b.B::Print();
	//调用自己类的同名成员函数时,其实不用注明哪个类也行,但注明也无妨 
	b.A::Print();
	//调用基类或间接派生类的同名成员函数时,要注明是哪个类的 
	return 0;
}

输出结果如下:

A被建造!
B被建造!
3,4
1,2
B被拆毁!
A被拆毁!

由输出结果可以看到,在派生类对象构建时,先执行基类的构造函数,再执行派生类的构造函数;在派生类对象消亡时恰好相反,先执行派生类的析构函数,再执行基类的析构函数。这个步骤与封闭类对象的创建和消亡恰好相反。

对于多层次的派生结构来说,派生类对象生成时,会从最顶层的基类开始逐层往下执行所有基类的构造函数,最后再执行自身的构造函数;当派生类对象消亡时,会先执行自身的析构函数,然后从底向上依次执行各个基类的析构函数。

假如在上面程序main函数里加上A a(5, 6);,那么出现的情况是:整个程序A被建造了两次(这个正常),A被销毁了两次,而不是一次。如果是多层次派生结构,又定义了许多对象,那么中间的构造和析构函数就会被调用很多次了。

五、私有派生和保护派生

上面四节说的都是公有派生的大前提下实现的,因为公有派生是最常用的。实际上还有私有派生和保护派生,这两个反而不常用。下表说明了不同派生方式导致派生类的可访问范围的不同

基类成员公有派生私有派生保护派生
私有成员不可访问不可访问不可访问
保护成员保护私有保护
公有成员公有私有保护

下面我们用程序来说明私有派生的访问范围(一看就懂):

#include<iostream>
using namespace std;

class A{
	private:
		int pri;
	public:
		int pub;
	protected:
		int pro;
};

class B:private A{  
	void test()
	{
		pri = 1;//出错,不能访问类A的私有成员
		pub = 2;//pub在类B里变成私有成员了,不会出错 
		pro = 3;//pro在类B里变成私有成员了,不会出错 
	}
};

class C:public B{
	void test()
	{
		pri = 1;//出错,不能访问类B的私有成员
		pub = 2;//出错,不能访问类B的私有成员
		pro = 3;//出错,不能访问类B的私有成员
	}
};

int main()
{
	B b;
	b.pri = 1;//出错,不能访问私有成员
	b.pub = 2;//出错,不能访问私有成员
	b.pro = 3;//出错,不能访问私有成员
	return 0;
}

下面我们用程序来说明保护派生的访问范围(一看就懂):

#include<iostream>
using namespace std;

class A{
	private:
		int pri;
	public:
		int pub;
	protected:
		int pro;
};

class B:protected A{  
	void test()
	{
		pri = 1;//出错,不能访问类A的私有成员
		pub = 2;//pub在类B里变成保护成员了,不会出错 
		pro = 3;//pro在类B里变成保护成员了,不会出错 
	}
};

class C:public B{
	void test()
	{
		pri = 1;//出错,不能访问类A的私有成员
		pub = 2;//pub在类C里变成保护成员了,不会出错 
		pro = 3;//pro在类C里变成保护成员了,不会出错 
	}
};

int main()
{
	B b;
	b.pri = 1;//出错,不能访问私有成员
	b.pub = 2;//出错,不能访问保护成员
	b.pro = 3;//出错,不能访问保护成员
	return 0;
}

无论是什么派生方式,私有成员都只允许自己访问,其他类是不可访问的。公有派生的特点是,基类该是什么成员,派生类也该是什么成员,原有访问范围不变。保护派生的特点是,无论基类是什么成员(除了私有成员),到了派生类就都是保护成员。私有派生的特点是,无论基类是什么成员(除了私有成员),到了派生类就都是私有成员。

一般情况下,都应该使用公有派生。

暂时先写到这吧,往后修改可能会有补充。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值