极客班C++OOP(下)第五周笔记
0.关于vptr和vtbl
虚指针vptr
和虚表vtbl
,通俗的说,两者主要用途就是,在继承关系中确定虚函数具体调用哪个函数时用的。
1 继承关系内存布局
子类一定含有父类的部分(part)
对于函数,继承的话,是继承调用权,而非函数的空间。
2 静态绑定
函数初始化的时候就知道成员的地址了,形如
//汇编代码
call xxxxxx
3 动态绑定 ,以及 this的解释
C++看到一个函数,有两个选择:
3.1.静态绑定
//汇编形式:
//__call_@ 0xABDI398A4@func1
3.2.动态绑定
如果符合某些条件:
- a) 通过指针
- b) 指针是向上转型的关系
- c) 调用的是虚函数
只要符合上面的三个条件,编译器就把代码编译成
class A{
public:
virtual void vfun1();
void func2();
};
class B:public A{
public:
virtual void vfun1();
void fun2();
};
//调用
A p=new B();//B是A的派生类,并且调用了虚函数
p->vfun1();//虚函数,覆盖了父类的虚函数,动态绑定
p->fun2(); //普通函数,静态绑定,无需动态确定,毕竟B里面本来就有了
编译器看来,p->vfun1() 的形式如下:
(*(p->vptr)[n])(p);
//或者
(* p->vptr[n])(p);
即,调用虚函数之前,我们仍然不知道该函数对应的是哪个调用(到底是调用A::vfun1()
呢,还是B::vfun1()
)。直到查找虚函数表,编译器才最终确定是哪个调用。所以这种虚函数和对象的绑定关系,就叫做动态绑定。
3.3 this指针
其中,n
就是这个虚函数在虚表中的顺序索引,需要了解的是:
编译器在编译初期就把虚函数定义的顺序记录了下来,并对做了索引关联
所以当我们是使用指针p
(向上转型)查找虚函数(p->ptr[n])
的时候,虚指针就确定了对应虚函数的地址,然后再调用该虚函数*(p->ptr[n])()
,实参为p
,即*(p->ptr[n])(p)
说了那么多,其实p就是this
3.4 多态
上面3.3
小节中说到的动态绑定(即,调用虚函数的时候才能确定是哪个对象来调用),实际上就是继承关系中的多态。
- vptr 虚指针 , 虚函数的指针
- vtbl 虚表 , 虚函数地址表,顺序存储了对象虚函数的地址
其中,vptr
是当基类定义了虚函数的时候,如果子类继承了基类
,那么子类在实例化的时候,虚指针就可以指出调用的是哪个函数了。
举个例子:
有时候我们需要在容器中存放很多不同的水果,香蕉、苹果、梨,但是容器只能存放一种东西,所以只好存指针(指针才是无差别的)。那么,存储什么类型的指针呢?我们这么多种类的东西,都是水果,所以,应该放水果的指针进去,根据之前的向上转型的例子,可以看出,具体水果可以转型为基类水果类型。
std::list<Fruit*> myList;
我们定义了一个myList,该容器存放的是 Fruit*
这种类型的指针。
class Fruit{
public:
virtual print(){ std::cout << "I'am Fruit!";}
};
class Apple:public Fruit{
public:
virtual print(){ std::cout << "I'am Apple!";}
};
class Banana:public Fruit{
public:
virtual print(){ std::cout << "I'am Banana!";}
};
class Pear:public Fruit{
public:
virtual print(){ std::cout << "I'am Pear!";}
};
//那我们就可以在里面放各种派生类型了
myList.push_back(new Apple());
myList.push_back(new Banana());
myList.push_back(new Pear());
Fruit pApple = new Apple();
pApple->print();//虚函数,调用派生类自己的虚函数
4. new && delete
new 和 delete 在C++中分别是创建和回收堆内存的配对操作符。既然是操作符,那么一般就可以重载。
但是,下面这段话,并不是操作符new真正的调用,此处只是一个关键字表达式,C++会将该关键字分配到具体的operator new()
上。
String *ps = new String("Hello world");
delete ps;
//数组
String *p = new String[3];
delete [] p;
上面的例子,就是表达式,表达式分解之后就是operator new
和operator delete
两个函数调用,以及相应内存管理过程。 ``
其中,new String("Hello world")
,这是表达式,可以分解为以下几个动作。
try {
void *mem =operator new (sizeof(String));//操作符函数,可以重载
String *ps = static_cast<String*>(mem);//转型
ps->String::String("Hello world");//构造
}
而,delete
操作符,扮演的是
p->~String(); //调用析构函数
operator delete(p);//释放内存
5. 重载 new和delete
既然new和delete是操作符,那么就可以重载(c++规定可以重载的范围内)。 重载操作符,分两大类
- 全域重载
- ::operator new , ::operator delete
- ::operator new[], ::operator delete[]
- 成员重载
- A::operator new(), A::operator delete()
- A::operator new[], A::operator delete[]
对于全域范围内重载,有一个条件是,不能在某个特定的namespace重载,必须是全域的。
同时,new操作符返回的必须是void *
类型,第一个参数必须是size_t
类型。
inline void * ::operator new(size_t size);
delete操作符则类似
inline void ::operator delete(void * ptr);
6. Basic_String 扩充申请量
在这个示例中,我们学习了new的主要用途,就是用于自定义的特殊的内存分配。比如 basic_string 类型的内存分配,并不是通常我们看到的new String()对象就完了,该类还做了扩展,即在基本类型占用空间的基础上,还增加了extra大小的扩展空间,以作特殊用途。
这类特性,得以给我们极大的内存管理上的便利。
另外,new 和delete在内存分配上是配对的,但是并不意味着,new之后必然会调用delete来释放空间。这一点,在ppt中,我们已经足够了解,这里就不多做说明了。