【C++】继承

从今天开始我们就要进入C++进阶的学习,进阶的语法总体难度也是高于C++初阶...

话不多说,开始我们进阶的第一课——继承吧~

一.继承的概念及定义
 

1.继承的概念

继承机制是 面向对象程序设计使代码可以复用的最重要的手段

它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程

以前我们接触的复用都是函数复用

而继承的本质则是类设计层次的复用
 

下面我们通过代码来观察一下:

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; // 工号
};


int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();
	return 0;
}

上面通过监视窗口查看Student和Teacher对象,我们可以看到变量的复用。

调用Print可以看到成员函数的复用我们可以看出,继承后父类的Person的成员都会变成子类的

一部分,这里体现出了子类 Student和Teacher复用了父类Person的成员

2.继承的定义

定义格式

下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类



 

继承关系和访问限定符

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

一块组合就是3*3=9种继承方式:

类成员/继承方式 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继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强
 

二.基类和派生类对象的赋值兼容转换
 

1.派生类对象(子类)可以赋值给基类(父类)的对象 / 指针 / 引用,这里有个形象的说法叫切片或者切割。意思就是把子类中父类那部分切来赋值过去

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

3.基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类

的指针是指向派生类对象时才是安全的

class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};

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

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

	2.基类对象不能赋值给派生类对象
	//sobj = pobj;
	
	// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
	pp = &sobj;
	Student * ps1 = (Student*)pp; // 这种情况转换时可以的
	ps1->_No = 10;
	pp = &pobj;
	Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
		ps2->_No = 10;
}


 

三.继承中的作用域
 

1. 在继承体系中基类和派生类都有独立的作用域

2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问

这种情况叫隐藏,也叫重定义(在子类成员函数中,可以使用 基类::基类成员 显示访问)

3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏

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

// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected:
	string _name = "小李子"; // 姓名
	int _num = 111; // 身份证号
};
class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " 身份证号:" << Person::_num << endl;
		cout << " 学号:" << _num << endl;
	}
protected:
	int _num = 999; // 学号
};
void Test()
{
	Student s1;
	s1.Print();
};

经典题目:

class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};

class B : public A
{
public:
	void fun(int i)
	{
		A::fun();
		cout << "func(int i)->" << i << endl;
	}
};


void Test() 
{
	B b;
	b.fun(10);
};

Q:两个fun()是什么关系?A.重载 B.隐藏 D.编译报错

最干扰的就是重载选项,但B中的fun和A中的fun不是构成重载,因为不是在同一作用域
 

所以答案是:此时编译会报错,同时函数还是构成隐藏

因为隐藏了父类的fun函数,此时子类需要传参,如果想访问,要加上作用域,即b.A::fun()

对于成员函数,只要父类和子类的同名成员函数,函数名相同就构成了隐藏

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

那么在子类,这几个默认成员函数是如何生成的呢?

总结了以下几点,大家可以先大致浏览一遍,等到看完后面的实例后,在返回来看这个总结~

1.子类的构造函数必须调用基类的构造函数初始化基类的那一部分成员

如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用

2. 子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化

3. 子类的operator=必须要调用父类的operator=完成父类的复制

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

5. 子类对象初始化先调用父类构造再调子类构造(初始化先父后子)

6. 子类对象析构清理先调用子类析构再调父类的析构(析构先子后父)

下面,一起来看一看各个默认成员函数吧~

对于构造函数

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:
protected:
	int _num; //学号
};


int main()
{
	Student s1;
	return 0;
}

通过运行结果我们可以知道:当我们没有写默认生成的子类的构造和析构时:子类调用父类的默认构造,且其他的与类和对象那里是一样的:内置类型进行值拷贝,自定义类型会调用自己的默认构造和析构(如果父类没有默认构造函数,此时就需要子类提供构造函数)

class Student : public Person
{
public:
	Student(const char* name, int num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	}
	//....

protected:
	int _num; //学号
};

注意:在初始化列表中必须得去调用父类的构造函数

下面的写法就是错误的:

Student(const char* name, int num)
        : _name(name)//错误写法:父类成员不能在子类通过初始化列表进行初始化
        , _num(num)
    {
        cout << "Student()" << endl;
    }

对于拷贝构造和赋值重载


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;
		Person::operator =(s);
		_num = s._num;

		return *this;
	}

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

protected:
	int _num; //学号
};


int main()
{
    Student s1("wyh", 18);  
	Student s2(s1);
    Student s3("csh", 20); 
	s1 = s3;
	return 0;
}

此时我们不写默认生成的子类的拷贝构造和赋值运算符重载:父类的部分调用父类的默认拷贝构造和重载赋值,内置类型并不处理

拷贝构造和赋值重载,如果子类存在深拷贝的问题,这个时候就需要我们自己实现拷贝构造和赋值重载来实现深拷贝了

对于析构函数

析构函数比较特殊,如果直接去调用父类的析构:

因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同,那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系

那如果我们指定作用域:

结果发现父类析构居然调用了两次,这说明了析构函数自己会调用,不需要我们显示的调用父类的析构函数

所以对于析构我们不需要自己写:

五、继承与友元

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


正确的写法:

六.继承与静态成员
 

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例,下面我们来看一下代码吧:

class Person
{
public:
	Person() { ++_count; }
public:
	static int _count;
};

int Person::_count = 0;
class Student :public Person
{
protected:
	int _stuNum;
};

int main()
{
	Person p;
	Student s;

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

	return 0;
}

七、菱形继承及菱形虚拟继承

讲到现在,我们所说的实际上都是单继承

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

接下来要讲的菱形继承则属于多继承,那么什么是多继承?

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

其中菱形继承是多继承的比较特殊的情况:

菱形继承的问题:

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

class Person
{
public:
	string _name;
};
class Student :public Person
{
protected:
	int _num;
};
class Teacher :public Person
{
protected:
	int _id;
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; 
};
int main()
{
	//二义性
	Assistant a;
	a._name;
    解决了二义性,但是代码冗余
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
	return 0;
}

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

需要注意的是,虚拟继承不要在其他地方去使用
 

class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}

至于虚拟继承解决数据冗余和二义性的原理,这里涉及到虚基表,本篇博客就不去探讨了,感兴趣的小伙伴可以自行搜索哦~

对于多继承,再说一点,为什么说C++语法复杂,其实多继承就是一个体现。有了多继承,就存

在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。

但实际中一般不建议设计出多继承

八、继承与组合

其实继承和组合都是一种复用,最大的区别是访问的方式有所不同

先举一个简单的例子,请看下面的代码:

再看下面这个继承和组合的代码:

   // Car和BMW Car构成is-a的关系 
   class Car{
   protected:
   string _colour = "黑色";
   string _num = "粤A10000"; 
   }; 
   class BMW : public Car{
   public:
   void Drive() {cout << "is-a" << endl;}
   };

   // Tire和Car构成has-a的关系(轮胎和车)
   class Tire{
   protected:
       string _brand = "Michelin";  
       size_t _size = 17;
   };
   class Car{
   protected:
   string _colour = "黑色";
   string _num = "粤A10000"; 
    Tire _t; 
   };  

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

为什么我们建议能用组合就不用继承? :

继承是白箱复用,组合是黑箱复用,组合的耦合度低,而继承的耦合度高,而高内聚低耦合,耦合度高意味着依赖关系大,所以当继承与组合都符合的情况下,比较建议使用组合.

相关推荐

  1. <span style='color:red;'>C</span>++<span style='color:red;'>继承</span>

    C++继承

    2024-07-10 18:38:03      43 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-10 18:38:03       5 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-10 18:38:03       5 阅读
  3. 在Django里面运行非项目文件

    2024-07-10 18:38:03       4 阅读
  4. Python语言-面向对象

    2024-07-10 18:38:03       6 阅读

热门阅读

  1. 四、Python日志系统之日志文件的备份和删除

    2024-07-10 18:38:03       9 阅读
  2. ZC2205-24V500mAUltralow-Quiescent-Current LDO

    2024-07-10 18:38:03       7 阅读
  3. 六、golang基础之面向对象特征

    2024-07-10 18:38:03       7 阅读
  4. 开源项目的机遇与挑战

    2024-07-10 18:38:03       11 阅读
  5. 关于go和rust语言的对比

    2024-07-10 18:38:03       8 阅读
  6. python入门基础知识·二

    2024-07-10 18:38:03       7 阅读
  7. 概率基础——矩阵正态分布matrix normal distribution

    2024-07-10 18:38:03       7 阅读
  8. 网络编程:常用网络测试工具

    2024-07-10 18:38:03       11 阅读
  9. 在 React 中使用自定义 Hooks 封装 Service 逻辑

    2024-07-10 18:38:03       10 阅读