C++面向对象继承与多态(1)

前言

先前的博客里有提到,面向对象的特性有三个:封装、继承与多态。其中继承与多态是最重要,也是内容比较多的部分,我会分三到四期来记录和总结自己的个人所学。主要的参考视频是浙江大学翁恺老师的《面向对象设计C++》,他的视频解答了我许多一直以来在面向对象上的迷惑,感兴趣的同学可以去B站搜一下他的视频。

当代码需求的功能比较多、规模比较大的时候,面向过程的编程思想就会暴露其项目代码臃肿,难以阅读,维护成本高,不易拓展功能等等的弊端,做个项目去实际体会一下就知道了。在这种情况下,面向对象的编程思想就能够很好地解决这些问题,当然这也说明了他需要大体量的项目才能较好地体现他的优势。总的来说各有各的好处,都是必须要理解和掌握的编程思想。

继承与派生

描述

继承与派生是同一个过程的不同角度,我们将保持已有类的特性从而构筑新类的过程叫做继承,在已有类的基础上增加自己新类自己的特性叫做派生。被继承的已有类被称为基类(父类),派生出的新类叫做派生类(子类)。

目的

  • 继承的目的:实现代码重用
  • 派生的目的:当新的问题出现时,原有的程序不能解决问题(或者不能完全解决问题)时,就要对原有的程序进行改造,这里的改造不是指修改源码。

派生类的声明与定义

假设这里有个基类:

class base
{
public:
	void who_am_i()
	{
		cout << "I'm base class" << endl;
	}
};

另一个类继承base类,并增加自己的属性和方法:

class derived: public base	//继承base类
{
public:
	void who_am_i()
	{
		cout << "I'm derived class" << endl;
		do_something();
	}
	
	void do_something()		//派生类新增的方法
	{
		cout << "I inherit from base class" << endl;
	}
	
private:
	int m_int;				//派生类新增的成员
};

派生类也可以有多个基类,其声明方法如下:

class derived: public base1, public base2, .....
{
	.....
};

protected访问权限

之前我们讲过了public和private访问权限,这个protected访问权限之所以留在这里讲是因为他是专为继承而生的访问权限。他跟private一样,限制类的对象对其成员的直接访问,但是在该类的派生类内又可以像public一样直接访问。

class base
{
public:
	void who_am_i()
	{
		cout << "I'm base class" << endl;
	}
	
protected:
	int m_pro_int;

private:
	int m_pri_int;
};
class derived: public base
{
public:
	void who_am_i()
	{
		cout << "I'm derived class" << endl;
		do_something();
	}
	
	void do_something()
	{
		cout <<  m_pro_int << endl;
		//cout <<  m_pri_int << endl;	//error
	}
	
private:
	int m_int;
};
int main()
{
	base b;
	//b.m_pro_int = 10; //error
	return 0;
}

继承方式

大家可能注意到了class derived冒号后面跟着的public,那要是换成protected或者private可不可以?当然是可以的,这是继承的三种方式:公有继承,私有继承和保护继承。具体描述如下:

  • 公有继承(public):基类中的public和protected成员的访问权限在派生类中保持不变。 派生类可以直接访问基类的public和protected成员,但private成员不能直接访问。通过派生类的对象,只能访问基类的public成员。
  • 私有继承(private):基类中的public和protected成员都以private的访问权限出现在派生类中。 派生类可以直接访问基类的public和protected成员,但private成员不能直接访问。通过派生类的对象,不能直接访问基类的任何成员。
  • 保护继承(protected):基类中的public和protected成员都以protected的访问权限出现在派生类中。 派生类可以直接访问基类的public和protected成员,但private成员不能直接访问。通过派生类的对象,不能直接访问基类的任何成员。

基类的构造函数

有时候基类并没有默认(无参)构造函数,需要我们给基类传一定的参数,或者我们需要特地去调用基类的某个构造函数。如果不显示地调用的话,编译器会默认你是调用的是基类的默认构造函数(如果有的话,没有编译就会直接报错)。具体操作如下:

class base
{
public:
	base(int m_int)
	: m_pri_int(m_int)
	{
		cout << "in function base(int m_int)" << endl;
	}

private:
	int m_pri_int;
};

class derived: public base
{
public:
	derived(int m_int)
	:base(m_int)
	{
		cout << "in function derived(int m_int)" << endl;
	}

	// 编译会报错,除非补充了base类的默认构造函数
	// derived()
	// {
	//		cout << "in function derived()" << endl;
	// }
};

提醒一下:如果派生类有多个基类时,会从左向右调用各个基类的构造函数,不管你在初始化列表是以什么样的顺序调用的基类构造函数。如果有特殊情况需要控制基类的构造顺序,注意继承的顺序就好了:

class derived: public base1, public base2, public base3		//基类构造顺序
{
public:
	derived()
	: base2()
	, base1()
	, base3()
	{
		//调用顺序还是1,2,3
	}
};

顺带一提,成员变量的初始化是按照你的声明顺序从上到下的进行的,也不会受初始化列表顺序的影响。

另外如果基类定义了自己的拷贝构造函数,在定义派生类的拷贝构造函数的时候,记得要调用基类的拷贝构造函数。他会先调用基类的拷贝构造函数,再调用派生类的:

class base
{
public:
	//其他函数省略...
	base(const base& another)
	{
		cout << "base(const base& another)" << endl;
	}
};

class derived
{
public:
	//其他函数省略...
	derived(const derived& another)
	: base(another)
	{
		cout << "derived(const derived& another)" << endl;
	}
};

int main()
{
	derived d1;
	derived d2 = d1;
	// 输出:
	// base(const base& another)
	// derived(const derived& another)
	return 0;
}

深浅拷贝及其造成内存泄漏的原因在我的另外一篇博客有详细说明,感兴趣可以去看一下。

作用域限定

如果你的派生类继承了base1类和base2类,而这两个类中有一个变量名一样的成员变量,这时候你在派生类中使用的该基类成员变量是base1类中的还是base2类中的?编译器不知道你到底是使用哪一边的,这就产生了二义性问题,编译会报错,而作用域操作符(::)就是解决这个问题的。

class base1
{
public:
	base1() : m_pro_int(1) {}

protected:
	int m_pro_int;
};

class base2
{
public:
	base2() : m_pro_int(2) {}

protected:
	int m_pro_int;
};

class derived: public base1, public base2
{
public:
	void do_something()
	{
		//cout << m_pro_int << endl;		//error
		cout << base1::m_pro_int  << endl;	//1
		cout << base2::m_pro_int  << endl;	//2
	}
};

这个作用域操作符还有别的用处,比如使用某个类中定义公有的枚举类型,或者是使用某个命名空间定义的类或函数,都要用到这个操作符。我们经常在main.cpp文件的开头写的using namespace std;意思就是使用std(标准)命名空间,这样我们可以直接使用里面的东西,比如cout其实是std::cout,endl也应该写成std::endl。还有各种STL,vector,string,map,都在这个命名空间里。命名空间也可以自己去定义,这样在开发的过程中就可以避免与他人的变量或者函数重名。作为小知识了解一下,这里就不展开细谈了。

棱形继承问题

上面产生的二义性问题是显而易见的,但是有一种情况的继承就不那么明显,而且可能会被经常使用,那就是棱形继承的情况。考虑如下代码:

class base0
{
public:
	int var0;
};

class base1 : public base0
{
public:
	int var1;
};

class base2 : public base0
{
public:
	int var2;
};

class derived: public base1, public base2
{
public:
	int var;
};

那么derived类的内存模型就是是这样的:
derived类的内存模型
很明显,var0产生了二义性问题,这是棱形继承所带来的问题。当然这个问题也可以通过作用域操作符来解决,但是一般不会去这么做。像这种问题应该采用虚继承的方式去解决:

class base0
{
public:
	int var0;
};

class base1 : virtual public base0
{
public:
	int var1;
};

class base2 : virtual public base0
{
public:
	int var2;
};

class derived: public base1, public base2
{
public:
	int var;
};

注意到base1类和base2类对base0类的继承中多了个virtual,这是C++的一个关键字,意思就是“虚拟的”,而这样的继承我们称其为“虚继承”。这个关键字的作用我会在下一篇中详细说明,他是多态的关键,这里先接触了解一下。

通过使用virtual关键字,derived类的内存模型会变成这样:
derived类的内存模型2
可以看到derived类中并没有两个var0成员,而是多出了两个指向base0类的指针,这样访问var0就能够清楚地定位到base0类的var0。(这里我少画了derived类新增的成员var,他在var2和var0之间)

总结

本篇大概总结了我知道的继承与派生的用法,写得比较随意,有时间的小伙伴可以自己测试一下。这些只是基础的东西,要全面理解和掌握之后,才能够理解多态及其应用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值