Bootstrap

c++:继承(超详解)

目录

一:什么是继承

二:继承的格式

继承的总结:

二:子类和父类(基类和派生类)

1.子类和父类的相互赋值:

2.同名的成员变量

3.同名成员函数

三:子类中默认的成员函数

1.构造函数

2.析构函数

3.拷贝构造

4.赋值运算符重载

 四:单继承和多继承

单继承:

 多继承:

菱形继承

解决方法一:

解决方法二:

单继承和多继承的总结:


一:什么是继承

定义:

继承(inheritance)机制是面向对象程序设计中使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生的新类,称派生类(或子类),被继承的类称基类(或父类)。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。之前接触的复用都是函数复用,继承是类设计层次的复用。

好吧,光看也看不出个啥,还是直接上代码吧

代码:

class human {//定义了一个父类,名字叫human
public:
	string name = "小明";//父类里面定义了一个string类型的和一个int类型
	int age = 18;
};
class student:public human {//定义了一个以public方式继承父类的子类student
public:
	int schoolnum = 666;//在父类的name和age的基础上增加了一个schoolnum
	void print()
	{
		cout << name << endl << age << endl << schoolnum << endl;//输出
	}
};
int main()
{
	student st;
	st.print();
	return 0;
}

结果如下:

 好吧是不是还是看不懂,那让我们把这个代码分成两半

第一部分:

class human {
public:
	string name = "小明";
	int age = 18;
};

第二部分:

class student:public human {
public:
	int schoolnum = 666;
	void print()
	{
		cout << name << endl << age << endl << schoolnum << endl;
	}
};

这样我们发现,其实第一部分的代码就是我们平时使用的class。

对于第二部分的解读我们先举一个例子

如果我们要设计一个学校系统,那么对于学生,老师.....一系列的人,我们都是需要将姓名和年龄等必要信息录入,但是单独针对到某一类人,比如学生,除了必要信息外,还单独有一个学号。比如老师除了必要信息外,还有一个单独的职工号。

 所以为了偷懒提高效率,我们这里就可以把姓名和年龄封装到一个class类里面,也就是我们第一部分的代码,然后再新创一个类,继承原有姓名和年龄类的基础上,再新增学号/职工号,也就是我们第二类。

所以说怎么继承呢?

二:继承的格式

class 新类的名字:继承方式 继承类的名字{};

以我刚才的例子为例

class student:public human{};
//student是新类的名字,public是继承方式,human是要继承的类
//意思就是说我定义了一个名叫 student的类 以public的方式 来继承你human

我们这里对于student和human就有两种叫法。

一种是教科书里面的基类(human)和派生类(student)。

我本人喜欢第二种父类(human)和子类(student)。毕竟感觉就像继承家产一样。

三:继承后的子类成员访问权限

这里我先丢一张图在,这是教科书里面老师铁定要求背诵的

在这里插入图片描述

 我这里分享一个很巧妙的方式

我们假设

public>protectd>private

我们取x和y中,两个较小的。

 最后一个private就都不可。这样我们就很轻松的记忆了下来

继承的总结:

1.基类private成员无论以什么方式继承到派生类中都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2.基类private成员在派生类中不能被访问,如果基类成员不想在派生类外直接被访问,但需要在派生类中访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3.基类的私有成员在子类都是不可见;基类的其他成员在子类的访问方式就是访问限定符和继承方式中权限更小的那个(权限排序:public>protected>private)。
4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,但最好显式地写出继承方式。

二:子类和父类(基类和派生类)

1.子类和父类的相互赋值:

代码:

class human {//父类
public:
	string name = "小明";
	int age = 18;
};
class student:public human {//子类
public:
	int schoolnum = 666;
};
int main()
{
	student st;
	human hm;
	hm = st;//将子类赋值给父类
	st = hm;//将父类赋值给子类
	return 0;
}

就会出现这样的结果:

 在这里我们引入一个叫做切片原则的东西

 因为父类中没有schoolnum,所以父类接收子类传过来的name和age之后,多余的schoolnum就不管了。但是如果父类传给子类,少传一个,所以会报错。

同时我们给出三种赋值方式

三种方式的赋值:

一:=符号

student st;//子类
	human hm;//父类
	hm = st;

 

二:引用

student st;//子类
	human& hm=st;父类

三:指针

student st;//子类
	human* hm=&st;//父类

2.同名的成员变量

在有些时候,父类和子类中出现了同一个成员变量,如下name

class human {
public:
	string name = "小明";
};
class student:public human {
public:
	string name = "小红";
	void print()
	{
		cout << name << endl;
	}
};
int main()
{
	student st;
	st.print();
	return 0;
}

这个时候编译器是以子类为优先

结果如下

但是如果我们就是想要访问父类的该成员变量,就需要加上修饰

void print()
	{
		cout << human::name << endl;
	}

 其实也很好理解,默认子类,父类就修饰限定

3.同名成员函数

如下,同样一个函数print在父类和子类中都存在

class human {
public:
	string name = "小明";
	void print()
	{
		cout << name << endl;
	}
};
class student :public human {
public:
	string name = "小红";
	void print()
	{
		cout << name << endl;
	}
};
int main()
{
	student st;
	st.print();
	return 0;
}

这就构成了隐藏。(函数重载是在同一个作用域,这里父类和子类是两个作用域)

函数的隐藏,编译器会默认调用子类中匹配的函数,如果没有编译器就会报错

上面的结果如下

虽然成员函数的隐藏,只需要函数名相同就构成隐藏,对参数列表没有要求。

 但是我们修改一下子类的函数

void print(int x)//我们对子类的print函数加入一个参数
	{
		cout << name << endl;
	}

 这是因为编译器默认调用子类中print函数,但是子类中唯一的print函数有一个默认的参数,所以编译器无法找到匹配的print函数,所以就会报错。

三:子类中默认的成员函数

1.构造函数

编译器会默认先调用父类的构造函数,再调用子类的构造函数,如下

class human {
public:
	human(string name = "小明")//先调用:父类默认构造调用一个print打印name
		:_name(name)
	{
		cout << name << endl;
	}
protected:
	string _name;
};


class student :public human {//后调用:子类默认构造调用一个print打印name和age
public:
	student(string name,int age)
		:_age(age)
	{
		cout << name << endl<<age<<endl;
	}
protected:
	int _age;
};


int main()
{
	student st("小红", 18);
	return 0;
}

 结果如下

 可以看到,编译器先调用了父类的,打印出了小明,然后再次调用了子类的打印出了小红和age。

所以说请务必保证父类构造有效,假如父类失效

human(string name)//你这里不传值,那么就不能完成初始化,相当于父类失效
		:_name(name)
	{
		cout << name << endl;
	}

那么就必须在子类中给父类构造赋值

student(string name,int age)
		:_age(age)
		, human(name)//新增,子类以自己的name给父类的析构中的name赋值,age和name的顺序随意变动

结果如下

 实在不行就把父类的构造删了,反正编译器也默认会生成的

2.析构函数

析构函数和构造函数相反,编译器默认先调用子类的析构函数,再调用父类的析构函数。

验证如下:

我们在原有的代码上,加入两个析构函数

class human {
public:
	human(string name = "小明")
		:_name(name)
	{}
	~human()
	{
		cout << "我是父类" << endl;
	}
protected:
	string _name;
};
class student :public human {
public:
	student(string name,int a = 20)
		:age(a)
	{}
	~student()
		
	{
		cout <<"我是子类"<< endl;
	}
protected:
	int age;
};
int main()
{
	student st("小明", 18);
	return 0;
}

结果如下:

 所以说

千万不要在子类中调用父类的析构

千万不要在子类中调用父类的析构

千万不要在子类中调用父类的析构

如果是指针类型,那么同一块区域被析构两次就会造成野指针的问题。

3.拷贝构造

子类中调用父类的拷贝构造时,直接传入子类对象即可,父类的拷贝构造会通过“切片”拿到父类的那一部分。

class human {
public:
	human(string name="小明")
		:_name(name)
	{
		cout << name << endl;
	}
protected:
	string _name;
};
class student:public human {
public:
	student(string name, int age)
		:_age(age)
	{
		cout << name << endl << age << endl;
	}
	student(student& s)
		:human(s)//直接将st传过来通过切片拿到父类中的值
		,_age(s._age)//拿除了父类之外的值
	{
		cout << s._age << endl<<s._name<<endl;
	}
protected:
	int _age;
};
int main()
{
	student st("小红",18);
	student st2(st);
	return 0;
}

结果如下:

4.赋值运算符重载

子类的operator=必须要显式调用父类的operator=完成父类的赋值。

因为子类和父类的运算符,编译器默认给与了同一个名字,所以构成了隐藏,所以每次调用=这个赋值运算符都会一直调用子类,会造成循环,所以这里的赋值要直接修饰限定父类

class human {
public:
	human(string name = "小明")
		:_name(name)
	{
	}
	human& operator=(const human& p)
	{
		if (this != &p)
		{
			cout << "调用父类" << endl;
			_name = p._name;
		}
		return *this;
	}
protected:
	string _name;
};
class student :public human {
public:
	student(string name, int age)
		:_age(age)
	{
	}
	student(student& s)
		:human(s)
		, _age(s._age)
	{
	}
	student& operator=(const student& s)
	{
		if (this != &s)
		{
			cout << "调用了子类" << endl;
			human::operator=(s);//必须调用父类运算符
			_age = s._age;
			_name = s._name;
		}
		return *this;
	}
protected:
	int _age;
};
int main()
{
	student st("小红", 18);
	student st2(st);
	student st3("小刚", 16);
	st = st3;
	return 0;
}

 结果如下:

 四:单继承和多继承

单继承:

一个子类只有一个直接父类的继承关系。

 多继承:

一个子类有两个或以上直接父类的继承关系。

 由以上两点,我们就会发现一个很蛋疼厉害的继承,

菱形继承

好,我们先上一段经典菱形继承代码 

这是个代码是有问题的

class A {
public:
	string name;
};
class B :public A {
public:
	int age;
};
class C :public A {
public:
	string sex;
};
class D :public B, public C {
public:
	int id;
};
int main()
{
	D student;
	student.name = "小明";
	student.age = 18;
	student.sex = "男";
	student.id = 666;
	return 0;
}

啪的一下,很快啊,报错就出来了 

 因为这里的name,同时存在B和C中,所以D不知道继承B的name还是C中的name

这也就是引出了代码冗余和二义性的问题。

所以我们有两种解决方法

解决方法一:

加修饰限定

student.B::name = "小明";

这里我们指定继承B中的name,就不会冲突了

解决方法二:

虚继承:在继承方式前加上virtual。

class B :virtual  public A {
public:
	int age;
};
class C :virtual public A {
public:
	string sex;
};

单继承和多继承的总结:

别用菱形继承就完了

多继承是C++复杂的一个体现。有了多继承,就存在菱形继承,为了解决菱形继承,又出现了菱形虚拟继承,其底层实现又很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;