[C++进阶]继承

一、继承的概念及定义


1. 继承的概念

继承(inheritance),是面向对象的三大特性之一。

它是面向对象编程中,使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类。

之前我们接触的复用,都是函数设计层面的复用,而继承则是类设计层面的复用,体现了由简单到复杂的认知过程。

2. 继承的定义

举个例子,一个Student类继承一个Person类:

class Person
{
protected:
	string _name;
	int _age;
};

class Student :public Person
{
protected:
	int _stuid;
};

在此处:

  • Person类称为基类(父类),Student类称为派生类(子类)
  • 继承方式为public(公有)继承

3 继承方式和访问限定符

我们之前讲访问限定符时,说过protected在继承会讲,是因为到了继承,protected才和private有所区别。

继承方式:public继承、protected继承、private继承

访问限定符:public访问、protected访问、private访问

4. 继承基类成员访问方式的变化

直接上表格

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

总结:
1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected
> private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强。

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

在我们正常的两个不同类型的对象进行赋值的时候一般是不允许的操作。如果真的允许了,那也是通过类型转换实现的

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "zhangsan";
	int _age = 18;
};
class Student : public Person
{
protected:
	int _stuid;
};
class Teacher : public Person
{
protected:
	int _jobid;
};
int main()
{
	int i = 0;
	double d = i; //发生了类型转换

	Person p;
	Student s;

	p = s;
	//s = p; 不允许的操作
	return 0;
}

在继承中,也是存在类似于类型转换的。
在赋值的过程中,子可以给父,但是父不可以给子
原因也是很简单的。因为派生类中有些成员基类就没有,而基类的所有成员派生类都有,所以有了子可以给父的赋值.

这里的父不可以给子是很严格的,即便使用了强制类型转换,依然报错。语法上直接给禁掉了。

一般我们也将子赋值给父称之向上转换,这样做是可以的。而向下转换,即父对象赋值给子对象是不允许的

这里的赋值转换和普通的赋值还是有一些不一样的。在我们之前的不同类型的赋值中,都要走一个隐式类型转换、强制类型转换等。这些都会产生临时变量。而这里是不会产生临时变量的。这里发生了一个特殊处理,即赋值兼容转换(也可以叫作切割、切片)

这个赋值兼容(切割、切片)是天然的,不会产生临时变量。它不像以前一样不同类型转换会产生临时变量。

这里的切割切片就是认为每一个子类对象都是一个特殊的父类对象,它会将属于父类的一部分切出来进行赋值,然后将它拷贝给父类,所以称为切片。

那么如何证明没有临时变量呢?

我们来测试一下:

int main()
{
	int i = 0;
	//double d = i; //发生了类型转换

	double& d = i;
	const double& d1 = i;

	Person p;
	Student s;

	p = s;
	//s = p; 不允许的操作
	Person& p1 = s;
	return 0;
}


我们发现如果中间产生了临时变量,那么我们使用引用的时候必须加上const进行修饰,因为临时变量具有常性。而我们父类引用子类的时候却没有加上const也不报错,故中间一定没有产生临时变量。而且我们还得出了,引用也可以向上转换。

经过引用以后p1就变成了s中父类部分的别名。我们先将 Person中的成员变量改为公有,然后使用p1这个别名进行修改,可以看到s也被修改了。从而印证了子类的别名也是可以给父类的。父类可以去引用子类。

int main()
{
	Student s;
	s.Print();
	Person& p1 = s;
	p1._name = "lisi";
	p1.Print();
	s.Print();
	return 0;
}

运行结果:

除了引用之外,还有指针也是可以通过向上转换的:

int main()
{
	Student s;
	s.Print();
	//Person& p1 = s;
	//p1._name = "lisi";
	//p1.Print();
	//s.Print();
	Person* Ptrp = &s;
	Ptrp->_name = "wangwu";
	s.Print();
	Ptrp->Print();
	return 0;
}

运行结果:

现在我们就知道了对于向上转换而言,子类对象给父类对象,父类引用子类,父类指针指向子类都是可以的。而对于向下转换,首先父类对象给子类对象是绝对不可以的,那么子类引用父类,子类指针指向父类呢?这里我们先留下一个悬念,我们之后一起探讨。

三、继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
  3. 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)  需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。

例如:

class Person
{
protected:
	string _name = "zhangsan";
	int _age = 18;
	int _num = 666;
};
class Student : public Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
		cout << "num:" << _num << endl;
	}
protected:
	int _stuid;
	int _num = 111;
};
int main()
{
	Student s;
	s.Print();
	return 0;
}

但是如果就想访问父类的也是可以的,我们使用域作用限定符即可。

	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
		cout << "num:" << Person::_num << endl; 
	}

运行结果:

而编译器这样的操作,我们也称之为:隐藏/重定义,即子类和父类有同名成员,默认子类的成员隐藏了父类的成员

同样的,对于成员函数,我们也是同样的道理,默认访问子类的成员函数,但是如果使用域作用限定符,也是可以访问到父类的函数的。

不仅仅对于成员变量存在隐藏,对于成员函数也是存在隐藏的。规则与前面是一样的

class Person
{
public:
	void func()
	{
		cout << "Person::func()" << endl;
	}
protected:
	string _name = "zhangsan";
	int _age = 18;
	int _num = 666;
};
class Student : public Person
{
public:
	void func()
	{
		cout << "Student::func()" << endl;
	}
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
		cout << "num:" << Person::_num << endl;
	}
protected:
	int _stuid;
	int _num = 111;
};
int main()
{
	Student s;
	s.func();
	s.Person::func();
	return 0;
}

运行结果:

我们如果对上面的代码稍作修改

即,在下面这种情况下,两个func构成什么关系?
a.隐藏/重定义 b.重载 c.重写/覆盖 d.编译报错

答案选择a

class Person
{
public:
	void func()
	{
		cout << "Person::func()" << endl;
	}
protected:
	string _name = "zhangsan";
	int _age = 18;
	int _num = 666;
};
class Student : public Person
{
public:
	void func(int i)
	{
		cout << "Student::func(i)" << endl;
	}
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
		cout << "num:" << Person::_num << endl;
	}
protected:
	int _stuid;
	int _num = 111;
};

这道题答案是选a的,我们很容易误选为b,事实上重载的前提条件是在同一个作用域,这两个并不在同一个作用域,所以肯定不是重载。

如下面的测试,只要函数名相同就会构成隐藏,不会考虑到参数这些问题(因为函数名修饰规则在链接阶段)。中间的会在编译阶段就已经报错了。编译阶段带参的隐藏了无参的。所以最终中间的代码会报错

注意:在实际中的继承体系里最好不要定义同名成员

四、派生类的默认成员函数

我们知道类有默认成员函数,那么在派生类中,它们的生成又是如何进行变化的呢?

1.构造函数

我们将下面这个类作为父类

class Person
{
public:
	Person(const char* name = "peter")
		: _name(name)
	{
		cout << "Person()" << endl;
	}
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}
	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
		return *this;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
};

然后当我们对派生类写它的构造函数的时候,我们传统的理解为_name可以直接使用,于是我们直接对_name放在了初始化列表中进行初始化。

class Person
{
public:
	Person(const char* name)
		: _name(name)
	{
		cout << "Person()" << endl;
	}
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}
	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
		return *this;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
};
class Student :public Person
{
public:
	Student(const char* name = "zhangsan", int id = 0)
		:_id(0)
	{}
protected:
	int _id;
};

很好,写出来没有标红的,但是一代我们运行了呢?

这里其实就有点类似于将父类当成一个自定义类型的成员进行处理了。

相当于这里其实就分的很清楚,父类的交给父类的构造函数去搞。子类的自己去搞

而这里如果我们要自己去调用构造函数的话,我们就要像定义一个匿名对象一样在初始化列表中

class Student :public Person
{
public:
	Student(const char* name = "zhangsan", int id = 0)
		:Person(name)
		,_id(id)
	{}
protected:
	int _id;
};

记住初始化列表中,永远也是父类的第一个进行执行。相当于它永远是第一个成员变量。

2.拷贝构造

当我们想要写一个拷贝构造的时候,拷贝构造本质也是一个构造函数,所以也要写初始化列表

class Person
{
public:
	Person(const char* name)
		: _name(name)
	{
		cout << "Person()" << endl;
	}
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}
	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
		return *this;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
};
class Student :public Person
{
public:
	Student(const char* name = "zhangsan", int id = 0)
		:Person(name)
		,_id(id)
	{}
	Student(const Student& s)
		:Person(s)
		,_id(s._id)
	{}
protected:
	int _id;
};
int main()
{
	Student s;

	Student s1(s);
	return 0;
}

如上所示,我们这里对于Person要显式调用它的拷贝构造函数,这里虽然我们没有父类对象,但是由于前面说了,可以向上转换,所以直接将s传过去就可以了。所以下面会被初始化为zhangsan

3.赋值运算符重载,

还有一个默认成员函数是赋值运算符重载,我们不难写出这样的代码,注意这里必须指定父类中的赋值运算符重载,才能将父类的成员函数给赋值过去。然后再来一个普通的赋值即可。如果不指定父类的话,父类默认隐藏的,因此就是默认找子类的,就会发生无穷递归,栈溢出了。

	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_id = s._id;
		}
		return *this;
	}

4.析构函数

如下所示,是我们想象中析构函数应该有的样子。注意,这里也必须加上父类的访问限定符,虽然看上去好像可以直接调用,但是必须加上,因为不加会报错,报错是因为由于多态的原因,析构函数的函数名被特殊处理了,统一处理为destructor

	~Student()
	{
		Person::~Person();
	}

但是这个时我们想象的,它对吗?答案是否定的,它当然不对,如下图所示,我们会发现Person被析构的次数多了一倍。

我们没写Student析构时的析构:

我们写了Student析构时的析构:

所以说,析构函数不需要我们自己去调用。因为它必须要保证析构顺序,默认是最后才析构的(构造顺序是,先父后子,析构顺序是先子后父),为了保证这个顺序,于是编译器始终默认最后才自动调用析构函数。而如果让我们显式调用的话,没法保证先子后父的。而且先析构子再析构父的一个原因就是子可以用父,父不能用子。也就是说,如果先析构了父的话,但是如果后面子突然调用了父的一部分成员,就会出错了。

五、继承与友元

请牢记:友元关系不可以被继承

如下代码所示:

class Student;
class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;
}
void main()
{
	Person p;
	Student s;
	Display(p, s);
}

我们先声明了Student类,然后我们用Student继承Person类,Display函数是Person的友元。所以在Display函数中可以去访问Person类成员变量,但是这个友元关系不可以被继承,所以Display中直接访问Student成员变量直接报错

解决方法也很简单对子类也使用友元

class Student;
class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;
}
void main()
{
	Person p;
	Student s;
	Display(p, s);
}

这样就可以打印了,不过这里我们没有赋值,打出来的是乱码:

六、继承与静态成员

静态成员能否被继承呢?
 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
换言之:静态成员可以认为是继承了,也可以认为没有被继承

在前面的继承中,继承就是指在子类里面存了一份父类的成员。在子类里面可以去访问父类的成员。子类里面存的父类成员和父类成员是没有关系的。都是单独的个体。


在静态成员中,由于一个静态成员只存储一份。所以子类里面并没有这个部分,但是子类确实可以去访问父类里面的这个静态成员。介于一个中间状态,所以我们可以认为它继承了,也可以认为它没有被继承。

class Person
{
public:
	Person() { ++_count; }
//protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};

int Person::_count = 0;

class Student : public Person
{
protected:
	int _stuNum; // 学号
};

int main()
{
	Person p;
	Student s;


	cout << Person::_count << endl;

	cout << &p._name << endl;
	cout << &s._name << endl;

	cout << &p._count<< endl;
	cout << &s._count << endl;

	cout << &Person::_count << endl;
	cout << &Student::_count << endl;

	return 0;
}

运行结果:


本篇的内容到此结束,感谢大家的阅读,如果有问题感谢大家的指出。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值