侯捷老师视频笔记----c++面向对象高级编程

以下记录是从侯捷老师视频的c++上的第11节视频开始。

复合

先从Compositon开始讲起,其实在C语言中也见到过,一个结构体里面包含另外一个结构体,在C++中复合也是一样的,侯捷老师以标准库来形象的说明了这种情况,在queue类中包含了deque类的某些方法。这张图很好的体现了queue是由deque实现的,就是把deque的部分方法给了queue(deque是两端可进可出的队列,而queue是一端进一端出)。
侯捷老师视频笔记----c++面向对象高级编程_第1张图片
2个类的关系在上图中,实心表示复合关系,那这2个对象怎么产生,先后顺序是什么?
侯捷老师视频笔记----c++面向对象高级编程_第2张图片

下面这张图表现了queue类中有一个deque类,deque类中又用了Itr结构体
侯捷老师视频笔记----c++面向对象高级编程_第3张图片

委托

接下来再谈谈委托(Delegation)关系,委托是利用指针包含另一个类,左边有一个右边,但是这个有只是一个指针,有点虚,所以是空心的
侯捷老师视频笔记----c++面向对象高级编程_第4张图片
string类只是对外的接口,真正的实现都在stringrep类里,当左边需要动作的时候,就去调用(委托)右边,被称为Pimpl(pointer to implementation),有一个指针去指向为我实现所有功能的类,这种手法的好处在于,这个指针还可以去指向不同的实现类,去实现不同的功能,右边不管怎么变动都不影响string类也就不影响客户端,string类也永远不用再编译。

上述代码的实现如左下图所示,abc3个string指向一个指针,称为引用计数(共享特性)有多个字符串指向同一个hello,这时候abc互相不知道大家都在用同一个内存,如果改变a就会影响其他,怎么做到不影响呢,当a想改变时,整个系统就copy一份给a,让a改,然后降低count,称为copy and write。

继承

public 继承表示is a,派生类是一种基类的东西,同样的继承了基类的数据。子类包含了父类,所以在构造子类之前要先构造父类,在子类的初始化列表上,同样的在析构子类的时候,也是先析构子类,再析构父类,和复合非常相似。
其中继承是:数据被继承下来占用了内存,函数是继承了其调用权
侯捷老师视频笔记----c++面向对象高级编程_第5张图片
一个类中成员函数分为3种:

non-virtual:不希望派生类对其进行重写(override).
virtual:希望派生类对其进行重写(override).
pure-virtual(纯虚函数):希望派生类一定要对其进行重写

继承搭配虚函数使用才是他最有用的地方
打开菜单,弹出对话框,查找或者选择文件名称,按下打开,程序收到名称校对文件名称,查找文件,打开文件读出来。读的内容不一样所以其他都可以事先写好。以下是一个例子:
比如下图,有很多常用的方法可以先写好,但是有某个方法不会写,就等到让他的子类取写,所以可以把他定义为虚函数,函数体为空,子类重新定义(覆写)他,如果设成纯虚函数,子类就一定要去重写他,这样要求太严苛,万一没写就出错了,所以还是设虚函数比较合适。

类中的成员函数都是以this为调用的,所以this->Serialize()在以CMyDoc的对象调用以后,传入的是myDoc的地址,最后调用了其函数,实现了具体实现

这种方法是一种设计模式Template Method,Template Method就是一种写好一个框架把可以设定的都设定好,然后再实现过程中以虚函数的方式让具体实现交由继承它的类,完成各种不同的功能。

复合和继承的情况

再看一下复合的关系,继承与复合同时存在时,在创建对象的时候,又该怎么进行构造呢
侯捷老师视频笔记----c++面向对象高级编程_第6张图片
下半个图很明显,我要构造派生类,我就要先构造父类,构造父类,我就要先构造其中包含的类。

而上面的图,当我们需要构造派生类时,肯定的是派生类是最后构造的,那么父类和复合类的先后顺序是什么样的呢,在不同的编译器上可能会有所不同。在WINDOWS下利用VS2019,实现以下的函数

#ifndef CONMPOSITON__INHERITANCE_H
#define CONMPOSITON__INHERITANCE_H
#include 
using namespace std;
class A{
public:
	A(){ cout << "A is make" << endl;}
	~A(){ cout << "~A is delete" << endl;}
};
class C{
public:
	C() { cout << "C is make" << endl;}
	~C(){ cout << "~C is delete" << endl;}
};
class B:public A{
public:
	B(){ cout << "B is make" << endl;}
	~B(){ cout << "~B is delete" << endl;}
private:
	C c;
};
 
 
#endif
int main() {
	B b;
	return EXIT_SUCCESS;
}

侯捷老师视频笔记----c++面向对象高级编程_第7张图片

我们发现父类先被构造,然后是复合类,然后才是子类,析构与之正好相反。

委托和继承的情况

比如像下面这种情况,以一个ppt的窗口为例,来讲解委托与继承的结合:要开好几个窗口,但是实际上数据只有一份,一个地方变了数据,其他也就跟着变化了。这是利用多个窗口来显示同一份数据,左边是4个窗口都显示一样的,右边是同一份数据利用不同的图形去表示。

要实现这样的功能就必须呈现数据和储存数据的class之间要具备关联性,如果数据有变换,这些窗口都需要变化。我们用subject去储存数据,用observer去观察数据,我们让subject可以拥有很多个observer(委托,指针实现),因为我们要开出很多个窗口去观察它。

下面这个图就实现了这种需求,左边一个类委托另一个类,去更新数据,然后这个类可以被继承,这时候委托的指针和之前讲的委托不一样的是,当时只有一个string的实现,现在传过去的是一个类,这个类可以不断的被继承,实现不同种类的功能(比如,让窗口显示不一样)。

class subject{
private:
    int m_value;
    vector<observer*> m_views;//一个容器包含多个窗口,就可以实现多个窗口
public:
    void attch(observer* obs){
        m_views.push_back(obs);//谁要使用就来注册,开辟窗口
    }
    void set_value(const int& value){
        m_value = value;
        notify();   //每次改变数据都去更新
    }
    void notify(){
        for(int i = 0;i<m_views.size();i++)
        {
            m_views[i]->update(this,m_value);
        }
    }
};
class observer{
public:
    virtual void update(suject& sub,int value);//一个父类,让每个子类窗口的表现都可以不一样
};

再来看看composite组合模式下,是怎么实现委托的,如果现在面对一个问题,file system,有目录,目录里可以放文件,目录还能和其他的文件结合在一起放在另外的目录里面,这时候需要写个个体,需要一个组合物,这个组合物应该可以容纳组合物和个体,那么如何做到,一个容器容纳2个不同的东西,除非他们是is a拥有父类,这样其实就是属于同一种东西,只是不同的表现罢了,所以有如下的关系


class Component{
private:
    int value;
public:
    virtual void add(Component*){}//如果设计为纯虚函数,那就不行了,Primitive不应该去定义
};

class Primitive:public Component{
public:
    Primitive(int value):Component(value){}
};
class Composite:public Component{
private:
    vector<Component*> c;  //定义一个容器,这个容器存放Component*,就是委托关系
public:
    Composite(int value):Component(value){}
    virtual void add(Component*){
        c.push_back(Component);
    }

如果现在有个题目是我需要一个继承体系,需要去创建未来的子类,是被客户买去以后才会出现,客户要用到这个类(框架)的数据,不能在父类里面new,子类还未出现,客户自己创建子类,能父类类看得到。所以有了prototype设计模式。客户在子类里创造了一个静态的对象,自己创造自己,把这个原型添加到父类里,父类就可以看到了。
侯捷老师视频笔记----c++面向对象高级编程_第8张图片
-号代表私有,#号代表protect,+(一般不写)表示Public
子类自己通过Static 创建静态对象,在私有的构造函数里,把这个this传递给父类,父类通过将addPrototype(Image*)设置为静态函数,让所有的类都可以调用它(不用写类名),就可以把自己添加到父类中的数组里,父类自己创建一个纯虚函数clone()函数,要子类去实现。而且父类创建一个方法来检测传回的那个原型,要去调用clone()函数。子类会通过刚刚传回父类的原型,去重写clone()函数来创建副本,并且设置一个保护的构造函数,使用参数来和之前的私有构造函数做区分(不能调用之前的私有构造函数,因为这样又会返回给父类原型)(通过之前在父类里的原型调用clone()函数,如果没有的话就不行了,可能会想到clone()为静态函数,可以通过类名调用,但是这个类名是未来的,还不知道class name 所以不可以。)

首先父类如下所示:

enum imageType{
	LSAT,SPOT;
}
class Image{
private:
	static Image* _prototypes[10];//储存各个子类传递来的原型
	static int _nextSlot;//标记原型个数
protected:
	static void addPrototype(Image*){
		_prototypes[_nextSlot++] = Image;//给所有的子类通用的函数接口,可以把自己添加到静态数组里面去
	}
	virtual Image* clone() = 0;
	virtual imageType returnType() = 0;//在子类中重载,返回各个子类特有的标识符
public:
	virtual void draw() = 0;
	static Image* findandclone(imageType);//找到该类型的函数原型,并且调用子类中重载的clone()进行创建副本返回。
};

定义式:

static Image* _prototypes[];
static int _nextSlot;
Image* Image::findandclone(imageType type){
    for(int i=0;i<_nextSlot;i++){
        if(_prototypes[i]->returnType() == Type)
            return _prototypes[i]->clone();
    }
}

接下来以其中一个子类为例,编写程序:

class LandSatImage:public Image{
public:
    virtual imageType returnType{
        return LSAT;
    }
    void draw(){
        cout << "LandSatImage::draw" << endl;
    }
    Image* clone(){
        return new LandSatImage(1);//返回的是指针所以new
        //类会通过刚刚传回父类的原型,去重写clone()函数来创建副本,并且设置一 
        //个保护的构造函数,使用参数来和之前的私有构造函数做区分(不能调用之前
        //的私有构造函数,因为这样又会返回给父类原型)
    }
protected:
    LandSatImage(int){
        id=count++;
    }
private:
    static LandSatImage landsatimage;//就是下面的this 自己的静态变量,交给父类的原型
    LandSatImage(){
        addPrototype(this);  //把原型添加
    }
    int id;
    static int count;
};
int LandSatImage::count = 1;
LandSatImage LandSatImage::landsatimage;

设计模式

Adapter 适配器模式
pImpl(pointer to implementation) 代理模式
Template method 模板方法模式
composite 组合模式
prototype 原型模式
详细介绍看这篇博文
https://blog.csdn.net/CoderAldrich/article/details/83272866

转换函数

侯捷老师视频笔记----c++面向对象高级编程_第9张图片
看上图 4+f,有没有去定义一个double+fraction的+号运算符,没有,再看程序可以将f转换为double(相当于实现强制类型转换,把当前的Fraction转换为别的,比如double),从而实现加法。
侯捷老师视频笔记----c++面向对象高级编程_第10张图片
这是没有explicit的只要一个参数就可以
这里看上图 f+4,定义一个 fraction + fraction 的+号运算符,但是4不是fraction 啊,不过可以看到,4可以看做一个参数,就可以调用转换构造函数,把4转成f(4,1),就是把4转换成fraction了, 从而实现加法。

侯捷老师视频笔记----c++面向对象高级编程_第11张图片
这个时候两条路都走得通,编译器就不知道走哪条了。
4能转换为Fraction,这条路就通了
也可以将f转化为double,double+4=double,然后再把double转化为fraction也是通的。
侯捷老师视频笔记----c++面向对象高级编程_第12张图片
explicit就是限制就为当前的构造函数,不能将其他的转换为构造函数

  1. 4不能转换为Fraction,然后就失败了
  2. 将 f 转化为double,double+4=double,但是不能把double转化为fraction,所以都失败了

但是double d = 4+f 是可以的,f 可以转换为double

再看一个例子
一个容器类,因为要取到下标,所以对 [ ] 做重载,返回的是一个reference,而reference是用了另一个结构体定义的,所以再看这个结构体,是做了bool()类型转换的。
侯捷老师视频笔记----c++面向对象高级编程_第13张图片

pointer-like classes

也就是智能指针,智能指针其实就是一个class(类),要让这个类像一个指针,里面一定会有一个指针去模拟指针,指针有 * 和 -> 操作符,对于一个class去模拟point,我们必须对其 * 和 -> 操作符进行重载。那指针又被封装在智能指针里,在智能指针里比较有用的是能够把封装在里面的指针使用析构函数进行资源管理。*

侯捷老师视频笔记----c++面向对象高级编程_第14张图片
另一个比较复杂的像指针的class就是迭代器了
这里也重载了很多操作符,因为迭代器就是要遍历,所以要仿造指针的移动(++ --),最关键的还是 * 以及 -> ,星号取出了这个node指针的data数据,->则是利用了 * 操作符重新取回原地址来实现的。
侯捷老师视频笔记----c++面向对象高级编程_第15张图片
侯捷老师视频笔记----c++面向对象高级编程_第16张图片

function-like classes

接下来看看仿函数,仿函数其实就是在class重载了运算符()


仿函数都会继承2种类型,unary_function(1个操作数),binary_function(2个操作数)。实际是没有大小的,但是用sizeof输出可能为0.
侯捷老师视频笔记----c++面向对象高级编程_第17张图片

成员模板

成员模板就是在一个模板类中再使用模板,以下图右下角代码为例,当外面的类型T被指定时,类里面的类型U还是可变的。如左下角,可以用鲸鱼,麻雀去初始化鱼类和鸟类,但是反过来不行。侯捷老师视频笔记----c++面向对象高级编程_第18张图片
下面又是一个成员模板的例子,智能指针的构造函数也是写成了成员模板,这样就可以直接以类型实现拷贝构造了。就是 _Tp1 可以是shared_ptr
侯捷老师视频笔记----c++面向对象高级编程_第19张图片

模板的特化

模板的特化:

template <class Key>
struct hash{};//泛化


template<>//特化
struct hash<char>{
	size_t operator() (char x) const { return x;}
};
template<>
struct hash<int>{
	size_t operator() (int x) const { return x;}
};
template<>
struct hash<long>{
	size_t operator() (long x) const { return x;}
};

如果我们指定了任意的类型,都会去泛化里面,如果这个类型是char,int,long被我们特化过的,就选择特化的版本。
如 cout << hash()(1000)<< endl; hash()表示一个临时对象。

模板的偏特化

partial specialization

1、个数上的偏
一个的情况

template<typename T,typename ALLoc=...>
class vector{
 
};
template<typename ALLoc=...>
class vector<bool,ALLoc>
//偏特化,T被绑定为了bool,ALLOC还未定,但是这里要写出来了
{
 
};

多个参数也是一样,但是不能跳着绑定参数,不能是1,3,5,只能是从左边开始一个一个来。

补充:
这里用bool类型是为了节省空间,因为char是一个字节8位,表示0或1就浪费了另外7位,所以要创建一个bool

在C语言中,并没有bool类型,如果出现bool类型,那么就是自定义类型,根据定义方式,可能占用不同空间,可以通过输出sizeof(bool)的方式确定。

在C++中是存在bool系统类型的,同样可以通过输出sizeof(bool)的方式,来确定其占用字节数。在C++中,sizeof(bool)的值为1,所以该类型是占用1字节的。
之所以不实现为一位,是因为计算机存取的最小空间是按字节计算的,在独立的单个bool型变量下,只占一位空间并没有实际意义。

当出现一组bool类型时,通过结构体的位域概念,可以实现单独占用一位。

struct TEST
{
bool a:1;
bool b:1;
bool c:1;
bool d:1;
bool e:1;
bool f:1;
bool g:1;
bool h:1;
};
在这个例子中,定义了8个bool型成员在一个结构体TEST中,成员变量名后的:1代表该成员仅占用1位,这样整个结构体共占用一个字节。

使用位域定义时,bool变量的赋值,引用等操作,与普通bool变量并没有区别,但成员变量不支持取地址操作,这是因为,地址的单位同样是字节,对于位为单位的成员,取字节为单位的地址是没有意义的。于是C/C++在编译的时候就禁止了该操作。

2、范围上的偏

侯捷老师视频笔记----c++面向对象高级编程_第20张图片
指针的范围比typename要小,所以进行了一部分特化,如果说传入的是普通类型就进行泛化版本,如果是指针类型,就进行下面的版本。

C<string> obj1;  //进行泛化版本,用上面部分的代码
C<string*> obj2; //指针类型,就进行下面的版本

模板模板参数

模板模板参数,在模板里面再加入一个模板作为其参数。
在模板里面的参数早先为class,后来出现了typename所以在模板参数里面typename和class共通。
侯捷老师视频笔记----c++面向对象高级编程_第21张图片
侯捷老师视频笔记----c++面向对象高级编程_第22张图片
看下图,这不是模板模板参数,和之前的写法不太一样,这里不是模板模板参数,如果写出的话,必须写出其实例了,没有模糊替代了,list已经写死了。之前一个是模糊的,所以才是一个模板
侯捷老师视频笔记----c++面向对象高级编程_第23张图片

Variadic templates

模板参数的个数可变化,分为一个和后部分一个包,用…表示,以递归的形式进行,让一个包递归,这个包的第一个参数就会输出,… 指定包的个数为0,就会调用void print()函数,所以一定有一个这样的空函数。

…在这里表示一个pack(包),这里的代码就是打印出输入的参数,最后参数为0的时候,为了防止程序错误,给出一个空参数的print。
如果想知道这一个包有几个,可以使用sizeof…(args);

auto

auto根据其后面的变量返回值编译器来确定类型,为了方便程序员使用。但是不要过于依赖这个关键词,我们要能够写出其真正的类型。因为编译器推类型也是费时间的,而且有的时候我们定义一个变量不一定赋值给他,而auto ite 是一定要赋值的。
侯捷老师视频笔记----c++面向对象高级编程_第24张图片

ranged-based for

for的新形式
decl是一个变量,coll表示容器,编译器从coll这个容器里抓出变量赋值给decl。

在以前可以使用foreach来遍历容器,可以使用迭代器来遍历容器。现在c++11增加了这一种新的方式。

这里是pass by value,如果在里面修改i的大小,也不会改变原来容器里变量的大小。如果需要修改的话,需要pass by reference。 reference,其实内部就是由指针实现的。只是表现的形式不同。
侯捷老师视频笔记----c++面向对象高级编程_第25张图片

reference

(a)从内存的角度去看reference

使用reference,直接读这个r的时候,编译器会制造一种假象就是,r就是x,在每一次去调用r的时候,会直接近似的用x来代替,一旦确定引用,也就是代表的东西以后,是无法再次修改其代表的变量的。虽然reference底层是用point实现的,但是呈现在我们眼前的是不一样的,point这个内存单元里面的值是x的地址,而我们直接去读reference内存单元里面的值,编译器会直接给我们一个其代表的对象。

侯捷老师视频笔记----c++面向对象高级编程_第26张图片

一个测试程序来表现这个现象。

typedef struct Stag{	int a,b,c,d;  }S;
int main(){
	double x = 0;
	double* p = &x; //P指向x,p的值现在是x的地址
	double& r = x;  //r代表x,现在r,x都是0
 
	cout << sizeof(x) << endl;  //8
	cout << sizeof(p) << endl;  //4  32位电脑下
	cout << sizeof(r) << endl;  //8
	cout <<  p << endl;         //0065FDFC
	cout << *p << endl;         //0
	cout <<  x << endl;         //0
	cout <<  r << endl;         //0
	cout << &x << endl;         //0065FDFC
	cout << &r << endl;         //0065FDFC
  
	S s;
	S& rs = s;
	cout << sizeof(s)  << endl; //16
	cout << sizeof(rs) << endl; //16
	cout <<  &s << endl;        //0065FDE8
	cout << &rs << endl;        //0065FDE8
}

(b)reference的常见用途

我们在写代码的时候很少这样用reference去代表一个变量,我们一般把reference用在参数传递上,主要还是为了让reference更像传递进来了一个对象,而不是一个指针,在写法上,其底层是表示一个指针。

如下面的方式:
侯捷老师视频笔记----c++面向对象高级编程_第27张图片
从图中可以看出,传指针的话,访问参数的形式不一样,调用函数接口也不一样,而引用就和普通的变量一样使用,所以说让引用变得更像一个对象。

(c ) 函数签名

如果是2个相同的函数(签名),如果一个使用reference,一个是直接传对象。不能同时存在,编译器无法知道你想调用谁,有歧义(Ambiguity)。

函数签名就是除去返回类型剩下来的部分,const也是其中一部分。如果2个函数完全一样,一个有const一个没有,可以同时并存,一个const的对象调用时,会去调用const函数。

object Model(对象模型):关于vptr和vtbl

这里设置了3个class,A,B,C之间是继承的关系,A有之间的data1和2,B继承了A,有了A的数据,然后加上自己的数据,C也是。

如果一个类里面有虚函数,这个对象里面就会多了一个指针,称为虚指针,指向虚函数表,那有多少个虚函数,虚函数表里面就会有多少个指针指向虚函数

如果父类有虚函数,子类一定有,会继承父类虚函数的调用权,这时候以B为例子,B推翻了vfunc1()重载了,留下了vfunc2().

vptr关联了vtbl(里面都是函数指针)然后关联了虚函数。

通过new c可以得到p,通过p要调用vfunc1(),因为vfunc1是虚函数,所以编译器不能通过老式的call一个地址(静态绑定),就只能通过动态绑定去寻找这个函数,如

(* (p-vptr)[n]) (p );

(* p->vptr[n]) (p );

n表示表的第几个。
侯捷老师视频笔记----c++面向对象高级编程_第28张图片
如果想设计一个能够画出各个图形的程序,首先为了让一个容器可以容纳各个对象,每个对象的大小肯定不一样,所以不好放入容器,只能通过指针指向同类,这个指针必须是父类,因为所有的东西都是它的一种。我们通过这个指针去调用draw,各自的指针就会调用自己的重写的虚函数去画自己,如果没有重写,就会默认用父类的

以前我们会对各种类型使用enum来判断这是什么图形,然后通过转接表的编号判断去调用哪个函数,在C语言中如果我现在多了几个新的图形,又得重新修改代码,现在在C++中就不用了。

多态的解释:
比如上面的一个父类被子类继承,子类可以重写父类的虚函数,使自己有自己的心态,父类又可以访问到他,如 A* p = new C; new 后面不一样,就让父类有的不一样的形态,从而实现多态。

符合下列3个条件(动态绑定):
1、是指针
2、向上转型
3、调用的是虚函数
动态绑定就是要看指针指向哪一个块,然后通过虚指针找到虚表,取出第n个,把他当成指针去调用相应的虚函数。

Object Model(对象模型):this

其实每个类里面都会有一个默认的this指针,子类创建一个对象myDoc,这个对象调用父类的方法,&myDoc就相当于OnfileOpen函数里面的this指针了,再通过这个this指针找到被子类重写的virtual Aerialize 函数。

浅谈const

1.const 加参数前面

void Show(const int&, const int&);
void Show(const int& num1, const int& num2) {
	//我们希望显示函数中,只能实现传入参数的显示功能
	//而禁止显示函数修改num1和num2的值(禁止函数内部修改引用参数值)
	//解决方案:在参数中使用const
	cout << num1 << "," << num2 << endl;
	//num1 = 998; 报错,禁止修改
}
  1. const 加在函数前面
sum(num1) = 55//只有在C++中才可以这样赋值,函数在左边
以下是例子:

int& sum(int& num1) {
	num1++;
	return num1;
}

int main() {
	int num1 = 10;
	int result1 = sum(num1);
	cout << result1 << endl;  //11

	int& result2 = sum(num1);
	//sum()是一个返回值的引用,又是result的引用,所以result和sum(num1)等价,
	//令sum(num1)=55,也就是令result=55;
	sum(num1) = 55;  //只有在C++中才可以这样赋值,函数在左边
	cout << result2 << endl;  //55
}

建议解决方案:
1.将返回值类型修改为const int&
const int& sum(int& num1){…}
2.const类型为不可以修改的左值,sum(num1)=55将不合法;
3.省略const会使函数的含义更加模糊,建议避免在设计函数中存
在模糊的情况,因为模糊会增加犯错误的机会,应尽量避免犯错

  1. const 加在函数后面,说明这个成员函数不能修改类的成员变量,只做读操作。


这张图说明 const对象调用const函数,const对象不能调用non-const函数,
non-const对象可以调用non-const函数,也可以调用const函数,但是当non-const函数和const函数都存在时,non-const对象调用non-const函数,const对象调用const函数。下面这个代码也说明了这个。

class AA {
public:
	void eating(string food) const {
		cout << "吃" << food << '1' << endl;
	}

	void eating(string& food) {
		cout << "吃" << food << '2' << endl;
	}
};


int main() {
	//调用:
	string food = "佛跳墙";
	const AA a;
	AA b;
	a.eating(food);   //输出  吃佛跳墙1
	b.eating(food);   //加入了const可以区分	输出  吃佛跳墙2		  
}

Object Model(对象模型):开放Dynamic Binding

是不是动态绑定就看是否符合下列3个条件:
1、是指针
2、向上转型
3、调用的是虚函数
动态绑定就是要看指针指向哪一个块,然后通过虚指针找到虚表,取出第n个,把他当成指针去调用相应的虚函数。

下面这个图,a是一个对象,不是指针,所以是静态绑定

下图的pa是指针,又向上转型,又是调用虚函数,所以是动态绑定
指针指向B,然后通过虚指针找到虚表,取出第n个,把他当成指针去调用相应的虚函数。

new和delete

在上次new和delete的讲解中已经说明了new和delete运作的过程和内存中其运行过程,这里补充的是重载::operator new,::operator delete以及[]的形式,
不过一旦重载了这2个操作符,影响是无穷无尽的,谨慎。
这张图是重载这四个操作符的接口函数
侯捷老师视频笔记----c++面向对象高级编程_第29张图片
接下来看一下成员函数的new和delete重载
侯捷老师视频笔记----c++面向对象高级编程_第30张图片
在成员函数里,写法和原来全局的基本类似,在创建Foo指针p的时候,先创建一个临时指针并且开辟一块空间给它,再将整个临时指针强制转换为p的类型,同时赋值给p,再进行p的对象的构造。
如果是delete的话,这时候先对整个对象进行析构,再释放这块内存。

侯捷老师视频笔记----c++面向对象高级编程_第31张图片
如果是new[]和delete[]的话,传入的大小需要改变为sizeof(foo)*n,当然在这块内存里面,我们需要进行n个对象的构造。进行析构的时候也是一样,析构n个,然后释放总的内存

下面是一个具体的例子,同样的是创建一个p指向new的Foo对象,如果没有member operator new和delete就使用全局的。

如果想直接采用全局的new就需要使用下面的写法

Foo* pf = ::new Foo;
::delete pf;

侯捷老师视频笔记----c++面向对象高级编程_第32张图片
在上面对于成员操作符new和delete进行重载的时候,为了防止出错,首先先分配了内存,保证最基本的实现。
对于上面,int 是 4个字节,long也是四个字节(32位机),string实际是指针,所以是4个字节,一共12个字节
如果对于一个对象new分配的大小那么是多少呢,如果是new就是简单的创建一个大小,如果有虚函数,会加上一个虚指针的大小。所以看到最上面输出12,如果里面有虚函数,就会增加4个字节,所以下面输出16.

对于new Foo [5],会增加4个字节用来存放这个5,也称为counter(计数器),所以125+4=64,输出64,下面是165+4=84,输出84

对于全局操作符,也是类似的,counter还是有,但不会进入刚刚那些重载的函数

重载new( ) , delete( )

placement new
之前是 new foo() ,现在是new() foo;每个版本有不同的参数且第一个参数时size_t,要给大小

但是这时候delete也可以重载,也可以重载class member operator delete(),写出多个版本。但是它们绝对不会被delete调用,只有当new所调用的构造函数抛出异常的时候,才会调用这些重载的operator delete().它只可能这样被调用,主要为了归还未能完全创建成功的object所占用的memory.(因为new的过程是分配内存,转型,再调用构造函数,而此时抛出异常了,就会失败,要避免此时的内存泄漏)
侯捷老师视频笔记----c++面向对象高级编程_第33张图片
下面这两幅图是一一对应的new(),delete()


测试所有的new,在foo(1)的时候抛出异常了,应该调用对应的重载的delete()。

标准库里string类使用placement new实现reference counting,这样是为了可以悄无声息地多增加一些空间。
侯捷老师视频笔记----c++面向对象高级编程_第34张图片

你可能感兴趣的:(c++)