前言

自虚函数之后,多态会带来一些小问题


正文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class father{
public:
int x;
int y;

virtual void Move(){
std::cout << "father move\n";
}
void test(){
Move();
}
};

class son :public father{
public:
void Move() override{
std::cout << "son move\n";
}
};

当我们实例化son,并且调用test的时候,这个test当然是继承过来的。

1
2
son s1;
s1.test();

第一感觉可能会以为输出father的move。
但是,这个test是被继承过来的,son类中也有一个move函数,这个move是通过虚函数继承的,也就是说move只要在这个派生类中重写了,那么就不会跟基类的move冲突。

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
class father{
public:
int x;
int y;

virtual void Move(){
std::cout << "father move\n";
}
void test(){
Move();
}

father(){
std::cout << this << std::endl;
}
};

class son :public father{
public:
void Move() override{
std::cout << "son move\n";
}
son(){
std::cout << this << std::endl;
}
};

为了好的了解,分别在构造函数的时候打印this指针。

可以看到打印的this指针的地址是相同的。

随便画个草图,基类的属性通过继承就像拷贝到派生类中一样,当实例化这个派生类的时候,由于发生了继承,基类要先完成构造,然后把这个x和y传给son。
然后还是实例化,这里实例化的是谁,son,那么this指针就是从son的地址开始的,所以两次this都是一样的。

注意派生类不会继承基类的构造函数,但是派生类在类中是可以访问的


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 father{
public:
int x;
int y;

virtual void Move(){
std::cout << "father move\n";
}
void test(){
this->Move();
}

father(){
std::cout << this << std::endl;
this->Move(); //基类构造函数中也调用一次move
}
};

class son :public father{
public:
void Move() override{
std::cout << "son move\n";
}
son(){
std::cout << this << std::endl;
}
};

当我们在基类的构造函数中也调用一次move函数。
然后实例化son执行test。又会发生不同的情况。

可以看到,他居然执行了基类的move函数。

原因也在构造函数上,继承的时候,基类先完成初始化,所以在基类的构造函数里面调用move,它只能找到自己成员中的move函数。而后面test调用的move是因为派生类完成构造了,它里面重写了虚函数,这样就可以指定成自己的move函数了。

1
2
3
4
5
6
7
8
9
10
class son :public father{
public:
void Move() override{
std::cout << "son move\n";
}
son(){
std::cout << this << std::endl;
son::test();
}
};

作用域指定调用肯定是调用自己,且不说派生类已经完成构造了。

1
2
3
4
5
6
7
8
9
10
class son :public father{
public:
void Move() override{
std::cout << "son move\n";
}
son(){
std::cout << this << std::endl;
father::test();
}
};

因为虚函数被重写了,所以即便是通过基类调用test还是调用派生类重写的move。

1
2
3
4
5
6
7
8
9
10
class son :public father{
public:
//void Move() override{
// std::cout << "son move\n";
//}
son(){
std::cout << this << std::endl;
father::test();
}
};

当注释掉son类中的move,自然没有重写的部分,就只能继承基类的move函数。

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 father{
public:
int x;
int y;

virtual void Move(){
std::cout << "father move\n";
}
void test(){
this->Move();
}

father(){
std::cout << this << std::endl;
this->Move();
}
~father(){
Move();
}
};

class son :public father{
public:
void Move() override{
std::cout << "son move\n";
}
son(){
std::cout << this << std::endl;
father::test();
}
~son(){
Move();
}
};

当增加了析构函数之后,再依次执行move函数。
顺序其实也很好猜,从son实例化,基类先完成构造,派生类在完成构造,派生类先完成释放,基类在完成释放。
所以说,father析构函数调用的move是基类中的move,而son的析构函数则还是调用自己的。

并且析构函数是类结束生命周期的象征,所以基本上都是属于静态绑定了,即便是this指针也还是自己。

如果真的要调用基类的虚函数,就直接通过作用域访问就像了


虚函数默认参数

1
2
3
virtual void AutoMove(int step = 2){
std::cout << "auto move" << step << std::endl;
}

往father类中添加个带默认参数的虚函数,然后在类中重写。

1
2
3
void AutoMove(int step = 3){
std::cout << "--auto move" << step << std::endl;
}

看到automove的step是2,却不是3,我们指针指向son的对象了。

虚函数的默认参数,即使函数被重写了,也不会改变这个默认参数


析构问题

1
2
3
4
son s1;
father *p = new son();
p->AutoMove();
delete p;

看到他先执行的father的析构函数,但是我们这个指针是指向son类的。

这是因为指针是father类型的,delete p就相当于调用了p的析构函数,所以调用的是father的析构函数。
当然这会造成一个内存泄漏的问题,son类作为派生类,谁能保证成员是不是多一点,至少会比基类大点,而释放的大小是参考father的。

所以虚函数又派上了用场。

1
2
3
4
virtual ~father(){
std::cout << "~father\n";
Move();
}

看到将析构函数也设置成虚析构函数之后,son类型的析构函数也会继承,这样一来释放的时候就会参考new的类型。


结语

虚继承这种动态的绑定关系真的要好好理一理。。
至于虚析构函数,如果没有特定内容,我们也可以用default指定。virtual ~father() = default;