最近在重学C++,做一些笔记。
我们现在考虑类的继承问题. 假如有一个父类 A A A, 有一个子类 B B B, 子类 B B B中重写了父类的一个方法(函数). 现在我们要额外写一个函数, 这个函数要调用父类的这个方法, 因此这个函数我们可以按值传递, 也可以按引用传递和指针传递. 但是在给这个函数传参的过程中, 我们有可能会传入的是子类对象, 即如下所示:
void func(A& a){
a.method();
}
int main(){
B b;
func(b);
}
那我们还有必要再写(重载)一个func, 把参数换成 B B B吗? 答案是否定的. 换言之, 既然函数里执行的是子类父类的相似行为, 那么我们可以期望利用父类类型来对可能传入的子类们进行统一操作, 提高代码的复用性.
但是现在有什么问题? 我们传入的是子类对象, 而参数是父类引用, 在这个过程中会发生类型转换. 因此, 在调用method的时候, 我们还是调用的父类的method.
解决这个问题的方式, 就是将父类的method声明为virtual的:
class A{
...
public:
virtual void method()...
};
这样编译器就会根据引用或指针指向的类型来确定调用哪个函数. 那么编译器是怎么做到的呢?
这里借用《C++ primer plus》中的讲述:
通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含另一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中。注意,无论类中包含的虚函数是1个还是10个,都只一需要在对象中添加1个地址成员,只是表的大小不同而已。
我简单写了一个测试程序,涵盖了以上几点:
class BaseClass{
private:
int value;
public:
BaseClass(const int v) { this->value = v;};
virtual void showValue() const { std::cout << "BaseClass " << this->value << "\n"; };
virtual ~BaseClass() { std::cout << "Deconstructor of BaseClass \n"; };
};
class Child : public BaseClass{
private:
char* name;
public:
Child(const int v, const char* str) : BaseClass(v){
this->name = new char[100];
strcpy(this->name, str);
}
void showValue() const { std::cout << "ChildClass " << this->name << "\n"; };
virtual ~Child(){
delete[] this->name;
this->name = nullptr;
std::cout << "Deconstructor of Child \n";
}
};
void func(BaseClass& bc){
bc.showValue();
}
void test(){
BaseClass class0 (10);
char str[] = "sadskda";
Child class1 (10, str);
func(class0);
func(class1);
}
int main(){
test();
system("pause");
return 0;
}
其中func函数实现了只传递父类对象,但也可以执行子类方法,这是通过虚函数实现的。此外,子类的析构函数需要释放申请的内存,因此程序输出如下:
BaseClass 10
ChildClass sadskda
Deconstructor of Child
Deconstructor of BaseClass
Deconstructor of BaseClass