C++中的virtual概念

此虚非彼虚

0. 目录
  1. 虚函数
  2. 纯虚函数和抽象基类
  3. 虚析构函数
  4. 虚函数和类作用域
1. 虚函数

在类的继承中引入虚函数(virtual function)的概念。如果基类中定义了虚成员函数,在派生类中可以为该函数定义派生类自己的版本。

注意:

  1. 基类中的虚成员函数声明时加virtual关键字,但是如果虚函数在类外定义则不能加virtual关键字
  2. 派生类中对应的函数可以不加virtual关键字,但派生类中的函数也是虚函数,即使没有virtual关键字,它的虚属性从基类中继承过来了。
  3. 要求派生类中定义的函数与基类中对应的函数的类型相同——返回类型、函数名、参数等。有个例外如果基类中函数返回值类型是基类,那么允许派生类中对应的函数的返回值类型是派生类类型。
  4. 如果函数类型不一致,不能看成是派生类对基类函数的改写(override),而是看成重载,那么派生类中的函数会覆盖基类中的函数。
  5. override:派生类中显示定义,这个函数是改写基类中的函数.override写在参数列表或者const关键字之后。
1
2
3
4
5
class child:public parent{
public:
void func1() const override; //可以不加virtual关键字
void func2() override;
};
  1. final:该关键字用在基类成员函数上,显式声明这个函数不能在派生类中改写。
    1
    2
    3
    4
    5
    class child:public parent{
    public:
    void func1() const final;
    void func2() final;
    };

成员函数如果没有定义成虚函数,那么函数解析发生在编译阶段;而虚函数的解析发生在运行时,也就是动态绑定,也叫运行时绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class T1 {
public:
void show() { cout << "this is T1 " << endl; } //基类中要被继承的成员函数没有定义成virtual类型
};
class T2 :public T1 { //T1继承T2
public:
void show() { cout << "this is T2" << endl; }
};
void showT1(T1& t) { //参数是基类对象的引用,可以接受派生类对象
t.show(); //因为show()函数不是虚函数,所以在编译时将类T1的show()函数绑定给对象t1。
}
int main(int argc,char** argv)
{
T1 t1;
T2 t2;
showT1(t1);
showT1(t2);
return 0 ;
}

输出

1
2
this is T1
this is T1

从上面输出可以看出两个showT1()函数调用的都是基类对象的show()函数,这是因为基类中的show()函数不是虚函数,则在函数showT1()中的show()函数在编译阶段进行解析,解析成基类对象t的show()函数。

当虚函数通过引用或者指针调用时,编译器产生的代码到运行时才能确定调用那个版本的函数

将基类中的show()函数定义成虚函数,加上virtual关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class T1 {
public:
virtual void show() { cout << "this is T1 " << endl; } //基类中要被继承的成员函数定义成virtual类型
};
class T2 :public T1 { //T1继承T2
public:
void show() { cout << "this is T2" << endl; }
};
void showT1(T1& t) { //参数是基类对象的引用,可以接受派生类对象
t.show(); //因为show()函数是虚函数,所以在运行阶段才能确定show()函数的版本——根据对象t的类型
}
int main(int argc,char** argv)
{
T1 t1;
T2 t2;
showT1(t1);
showT1(t2);
return 0 ;
}

输出

1
2
this is T1
this is T2

可以将共有派生类对象绑定到基类对象的引用或者指针上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class T1 {
public:
virtual void show() { cout << "this is T1 " << endl; } //基类中要被继承的成员函数定义成virtual类型
};
class T2 :public T1 { //T1继承T2
public:
void show() { cout << "this is T2" << endl; }
};
void showT1(T1 t) { //参数是基类对象
t.show(); //因为show()函数是虚函数,所以在运行阶段才能确定show()函数的版本——根据对象t的类型
}
int main(int argc,char** argv)
{
T1 t1;
T2 t2;
showT1(t1);
showT1(t2);
return 0 ;
}

输出

1
2
this is T1
this is T1 //这里调用的是基类对象的show()函数

这里将showT1()的参数定义成基类对象,而不是引用或者指针,这样在调用showT1()函数时,如果参数是派生类,会将派生类对象转换成基类对象,这样在showT1(t2)调用的是基类的show()函数。

2. 纯虚函数和抽象基类

纯虚函数无须定义,如下所示。当然也可以为其定义,但是必须在类外。

1
2
3
4
class parent{
public:
virtual void func1() const =0;
};

含有纯虚函数的类是抽象基类,抽象基类负责定义接口,而它的派生类覆盖这些接口。

不能为抽象基类创建对象

3. 虚析构函数

通常基类中的析构函数定义成虚析构函数。这是为了确保对象能够正确析构。

1
2
3
4
5
6
7
8
class Base{
public:
virtual ~Base()=default;
};
class Derived : public Base{
public:
~ Derived()=default;
};

有的时候我们将Base*类型绑定到Derived派生类对象上,释放派生类对象时需要执行派生类对象自己版本的析构函数,所以要将基类的析构函数定义成虚函数。

析构函数的属性会被继承

4. 虚函数和类作用域

前文中讲了派生中的虚函数要和基类中的虚函数参数列表一致,这是因为在使用将基类对象的指针或引用绑定到派生类对象上这个规则的时候,对象的静态对象(变量声明时决定的)动态对象(运行时决定)是不同的,静态对象是基类类型,动态对象是派生类类型。虚函数是动态类型绑定,所以即使将派生类对象绑定到了基类的引用或指针上,虚函数的对象类型依旧是派生类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base{
public:
virtual int memfun();
};
class Derived : public Base{
public:
int memfun(int);//参数类型不一致
};
void test(Base&); //参数是基类的引用
Derived d;Base b;
b.memfun();// 调用Base的memfun()函数
d.memfun(10);//调用Derived的memfun(int)函数
d.memfun();//错误,Derived没有memfun()这个函数
d.Base::memfun();//正确,调用Base::memfun()
test(b); //调用Base的memfun()函数
test(d);//错误,Derived没有memfun()这个函数

派生类的作用域在基类作用域内。根据上面的自理,在编译阶段现在派生类作用域内按名字查找函数名memfun,找到memfun(int)函数,停止查找,但对于基类对象引用或指针,他没有memfun(int)这个函数,所以test(d)语句会报错。

事实上派生类中的memfun(int)函数隐藏了基类中的memfun()函数。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!