您的位置:首页 > 编程语言 > C语言/C++

《Effective C++》学习笔记

2014-12-07 15:41 267 查看
1,尽可能使用const,类中成员函数被声明为const的话,函数体内不能对当前类中的成员变量进行变更。除非变量前面加上mutable。

在用const修饰成员函数的时候,放在函数名前面修饰的是函数返回值,放在函数名后面则修饰的是整个函数,其功能就是上面所说的。const修饰是可以通过const_cast进行移除的。这点可能在有些情况下很用的。

2,确定对象被使用前已先被初始化。在构造函数中对类中成员变量初始化的时候,尽量用初始化表,不要用赋值语句。因为初始化表是在程序进入构造函数之前初始化的。而赋值语句之前其实已经初始化了,只是赋值改变了成员变量的值而已。论效率前者要高出很多。

非本地static成员和本地static成员进行初始化的时候要注意。例如

class FileSystem

{

public:

std::size_t numDisks() const;

};

extern FileSystem tfs;

class Directory

{

public:

Directory(params);

};

Directory::Directory(params)

{

std::size_t disks = tfs.numDisks();

}

在Directory构造函数中使用tfs的时候并不知道tfs是否已经初始化。可能已经初始化,也可能没有初始化。

有一种很好的方法可以解决这个问题。

不使用

extern FileSystem tfs;

使用

FileSystem& tfs()

{

static FileSystem fs;

return fs;

}

Directory::Directory(params)

{

std::size_t disks = tfs().numDisks();

}

Directory& tempDir()

{

static Directory td;

return td;

}

这么修改之后,这个系统程序的客户完全像以前使用它,唯一不同的是他们现在使用tfs()和tempDir()而不再是tfs和tempDir。也就是说他们使用函数返回指向static对象的引用,而不再是使用static对象自身。而且这里在使用的时候才创建对象,并能保证初始化。

在进行类赋值操作的时候,要注意类中成员变量是否有引用和const常量。如果有的话,要定义一个赋值操作符,进行自定赋值操作。因为引用和const都是不允许改变的。

为驳回编译器自动提供的机能,想默认构造函数和析构函数,以及赋值操作等,可将相应的成员函数声明为private并且不予实现。使用Uncopyable这样的base class也是一种做法。

为多态基类声明virtual析构函数。在用基类指针指向派生类对象时候,进行资源释放的时候,即delete时候,只能删除派生类中基类的部分,没有办法删除派生类独有的部分,会导致内存泄漏。这时候为多态基类声明virtual析构函数就可以有效的解决这个问题。但需要注意的时候,如果基类中本省没有虚函数,声明virtual析构函数,会增大开销。原因就是如果没有virtual,在进行类定义的时候,不需要分配虚函数查询表内存。一旦有一个虚函数,就会生成一个虚函数表,类中会自动生成一个指向这个表的指针,一定程度上增大了开销。通常只有类中至少含有一个virtual函数时才为它声明virtual析构函数。

还应该注意析构函数的运作方式,最深层的那个类其析构函数最先被调用,然后是其每一个基类的析构函数被调用。编译器会在基类的派生类析构函数中调用基类的析构函数,这也要求基类的析构函数必须要有定义。否则,编译器会报错。

别让异常逃离析构函数

class Widget

{

public:

~widget(){}//假设这个可能吐出一个异常

};

void doSomething()

{

std::vector<Widget>v;//v在这里被自动销毁

}

假设v有10个元素,在销毁第一个元素的时候,就抛出异常了,后面的元素还没有得到销毁,可能出现无法预知的错误。或者出现前面两个出现异常。

或者下面的例子

class DBConnection

{

public:

static DBConnection create();//这个函数返回DBConnection对象

void close();

}

为确保客户不忘记DBConnection对象上调用close(),一个合理的想法是创建一个用来管理DBConnection资源的class,并在析构函数中调用close()。如下:

class DBConn

{

public:

~DBConn()

{

db.close();

}

private:

DBConnection db;

};

客户可以直接书写DBConn dbc(DBConnection::create());

但是如果DBConn的析构函数抛出异常,不能正确调用close(),就会出现问题。

一般可以通过下面两个方法解决

1.如果close抛出异常就结束程序。通常通过调用abort完成。

DBConn::~DBConn

{

try {db.close();}

catch(...){std::abort();}

}

2.吐下因调用close而发生的异常。

DBConn::~DBConn

{

try{db.close();}

catch(...){}

}

一般而言,将异常吞掉是个坏主意,因为它压制了“某些动作失败”的重要信息。然而有时候吞下异常也比负担“草率结束程序”或“不明确行为带来的风险”好。

比较好的解决方法,是将close提供给用户调用。

class DBConn

{

public:

void close()

{

db.close();

closed = true;

}

~DBConn()

{

if(!closed)

{

try{db.close();}

catch(...){}

}

}

private:

DBConnection db;

bool closed;

};

用户可以手动调用close,如果没有手动调用,就在够函数中调用,如果抛出异常就将异常吞掉。

绝不在构造和析构过程中调用virtual函数。

class Transaction

{

public:

Transaction();

virtual void logTransaction() const = 0;//做出一分因类型不同而不同的日志记录

};

Transaction::Transaction()

{

logTransaction();

}

class BuyTransaction:public Transaction

{

public:

virtual void logTransaction() const;

};

class SellTransaction:public Transaction

{

virtual void logTransaction() const;

};

BuyTransaction b;

无疑会有一个BuyTransaction构造函数被调用,但首先Transaction构造函数一定会更早被调用。这时候被调用的logTransaction是Transaction内的版本,不是BuyTransaction内的版本。即使目前即将建立的对象类型是BuyTransaction。原因就是基类构造期间virtual函数绝不会下降到派生类阶层。在基类构造函数执行时,派生类成员变量尚未初始化。如果在此期间调用的虚函数下降到派生类阶层,要知道派生类函数几乎必然要取local成员变量,而那些成员变量尚未初始化。“要求使用对象内部尚未初始化的成份”。

更根本的原因:在派生类对象的基类构造期间对象的类型是基类,而不是派生类。不只virtual函数会被编译器解析至基类,若使用运行期类型信息检查,如dynamic_cast和typeid,也会把对象当成是基类对象。

需要注意的是不仅是构造或析构函数中不使用virtual函数,构造和析构函数中使用的非虚函数中也不能使用虚构函数,原因是一样的。

一般解决方法

class Transaction

{

public:

explict Transaction(const std::string& logInfo);

void logTransaction(const std::string& logInfo) const;//这里不再是虚函数

};

Transaction::Transaction(const std::string& logInfo)

{

logTransaction(logInfo);

}

class BuyTransaction:public Transaction

{

public:

BuyTransaction(parameters)

:Transaction(createLogString(parameters))//将log信息传给基类构造函数

{}

private:

static std::string createLogString(parameters);

};

请注意static函数createLogString(parameters)运用。比起在成员初始值内给予基类所需数据,利用辅助函数创建一个值传给基类构造函数往往比较方便(也比较可读)。令次函数为static,也就不可能意外指向“初期未成熟的BuyTransaction对象内尚未初始化的成员变量”。

令operator=返回一个reference to *this

Widget& operator=(const Widget& rhs)
{
return *this;
}


在operator=中处理“自我赋值”当自己给自己赋值的时候,有可能会发生异常,需要处理。

Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs)return *this;
delete pb;
pb = new Bitmap(*ths.pb);
return *this;
}
较好的方法

Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
更好的方法

Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp);//交换*this和temp数据
return *this;
};
或者(跟上面是一样的,注意传值方式)

Widget& Widget::operator=(Widget rhs)
{
swap(rhs);
return *this;
}


复制对象时勿忘其每一个成份。如果自己没有写copy函数,编译器会给自己写的类默认创建一个copy函数。而如果自己写copy函数,就要保证自己类中的每一个对象都要被copy。最应该要注意的是自己写的类有继承基类的时候,不能只copy自己类中的成员,基类中的成员也要自己手动copy,编译器不会帮住copy。通常在自己copy函数函数中调用基类的copy函数就好了。

PriortyCustomer::PriortyCurstomer(const PriotyCustomer& rhs):Custom(rhs)
{}
PriorityCustomer& PriortyCustomer::operator=(const PriortyCustomer& rhs)
{
Customer::operator=(rhs);
priorty = rhs.priorty;
return *this;
}
注意上面两个函数不能互相调用,即构造函数和操作符函数不能互相调用,虽然代码相近。试图构造一个已经存在的对象是不合理的,试图在已经存在的对象里面构造新对象也是不合理的。改善代码相近的方法,是单独写一个函数,让两者都去调用就好了。

以对象管理资源。分配资源的时候,在C++中最常用的就是内存分配,如果手动分配,会发生遗忘delete,即便没有遗忘,如果在delete之前发生异常和return,delete同样不会得到执行,资源就没办法释放。比较好的思路就是在析构函数中进行资源释放,对象在相应的块或函数中执行完毕后,就会执行析构函数,资源会得到释放。std::auto_ptr是标准库中所谓的智能指针,这个指针在执行完毕的时候,会调用自己的析构函数,其析构函数自动对其所指对象调用delete。由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象。如果真实那样,对象会被删除一次以上,而那个使你的程序搭上“未定义行为”。为预防这个问题,auto_ptrs有一个不同寻常的性质:若通过copy构造函数或赋值操作符复制它们,它们会编程null,而赋值所得的指针所得的指针取得资源的唯一所有权!p2=p1,p1将变为null,p2指向所要指向的地址。

tr1::shared_ptr可以解决stl::auto_ptr所出现的问题。tr1::shared_ptr持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。不同的是其无法打破环状引用。

需要注意的是tr1::shared_ptr和stl::auto_ptr都在其析构函数内做delete而不是delete[]动作。boost::scoped_array和boost::shared_array classes,提供期望的行为。

在医院管理类中小心copying行为。需要注意的是tr1::shared_ptr允许指定所谓的“删除器”,那是一个函数或者一个函数对象,当引用次数为0时便被调用(此机能并不存在与auto_ptr,它总是将其指针删除)。删除器对tr1::shared_ptr构造函数而言是可有可无的第二个参数。std::tr1::shared_ptr<Mutex>mutexPtr;mutexPtr(pm,unlock);

在资源管理类中提供对原始资源的访问。std::tr1::shared_ptr<Investment>pInv(createInvestment());假设你希望以某个处理Investment对象,像这样int daysHeld(const Investment* pi);//返回投资天数

你想要这么使用int days = dayHeld(pInv);这样编译通不过,因为daysHeld需要的是Investment*指针,你传给它的却不是。tr1::shared_ptr和stl::auto_ptr都提供一个get成员函数,用来执行显示转换,也就是它会返回智能指针内部的原始指针(的复件):int days = dayHeld(pInv.get());这样就没有问题了。就像(几乎)所有智能指针一样,tr1::shared_ptr和stl::auto_ptr也重载了指针取值操作符(operatro->)和(operator*),它们允许隐式转换至底部原始指针。

举个实例

FontHandle getFont();
void releaseFont(FontHandle fh);//这两个函数来自同一个api
class Font
{
public:
explicit Font(FontHandle fh)
:f(fh)
{}
~Font(){releaseFont(f);}
private:
FontHandle f;
}
假设有大量与字体相关的api,其处理的是FontHandles,那么“将Font对象转换成FontHandle”会是一种很频繁的需求。Font class可为此提供一个显式的转换。像get那样:

class Font
{
public:
FontHandle get() const{return f;}
}
也可以提供隐式转换

class Font
{
public:
operator FontHandle() const{return f;}
}


类型转换函数的一般形式为:

operator 类型名( )

{

实现转换的语句

}

注意事项:

1.在函数名前面不能指定函数类型,函数没有参数。

2.其返回值的类型是由函数名中指定的类型名来确定的。

3.类型转换函数只能作为成员函数,因为转换的主体是本类的对象,不能作为友元函数或普通函数。

4.从函数形式可以看到,它与运算符重载函数相似,都是用关键字operator开头,只是被重载的是类型名。double类型经过重载后,除了原有的含义外,还获得新的含义(将一个Complex类对象转换为double类型数据,并指定了转换方法)。这样,编译系统不仅能识别原有的double型数据,而且还会把Complex类对象作为double型数据处理。
显式较隐式安全些,隐式较方便些。

以独立语句将newed对象置入智能指针。

int priortity();
void processWidget(std::tr1::shared_ptr<Widget>pw,int priority)
processWidget(new Widget,priority());
使用new Widget作为参数会出问题。因为shared_ptr构造函数是explicit,不允许隐式转换。

processWidget(std::tr1::shared_ptrM<Widget>(new Widget),priority());

这种方法可行,但是需要注意执行顺序

1,执行new Widget

2,调研那个priority()

3,调用tr1::shared_ptr构造函数

如果在第二步的时候出现异常,将会造成资源的泄漏。解决方法就是分开写

std::tr1::shared_ptr<Widget>pw(new Widget);

processWidget(pw,priority());

这个过程就是保证以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

让接口容易被正确使用,不易不勿用。

class Date
{
public:
Date(int month,int day,int year);
};
如果用户不是按照月,天,年格式定义日期,将不会的到正确的结果。一种解决方案就是限定类型。

struct Day{expicit Day(int d):val(d){} int val;};

class Date

{

public:

Date(const Month& m,const Day& d,const Year& y);

};

这个时候用户在进行使用的时候初始化参数输的不正确的话,将不会通过编译。

tr1::shared_ptr支持定制型删除器。这可防范DLL问题,因为shared_ptr在进行资源释放的时候将会调用自己所在dll中的删除器或析构函数。不会跨dll。

必须返回对象时,别妄想放回其引用.绝不要返回pointer或reference指向一个local stack对象(局部对象),因为局部对象在函数结束的时候就会销毁,用指针或引用指向一个已经销毁的对象是没有意义的.也不要返回引用指向一个heap-allocated对象(new对象,需要delete).当new一个对象时候,什么时候delete将是一个比较麻烦的事情,处理不当很容易造成内存泄漏.最好也不要放回一个local static(局部静态对象)的引用.因为当这个函数被多次调用的时候,使用的是同一个对象.很容易出问题.最应该注意的是条款4中的,如果在一个函数中定义一个static对象,并返回这个对象的引用,以后再次调用这个函数
的时候不会再次定义这个对象,直接返回的是这个函数对象.这样就会多次调用此对象的构造函数.

只有private才能够提供封装,而且在进行设计的时候尽量使用这种封装,不要直接对类中的成员变量进行使用,而是使用函数返回此成员变量。public和protected都没有封装,一旦类中成员改变,所有使用此类对象的地方都需要更改,或者所有继承于此类的对象也需要更改。工作量之恐怖!

尽量少做转型动作,const_cast通常被用来将对象常量性移除。也是唯一有此能力的C++转型操作符。dynamic_cast主要执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。reinterpret_cast意图执行低级转型,实际动作可能取决于编译器,这也就表示它不可移植性。static_cast用来强迫隐式转换,将non-const对象为const对象,或者int转double等等。将pointer-to-base转为pointer-to-derived。但它无法将const转换为non_const这个只有const_cast才办得到。这几种类型转换都是C++中特有的新型转换函数。多用新型,少用旧型。

if(SpecialWindow* psw1 = dynamic_cast<SpecialWindow*>(base)){}

dynamic_cast就检查如果base能转换成SpecialWindow类型将转换,否则返回null。转换成功的方式就是SpecialWindow是派生类。

避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降至最低。通过一个函数返回类中private成员变量的引用或者指针的时候,实际上就破坏了其封装性。因为返回的指针或引用可以直接更改private成员变量的数据。解决方法为可在函数返回前加上const修饰符。但这并没有解决根本问题。有时候还会出现虚吊情况。也就是说,如果使用一个临时的类对象temp,使用temp调用成员函数返回其private中的成员变量(public也可以)。当temp出了作用域之后,会自动销毁,这时候其中的成员变量也会销毁。这时候返回的指针或者引用就成了虚吊的了。

为“异常安全”而努力是值得的。考虑下面代码

void PrettMenu::changeBackGround(std::istream& imgSrc)
{
lock(&mutex);//取得互斥器
delete bgImage;//摆脱旧的背景图像
++imageChange;
bgImage = new Image(imgSrc);//安装新的背景图像
unlock(&mutex);//释放互斥器
}
相对应的改进

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
再改进

class PrettyMenu
{
std::tr1::shared_ptr<Image>bgImage;
}

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
bgImage.reset(new Image(imgSrc));
++imageChange;//保证确实改变了再改变状态
}
这里不再需要手动delete旧图像,因为这个动作已经由智能指针内部处理掉了。此外,删除动作只发生在新图像被成功创建之后。更正确的说,tr1::shared_ptr::reset函数只有在其参数(也就是“new Image(imgSrc)”的执行结果)被成功生成之后才会被调用。delete只在reset函数内被使用,所以如果从喂进入那个函数也就绝对不会使用delete。也请注意,以对象(tr1::shared_ptr)管理资源再次缩减了changeBackground的长度。还有一种策略就是copy-and-swap。原则很简单,为你打算修改的对象做出一分副本,然后在那副本身上做一切必要修改。若有任何修改异常,原对象仍保持未改变状态。待所有改变都成功之后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。

客观看待inline函数。需将大多数inline函数限制在小型、被频繁调用的函数身上。这可能是日后的调试过程和二进制升级更容易(因为inline函数中是没办法断点调试的,而且程序升级的时候需要重新编译),也可是潜在的代码膨胀问题(之所以导致代码膨胀,是因为编译器会用inline函数直接代替调用函数代码的地方,不像函数调用,调用的地方只是所指向函数的地址,并不需要把函数代码弄过来)最小化,是程序的速度提升机会最大化。而且不要因为function templates出现在头文件中,就将他们声明为inline。因为不同的类型,可能需求不一样。并不一定都需要inline。

31,将文件间的编译依存关系降至最低。

class Person
{
public:
std::string name() const;
std::string birthDate() const;
std::string address() const;
private;
std::string name;
Date theBrithDate;
Address theAddress;
}
这里如果不添加相应类的头文件的话是没办法编译通过,因为里面定义了相应类的对象需要对象定义。然而一旦添加头文件,便是在Person定义文件和其含入的文件之间形成了一种编译依存关系。如果这些头文件中有任何一个被改变,或这些头文件所依赖的其他文件有任何改变,那么每一个含入Person Class的文件就得重新编译,任何使用Person class的文件也得重新编译。

namespace std{ class string;}
class Date;
class Address;
class Person
{
public:
std::string name() const;
std::string birthDate() const;
std::string address() const;
}
这里使用前置声明,并没有任何类的定义。但是如果在定义Person对象的时候,编译器是没办法获取对象所需的内存空间的。而且namespace std{ class string;}使用是错误的,string不是一个类,它是typedef类型。

这时候可以通过一下两种方案解决:

1,将Person声明定义成两个类,一个用与声明接口,一个用于定义实现。在声明接口类中定义一个定义实现的指针。两者的函数以及传参保持一直。

其策略是如果使用object reference或object pointers可以完成任务,就不要使用object。如果能过,尽量以Class声明式替换Class定义式。为声明和定义式提供不同的头文件。

2,将Person声明一个抽象类(函数全部纯虚),具体实现在派生类中。

32,派生类以public形式继承基类,意味者编译器会说每一个类型为派生类对象同时也是一个基类对象。基类可用的场所,派生类都可以来替代。包括函数参数!这一点很重要,应该第一时间想到多态的实现。

33,关于作用域中重名函数调用问题,避免遮掩继承而来的名称

class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int );
virtual void mf1();
void mf3();
void mf3(double);
}

class Derived:public Base
{
public:
using Base::mf1;//让Base类内名为mf1和mf3的所有东西在Derived作用域内都可以见(并且public)
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
}


现在

Derived d;
int x;
d.mf1();//仍然没问题,仍然调用Derived::mf1
d.mf1(x);//现在没问题了,调用Base::mf1
d.mf2();//仍然没问题,仍然调用Base::mf2
d.mf3();//没问题,调用Derived::mf3
d.mf3(x);//现在没问题了,调用Base::mf3


这意味着如果你继承base类并加上重载函数,而你又希望重新定义或覆写其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩。

34,区分接口继承和实现继承。含有纯虚函数的类是抽象类,不能定义实体对象!virual function() = 0;如果有纯虚函数的话,意味着有这接口,派生类中必须定义重新声明定义这个函数。应该注意的是在基类中也可以为这个纯虚函数添加定义。使用的时候如下

class Shape
{
public:
virtual void draw() const = 0;
};

Shape* ps = new Shape;//错误,Shape是抽象的
Shape* ps1 = new Rectangle;//没问题
ps1->draw();//调用Rectangle::draw
Shape* ps2 = new Ellipse;//没问题
ps2->draw();//调用Ellipse::draw
ps1->Shape::draw();//调用Shape::draw
ps2->Shape::draw();//调用Shape::draw


非纯虚的虚函数virtual void function();表示每个类(包括基类和派生类)都必须支持这样一个函数,每个派生类可以根据自己的特性进行自由定义,如果不进行定义的话,将使用基类的函数。非虚函数目的是为了令派生类继承函数的接口及一分强制性实现,而且期望派生类不应该对其有任何改变。也就是不期望派生类中重写这个函数。

35,在实现多态的过程中可以考虑虚函数之外的选择。其中之一

class GameCharacter
{
public:
int healthValue() const
{
...//做一些事前工作
int reVal = doHealthValue();//做真正的工作
...//做一些时候工作
}

private:
virtual int doHealthValue() const//派生类可以重新定义
{}
};
这里调用的时候直接使用healthValue,不需要重新定义,而又可以根据特性重写doHealthValue。还有一个特点是在doHealthValue使用前后都可以添加一些环境设置和解除工作,为doHealthValue的执行添加更便利的执行环境。

之二,函数指针,如下

class GameCharacter;//前置声明
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
publlic:
typedef int (*HealthCalcFunc)(const GameCharacer&);
explicit GameCharacer(HealthCalcFunc hcf = defaultHealthCalc)
:healthFunc(hcf)
{}
int healthValue() const
{ return healthFunc(*this);}
private:
HealthCalcFunc healthFunc;
};
使用的时候只需要传递到构造函数中相应的函数指针就可以实现根据自己定义的函数进行自由操作。

36,绝不重新定义继承而来的non-virtual函数。如果基类和派生类指针同时指向一个派生类对象,调用派生类中重新定义的派生类函数。这个时候基类指针会调用基类部分中的函数,而派生类会调用派生类中的函数。如果定义成虚函数的话,两者都将调用派生类中的函数。

39,当面对“并不存在is-a关系”的两个classes,其中一个需要访问另一个的protected成员,或需要重新定义其一或多个virtual函数,private继承极有可能称为正统策略。即便如此你也应该注意到,一个混合了public继承和复合的设计,往往能够解释出你要的行为,尽管这样的设计有较大的复杂度。“明智而慎重地使用private继承”意味着,在考虑过所有其他方案之后,如果仍然认为private继承是“表现程序内两个classes之间的关系”的最佳方法,这才用它。

class Widget:private Timer
{
private:
virtual void onTick() const;//查看Widget的数据等等
};
上面是用private继承实现的,这样可以保证在派生类中只能用基类中的protected和public函数。之所以再声明为private,是为了提醒客户这个函数不能被客户调用。

class Widget
{
private:
class WidgetTimer:public Timer
{
public:
virtual void onTick() const;
}
WidgetTimer timer;
};
这种方式可以实现比上面更好的方式。这种方法还可以实现使Widget使它拥有派生类,但又可以阻止派生类重新定义onTick。如果是第一中方式是不可能实现的,因为继承虚函数可以被重写的。这里的Widget派生类中是不能访问私有成员WidgetTimer timer的。所以是不可能被重写的。而且可以降低依赖性。把WidgetTimer定义在外面,Widget中只需要使用声明WidgetTimer即可,这个时候就不再需要timer的头文件。

比较注意的空类class Empty{}。如果是这样用的话

class HoldsAnInt{ private: int x;Empty e;}这时候sizeof(HoldsAnInt)>sizeof(int)。在大多数编译器中sizeof(Empty)获得的是1,因为面对“大小为零之独立(非附属)对象”,通常C++官方类型默默安插一个char到空对象内。然而齐位需求,可能不止一个char大小。如果是class HoldsAnInt:private Empty{ private: int x;};这样的话sizeof(HoldsAnInt)==sizeof(int);这是继承的好处。一定程度上节省空间。想象如果是多重继承的时候这中节俭还是挺值得的。现实中的空类往往不是真正的空类。虽然他们从未拥有non-static成员变量,却往往内含typedefs,enums,static成员变量或non-virtual函数(虚函数的话会有虚函数表指针的)。这样使用继承就会很少增加派生类的大小。

40,明智而审慎的使用多重继承。所谓多重继承就是,派生类同时继承了多于一个的基类!这个时候如果是两个基类中含有同名的函数,派生类对象进行函数调用的时候需要明确指出所要调用的函数,如mp.BorrowableItem::checkOut();如果不是显示调用的话,C++进行调用的时候,最早的就是判断是最佳匹配,如果格式都是一样的,将会报错。然后才会是判断是是否可取,如private的函数之类的。如果继承的基类中,有两个或以上的基类又是从同一个基类继承而来的话,这时候派生类中最早基类的成份,C++表示可以只保留一份,亦可以都保留。如果想要保留一份的话,将会使用虚继承。即:virtual
public BaseClass。但是虚继承是需要代价的,使用虚继承而来的派生类比非虚继承而来的派生类要大很多,访问成员变量时速度也会慢。而且虚继承必须承担virtual base的初始化责任,无论这个虚基类距离当前派生类多远,当前派生类都必须承担起这个虚基类的初始化责任。

41,泛型编程templates。classes和templates都支持接口和多态,对与classes而言接口是显示的,以函数签名(函数名,返回值,参数)为中心,多态则是通过virtual函数反生在运行时期的。而templates接口是隐式的,基于有效表达式。多态是通过template具现化和函数重载解析,发生在编译期。templates在编译的时候去判断对象中的函数有没有当前使用的函数,隐式调用。而且在编译的时候确定类型。这也是一种多态表现。跟类完全不一样的机制。

42,typename和typedef。这两者没有关系,只是看着像,有时候会搞混,所以这里写出来了。这里着重学习一下typename。template<class T>class Widget;和template<typename T>class Widget;这里typename跟class没有什么区别,只是后者是老版本而已。最重要的一点是在从署名的时候只能,也必须要使用typename。例如

template<typename C>

void print2nd(const C&container)

{

typename C::const_iterator* x;

}

函数里面必须要使用typename,因为在编译之前,是不知道const_iterator是一个类型的。所以需要加上typename来声明,表示后面是一个类型。所有的情况都是这样。只是在继承基类列表中和初始化列表中禁止使用。也就是Derived:public Base<T>::nested和explicit Derived(int):Base<T>::Nested(x)这两种情况不能使用,其他地方都要用,包括参数列表等。这里跟typedef唯一有关系的可能就是typedef typename std::iterator_traits<IterT>::value_types
value_type

43,学习处理模版化基类内的名称。

template<typename Company>
class MsgSender
{
public:
void sendClear(const MsgInfo& info)
{
std:string msg;
Company c;
c.sendClearText(msg);
}
void sendSecret(const MsgInfo& info)
{}
};
派生类定义为下面:

template<typename Company>
class LoggingMsgSender:public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
sendClear(info);
}
};
虽然LoggingMsgSender是从MsgSender继承而来,但是编译器依旧会报错,因为sendClear(info)不确定是LoggingMsgSender的函数。因为编译器考虑到了特版化的情况。

如下:

class CompanyZ
{
public:
void sendEncrypted(const std::string& msg);
};

template<>
class MsgSender<CompanyZ>
{
public:
void sendSecret(const MsgInfo& info)
{}
};
上面的template<>的定义注意跟一般的模版定义的区别。这是一个特版化的MsgSender template,在template是CompanyZ时使用。这是如果派生类派生MsgSender模版,必须要考虑到其特版化。所以编译器会报错。通常解决方法有三个。

1)使用this->sendClear(info);2)使用using MsgSender<Company>::sendClear;3)明确指出被调用函数位于基类中MsgSender<Company>::senderClear(info);不过这里如果调用的是virual函数,这种使用将会关闭动态绑定。

使用上面的方式可能定义的时候不会有问题,但是在实际使用的时候编译器还是会给出一个明确的判断。例如

LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
zMsgSender.sendClearMsg(msgData);
最后一句使用的时候编译器会报错,因为编译器会判断sendClearMsg()中的函数调用,其调用的sendClear在CompanyZ中是不存在的,所以编译通不过。

44,将与参数无关的代码抽离templates。这里需要单独仔细的在看看。在使用模版的时候,常常要考虑代码膨胀的因素。就是模版在具现多次之后,可能会有很多重复的地方,所以要考虑将这些重复的地方声明为一个共同的代码块,进行共享。将与参数无关的代码抽离就是为了达到这个需求。因为在typename定义的时候,所得模版具现最主要的不同就是typename参数。

45,这里提到了泛型编程。,运用成员函数模版接受所有兼容类型。考虑stl中的智能指针的实现。从派生类转换到基类,在模版中实现。有点难,需要好好看看泛型编程,需要另起灶炉~

46,当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template”内部的friend函数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: