继承和多态
一、继承
1.继承的定义和概念
继承我们可以理解为是一种手段,我们是不是可以从其他的地方继承一些东西来供自己使用,继承是面向对象程序设计使代码可以复用的一个很重要的手段。我们可以再保持原有类特性的基础上进行扩展,增添一些其他的功能,而这种新产生的类,就叫做派生类。
#include <iostream>
#include <string>
using namespace std;
class Person //基类
{
public:
void print()
{
cout << "print()" << endl;
}
protected:
int _age = 20;
private:
string _name = "xiaowang";
};
class Student : public Person //派生类
{
protected:
string _stutele;
};
int main()
{
Student s; //这里我们创建一个派生类,看用派生类能否调到基类的方法
s.print(); //也就是看看是否完成了继承
return 0;
}
我们用派生类调是不是也可以调的到啊,这就可以说明,Student类就继承了Person类中的成员,我们打开调试窗口来看一下。
接下来让我们看看继承的定义格式。
继承的方式一共有三种。
- public继承
- protected继承
- private继承
访问限定符也有三种。
- public访问
- protected访问
- private访问
我们来看一下共有继承(public继承)
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
void print()
{
cout << "print()" << endl;
}
protected:
int _age = 20;
private:
string _name = "xiaowang";
};
class Student : public Person
{
protected:
string _stutele;
};
int main()
{
Student s;
s.print();
s._age = 10;
s._name = "xiaoli";
return 0;
}
我们来对基类的(public成员)(protected成员)(private成员)来分别进行访问。看看是否都能够访问。
我们来看看下面的报错信息,对于public的成员在类外是能够进行正常的访问的,而对于(protected成员)(private成员)是不能进行访问的,我们再来看看在派生类中是否可以进行访问。
对于(protected成员)我们是不是可以在派生类中进行访问啊,而不能对(private成员)进行访问,说明我们继承基类的(private成员)是不能访问的,但是没有访问的办法了吗,我们可以通过调用基类的公有或保护成员来间接访问。
接下来我们看保护继承(protected)
可以看到我们将共有继承改成保护继承的时候,从基类继承的print 函数我们也无法在类型进行访问了,这是因为权限的问题,等会我们进行总结一下。(如果有两种权限的话,我们取权限小的),既然在类外无法访问,我们看看在派生类中是否可以访问。
我们发现在派生类中可以进行访问,但是(private成员)我们依然是在类内类外都是无法访问的,就是因为(private成员)的权限是最小的。我们取最小的那个权限。
现在我们可以总结一下上面的那张图了。
-
基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
-
基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
-
权限的大小是 public > protected > private
我们在平常中基本遇到的都是上面圈住的两种,其他的进行了解即可。
2.基类和派生类对象赋值转换
派生类其实就是在基类继承的基础上又添加了点自己的东西。这两者能否相互赋值进行转换呢 ?
派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个名字叫切片。怎么理解,可以想象派生类从基类继承过来一部分东西,而自己也有一些独有的一部分东西,切片就是把从基类继承过来的进行切割,然后再赋值给基类。
需要注意的是(基类对象不能赋值给派生类对象。)基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
这一个测试是将基类赋值给派生类(强制类型转换)。
大家有需要可以自己测试一下,代码如下。
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
int _age = 20;
string _name = "xiaowang";
};
class Student : public Person
{
public:
string _stutele;
};
int main(){
Student s;
//Person p;
//Person p = s;
Person* pp = &s;
//Person& rp = s;
//s = p;
//Person* pp;
Student* ps = (Student*)pp;
return 0;
}
3.继承中的作用域
在继承体系中基类和派生类都有独立的作用域。
在不同的作用域是不是可以定义同名的变量啊,那如果出现同名的函数呢 ,这个时候子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
来个例子看一下。
Student的_age和Person的_age构成隐藏关系。我们打印的第一个是打印的自己的,这个也可以看做是就近原则。当出现同名的成员的时候,我们要访问基类的成员,我们要在前面加上一个基类的类名和两个冒号 ::,可以使用 基类::基类成员显示访问。
using namespace std;
class Person
{
public:
int _age = 20;
string _name = "xiaowang";
};
class Student : public Person
{
public:
void Print()
{
cout << _age << endl;
cout << Person::_age << endl;
}
string _stutele;
int _age = 18;
};
int main(){
Student s;
s.Print();
return 0;
}
我们在看下面的情况。
using namespace std;
class Person
{
public:
void Print()
{
cout << "Print()" << endl;
}
int _age = 20;
string _name = "xiaowang";
};
class Student : public Person
{
public:
void Print(int i)
{
cout << "Print(int i)" << endl;
}
string _stutele;
};
int main(){
Student s;
//s.Print(); //报错
s.Print(1);
return 0;
}
4.派生类的默认成员函数
默认的意思是不是我们不写,编译器会自动帮我们生成。是的。
总结一下:派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
总结一下: 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
总结一下:派生类的operator=必须要调用基类的operator=完成基类的复制。
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。这样也比较合理。
大家如果想测试一下的话也可以,代码如下:
using namespace std;
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, 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;
};
int main(){
Student s1("xiaoli", 18);
//Student s2(s1);
Student s3("xiaowang",20);
s1 = s3;
return 0;
}
5.继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
#include <iostream>
#include <string>
using namespace std;
class Student;
class Person
{
public:
friend void Print(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person
{
protected:
int _num;
};
void Print(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._num << endl;
}
int main(){
Person p;
Student s;
Print(p, s);
return 0;
}
再来说一下静态成员。
using namespace std;
class Student;
class Person
{
public:
static int _count;
Person()
{
_count++;
}
protected:
string _name;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _num;
};
int main(){
Student s1;
Student s2;
Student s3;
cout << Person::_count << endl;
Student::_count = 0;
cout << Person::_count << endl;
return 0;
}
总结一下:基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
6菱形继承与虚继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
这种就是单继承。
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承会引发一些问题。(有数据冗余和二义性)
使用菱形继承,此时如果想访问_name这个成员就会产生歧义:你要访问的是哪个_name?
编译器会报访问不明确,不知道访问哪一个。
我们可以用显式指定目标来解决该问题,但是依旧无法解决数据冗余的问题。
又引入了下面的概念来解决这个问题。(virtual)
using namespace std;
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
cout << sizeof(d) << endl;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
using namespace std;
class A
{
public:
int _a;
};
class B :virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
cout << sizeof(d) << endl;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
加了(virtual)居然多了4个字节。这是怎么回事 ?我们来看看。
先来看看不加(virtual)
再来看看加(virtual)(虚继承)
这里我们就可以看到,加了(virtual),两个_a共用了,而且还多出来了两个指针,这两个指针又是什么东西呢 ?我们跳转一下去看看。
这两个箭头所指的东西是什么呢 ?难道说是随机的吗 ?(这里是16进制数),14转为10进制是20,0c转为10进制是12,在看看地址的变化,是不是就是偏移量啊。上面的两个指针分别为B和C的指针,这个指针叫做虚基表指针,指向了两个虚基表,虚基表中存的偏移量。通过偏移量可以找到下面的A。
这样就解决了数据冗余和二义性的问题。
二、多态
1. 多态的概念
多台也对应多种状态。当不同的对象去干一件事会产生不同的状态。
很形象的例子就是买票,都知道学生,军人购票都是有优惠的。而普通的人买票就是没有优惠了。这是不是就是一种多态。
2.多态的定义和实现
using namespace std;
class Person
{
public:
void BuyTicket()
{
cout << "全价" << endl;
}
};
class Student : public Person
{
public:
void BuyTicket()
{
cout << "半价" << endl;
}
};
void test(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
test(p);
Student(s);
test(s);
return 0;
}
这是不是没有达到我们的要求啊,我们想要实现多态,但是实现多态有两个条件。
必须通过基类的指针或者引用调用虚函数
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
我们现在已经完成了通过基类的指针或引用调用函数,但什么是虚函数呢 ?重写又是什么呢 ?
又是(virtual)但这个和继承里面的虽然是同一个关键字,但是没有关系,作用也不一样。这里被virtual修饰的类成员函数称为虚函数。
即
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "半价" << endl;
}
};
void test(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
test(p);
Student(s);
test(s);
return 0;
}
此时是不是达到我们的目的了。
虚函数的重写:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。(有两个例外,1.协变,2.析构函数的重写)
简单举一个协变的例子。
代码如下:
using namespace std;
class A
{};
class B : public A
{};
class Person
{
public:
virtual A* BuyTicket()
{
cout << "全价" << endl;
return new A;
}
};
class Student : public Person
{
public:
virtual B* BuyTicket()
{
cout << "半价" << endl;
return new B;
}
};
void test(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
test(p);
Student(s);
test(s);
return 0;
}
析构函数的重写:
提示一下:编译器会将析构函数的名称统一处理成destructor。所以函数名是相同的。
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "全价" << endl;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "半价" << endl;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
void test(Person& p)
{
p.BuyTicket();
}
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
我们可以把(virtual)去掉看会造成什么样的结果。
去掉之后是不是就不会构成重写了,也就不构成多态,这时也不调用Student 的析构函数,假如说派生类里面有很多资源没有被释放呢 ?是不是就造成了内存泄漏啊。这种方式是不是很危险啊。所以我们必须要加上(virtual)。(这个时候会构成隐藏,在继承里面说过)
这里说两个关键字。
override和final。
final:修饰虚函数,表示该虚函数不能再被重写。
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
3.抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类。
需要注意的是,抽象类不能实例化出对象。
如果一个派生类继承了一个抽象类,则必须对纯虚函数进行重写,否则也无法实例化对象。
4.多态的原理
using namespace std;
class A
{
public:
virtual void test1()
{
cout << "test1()" << endl;
}
private:
int _a = 1;
};
int main()
{
A a;
cout << sizeof(a) << endl;
return 0;
}
这里会打印多少呢 ?
我们来看看这8字节都是什么 ?
除了我们已知的_a是4个字节,还是4个字节是不是这个叫_vfptr的,这个东西叫做虚函数表指针(虚表指针),一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
当然我们现在看的是基类,那么派生类呢 ?
using namespace std;
class A
{
public:
virtual void test1()
{
cout << "A::test1()" << endl;
}
virtual void test2()
{
cout << "A::test2()" << endl;
}
void test3()
{
cout << "A::test3()" << endl;
}
private:
int _a = 1;
};
class B : public A
{
public:
virtual void test1()
{
cout << "B::test1()" << endl;
}
private:
int _b = 1;
};
int main()
{
A a;
//cout << sizeof(a) << endl;
B b;
return 0;
}
这里我们可以看到test1的地址是不是不一样啊,test2是继承的,地址是一样的。
我们总结一下:
派生类对象b中也有一个虚表指针,派生类会继承基类的虚表。
基类a对象和派生类b对象虚表是不一样的,这里我们发现test1完成了重写,所以b的虚表中存的是重写的B::test1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。
另外test2继承下来后是虚函数,所以放进了虚表,test3也继承下来了,但是不是虚函数,所以不会放进虚表。
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
观察下图的红色箭头我们看到,p是指向a对象时,p->BuyTicket在a的虚表中找到虚函数是A::BuyTicket。找到B也是一样的。 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
5.虚函数表
单继承中的虚函数表
using namespace std;
class A
{
public:
virtual void test1()
{
cout << "A::test1()" << endl;
}
virtual void test2()
{
cout << "A::test2()" << endl;
}
private:
int _a = 1;
};
class B : public A
{
public:
virtual void test1()
{
cout << "B::test1()" << endl;
}
virtual void test3()
{
cout << "B::test3()" << endl;
}
virtual void test4()
{
cout << "B::test4()" << endl;
}
private:
int _b = 1;
};
int main()
{
A a;
B b;
return 0;
}
问题来了,为什么只有两个虚函数。(派生类虚函数去哪里了 ?)
我们来打开内存来看一下。
对比一下前面的地址看看是不是一样的。是一样的。这里我们看不到那两个虚函数可以认为是编译器做了手脚。我们在内存中还是可以看到的。
多继承中的虚函数表
using namespace std;
class A
{
public:
virtual void test1()
{
cout << "A::test1()" << endl;
}
virtual void test2()
{
cout << "A::test2()" << endl;
}
private:
int _a = 1;
};
class B
{
public:
virtual void test1()
{
cout << "B::test1()" << endl;
}
virtual void test2()
{
cout << "B::test2()" << endl;
}
private:
int _b = 2;
};
class C : public A,public B
{
public:
virtual void test1()
{
cout << "C::test1()" << endl;
}
virtual void test3()
{
cout << "C::test3()" << endl;
}
private:
int _c = 3;
};
int main()
{
C c;
return 0;
}
我们可以看到派生类中同时继承了两个基类的虚表。说明多继承中派生类会同时继承所有基类的虚表。
我们上面是不是还写了一个test3,那test3存在那个虚表里面了呢 ?还是两个虚表各存一个 ?
A的虚表
B的虚表
我们是不是可以得出下面的结论:
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。
6.重载、隐藏和重写的区别
先来看看重载(函数重载)
- 两个函数位于同一个作用域
- 函数名相同,参数不同(个数不同,类型不同,位置不同)
隐藏(继承)
- 两个函数位于不同的作用域(基类和派生类)
- 函数名相同
- 函数不能构成重写
重写(多态)
- 两个函数位于不同的作用域(基类和派生类)
- 函数名,参数和返回值都必须相同(斜边除外)
- 两个函数都要是虚函数
本期到此结束,谢谢大家的观看。
标签:cout,继承,基类,多态,C++,派生类,public,函数 From: https://blog.csdn.net/2402_84602321/article/details/142657866