【1++的C++初阶】之继承

本文详细介绍了面向对象编程中的继承机制,包括基类和派生类的概念、对象赋值规则、构造函数和析构函数的行为、继承与友元的关系以及菱形继承和虚拟继承的解决方案。同时讨论了组合与继承的区别,强调了在实际编程中的选择和权衡。
摘要由CSDN通过智能技术生成

👍作者主页:进击的1++
🤩 专栏链接:【1++的C++进阶】

一,什么是继承?

在这里插入图片描述
继承机制是面向对象程序设计使代码可以复用的重要手段。它使得我们可以在原有类的基础上可以进行扩展,其产生的新类我们称为派生类或者子类;原来的类我们称为基类或父类。继承呈现了面向对象程序设计的层次结构。
我们以一下代码为例:

class person
{
public:
	void Print()
	{
		cout << "person" << endl;
	}
protected:
	char name;
	int age;
};

class student : public person
{
private:
	int num;//学号
};


void Test1()
{
	student s1;
	person p1;
	s1.Print();
	p1.Print();
}

在这里插入图片描述
在这里插入图片描述

上述代码中,我们先是定义了一个person类,其定义了人的基本信息:姓名,年龄。接着,又定义了另一个学生类,定义了学号,并且继承了person类,复用了person的成员。通过监视窗口 我们可以很清楚的看到,在student类实例化出的对象s1中,其成员变量除了自己的还有从person中继承下来的成员变量。成员函数也继承了下来。

讲完了什么是继承,我们接下来讲继承的格式。

//    派生类    继承方式  基类
class student : public person
{
private:
	int num;//学号
};

如上就是继承的格式,派生类和基类我们在前面提过,那么什么是继承方式呢?
在这里插入图片描述
继承方式与我们的访问限定符一样都是public , protected ,private三种。
继承方式与访问限定符的关系如下表:
在这里插入图片描述
这里我们可以总结一个规律来记住上表:我们设public的权限>protected的权限>private的权限。继承后的成员的访问限定符则是在基类的访问限定符与继承方式中选择权限最小的那一个。接下来我们来细细讲解继承后的成员权限的不同处。若继承后是public成员,则在类的里面外面都可以进行访问 ;若是protected则只能在类里面访问;若是private,则不可见,也就是其虽然被继承到了派生类中,但在类里面类外面都访问不了。
在这里插入图片描述

二,基类和派生类对象赋值转换

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
以下代码为例:

class person
{
public:
	void Print()
	{
		cout << "person" << endl;
	}
protected:
	char name;
	int age;
};

//    派生类    继承方式  基类
class student : public person
{
private:
	int num=111;//学号
};

void Test2()
{
	student s1;
	person p1 = s1;//派生类对象赋值给基类
	person* ptr1 = &s1;//派生类对象赋值给基类的指针
	person& p2 = s1;//派生类对象赋值给基类的引用
}

在这里插入图片描述
通过观察结果,我们发现确实有切片的现象。

基类对象不能赋值给派生类对象。

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
要注意的是:这种情况转换时虽然可以,但是会存在越界访问的问题。

在这里插入图片描述

基类与派生类的作用域

基类与派生类都有其独立的作用域。
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。

如下:

class person
{
public:
	int Print()
	{
		cout << "person" << endl;
	}
protected:
	char name;
	int age;
};

//    派生类    继承方式  基类
class student : public person
{
public:
	void Print()
	{
		cout << "test" << endl;
	}

	int num=111;//学号
};


void Test1()
{
	student s1;
	s1.Print();
	
}

在这里插入图片描述
若想访问基类中的同名函数,可以通过 基类::基类成员
在这里插入图片描述
还要注意区分重载与隐藏:
重载必须在同一个作用域。

三,派生类的默认成员函数

构造函数
. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

来看代码:

class person
{
public:

	person(string name="张三", const int age = 20)
		:_name(name)
		,_age(age)
	{
		cout << "person()" << endl;
	}
	
	void Print()
	{
		cout << "person" << endl;
	}
protected:
	string _name;
	int _age;
};

//    派生类    继承方式  基类
class student : public person
{
public:

	student(int num,string name = "张三", const int age = 20)
		:_num(num)
		//,person(name,age)
	{
		cout << "student()" << endl;
	}

	void Print()
	{
		cout << "test" << endl;
	}
private:
	int _num=111;//学号
};

void test3()
{
	student s1(12345);
}

当基类有默认构造时,派生类会自己调用基类的构造函数去初始化基类的那部分成员。
在这里插入图片描述
若没有默认构造则必须在派生类初始化列表显式调用。

student(int num,string name = "张三", const int age = 20)
		:_person(name,age)_num(num)
	{
		cout << "student()" << endl;
	}
		
	{
		cout << "student()" << endl;
	}

拷贝构造
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

class person
{
public:

	person(string name="张三", const int age = 20)
		:_name(name)
		,_age(age)
	{
		cout << "person()" << endl;
	}

	person(const person& p)
	{
		_name = p._name;
		_age = p._age;
		cout << "person拷贝" << endl;
	}
	
	void Print()
	{
		cout << "person" << endl;
	}
protected:
	string _name;
	int _age;
};

//    派生类    继承方式  基类
class student : public person
{
public:

	student(int num,string name = "张三", const int age = 20)
		:_num(num)
		
	{
		cout << "student()" << endl;
	}

	student(const student& s)
		:person(s)
		,_num(s._num)
	{
		cout << "student拷贝" << endl;
	}


	void Print()
	{
		cout << "test" << endl;
	}
private:
	int _num;//学号
};

void test3()
{
	student s1(12345);
	//student s2("李四",21,12345);
	student s2(s1);
}

在这里插入图片描述

. 派生类的operator=必须要调用基类的operator=完成基类的复制

class person
{
public:

	person(string name="张三", const int age = 20)
		:_name(name)
		,_age(age)
	{
		cout << "person()" << endl;
	}

	person(const person& p)
	{
		_name = p._name;
		_age = p._age;
		cout << "person拷贝" << endl;
	}

	person& operator=(const person& p)
	{
		cout << "operator=" << endl;
		if (this != &p)
		{
			_name = p._name;
			_age = p._age;
		}
		return *this;
	}
		
		
		
	
	
	void Print()
	{
		cout << "person" << endl;
	}
protected:
	string _name;
	int _age;
};

//    派生类    继承方式  基类
class student : public person
{
public:

	student(int num,string name = "张三", const int age = 20)
		:person(name,age)
		,_num(num)
		
	{
		cout << "student()" << endl;
	}

	student(const student& s)
		:_num(s._num)
	{
		cout << "student拷贝" << endl;
	}

	student& operator=(const student& s)
	{
		cout << "operator=" << endl;
		if (this != &s)
		{
			person::operator=(s);
			_num = s._num;
		}

		return *this;
	}


	void Print()
	{
		cout << "test" << endl;
	}
private:
	int _num;//学号
};

void test3()
{
	student s1(12345);
	student s2(123,"李四",21);
	//student s2(s1);
	s1 = s2;

}

在这里插入图片描述

析构函数

派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

class person
{
public:

	person(string name="张三", const int age = 20)
		:_name(name)
		,_age(age)
	{
		cout << "person()" << endl;
	}

	person(const person& p)
	{
		_name = p._name;
		_age = p._age;
		cout << "person拷贝" << endl;
	}

	person& operator=(const person& p)
	{
		cout << "operator=" << endl;
		if (this != &p)
		{
			_name = p._name;
			_age = p._age;
		}
		return *this;
	}
		
	~person()
	{
		cout << "~person" << endl;
	}

	void Print()
	{
		cout << "person" << endl;
	}
protected:
	string _name;
	int _age;
};

//    派生类    继承方式  基类
class student : public person
{
public:

	student(int num,string name = "张三", const int age = 20)
		:person(name,age)
		,_num(num)
		
	{
		cout << "student()" << endl;
	}

	student(const student& s)
		:_num(s._num)
	{
		cout << "student拷贝" << endl;
	}

	student& operator=(const student& s)
	{
		cout << "operator=" << endl;
		if (this != &s)
		{
			person::operator=(s);
			_num = s._num;
		}

		return *this;
	}

	~student()
	{
		cout << "~student" << endl;
	}

	void Print()
	{
		cout << "test" << endl;
	}
private:
	int _num;//学号
};

void test3()
{
	student s1(12345);
	//student s2(123,"李四",21);
	//student s2(s1);
	//s1 = s2;

}

在这里插入图片描述
通过上述运行结果我们发现,在派生类的析构函数调用完后,自动调用了基类的析构函数。
若我们我们在派生类中显式调用基类的析构函数时。

~student()
	{
		person::~person();
		cout << "~student" << endl;
	}

在这里插入图片描述
我们会发现基类的析构函数被调用了两次,这时就会造成空间的二次释放。
需要注意的是:在一些场景中析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

四,继承与友元,静态成员

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。

如下实例:

class student;
class person
{
public:
	friend void Print(const person& p , const student& s);

	person(string name="张三", const int age = 20)
		:_name(name)
		,_age(age)
	{
		cout << "person()" << endl;
	}

	person(const person& p)
	{
		_name = p._name;
		_age = p._age;
		cout << "person拷贝" << endl;
	}

	person& operator=(const person& p)
	{
		cout << "operator=" << endl;
		if (this != &p)
		{
			_name = p._name;
			_age = p._age;
		}
		return *this;
	}
		
	~person()
	{
		cout << "~person" << endl;
	}

	void Print()
	{
		cout << "person" << endl;
	}
protected:
	string _name;
	int _age;
};


void Print(const person& p, const student& s)
{
	cout << p._name	<< endl;
	cout << s._num << endl;

}

在这里插入图片描述
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
如下实例:

class student;
class person
{
public:
	friend void Print(const person& p , const student& s);

	person(string name="张三", const int age = 20)
		:_name(name)
		,_age(age)
	{
		cout << "person()" << endl;
		_count++;
	}

	person(const person& p)
	{
		_name = p._name;
		_age = p._age;
		cout << "person拷贝" << endl;
	}

	person& operator=(const person& p)
	{
		cout << "operator=" << endl;
		if (this != &p)
		{
			_name = p._name;
			_age = p._age;
		}
		return *this;
	}
		
	~person()
	{
		cout << "~person" << endl;
	}

	void Print()
	{
		cout << "person" << endl;
	}

protected:
	string _name;
	int _age;

public: 
	static int _count;
};

int person::_count = 0;

void Print(const person& p, const student& s)
{
	cout << p._name	<< endl;
	//cout << s._num << endl;

}

//    派生类    继承方式  基类
class student : public person
{
public:

	student(int num,string name = "张三", const int age = 20)
		:person(name,age)
		,_num(num)
		
	{
		cout << "student()" << endl;
	}

	student(const student& s)
		:_num(s._num)
	{
		cout << "student拷贝" << endl;
	}

	student& operator=(const student& s)
	{
		cout << "operator=" << endl;
		if (this != &s)
		{
			person::operator=(s);
			_num = s._num;
		}

		return *this;
	}

	~student()
	{
		//person::~person();
		cout << "~student" << endl;
	}

	void Print()
	{
		cout << "test" << endl;
	}
private:
	int _num;//学号
};


void test4()
{
	student s1(1234);
	student s2(12435);
	student s3(6535);
	cout << person::_count << endl;


}

在这里插入图片描述

五,菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承.

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况。

下图为菱形继承
在这里插入图片描述
代码如下:

class person
{
public:
	string _name;
	int _age;

};

class student : public person
{
protected:
	int _num;//学号
};

class teacher:public person
{
protected:
	int id;//职工号码
};

class Assistant :public student, public teacher
{

protected:
	string major;//专业

};

我们从对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中person成员会有两份。

二义性问题可以通过显式指定访问来解决,但数据冗余问题解决不了。

void test5()
{
	Assistant a1;
	a1.teacher::_name = "张三";
	a1.student::_name = "李四";
}

在这里插入图片描述
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。

在这里插入图片描述

在这里插入图片描述
通过上述窗口中的结果我们可以得出,其person似乎是仅有一份?这是怎么回事呢?
在这里插入图片描述

class person
{
public:
	
	int pp;

};

class student :virtual public person
{
public:
	int ss;
};

class teacher:virtual public person
{
public:
	int tt;
};

class Assistant :public student, public teacher
{

public:
	int aa;

};

void test6()
{
	Assistant a1;
	a1.aa = 1;
	a1.student::pp = 2;
	a1.teacher::pp = 3;
	a1.ss = 4;
	a1.tt = 5;
	

}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六,总结

组合与继承
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。

组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

如何使类不被继承

  1. 父类的构造函数私有化,在子类不可见。子类对象实例化时,就无法调用父类的构造函数。
  2. final关键字

在这里插入图片描述
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进击的1++

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值