C++面向对象高级编程 part5
2017-11-14 11:59:35 / herohlc
item1. 对象模型:vptr与vtbl
1. 类对象内存模型
class A{
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1;
int m_data2;
};
class B :public A{
public:
virtual void vfunc1();
void func2();
private:
int m_data3;
};
class C :public B{
public:
virtual void vfunc1();
void func2();
private:
int m_data1;
int m_data4;
};
- 注意图中的类对象模型,类对象中仅包含成员数据/vptr,不包括函数地址。
- deriverd类数据成员可以与base类数据成员重名,两者保存在不同的区域。
- base类有虚函数,base类就会有vptr,base类对象有vptr则子类一定有vptr。
扩展:dervied class 调用base class 函数/函数覆盖
#include <iostream>
using std::cout;
using std::endl;
class A{
public:
virtual void vfunc1(){};
virtual void vfunc2(){};
void func1(){ cout << "A::func1" << endl;};
void func2(){ cout << "A::func2" << endl;};
private:
int m_data1;
int m_data2;
};
class B :public A{
public:
virtual void vfunc1(){};
void func2() {
cout << "B::func2" << endl;
};
private:
int m_data3;
};
class C :public B{
public:
virtual void vfunc1(){};
void func2(){ cout << "C::func2" << endl;};
private:
int m_data1;
int m_data4;
};
int main() {
A a;
B b;
C c;
b.func1();
b.func2();
c.func1();
c.func2();
return 0;
}
A::func1
B::func2
A::func1
C::func2
- derived 类继承了base类的函数调用权,所以可以通过derived对象调用base类的接口。
- derived 类的与base类的同名接口,derived会覆盖base类。
注意⚠️:C++继承都继承了哪些东西
- 数据
- 函数调用权。不是继承函数相关的内存
2. vptr vs. vtbl
base类有虚函数,base类就会有vptr,base类对象有vptr则derived类一定有vptr。
虚函数的继承方式
- dervied 类如果不override base类的虚函数,则直接继承base类的虚函数
- derived 类如果override base类的虚函数,则在虚表中替换base类的虚函数地址
- 多重继承中,虚函数不会“跳着”间接继承,而是继承自己的base类的虚函数。上图中c直接继承b的虚函数,而不是直接继承自a的虚函数。
继承关系下生成的函数
关注上图中的a/b/c 三个类中生成的所有的函数,分成虚函数,非虚函数两种类型。
vtbl
3. 静态绑定 vs. 动态绑定
静态绑定的函数调用方式
函数调用时,执行call xxx(地址)。
动态绑定的函数调用方式
通过指针找到对象的vtbl,然后找到正确的函数地址。
解释成代码形式如下:
(*(p->vptr)[n])(p)
(*(p->vptr)[n])(p)
中的n与虚函数的声明顺序一致
4. 多态的示例
多态解决的问题
1. 如何用一个只能容纳一种元素类型的容器,存储不同类型的元素?
容器保存的元素类型为,具有继承关系的base类指针,其指向对象为dervied类对象。
2. 如何让容器中的元素(base类指针),调用相同的接口却具有不同的行为?
接口为虚函数。
多态的条件= up-cast pointer+ virtual function
item2. 对象模型:this
1. this指针参与到多态中
- 利用多态机制,在base类的成员函数流程中实现通用的逻辑,base类提供通用的接口,通用接口的实现可以迟后由derived 类实现。
- 成员函数通常是通过对象调用(static成员函数除外),所以在调用函数时编译器会知道哪个对象在调用函数,此时当前对象的指针会传递给该成员this指针。
注意⚠️:
- 编译器在执行虚函数的调用(通过指针的方式)时,是通过指针指向内存单元的vptr找到vtbl中的函数地址,最后去调用地址中的函数。从实际的底层实现机制更容易解释多态语法。
item3. 对象模型:dynamic binding
1. 从汇编的角度解释dynamic binding
- 上图中
a.vfunc1()
的调用为static binding。 - static binding的汇编执行形式:
call xxxx
- dynamic binding 汇编执行等价于 c的形式
(*(p->vptr)[n])(p))
注意⚠️
用对象(非指针)的方式调用成员函数,不会造成多态。
item4. const
1. const member function
注意⚠️
const member function中const修饰成员函数的形式只能用在成员函数中,不能用在全局的函数中。理解:这里的const是用来修饰this指针的,全局函数没有this指针。
2. const obj vs. non-const obj vs. const member function vs. no-const member function
const member function中的const的作用,承诺该成员函数不会修改对象内容。本质是将this指针声明为const*const形式。
xx | const object | non-const object |
---|---|---|
const member function | v | v |
non-const member function | x | v |
在设计类接口时要考虑const
class String{
public:
print(){} // bad , non-const print
}
const String str("hello");
str.print(); // error,
建议:如果member function 不改变对象数据,应该将该member function 声明为const。否则const object 无法调用该接口。
在设计类的接口时就要确定要不要加const。
引申:函数(全局/class member function)的形参,如果不想改变实参,要将其声明为const &
成员函数的const 和non-const 版本共存时,调用谁?
class string{
charT operator[](size_type pos)const
{/*不考虑copy on write*/}
reference operator[](size_type pos)
{/* 必须考虑copy on write*/}
}
}
string a;
cout << a[1]; // 调用 non-const operator[]
// non-const operator[] ?
a[1] = 'a'; // 调用 non-const operator[]
// non-const operator[] ?
- const 作为函数签名的成分
- reference 返回值类型的函数可以作为左值。 因此上面代码中non-const版本中的operator[] 可以作为左值使用,需要考虑copy on write,但const 版本的operator[] 返回值类型为charT 智能是右值不必考虑copy on write。
- 函数设计要考虑是否会把函数作为左值使用。
注意⚠️
当成员函数的const和non-const版本同时存在,const object只会调用const版本,non-const object 只会调用non-const版本。
item5. new & delete
1. new /delete 表达式 vs. operator new /delete
- new /delete表达式 实现中会分解为多个步骤,其中包括调用operator new/delete。
注意⚠️:
- new /delete 表达式 不能被重载,operator new /delete 可以被重载。
- operator new /delete 是对内存的操作.operator delete不包含调用析构函数的动作,析构函数在delete 表达式中调用。
item6. 重载operatro new /delete
1. 重载全局operator new/delete
inline void* operator new(size_t size){
cout << "my new :" <<size<< endl;
return malloc(size);
}
inline void* operator new[](size_t size) {
cout << "my new [] : " << size<< endl;
return malloc(size);
}
inline void operator delete(void* ptr){
cout << "my delete" << endl;
free(ptr);
}
inline void operator delete[](void * ptr) {
cout << "my delete[]" << endl;
free(ptr);
}
- operator new 需要一个size参数
- operator new 不是由用户调用,由编译器在expression new中调用
- 注意返回值类型为void*
- operator new 只需指定 size
- operator delete需要指定地址和可选的size
2. 重载member operator new/delete
class Foo {
public:
void*operator new(size_t size) {
cout << "Foo new" << endl;
return malloc(size);
}
void operator delete(void* ptr){
cout << "Foo delete" << endl;
free(ptr);
}
};
int main() {
Foo* f = new Foo;
delete f;
return 0;
}
Foo new
Foo delete
类如果重载operator new/delete ,在调用expression new 创建类对象时,expression new中将调用类的operator new。
expression new /delete分解过程
3. 重载member operator new[]/delete []
class Foo {
public:
void*operator new[](size_t size) {
cout << "Foo new [] : " << size <<endl;
return malloc(size);
}
void operator delete[](void* ptr){
cout << "Foo delete[] : " << ptr << endl;
free(ptr);
}
};
expression new[] /delete[]分解过程
- 注意构造和析构的调用次数,operator new中传入的size值。
item7. 示例
class Foo {
public:
void* operator new(size_t size) {
cout << "Foo new" << endl;
return malloc(size);
}
void operator delete(void* ptr){
cout << "Foo delete" << endl;
free(ptr);
}
void*operator new[](size_t size) {
cout << "Foo new [] : " << size <<endl;
return malloc(size);
}
void operator delete[](void* ptr, size_t size){
cout << "Foo delete[] : " << ptr << endl;
free(ptr);
}
private:
int _id;
long _data;
string _str;
};
1. 如何使用全局的operator new / delete
Foo * p = ::new Foo;
::delete p
2. operator new 传入的size 大小
operator new[] 需要分配一个保存对象个数的内存单元。
item8. 重载 new(), delete()
class member operator new() - placement new
1. class member placement new
示例
Foo* pf = new(300,'c')Foo
- class member 可以重载 operator new,写出多个版本的operator new()
- 每个版本的声明必须具有独特的参数列,其中第一个参数必须是size_t,其余参数以new 所指定的placment arguments 为初值。
placement argument:new (…)小括号中的参数。size_t在声明时默认需要定义,调用处不用显示指定,size_t 不是 使用时(…)中的参数。
2. class member operator delete () - placement delete
绝不会被 expression delete 调用,只有当调用 placement new 之后调用的ctor抛出异常才会被调。
- class member operator delete () 不是必须定义的,如果定义要与placement new对应。
3. 示例
class Foo {
public:
Foo(){};
Foo(int a){
throw a;
};
void* operator new(size_t size, int extra){
return malloc(size+extra);
}
void operator delete(void* ptr, int extra){
cout << "placement delete called" << endl;
}
private:
int _id;
long _data;
string _str;
};
int main() {
Foo* a = new(1)Foo;
Foo* b = new(1)Foo(1);
return 0;
}
item9. basic_string使用new(extra)扩充申请量
1. why using placement new
如果想在new对象时创建额外超过对象大小的内存,使用placement new 代替 默认的new。