一、虚指正(vptr)和虚表(vtbl)
我们以下图介绍上述两者:
1、当类中存在虚函数就会出现虚指针vpt,无论虚函数有多少个,有且仅有一个虚函数,指向虚表(rvtbl)的地址;
2、虚表是什么呢??
我们可以将它理解为一种表格,每个表格的位置存放一个虚函数对应内存的地址;
例如:基类A中包含两个虚函数vfunc1()、vfunc2(),那么类A的对象在在内存中表现如上图a(A object),其存储类的两个基本数据:m_data1、2m_data2以及两个虚函数对应的虚指针vptr,而虚指针指向虚表的地址,虚表存放虚函数内存的两个地址:0x401ED0、0x401FD0;同理,A类的子类B,B类的子类C也有类似的原理;
将vptr实现vtbl内容翻译为C:
(*p->vptr)n;
(* p->vptr[n])(p);
3、动态绑定: 虚机制
动态绑定(dynamic binding):动态绑定是指在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
C++中,通过基类的引用或指针调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的;
C++中动态绑定条件发生需要满足2个条件:
(1)只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不能进行动态绑定
(2)必须通过基类类型的引用或指针进行函数调用
所谓的动态类型,当引用或指针调用了虚函数时,它就是动态类型,它的行为要到程序运行时才能定义 ;
当我们用派生类去初始化基类的引用或指针后,假如调用的是非虚函数,那么这时实际调用的函数是基类的函数;假如调用的是虚函数,那么这是调用的是派生类自己定义的虚函数 下面是具体的例子来说明静态类型和动态类型
class A{
public:
virtual void show(){cout<<"j基类的show()"<
void get(){cout<<"基类的get()"<
};
class B:public A{
public:
virtual void show(){cout<<“派生类的show()”<
void get(){cout<<"派生类的get()"<
};
main:
A a;
B b;
A &c=b;
c.show();//show函数是虚函数,并且此时使用派生类的对象去初始化基类的引用,发生了动态绑定,调用的是实际类 型B的show()----"派生类的show"
c.get();//此时不满足动态绑定的条件,c是静态类型,结果是-------基类的get()
二、this指针
1、C++this指针,一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。this指针是类的一个自动生成、自动隐藏的私有成员,它存在于类的非静态成员函数中,指向被调用函数所在的对象。全局仅有一个this指针,当一个对象被创建时,this指针就存放指向对象数据的首地址;
注:简单点说,通过一个对象调用函数时,函数的地址就是this;
举例:
父类CDocument
子类CMyDoc,子类对虚函数Serialise()进行了重新定义;
通过语句myDoc.OnFileOpen()调用父类函数时,子类对象myDoc的地址即为this,上述语句可表述为:CDocument::OnFileOpen(&myDoc),&myDoc就是this,this调用虚函数,进行动态绑定,通过this->Serialize()调用了子类的虚函数,而不是父类的虚函数,this->Serialize()也可以表达为虚指针、虚表的形式,即:*(this->vptr)[n](this),这样我们就可以更好的理解this以及虚机制;
三.动态绑定
再第一章节已介绍过动态绑定,这里就不再赘述;
四、const
课件中已做了详细的介绍,我这里简单总结下,并做些衍生:
1、常数对象可以调用常函数;
非常数对象可以调用常函数;
常数对象不可以调用非常函数;
非常数对象可以调用非常函数;
注:当成员函数的常数版本和非常版本同时存在时(以函数重载形式出现),常数对象只可以调用常函数;非常数对象只可以调用非常函数。
2、注意的几点:
1)const一般放在成员函数后头,不放在全局函数后头, 例:void function() const { return data;} ;
2)在成员函数后面加const是属于签名, 就是当两个成员函数传参相同,那么加不加const也会被区分成两个函数. ;
3、const的其他使用方法:
1)定义常量
(1)const修饰变量,以下两种定义形式在本质上是一样的。
它的含义是:const修饰的类型为TYPE的变量value是不可变的。
TYPE const ValueName = value;
const TYPE ValueName = value;
(2)将const改为外部连接,作用于扩大至全局,编译时会分配内存,并且可以不进行初始化,仅仅作为声明,编译器认为在程序其他地方进行了定义.
extend const int ValueName = value;
2)指针使用CONST
(1)指针本身是常量不可变
char* const pContent;
(2)指针所指向的内容是常量不可变
const char *pContent;
(3)两者都不可变
const char* const pContent;
(4)还有其中区别方法,沿着*号划一条线: 如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量; 如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。
五、关于New,Delete
new对象的流程不能更改,但是实现过程中的函数可以被更改.
operator new
operator delete
array new一定要array delete;
回忆前边的内容:delete 某个对象,其实质是先调用析构函数,再释放内存
六、重载::operator new, ::operator new[],::operator delete ,::operator delete[]
在全局当中:
Note: 如果你重载了全局的操作符, 所以要额外小心.
这些重载不可以被声明在一个namespace中.
//这里的函数是编译器去调用, 所以size是编译器给出.
void* operator new( size_t size )
{ return malloc(size);}
void* operator new[]( size_t size )
{ return malloc(size);}
void* operator delete(void* ptr )
{ free(ptr);}
void* operator delete[](void* ptr )
{ free(ptr);}
重载 member new , delete
在class里面重载new, delete
class foo{
public:
void* operator new(size_t size);
void operator delete(void *, size_t size); //size为可选
…….
};
那么你在:
foo *a = new foo;
delete a;
就会调用上面重载的函数.
new[] , delete[] 也如此.
七、实例
当类中重载了new , delete , 而又想调用全局的new , delete
可以这样写:
::delete a;
string类内其实是一个指针.
当创建一个数组的时候, 内存当中就会多分配一个指针,该指针用于保存当前数组个数.
八、重载new(),delete()示例
允许重载成员函数new(….) 其中参数中,必须有第一个且第一个必须是size_t size. 其余参数以new所指定的placement argument为初值.
Foo* p = new(300,’c’)Foo; //这里是三个参数
我们也可以重载类成员函数 operator delete() ,写出多个版本. 但他们绝不会被 通常所使用的delete调用.只有当new所调用的ctor抛出 异常,才会调用这些重载版的operator delete(). 它们只能这样被调用,主要用来归还未能完全创建成功的对象所占用的内存.
九、Basic_String使用new(extra)扩充申请量
Basic_String在重载new()过后,传递了一个extra参数, 用于后台自动多申请extra空间。