剖析C++中的继承

剖析C++继承

前言

​ 继承是C++中非常重要的一大特性,本文将从概念开始讲解继承的规范、特性以及不同情况下的注意点,包括赋值转换、作用域问题、默认函数调用问题、友元函数、静态成员等,希望本文对你能够有所帮助。


一、继承的概念

1. 概念

​ 继承(inheritance) 机制是面向对象程序设计代码复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

示例代码:

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18; // 年龄
};

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

class Teacher : public Person
{
protected:
	int _jobid; // 工号
};

void test()
{
	Student s;
	s.Print();
}

在上面代码中,Student 类继承了 Person 类的公有成员和保护成员,当然类成员包括成员函数和成员变量。于是我们定义的 Student 类型对象 s 可以调用 Person 类中的成员函数 Print() 和成员变量 _name 和 _age。

运行结果:

在这里插入图片描述

继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。

测试函数中加入 Teacher 和 Person 类对象:

void test()
{
	Student s;
	s.Print();
	Teacher t;
	t.Print();
	Person p;
	p.Print();
}

下面我们使用监视窗口查看Student和Teacher对象:

在这里插入图片描述

2. 定义方式

在这里插入图片描述

继承方式有“public protected private”三种,分别会对子类访问父类成员造成不同的影响:

在这里插入图片描述

3. 注意要点

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它

  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected

  3. 基类的私有成员在子类都是不可见,基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。

  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。

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

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去
  • 基类对象不能赋值给派生类对象
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的

在这里插入图片描述

为了方便测试,将Student类中限定_stuid成员的限定符由 protected 改为 public:

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

给出测试函数:

Student sobj;
// 1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;

// 2.基类对象不能赋值给派生类对象
sobj = pobj;

// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj;	// 父类指针指向子类对象
Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
ps1->_stuid = 10;

pp = &pobj;	//	父类指针指向切片后的父类对象
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
ps2->_stuid = 10;	// 试图通过子类指针修改经过切片后的子类对象赋值给的父类对象中并不含有的_stuid元素(造成越界)

在这里插入图片描述

所以需要特别注意对象切片后原有属性中被切掉的部分不会被保留,切片后继续访问会导致越界访问!

三、继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显式访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际中在继承体系里面最好不要定义同名的成员
class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		cout << "func(int i)->" << i << endl;
	}
};

如上代码中,两个 fun() 函数构成重载吗?

重载的必要条件:同一作用域

所以,上面两函数不构成重载!

现在我们测试通过 B 类型对象调用 fun 函数,观察特性:

B b1;
b1.fun(10);

运行结果:

在这里插入图片描述

我们发现最终调用的是B类的函数,如果我要利用B类型对象访问A类中的被覆盖的函数呢?

// 可以通过显式调用的方法
B b1;
b1.fun(10);
b1.A::fun();	// 显式调用

运行结果:

在这里插入图片描述

四、基类与派生类默认成员函数调用关系

为了便于我们观测类对象创建到销毁整个过程,下面对构造、析构、拷贝函数作了可视化处理,通过打印结果方便我们确定各个函数的调用先后问题:

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; // 姓名
};
class Student : public Person
{
public:
	Student(const char* name, int num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	}
	Student(const Student& s)
		: Person(s)
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}
	Student& operator = (const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator =(s);
			_num = s._num;
		}
		return *this;
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	int _num; //学号
};

先对派生类对象进行构造析构测试:

void test()
{
	Student s1("jack", 18);
}

运行结果:

在这里插入图片描述

接着我们利用派生类的拷贝构造函数生成一个新对象s2:

void test()
{
	Student s1("jack", 18);
	Student s2(s1);
}

运行结果:

在这里插入图片描述

我们看到派生类在创建时,无论通过普通构造函数还是拷贝构造函数,都会先行调用父类的对应函数,接着才调用子类构造。而析构函数恰恰相反,往往需要子类析构函数先行调用后才执行父类析构。可以将整个过程巧记为:子类 – “迟到早退”

五、继承与友元函数

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

在这里插入图片描述

六、继承与静态成员

基类定义了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; // 学号
};
class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};
void test()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << "人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << "人数 :" << Person::_count << endl;
}

运行结果:

在这里插入图片描述

七、菱形继承

​ 由于想要解读清楚菱形继承三言两语是难以做到的,为了讲述的详细度和逻辑性,我将在下面这篇文章进行详细解读,文章直达链接:剖析菱形继承与虚继承

总结

​ 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。后面我会推出部分场景下更适合用组合而不是继承的案例,并对两者的适用情况分析对比。在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

螺蛳粉只吃炸蛋的走风

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

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

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

打赏作者

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

抵扣说明:

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

余额充值