C++程序设计2(侯捷video all)(转换函数、explicit、智能指针、成员模板、特化、偏特化、范围偏特化、模板的模板参数、不定模板参数、auto、2.0for循环、对象模型、多态、动态绑定、静态绑定、const、new delete、)
一、转换函数Conversion function(video2)
一个类型的对象,使用转换函数可以转换为另一种类型的对象。
例如一个分数,理应该可以转换为一个double数,我们用以下转换函数来实现:
class Fraction { public: //构造函数,输入分子和分母 Fraction(int num, int den = 1) :m_num(num), m_den(den) {} //转换函数,不需要返回值,因为定义的就是转为double的函数,返回值是固定的 //因为该函数并未修改m_num和m_den的值,所以加上const operator double() const { //这里需要先把m_num和m_den转换为double,否则除法的结果是一个整数 return (double)m_num / (double)m_den; } private: int m_num; int m_den; };
调用:
Fraction f(10, 3); double dvi = 4.5 + f; cout << dvi << endl;
转换后的类型不一定要是基本类型,只要是编译器编译到这里的时候认得的类型都可以,例如string或自己定义的类型。
二、单参数构造函数(video3)
class Fraction { public: //该构造函数的den参数有默认值,这就形成了单参数构造函数 Fraction(int num, int den = 1) :m_num(num), m_den(den) {} Fraction operator +(const Fraction& f) { //完成Fraction之间的加法,并返回 return Fraction(this->m_num*f.m_den + this->m_den*f.m_num, this->m_den*f.m_den); } int get_num() const { return m_num; } int get_den() const { return m_den; } private: int m_num; int m_den; }; inline ostream& operator<<(ostream& os,const Fraction& f) { os << f.get_num(); os << "/"; os << f.get_den(); return os; }
调用:
Fraction f(10, 3); //当Fraction对象加一个int时,编译器会自动将4转换为4/1的Fraction对象 Fraction dvi = f + 4 ; cout << dvi << endl; //输出3/22
三、单参数构造函数搭配转换函数的问题(报错)(video3)
class Fraction { public: //该构造函数的den参数有默认值,这就形成了单参数构造函数 Fraction(int num, int den = 1) :m_num(num), m_den(den) {} //Fraction转double的转换函数 operator double() const { return (double)m_num / (double)m_den; } Fraction operator +(const Fraction& f) { //完成Fraction之间的加法,并返回 return Fraction(this->m_num*f.m_den + this->m_den*f.m_num, this->m_den*f.m_den); } int get_num() const { return m_num; } int get_den() const { return m_den; } private: int m_num; int m_den; }; inline ostream& operator<<(ostream& os,const Fraction& f) { os << f.get_num(); os << "/"; os << f.get_den(); return os; }
此时,Fraction类的定义中,同时存在单参数构造函数和转换函数。当调用以下代码时(报错):
Fraction f(10, 3); //当Fraction对象加一个int时,编译器会自动将4转换为4/1的Fraction对象 Fraction dvi = f + 4 ; cout << dvi << endl;
Fraction dvi = f + 4;会报错。因为编译器认为,可以把f转换为double,也可以把4转换为Fraction。当把4转换为Fraction时,这条路能走通。但是把f转换为double时,做完加法后,无法将得到的double类型的dvi转换为Fraction,所以报错。
同样的,这样调用也会报错,与上面的过程相反。
Fraction f(10, 3); //当Fraction对象加一个int时,编译器会自动将4转换为4/1的Fraction对象 double dvi = f + 4 ; cout << dvi << endl;
四、使用explicit关键字(video3)
explicit关键字意为“明确的”。很大几率用在构造函数前面,指明该构造函数只做构造函数使用,让编译器不要自动去调用它。
class Fraction { public: //该构造函数的den参数有默认值,这就形成了单参数构造函数 explicit Fraction(int num, int den = 1) :m_num(num), m_den(den) {} //Fraction转double的转换函数 operator double() const { return (double)m_num / (double)m_den; } Fraction operator +(const Fraction& f) { //完成Fraction之间的加法,并返回 return Fraction(this->m_num*f.m_den + this->m_den*f.m_num, this->m_den*f.m_den); } int get_num() const { return m_num; } int get_den() const { return m_den; } private: int m_num; int m_den; }; inline ostream& operator<<(ostream& os,const Fraction& f) { os << f.get_num(); os << "/"; os << f.get_den(); return os; }
在构造函数前使用了explicit关键字,也就相当于把前面例子里编译器面对的两条路减少为一条,这就没有冲突了。程序就能正确运行,但只能将f转换为double进行运算。
Fraction f(10, 3); //当Fraction对象加一个int时,编译器会自动将4转换为4/1的Fraction对象 double dvi = f + 4 ; cout << dvi << endl; //输出7.3333
总结:explicit很少使用,90%的几率都使用在构造函数之前,用作控制编译器的自动行为,减少从中带来的莫名错误。
五、Pointer-like类(关于智能指针)(video4)
Pointer-like class 即做出来的对象,像一个指针。例如智能指针就是其中一种。在这种类中,一定包含一个普通的指针。
template<class T> class shared_ptr { public: T& operator*() const { return *px; } T* operator->() const { return px; } //构造函数 shared_ptr(T* p):px(p) {} private: T* px; }; //测试类,将其对象指针包装成shared_ptr智能指针 class Test { public: void mytest() { printf("testing...\n"); } };
调用:
Test a; shared_ptr<Test> ptr(&a); ptr->mytest();
注意T& operator*() const;和T* operator->() const;两个操作符重载函数,其中“*”的重载函数很好理解,就是取指针指向地址的数据。但是“->”符号的重载不太好理解,这是因为在C++中,->这个符号比较特殊,这个符号除了第一次作用在shared_ptr对象上,返回原始指针px,还会继续作用在px上,用来调用函数mytest。但好在重载这个符号,基本就是这种固定写法。
六、Pointer-like类(关于迭代器)(video4)
迭代器类型的Pointer-like和智能指针的Pointer-like有一定的区别。
作为迭代器,他将一个对象(结构体或类产生的对象)的指针包装为一个迭代器(类指针对象),例如链表的某个节点指针,这样,在他的内部重载多个操作符,比如“*”、“->”、“++”、“--”、“==”、“!=”等,分别对应链表节点之间的取值、调用、右移、左移、判断等于、判断不等于。
链表的基本结构图:
迭代器中“*”和“->”的重载:
图中所示,迭代器中的“*”,返回的是某个node中的data,而不是这个node(这些实现都是根据用户需求来实现)。
迭代器中的“->”返回的是data的指针,因为operator*()就是调用“*”的重载函数,返回data数据,前面再使用“&”来取地址,则就是data的指针。从而可以达到使用“->”来完成data->func_name()的目的。
举一反三,其他的符号重载也是根据用户需求来做相应的操作。
七、Function-like类(video5)
像函数一样的类,意思就是将一个类定义为可以接受“()”这个操作符的东西。
能接受“()”的东西,就是一个函数或仿函数。
template<class T> class Identity { public: //重载()操作符,使该类衍生的对象可以像函数一样调用 T& operator()(T& x) { return x; } }; class Test { public: void mytest() { printf("testing...\n"); } };
调用:
int a1 = 5; Identity<int> iden_i; int a2 = iden_i(a1); cout << a2 << endl; Test t1; Identity<Test> iden_t; Test t2 = iden_t(t1); t2.mytest();
八、成员模板(video7)
template<class T1,class T2> class Pair { public: T1 first; T2 second; //普通构造 Pair() :first(T1()), second(T2()) {} Pair(const T1& a,const T2& b):first(a),second(b){} //成员模板 template<class U1,class U2> Pair(const Pair<U1, U2>& p) : first(p.first),second(p.second) {} }; //Base1理解为鱼类 class Base1 {}; //derived1理解为鲫鱼 class derived1 :public Base1 {}; //Base2理解为鸟类 class Base2 {}; //derived2理解为麻雀 class derived2 :public Base2 {};
调用:
Pair<derived1, derived2> p; //将p,即鲫鱼和麻雀组成的Pair作为参数传入Pair的构造函数 //形成的p2里面,first是鱼类,second是鸟类。但是他们的具体内容是鲫鱼和麻雀 Pair<Base1, Base2> p2(p);
将成员模板引申到前面所讲的只能指针,即可实现父类指针指向子类对象的效果。
因为普通的指针都可以指向自己子类的对象,那么作为这个指针的封装(智能指针),那必须能够指向子类对象。所以,可以模板T1就是父类类型,U1就是子类类型。实现如下:
template<class T> class shared_ptr { public: T& operator*() const { return *px; } T* operator->() const { return px; } shared_ptr(T* p):px(p) {} //成员模板 template<class U> explicit shared_ptr(U* p) : px(p) {} private: T* px; }; //测试类,将其对象指针包装成shared_ptr智能指针 class Test { public: virtual void mytest() { printf("testing...\n"); } }; class Test2 :public Test { void mytest() { printf("testing2...\n"); } };
调用:
Test2 t2; shared_ptr<Test> ptr(&t2); //实际上里面的成员属性px是Test类型的,但是保存的却是Test2类型数据 ptr->mytest(); //输出testing2..
九、模板特化(Specialization特殊化)(video10)
模板特化和泛化是反义的。我们使用的普通模板化指模板泛化,而特化就是指在泛化的基础上,有一些特殊的类型做特殊处理,例如:
//这个是Hash类的泛化定义 template<class T> class Hash { public: void operator()(T in)const { cout << in << endl; } }; //这个是Hash类对于int类型的特化定义 template<> class Hash<int> { public: void operator()(int in)const { cout << "Int:"<<in<< endl; } }; //这个是Hash类对于Double类型的特化定义 template<> class Hash<double> { public: void operator()(double in)const { cout << "Double:" << in << endl; } };
调用:
//int类型调用的是int类型的特化 Hash<int> hash_int; hash_int(50); //double类型调用的是double类型的特化 Hash<double> hash_db; hash_db(88.8); //float类型调用的是泛化情况下的构造方法 Hash<float> hash_fl; hash_fl(77.7);
十、偏特化(局部特化)(video10)
偏特化是在全特化的基础上发展来的,全特化就是上面所述的例子,只有一个泛化类型T。而偏特化就是指有多个泛化类型,例如:
//多类型模板泛化 template<class T1,class T2,class T3> class Hash2 { public: void operator()(T1 in1,T2 in2,T3 in3)const { cout <<"泛化"<< endl; } }; //前两个固定为int和double,偏泛化 template<class T3> class Hash2<int,double, T3> { public: void operator()(int in1,double in2,T3 in3)const { cout << "偏特化"<<endl; } };
调用:
//泛化 Hash2<float, double, float> hash_fdf; hash_fdf(5.0, 6.6, 7.7); //输出 泛化 //偏特化 Hash2<int,double,float> hash_idf; hash_idf(5,6.6,7.7); //输出 偏泛化
十一、范围的偏特化
本来是全泛化,假设为template<class T>,我们想单独拿出任意类型的指针来偏特化。
template<class T> class A { public: void operator()(T t)const { cout << "泛化" << endl; } }; template<class T> class A<T*> { public: void operator()(T* tp)const { cout << "范围偏特化" << endl; } };
调用:
int num = 5; int *p_num = # A<int> a1; A<int*> a2; a1(num); //输出泛化 a2(p_num); //输出范围偏特化
十二、模板的模板参数(video12)
模板的参数就是指template<typename T>中的T,他可以是任意类型。
但是当T也是一个模板时,就叫模板的模板参数。
template<class T> class Test{}; template<typename T,template<typename CT> class C> class XC { private: //C可以是List,该list需要一个模板参数CT,可以是任何类型,这里使用T的类型 C<T> c; };
调用:
//实际上xc中的c的类型为Test<int> XC<int, Test> xc;
当然,像vector等类型拥有多于1个的模板参数,所以以上代码中的C不能是vector。如果要实现vector作为XC的第二个模板,那么需要指明vector的两个模板参数:
template<class T> class Test{}; template< class T1,class T2, template<class CT1, class CT2> class C> class XC { private: //C可以是List,该list需要一个模板参数CT,可以是任何类型,这里使用T的类型 C<T1,T2> c; };
调用:
//实际上xc中的c的类型为vector<int,std::allocator<int>> XC<int, std::allocator<int>,vector> xc;
模板的模板参数,还需要和另一种形式区分开:
template<class T, class Sequence = deque<T>> //这种不是模板的模板参数
Sequence的默认值deque<T>实际上已经指明了Sequence的类型时deque,只是因为deque还有一个模板参数而已。
它和上面讲的不一样,上面讲的 template<class CT1, class CT2> class C,相当于Sequence<T>,Sequence和T都是待指定的模板参数。所以还是有本质区别的。
十三、不定模板参数(C++2.0新特性)(video14)
//用于当args...里没有参数的时候调用,避免报错 void leo_print() { //Do Nothing } //数量不定模板参数 template<class T, class ... Types> void leo_print(const T& firstArg, const Types& ... args) { cout << firstArg << endl; //递归调用leo_print,每次打印下一个参数 //自动将args...里的多个参数分为1+n个 leo_print(args...); }
调用:
leo_print(64, 5.5, "gogogo", 90, 377);
十四、auto关键字(C++2.0新特性)(video14)
auto关键字的作用在于,让编译器自动帮你推导需要的类型。这是一个语法糖,仅仅是让写代码更加方便。
例如:
list<int> c; c.push_back(11); c.push_back(22); c.push_back(33); //普通写法,自定指定类型 list<int>::iterator ite; ite = find(c.begin(), c.end(), 33); //auto写法,编译器从右边的find返回值推导其变量类型 auto ite = find(c.begin(), c.end(), string("leo")); //错误写法 auto ite; //光从这句话,编译器没法推导他的类型 ite = find(c.begin(), c.end(), string("hello3"));
十五、基于range的for循环(C++2.0新特性)(video14)
list<int> c; c.push_back(11); c.push_back(22); c.push_back(33); //遍历一个容器里的元素 for (int i : c) { cout << i << endl; } //除了遍历传统的容器,还能遍历{...}形式的数据 //{}中的数据要和i的类型符合 for (int i : {1,2,3,4,5,6,7,8,9,10}){ cout << i << endl; } //使用auto自己推导遍历元素的类型 for (auto i : c) { cout << i <<endl; }
上面过程,遍历到的数据都是拷贝给“i”的,不会影响容器中的数据。
//pass by reference,直接作用于容器元素本身 for (auto& i : c) { i *= 3; }
十六、引用reference(video15)
1.x是一个int类型的变量,大小为4bytes。
2.p是一个指针变量,保存的是x的地址,在32bit计算机中,指针的大小为4bytes。
3.r是x的引用,可以认为r就是x,x就是r,
4.当r=x2时,相当于也是x=x2。
5.r2是r的引用,也即是x的引用。现在r、r2、x都是一体。
6.使用sizeof()来看引用r和变量x的大小,返回的数值是一样的。
7.对引用r和变量x取地址,返回的地址也都是一样的。
从实现的角度来看,引用的底层都是通过指针来实现的。但是从逻辑的角度来看,引用又不是指针,我们看到引用,就要像看到变量本身一样,例如一个int类型变量,其引用也应该看成整数。
引用就是一个变量的别名,引用就是那个变量。引用一旦创建,就不能再代表其他变量了。如果此时再将某个变量赋值给这个引用,相当于赋值给引用代表的变量。参照前面第4点。
所以,对引用最好的理解方式,就是认为引用就是变量,不用关心为什么,编译器给我们制造了一个假象,就像一个人有大名和小名一样。你对引用做的任何操作,就相当于对变量本身做操作。
double imag(const double& im){} double imag(const double im){}
上述两句代码不能并存,因为他们的签名(signature)是相同的,signature就是[ imag(const double im) ----- ]这一段,‘----’表示可能存在的const关键字等。(const是作为签名的一部分的)
为什么不能并存,因为这两个函数虽然传参数的方式不同,一个传引用,一个传值。但对于调用来说,是一样的imag(im),这样对于编译器来说,它不知道该调用哪一个,所以不能并存(重载)。
十七、对象模型Object Model(video17)
图中最右边是A,B,C三个类的继承关系。
最左边是对象a,b,c的内存占用情况。
当类中存在虚函数的时候(不管有几个虚函数),对象的内存占用都会多出4bytes,这个空间存放了一个指向虚表(Virtual table:vtbl)的指针(Virtual Pointer:vptr)。虚表里放的都是函数指针。
从这张图中可以看出:
1.父类有虚函数,子类必定有虚函数(因为子类会继承父类的所有数据,包含虚表指针)。如果子类有对其进行override,那么父类和子类所指向的同名虚函数是不同的,例如A中的vfunc1()虚函数,B将其进行override,C又再次override,所以各自一份不同的函数体。分别颜色为浅黄、浅蓝、深黄。
2.子类继承了父类的虚函数,但没有进行override,例如A中的vfunc2(),B和C都没对其进行override,所以大家的虚表里指向的都是同一份函数代码。
3.非虚函数不存在override,各自的非虚函数,都各自拥有,即使名字一样,但函数都是不相干的。子类只是继承了父类非虚函数的调用权而已(子类和父类有同名的非虚函数,子类可以使用调用权调用父类的函数)。
调用函数的流程:
当C* p = new C();的指针p(图中左边的红色p)去调用C的vfunc1()时,流程是 虚指针->虚表->vfunc1()地址,这叫动态绑定。C语言中对函数的调用,是编译器使用call XXX直接调用函数,那个叫静态绑定。如下图所示:
这是通过p指针找到vptr,然后通过*vptr取到虚表,然后选择虚表中第n个函数指针,再通过函数指针加()来调用函数。是用C语言实现的一个正确调用流程。
如何在虚表中确定需要调用的虚函数是第几个(选择n),就是在定义虚函数时,编译器会看定义的顺序,例如A类中vfunc1是第0个,vfunc2是第1个。
什么时候编译器使用动态绑定,三个条件(也是多态的条件):
1.函数由指针调用。
2.指针向上转型(up cast),父类指针指向子类对象。
3.调用的是虚函数。
十八、多态的例子(video17)
延续十六节里面的三个类,我们假设A代表平行四边形,B代表长方形,C代表正方形。他们的继承关系就是C->B->A。
当我们要在一个容器中存放不同的对象时,由于容器中存放元素的大小和类型都必须一致,所以我们只能存放不同对象的指针,而且指针类型还必须一致,我们只能选择父类A的指针A*。
假设容器中存放了a,b,c三个对象的指针,但这三个指针都是存放的父类指针,也就是A*。那么,当我们要分别画出他们的时候,可以从该容器中取出这些指针,然后调用draw()函数。虽然都使用的是父类的指针,但是a,b,c三个对象内存中存放的虚指针指向的虚表是各自不同的,虚表中所保存的draw()函数地址对应的函数也是各自分别实现的。所以最终可以成功画出自己的图形。这种使用父类指针调用子类的个性化方法,就实现了多态。如图所示:
这就是多态。。多态。。多态。。多态。。多态。。。。
十九、this指针(video18)
什么是this指针:通过一个对象来调用成员函数,那个对象的地址就是this指针,就是这么简单。
如上图所示,我们使用子类对象myDoc来调用OnFileOpen()。OnFileOpen()函数是父类的非虚函数,但是myDoc的指针会被编译器以隐式的方式传入OnFileOpen(),这个指针就是this指针,OnFileOpen()函数中的步骤运行到需要调用Serialize()函数时,因为Serialize()函数是一个虚函数,并且子类CMyDoc对其进行了override,所以编译器去调用Serialize()时,是使用的this->Serialize()。
注意图中左上角的红框,this指针指向的是myDoc对象,该对象里有虚指针,虚指针指向的虚表中有属于CMyDoc子类的Serialize()虚函数。所以最终调用的是子类的Serialize()。
二十、动态绑定和静态绑定(video19)
静态绑定:
如上图所示,对象b被强转为A类对象,那么由a来调用vfunc1()就是静态绑定,因为它不满足动态绑定的三个条件(见十六节)的第一个,需要由指针来调用虚函数,并且指针是父类指针。
图右边的紫色是编译器注释以及候老师的注释,说明调用的是A类的A::vfunc1()虚函数,使用的是call xxx的静态绑定形式。
动态绑定:
如图所示,pa为A类指针,指向新new出来的B类对象,满足第一个条件:是指针,满足第二个条件:向上转型。然后使用pa调用vfunc1(),满足第三个条件:调用虚函数。所以是动态绑定。
取对象b的地址,并赋值给指针pa,b是类B的对象,而pa是A类的指针,所以满足前两个条件:指针以及向上转型。然后再使用pa指针调用vfunc1(),满足第三个条件:调用虚函数。所以也是使用的动态绑定。
二十一、const关键字(video19)
const的使用情况:
1.放在成员函数的小括号之后,函数本体的前面:例如int test() const { reutrn this->img; }
这种情况下,const表示这个成员函数保证不去修改类的成员变量,只做访问等操作。
注意:这个位置加const,只发生在类的成员函数上,全局函数不能加const。(因为这个const保证不修改数据,是有保证对象的,保证的对象就是该类的一个常量对象,即使用const修饰的该类对象,见后续说明)。
2.放在变量的前面:例如const A a;
这种情况下,const表示修饰的变量(基础变量或对象等)是不能修改的(如果是对象,就不能修改他内部的成员变量)。
以上两种情况就可以搭配起来使用:
分为四种情况:
①.对象是const,成员函数也是const,能够完美搭配。因为对象是不能修改的,而该函数又保证不修改数据,一拍即合。
②.对象是non-const,成员函数是const。也是能够配合的。因为对象可以接受修改,也可以接受不修改,而函数保证不修改,没毛病。
③.对象是const,成员函数是non-const,不能搭配。因为对象不允许修改数据,但是函数不保证,无法协调。
④.对象是non-const,成员函数也是non-const,能搭配。因为对象不限制修改数据,函数也不保证,那就随便吧。
侯老师经验:在设计类的时候,考虑一共要设计几个函数,每个函数叫什么名字的时候,心里就应该知道那些函数是绝对不会修改数据的,那么这些函数就必须加上const。否者,如果其他人使用你设计的类,定义了一个const类型的对象,却因为你没有指明const关键字而无法调用一个可能名为print的输出函数(输出函数肯定不用修改数据,只是做访问和打印而已)。
二十二、const成员函数和non-const成员函数共存(video19)
在一个类中存在两个同名、同参数的函数,但一个有const标记,一个没有。例如标准库中的basic_string类(其实就是我们使用的string类的底层类),对方括号“[]”进行重载的函数就存在两个:
第一个成员函数有const标记,该函数是提供给常量字符串调用“[]”时使用的,例如打印index=2位置的字符,返回index=2的字符数据。
第二个成员函数没有const标记,该函数是提供给非常量字符串调用“[]”时使用的,例如修改index=2位置的字符,返回index=2的字符引用,引用就可供读取,也可以修改。
第二个函数需要考虑COW(copy on write)的情况,因为标准库实现string使用了相同数据共享的技术,也就是多个字符串如果数据相同,那么内部指向的可能是同一份数据,当有某一个字符串要修改数据时,才单独提供一份给它修改,而其他字符串不受影响,还是保持共享数据状态,这就叫COW,也就是说要写或修改时进行复制(一份数据)。(docker容器技术也是采用的COW思想)
注意一下调用准则:
二十三、new和delete(video20)
在C++编程一里面我们知道new和delete分解的动作:
new分解为三步:
1.分配内存(底层是malloc(size),内存大小为sizeof(class_name))
2.转换内存指针的类型为class_name*,原本为void*
3.用该指针调用构造函数,p->Foo::Foo();
delete分解为两步:
1.使用传入的指针调用析构函数p->~Foo();
2.释放对象占用的内存空间(底层是free())
我们可以重载全局的new和delete(但一定要小心,影响很大):
我们也可以重载类成员new和delete(只影响该类):
一般重载成员new和delete,主要是做内存池。
重载成员Array new和Array delete:
注意:array new的时候,分配的内存大小为sizeof(Foo)*N + 4,这里的4个byte,只在N个Foo对象内存的前面,有一个int类型的数,里面的值为N,用于标记有几个Foo对象。如下图:
红框处就是一个int所占用的空间,里面的值为N(假设N=5),一个Foo对象为12bytes,那么总共分配内存大小为12*5+4=64。
二十四、new、delete重载示例(video22)
为了不然程序崩掉,重载的new和delete需要真正的去分配内存和释放内存,底层采用的是malloc和free。如果不想使用已重载的new和delete,可以使用::new class_name;来强制使用全局new,::delete pf;强制使用全局delete。
二十五*、重载特殊的placement new和delete(video23)
我们可以重载多个版本的new和delete,每个不同的new和delete的参数列不同。例如:
class Foo { public: Foo() {} Foo(int) {} //这个是普通的new重载,只有一个默认参数 void * operator new (size_t size) { return malloc(size); } //下面三个都是placement new重载 void * operator new(size_t size, void * start) { return start; } void * operator new (size_t size, long extra) { return malloc(size + extra); } void * operator new(size_t size, long extra, char init) { return malloc(size + extra); } };
如何使用这些new呢?
void * p = 0; //普通new Foo * f1 = new Foo(); //额外带一个void*参数的new Foo * f2 = new(p) Foo; //额外带一个long参数的new Foo * f3 = new(300) Foo; //额外带一个long和一个char参数的new Foo * f4 = new(300, 'a') Foo;
(以下部分作为了解:)
对于delete来说,我们当然也可以重载多个placement delete,但是注意一点,除了默认的那个delete,其余几个特殊的delete都不会被调用,他们被调用的唯一可能是,当对应的placement new分配内存后,调用构造函数抛出异常的时候,才会去调用对应的placement delete。例如:
//对应普通的new,size_t是默认参数(可选的) void operator delete(void * ,size_t){} //对应operator new(size_t size, void * start) void operator delete(void *, void*){} //对应operator new (size_t size, long extra) void operator delete(void *, long) {} //对应operator new(size_t size, long extra, char init) void operator delete(void *, long, char) {}
delete的时候默认都是调用第一个普通的,后面几个特殊的,只有在上述条件下才会被调用(不一定?根据编译器不同可能会有变化)。
二十六*、placement new重载在标准库string中的应用(video24)
如上图所示,在标准库的basic_string(就是string的底层)中,有一个内部结构体Rep,标准库针对Rep做了placement new的重载,额外添加了一个size_t的参数,在Rep *p = new(extra) Rep;中,传入了一个参数extra,这个参数表示在Rep初始化的过程中除了分配自身大小的空间,还额外分配了一个大小为extra的空间。basic_string就是利用这块额外的空间来存放实际的字符串数据,而且利用Rep对象来执行COW的控制,也就是控制有多少个string共享同一份数据。