继承与多态
Qingyh Lv3

继承

继承的本质:

  • 实现代码的复用
  • 在基类中提供统一的虚函数接口,可以让派生类进行重写,就可以使用多态了。(具体看本文多态章节)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class A
{
public:
A(int a1 = 1, int a2 = 2, int a3 = 3) :a1_(a1), a2_(a2), a3_(a3) {}
~A(){}
int a1_;
protected:
int a2_;
private:
int a3_;

};

class B:public A
{
public:
B(int b1 = 10, int b2 = 20, int b3 = 30) :b1_(b1), b2_(b2), b3_(b3) {}
~B() {}
int b1_;
protected:
int b2_;
private:
int b3_;

};

B类继承了A中的成员变量和方法,如下图所示。并且由于在派生的时候会在变量前面加入基的限定符,如A:a1_,所以即使A类和B类的成员变量和方法同名,也不会冲突。
image

1
2
3
B b;
b.show();
//当调用show方法时,优先调用B类自己定义的成员变量和方法

继承与访问限定比较

public:可以在类的内部和外部访问。
protected:只能在类的内部以及派生类(子类)中访问。
private:只能在类的内部访问。
Tips:在c++中,默认情况下,类的成员(属性和方法)的访问权限是private。
class(默认private)、struct(默认public)。
image
访问权限顺序:public>protected>private
继承方式:public、protected、private

重点:
基类到派生类的访问权限是不能大于基类的访问权限的,比如说protected继承方式,那么基类中的public成员变量只能变成protected方式

派生类继承访问权限分析

这里以公有继承为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
using namespace std;
class A
{
public:
A(int a1 = 1, int a2 = 2, int a3 = 3) :a1_(a1), a2_(a2), a3_(a3) {}
~A(){}

int a1_;
protected:
int a2_;
private:
int a3_;

};

class B:public A
{
public:
B(int b1 = 10, int b2 = 20, int b3 = 30) :b1_(b1), b2_(b2), b3_(b3) {}
~B() {}
//
void show()
{
cout << "基类公有变量a1_:"<<a1_;//可以访问
cout << "基类保护变量a2_:" << a2_;//可以访问
cout << "基类私有变量a3_:" << a3_; //不能访问
}

int b1_;
protected:
int b2_;
private:
int b3_;

};
int main()
{
//外部访问基类变量权限限定
B b;
cout << "基类公有变量a1_:" << b.a1_; //可以访问
cout << "基类保护变量a2_:" << b.a2_; //不能访问
cout << "基类保护变量a3_:" << b.a3_; //不能访问
return 0;
}

依次改变B的继承方式,总结可以得到下表
image
注意:派生类从基类继承private的成员,但是派生类无法直接访问。

class定义派生类,默认继承方式是private继承;
struct定义派生类,默认继承方式是public继承。

派生类和基类关系

派生类的构造顺序

  • 派生类的构造和析构函数,负责初始化和清理派生类部分
  • 派生类从基类继承来的成员,由基类的构造和析构函数负责初始化和清理工作

下面是代码验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Base
{
public:
Base(int data) :dataa_(data) { cout << "Base()" << endl; }
~Base() { cout << "~Base()" << endl; }
protected:
int dataa_;
};

class Derive :public Base
{
public:
Derive(int data)
:datab_(data), Base(data)
{
cout << "Derive()" << endl;
}

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

private:
int datab_;
};
int main()
{
Derive b(200);
return 0;
}

执行结果为
image
派生类的完整生命流程如下
1、派生类调用基类的构造函数,初始化从基类继承的成员
2、调用派生类自身的构造函数,初始化派生类成员
3、调用派生类的析构函数,析构派生类成员
4、调用基类析构函数,释放派生类中从基类继承来的成员

基类对象(指针)派生类对象(指针)的转换

一般来说,派生类相对基类来说是占用更大的内存空间的,基于这一点理解以下结论。

  • 将派生类对象赋值给基类对象,可以,赋值后的基类对象只能返回基类的成员
  • 将基类对象赋值给派生类对象,错误,(因为基类可能没有包含派生类独有的那部分数据)
  • 将派生类对象指针(引用)给基类对象指针(引用),可以,(基类指针是指向派生类对象的,但是由于基类指针的限定,只能访问派生类中的基类成员。)
  • 将基类对象指针(引用)给派生类对象指针(引用),错误

image

总结:在继承结构的转换,一般只支持从派生类到基类的转换

重载和隐藏

重载关系:一组函数要重载,必须要处在同一个作用域当中;并且函数名字相同,参数列表不同
隐藏关系:在继承结构当中,派生类的同名成员,把基类的同名成员给隐藏调用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base
{
public:
void show() { cout << "Base::show()" << endl; }
void show(int) { cout << "Base::show(int)" << endl; }

};

class Derive :public Base
{

};
int main()
{
Derive b;
b.show();
b.show(10);
return 0;
}

代码的执行结果为,调用的是基类的方法
image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base
{
public:
void show() { cout << "Base::show()" << endl; }
void show(int) { cout << "Base::show(int)" << endl; }

};

class Derive :public Base
{
public:
void show() { cout << "Derive ::show()" << endl; }
};
int main()
{
Derive b;
b.show();
//因为派生类定义了show方法,所有基类的同名方法show()和show(int)被隐藏
//只能通过b.Base::show()、b.Base::show(10)调用基类的show方法
//b.show(10);
return 0;
}

image

因为重载是要处于同一作用域内才起作用,而基类和派生类处于不同作用域,当派生类定义了与基类同名的函数,基类中的与其同名及其重载函数就被隐藏了。

虚函数

①如果一个类里面定义了虚函数,那么在编译阶段,编译器会给这个类产生一个唯一的vftable,即虚函数表,其中主要存储的就是RTTI(Run-Time Type Information,运行时类型识别)指针和虚函数的地址。
当程序运行时,每一张虚函数表都会加载到内存的.rodata区(只读数据区),不可更改

1
2
3
4
5
6
7
8
9
10
class Base
{
public:
Base(int data=10):dataa_(data){}
//虚函数
virtual void show() { cout << "Base::show()" << endl; }
virtual void show(int) { cout << "Base::show(int)" << endl; }
protected:
int dataa_;
};

如Base类中定义了两个虚函数,其生成的虚函数表如下,使用命令查看Base类的内存分布情况
c++打印类的内存布局
vs studio中打印信息

1
2
//进入到包含test.cpp的文件夹下,使用这个命令单独查看Base类的内存分布
cl test.cpp /d1reportSingleClassLayoutBase

打印的结果为:
image
注意:由于这里打印Base类的内存布局,发现存在{vfptr}这个字段,以为类中也存在这个虚函数指针。其实不是的,
用{}强调这是隐式生成的成员,不是用户显示声明的。

类(Class)会生成一份虚函数表 vtable(静态只读)
对象(Object)里才有虚函数表指针 vptr(vfptr)指向这张表

image
这里的偏移量指的是vfptr指针在内存的偏移量为零,即基类内存中的第一个成员变量为vfptr。
②如果使用带有虚函数的类定义一个对象,其大小为成员变量的大小+虚函数指针的大小(一般为8字节),该虚函数指针指向虚函数表。(该Base类因为定义了虚函数,所以有一个用户不可见的vfptr指针)。
一个类型定义的多个对象,他们的vfptr都是指向同一个虚函数表。

1
2
3
4
Base b1;
Base b2;
Base b3;
//这三个对象的vfptr都是指向Base类的虚函数表

③一个类里面的虚函数的个数,不会影响对象的内存大小(不是说多个虚函数,就需要多个vfptr,vfptr始终指向的是虚函数表);虚函数的个数影响的是虚函数表的大小

image

基类是虚函数对派生类的影响

如果派生类中的方法,和从基类中继承的某个方法,满足:

  • 返回值、函数名、参数列表都相同
  • 基类的方法是虚函数

则派生类的这个方法,会被处理成虚函数

1
2
3
4
5
6
7
8
class Derive :public Base
{
public:
Derive(int data=20):Base(data),datab_(data){}
void show() { cout << "Derive ::show()" << endl; }
protected:
int datab_;
};

这里派生类的show()方法满足上述条件,发生覆盖(在虚函数中,派生类的Derive::show()覆盖了基类中的Base::show())。
image
image

基类和派生类的内存变化
image

静态绑定与动态绑定

指针调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Base
{
public:
//普通的show函数,是静态绑定
//void show() { cout << "Base::show()" << endl; }
//virtual show函数,是动态绑定
virtual void show() { cout << "Base::show()" << endl; }
private:
int dataa_;
};

class Derive :public Base
{
public:
void show() { cout << "Derive::show()" << endl; }
private :
int datab_;
};
int main()
{

Derive d;
Base* pb = &d;

pb->show();

cout << "基类Base大小:" << sizeof(Base) << endl;
cout << "派生类Derive大小:" << sizeof(Derive) << endl;
cout << typeid(pb).name() << endl;
cout << typeid(*pb).name() << endl;

return 0;
}

pb是基类指针,但其指向的是派生类对象,由于基类指针的限定,当调用pb->show()方法时,就自动去派生类中的基类部分去找show方法的实现。
1.如果Base::show()是普通方法,则进行静态绑定(call Base::show())

2.如果Base::show()是虚函数,则进行动态绑定,反汇编代码如下
image

具体步骤为:

  • 根据pb指向的对象的前四个字节获取虚函数指针vfptr的值(其指向的对象是一个派生类Derive对象)
  • 根据vfptr获取其指向的虚函数表(这里的虚函数表为,Derive的虚函数表)
  • 根据虚函数表得到其对应的虚函数(这里的&Derive::show()虚函数重写了&Base::show(),所以最后调用的是Derive::show()方法)

Base::show()方法是否是virtual的输出比较
image

  • 对于普通方法,Base类中有一个int类型成员变量,占4字节;Derive继承基类的成员变量+自身定义的int类型变量,共8字节。
  • 对于Base::show()方法是虚函数,则有一个虚函数指针vfptr,8字节,则基类大小为4+8=12(我这里是64位系统,8字节对齐,变成了16个字节大小);派生类在基类的16字节基础上加上本身的int成员变量4字节,再次内存对齐,共24字节。

对于pb指向类型的理解

1
2
3
4
5
6
7
8
9
	Derive d;
Base* pb = &d;

pb->show();
cout << typeid(*pb).name() << endl;
/*对于这里pb指向的类型:取决于Base有没有虚函数
*如果Base没有虚函数,*pb识别的就是编译时期的类型;
*如果Base有虚函数,*pb识别的就是运行时期的类型 RTTI类型。
*/

前提:pb为Base类型的指针,指向的是Derive的派生类对象:

  • Base没有虚函数,派生类中没有虚函数,则识别的是编译时期的类型,即Base类。
  • Base存在虚函数,存在Derive类的虚函数表,则识别的就是运行时候的RTTI类型,即为Derive类。

其他方式调用的绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Base
{
public:
virtual void show() { cout << "Base::show()" << endl; }
private:
int dataa_;
};

class Derive :public Base
{
public:
void show() { cout << "Derive::show()" << endl; }
private :
int datab_;
};

int main()
{

/*************对象本身调用*****************/
Base b;
Derive d;
//这里因为是对象本身访问自己的成员方法,发生的是静态绑定
//无论show是不是虚函数,都是发生静态绑定
//很好理解,通过对象本身调用,在编译时期就可以确定形式,不用动态绑定
b.show(); //静态绑定
d.show(); //静态绑定

/*************指针方式调用*****************/
Base *pb1=&b;
pb1->show(); //动态绑定
Base *pb2=&d;
pb2->show(); //动态绑定

/*************引用方式调用*****************/
Base &ref1=b;
ref1.show(); //动态绑定
Base &ref2=d;
ref2.show(); //动态绑定

return 0;
}

image

总结:动态绑定必须当通过指针(引用)调用虚函数,才会发生。

虚函数的默认参数问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;

class Base
{
public:
int a = 1;
virtual void print(int n = 2)
{
cout << "base:" << a + n << endl;
}
};

class Derive :public Base
{
public:
int b = 3;
void print(int n = 10)
{
cout << "derive:" << b + n << endl;
}
};

int main()
{
Base* ptr = new Derive();
ptr->print();
delete ptr;
}

执行结果:
derive:5

分析:在编译阶段,基类虚函数的默认参数就已确定(发生静态绑定)。当调用子类函数时会发生多态行为,但编译器仍会采用基类的默认参数。

虚函数实现的依赖

  • 虚函数能产生地址,存储与vftable中
  • 对象必须存在(通过vfptr—>>>vftable—>>>虚函数地址)

构造函数不存在虚函数,因为这个时候还没有对象产生(不满足虚函数依赖的条件二)。即使在类的构造函数中,调用了虚函数,也是静态绑定。
static静态成员方法也不能被实现成虚函数方法,(其不依赖于对象,不满足条件二)

虚析构函数的实现

情景:基类的指针(引用)ptr指向堆上new出来的派生类对象时,delete ptr;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Base
{
public:
Base(int data) :dataa_(data) { cout << "Base()" << endl; }
~Base() { cout << "~Base()" << endl; }
protected:
int dataa_;
};

class Derive :public Base
{
public:
Derive(int data)
:datab_(data), Base(data)
{
cout << "Derive()" << endl;
}

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

private:
int datab_;
};
int main()
{
Base* pb = new Derive(10);
delete pb;
return 0;
}

image

在本篇 派生类的构造顺序 一小节中提到,派生类的构造顺序是基类构造–派生类构造–派生类析构–基类析构。
但是这里却没有调用派生类的析构函数,如果派生类有指向外部资源,就会造成内存泄露。

这里为什么没有调用派生类的析构函数?

这里pb是Base类型的指针,当调用delete pb的时候,在Base类中找到其对于的析构函数,Base::~Base(),这里发生的是静态绑定

解决方法
1.使用Derive 类型的指针指向开辟的内存(不是本节重点)

1
2
Derive* pb = new Derive(10);
delete pb;

2.将基类的析构函数定义为virtual(发生动态绑定)
重点:基类的析构函数是virtual函数,那么派生类的析构函数自动成为virtual函数。

1
2
3
4
5
6
7
8
class Base
{
public:
Base(int data) :dataa_(data) { cout << "Base()" << endl; }
virtual ~Base() { cout << "~Base()" << endl; }
protected:
int dataa_;
};

image
这里发生的是动态绑定,pb是Base类型指针,指向派生类对象(存在虚函数),所以这里发生动态绑定;由于派生类的虚析构函数重写了基类的虚析构函数,所以这里调用的是派生类的析构函数。

image

虚继承与虚基类

普通继承

1
2
3
4
5
6
7
8
9
10
11
12
class Base
{
public:
private:
int ma_;
};
class Derive :public Base
{
public:
private:
int mb_;
};

虚继承

1
2
3
4
5
6
7
8
9
10
11
12
class Base
{
public:
private:
int ma_;
};
class Derive :virtual public Base
{
public:
private:
int mb_;
};

virtual方法修饰:

  • 修饰的是成员方法则是虚函数
  • 可以修饰继承的方式,是虚继承。被虚继承的类,成为虚基类。

调用函数查看这两个类的内存分布情况

1
2
// 输出所有类型的内存布局
cl test.cpp /d1reportAllClassLayout

image
总结:
当一个类中有虚函数存在时,则有vfptr(虚函数指针),指向vftable(虚函数表),其中包含RTTI信息、虚函数地址等信息;
当派生类从基类虚继承数据,则存在vbptr(虚基类指针),指向vbtable(虚基类表),其中分别表示vbptr指针和虚基类数据在派生类对象的偏移量。
并且可以观察到,当有虚基类存在的时候,虚基类部分是存在于派生类的末尾部分。
从上图分析可知,普通继承到虚继承的过程(派生类中),将虚继类移到派生类的末尾,并在原来虚基类的地方加上一个vbptr,指向虚基类表。

虚继承与虚函数结合

虚函数+公有继承函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base
{
public:
virtual void show() { cout << "Base::show()" << endl; }
private:
int ma_;
};
class Derive : public Base
{
public:
void show() { cout << "Derive::show()" << endl; }
private:
int mb_;
};

int main()
{
Base* p = new Derive();
p->show();
delete p;
return 0;
}

执行上述代码是没有问题的

虚函数+虚继承
当加入虚继承后,就报错
image
有打印信息可以知道,p->show()是被成功调用的,出错的地方在delete p部分;
查看Derive虚继承前后的内存布局,如下图所示(这里我是根据打印结果绘制的一份示意图,便于理解)
可以看到,当存在虚继承是,虚基类的部分是被放到了派生类的末尾部分,而这时候定义的Base类型指针(始终指向基类部分),因为基类部分存在vfptr,所以这里可以成功的动态绑定,调用Derive::show()函数;
但是当析构的时候,由于基类指针p2指向的是派生类末尾的基类部分数据,析构的时候只能析构(p2到末尾的数据),而派生类的真正起始位置到p2位置的内存数据,无法被有效析构。
image
我们在基类中重载delete函数,在派生类中重载new函数,并打印对应的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Base
{
public:
virtual void show() { cout << "Base::show()" << endl; }
void operator delete(void* ptr)
{
cout << "Base类删除的内存空间起始地址为:" << ptr << endl;
free(ptr);
}
private:
int ma_;
};
class Derive : virtual public Base
{
public:
void show() { cout << "Derive::show()" << endl; }
void * operator new(size_t size)
{
void* p = malloc(size);
cout << "Derive类开辟的内存空间起始地址为:" << p << endl;
return p;
}
private:
int mb_;
};

int main()
{
Base* p = new Derive();
cout << "main p:" << p << endl;
p->show();
delete p;
return 0;
}

打印的结果为:
image
这里表明派生类开辟的实际内存空间和删除的实际起始空间相差16个字节,vbptr+mb_(8+4=12—>>>内存对齐,16字节)。这是导致这里错误的主要原因。

vfptr指针在派生类中的哪个位置

image
总结
不存在虚继承
①基类存在虚函数,vfptr位于派生类的基类对象部分,且位于派生类的起始地址;
②基类不存在虚函数,派生类中有虚函数,vfptr位于派生类的独有部分,且位于派生类的起始地址;

存在虚继承
③基类存在虚函数,vfptr位于派生类的虚基类部分,位于派生的末尾部分;
④ 基类不存在虚函数,派生类中有虚函数,vfptr位于派生类的独有部分,且位于派生类的起始地址;

多继承

菱形继承

多继承的好处是可以更多的代码复用;缺点是派生类对象有多份间接基类的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class A
{
public:
A(int data) :ma_(data) { cout << "A()" << endl; }
~A(){ cout << "~A()" << endl; }
private:
int ma_;
};
class B : public A
{
public:
B(int data) :A(data),mb_(data) { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
private:
int mb_;
};

class C : public A
{
public:
C(int data) :A(data), mc_(data) { cout << "C()" << endl; }
~C() { cout << "~C()" << endl; }
private:
int mc_;
};


class D : public B,public C
{
public:
D(int data) :B(data), C(data), md_(data) { cout << "D()" << endl; }
~D() { cout << "~D()" << endl; }
private:
int md_;
};

int main()
{
D d(10);
return 0;
}

image
查看D的内存布局发现确实有两个dataa_变量,调用了两次A的构造函数。
如何解决呢,对所有基类的继承采用虚继承,即将A变成虚基类
image

并且由于类B、类C采用的是虚继承,使得D只包含一个A的子对象,D必须直接调用A类的构造函数才能初始化A的成员变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class B : virtual public A
{
public:
B(int data) :A(data), mb_(data) { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
private:
int mb_;
};

class C : virtual public A
{
public:
C(int data) :A(data), mc_(data) { cout << "C()" << endl; }
~C() { cout << "~C()" << endl; }
private:
int mc_;
};


class D : public B, public C
{
public:
D(int data) :A(data), B(data), C(data), md_(data) { cout << "D()" << endl; }
~D() { cout << "~D()" << endl; }
private:
int md_;
};

构造与析构顺序为
image

半圆形继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class A
{
public:
A(int data) :ma_(data) { cout << "A()" << endl; }
~A(){ cout << "~A()" << endl; }
private:
int ma_;
};
class B : public A
{
public:
B(int data) :A(data),mb_(data) { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
private:
int mb_;
};

class C : public A,public B
{
public:
C(int data) :A(data) ,B(data),mc_(data) { cout << "C()" << endl; }
~C() { cout << "~C()" << endl; }
private:
int mc_;
};

int main()
{
C c(10);
return 0;
}

image
将B和C改为虚继承A,即使得A变为虚基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class B : virtual public A
{
public:
B(int data) :A(data), mb_(data) { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
private:
int mb_;
};

class C : virtual public A, public B
{
public:
C(int data) :A(data), B(data), mc_(data) { cout << "C()" << endl; }
~C() { cout << "~C()" << endl; }
private:
int mc_;
};

image
构造和析构顺序为:
image

多态

静态(编译时期)多态:

  • 函数重载,bool compare(int a,int b),bool compare(double a,double b);
  • 模板(函数模板、类模板)

动态(运行时期)多态:
虚函数机制,调用哪个函数在运行时决定。
基类指针(引用)调用哪个派生类对象,就会调用该派生类对象方法,称为多态。
如代码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Animal
{
public:
Animal(string name):name_(name){}
virtual void bark() {}
protected:
string name_;
};
class Cat:public Animal
{
public:
Cat(string name) :Animal(name) {}
void bark() { cout << name_ << "bark:miao miao~~~" << endl; }

};

class Dog :public Animal
{
public:
Dog(string name) :Animal(name) {}
void bark() { cout << name_ << "bark:wang wang~~~" << endl; }

};
class Pig :public Animal
{
public:
Pig(string name) :Animal(name) {}
void bark() { cout << name_ << "bark:heng heng~~~"<<endl; }

};
//animal作为基类指针,当传入派生类的地址后,发生动态绑定,调用对应类的bark方法
void bark(Animal* animal)
{
animal->bark();
}
int main()
{
Cat cat("加菲猫");
Dog dog("汪汪队");
Pig pig("佩奇");

bark(&cat);
bark(&dog);
bark(&pig);
return 0;
}

这里bark函数根据传入的派生类类型,通过基类指针指向,从而实现调用不同派生类的函数。

抽象类和普通类的区别

普通类是用于抽象一个实体的类型,比如这里的Cat类、Dog类、Pig类等;
而这里的Animal作为抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
//1.string name_;让所有的动物实体通过继承Animal直接复用该属性
//2.给所有的派生类保留统一的覆盖/重写接口

class Animal
{
public:
Animal(string name):name_(name){}
//因为bark在animal中并没有实际的作用,只是为派生类提供一个统一的接口,定义为纯虚函数
//纯虚函数
virtual void bark()=0;
protected:
string name_;
};

拥有纯虚函数的类,叫做抽象类。
抽象类不能实例化对象(抽象类并不是为了抽象某个类型而存在的。),但是可以定义指针和引用变量。

$share-item-width = 1.8rem .post-share-container { flex-shrink 0 .share-list-wrap { display flex justify-content flex-end .share-item { width $share-item-width height $share-item-width margin-left 0.5rem padding 0.4rem border-style solid border-width 0.1rem border-radius 50% cursor pointer transition-t("background", "0", "0.3", "ease") i { color inherit font-size 1rem } &.qq { color var(--keep-primary-color) border-color var(--keep-primary-color) &:hover { color var(--background-color-1) background var(--keep-primary-color) } } &.wechat { color var(--keep-success-color) border-color var(--keep-success-color) img { filter brightness(1) !important &[lazyload] { &::before { background #fff !important } } } &:hover { color var(--background-color-1) background var(--keep-success-color) } } &.weibo { color var(--keep-danger-color) border-color var(--keep-danger-color) &:hover { color var(--background-color-1) background var(--keep-danger-color) } } } } }
if (hexo-config('comment') && hexo-config('comment.enable') == true && hexo-config('comment.use')) { if (hexo-config('comment.use') == "valine") { @import "./valine.styl" } else if (hexo-config('comment.use') == "gitalk") { @import "./gitalk.styl" } else if (hexo-config('comment.use') == "twikoo") { @import "./twikoo.styl" } else if (hexo-config('comment.use') == "waline") { @import "./waline.styl" } } .comments-container { display inline-block width 100% margin-top var(--component-gap) .comment-area-title { width 100% color var(--text-color-3) font-size 1.38rem line-height 2 i { color var(--text-color-3) } +keep-tablet() { font-size 1.2rem } } .configuration-items-error-tip { display flex align-items center margin-top 1rem color var(--text-color-3) font-size 1rem i { margin-right 0.3rem color var(--text-color-3) font-size 1.2rem } } .comment-plugin-fail { display none flex-direction column align-items center justify-content space-around width 100% padding 2rem .fail-tip { color var(--text-color-3) font-size 1.1rem } .reload { margin-top 1rem } } .comment-plugin-loading { flex-direction column padding 1rem color var(--text-color-3) .loading-icon { color var(--text-color-4) font-size 2rem } .load-tip { margin-top 1rem color var(--text-color-4) font-size 1.1rem } } }
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 42.9k
$li-margin-bottom = 0.8rem $post-tool-button-width = 2.5rem .post-tools-container { padding-top var(--component-gap) .post-tools-list { li { margin-bottom $li-margin-bottom &:last-child { margin-bottom 0 } } li.tools-item { position relative box-sizing border-box width $post-tool-button-width height $post-tool-button-width color var(--text-color-3) font-size 1.2rem background var(--background-color-1) border-radius 50% box-shadow 2px 2px 5px var(--shadow-color) cursor pointer &:hover { box-shadow 2px 2px 8px var(--shadow-hover-color) } i { color var(--text-color-3) } &:hover { color var(--background-color-1) background var(--primary-color) i { color var(--background-color-1) !important } } &.toggle-show-toc { display none } &.go-to-comments { .post-comments-count { position absolute top 0 right -1rem display none align-items center justify-content center box-sizing border-box min-width 1.1rem height 1.1rem padding 0 0.2rem color var(--badge-color) font-size 12px background var(--badge-background-color) border-radius 0.4rem +keep-tablet() { display none !important } } } } li.status-item { width $post-tool-button-width height $post-tool-button-width color var(--text-color-3) font-size 1.6rem cursor pointer &.post-lock { cursor default .fa-lock-open { display none color var(--keep-success-color) } &.decrypt { cursor pointer .fa-lock-open { display block } .fa-lock { display none } } } } } }