C++三大特性之继承,由浅入深全面讲解,由基础语法到深度刨析。

C++三大特性之继承,由浅入深全面讲解,由基础语法到深度刨析。,第1张

1.什么是继承及继承的语法 1.1什么是继承?

记住一句话,在C++中,继承是一种使用子类对代码进行复用的手段,在写对实际设计的时候可以将数据抽象出来,使代码更具层次性和结构性。


1.2继承的语法 1.2.1如何定义继承

我们以定义一个父类People,一个子类为Student,其中父类也叫做基类,子类又称派生类。


这里先不要纠结继承方式,下面我会给大家详细的介绍继承方式,基本语法为:

有了基本语法的支持,我们看代码,如下:

class People
{
protected:
	int _age;
	string _name;
};
class Student :public People
{
protected:
	int _id;
};
1.2.2继承关系和访问限定符

在我的C++类的内容讲解中我们可以了解到类的访问限定符有如下图所示的三个,这里我们不再仔细介绍,不熟悉的同学可以查看我以前的文章。




了解三个继承方式,如下图:

有了继承方式和访问限定符,那么我们在继承的过程中,当这两个关键字相撞时,就会产生一种特性,如下图:

我相信上面的图片大多数同学已经很熟悉了,但是不与其中的意义还不是深度的了解,下面让我来细细的刨析:
1、基类private成员在派生类中无论以什么方式继承都是不可见的。


注意:这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。


可以理解为他在子类当中,但是子类对他没有使用权,因为子类看不见他。



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


可以看出保护成员限定符是因继承才出现的。



小tips:在父类中protected和private的特性几乎没有区别,这两个限定符的区别是体现在派生类中的。



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



4、. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。



5、如何快速识别一个口诀:两两相遇选权限最小。


实例演示三种继承关系下基类成员的各类型成员访问关系的变化

class Person
{
public :
 void Print ()
 {
 cout<<_name <<endl;
 }
protected :
 string _name ; // 姓名
private :
 int _age ; // 年龄
};
class Student : protected Person{};
//Print仅内类可访问
//_name仅内类可访问
//_age不可见
class Student : private Person{};
//Print不可见
//_name不可见
//_age不可见
class Student : public Person{};
//Print函数类内外都可访问到
//_name仅内类可访问
//_age不可见
2.基类和派生类对象赋值转换
  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。


    这里有个形象的说法叫切片或者切割。


    寓意把派生类中父类那部分切来赋值过去。


    注意:这里的赋值顺序一定不能写反了,基类对象不能赋值给派生类对象。


  • 基类的指针可以通过强制类型转换赋值给派生类的指针。


    但是必须是基类的指针是指向派生类对象时才是安全的,其余的都有可能报错。


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


    两作用域互相独立,互不干扰。


  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。


    (在子类成员函数中,可以使用 基类::基类成员 显示访问)

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


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


  5. 来看下列代码来加深对上述知识点的刨析
// Student的_num和People的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class People
{
protected :
 string _name = "小李子"; // 姓名
 int _num = 111; // 身份z号
};
class Student : public People
{
public:
 void Print()
 {
 cout<<" 姓名:"<<_name<< endl;
 cout<<" 身份z号:"<<People::_num<< endl;
 cout<<" 学号:"<<_num<<endl;
 }
protected:
 int _num = 999; // 学号
};
void Test()
{
 Student s1;
 s1.Print();
};
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。


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); };

4.派生类的默认成员函数 4.1 派生类的重点的四个默认成员函数,我们不写,编译器会默认生成的会干些什么事情呢!

我们不写默认生成的派生的构造和析构?
a、父类继承下来得 (调用父类默认构造和析构处理) b、自己的(内置类型和自定义类型成员)(跟普通类一样)
我们不写默认生成的拷贝构造和operator=?
a、父类继承下来得 (调用父类拷贝构造和operator=) b、自己的(内置类型和自定义类型成员)(跟普通类一样)
总结:原则,继承下来调用父类处理,自己的按普通类基本规则,普通类的基本规则不懂查看我的C++类的基本知识文章了解。


4.2 什么情况下必须自己写?

1、父类没有默认构造,需要我们自己显示写构造
2、如果子类有资源需要释放,就需要自己显示写析构
3、如果子类存在浅拷贝问题,就需要自己实现拷贝构造和赋值解决浅拷贝问题。


4.2 自己写怎么写?

父类成员调用父类的对应构造、拷贝构造、operator=和析构处理,自己成员按普通类处理。


class People
{
public:
	People(const char* name)
		:_name(name)
	{}
	People(const People& p)
		:_name(p._name)
	{}
	People& operator=(const People& p)
	{
		if (this != &p)
			_name = p._name;
		return *this;
	}
	~People()
	{
		delete[] _ptr;
	}
protected:
	string _name;
	int* _ptr = new int[10];
};
class Student :public People
{
public:
	Student(const char* name,int num=1)
		:People(name)
		,_num(num)
	{}
	Student(const Student& s)
		:People(s)
		,_num(s._num)
	{}
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			People::operator=(s);
			_num = s._num;
		}
		return *this;
	}
	~Student()
	{
		delete[] _p;
	}
protected:
	int _num = 1;
	string _s = "nice to meet you";
	int* _p = new int[10];
};

注意:这里大家可能会有疑问,为什么没有对父类的资源进行主动的释放?

  • 这里析构函数名字会被统一处理成destructor()。


    子类的析构函数跟父类的析构函数就构成隐藏。


  • 子类析构函数结束时,会自动调用父类的析构函数,这样才能保证先析构子类成员,再析构父类成员。


  • 所以我们自己实现子类析构函数时,不需要显示调用父类析构函数。


5.继承与友元

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


一段代码加深理解:

lass 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);
}

虽然子类继承了父类,但是Display是友员函数,所以在子类中无法使用Display。


6. 继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。


无论派生出多少个子类,都只有一个static成员实例 。


7.进阶:复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承,如下图所示的关系

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承,如下图所示的关系

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


如下图所示的关系:

通过菱形继承关系图,可以看出菱形继承的问题会有些问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。


在doctor的对象中Person成员会有两份。



class Person
{
public :
 string _name ; // 姓名
};
class Student : public Person
{
protected :
 int _num ; //学号
};
class Teacher : public Person
{
protected :
 int _id ; // 职工编号
};
class doctor: public Student, public Teacher
{
protected :
 string _majorCourse ; // 主修课程
};
void Test ()
{
 // 这样会有二义性无法明确知道访问的是哪一个
  doctor a ;
 a._name = "peter";
 
 // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
 a.Student::_name = "xxx";
 a.Teacher::_name = "yyy";
}

虽然可以解决二义性问题,但是数据冗余问题无法解决,所以我们可以采用虚拟继承来解决菱形继承的二义性和数据冗余的问题。


如上面的继承关系,在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 doctor : public Student, public Teacher
{
protected :
 string _majorCourse ; // 主修课程
};
void Test ()
{
 doctor a ;
  a._name = "peter";
}

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/584962.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-12
下一篇 2022-04-12

发表评论

登录后才能评论

评论列表(0条)

保存