首先我们有以下简单的C++代码
class X {
int x;
string str;
public:
X() {}
virtual ~X() {}
virtual void printAll() {}
};
class Y : public X {
int y;
public:
Y() {}
~Y() {}
void printAll() {}
};
然后我们给出class Y的object的memory layout
| |
|------------------------------| <------ Y class object memory layout
| int X::x |
stack |------------------------------|
| | int string::len |
| |string X::str ----------------|
| | char* string::str |
\|/ |------------------------------| |-------|--------------------------|
| X::_vptr |------| | type_info Y |
|------------------------------| |--------------------------|
| int Y::y | | address of Y::~Y() |
|------------------------------| |--------------------------|
| o | | address of Y::printAll() |
| o | |--------------------------|
| o |
------|------------------------------|--------
| X::X() |
|------------------------------| |
| X::~X() | |
|------------------------------| |
| X::printAll() | \|/
|------------------------------| text segment
| Y::Y() |
|------------------------------|
| Y::~Y() |
|------------------------------|
| Y::printAll() |
|------------------------------|
| string::string() |
|------------------------------|
| string::~string() |
|------------------------------|
| string::length() |
|------------------------------|
| o |
| o |
| o |
| |
我们可以看到class Y继承了class X所以class的Y的对象中也有class X的虚指针(X::_vptr
),并且class Y重载了class X虚函数的部分,所以class Y中虚指针(X::_vptr
)指向的是class Y重载之后的函数(Y::~Y()
,Y::printAll()
),那么class X的对象呢?class X的对象中的虚指针(X::_vptr
)其实也指向了Y::~Y()
,Y::printAll()
,因为被派生类class Y重载
最常用的例子就是基类将析构函数设置成虚函数,我们看下面代码如果基类析构函数不加virtual
会发生什么
#include
class base{
public:
base(){ std::cout << "base construct" << std::endl; }
~base(){ std::cout << "base destruct" << std::endl; }
};
class derive : public base{
public:
derive(){ std::cout << "derive construct" << std::endl; }
~derive(){ std::cout << "derive destruct" << std::endl; }
};
int main(){
base* b = new derive;
delete b;
}
base construct
derive construct
base destruct
为什么在delete b的时候只会析构基类而不会析构派生类?我们new的是一个派生类(包含派生类的成员和基类成员),假如只析构基类那么派生类的成员就会留在内存中从而导致内存泄漏…
因为我们析构的是基类,并且基类对象(我们传递派生类对象给基类,代表我们只能使用基类部分的对象)没有找到派生类的析构函数,所以只会析构基类,此时我们给基类的析构函数加上virtual
关键字,代表基类对象中vptr(base::_vptr
)所指向的虚函数被重载,且虚函数包括基类的析构函数,此时这个析构函数被重载成派生类的析构函数也就是derive::~derive()
,那么此时基类就有了派生类的析构函数从而可以析构memory中派生类的部分,看如下代码
#include
class base{
public:
base(){ std::cout << "base construct" << std::endl; }
virtual ~base(){ std::cout << "base destruct" << std::endl; }
};
class derive : public base{
public:
derive(){ std::cout << "derive construct" << std::endl; }
~derive(){ std::cout << "derive destruct" << std::endl; }
};
int main(){
base* b = new derive;
delete b;
}
输出
base construct
derive construct
derive destruct
base destruct
我们今天所讨论的如何在list中存储不同类型的元素也是用到上述的原理,将不同类型的变量存储在派生类中,std::list
中存储的是基类类型的指针,而传入std::list
容器的是派生类对象,此时我们可以通过基类的虚函数(被派生类重载)访问派生类中不同类型的对象,如下面代码
#include
#include
#include
#include
class Container{
public:
Container(const std::string& Key)
:Key(Key){
};
virtual ~Container(){};
public:
std::string get_Key() const { return Key; }
virtual void get_Value(){};
private:
std::string Key;
};
template<typename T>
class Member : public Container{
public:
Member(std::string Key, T Value)
:Container(Key), Value(Value){
};
virtual ~Member(){};
public:
void get_Value() const { std::cout << Value << std::endl; }
private:
T Value;
};
int main(){
std::list<Container> c;
c.push_back(Member<int>("key1", 2));
c.push_back(Member<char>("key2",'a'));
c.push_back(Member<bool>("key3",true));
return 0;
}
对于设计方面来说没有什么大的问题,因为前面都解释了,这里总结一下语言层面的坑(主要是虚函数方面)
virtual func() = 0;
这种类型)那么这个class就是abstract class,如果是abstract class那么意味着我们不能对这个class实例化,我们能做的是让他作为基类,让派生类重载,比如下面代码class A{
public:
A();
virtual ~A();
virtual void print() = 0; //虚函数
};
class B{
public:
B();
~B();
void print();
}
class C : public A{
public:
C();
~C();
void print();
}
class D : public A{
public:
D();
~D();
void print();
virtual void print2() = 0; //虚函数
}
int main(){
A a; // error!!!
D d; //error!!!
B b;
C c;
return 0;
}
上述代码中class A和class D为abstract class所以他们不能被示例化所以A a;
和D d;
是错误的,他们只能被用于其他class的基类
Undefined Reference to vtable ...
,它本质上是在告诉我们我们的虚函数只声明没有定义…并且我们在后续使用了他,看下面的代码class A{
public:
A();
virtual ~A(){};
virtual void print(); //只声明没有定义
};
class B{
public:
B();
~B();
void print(){/****/};
};
上述代码中虚函数只被声明,而没有被定义,所以在编译器眼里代表在一个编译单元内,虚函数的具体定义在别处(因为我们这里只有声明),但是我们的确没有定义所以链接器会报错,我们写项目代码的时候一般这种class文件放在头文件.h中而,class的method放在对应的.cc文件,他们组成一个编译单元,所以我们在实例化class A的时候会报错,因为编译器发现A中的虚函数print可能被重载,换句话说被使用,但是编译器没有找到关于class A中虚函数print的定义,所以编译器以为定义在别的文件中(对应的.cc)此时编译器会去从同一个编译单元中找,然后链接,但是因为没有找到所以链接错误,而解决方法很简单,就是将声明变成定义,virtual void print();
变成virtual void print(){};
即可,还有一个例子和上述问题非常像就是如下代码
extern int i;
int *pi = &i;
我们声明了i,告诉编译器i的定义在文件外部,然后我们继续使用这个外部的变量i,在编译的时候编译器会去从别的文件中找这个变量i,正常情况下是找到并且链接在一起,但是没有找到,所以也会报错,和我们上述的问题一模一样