类的继承——虚函数(二)
● 由虚函数所引入的动态绑定属于运行期行为,与编译期行为有所区别
虚函数与继承紧密相关
– 虚函数的缺省实参只会考虑静态类型
struct Base
{
virtual void fun(int x = 3)
{
std::cout << "virtual void fun(int x = 3): " << x << std::endl;
}
};
struct Derive : Base
{
void fun(int x = 4) override
{
std::cout << "void fun(int x = 4) override: " << x << std::endl;
}
};
void ptrProc(Base* b)
{
b->fun(); //编译期翻译为b->fun(3)
}
void refProc(Base& b)
{
b.fun(); //编译期翻译为b.fun(3)
}
int main()
{
Derive d;
ptrProc(&d); //等价于ptrProc(static_cast<Base&>(d)),虚函数的缺省实参只会考虑静态类型,所以输出3
refProc(d); //等价于refProc(static_cast<Base&>(d)),同上
return 0;
}
– 虚函数的调用成本高于非虚函数
Java里面所有函数都是虚函数实现,对用户很友好
● final 关键字
一方面表示某个类不会被派生或者某个虚函数不会被修改,给编译器最大的余地优化。
struct Derive final: Base, C++11
struct Base
{
virtual void fun(int x = 3)
{
std::cout << "virtual void Base::fun(int x = 3): " << x << std::endl;
}
};
struct Derive : Base
{
void fun(int x = 4) override final //final关键字
{
std::cout << "void Derive::fun(int x = 4) override: " << x << std::endl;
}
};
struct Derive2 : Derive
{
//void fun(int x = 5) //不能再重写虚函数,Error: Declaration of 'fun' overrides a 'final' function
void func(int x = 5)
{
std::cout << "void Derive2::func(int x = 5) override: " << x << std::endl;
}
};
struct Derive3 : Derive2
{
void func(int x = 6)
{
std::cout << "void Derive3::func(int x = 6) override: " << x << std::endl;
}
};
void ptrProc(Base* b)
{
b->fun();
}
void refProc(Base& b)
{
b.fun();
}
int main()
{
Derive d;
ptrProc(&d);
refProc(d);
std::cout << '\n';
Derive2 d2;
ptrProc(&d2);
refProc(d2);
d2.func();
std::cout << '\n';
Derive3 d3;
ptrProc(&d3);
refProc(d3);
d3.func();
return 0;
}
– 为什么要使用指针(或引用)引入动态绑定
void Proc(Base b)
//void Proc(Base b)开辟了另外一块内存,构造了一个Base对象,使用Base类内的函数版本,构造在编译期执行,因此只能通过指针或引用引入动态绑定
//引用的底层实现是指针,因此讨论指针void Proc(Base* b): 只是构造了一个指针,指向原始的对象,调用函数时,
//根据指针找到原始对象,再根据原始对象找到vtable,再根据vtable决定调用的函数,
//在这个过程中,没有Derive到Base的转换,因此才能调用相应的虚函数版本
{
b.fun();
}
– 在构造函数中调用虚函数要小心
struct Base
{
Base()
{
fun(); //一定不会调用派生类的重写函数
std::cout << "Base()\n";
}
virtual void fun()
{
std::cout << "virtual void Base::fun()" << std::endl;
}
};
struct Derive final: Base //C++11
{
Derive()
: Base()
{
fun();
std::cout << "Derive()\n";
}
void fun() override
{
std::cout << "void Derive::fun()" << std::endl;
}
};
int main()
{
Derive d; //执行到Derive()时d还没有构造好(先构造Base),进入Base()内部到第一行fun()的时候,
//Base已经(缺省)初始化好了,即vtable已构造好,vtable里面的所有虚函数都会指向Base内的虚函数
//执行到Derive() : Base() { fun();时Derive已经构造完成,vtable里面的基类虚函数版本被替换成派生类中重写的版本,因此会打印重写版本中的内容
return 0;
}
– 派生类的析构函数会隐式调用基类的析构函数
struct Base
{
~Base()
{
std::cout << "~Base()\n";
}
};
struct Derive final: Base
{
~Derive()
{
std::cout << "~Derive()\n";
}
};
int main()
{
Derive obj; //编译器已知Derive的析构函数地址,Base的析构函数地址和Derive继承自Base,所以销毁时先调用~Derive()然后~Base()
Derive* d = new Derive(); //动态申请一个Derive对象
Base* b = d; //创建一个Base类型的指针指向Derive对象
delete b; //释放内存,调用基类的析构函数是在编译期决定的,所以只调用~Base()
//C++标准规定该程序的行为未定义(系统会表现出任何意义的行为)。大概率的行为会是只调用基类的析构函数
return 0;
}
#include<memory>
struct Base
{
~Base()
{
std::cout << "~Base()\n";
}
};
struct Derive final: Base
{
~Derive()
{
std::cout << "~Derive()\n";
}
};
int main()
{
std::shared_ptr<Base> sptr(new Derive());
std::unique_ptr<Base> uptr(new Derive());
return 0;
}
– 通常来说要将基类的析构函数声明为 virtual 的
struct Base
{
//virtual ~Base() = default; Since C++11,目的不是声明一个缺省析构函数,而是把它声明成虚函数,所有派生自基类的类都会隐式添加virtual关键字
virtual ~Base() //将析构函数声明为virtual
{
std::cout << "~Base()\n";
}
};
struct Derive final: Base
{
~Derive() //派生类析构函数会继承基类虚函数的virtual特性
{
std::cout << "~Derive()\n";
}
};
int main()
{
std::shared_ptr<Base> sptr(new Derive()); //C++规定当执行派生类的析构函数时发现它是虚函数,就会找到基类的析构函数并执行
std::unique_ptr<Base> uptr(new Derive());
return 0;
}
当用派生类类型的指针挂载派生类对象的时候,释放该指针,不需要将基类的析构函数声明成虚函数。
当用基类类型的指针挂载派生类对象时,释放该指针,一定要将基类的析构函数声明成虚函数。
大部分情况下会用基类类型的指针挂载(绑定)派生类对象,很少会用派生类类型的指针挂载派生类对象。
– 在派生类中修改虚函数的访问权限
struct Base
{
protected: //访问权限是编译期行为,访问权限限定符只会影响到会不会被外部访问,不会影响到vtable里面指向虚函数的位置
virtual void fun()
{
std::cout << "virtual void Base::fun()\n";
}
};
struct Derive final: Base
{
public:
void fun() override //重写的概念是替换槽里的东西
{
std::cout << "void Derive::fun()\n";
}
};
int main()
{
Derive d; //静态类型,所以d.fun()只看void Derive::fun() override
d.fun(); //能否通过编译是在编译期决定,能否通过编译的其中一个重要条件是该函数是否具有外部访问权限
Base& b = d; //b的静态类型是Base&
b.fun(); //编译期只看静态类型,所以只看virtual void Base::fun() Error: 'fun' is a protected member of 'Base'
return 0;
}
参考
深蓝学院:C++基础与深度解析