一、让自己习惯C++
构造函数对成员变量的初始化是在进入构造函数内之前!如果在构造函数内(会使用pass by value),是赋值。前者比较好(效率更高)!
ABEntry::ABEntry(const string& name,
const string& address,
const list& phones)
:theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{
}
static对象:global对象、定义域namspace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。
第三条解释:C++对定义于不同的编译单元内的non-local static对象的初始化次序无明确定义,可能会造成使用一个未初始化的对象。可以利用小小的设计解决,将每一个non-local static对象搬到自己的专属函数内(该对象在此函数内声明为static),这些函数返回一个reference指向它所含的对象,然后用户调用这些函数,而不直接指涉这些对象,这是Singleleton模式的一个常见实现手法。
class FileSystem
{
public:
inline FileSystem& tfs()//绝佳内联函数
{
static FileSystem fs;//定义并初始化
return fs;//返回
}
};
理论基础:C++保证,函数内的local static对象会在该函数被调用期间,首次遇上该对象定义式时被初始化。
编译器会为空类声明一个defaut构造函数,一个copy构造函数,一个copy assignment操作符,一个析构函数。这些函数都是public且inline。一般会自己创建默认构造函数,编译器便不会再创建。
默认构造函数可被调用,无实参或者每个实参均有默认值,前面可以加explict,可防止隐式类型转换(比如int到class)。
copy构造函数被用来以同型对象初始化自我对象,很重要定义一个对象如何passed by value。
copy assignment操作符被用来从另一个同型对象中拷贝其值到自我对象。
(注意Name n = name是copy构造函数;
Name n; Name name; n = name;是copy assignment操作符)
//copy构造函数被用来以同型对象初始化自我对象,很重要定义一个对象如何passed by value
bool hasName(Name n) {}
Name name;
if (hasName(name)) {}
//name被复制到n体内,有Name的copy构造函数完成。相当于Name n = name;
//pass by value意味着调用copy构造函数。
//所以通常用引用传递用户自定义类型。
//初始化成员列表而不是放到构造函数内也是因为这个原因。
将相应的成员函数声明为private并且不予实现,阻止编译器生成和用户调用他们,不定义是因为成员函数和友元函数有可能还会调用他们。如果不慎被调用会获得一个连接错误。例子标准程序库中的ios_base、basic_ios和sentry。
改进:将连接期错误移至编译期间,能更早侦测出错误,通过定义一个基类,缺点可能会导致多重继承
class Uncopyable()
{
protected:
Uncopyable() {}//允许派生类对象构造和析构
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&);//阻止copying
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale :private Uncopyable//不再声明copy构造函数或copy assigment操作符
{
};
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
//释放DBConnection类
class DBConn
{
private:
DBConnection db;
bool closed;
void close()
{
db.close(); //供客户使用的新函数
closed = true;
}
~DBConn()
{
if (!closed)
{
try
{
db.close();
}
catch (...)
{
//制作运转记录,记下对close的调用失败
}
}
}
};
class Transaction {//所有交易的base class
public:
Transaction();
virtual void logTransaction() const = 0;//因类型而异的日志记录(在构造函数中执行)
};
Transaction::Transaction()//基类构造函数实现
{
...
logTransaction();//最后动作是记录这笔交易
}
class BuyTransaction :public Transaction {
public:
virtual void logTransaction() const;
};
class SellTransaction :public Transaction {
public:
virtual void logTransaction() const;
};
BuyTransaction b;//会执行基类中的virtual函数
派生类对象内的基类成分先构造,之后构造自身成分;即1.先执行基类构造函数,2.后执行派生类构造函数
比如,如果你的派生类构造函数中使用了virtual函数,会先执行基类的构造函数以及里面的virtual函数,而不执行派生类本身的virtual函数,即这类调用从不下降至derived classes。并且执行基类的构造函数以及里面的virtual函数时,派生类构造函数还未被执行,有一些变量还未被初始化,所以C++不会让执行派生类中的virtual函数。
令赋值操作符assignment operator返回一个reference to *this(为了连锁赋值x = y = z = 15;)
不同的变量指涉同一对象,可能会造成在停止使用资源之前意外释放了它。
class Bitmap {
};
class Widget {
private:
Bitmap* pb;
public:
Widget& operator=(const Widget& rhs);
};
Widget& Widget::operator=(const Widget& rhs)
{
delete pb;//rhs和*this有可能是同一个对象,会删除rhs
pb = new Bitmap(*rhs.pb);//rhs's bitmap的副本
return *this;
}
//改进
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;//相同测试
delete pb;//rhs和*this有可能是同一个对象,会删除rhs
pb = new Bitmap(*rhs.pb);//rhs's bitmap的副本
return *this;
}
note:如果你要为派生类编写copying函数,要小心的复制其base class成分(往往是private),所以无法直接访问,要调用相应的base class函数获取base成员。
把资源放进类(对象)中,利用对象的析构函数自动调用释放资源;
对于资源在heap-based的资源
思想,类似于利用对象的析构(自动)来释放资源。管理对象(managing object)例如智能指针shared_ptr
class Investment {
...//投资类型,继承体系中的root class
};
Investment* creatInvestment();//factory函数,返回指针,调用者要删除它
//利用creatAndDeleteInves函数建立和删除
void creatAndDeleteInves()
{
Investment* pInv = creatInvestment();//调用factory函数
...;//一些操作
delete pInv;//释放pInv所指对象
}
存在的问题:在一些操作中,可能会提前return、抛出异常等没有执行到delete pInv,则会造成资源的泄露。
改进:利用auto_ptr智能指针,避免creatAndDeleteInves函数潜在的资源泄露可能性
void creatAndDeleteInves()
{
auto_ptr pInv(creatAndDeleteInves());//调用factory函数
...;//一些操作,经由auto_ptr的析构函数自动删除pInv
}
note:为了预防auto_ptr同时指向一个对象时,被删除多次,auto_ptr有个性质:若通过copy构造函数或者copy assignment操作符复制它们,它们会变成null,而复制所得的指针获得资源的唯一拥有权。总结auto_ptr并不太好,改进版是shared_ptr,引用计数型智慧指针(reference-counting smart pointer,RCSP),持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。
void creatAndDeleteInves()
{
...;
shared_ptr pInv1(creatAndDeleteInves());//调用factory函数
shared_ptr pInv2(pInv1);//利用copy构造函数,pInv1和pInv2指向同一对象
pInv1 = pInv2;//copy assignment操作符,无任何改变
//pInv1和pInv2被销毁
}
KJHwq2RAII:资源获得时机便是初始化时机
并非所有资源都是heap-based,例如c的互斥器对象Mutex, shared_ptr不适用,要建立自己的资源管理类,在构造函数内获取资源,在析构函数内释放资源:
许多API直接指涉资源,所以除非你永远不用它们,否则都会绕过资源管理对象直接访问原始资源。假设使用tr1::shared_ptr管理对象。
std::tr1::shared_ptr pInv(createInvestment());
函数daysHeld的声明是这样的:
int daysHeld(const Investment *pi);
下面这种调用方式,肯定是错误的:
int days = daysHeld(pInv); //错误
因为函数需要的是指针,你传递是一个tr1::shared_ptr
(1)显式转换
tr1::shared_ptr和auto_ptr都提供了一个成员函数get返回内部的原始指针,这是显式转换。
int days = daysHeld(pInv.get()); //好的,没有问题
(2)隐式转换
tr1::shared_ptr和auto_ptr都重载了操作符operator->和operator*,这样就允许隐式转换到原始指针。举例:假设Investment类有个成员函数bool isTaxFree() const;那么下面的调用是OK的:
bool taxable1 = !(pInv->isTaxFree()); //好的,没有问题
bool taxable2 = !((*pInv).isTaxFree()); //好的,没有问题
现在的问题是,需要原始指针的地方(例如,函数形参),如何以智能指针代替。解决方法是:提供一个隐式转换函数。下面举个字体类的例子:
FontHandle getFont(); //取得字体句柄
void releaseFont(FontHandle fh); //释放句柄
class Font
{
public:
explicit Font(FontHandle fh) : f(fh){}//只允许显式转换
~Font()
{
releaseFont(f);
}
private:
FontHandle f;
};
如果C API处理的是FontHandle而不是Font对象,当然你可以像tr1::shared_ptr和auto_ptr那样提供一个get()函数:
FontHandle get() const { return f; } //显示转换函数
这样是可以的,但客户还是觉得麻烦,这时候定义一个隐式转换函数是必须的。
class Font
{
public:
...
operator FontHandle() const { return f; }
...
};
注意:假设你已经知道了隐式转换函数的用法。例如:必须定义为成员函数,不允许转换为数组和函数类型等。
完成了以上工作,对于下面这个函数的调用是OK的:
void changeFontSize(FontHandle f, int newSize);
Font f(getFont());
int newFontSize;
changeFontSize(f, newFontSize); //好的,Font隐式转换为FontHandle了
隐式类型转换也增加了一种风险。例如有以下代码:
Font f1(getFont());
FontHandle f2 = f1; //将Font错写成FontHandle了,编译仍然通过。
f1被隐式转换为FontHandle,这时f1和f2共同管理某个资源,f1被销毁,字体释放,这时候你可以想象f2的状态(原谅我这个词我不会说),再销毁f2,必然会造成运行错误。通常提供一个显示转换get函数是比较好的,因为它可以避免非故意的类型转换的错误,这种错误估计会耗费你很长的调试时间(我遇到过的情况)。
string* str1 = new string;//一个string对象
string* str2 = new string[100];//100个string对象组成的数组
delete str1;
delete [] str2
对于typedef更易出错,要考虑到底该以哪种形式delete,eg:
typedef string AddressLines[4];//四行,每行是一个string
//AddressLines代表一个string数组
string* pal = new AddressLines;//返回一个string,就像new string[4]
delete [] pal;//容易出错
尽量嫑对数组形式做typedef动作,用vector代替。
processWidget(std::trl::shared_ptr(new Widget), priority());
当编译器:执行“”new Widget“” -->调用prioruty-->trl::shared_otr构造函数次序时,如果对priority函数调用异常,则new Widget返回的指针会遗失,因为它尚未被置入shared_ptr内。
解决:分离语句
std::trl::shared_ptr(new Widget);
processWidget(pw, priority());
Investment* createInvestment();
//改为
std::trl::shared_ptr createInvestment();
实质上强迫客户将返回值存储于一个shared_ptr内,防止忘记删除底部Investment对象(当它不再被使用时)。
shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变为0时将被调用的“”删除器。
//0转为null,getRidOfInvestment为自定义的删除器
shared_ptr pInv(static_cast 0, getRidOfInvestment);
效率更高,放置切割
class Window;
class WindowWithScrollBars;
void printNameAndDisplay(const Window& w)
{
cout << w.name();
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);//传进来的窗口是什么类型,w就表现出那种类型
例外:对于内置类型,以及STL的迭代器和函数对象,pass-by-value更恰当
绝不要返回pointer或reference指向一个local stack对象;
绝不要返回reference指向一个heap-allocated对象;不知道何时delete
绝不要返回pointer或reference指向一个local stack对象而有可能同时需要多个这样的对象;因为只保留local static对象的现值。条款4有一个在单线程中合理返回reference指向一个local static对象提供了一份设计实例。
protected并不比public更具封装性
考虑如果有一个public成员变量被取消,所有使用它的客户码都会被破坏,同样如果取消一个protected成员变量被取消,所有derived class都会被破坏,因此protected也缺乏封装性。
#include
using namespace std;
//利用member函数
class WebBrowser {
public:
void clearCache();
void clearHistory();
void removeCookies();
void clearEverything();
};
//利用non-member函数
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
member函数可以看到更多的数据,而non-member函数只能看到member成员函数,所以non-member函数封装性更好。
可以将non-member functions 和 class 放在同一个命名空间内,不同的功能放在不同的头文件中,但是均在一个namespace内,客户可以轻松扩展这一组便利函数,这也是C++标准程序库的组织方式,比如std内的vector、algorithm、list等头文件,不同的功能实现,降低了编译依存性。例如浏览器相关的功能实现:
//头文件webbrowser.h,针对class WebBrowser自身
//以及WebBrowser核心机能
namespace WebBrowserStuff
{
class WebBrowser {....};
...;//核心机能,例如所有客户都需要的non-member函数
}
//头文件webbrowserbookmarks.h
namespace WebBrowserStuff {
...;//与书签相关的便利函数
}
//头文件webbrowsercookies.h
namespace WebBrowserStuff {
...;//与cookie相关的便利函数
}
member函数的反面是non-member函数,不是friend函数,尽量避免friend函数。
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member函数。eg:
#include
using namespace std;
class Rational {
public:
Rational(int numberator = 0,
int denuminator = 1);//构造函数刻意不为explicit;允许int-to-Rational隐式转换
int numerator() const;//分子分母的访问函数
int denominator() const;
const Rational operator*(const Rational& rhs) const;
private:
int m_num;
int m_den;
};
const Rational operator*(const Rational& lhs,
const Rational& rhs)//non-member函数
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
Rational oneEighth(1, 8);//1/8
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth;//good
result = result * oneEighth;//good
//如果利用member operator
result = oneHalf * 2;//good,等价于result = oneHalf.operator*(2),2发生了隐式转换
result = 2 * oneHalf;//错误,result = 2.operator*(oneHalf)
//如果利用non-member函数
result = oneHalf * 2;//good
result = 2 * oneHalf;//good
涉及到default swap、member swaps、non-member swaps、std::swap特化版本、以及对swap的调用
swap不只是STL的一部分,还是异常安全性编程的脊柱(条款29)
标准程序库的swap:基于类型T的copy构造函数和copy assignment操作符
namespace std {
template
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
评价:设计三个对象的复制,效率低,当涉及到指针指向的对象交换时,只用交换指针即可,无需再对对象进行操作!
改进利用“pimpl手法”(pointer to implementation):
#include
#include
using namespace std;
class WidgetImpl {//针对Widget数据而设计的,细节不重要
public:
private:
int a, b, c;
vector v;
};
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{
*pImpl = *(rhs.pImpl);//operator的一般性实现见条款10,11,12
}
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl);//利用std的swap置换指针
}
private:
WidgetImpl* pImpl;//只交换指针
};
//默认的swap不只复制三个Widgets还有WidgetImpl对象,效率极低
//需要自己编写特化版本,利用non-member swap调用member swap,整个结果:
namespace WidgetStuff
{
....;//模板化的WidgetImpl等等
template
class Widget {//同前,内含swap成员函数
};
template//non-member swap函数,并不属于std命名空间
void swap(Widget& a, Widget& b)
{
a.swap(b);
}
}
//调用
template
void doSomething(T& obj1, T& obj2)
{
using std::swap;//令std::swap在此函数内可用
...;
swap(obj1, obj2);//为T型对象调用最佳swap版本
}
特别是有些变量在判断语句之前定义,在判断语句之后使用是不合理的,因为有可能在判断语句块中抛出异常,使你承受析构成本,并且直接在构造时指定初值比通过default构造函数构造出一个对象后对它赋值效率好。
尽量使用做法B,除非n很大,对效率要求很高。因为做法B更易理解和维护。
转型风格通常有三种形式:
const_cast(expression)//将对象的常量性移除!唯一由此能力的C++style转型操作符
dynamic_cast(expression)//决定某对象是否归属继承体系中的某个类型。
reinterpret_cast(expression)//不可移植。
static_cast(expression)//强迫隐式类型转换,non-const-->const;void*-->typed*;int-->double
新式优点:在代码中易被辨别出来;转型细化,易被编译器诊断错误
一个函数如果说是“异常安全”的,必须同时满足以下两个条件:1.不泄漏任何资源;2.不允许破坏数据。 我们先通过两个反面的例子开始。
第一个是造成资源泄漏的例子。一个类Type,内含一个互斥锁成员 Mutex mutex,以及一个成员函数void Func()。假设Func函数的实现如下所示:
void Type::Func()
{
Lock(&mutex);
DoSomething();
UnLock(&mutex);
}
首先是获得互斥锁,中间是做该做的事,最后释放互斥锁。从功能上来讲很完整,没任何问题。但从异常安全角度来说,它却不满足条件。因为一旦DoSomething()函数内部导致异常,UnLock(&mutex)将不会被执行,于是互斥器将永远不会被释放了。换句话说即造成了资源泄漏。
再来看第二个造成数据破坏的例子。这个例子是我们很熟悉的重载 ‘=’ 操作符的成员函数。依然假设一个类Type,其中一个成员是一个指向一块资源(假设类型为T)的指针。这时候我们一般就需要来自定义复制构造函数和重载复制操作符以及析构函数了。(绝大多数情况下,这三个成员总是要么同时存在,要么都不用定义,因为编译器默认定义了,即C++中所谓的 ”Rule of 3" 规则。这里不作详细介绍)。这里我们只考虑重载复制操作符的问题,其部分代码假设如下:
class Type
{
public:
....
Type& operator = (const Type &t)
{
if(this == &t)
return *this;
else
{
delete m_t;
m_t = new T(t->m_t);
return *this;
}
}
....
private:
T *m_t;
};
首先来判断是否是自我复制,如果是,则直接返回自己。如果不是,则安全释放当前指向的资源,再创建一块与被复制的对象资源一模一样的资源并指向它,最后返回复制好的对象。同样,抛开异常安全来看,没问题。但是考虑到异常安全性时,一旦“new T(t->m_t)"时抛出异常,m_t将指向一块已被删除的资源,并没有真正指向一块与被复制的对象一样的资源。也就是说,原对象的数据遭到破坏。
C++中’异常安全函数”提供了三种安全等级:
1. 基本承诺:如果异常被抛出,对象内的任何成员仍然能保持有效状态,没有数据的破坏及资源泄漏。但对象的现实状态是不可估计的,即不一定是调用前的状态,但至少保证符合对象正常的要求。
2. 强烈保证:如果异常被抛出,对象的状态保持不变。即如果调用成功,则完全成功;如果调用失败,则对象依然是调用前的状态。
3. 不抛异常保证:函数承诺不会抛出任何异常。一般内置类型的所有操作都有不抛异常的保证。
如果一个函数不能提供上述保证之一,则不具备异常安全性。
现在我们来一个个解决上面两个问题。
对于资源泄漏问题,解决方法很容易,即用对象来管理资源。RAII技术之前介绍过,这里就不再赘述。我们在函数中不直接对互斥锁mutex进行操作,而是用到一个管理互斥锁的对象MutexLock ml。函数的新实现如下:
void Type::Func()
{
MutexLock ml(&mutex);
DoSomething();
}
对象ml初始化后,自动对mutex上锁,然后做该做的事。最后我们不用负责释放互斥锁,因为ml的析构函数自动为我们释放了。这样,即时DoSomething()中抛出异常,ml也总是要析构的,就不用担心互斥锁不被正常释放的问题了。
对于第二个问题(数据破坏问题),一个经典的策略叫“copy and swap"。原则很简单:即先对原对象做出一个副本(copy),在副本上做必要的修改。如果出现任何异常,原对象依然能保证不变。如果修改成功,则通过不抛出任何异常的swap函数将副本和原对象进行交换(swap)。函数的新实现如下:
Type& Type::operator = (const Type &t)
{
Type tmp(t);
swap(m_t,tmp->m_t);
return *this;
}
先创建一个被复制对象t的副本tmp,此时原对象尚未有任何修改,这样即使申请资源时有异常抛出,也不会影响到原对象。如果创建成功,则通过swap函数对临时对象的资源和原对象资源进行交换,标准库的swap函数承诺不抛出异常的,这样原对象将成功变成对象 t 的复制版本。对于这个函数,我们可以认为它是”强烈保证“异常安全的。
当然,提供强烈保证并不是总是能够实现的。一个函数能够提供的异常安全性等级,也取决于它的实现。考虑以下例子:
如果f1和f2都提供了”强烈保证“,则显然Func函数是具有”强烈保证“的安全等级。但是如果f1或f2中有一个不能提供,则Func函数将不再具备”强烈保证“等级,而是取决于f1和f2中安全等级最低的那个。
void Func()
{
f1();
f2();
}
总结:
为了让代码具有更好的异常安全性,首先是”用对象来管理资源“,以避免资源的泄漏。其次,在异常安全性等级上,应该尽可能地往更高的等级上来限制。通过 copy-and-swap 方法往往可以实现”强烈保证“。但是我们也应该知道,”强烈保证“并不是对所有的情况都可实现,这取决于你在实现中用到的函数。函数提供的异常安全性的最高等级只能是你实现中调用的各个函数中异常安全性等级最低的那个
不要只因为function templates出现在头文件,就将它们声明为inline。
对于C++类而言,如果它的头文件变了,那么所有这个类的对象所在的文件都要重编,但如果它的实现文件(cpp文件)变了,而头文件没有变(对外的接口不变),那么所有这个类的对象所在的文件都不会因之而重编。
因此,避免大量依赖性编译的解决方案就是:
在头文件中用class声明外来类,用指针或引用代替变量的声明;在cpp文件中包含外来类的头文件。
// ComplexClass.h
#include “SimpleClass2.h”
class SimpleClass1;
class ComplexClass
{
SimpleClass1* xx;//编译的时候知道指针的大小,所以不包含SimpleClass1.h也可可以通过编译
SimpleClass2 xxx;
};
// ComplexClass.cpp
#include “SimpleClass1.h”
void ComplexClass::Fun()
{
SimpleClass1->FunMethod();
}
继承link:public、protected、private、virtual、non-virtual
成员函数:virtual、non-virtual、pure virtual
virtual函数:接口必须被继承,分为纯虚函数和虚函数。
non-virtual函数:接口和实现必须被继承
正方形是矩形,企鹅是鸟
但是长方形能单独修改高度,正方形不能;而且企鹅不会飞,鸟会飞;有问题
public继承主张:能够施行与base class对象身上的每件事情,也可以施加于derived class对象身上!
解决:好的接口可以防止无效的代码通过编译,采取“在编译期间拒绝企鹅飞行”,而不是“在运行期检测飞行错误”
只要函数名称相同,派生类成员函数就会遮掩基类成员函数,可以使用using声明式达到继承重载函数的目标。
#include
using namespace std;
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 class 内名为mf1和mf3的所有东西在Derived作用域内都可见(并且public)
using Base::mf3;
void mf3();
void mf4();
};
derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此;
为了让被遮掩的名称再见天日,可使用using 声明式或转交函数(forwarding functions)。
对应三种成员函数:non-virtual、impure virtual、pure virtual。
有时想只继承成员函数的接口(pure virtual),有时又想继承声明和实现但是想override所继承的实现(impure virtual),有时又想继承声明和实现但是不允许覆写任何东西(non-virtual)。
存疑
template
class Set :public list//将list应用于Set,错误做法,对于list为真的做法对于Set并不完全正确
{//比如list中可以有重复数值
};
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数-----你唯一应该覆写的东西----却是动态绑定
对象分为应用域和实现域
应用域:相当于你塑造的世界中的某些事物,比如人、汽车、一张张视频画面等
实现域:纯粹是实现细节上的人工制品,比如缓冲区、互斥器、查找树等。
当复合发生在应用域内的对象之间,表现出has-a关系(有一个);发生在实现域内则是表现出is-implementd-in-terms-of的关系(根据某物实现出)。
note:根据某物实现出和is-a不易区分,容易用错。例如如果要实现自己的Set集合(Set对象其实是个list对象),默认的set每个元素需要三个指针,空间大速度快,当你需要空间小对速度没要求时需要自己的Set:
设计一:is-a(错误)
template
class Set :public list//将list应用于Set,错误做法,对于list为真的做法对于Set并不完全正确
{//比如list中可以有重复数值
};
设计二:根据某物实现出(正确方案)
template
class Set
{
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
size_t size() const;
private:
list rep;//用来表述Set的数据
};
存疑
存疑
隐式接口:在编译期,根据某个对象支持什么操作来隐式判断是什么对象。
面向对象编程世界总是以显式接口(explicit interfaces)和运行期多态(runtime polymorphism)即动态绑定解决问题
在Template及泛型编程的世界,与面向对象有根本上的不同,隐式接口(implicit interfaces)和编译期多态(compile-time polymorphism)更重要,因为templates事先并不知道具体的类型!
运行期多态和编译期多态之间的差异:类似于“哪一个重载函数该被调用,编译期”和“哪一个virtual函数该被绑定,运行期”。
从属名称:template内出现的名称依赖于某个template参数;
嵌套从属名称:从属名称在class内呈嵌套状;有可能导致解析(parsing)困难。
一般性规则:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧邻它的前一个位置放上关键字typename,
typename只被用来验明嵌套从属类型名称,其他名称不该有它存在,eg:
template
void f(const C& container,//并非嵌套于任何“取决于template参数”的东西内,不允许使用typename
typename C::iterator iter);//嵌套从属名称,一定要使用“typename”
例外:typename不可以出现在base class list内的嵌套从属类型名称之前,也不可在member initialization list(成员初始值)中作为base class修饰符。eg:
template
class Derived : public Base::Nested//base class list中不允许"typename"
{
public:
explicit Derived(int x)
:Base::Nested(x)//mem.init.list中不允许typename
{
typename Base::Nested temp;//嵌套从属名称,需要typename
}
};
请记住:
请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或者member initialion(成员初始列)内作为base class 修饰符。
当我们从Object Oriented C++跨进了template C++继承就不像以前那般畅行无阻了。
可在derived class templates内通过this-->“”指涉base class templates内的名称,或藉由一个明白写出的“base class资格修饰符”完成
class CompanyA
{
public:
void sendCleartext(const string& msg);
void sendEncrypted(const string& msg);
};
class CompanyB
{
public:
void sendCleartext(const string& msg);
void sendEncrypted(const string& msg);
};
class CompanyA
{
public:
void sendCleartext(const string& msg);
void sendEncrypted(const string& msg);
};
class MsgInfo{};//保存信息,以备将来产生信息
template
class MsgSender {
void sendClear(const MsgInfo& info)
{
string msg;
...;//根据info产生信息
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info)
{
string msg;
...;//根据info产生信息
Company c;
c.sendEncrypted(msg);
}
};
template
class LoggingMsgSender :public MsgSender
{
public:
void sendClearMsg(const MsgInfo& info)
{
...;// 将传送前的信息写到log
sendClear(info);//调用基类的函数,无法通过编译
...;// 将传送后的信息写到log
}
};
//改变,方法1
template
class LoggingMsgSender :public MsgSender
{
public:
void sendClearMsg(const MsgInfo& info)
{
...;// 将传送前的信息写到log
this->sendClear(info);//成立,假设sendClear将被继承
...;// 将传送后的信息写到log
}
};
//方法二
template
class LoggingMsgSender :public MsgSender
{
public:
void sendClearMsg(const MsgInfo& info)
{
using MsgSender::sendClear;//告诉编译器,请他假设sendClear位于base class内
...;// 将传送前的信息写到log
sendClear(info);//成立,假设sendClear将被继承
...;// 将传送后的信息写到log
}
};
在template代码中,重复是隐晦的,毕竟只存在一份template源码,所以必须感受template被具现化多次时可能发生的重复
类似于,编写某个class和另一个class的某些部分相同,把共同部分搬移到新的class1t去,然后使用继承和复合,令原先的classes取用这共同特性。
非类型参数及template代码膨胀举例:
template//n为非类型参数
class SquareMatrix {
public:
void invert();//求逆矩阵
};
//具现化两份invert,除了常量5和10不同,其他均相同,造成代码膨胀
SquareMatrix sm1;
sm1.invert();
SquareMatrix sm2;
sm2.invert();
template
class SquareMatrixBase {//存储矩阵大小和一个指针指向矩阵数值
protected:
SquareMatrixBase(size_t n, T* pMem)
:size(n),pData(pMem){}
void setDataPtr(T* ptr) { pData = ptr; }//重新赋值给pData
private:
size_t n;
T* pData;
};
//允许派生类决定内存分配方式,此处用数组,无需动态分配内存,但对象自身可能非常大
template
class SquareMatrix :private SquareMatrixBase {
public:
SquareMatrix()//矩阵大小和数据指针给基类
:SquareMatrixBase(n,data){}
private:
T data[n*n];
};
//此处把没一个矩阵的数据放进heap
template
class SquareMatrix :private SquareMatrixBase {
public:
SquareMatrix()
:SquareMatrixBase(n, 0),
pData(new T[n*n])
{
this->setDataPtr(pData.get());
}
private:
boost::scoped_array pData;//条款13
};
请记住:
■ Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系.
■ 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数.
■ 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码.
STL容器的迭代器几乎总是智能指针,无疑的你不会奢望使用++将一个内置指针从linked list的某个节点移到另一个结点,但这在list::iterators身上办得到。
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。
TMP是编写template-based C++程序并执行于编译期的过程;即以C++写成、执行于C++编译器内的程序。
有许多理由需要写个自定义的new和delete,包括改善性能、对heap运用错误进行调试、收集heap使用信息