被声明为explicit的构造函数通常比其non-explici兄弟更受欢迎,因为它们禁止编译器执行非预期(往往也不被期望)的类型转换。**除非我有一个好理由允许构造函数被用于隐式类型转换,否则我会把它声明为explicit。**我鼓励你遵循相同的政策。
class Widget {
public:
Widget(); // default构造函数
Widget(const Widget& rhs); // copy构造函数
Widget& operator=(const Widget& rhs); // copy assignment操作符
};
Widget w1; //调用default构造函数
Widget w2(w1); //调用copy构造函数
w1 = w2; //调用copy assignment操作符
Widget w3 = w2; //调用copy构造函数
copy构造和copy赋值的区别:
如果一个新对象被定义(例如以上语句中的w3),一定会有个构造函数被调用,不可能调用赋值操作。如果没有新对象被定义(例如前述的"w1=w2"语句),就不会有构造函数被调用,那么当然就是赋值操作被调用。
函数Pass-by-value 意味“调用copy构造函数”。以by value传递用户自定义类型通常是个坏主意,Pass-by-reference-to-const往往是比较好的选择,详见条款20。
Ihs和rhs。它们分别代表"left-hand side"(左手端)和"right-hand side"(右手端)。可以作为二元操作符(binary operators)函数如operator==和operator*的参数名称。
对于成员函数,左侧实参由this指针表现出来,所以单独使用参数名称rhs。
将“指向一个T型对象”的指针命名为pt,意思是"pointer to T"。
Airplane* pa; //pa ="ptr to Airplane".
GameCharacter* pgc; //pgc="ptr to GameCharacter"
对于references:rw可能是个reference to widget,ra则是个reference to Airplane。
C++可视为:
四者的集合。
c++高效编程守则视状况而变化,取决于你使用c++的哪一部分。
比如:
传值还是传引用?
enum hack:
void fun(){
enum { x =5 };//不限定作用域枚举
int arr[x];
}
一个属于枚举类型的数值可权充ints被使用,类似于局部的#define而不像const,该数值无法取地址。
如果你不想让别人获得一个pointer或reference指向你的某个整数常量,enum可以帮助你实现这个约束。Enums和#defines一样绝不会导致非必要的内存分配。
建议:
对于单纯常量,尽量以const对象或enums枚举来代替#define。
对于形式函数的宏,用inline函数代替#define。
char greet[] = "hello";
const char* p = greet; //const data,non-const pointer;
char* const p = greet; //non-const data,const pointer;
const char* const p = greet; //const data,const pointer;
如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量; 如果出现在星号两边,表示被指物和指针两者都是常量。
如果被指物是常量,有些程序员会将关键字const写在类型之前,有些人会把它写在类型之后、星号之前。两种写法的意义相同,所以下列两个函数接受的参数类型是一样的:
void f1(const Widget* pw);
void f2(Widget const* pw);
STL送代器系以指针为根据塑模出来,所以迭代器的作用就像个T*
指针。声明迭代器为const就像声明指针为const一样(即声明一个T* const
指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值是可以改动的。如果你希望迭代器所指的东西不可被改动(即希望STL模拟一个const T*
指针),你需要的是const-iterator
:
std::vector<int> vec;
//iter2类型为int* const
const std::vector<int>::iterator iter2 = vec.begin();//iter类似于T* const
*iter2 = 1; //正确
iter2++; //错误!iter是const
//iter3类型为const int*
std::vector<int>::const_iterator iter3 = vec.cbegin();//iter类似于const T*
*iter3 = 1; //错误!*iter3是const
iter3++; //正确
int x=2;
const int *p = &x;
*p=2; //报错:表达式必须是可修改的左值
x=3;
cout<<*p; //输出为3
const只是限制 *p 不可变,但x依然可以改变。
const最具威力的用法是面对函数声明时的应用。在一个函数声明式内,const可以和函数返回值、各参数、函数自身(如果是成员函数)产生关联:
const返回值
令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。举个例子,考虑有理数(rational numbers,详见条款24)的operator*声明式:
class Rational{....};
const Rational operator* (const Rational& lhs, const Rational& rhs);
返回const 可以避免诸如if(a * b = c)
之类的错误,编译直接报错。
const参数
至于const参数,没有什么特别新颖的观念,它们不过就像local const对象一样,你应该在必要使用它们的时候使用它们。除非你有需要改动参数或local对象,否则请将它们声明为const。只不过多打6个字符,却可以省下恼人的错误,像是“想要键入’==’,却意外键成’=’ ”的错误,一如稍早所述。
const 成员函数
将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。这一类成员函数之所以重要,基于两个理由。第一,它们使class接口比较容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重要。第二,它们使“操作const对象”成为可能。这对编写高效代码是个关键,因为如条款20所言,改善C++程序效率的一个根本办法是以pass by reference-to-const方式传递对象,而此技术可行的前提是,我们有const成员函数可用来处理取得(并经修饰而成)的const对象。
编译器对const是“bitwise”(逐位)的不变检查,但编程时应该以“逻辑级”的不变思路来做,一个const成员函数可以修改它所处理的对象内的某些bits,通过使用mutable修饰这些变量,mutable释放掉non-static成员变量的bitwise constness约束。
由于函数有重载特性,当const和non-const成员函数有实质等价的实现时,用non-const版本调用const版本来避免代码重复,但是要做好类型转换,否则会出现自己调用自己的现象。也不能反向调用,这不符合逻辑。
class CTextBlock{
public:
const char& operator[](std::size_t position)const
{
...
...
...
return pText[position];
}
char& operator[](std::size_t position)
{
//static_cast添加了const以调用const版本的[],第二次则从返回值中移出const
return const_cast<char&>(static_cast<const CTextBlock&>(*this)[position]);
}
string pText;
int length;
};
建议:
所谓static对象,其寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都被排除。这种对象包括global对象、定义于namespace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。
所谓编译单元(translation unit)是指产出单一目标文件(single object file)的那些源码。基本上它是单一源码文件加上其所含入的头文件(#include files)。
问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。
解决办法:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。
这个手法的基础在于:**C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。**所以如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,你就获得了保证,保证你所获得的那个reference将指向一个历经初始化的对象。更棒的是,如果你从未调用该改造后的函数,就绝不会引发构造和析构成本!
例子:https://blog.csdn.net/chgaowei/article/details/6001433?utm_source=jiancool
任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”可能会产生竞态条件,比如:
建议:
如果类没有声明定义构造函数、拷贝构造函数、拷贝赋值运算符,则会在必要时自动生成需要的函数,比如在有虚函数时自动生成合成的默认构造函数(因为有虚函数时需要构造函数生成虚表指针),需要用到拷贝构造函数时自动生成合成拷贝构造函数,需要用到拷贝赋值运算符时自动生成合成拷贝赋值运算符。
class Empty{}
Empty e1;//如今的编译器并不会生成合成的默认构造函数,因为没有必要。
如何证明?使用 nm -C a.out
查看可执行文件a.out的符号表就可以看到这些函数是否存在。或者使用objdump反汇编。
编译器并不是一定会生成这些函数,如果生成的这些函数会违反C++规则的话,编译器会拒绝编译。
如果不想使用编译器自动生成的函数:
class Uncopyable{
{
protected:
Uncopyable(){}
~Uncopyable(){};
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
}
class HomeForSale : public Uncopyable{
...
};
也可以使用boost提供的版本:
#include
class HomeForSale : public boost::noncopyable{
...
};
class HomeForSale{
...
HomeForSale(const HomeForSale &) = delete;
HomeForSale &operator=(const HomeForSale &) = delete;
};
C++指出,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没被销毁。
解决办法:给base class一个virtual析构函数。此后删除derived class对象就会如你想要的那般。是的,它会销毁整个对象,包括所有derived class成分。(因为此时的析构函数被放在虚函数表中,调用的实际上是派生类的析构函数)
任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。有虚函数的类的对象会自动增加一个vptr(virtual table pointer,虚函数表指针) ,增加至少 32bits或 64bits。
含有纯虚函数的抽象类不能被实例化,但即便虚析构函数是纯虚函数,他还是需要被定义,因为派生类析构时会按相反顺序调用其继承的基类的析构函数,这些析构函数必须被定义。
class AWOV
{
public:
virtual ~AWOV()=0;
};
AWOV::~AWOV(){}//这一步是必要的
请记住:
析构函数绝对不要吐出异常。
如果一个被析构函数调用的函数可能跑抛出异常,析构函数应该捕捉任何异常,然后
class DBConn{ // 这个class用来管理DBConnection对象
public:
~DBConn() // 确保数据库连接总是会被关闭;
{
try{
db.close();
}
catch(...){
// 制作运转记录,记下对close的调用失败
//选择1:结束程序,如下
abort();
//选择2:吞下异常
}
}
private:
DBConnection db;
};
更好的办法,是让用户自己选择:
class DBConn{ // 这个class用来管理DBConnection对象
public:
void close()//供客户使用的新函数
{
db.close();
closed = true;
}
~DBConn() // 确保数据库连接总是会被关闭;
{
if(!closed) // 判断是否关闭
{
try{
db.close();
}
catch(){
// 制作运转记录,记下对close的调用失败;或吞下异常
abort(); //close()抛出异常的处理,
}
}
}
private:
DBConnection db;
bool closed;
};
请记住:
基类中的构造函数调用的虚函数永远是基类的虚函数(因为此时派生类部分还没有生成),不能指望派生类使用基类构造函数时,该基类构造函数使用派生类的虚函数。
析构函数同理。
class A{
public:
A(){
//do sth;
// ······
// final
print(); //调用A的方法,而不是B的。
}
virtual void print() const;
};
class B:public A{
public:
virtual void print() const;
};
解决办法:
class A{
public:
A(){
//do sth;
// ······
// final
}
virtual void print() const;
};
class B:public A{
public:
virtual void print() const;
B(){
print();
}
};
class A{
public:
explict A(string& loginfo)
{
//do sth;
······
// final
print(loginfo); //调用A的方法,而不是B的。
}
void print(string& loginfo) const;
};
class B:public A{
public:
B(param):A(createlogstring(param))
{···}
private:
string createlogstring(param);
};
如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或虚函数所属类型相对应的虚函数版本。——《C++primer第五版》P556
为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议。
class Widget
{
public:
Widget& operator=(const Widget& rhs)
{
···
return *this;
}
};
Widget x, y, z;
//连锁赋值
x=y=z;
//返回引用是为了如下正确。
(x=y)=z;
由于变量有别名的存在(多个指针或引用指向同一个对象),所以可能出现自我赋值的情况。比如 a[i] = a[j],可能是同一个对象赋值。这时就需要慎重处理赋值操作符以免删除了自己后再用自己来赋值,比如:
class Widget{
public:
Widget& operator=(const Widget& rhs){
delete p;//如果p之前就已经释放掉了,再次释放会被报错
p=new int(ths.p);
return *this;
}
int *p;
};
解决方法:
class Widget{
public:
Widget& operator=(const Widget& rhs){
if(this==&rhs)//证同测试
return *this;
delete p;
p=new int(rhs.p);
return *this;
}
int *p;
};
2.我们只需注意在复制p所指东西之前别删除p:
Widget& operator=(const Widget& rhs){
Bitmap* pOrig = p; // 记住原先的pb
p = new Bitmap(*rhs.p); // 使用rhs bitmap的副本
delete pOrig; // 删除原先的pb
return *this;
}
该版本同时具备之前版本不具备的“异常安全性”。
3.使用copy and swap技术:
class Widget{
public:
void swap(const Widget& rhs);//交换rhs和this
Widget& operator=(const Widget& rhs){
Widget tmp(rhs);//赋值一份数据
swap(tmp)//交换
return *this;//临时变量会自动销毁
}
int *p;
};
请记住:
当你编写一个copying函数(拷贝构造函数、拷贝赋值运算符),请确保:
class A {
public:
int A_mem;
A() {}
A(const A& rhs) :A_mem(rhs.A_mem) {}
A& operator=(const A& rhs) { A_mem = rhs.A_mem; }
};
class B : public A {
public:
int B_mem;
B() {}
B(const B& rhs) :A(rhs), B_mem(rhs.B_mem) {}
B& operator=(const B& rhs) {
A::operator=(rhs);
B_mem = rhs.B_mem;
return *this;
}
};
如果B的copy函数漏掉了基类A部分,则A部分会使用默认构造函数,而不是copy。
请记住:
我们可以将资源放进局部对象内,当对象离开作用域时,该对象的析构函数会自动释放资源。shared_ptr就是一种这样的对象。
实际上“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机"(Resource Acquisition Is Initialization;RAll),因为我们几乎总是在获得一笔资源后于同一语句内以它初始化某个管理对象。有时候获得的资源被拿来赋值(而非初始化)某个管理对象,但不论哪一种做法,每一笔资源都在获得的同时立刻被放进管理对象中。
请记住:
为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
并不是所有资源都是开辟在堆上,有时候我们需要自己建立资源管理类:
class Lock{
public:
explicit Lock(Mutex* mu):mutexPtr(mu)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex* mutexPtr;
};
客户对Lock的如下使用方法符合RAII:
Mutex m;//定义互斥器
……
{//建立区块来定义critical section
Lock(&m);
……//执行critical section 内的操作
}//在区块末尾,自动解除互斥器的锁
但是,当一个RAII对象被复制,会发生什么?有以下做法:
禁止复制,将coping函数设置为私有或=delete,条款6。
对底层资源使用引用计数法。复制的时候引用计数加1,为0时销毁。
通常只要内含一个shared_ptr成员变量,RAII classes便可实现出引用计数行为。如果前述的Lock打算使用reference counting,它可以改变mutexPtr的类型,将它从Mutex*改为shared_ptr< Mutex > 。
但是shared_ptr默认释放动作为delete,我们需要制定专门的删除器,一个函数或函数对象。
class Lock{
public:
//以某个Mutex初始化shared_prt,unlock作为删除器
explicit Lock(Mutex* mu):mutexPtr(mu,unlock){
lock(mutexPtr);
}
private:
shared_prt<Mutex> mutexPtr;
};
这个类中不需要自己编写析构函数,因为mutexPtr是类中的普通成员变量,编译器会自动生成析构函数类析构这样的变量。
复制底部资源
如果这种资源可以任意复制,我们只需编写好适当的copying函数即可。确保复制时是深拷贝。
转移底层资源的拥有权
有时候资源的拥有权只能给一个对象,这时候当资源复制时,就需要剥夺原RAII类对该资源的拥有权,比如使用unique_ptr的release()。
请记住:
对内置数组的new,数组所用的内存通常还包括“数组大小”的记录,以便delete知道需要调用多少次析构函数。很多编译器如下实现:
不要对内置数组做typedefs动作,很容易让人delete时忘记添加[]。
建议:
如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。
C++参数列表中表达式的计算顺序并没有在C/C++中明确定义,每个编译器可以自由发挥,有自己的实现。
比如:
int processWidget(shared_ptr<Widget>(new Widget), int priority());
如果按如下顺序:
1、执行new Widget
2、执行priority()函数
3、执行shared_ptr构造函数
并且在priority()时抛出异常,则会发生资源泄漏。
因此,最好使用make_shared
代替shared_ptr
或者将如下将其独立出去:
shared_prt<Widget> pw(new Widget);
processWidget(pw,priority());
请记住:
以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。假设你为一个用来表现日期的class设计构造函数:
class Date {
public:
Date (int month, int day, int year);
...
};
客户很可能以错误的次序传递参数。许多客户端错误可以因为导入新类型而获得预防。在防范“不值得拥有的代码"上,类型系统是你的主要同盟国。既然这样,就让我们导入简单的外覆类型(wrapper types)来区别天数、月份和年份,然后于Date构造函数中使用这些类型:
令Day,Month和Year成为成熟且经充分锻炼的classes并封装其内数据,比简单使用上述的structs好(见条款22)。但即使structs也已经足够示范:明智而审慎地导入新类型对预防“接口被误用”有神奇疗效。
预防客户错误的另一个办法是,限制类型内什么事可做,什么事不能做。常见的限制是加上const。
下面是另一个一般性准则“让types容易被正确使用,不容易被误用”的表现形式:
“除非有好理由,否则应该尽量令你的types的行为与内置types一致”。客户已经知道像int这样的type有些什么行为,所以你应该努力让你的types在合样合理的前提下也有相同表现。例如,如果a和b都是ints,那么对a*b赋值并不合法,所以除非你有好的理由与此行为分道扬镳,否则应该让你的types也有相同的表现。是的,一旦怀疑,就请拿ints做范本。
避免无端与内置类型不兼容,真正的理由是为了提供行为一致的接口。很少有其他性质比得上“一致性”更能导致“接口容易被正确使用”。比如STL容器的接口十分一致,没一个STL容器都有size成员函数。
任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用"的倾向。比如需要用户自己delete释放内存,更好的方法是返回智能指针对象。
shared_ptr有一个特别好的性质是:它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在的客户错误:所谓的"cross-DLL problem" 。这个问题发生于“对象在动态连接程序库(DLL)中被new创建,却在另一个DLL内被delete销毁”。在许多平台上,这一类“跨DLL之new/delete成对运用”会导致运行期错误。shared_ptr没有这个问题,因为它默认的删除器是来自"shared_ptr诞生所在的那个DLL"的delete。
虽然shared_ptr比原始指针大且慢,但是额外执行成本并不明显,并且降低客户错误的成效很好。
请记住:
要注意解决以下问题:
在默认情况下,C++函数传递参数是继承C的方式,是值传递(pass by value)。这样传递的都是实际实参的副本,调用端所获得的也是函数返回值的副本,这些副本都是通过调用复制构造函数来创建的。有时候创建副本代价非常昂贵。
使用pass-by-reference-to-const的好处:
例外:引用往往以指针实现,对于编译器会给予优化的内置类型,以及STL迭代器和函数对象,使用传值效率更高。
但是,对于小型用户自定义类型,对象小并不意味着copy构造函数代价小,编译器也不一定会对其优化,应该使用pass-by-reference-to-const。
请记住:
绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
成员变量应该是private,原因:
语法一致性:如果成员变量不是public,客户唯一能够访问对象的办法就是通过成员函数。如果public接口内的每样东西都是函数,客户就不需要在打算访问class成员时迷惑地试着记住是否该使用小括号(圆括号)。
可细微划分访问控制:使用函数可以让你对成员变量的处理有更精确的控制。如果你令成员变量为public,每个人都可以读写它,但如果你以函数取得或设定其值,你就可以实现出“不准访问”、“只读访问"以及“读写访问”。
提供弹性:将成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。例如这可使得成员变量被读或被写时轻松通知其他对象、可以验证class的约束条件以及函数的前提和事后状态、可以在多线程环境中执行同步控制…等等。**被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。**只要类的接口不变,用户代码就无需改变。
protected同理,因为他影响的是派生类。从封装的角度观之,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。
请记住:
(1)假设有一个WebBrower类,代表浏览器,其中有的成员方法有:
class WebBrower
{
public:
void ClearCach();
void ClearHistory();
void RemoveCookies();
};
(2)现在有一个需求:同时使用这三个函数,但是我们实现上述需求有两种方式:
方式一:在WebBrower类中再写一个成员方法,直接在其内部调用其他成员方法。
class WebBrower
{
…
void ClearEverything()
{
ClearCach();
ClearHistory();
RemoveCookies();
}
…
};
方式二:写一个不属于这个类的函数,在函数内调用这个类的三个成员方法。
void ClearWebBrowser(WebBrower& w)
{
w.ClearCach();
w.ClearHistory();
w.RemoveCookies();
}
(3)现在的问题是,哪一种实现方式更好?也就是把它写成成员函数好,还是写成 non-member 、non-friend 函数好呢?
答案:写成:non-member、non-friend 好。
首先,对于面向对象的一个误解:数据应该和操作数据的函数绑定在一起。如果按照这种解释,那么应该写成member的。但是实际上,面向对象强调的是封装性。
所谓封装就是不可见,越多东西被封装,能够看见它的人越少。越少的人看到它,我们就能够更大弹性的修改它。因此,封装性越好,我们改变实现的能力就越高。推崇封装的原因:我们能够改变事物,而只影响有限的客户。
我们计算能够访问该数据的成员函数以及其它函数的数量,作为一种粗糙的衡量。越多的函数能够访问它,它的封装性就越低。
例如:public数据,所有的函数都可以访问它,它就是毫无封装性的。private数据,只有friend和member函数可以访问它,它的封装性的高低,就和能够访问它的friend函数和member函数数量有关,数量越大,代表封装性越低,数量越小,代表封装性越高。
换句话说,member函数可以访问类的private数据,而non-member、non-friend函数无法访问,所以说后者封装性更好。
总之,在实现同一机能的情况下,面对使用member函数和non-member、non-friend函数的抉择时,后者提供更好的封装性。(这个member只是该类的member,不包括其他类的member函数,其他类的member函数也提供封装性)
虽然可以将其写到其它类中,但是C++比较自然的做法是,将它写成一个non-member函数,并让它和浏览器类在同一个命名空间中。
这样的做法是由原因的:
namespace 和class 是不同的,namespace是可以跨越多个文件的,但是class却不能。class 内的是核心技能,但是便利函数只是提供便利的,可有可无的。即使没有便利函数,用户可以通过访问class进行相关的操作。因此说便利函数时外覆的。
一个类可以由不同的机能分化出拥有多个便利函数,与cookies管理有关的、与书签有关的、与打印有关的:
用户可能只对其中一部分感兴趣,那就没有必须让他们之间存在编译相依的关系。可以将他们进行分离,与不同模块相关的便利函数写到不同的头文件中,这样用户对哪个模块感兴趣,就包含哪个头文件就可以了。
将所有便利函数放在多个头文件中但隶属于同一个命名空间,意味着客户可以轻松扩展这一组便利函数。他们需要增加什么便利函数时,也添加到这个命名空间就好。class就不能这样扩展。
注意: 原因一 组织代码的 方式也正是C++标准程序库的组织方式。C++并没有将所有的功能都写到一个头文件里,而是写成数十个头文件,每个头文件中包含某些机能。用户需要什么,就包含什么。这样形成一个编译相依的小系统。 这种分割机能的方式不适用于class成员函数,因为class必须整体定义,不能被分割成片段。
转自:
https://blog.csdn.net/lintianyi9921/article/details/103793070
记住:
宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。
更好的做法是将这些non-member non-friend函数放在同一命名空间,但分模块位于不同头文件,这样编译时只需加入需要的模块头文件,降低了编译依赖性,而且便于客户扩展自己的函数。
通常,令class支持隐式转换是不好的设计。但是也有例外,最常见的例外是在建立数值类型时。例如设计一个类表示有理数时,允许整数隐式转换为有理数是合理的。
class Rational{
public:
Rational(int numerator = 0, int denominator = 1); //刻意不为explicit;允许int-to-Rational隐式转换
int numerator()const;
int denominator()const;
};
我需要为有理数类实现加法、乘法等,是该写成member函数、non-member、non-friend函数还是non-member、friend函数呢?
答案是:non-member、non-friend函数。
class Rational
{
public:
Rational(int numerator = 0,int denominator = 1) : x(numerator), y(denominator){}
//构造函数不为explicit,允许int-to-Rational隐式转换
int numerator() const; //分子访问函数
int denominator() const; //分母访问函数
const Rational operator*(const Rational& rhs) const //有理数的乘法
{
return Rational(x*rhs.x, y*rhs.y);
}
private:
int x;
int y;
};
Rational oneEight(1, 8);
Rational oneHalf(1, 2);
//执行混合运算时会发生错误:
Rational result = oneHalf * 2; //很好
result = 2 * oneHalf; //错误
//函数形式重写:
result = oneHalf.operator*(2);
result = 2.operator*(oneHalf);
答案就是发生了隐式转换,首先oneHalf有成员方法operator* ,它需要一个参数Rational 类对象,但是传递的参数确是一个int 型的2,于是编译器开始查找Rational 的类型转换构造函数,看能否把2转换成Rational 类对象,它找到了,于是产生了一个临时的对象,然后将这个对象传递给了operator*,因此编译通过。这便是所谓的隐式转换。
只有当参数被列于参数列内,这个参数才是隐式转换的合格参与者。地位相当于this的隐喻参数不是隐式转换的合格参与者。
如果将类型转换构造函数写成non-explicit 的话,就达成了二者的一致性,即:都不能通过编译。
显然我们的有理数类是应该支持混合运算的,那么我们的设计就需要改进。
将operator* 写成non-member、non-friend函数,使两个参数都列于参数列表:
const Rational operator*(const Rational& lhs, const Rational& rhs){
return Rational(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator());
}
不应该,因为使用public接口完全可以完成任务。无论何时,我们都应该尽量避免non-member函数写成友元函数以增强封装性(条款23)。当然,也有必须写成friend的场景,friend是有用的。
当进入泛型编程时,本条款可能就不太适用了。
转自:https://blog.csdn.net/lintianyi9921/article/details/103794852
记住:
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。因为只有参数列表内的参数会被隐式转换。
注:C++11后出现了移动语意,因此现在的swap只要类提供移动语意,效率应该不低。
swap()属于STL的一部分(算法),而后成为异常安全性编程的脊柱(因此必须不抛异常),以及用来处理自我赋值的可能性的一个常见的机制。它是如此的有用,适当的实现就显得十分重要。然而在C++11前它的实现的复杂度也比较高。这个条款就在讲swap()函数的实现问题。
C++11前swap实现:
namespace std {
template<typename T>
void swap(T&a, T&b) {
T temp(a);
a = b;
b = temp;
}
}
C++11后(大致实现):
template<class _Ty> inline
void _Swap(_Ty& _Left, _Ty& _Right)
{
_Ty _Tmp = std::move(_Left);
_Left = std::move(_Right);
_Right = std::move(_Tmp);
}
但本节主要讲了“pimpl”手法,以及如何在std中全特化原有的templates。
“以指针指向一个对象,内含真正数据”的类型的,就是“pimpl手法”的表现形式:
class WidgetImpl {
private:
int i;
vector<double> v;
...
};
class Widgt {
Widgt(const Widgt&rhs);
Widgt&operator = (const Widgt&rhs) {
*imp = *rhs.imp;
}
private:
WidgetImpl* imp;
};
在这种情况下,实际上我们只交换两个指针的指向便可,没有必要交换所指物。但是怎么才能告诉标准库的swap呢?
答案:在std空间内全特化一个swap函数,然后在widget类内写一个成员函数swap,调用该全特化的swap函数。具体代码如下:
class WidgetImpl {
public:
int i;
};
class Widgt {
public:
Widgt() = default;
Widgt(const Widgt&rhs);
Widgt&operator = (const Widgt&rhs) {
*imp = *rhs.imp;
}
void swap(Widgt& other) {//member swap
using std::swap; //令std::swap在函数内可用
swap(imp, other.imp);
}
public:
WidgetImpl* imp;
};
namespace std {
template<>
void swap<Widgt>(Widgt&a, Widgt&b) {//std::swap 特化版本
a.swap(b);
}
}
"template<>"表示它是std::swap的一个全特化(total template specialization)版本。换句话说当一般性的swap template施行于widgets身上便会启用这个版本。通常我们不能够(不被允许)改变std命名空间内的任何东西,但可以(被允许)为标准templates(如swap)制造特化版本,使它专属于我们自己的classes(例如widget)。
这种做法的好处:
C++只允许对类模板偏特化,在函数模板上偏特化是行不通的,因此不能用3的方法。
正确方法:
在一个命名空间中定义一个swap 和 Widget WidgetImpl 等等模板, 在这个空间中的swap调用类中的成员函数。但与此同时建议提供一个std的特化Swap版本。
为了简化起见,假设Widget的所有相关机能被置于命名空间WidgetStuff,于是:
namespace WidgetStuff {
... //模板化的WidgetImpl等等
template<typename T> //和前面一样,内含swap成员函数
class Widget { ... };
...
template<typename T> //non-member swap函数
void swap(Widget<T>& a, //这里并不属于std命名空间
Widget<T>& b)
{
a.swap(b);
}
}
此时调用swap,C++的名称查找法则(name lookup rules;更具体的说是所谓argument-dependent lookup或Kobeig lookup法则)将会找到WidgetStuff内的Widget专属版本,这正是我们所希望的。
注意:虽然上面的做法对于class和classtemplate都行得通,但我们还是应该为class特化std::swap。所以,如果我们想让“class专属版”的swap在尽可能多的语境下被调用,我们就应该同时在该class所在命名空间内写一个non-member版本以及一个std::swap特化版本。
从用户角度考虑,假设正在写一个function template,其内需要置换两个对象值。
template<typename T>
void doSomething(T& obj1, T& obj2)
{
...
swap(obj1, obj2);
...
}
(1)此时,调用了swap,但是调用的是哪一个一般化版本?
我们希望的是调用T专属版本,并在该版本不存在的情况下,再去调用std内的一般化版本,那么正确的写法如下:
template<typename T>
void doSomething(T& obj1, T& obj2)
{
usint std::swap; //令std::swap在此函数内可用
...
swap(obj1, obj2); //为T型对象调用最佳swap版本
...
}
一旦编译器看到了对swap的调用,它们便查找适当的swap并加以调用。C++的名称查找法则会确保将找到global作用域或者T所在的命名空间内的任何T专属的swap。
(2)C++的名称查找法则的具体做法:
首先,如果swap的缺省实现对我们的class或class template提供可接受的效率,那么我们并不需要做其他的事情。
其次,如果swap的缺省版本效率不足(通常就是因为class或者class template使用了某种pimpl手法),则:
最后,如果我们调用swap,请确定包含一个using声明式,以便让std::swap在我们的函数内部可以曝光可见,然后不加任何namespace修饰符,直接去调用swap。
原因
swap的一个最好的应用就是为了帮助class(和class template)提供强烈的异常安全性(exception-safety)保障。
适用
当然,这一约束只施行于成员版!不可实施于非成员版,因为swap缺省版本是以copy构造函数和copy assignment操作符为基础的,在一般情况下是允许抛出异常的。
因此,当我们写一个自定义版本的swap时,往往需要提供以下两点:
一般而言,上面这两个特性是连在一起的,因为高效率的swap几乎总是基于对内置类型的操作(例如pimpl首发的底层指针),而内置类型上的操作绝不会抛出异常。
记住:
参考:https://blog.csdn.net/lintianyi9921/article/details/103799093
原因:
如果变量只在循环内使用,那么把它定义于循环外并在每次循环迭代时赋值给它比较好,还是该把它定义于循环内?也就是说下面左右两个一般性结构,哪一个比较好?
在widget函数内部,以上两种写法的成本如下:
当类的一个赋值成本低于一组构造+析构成本,A大体比较高效,否则做法B更好。另外A版本造成变量的作用域比B的大,这可能对于程序的易理解性、易维护性有一定的冲击。
因此,除非
否则应该选择做法B。
C++中转型破坏了类型系统。那可能导致任何种类的麻烦,有的容易辨识,有些非常隐晦。C、java、c#语言中可能转型是必要的、无法避免的,相比于C++也比较不那么危险。但是C++中,应该尽量少的做转型,C++中使用转型比较危险,应该尽量将转型动作使用不转型的手法给化解掉。
C++的转型并不是在原变量地址上做转型,而是创建了一个临时对象。
(1)形式一:C语言风格的转型语法:
(T)expression //将expression转换为T类型
(2)形式二:函数风格的转型:
T(expression) 将expression转换为T类型
(3)形式三:C++风格的转型语法
cpp const_cast(expression);//const->non const
dynamic_cast(expression);
reinterpret_cast(expression);
static_cast(expression);
注意:形式一、二并无差别,统称旧式转型,形式三称为新式转型。
新式转型的优点
任何一种转型动作往往真的令编译器额外地编译出运行期间执行的代码,非指针转型会创建一个额外的副本,而不是在原对象的内存空间上操作:
class A{
public:
int A_mem;
A(){ cout<<"A构造"<<endl; }
A(const A&a):A_mem(a.A_mem){ cout<<"A复制构造"<<endl;}
~A(){ cout<<"A析构"<<endl; }
};
class B:public A{
int B_mem;
};
int main()
{
B b;
static_cast<A>(b);
cout<<"---"<<endl;
}
//输出:A构造 A复制构造 A析构 ---
//A析构
dynamic_cast在运行期执行,很多实现都是使用strcmp比较class名称,执行速度相当的慢。尤其是在深度继承和多重继承中,速度更慢,因此应尽量避免,尤其是循环中的dynamic_cast,更要避免。
优良的C++代码很少使用转型,若说要完全摆脱它们又太过不切实际,但是我们应该尽量避免转型。就像面对众多蹊跷可疑的构造函数一样,我们应该尽可能隔离转型动作,通常是把它隐藏在某个函数内,函数的接口会保护调用者不受函数内部任何肮脏龌龊的动作的影响。
请记住:
reference、指针、迭代器系统都是所谓的handles(号码牌,用来获得某个对象)。函数返回一个handle,随之而来的便是“减低对象封装性”的风险。它也可能导致:虽调用const成员函数却造成对象状态被更改的风险。
class Rectangle{
……
public:
Point& upperLeft()const{return pData->ulhc;}//返回了引用,可以修改所致的对象
Point& lowerRight()const{return pData->lrhc;}
};
上面两个函数就是返回handle的成员函数。
不论这所谓的handle是个指针或迭代器或reference,也不论这个handle是否为const,也不论那个返回handle的成员函数是否为const。这里的唯一关键是**,有个handle被传出去了,一旦如此你就是暴露在"handle比其所指对象更长寿”的风险下**。当使用handle成员函数的对象是个临时量时,语句结束,该临时量消失,handle就变成了空悬。
这并不意味你绝对不可以让成员函数返回handle。有时候你必须那么做。例如operator[]就允许你“摘采”strings和vectors的个别元素,而这些operator[]s就是返回references指向“容器内的数据”(见条款3),那些数据会随着容器被销毁而销毁。尽管如此,这样的函数毕竟是例外,不是常态。
“异常安全”有两个条件,即当异常被抛出时,带有异常安全性的函数会:
异常安全函数提供以下三个保证之一:
异常安全码(Exception-safe code)必须提供上述三种保证之一。如果它不这样做,它就不具备异常安全性。
对于changeBackground()函数而言,为了尽量接近强烈保证,可以更改为下面的代码:
class PrettyMenu {
//...
private:
std::tr1::shared_ptr<Image> bgImage;
};
//重写修改PrettyMenu的成员函数
void PrettyMenu::changeBackground(std::istream& imgSrc) {
Lock ml(&mutex);
bgImage.reset(new Image(imgSrc));
++imageChanges;
}
这两个改变几乎足够让changeBackground提供强烈的异常安全保证。美中不足的是参数imgSrc.如果Image构造函数抛出异常,有可能输入流(input stream)的读取记号(read marker)已被移走,而这样的搬移对程序其余部分是一种可见的状态改变。所以changeBackground在解决这个问题之前只提供基本的异常安全保证。
因此,需要使用copy-and-swap策略,这个一般化的设计策略很典型地会导致强烈保证。
原则:为你打算修改的对象(原件)做一份副本,然后在副本身上做修改:
实现上通常是使用pimpl idiom(pimpl惯用法):将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(implementation object,即副本)。
//将bgImage和imageChanges从PrettyMenu独立出来,封装到一个结构中
struct PMImpl {
std::tr1::shared_ptr<Image> bgImage;
int imageChanges
};
class PrettyMenu {
//...
private:
std::tr1::shared_ptr<PMImpl> pImpl; //创建一个该结构
};
//使用copy and swap
void PrettyMenu::changeBackground(std::istream& imgSrc) {
using std::swap; //见条款25
Lock ml(&mutex);
//以pImpl为原件,创建一个副本,然后在副本上做修改
std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc));
pNew->imageChanges++;
//如果上面副本的修改没有抛出异常,那么交换副本与原件
swap(pImpl, pNew);
}
注意:copy and swap只能保证这部分有强烈的异常安全性,如果调用的其他函数异常安全性比“强烈保证”低,则不能保证整体强烈异常安全;
又或者将多个具有强烈异常安全性的函数顺序调用,则会有“连带影响”:前面函数调用成功,后面的函数抛出异常,也不能恢复整体状态,因此也不能提供“强烈保证”。
此外,copy and swap创建副本的消耗可能无法接受,所以“强烈保证”并非任何时刻都显得实际。
记住:
inline只是对编译器的一个建议,不是强制命令,编译器可能接受也可能忽略。
优点:
缺点:
隐式内联:
当函数定义在类的内部时(包括友元函数),这个函数是隐式inline的(隐式内联只有这一种情况,构造、析构、虚函数除外)。
class Person {
public:
//隐式内联(编译器自动申请),这个函数不仅在类中声明,还在类中进行了定义
int age()const { return theAge; }
private:
int theAge;
};
构造函数与析构函数除外,因为一般编译器会在其中添加很多隐藏代码。虚函数在运行期确定,因此也不内联。
显式内联:
我们也可以通过inline关键字显式的指出一个函数作为内联函数。例如:
template<typename T>
inline const T& std::max(const T& a, const T& b)
{
return a < b ? b : a;
}
注意:inline函数通常被置于头文件内,因为大多数建置环境在编译过程中进行inlining,编译器需要知道内联函数长什么样子。inlining在大多数C++程序中是编译期的行为(但是也有少数情况是在运行期链接期完成inlining)。
即使你将函数声明为inline的,但是在有些情况下编译器会拒绝将函数作为inlining。例如:
注意:g++是否内联可能和编译选项中的优化等级相关:不优化时不进行内联,开优化才内联。
将大多数inlining限制在小型、被频繁调用的函数身上。这可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
假如你修改了C++ class实现文件,修改的仅仅是实现,而没有修改接口,而且只修改private部分。此时,重新构建这个程序时,会发现整个文件、以及用到该class 的文件都被会被重新编译和连接,这不是我们想要看到的。
问题出在C++没有把关于接口与实现相分离这件事做好。C++ 的class 的定义式中不仅定义了接口,还定义了实现细目(成员变量)。
例如:
class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
private:
std::string theName; //实现细目
Date theBirthDate; //实现细目
Address theAddress; //实现细目
};
当编译器没有取得实现代码所需要的class string,Date和Address的定义式时,它无法通过编译它所需要的这样的定义式往往由#include <>提供(里面有class string,Date和Address的实现代码)。例如本例中需要:。
#include
#include "date.h"
#include "address.h"
如果这些头文件中(或头文件所依赖的头文件)的一个的实现被改变了,那么每一个用到class 类的文件都得重新编译。这就是所谓的文件间的依存度比较高。
有两个手法可以降低编译依存关系:
当编译器看到一个自定义类型对象时,它必须通过该类的定义得知为该对象分配多少内存,但是如果看到的是该对象的指针,只需要清楚为该指针分配多少内存。
因此,可以将对象的实现细目隐藏到一个指针(通常是一个智能指针)背后。
例如:
#include
#include
class PersonImpl;
class Date;
class Address;
class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name()const;
std::string birthDate() const;
std::string address()const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl; // 指向实现物的指针
};
上述程序中,将原本的Person 类写成两个部分,接口那部分是主要的部分,其中含了一个智能指针,指向实现细目。而实现细目另外定义了一个类:PersonImpl。这种设计手法被称为:pimpl idiom。
注意:pimpl 指的是 pointer to implementation。这种class内的指针往往被称为:pImpl指针。上述class的写法 往往被称为handle class。
实现了接口与实现的分离。即:Person的客户与 Date、Address、以及Person的实现细目就分离了。
实现接口与实现的分离所带来的的好处:
这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。其他每一件事都源自于这个简单的设计策略:
如果用 object reference 或 object pointer 可以完成任务,就不要用 objects。
可以只靠声明式定义出指向该类型的 pointer 和 reference;但如果定义某类型的 objects,就需要用到该类型的定义式。
如果能够,尽量以 class 声明式替换 class 定义式。
当你声明一个函数而它用到某个 class 时,你并不需要该 class 的定义式,纵使函数以 by value 方式传递该类型的参数(或返回值)亦然:
class Date;
Date today();
void clearAppointments(Date d);
只有使用了这两个函数的客户才需要定义Date。将“提供class定义式”(通过#include完成)的义务从“函数声明所在”之头文件移转到“内含函数调用”之客户文件,便可将“并非真正必要之类型定义”与客户端之间的编译依存性去除掉。
为声明式和定义式提供不同的头文件。
两个头文件应该包吃一致性,其中一个头文件发生改变,另一个就得也改变。一个内含了class 接口的定义,另一个仅仅内含声明。
例如:只含声明式的Date class 的头文件应该命名为datefwd.h(有一定的命名规则),该命名规则出自C++标准程序库头文件
。
注意:本条款适用于template 也适用于 non-template。template如果想要分离声明式和定义式,则需要使用关键字export。
令Person class 成为一种特殊的abstract base class (抽象基类),称为interface class。这样的类通常:没有成员变量,也没有构造函数,只有一个virtual 的析构函数以及一组pure virtual 用来描述接口。
像.net和java 的接口,他们不允许在接口类中定义成员函数和成员变量。但是C++的接口类并不禁止,这样的规则使得C++语言具有更大的弹性。
由于这样的类往往没有构造函数,因此通过工厂函数或者virtual构造函数创建,他们返回指针,指向动态分配对象所得的对象,这样的对象支持interface class的 接口,这样的函数在interface class往往被声明为 static,例如:
class Person{
public:
...
static std::tr1::shared_ptr<Person>
create(const std::string& name, const Date& birthday, const Address& addr);
};
客户使用他们像这样:
std::string name;
Date dateBirth;
Address address;
std::tr1::shared_ptr<Person> pp(Person::create(name, dateBirth, address));
...
std::cout << pp->name()
<< "was born on "
<< PP->birthDate()
<< " and now lives at "
<< pp->address();
...
当然支持 interface class 接口的那个具象类(concrete classes)必须被定义出来,而真正的构造函数必须被调用。
假设有个 derived class RealPerson,提供继承而来的 virtual 函数的实现:
class RealPerson : public Person{
public:
RealPerson(const std::string& name, const Date& birthday, const Address& addr)
: theName(name), theBirthDate(birthday), theAddress(addr)
{}
virtual ~RealPerson(){}
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
有了 RealPerson 之后,写出 Person::create 就真的一点也不稀奇了:
std::tr1::shared_ptr<Person> Person::create(const std::string& name, const Date& birthday, const Address& addr)
{
return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
一个更现实的 Person::create 实现代码会创建不同类型的 derived class 对象,取决于诸如额外参数值、独自文件或数据库的数据、环境变量等等。
RealPerson 示范实现了 Interface class 的两个最常见机制之一:从 interface class 继承接口规格,然后实现出接口所覆盖的函数。第二个实现设计条款40的多重继承。
handle classes 和 interface classes 解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。
handle classe
成员函数必须通过 implementation pointer 取得对象数据。那会为每一次访问增加一层间接性。每个对象消耗的内存必须增加一个 implementation pointer 的大小。 implementation pointer 必须初始化指向一个动态分配的 implementation object,所以还得蒙受因动态内存分配带来的额外开销。
Interface classe
由于每个函数都是 virtual,必须为每次函数调用付出一个间接跳跃。此外 Interface class 派生的对象必须内含一个 vptr(virtual table pointer)。
在程序开发过程中使用 handle class 和 interface class 以求实现码有所改变时对其客户带来最小冲击。
而当他们导致速度和/或大小差异过于重大以至于 class 之间的耦合相形之下不成为关键时,就以具象类(concrete class)替换 handle class 和 interface class。
记住:
“is-a”的概念:
记住:
“public”继承意味着is-a。适用于base classes身上的每一件事一定也适用于derived身上,因为每一个derived对象也是一个base class 对象。
当不同作用域有相同名称的变量、函数时,需要用到名称遮掩/隐藏。
名称遮掩规则做的事就是:遮掩名称。至于名称是否应和相同或不同的类型对应,并不重要。
只要找到名称相同的函数,无论参数是否相同,都算查找完毕,如果参数不同则出错。
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived :public Base
{
public:
virtual void mf1(); //基类中的所有mf1()都被隐藏
void mf3(); //基类中的所有fm3()都被隐藏
void mf4();
};
调用代码:
Derived d;
int x;
d.mf1(); //正确
d.mf1(x); //错误,Base::fm1(int)被隐藏了
Base *b=new Derived;
b->mf1(x);//正确,多态还是可以实现
d.mf2(); //正确
d.mf3(); //正确
d.mf3(x); //错误,Base::mf3(double)被隐藏了
主要有两种办法可以解决名称遮掩问题:
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived :public Base
{
public:
using Base::mf1; //Base所有版本的mf1函数在派生类作用域都可见
using Base::mf3; //Base所有版本的mf3函数在派生类作用域都可见
virtual void mf1();
void mf3();
void mf4();
};
Derived d;
int x;
d.mf1(); //正确,调用Derived::mf1()
d.mf1(x); //正确,调用Base::mf1(int)
d.mf2(); //正确,调用Derived::mf2()
d.mf3(); //正确,调用Derived::mf3()
d.mf3(x); //正确,调用Base::mf3(double)
如果你继承base class并加上重载函数,而你又希望重新定义或覆写(推翻)其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩。
注:using只能声明该类能访问(protected或public访问级别)的成员。using详细介绍
当我们只想继承一系列重载函数的部分函数,而不是全部时,不处理会导致全部继承,使用using会导致某给定名称的所有同名函数在派生类中都可见,因此需要用到转交函数(转交函数能够同时取消私有继承的限制):
class Base
{
public:
virtual void mf1();
virtual void mf1(int);
};
class Derived :private Base //私有继承
{
public:
//这是一个转交函数
virtual void mf1() {
Base::mf1(); //调用基类的mf1()函数
}
};
Derived d;
int x;
d.mf1(); //正确,虽然调用的是Derived::mf1(),但是本质上调用的是Base::mf()
d.mf1(x); //错误,Base::mf(double)被隐藏了
此外,转交函数可以实现uisng的功能。
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
};
class Derived : public Base {
public:
virtual void mf1();
};
Derived d;
int x;
d.mf1(); //正确,调用Derived::mf1()
d.Base::mf1(x); //正确,调用Base::mf1(int)
记住:
public
继承细分实际上细分为:函数接口继承和函数实现继承。这两种细分更像是函数声明和函数定义之间的差异。
从这两个角度出发,public
继承可以分为:
pure virtual
。注意:pure virtual
函数是可以写定义的,但是只能通过类名调用(Base::fun())。
virtual
。non-virtual
。记住:
大多时候,我们会自然而然的想到使用virtual手法来塑模现实中的类。但是,实际上也有别的方案可以替代virtual手法的,即:考虑virtual函数以外的其他选择。下面介绍的便是几种可以替代virtual的方案。
class GameCharacter {
public:
//1、派生类不可改变的算法骨架
int healthValue() const
{
...//做一些事前工作
int retVal = doHealthValue();//核心点
...//做一些事后工作
return retVal;
}
private:
//2、派生类可以改变的算法细节
virtual int doHealthValue() const //derived class 可以重新定义它。
{
...//缺省计算,计算健康指数
}
};
这个实现方法的优点:可以在核心点前做准备工作,和核心点后做善后工作。
注意:准备工作可以是:锁定互斥器、验证class约束条件、验证函数先决条件等等。善后工作可以是:解除互斥器锁定、验证函数的事后条件、再次验证class约束条件等等。
在游戏角色类中添加一个指针,指向一个计算健康指数的函数。
通过Funtion Pointers完成Strategy与使用virtual函数实现的比较:
上面的方法是让游戏角色类拥有一个指向函数的指针。而这个方案是让游戏角色类拥有一个像函数一样的函数对象。
注意:对于宣称只接收一个参数的成员函数,实际上接收两个参数,包括隐式的(*this),因此转化为function对象时得先用bind函数将 *this 对象绑定到成员函数上:
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
//其余部分同上
//只是将函数指针改为了function模板,其接受一个const GameCharacter&参数,并返回int
typedef function<int(const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
:healthFunc(hcf) {}
int healthValue() {
return healthFunc(*this);
}
private:
HealthCalcFunc healthFunc;
};
class EvilBadGuy :public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
:GameCharacter(hcf) {}
//..
};
//其提供一个成员函数,用以计算健康
class GameLevel {
public:
float health(const GameCharacter&)const{}
};
int main()
{
//人物,其使用GameLevel类的health()成员函数来计算健康指数
GameLevel currentLevel;
EvilBadGuy ebg2(bind(&GameLevel::health, currentLevel, placeholders:: _1));
//或者EvilBadGuy ebg2(bind(&GameLevel::health, ¤tLevel, placeholders:: _1));
}
实际上也就是说,健康指数计算函数不再是游戏角色类的成员函数了,这也意味着他们不能直接访问游戏角色类的成员了。
弱化游戏角色类的封装,这也是这种替代方案的缺点,这是下面Strategy设计模式都要需要面临的问题。
方案一:游戏角色类可以将该函数声明为友元。
方案二:将游戏角色类的某些成员声明为public。
弱化了封装。
记住:
class B {
public:
virtual void mf();
};
class D :public B {
public:
virtual void mf();
};
D x;
B *pB = &x;
pB->mf(); //调用B::mf()
D *pD = &x;
pD->mf(); //调用D::mf()
原因:
non-virtual函数都是静态绑定(程序编译过程中,把函数调用与响应调用所需的代码结合的过程称之为静态绑定)的,即:假如P是基类指针,无论P指向的基类对象还是派生类对象,它发起函数调用的版本都是基类的版本。不同于与virtual的动态绑定。
注:如果不通过指针而是直接通过对象调用,也属于静态绑定。
记住:
绝对不要重新定义继承而来的non-virtual函数。
因为virtual 函数体是动态绑定的,但是virtual 函数的缺省参数是静态绑定的。假如通过指针或者引用访问重新定义继承而来的virtual函数的缺省参数值,则会造成错误。(例如:函数体为派生类版本,但是缺省参数却是基类版本。)
注:C++编译器将 virtual函数体设为动态的,而将virtual的缺省参数设为静态的是出于效率的考虑。如果缺省参数设为动态的,程序运行机制会变得更慢、更复杂。
class Shape {
public:
enum ShapeColor {Red, Green, Blue};
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle: public Shape{
public:
virtual void draw(ShapeColor color = Red) const;
...
};
代码的缺点:
ShapeColor color
等于Red
)class Shape {
public:
enum ShapeColor {Red, Green, Blue};
void draw(ShapeColor color = Red) const //如今它是non-virtual
{
doDraw(color); //调用一个virtual
}
...
private:
virtual void doDraw(ShapeColor color) const = 0; //真正的工作在此处完成
};
class Rectangle: public Shape{
public:
...
private:
virtual void doDraw(ShapeColor color) const; //注意,无须指定缺省参数值。
...
};
Shape * rec = new Rectangle();
rec->draw(); // 缺省参数调用,缺省参数为Red 矩形版本draw()
rec->draw(Green); // 带参数调用 矩形版本draw()
优点:
记住:
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual 函数——你唯一应该覆盖的东西——却是动态绑定。
复合是类型之间的一种关系,当某种类型的对象包含其它类型对象时,便是这种关系。复合有很多同义词:layering(分层),containment(内含),aggregation(聚合),embedding(内嵌)。
has-a
关系is-impemented-in-terms-of
关系区分这两种关系的依据:你正打算在你的软件中处理两个不同的领域:
记住:
derived class
对象转换为一个base class
对象(无法类型转换)。private base class
继承而来的所有成员,在derived class
中都会变成private
属性,纵使它们在base class
中原本是protected
或public
属性。private
继承 意味implemented-in-terms-of 。private
继承意味只有实现部分被继承,接口部分应略去。复合和private
继承都是描述implemented-in-terms-of
关系,那么在类设计时应该使用复合还是private继承该如何选择呢?
主要是在protected成员或virtual函数牵扯进来的时候(比如派生类想要使用基类protected成员,或需要重新定义继承而来的virtual函数)。
另一种更激进的情况:当基类为空类时,可以起到节约内存空间的作用。(如果客户对于内存空间的要求极高的话,可以采用private继承),示例:
class Empty {}; //空类
class HoldsAnint :private Empty {
private:
int x;
Empty e;
};
sizeof(HoldsAnint); //8
之所以是8而不是4,是因为C++标准要求凡是独立(非附属)对象必须有非0大小(通过默默安插一个char到空对象内)。非附属/独立对象,指的是不以“某对象之base class成分”存在的对象。
class Empty {};
class HoldsAnint :private Empty {
private:
int x;
};
sizeof(HoldsAnint); //4
上面的约束不适用于derived class对象内的base class成分,因为它们并非独立(非附属)。EBO不适用于多重继承。注意:“empty class
”:这里的空类是指不包含non-static
成员,但可以包含:typedefs
,enums
,static
成员变量或者non-virtual
函数。
记住:
clas BorrowableItem{
public:
void checkOut();
……
};
class ElectronicGadgent{
private:
bool checkOut() const;
……
};
class MP3Player: public BorrowableItem, public ElectronicGadget
{
……
};
MP3Player mp;
mp.checkOut();//歧义,调用哪个checkOut?
虽然上面两个函数一个是public
,一个是private
,但还是有歧义。
这与C++用来解析(resolving)重载函数调用的规则相符:在看到是否有个函数可取用之前,C++首先确认这个函数对此调用是最佳匹配。找出最佳匹配函数后才检验其可取用性。
本例的两个checkouts有相同的匹配程度,没有所谓最佳匹配。因此ElectronicGadget::checkout
的可取用性也就从未被编译器审查。
调用时写清楚调用的函数版本,符合编译器规则就能够被正确调用。
mp.BorrowableItem::checkOut(); //成功调用
多重继承的意思是继承一个以上的base classes,但这些base classes并不常在继承体系中又有更高级的base classes,因为那会导致要命的“菱形多重继承”:
class File { ... };
class InputFile : public File { ... };
class OutputFile : public File { ... };
class IOFile : public InputFile, public OutputFile { ... };
任何时候如果你有一个继承体系而其中某个基类和某个派生类之间有一条以上的相通路线,你就必须面对这样一个问题:是否打算让基类内的成员变量经由每一条路径被复制?
两个方案C++都支持——虽然其缺省做法是执行复制。如果不想复制,必须令那个带有此数据的基类(也就是File)成为一个虚基类,令所有直接继承自它的派生类采用虚继承:
class File { ... };
class InputFile : virtual public File { ... };
class OutputFile : virtual public File { ... };
class IOFile : public InputFile, public OutputFile { ... };
virtual
继承的class
比non-virtual
继承的class体积要大、访问成员变量的速度也相比较慢,此种细节因编译器不同而异。virtual
继承的初始化及赋值情况复杂,且不直观:virtual base的初始化责任是由继承体系中的最低层(most derived)class负责,这暗示
virtual base classes
的建议多重继承有一个通情达理的应用:将"public继承自某接口”和"private继承自某实现”结合在一起:
class IPerson {
public:
virtual ~IPerson();
virtual std::string name()const = 0;
virtual std::string birthDate()const = 0;
};
class DatabaseID {};
class PersonInfo {
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName()const;
virtual const char* theBirthDate()const;
private:
virtual const char* valueDelimOpen()const;
virtual const char* valueDelimClose()const;
};
class CPerson :public IPerson, private PersonInfo {
public:
explicit CPerson(DatabaseID pid) :PersonInfo(pid) {}
virtual std::string name()const = 0{
return PersonInfo::theName();
}
virtual std::string birthDate()const = 0 {
return PersonInfo::theBirthDate();
}
private:
virtual const char* valueDelimOpen()const;
virtual const char* valueDelimClose()const;
};
多重继承只是面向对象的一个工具。和单一继承比较,它通常比较复杂,使用上也比较难以理解,所以如果你有个单一继承的设计方案,而它大约等价于一个多重继承设计方案,那么单一继承设计方案儿乎一定比较受欢迎。
如果你唯一能够提出的设计方案涉及多重继承,你应该更努力想一想——几乎可以说一定会有某些方案让单一继承行得通。
然而多重继承有时候的确是完成任务之最简洁、最易维护、最合理的做法,果真如此就别害怕使用它。
请记住:
面向对象编程总是以显式接口和运行期多态来解决问题。例如:
class Widget{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
virtual swap(Widget& other);
};
void doProcessing(Widget& w)
{
if (w.size() > 10 && w != someNastyWidget){
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
由于w的类型被声明为Widget,因此w需要Widget接口,并且我们可以在源码中找到这个接口,看到源码的样子,所以称为是显式接口。
由于Widget的某些函数是虚函数,因此w的某些函数在运行期间才可以根据w的类型动态调用相关版本的函数,这就是所谓的运行期多态。
在泛型编程中,显式接口与运行期多态仍有使用,但是其主要应用的是隐式接口和编译期多态。
例如将刚才的函数改为函数模板:
template<typename T>
void doProcessing(T& w)
{
if (w.size() > 10 && w != someNastyWidget){
T temp(w);
temp.normalize();
temp.swap(w);
}
}
void doProcessing(T& w) //w需要支持的操作都是隐式接口
这个时候w发生了什么样的改变呢?
w所需要支持的接口需要当函数模板具现化时执行于w身上的操作决定(执行了什么操作,说明w一定需要支持这些接口),例子中w使用了size、normalize、swap函数、copy构造函数、不等比较。并且if语句中还有一个长表达式。这所有的函数与长表达式便是T必须支持的一组隐式接口(其实就是w需要被约束的东西)。
使用到w的任何函数调用,都可能会造成模板具现化,这样的函数具现化发生在编译期。“以不同的template参数具体化函数模板”会导致调用不同的函数,这便是所谓的编译期多态。
通常显式接口是由函数的签名式(函数名称、参数类型、返回类型)构成。
但是隐式接口不是基于签名式的,而是由一组有效表达式构成。
例如:
template<typename T>
void doProcess(T& w) {
if(w.size()>10&&w!=someNastyWidget)
……
}
w.size()>10&&w!=someNastyWidget//这就是所谓的隐式接口之一,是一组有效表达式。
w的隐式接口似乎有下述的约束:
由于运算符重载的也行,上面的两个约束都不需要满足:
此外,隐式表达式还包括:上述表达式需要与bool兼容,copy构造函数、normalize和swap 也 都必须对T型对象有效。
记住:
typename可以在template中声明类型参数,此时等价于class:
//两者是等价的
template<class T> class Widget;
template<typename T> class Widget;
可以用来告诉编译器后面跟随的名称是一个类型名。
//编译错误,无法通过
template<typename C>
void print2nd(const C& container)
{
if (container.size() >= 2) {
C::const_iterator iter(container.begin()); //初始化迭代器,绑定到第一个元素上
++iter;
int value = *iter;
std::cout << value;
}
}
关于“嵌套从属名称”和“非从属名称”的概念:
vector
在我们知道C是什么之前,没有任何办法可以知道C::const-iterator是否为一个类型或是静态变量等等。而当编译器开始解析template print2nd时,尚未确知C是什么东西。
C++有个规则可以解析(resolve)此一歧义状态:如果解析器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。所以缺省情况下嵌套从属名称不是类型。此规则有个例外,稍后我会提到。
解决的方法就是在其之前放置关键字tyname:
template<typename C>
void print2nd(const C& container)
{
if (container.size() >= 2) {
//使用typename,显式告诉编译器,const_iterator是一个类型
typename C::const_iterator iter(container.begin());
++iter;
int value = *iter;
std::cout << value;
}
}
C++11后还有其他办法,比如使用auto代替C::const_iterator
。
一般性规则:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename。
但有两个例外:typenane不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列表)中作为base class修饰符。例如:
template<typename T>
class Derived :public Base<T>::Nested //此处不可以使用typename
{
public:
explicit Derived(int)
:Base<T>::Nested(x)//此处不可以使用typename
{
typename Base<T>::Nested temp; //此处可以使用typename
}
};
总之,typename关键字告诉了编译器把一个特殊的名字解释成一个类型。保险起见,你应该在所有编译器可能错把一个type当成一个变量的地方使用typename。
记住:
在继承模板基类时,C++拒绝在模板化基类(templatized base classes)内寻找继承而来的名称,例如,对于以下模板基类:
template<typename T>
class Base{
public:
void fun(){
...
}
}
以下代码通不过编译:
template<typename T>
class Derived:public Base<T>{
public:
void useFun(){
fun(); //通不过编译,因为编译器拒绝在Base类模板中查找fun
}
}
因为基类模板(base classe templates)有可能被特化,而那个特化版本可能不提供和一般性template相同的接口(如下),因而C++拒绝在模板化基类中查找继承而来的名称。
全特化版本(不再有fun函数):
template<>
class Base<int>{
public:
void foo(){//fun()被 foo()替代
...
}
}
我们有三种办法令C++"不进入templatized base classes观察”的行为失效:
在基类函数调用动作之前加上this->,即将对fun的调用改为如下:
template<typename T>
class Derived:public Base<T>{
public:
void useFun(){
this->fun(); //通不过编译,因为编译器拒绝在Base类模板中查找fun
}
};
使用using 声明式,使编译器在模板作用域中查找改名字:
template<typename T>
class Derived:public Base<T>{
public:
void useFun(){
this->fun(); //通不过编译,因为编译器拒绝在Base类模板中查找fun
}
};
这里using声明式的作用和条款33不同,它解决的并不是基类名字被派生类掩盖的问题,而是编译器不进入base class作用域内查找的问题。
明确指出被调用的函数位于base class内:
template<typename T>
class Derived:public Base<T>{
public:
void useFun(){
Base<T>::fun(); //通不过编译,因为编译器拒绝在Base类模板中查找fun
}
};
这往往是最不让人满意的一个解法,因为如果被调用的函数是虚函,上述的"明确资格修饰"(explict qualification)会关闭"virtual绑定行为"。
记住:
可在derived class templates内通过"this->"指涉base class templates内的成员名称,或通过使用using声明式,或藉由一个明白写出的"base class资格修饰符”完成。
以下是用于操作方矩阵的类模板,该矩阵还支持一个逆矩阵运算的方法。
//矩阵的元素类型为T,矩阵的大小为n*n类型(模板第二个类型为非类型参数)
template<typename T, std::size_t n>
class SquareMatrix {
public:
void invert(); //求逆矩阵
};
注:n为非类型参数
那么对于以下代码:
SquareMatrix<double,5> sm1;
sm1.invert();
SquareMatrix<double,10> sm2;
sm2.invert();
具现化sm1和sm2时,尽管它们类型参数相同,都为double,但由于非类型参数不同,SquareMatrix会产生两份实体,包括其成员函数invert等,而它们的差别仅在于非类型参数n,这明显产生了大量重复的代码,初步改进方法如下:
//不论SquareMatrix产生多少粉,SquareMatrixBase在代码中只产生一份
template<typename T>
class SquareMatrixBase {
protected:
void invert(std::size_t matrixSize);
};
template<typename T,std::size_t n>
class SquareMatrix :private SquareMatrixBase<T> //私继承:is-implemented-in-terms of关系
{
private:
using SquareMatrixBase<T>::invert; //避免派生类隐藏基类的invert函数
public:
void invert() {
this->invert(n); //为什么使用this,参阅条款43
}
};
以上代码中,base class SquareMatrixBase含有一个带大小参数的invert,与之前SquareMatrix不同,它只对矩阵元素类型参数化,并不对矩阵大小参数化。因此对于同一类型参数,SquareMatrixBase只具现化出一份实体,结果就是类型参数相同,非类型参数不同的SquareMatrix虽然具现化出不同实体,但继承的是同一个SquareMatrixBase。SquareMatrixBase只是用来帮助derived classes实现,两者关系为is-implemented-in-terms of,因而采用priva继承。
至于矩阵内容,可以储存在派生类对象中或堆中,基类包含指向矩阵内容的指针。
效率比较:
所谓work set是指对一个在“虚内存环境”下执行的进程而言,其所使用的那一组内存页。
本条款是讨论由non-type template parameters(非类型模板参数)带来的膨胀,其实type parameters(类型参数)也会导致膨胀,比如:
vector
和vector
的成员函数有可能完全相同——造成代码重复/膨胀。有些链接器(linkers)会合并相同的函数实现码,但有些不会。后者意味着模板被具体化为int和long两个版本,并因此造成代码重复/膨胀。list
,list
,list*>
等等)常常应该为每一份成员函数提供唯一一份被不同类型参数共享的底层实现。也就是说,如果某些成员函数操作强类型指针(T*)等,应该令它们调用另一个操作无类型指针void*
的函数,由后者完成实际工作。某些C++标准程序库的实现版本的确为vector,list等templates做了这件事。记住:
class Top {};
class Middle :public Top {};
class Bottom :public Middle {};
//自己设计的智能指针类
template<typename T>
class SmartPtr
{
public:
explicit SmartPtr(T* realPtr);
};
int main()
{
//下面是我们希望能完成的,但是还没有实现
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top> pct2 = pt1;
return 0;
}
对于上述不同参数的智能指针之间相互转换,如何实现?
解决方法:使用成员函数模板。
template<typename T>
class SmartPtr
{
public:
//拷贝构造函数,是一个成员函数模板
typename<typename U>
SmartPtr(const SmartPtr<U>& other);
};
我们可以为自己的只能指针类提供一个类似于shared_ptr的get()成员函数,这个函数返回智能指针锁封装的那个原始指针
设计的代码如下:
template <typename T>
class SmartPtr{
public:
typename<typename U>
SmartPtr(const SmartPtr<U>& other)
: heldPtr(other.get()) { ... }
T* get() const { return heldPtr; }
private:
T* heldPtr;
};
这样就把检查底层指针能否转换的任务交由底层指针来自行检查。
同理,成员模板函数(member template function)可用于支持赋值操作:
template<typename T>
class shared_ptr{
public:
template<class Y> //声明类型参数时class和typename含义相同
explict shared_ptr(Y* p);
template<class Y>
shared_ptr(shared_ptr<Y> const& r);
template<class Y>
explict shared_ptr(weak_ptr<Y> const& r);
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const& r);
...
}
以上所有构造函数都为explict,唯有泛化copy构造函数除外,这意味着shared_ptr允许shared_ptr之间的隐式类型转换,而禁止原始指针或其他智能指针类型向shared_ptr的转换。
注意事项:
虽然成员函数模板(member function template)可以实例化出用于shared_ptr
向shared_ptr
转换的copy构造函数,但如果没有声明copy构造函数,编译器还是会合成一个,所以不能依赖于成员函数模板的实例化,要手动声明一个普通的copy构造函数,copy赋值操作符也是一样。
记住:
对于条款24Rational的例子,变为模板类时,规则不再成立:
template<typename T>
class Rational {
public:
Rational(const T& numerator = 0, const T& denominator = 1)
:x(numerator), y(denominator) {}
const T numerator() const { return x; }
const T denominator()const { return y; }
private:
T x;
T y;
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * lhs.denominator());
}
Rational<int> oneHalf(1,2);
Rational<int> oneThird(1,3);
Rational<int> result=oneHalf*oneThird;
result=oneHalf*2; //出错
看起来 oneHalf*2
似乎应该使函数模板operator*
具现化并调用它,但实际上编译不通过。
根本原因在于"编译器在template实参推导过程中从不将隐式转换纳入考虑":转换函数在函数调用过程中的确被使用(如果operator*是一个函数而不是函数模板的话),但在调用一个函数之前,必须要知道那个函数存在,而为了知道它,必须先为相关的function template推导出参数类型(然后才可将适当的函数具现化出来)。
然而template实参推导过程中并不考虑采纳"通过构造函数而发生的"隐式类型转换!
只要利用一个事实就可以改变这种现状:template class内的friend声明式可以指涉该特定函数。也就是说Rational可以声明operator为它的一个friend函数,class template并不依赖于template实参推导,因此编译器总是能够在class Rational具现化时得知T,因此,令Rational class声明适当的operator为其friend函数可简化整个问题:
template<typename T>
class Rational{
public:
friend Rational operator*(const Rational& lhs,
const Rational& rhs);
...
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs){
...
}
现在对operator*
的调用就可以通过编译了,发生的改变是:oneHalf被声明时,class Rational
被具现化出来,而作为过程的一部分,friend函数operator*
也就被作为一个函数而非函数模板自动声明出来,因此编译器可以在调用它时使用隐式转换函数.
此时还未结束,以上代码虽然通过了编译,但却无法链接——Rational
已经被声明出来,但却没有定义。使用template是行不通的,因为此当Rational被具现化时,operator*
只是作为一个普通的函数声明被具现化出来,编译器不会认为它和operator*函数模板有关联而为它具现化出一个函数实体。
解决办法就是将operator*函数本体合并至其声明式内:
template<typename T>
class Rational {
public:
friend const Rational operator*(const Rational& lhs, const Rational& rhs){
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * lhs.denominator());
}
Rational(const T& numerator = 0, const T& denominator = 1)
:x(numerator), y(denominator) {}
const T numerator() const { return x; }
const T denominator()const { return y; }
private:
T x;
T y;
};
此时对operator*的调用可编译链接并执行。
使用模板函数不能实现隐式转换,因此必须使用普通函数代替模板函数:
在本条款中,friend的作用不再是提升函数或类的访问权限,而是使类型转换发生在所有参数身上:为了使该函数被自动具现化,需要把它声明在类内部,而在类内部声明non-member函数的唯一办法就是令它成为一个friend。
优化:
由于operator*
需要在Rational内部定义,它被默认声明为inline,为了使这种inline声明带来的冲击最小(本例中operator*
已经是一个单行函数,但更复杂的函数也许需要这样),可以使它调用一个定义域class外部的辅助函数,由该辅助函数完成实际功能:
template<typename T>
const Rational<T> doMultiply(const Rational& lhs,
const Rational& rhs){
...
}
template<typename T>
class Rational{
public:
friend Rational operator*(const Rational& lhs,
const Rational& rhs){
doMultiply(lhs,rhs);
...
};
doMultiply()不支持混合式操作(例如将Rational对象和一个int相乘),但是该函数只由operator*()调用,而operator*()支持混合操作,因此功能完善。
在一个class template内,template名称可被用来作为"template和其参数”的简略表达方式,所以在
Rational
内我们可以只写Rational而不必写Rational
,本例中这只节省我们少打几个字,但若出现许多参数,或参数名称很长,这可以节省我们的时间,也可以让代码比较干净。
记住:
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”,因为模板函数不能实现参数隐式转换,而friend可以使函数具现化(将模板函数具现化为具体函数)。
traits:一种技术,允许你在编译期间取得某些类型信息。
本条款以 iterator_traits
为例介绍了如何实现traits类,以及如何使用traits类以实现advance
。
STL提供了很多的容器、迭代器和算法,其中的 advance
便是一个通用的算法,可以让一个迭代器移动n步:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d); // 如果d小于0,就逆向移动
ostream_iterator
就是一个输出迭代器。slist
,STL并未提供)和TR1哈希容器的迭代器就属于前向迭代器。set
, multiset
, map
, multimap
。+=
, -=
等移动操作,支持它的容器包括 vector
, deque
, string
等。对于上述五种迭代器,C++ 提供了五种专属卷标结构(tag struct) 来标识迭代器的类型,它们之间是“is-a”的关系:
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag: public input_iterator_tag {};
struct bidirectional_iterator_tag: public forward_iterator_tag {};
struct random_access_iterator_tag: public bidirectional_iterator_tag {};
现在回到 advance
的问题,它的实现方式显然取决于 Iter
的类型:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d){
if (iter is a random access iterator) {
iter += d; // use iterator arithmetic
} // for random access iters
else {
if (d >= 0) { while (d--) ++iter; } // use iterative calls to
else { while (d++) --iter; } // ++ or -- for other
} // iterator categories
}
怎么得到 Iter
的类型呢?这正是 traits 的作用。
Traits 不是关键字,也不是 std 类型或模板,它只是 C++ 中的一种编程惯例,允许我们在编译期得到类型的信息。 用 Bjarne 的话讲,Traits 是一个用来携带信息的很小的对象(或结构), 在其他对象或算法中用这一信息来确定策略或实现细节。
traits的另一个需求在于 advance
对与基本数据类型也能正常工作,比如 char*
。所以traits不能借助类来实现, 于是我们把traits放到模板中。比如:
template<typename IterT> // template for information about
struct iterator_traits; // iterator types
iterator_traits
将会标识 IterT
的迭代器类别。iterator_traits
的实现包括两部分:
在用户定义的类型中,typedef 该类型支持迭代器的 Tag,例如 deque
支持随机迭代器:
template < ... > // template params elided
class deque {
public:
class iterator {
public:
typedef random_access_iterator_tag iterator_category;
};
};
然后在全局的 iterator_traits
模板中 typedef
那个用户类型中的 Tag,以提供全局和统一的类型识别。
template<typename IterT>
struct iterator_traits {
typedef typename IterT::iterator_category iterator_category;
};
上述办法对基本数据类型的指针是不起作用的,我们不能在指针里面 typedef
一个 Tag 。
解决办法: 偏特化 iterator_traits
,因为内置类型指针都是可以随机访问的:
template<typename IterT> // partial template specialization
struct iterator_traits<IterT*>{
typedef random_access_iterator_tag iterator_category;
};
你已经看到了实现一个traits类的整个过程:
iterator_catetory
;我们已经用 iterator_traits
提供了迭代器的类型信息,是时候给出 advance
的实现了。
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
typeid(std::random_access_iterator_tag))
...
}
由于iterator_traits
在编译期即可确定,但if语句的判断却要在运行期核定,这不仅浪费时间,也会造成可执行文件膨胀。
实际上,C++提供了在编译期完成核定的方法:函数重载。
当你重载某个函数,你必须详细叙述各个重载件的参数类型。当你调用f,编译器便根据传来的实参选择最适当的重载件。编译器的态度是“如果这个重载件最匹配传递过来的实参,就调用这个f;如果那个重载件最匹配,就调用那个f;如果第三个f最匹配,就调用第三个f!”依此类推。这正是一个针对类型而发生的“编译期条件句”。
因此,我们可以产生两种重载函数,但接受不同的iterator_category对象,由它们完成advance的实际功能,因此advance的最终实现版本如下:
template<typename IterT,typename DistT>
void advance(Iter& iter,Dist d){
doAdvance(iter,d,typename std::iterator_traits<IterT>::iterator_category());
}
// 随机访问迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) {
iter += d;
}
// 双向迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag) {
if (d >= 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
// 输入迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag) {
if (d < 0 ) {
throw std::out_of_range("Negative distance"); // see below
}
while (d--) ++iter;
}
其中,由于之前iterator卷标结构的继承关系,doAdvance的input_iterator版本也可以被forward iterator调用。
总结一下上面代码是如何使用traits类的:
STL库中有类似的char_traits用于保存字符类型的相关信息,numeric_limits用于保存数值类型相关信息等等。
C++11头文件
导入了许多新的traits classes用以提供类型信息,包括is_fundamental
(判断T是否为内置类型),is_array
(判断T是否为数组类型),is_base_of
(判断T1,T2是否相同,抑或T1是T2的base classes)等。
记住:
模板元编程(Template Metaprogramming,TMP)就是利用模板来编写那些在编译时运行的C++程序。 模板元程序(Template Metaprogram)是由C++写成的,运行在编译器中的程序。当程序运行结束后,它的输出仍然会正常地编译。
C++并不是为模板元编程设计的,但自90年代以来,模板元编程的用处逐渐地被世人所发现。
TMP后来被证明是图灵完全的,这意味着TMP可以用来计算任何可计算的问题。你可以声明变量、执行循环、编写和调用函数等等。 但它的使用风格和普通C++完全不同。
我们来看看TMP中如何执行一个循环:
template<unsigned n>
struct Factorial{
enum{ value = n * Factorial<n-1>::value };
};
template<>
struct Factorial<0>{
enum{ value = 1 };
};
int main(){
cout<<Factorial<5>::value;
}
为了更好地理解TMP的重要性,我们来看看TMP能干什么:
记住:
当operator new无法满足某一内存分配需求时,它会先调用客户指定的错误处理函数(如果客户未指定,它就会抛出异常),即new-handler,并且会一直调用直到内存足够。
为了指定这个"用以处理内存不足"的函数,客户必须调用set_new_handler,那是声明于
的一个标准库函数:
namespace std{
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
throw()是一个异常声明,表示保证不抛任何异常,但不是绝对。如果抛出异常则是严重错误。
该函数用于指定当无法分配足够内存时调用的函数,返回的函数指针指向在此之前的处理函数。使用:
void outOfMem(){
std::cout<<"Unable to alloc memory";
std::abort();
}
int main(){
std::set_new_handler(outOfMem);
int *p = new int[10000000000000L];
}
一个设计良好的new-handler应该做以下事情:
让更多内存可被使用.实现这个目的的策略之一是,在程序一开始就分配一大块内存,当new-handler第一次被调用(说明内存不足)时,就把它们归还给程序使用.
安装另一个new-handler,如果当前new-handler无法获取更多内存但它知道另一个new-handler有此能力,它可以调用set_new_handler将那个new-handler设为默认new-handler使得下一次operator new调用的new-handler是最新的那个(令一种策略是令当前new-handler修改自己的行为,方法是让它修改会影响当前new-handler行为的static数据,namespace数据,global数据等)
卸除new-handler.将NULL指针传给set_new_handler,当operator new分配内存不成功时抛出异常.
抛出bad_alloc(或派生自bad_alloc的)异常.这样的异常不会被operator new捕捉,因而会被传递到内存索求处.
不返回.调用abort或exit.(正如outOfMem所做的那样)
std::set_new_handler
设置的是全局的bad_alloc
的错误处理函数,C++并未提供类型相关的bad_alloc
异常处理机制。 但我们可以重载类的operator new
,当创建对象时暂时设置全局的错误处理函数,结束后再恢复全局的错误处理函数。
比如Widget
类,首先需要声明自己的set_new_handler
和operator new
:
class Widget{
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void * operator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler current;
};
// 静态成员需要定义在类的外面
std::new_handler Widget::current = 0;
std::new_handler Widget::set_new_handler(std::new_handler p) throw(){
std::new_handler old = current;
current = p;
return old;
}
关于
abort
,exit
,terminate
的区别:abort
会设置程序非正常退出,exit
会设置程序正常退出,当存在未处理异常时C++会调用terminate
, 它会回调由std::set_terminate
设置的处理函数,默认会调用abort
。
最后来实现operator new
,该函数的工作分为三个步骤:
std::set_new_handler
,把Widget::current
设置为全局的错误处理函数;operator new
来分配真正的内存;如果分配内存失败,Widget::current
将会抛出异常;Widget::current
(通过RAII),并安装调用Widget::operator new
之前的全局错误处理函数。结合资源管理类实现Widget::operator new如下:
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh) :handler(nh) {}
~NewHandlerHolder() { set_new_handler(handler); }
private:
new_handler handler; //用于保存当前global new-handler
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
void* Widget::operator new(std::size_t size) throw(std::bad_alloc) {
NewHandlerHolder h(std::set_new_handler(current));//安装new-handler并使h保存global new-handler
return ::operator new(size); //h析构时恢复global new-handler
}
客户使用Widget
的方式也符合基本数据类型的惯例:
void outOfMem();
Widget::set_new_handler(outOfMem);
Widget *p1 = new Widget; // 如果失败,将会调用outOfMem
string *ps = new string; // 如果失败,将会调用全局的 new-handling function,当然如果没有的话就没有了
Widget::set_new_handler(0); // 把Widget的异常处理函数设为空
Widget *p2 = new Widget; // 如果失败,立即抛出异常
以上代码具有一般性,因此可以考虑将其设为模板,由其他类继承,从而继承这种"可以设定类之专属new-handler"的能力:
template<typename T>
class NewHandlerSupport{
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static std::operator new(std::size_t size) throw(std::bad_alloc);
...
private:
static std::new_handler currentHandler;
}
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler=0;
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw(){
std::new_handler oldHandler=currentHandler;
currentHandler=p;
return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::::operator new(std::size_t size) throw(std::bad_alloc){
NewHandlerHolder h(std::set_new_handler(currentHandler));//安装new-handler并使h保存global new-handler
return ::operator new(size); //h析构时恢复global new-handler
}
有了这个class template,就可以为Widget和其他类添加set_new_handler支持能力了——只要令Widget继承自NewHandlerSupport
就好:
class Widget:public NewHandlerSupport<Widget>{
...
}
类模板NewHandlerSupport看起来相当奇怪,因为参数T从未被使用,实际上参数T只是用来区分不同的derived class,使继承自NewHandlerSupport的每一个class都有自己独立的static成员变量currentHandler。
至于Widget继承自一个以Widget为参数的类模板具现化的类,这是一个有用的技术,叫做——“怪异的循环模板模式”(curiously recurring template pattern;CRTP)。
1993年之前C++的operator new在失败时会返回null而不是抛出异常。如今的C++仍然支持这种nothrow的operator new:
Widget *p1 = new Widget; // 失败时抛出 bad_alloc 异常
assert(p1 != 0); // 这总是成立的
Widget *p2 = new (std::nothrow) Widget;
if(p2 == 0) ... // 失败时 p2 == 0
由于以上new Widget表达式发生两件事:调用nothrow版的operator new,调用Widget的默认构造函数,因而尽管nothrow版的operator new保证不抛出异常,但这并不能阻止Widget的默认构造函数使用普通new再抛出异常,因而具有局限性。
记住:
替换标准库提供的operator new或operator delete通常基于以下三个理由:
用来检测运用上的错误。
new得到的内存如果没有delete会导致内存泄露,而多次delete又会引发未定义行为。如果自定义operator new来保存动态内存的地址列表,在delete中判断内存是否完整,便可以识别使用错误,避免程序崩溃的同时还可以记录这些错误使用的日志。
为了强化效能,提高效率。
全局的new和delete被设计为通用目的(general purpose)的使用方式,通过提供自定义的new,我们可以手动维护更适合应用场景的存储策略。
为了收集使用上的统计数据。
在定制new之前,你可能需要先自定义一个new来收集地址分配信息,比如:软件内存区块大小分布,寿命分布,内存归还次序,最大动态分配量等信息。
示例:
struct const int signature=0xDEADBEEF;
typedef unsigned char Byte;
//这段代码还有若干小错误,详下
void* operator new(std::size_t size) throw(std::bad_alloc){
using namespace std;
size_t realSize=size+2+sizeof(int);
void* pMem=malloc(realSize);
if(!pMem)
throw bad_alloc();
*(static_cast<int*>(pMem))=signatrue;
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int)))=signature;
return static_cast<Byte*>(pMem)+sizeof(int);
}
"这个operator new的主要缺点在于它疏忽了身为这个特殊函数所应该具备的’坚持C++规矩’的态度.例如,条款51提到所有operator news都应该内含一个循环,反复调用某个new-handling函数,这里却没有."此外,还有一个更加微妙的主题:对齐(alignment)。
许多计算机体系结构要求特定的类型必须放在特定的内存地址上。例如它可能会要求doubles的地址必须是8倍数。如果没有奉行这个约束条件,可能导致运行期硬件异常。有些体系结构没有这么严苛,而是宣称如果对齐条件获得满足,便提供较佳效率。例如Intelx86体系结构上的doubles可被对齐于任何byte边界,但如果它是8-byte对齐,其访问速度会快许多。
C++要求所有operator news返回的指针都有适当的对齐(取决于数据类型)。malloc就是在这样的要求下工作,所以令malloc返回一个得自malloc的指针是安全的。
然而上述operator new并未提供这样的保证,存在不安全性,可能导致程序崩溃或变慢。
因此写一个能优良运作的内存管理器可能并不容易。其实我们还有别的选择:比如去读编译器文档、内存管理的商业工具、开源内存管理工具等。
许多编译器已经在它们的内存管理函数中切换到调试状态或志记状态,许多平台已有可以替代编译器自带之内存管理器的商业产品,开放源码领域的内存管理器(如Boost库的Pool内存池)也都可用,因而可能并不需要自己定制operator new和operator delete。
本条款的主题是,了解何时可在“全局性的”或"class专属的”基础上合理替换缺省的new和delete:
记住:
有许多理由需要写个自己的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。
operator new需遵循的规则有:
正确处理size为0的内存申请需求,通过将0改为1;
内含无穷循环,内存不足时:
伪代码如下:
void* operator new(std::size_t size) throw(std::bad_alloc){
using namespace std;
if(size==0)
size=1;
while(true){
尝试分配size bytes;
if(分配成功)
return(一个指针,指向分配而来的内存);
new_handler globalHandler=set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler)
(*globalHandler)();
else
throw std::bad_alloc();
}
}
重载operator new
为成员函数通常是为了对某个特定的类进行动态内存管理的优化,但会被其派生类继承,因此需要在实现Base::operator new()
时,基于对象大小为sizeof(Base)
来进行内存管理优化的。
class Base{
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
...
};
class Derived:public Base{
...
};
void * Base::operator new(std::size_t size) throw(std::bad_alloc){
if(size!=sizeof(Base))
return ::operator new(size); //使用默认operator new处理
...
}
Derived*p =new Derived; //这里调用的是Base::operaotr new!
(当然,有些情况你写的Base::operator new
是通用于整个class及其子类的,这时这一条规则不适用。)
对于operator new[]
(被称为array new),那么operator new[]唯一要做的就是分配一块内存,而无法对array内尚未存在的元素做任何事情,设置无法计算每个元素对象有多大。因为:
size
不一定等于sizeof(Base)
。size
实参的值可能大于这些对象的大小之和。因为数组的大小可能也需要存储。operator delete的情况比较简单,唯一需要注意的是C++保证"删除null指针永远安全",下面是non-member operator delete的伪码:
void operator delete(void* rawMemory) throw(){
if(rawMemory==0)
return;
归还rawMemory所指内存;
}
operator delete的member版本也比较简单,只需要多加一个动作检查删除数量.万一class专属的operator new将大小有误的分配行为转交::operator new执行,大小有误的删除行为也必须转交::operator delete执行:
class Base{
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
static void operator delete(void* rawMemory,str::size_t size) throw();
...
};
void Base::operator delete(void* rawMemory,std::size_t size) throw(){
if(rawMemory==0)
return;
if(size!=sizeof(Base)){
::operator delete(rawMemory);
return;
}
现在,归还rawMemory所指内存;
return;
}
当我们将operator delete或operator delete[]定义成类的成员时,该函数可以包含另外一个类型为size_t的形参。此时,该形参的初始值是第一个形参所指对象的字节数。size_t形参可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给operator delete的字节数将因待删除指针所指对象的动态类型不同而有所区别。而且,实际运行的operator delete函数版本也由对象的动态类型决定。——《C++primer 5th》
记住:
placement new(定位new)和place ment delete指的是正常的operator new和operator delete的重载版本,所谓的正常的operator new和delete,指的是拥有以下正常签名式的版本:
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw(); //global作用域中的正常签名式
void operator delete(void*,std::size_t) throw(); //class作用域中的正常签名式
由于C++允许对重载的operator new和operator delete添加额外参数,因而具有额外参数的operator new和operator delete就是placement new和placement delete。
众多placement new版本中比较常见的一个是"接受一个指针指向对象该被构造之处"(大多数时placement new特指这一版本),其主要用途是接受一个指针,然后将其返回以供构造函数在其上构造对象,签名式为:
void* operator new(std::size_t,void* pMemory) throw();
对于以下语句:
Widget* pw1=new Widget; //Widget是一个类
共有两个函数被调用:一个是用以分配内存的operator new,另一个是用于构造Widget的Widget default构造函数。
假设第一个函数调用成功,第二个却抛出异常,那么步骤1所申请的内存必须被释放,否则就是内存泄露.这个任务客户端无法做到,因此由C++运行期系统来完成:运行期系统会调用步骤一所调用的operator new对应的operator delete版本。所谓"对应",与具有正常签名式的operator new对应的版本就是具有正常签名式的operator delete(1中所列),而对于placement new,"对应"指的是额外参数相同的placement delete。
假设Widget的定义如下:
class Widget{
public:
...
static void* operator new(std::size_t size,std::ostream& logStream) throw(std::bad_alloc); //placement operator new
static void operator delete(void* pMemory,std::size_t size) throw(); //正常operator delete
...
};
对于以下语句:
Widget* pw=new (std::cerr) Widget;
如果该语句在Widget default构造函数中抛出异常,那么运行期系统有责任取消operator new的分配并恢复旧观,正如以上所言,它需要找到与placement new对应的placement delete:
static void operator delete(std::size_t size,std::ostream& logStream) throw();
结果就是"如果一个带额外参数的operator new没有’带相同额外参数’的对应版operator delete,那么当new的分配动作需要取消并恢复旧观时就没有任何operator delete会被调用",内存泄露也就不可避免。
解决办法就是:为Widget声明并定义一个与之前带额外参数的operator new对应的operator delete:
class Widget{
public:
...
static void* operator new(std::size_t size,std::ostream& logStream) throw(std::bad_alloc); //placement operator new
static void operator delete(void* pMemory,std::size_t size) throw(); //正常operator delete
static void operator delete(std::size_t size,std::ostream& logStream) throw(); //对应的operator delete
...
};
此时可避免内存泄漏。
需要注意的是,只有当抛出异常时,调用的才会是对应的placement delete,如果以上语句执行正常,那么执行:
delete pw;
调用的是正常版本的operator delete。
在条款33中提到,类中的名称会隐藏外部的名称,子类的名称会隐藏父类的名称。 所以当你声明一个”placement new”时:
class Base{
public:
static void* operator new(std::size_t size, std::ostream& log) throw(std::bad_alloc);
};
Base *p = new Base; // Error!
Base *p = new (std::cerr) Base; // OK
普通的new
将会抛出异常,因为”placement new”隐藏了外部的”normal new”。同样地,当你继承时:
class Derived: public Base{
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
};
Derived *p = new (std::clog) Derived; // Error!
Derived *p = new Derived; // OK
这是因为子类中的”normal new”隐藏了父类中的”placement new”,虽然它们的函数签名不同。条款33中提到,按照C++的名称隐藏规则会隐藏所有同名(name)的东西,和签名无关。
为了避免隐藏全局”new”,你在创建自定义的”new”时,需要分别声明这些签名的”new”并调用全局的版本。
为了方便,我们可以为这些全局版本的调用声明一个父类StandardNewDeleteForms
:
class StandardNewDeleteForms {
public:
// normal new/delete
static void* operator new(std::size_t size) throw(std::bad_alloc) { return ::operator new(size); }
static void operator delete(void *pMemory) throw() { ::operator delete(pMemory); }
// placement new/delete
static void* operator new(std::size_t size, void *ptr) throw() { return ::operator new(size, ptr); }
static void operator delete(void *pMemory, void *ptr) throw() { return ::operator delete(pMemory, ptr); }
// nothrow new/delete
static void* operator new(std::size_t size, const std::nothrow_t& nt) throw() { return ::operator new(size, nt); }
static void operator delete(void *pMemory, const std::nothrow_t&) throw() { ::operator delete(pMemory); }
};
然后在用户类型Widget
中using StandardNewDeleteForms::new/delete
即可使得这些函数都可见:
class Widget: public StandardNewDeleteForms { // inherit std forms
public:
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
static void* operator new(std::size_t size, std::ostream& log) throw(std::bad_alloc); // 自定义 placement new
static void operator delete(void *pMemory, std::ostream& logStream) throw(); // 对应的 placement delete
};
记住:
mem_fn
:这是个语句构造上与成员函数指针一致的东西。提供了统一的方式来适配成员函数指针。TR1::reference_wrapper
:使得引用更像一个对象,原本在容器中只能存储指针和对象的。参考:
《Effective C++》
《C++primer 5th》
https://www.cnblogs.com/reasno/archive/2015/09.html
https://blog.csdn.net/lintianyi9921/category_9564213.html
https://blog.csdn.net/qq_41453285/category_9690724.html
https://harttle.land/tags.html#Effective-C++