effective cpp笔记
const 与 #define的区别
- 编译器处理方式不同:define宏是在预处理阶段展开,const常量是要在编译运行阶段使用
- 类型和安全检查不同:define宏没有类型,不做任何类型检查,仅仅是展开。const常量有具体的类型,在编译阶段会执行类型检查
- 存储方式不同:define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。const常量会在内存中分配(可以是堆中也可以是栈中。
尽可能的使用const
- 将某些东西声明为const,可以帮助编译器检查错误。const可以被施加到任何作用域的对象,函数参数,函数返回类型,成员函数本体。
- 编译器强制bitwise constness,但是我们写程序的时候,应该使用概念上的常量性。
- 当const和non-const成员有着等价的实现,另non-const的版本调用const版本,来避免代码重复。
const的语义之一就是告诉编译器和其他程序员某值应该不变,只要这个是事实,你就确实应该说出来。关键字const多才多艺,它能够修饰全局作用域的常量,修饰文件,函数,块作用域中被生命为static的对象,你可以用它修饰class内部的static和non-static的成员变量,你也可以使用指针自身,指针所指物,或两者都是const。
const指针
char greeting[] = "hello";
char *p = greeting;
const char *p = greeting;
char* const p = greeting; // const pointer, non-const data
const char * const p = greeting;
指向非常量的指针可以转化成指向常量的指针,而指向常量的指针转成非常量的指针则是非法的。
const迭代器
迭代器以指针来塑模出来的,迭代器的作用就像个T* 的指针。声明迭代器为const就像声明指针为const一样,表示该迭代器不得指向不同的东西,但是它所指向的值可以改动。如果你需要表明,你的迭代器不能够改变其所指向的东西,那么你应该使用const_iterator
const修饰函数的返回值
令函数返回一个常量值,往往能够降低应客户造成的意外,又不至于放弃安全性和高效性。
class Rational { ... };
const Rational operator*(const Rational& lhs, const Rational& rhs) ;
可能你会说,为什什么会返回const对象?原因可能误用成如下:
Rational a, b;
(a * b) = c;
if( a * b = c) {
}
const成员函数
const修饰函数,目的确定该成员对象可用于const对象身上。两个成员函数如果只是常量性不同,可以被重载。在类X非常量成员函数,this指针是X *const类型,因为其是一个常量指针,而其指向的对象是一个非常量,所以该对象是可以修改的。而在类X的常量成员中,this的指针类型是const X* const,所以其指向的对象为一个常量,所以其是不能够修改的。
那么什么样的成员函数具有const性质了? 一种认为const成员函数不能够改变类的任意一个bit,就可以。这就是C++对常量性的定义,因此const成员函数不可以改变对象内任何non-static的成员变量。
不幸的是,虽然很多成员函数能够通过bitwise 测试,更确切的说是,一个更改了指针所值之物的成员函数,虽然不能够算const,但是如果只有指针隶属于对象,那么称此函数为bitwise const 不会引发 编译器异常。这会导致反直观的结果。
class CTextBlock {
public:
// bitwise const 声明
char& operator[](std::size_t position) const {
return pText[position];
}
private:
char* pText;
};
虽然operator[]的实现不改变pText,但是它是bitwise const,所有编译器都这么认定,但是,你看会发现什么事情?
const CTextBlock cctb("Hello");
char *rc = &cctb[0];
*pc = 'J' ; //现在cctb 拥有的Jello的内容
创建了一个常量对象并设置某值,而且对它至调用了const成员函数,但是还是改变了其值。所以就导出了logical constness。logical constness 认为一个const成员函数可以修改它所处理的对象内的某些bits,但只有在可客户端检查不到才如此。
class CTextBlock {
public:
...
std::size_t length() const;
private:
char* pText;
std::size_t textLength;
bool lengthIsValid;
};
std::size_t CTextBlock::length() const {
if( !lengthIsValid) {
textLength = std::strlen(pText); //错误,在const成员函数内不能够修改textLength及lengthIsValid
lengthIsValid = true;
}
}
由于编译器坚持bitwise constness,所以上述的代码编译通不过,那么要坚持logical constness ,怎么修改,使编译器通过了?解决办法就是利用mutable 释放掉 non-static 成员变量的bitwise constness 约束。
class CTextBlock {
public:
....
private:
char* pText;
mutable std::size_t textLength; // 这些成员函数总是可以被修改,即使在const成员函数中。
mutable bool lengthIsValid; //
};
类的非静态成员可以声明为mutable,这样它们的值可以被该类的常量成员函数(包含非常量成员函数)修改。
确定对象被使用前先被初始化
要点
- 为内置对象进行手工初始化,因为C++不保证初始化它们
- 构造函数最好使用初始化成员列表,而不要在构造函数中,使用赋值操作。初始值列列出的成员变量,其排列次序应该和它们在class中声明的次序相同。
- 为了免除跨编译单元之初始化次序问题,请使用local static 对象替换non-local static 对象。
C++ 有着十分固定的”成员初始化次序”。次序总是相同: base class早于其derived classes 被初始化,而class 的成员变量总是以其声明次序被初始化。
C++ 对”定义于不同编译单元内的non-local static 对象”的初始化次序并无明确定义。为免除”跨编译单元之初始化次序”问题,请以local static 对象替换non-local static 对象。
所谓static对象,就是其寿命从被构造函数构造出来,直到程序结束为止。因此,stack和heap-based的对象,全都被排除,这种对象包括全局对象,定义于namespace作用域的对象,在class内,在函数内,以及在file作用域中被声明为static的对象,函数内的static对象,被称为local-static对象,其它的static对象为non-static对象。程序结束时,static对象会在main()函数结束时析构。
所谓编译单元是指产生单一的目标文件的源文件,基本上它是单一的源代码文件加上其包含的头文件。现在我们关系的的问题至少设计2个源码文件,每一个至少内含一个non-local static 对象,也就是该对象是global或位于namespace作用域中,抑或是class内或file作用域中声明为static。
class FileSystem { ... };
// 代替tfs对象
FileSystem& tfs() {
static FileSystem fs; // 以local static的方式定义和初始化object
return fs; // 返回一个引用
}
class Directory { ... };
Directory::Directory( params ) {
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir() // 代替tempDir对象,{
static Directory td;
return td;
}
了解C++默认编写并调用了哪些函数
要点
- 了解编译器暗自为class创建了默认构造函数,赋值构造函数,复制赋值操作符,以及析构函数。
class Empty {
public:
Empty() { ... }
Empty(const Empty& rhs) { ... )
~Empty( ) { ... }
Empty& operator=(const Empty& rhs) { ... }
};
需要注意的是,只要你显式地定义了一个构造函数(不管是有参构造函数还是无参构造函数),编译器将不再为你创建default构造函数。
若不想使用编译器自动生成的函数,就该明确的拒绝
为驳回编译器自动提供的机能,可能将相应的成员函数声明为private并且不予以实现。使用Uncopyable这样的base class也是可以的。
为多态基类声明virtual析构函数
要点
- 多态性质的基类应该声明一个virtual析构函数。如果类带有任何virtual函数,它就应该拥有virtual析构函数。
- 类的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数
设计一个计时类,如下:
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
};
class AtomicClock: public TimeKeepr {....};
class WaterClock: public TimeKeepr{.....};
如果你使用一个工厂,来创建不同的计时方法:
TimeKeeper* getTimeKeeper();
这样,getTimeKeeper()返回的对象必须位于heap。因此为了避免资源泄漏,必须对每个对象适当的delete。
TimeKeep* ptk = getTimeKeeper();
...
delete ptk;
问题在于getTimeKeeper 返回的指针指向的是一个子类的对象的是,通过基类指针来delete被删除时,而基类是没有带virtual的析构函数的时候,会引发灾难,因为子类的成分没有销毁,子类的析构函数没有调用。这样的问题,可以通过给TimeKeeper添加一个virtual析构函数来解决。
无端的对将每一个类的析构函数声明为virtual函数也是错误的,只有在类中含所有至少一个virtual函数的时候才声明析构函数为virtual。
别让异常逃离析构函数
- 析构函数绝对不能够吐出异常,如果被析构函数调用的函数可能抛出异常,析构函数应该能够捕捉任何异常,然后吞下异常或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非析构函数)执行该操作。
C++并不禁止析构函数吐出异常,但是不建议你这样做。
class Widget {
public:
...
~Widget() {
}
};
void doSomething() {
std::vector<Widget> v;
}
当vector v 销毁的时候,它有责任销毁所有的Widgets,如果v内含了10个Widgets,如果析构第一个的时候,有个异常被抛出。其他九个Widgets还是应该被销毁,但是如果第二个Widgets也抛出了异常。在两个异常同时存在的条件下,程序如果不结束执行,就是会导致未定义的行为。及时不在Vector中,只要析构函数抛出异常,程序也会过早的退出或出现未定义的行为。下面是一个数据库的资源管理类:
class DBConn {
public:
...
~DBConn() {
db.close();
}
};
在析构函数中,关闭数据库的连接,无可厚非,但是如果该调用导致了异常,就会传播该异常,就是允许异常离开析构函数。那就会造成问题。如何解决呢?
DBConn::~DBConn() {
try {
db.close();
} catch(...) {
//...
}
}
绝对不要在构造函数和析构函数中调用virtual函数
要点
- 在构造函数和析构函数不要调用virtual函数,因为这类调用从不降至子类。
复制对象时勿忘其每一个成分
本条款阐释了复制对象时容易犯的一些错误,给出的教训是:
- Copying 函数应该确保复制对象内的所有成员变量及所有base class 成分。
- 不要尝试以某个copying 函数实现另一个copying 函数。应该将共同机能放进第三个函数中,并由两个coping 函数共同调用。换句话说,如果你发现你的copy 构造函数和copy assignment 操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private 而且常被命名为init。
令operator = 返回一个reference to *this
令赋值操作符返回一个reference to *this。这样是为了能够写成连锁形式,比如
int x, y, z;
x = y = z = 15;
class Widget {
public:
Widget& operator = (const Widget& rhs) {
return *this;
}
Widget& operator+= (const Widget& rhs) {
};
};
在operator = 中处理”自我赋值”
要点
- 确保对象自我赋值的时候,operator = 有良好的行为,其中的技术包比较来源对象和目标对象的地址,精心周到的语句顺序,copy and swap。
- 确保任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
自我赋值是自己赋值给自己,看起来愚蠢,但是是合法的。比如:
a[i] = a[j]; //潜在的自我赋值
*px = *py; //潜在的自我赋值
这里有一种错误的写法:
class Bitmap {
....
};
class Widget {
...
private:
Bitmap *pb;
};
Widget& Widget::operator =(const Widget rhs) {
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
如果*this 和 rhs是同一个对象,那么delete pb销毁的就不只是当前对象bitmap,连rhs的bitmap都给销毁了,可以这样来解决这个问题:
Wideget& Widget::operator = (const Widget& rhs) {
if(this == &rhs)
return
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
但是上面的代码不具有异常安全性,如果 new Bitmap发生了异常,Widget最终会指向一块被删除的Bitmap,这样的指针是有害的。那我们就写一个异常安全的代码:
Widget& Widget operator = (const Widget& rhs) {
Bitmap* pOrigin = pb;
pb = new Bitmap(*rhs.pb);
delete pOrigin;
return *this;
}
原理就是:先复制资源,然后再删除。另外一种方法是基于copy-swap的技术,我们来看一看:
Widget& Widget::operator=(const Widget& rhs) {
Widget temp(rhs); // 为rhs数据制作一份复件
swap(temp); // 将this数据和上述复件的数据交换
return *this;
}
以对象管理资源
为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
在资源管理类中小心的coping的行为
复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定了RAII对象的copying行为。
在资源管理类中提供对原始资源的访问
- API往往要求访问原始资源,所以每一个RAII 类应该提供一个取得其所管理资源的办法。
- 对原始资源的访问可能经过显示转换或隐式转换。但是显示转换比较安全,隐式转换对客户比较方便。
成对使用new 和delete时采取相同的形式
- 如果你在new表达式中使用[],那么必须在相应的delete表达式中也是用[],如果你在new表达式中不使用[],那么在delete表达式中不要使用[]
设计类犹如设计type
设计类的时候要考虑的事情如下:
- 新type的对象应该如何被创建和销毁 ?
- 对象的初始化和对象的赋值应该有怎么样的差别?
- 新type的对象如果以以传值传递意味着什么?
- 什么是新type的合法值?
- 你的新type有多么一般化?
宁以传递const值的引用替换传值
- 尽量以传递const值的引用来代替传值,前者通常比较高效,并且避免切割问题。
- 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。它们以传值的方式更为合适
必须返回对象的时候,别妄想返回其引用
- 在必须返回新对象的时候,绝不要返回指针或引用指向一个局部栈对象,或返回引用指向一个堆对象,或返回引用或指针指向一个局部静态对象。
宁以non-member,non-friend 替换member函数
宁可拿non-member non-friend 函数替换成员函数。这样做可以增加封装性,包裹性和机能扩充性。假如你要写一个浏览器类,其中有清除历史记录,url、cookie等。那么可以这样设计。
class WebBrowser {
public:
void clearCache();
void clearHistory();
void clearCookies();
};
那么你可能想提供一个函数来清除所有的这些动作:
class WebBrowser {
public:
void clearEveryThing():
};
也可以这样写:
void clearBrowser(WebBroser& web) {
web.clearCache();
web.clarHistory();
web.clearCookies();
}
哪种方式好呢? 面向对象告诉我们,数据应该和操作绑定一起,这意味着member函数是好的选择,实际上这样是不对的。此外,提供non-member函数可允许对WebBrowser提供相关的包裹弹性,导致较低的编译依赖。
我们可以认为越多的东西被封装,越少的人就可以看到它,越少的人看到,我们就可以有越多的弹性去改变。这就是我们推崇封装的原因。我们建议成员变量为private,这样就会有越少的函数来访问。同样,在non-member,non-friend函数和member函数提供相同的功能的情况下,要我们去选择,我们应该选择non-member和non-friend的函数,因为它不增加能够访问private成员函数的数量
。
这并不意味着这个non-member non-friend函数不可以是别的类的成员。我们可以令clearEveryThing成为某个工具类的static成员。在C++中,比较自然的做法是声明一个名字空间,将这些non-member函数位于WebBroser的名字空间中。
namespace WebBrowserStuff {
class WebBrowser {
};
void clearBrowser(WebBrowser& web);
}
当然,我们可以有很多关于浏览器的便利函数,有的是关于书签的,有的是关于cookie的。对于这些便利函数的管理,我们可以这样做:
namespace WebBrowserStuff {
class WebBrowser{
};
}
// 头文件 "webbrowserbookemarks.h"
namespace WebBrowser {
// 书签相关的便利函数
}
// 头文件"webbrowsercookies.h"
namespace WebBrowserStuff {
// cookie相关的头文件
}
这就是标准库的做法,可以降低编译依赖。
为什么这样写可以提供扩展性
可以考虑,我们要写一个关于影像下载的函数,我们只需在WebBrowserStuff的命名空间中,声明这些变量函数就可以与原来的便利函数一起使用。而class的定义对于客户是不能扩展到。
若所有参数都需要类型转化,请为此采用non-member函数
要点
尽量少做转型的操作
本条款论证了为什么要尽量少做类型转换,并告诉读者,如果必须要进行类型转换,有哪些注意事项:
常见的有三种类型转换方式
- C风格:(T)expression
- 函数风格:T(expression)
- C++ style cast
举例子:
- const_cast
(expression) : 移除变量的const属性 - dynamic_cast
(expression) : 安全向下转型,即:基类指针/引用到派生类指针/引用的转换。如果源和目标类型没有继承/被继承关系,编译器会报错;否则必须在代码里判断返回值是否为NULL来确认转换是否成功。 - reinterpret_cast
(expression):底层转换 - static_cast
(expression):强迫隐式转换,如,将non-const对象转换为const对象,将int转换为double类型。
考虑写出一个不抛出异常的swap函数
swap是STL中的标准函数,用于交换两个对象的数值。后来swap成为异常安全编程(exception-safe programming)的脊柱,也是实现自我赋值(条款11)的一个常见机制。swap的实现如下:
namespace std {
template<typename T>
void swap(T& a, T& b) {
T temp(a);
a=b;
b=temp;
}
}
只要T支持copying函数(copy构造函数和copy assignment操作符)就能**允许swap函数&&。这个版本的实现非常简单,a复制到temp,b复制到a,最后temp复制到b。
但是对于某些类型而言,这些复制可能无一必要。例如,class中含有指针,指针指向真正的数据。这种设计常见的表现形式是所谓的“pimpl手法“。如果以这种手法设计Widget class
class WidgetImpl{
public:
……
private:
int a,b,c; //数据很多,复制意味时间很长
std::vector<double> b;
……
};
下面是pimpl实现
class Widget{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) {
…… // 复制Widget时,复制WidgetImpl对象
*pImpl=*(ths.pImpl);
……
}
……
private:
WidgetImpl* pImpl;//指针,含有Widget的数据
};
如果置换两个Widget对象值,只需要置换其pImpl指针,但STL中的swap算法不知道这一点,它不只是复制三个Widgets,还复制WidgetImpl对象,非常低效。 我们希望告诉std::swap,当Widget被置换时,只需要置换其内部的pImpl指针即可,下面是基本构想,但这个形式无法编译(不能访问private)。
namespace std{
template<> // 这是std::swap针对T是Widget的特换版本,
void swap<Widget>(Widget& a, Widget& b) //目前还无法编译
{ // 只需要置换指针
swap(a.pImpl, b.pImpl);
}
}
其中template<>表示std::swap的一个全特化(total template specialization),函数名之后的**
上面函数试图访问private数据,因此无法编译。我们可以将swap函数声明为friend,但这个和以往有点不同。可以令Widget的swap函数为public,然后将std::swap特化
calss Widget{
public:
……
void swap(Widget& other)
{
using std::swap; // 这个声明有必要
swap(pImpl, other.pImpl);
}
……
};
namespace std{
template<> //修订后的swap版本
void swap<Widget>(Widget& a, Widget& b) {
a.swap(b); //调用其成员函数
}
}
这个做法还跟STL容器保持一致,因为STL容器也提供public swap和特化的std::swap(用来调用前者)。 刚刚假设Widget和WidgetImpl都是class,而不是class template,如果是template时:
template<typename T>
class WidgetImpl{……};
template<typename T>
class Widget{……};
可以在Widget内或WidgetImpl内放个swap成员函数,像上面一样。但是在特化std:swap 时会遇到麻烦
namespace std{
template<typename T>
void swap<Widget<T> >(Widget<T>& a,//不合法,错误
Widget<T>& b)
{
a.swap(b);
}
}
开起来是对的,我们偏特化一个函数模板,但是C++只允许对类模板进行偏特化。但是,当我们偏特化一个函数模板,我们可以简单的添加一个重载版本。
namespace std {
template<typename T>
void swap(Widget<T>& a), Widget<T>& b) {
a.swap(b);
}
}
一般来说,重载函数模板是没有问题的,但是std是一个特殊的命名空间,其管理规则比较特殊:客户可以全特化std中的模板,但不可以添加新的模板,所以我们定义一个新的命名空间,然后其中定义swap模板函数。
namespace WidgetStuff {
template<typename T>
class Widget { ... }
...
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) {
a.swap(b);
}
}
- 当std::swap对你的类型效率不高的时,提供一个swap成员函数,并确定这个函数不抛出异常。
- 如果你提供一个member swap,也该提供一个non-member swap 用来调用前者。对于classes而不是templates而言,请特化std::swap。
- 调用swap时,应针对std::swap使用using 声明式,然后调用swap并且不带任何命名空间资格修饰。
- 为用户自定义类型进行std::templats全特化是最好的,但最好不要尝试在std内添加某些对std而言全新的东西。
尽量延后变量定义式的出现时间
要点
- 尽可能延后变量定义式的出现,这样可以增加程序的清晰度并改善程序的效率。
尽量少做转型操作
- 如果可以,尽量避免转型,特别是注重效率的代码中避免dynamic_casts,如果有个设计需要转型操作,那么尝试设计无需转型的设计。
- 如果转型是必要的,试着隐藏到某个函数的背后,客户随后可以调用该函数,而不需要将转型放进它们自己的代码
- 宁可使用C++style的转型,不要使用旧式转型。前者有着很容易的辨识。
避免返回Handles指向对象的内部成分
- 避免返回Handles(包含引用,迭代器,指针)指向对象内部。这样就可以增加封装性,帮助成员函数的行为像一个const。
下面看看这个例子
class Point {
public:
Point(int x, int y);
void setX(int newVal);
void setY(int newVal);
};
struct RectData {
Point Ulhc;
Point lrhc;
};
class Rectangle {
...
private:
std::tr1::shared_ptr<RectData> pData;
};
为了让使用矩形的客户能够计算矩形的范围,这个函数将会提供upperLeft函数和lowerRight函数。Point是一个用户自定义类型,所以根据上面的条款的忠告,这些函数将会返回引用,代表底层的Point对象。
class Rectangle {
public:
Point& upperLeft() const {
return pData->ulhc;
}
Point& lowerRight() const {
return pData->lrhc;
}
};
这样的设计是可以通过编译,但是设计却是错误的。一方面upperLeft函数和lowerRight函数被声明为const的成员函数,目的是为了提供一个Rectangle相关的坐标点方法,而不是让用户去修改Rectangle。
为异常安全而努力是值得的
透彻了解inlining的里里外外
inline函数可免除函数调用成本,提高程序执行效率,但它也会带来负面影响:
- 增大目标代码的大小,有时候会非常庞大,需要动用虚存,这将大大降低程序执行速度。
- inline 函数无法随着程序库的升级而升级。换句话说如果f 是程序库内的一个inline 函数,客户将”f 函数本体”编进其程序中,一旦程序库设计者决定改变f ,所有用到f 的客户端程序都必须重新编译。总之,将大多数inlining 限制在小型、被频繁调用的函数身上才是最明智的选择(根据80-20经验准则,80%的时间花在20%的函数上)。
确定你的public继承塑造出来的是is-a模型
- public 继承意味着 is-a,适用于base class身上的每件事情一定也要适用于子类身上,因为每个子类对象都是一个基类对象。
- virtual 函数表示接口必须继承、non-virtual表示接口和思想必须被继承。
避免遮掩继承而来的名称
下面的代码因为存在遮掩的问题
int x;
void someFunc() {
double x;
std::cin >> x;
}
读取的数据时local变量x,而不是global变量x。遇到名称x的时候,它在local作用域中查找是否有什么东西带着这个名称,如果找到,就不找到其他作用域。
- 请记住子类的名称会遮掩父类的名称。在public继承下,从没有人希望如此。
区分接口继承和实现继承
有如下的例子
class Shape {
public:
virtual void draw() const = 0;
virtual void error(const std::string& msg);
int objectId() const;
};
class Rectangle: public Shape {
};
class Ellipse: public Shape {
};
Shape是抽象类,它的 pure virtual 函数draw 使它成为一个抽象类。意味着不能够创建一个Shape 类的实体,只能创建子类的实体。
- 成员函数的接口总是会被继承。因为public继承是is-a继承,所以对父类为真的任何事情一定对其子类的为真。
- 声明一个纯虚函数的目的是为了让子类只继承函数接口。
- 声明非纯虚函数的目的,是让子类继承该函数的接口和缺省实现。
- 非虚函数具体指定了接口继承以及强制性实现继承。
考虑virtual函数以外的其他选择
使用Non-Virtual Interface 手法实现模板方式
class GameCharacter {
public:
int healthValue() const {
...
int retVal = doHealthValue();
...
return retVal;
}
private:
virtual int doHealthValue() const {
...
}
};
借助函数指针来实现策略模式
class GameCharacter;
int defaultHealthCalc(const GameCharactor& gc);
class GameCharacter {
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hcf) {
}
private:
HealthCalcFunc healthFunc;
};
绝不重新定义继承而来的non-virtual函数
例子:
class B {
public:
void mf();
};
class D: public B {
};
D x;
B* pB = &x; //获取一个指针指向x
pB->mf(); // 调用mf
D* pD= &x; // 获取一个指针指向x
pD->mf(); // 经由指针调用mf
你会觉得通过对象x调用成员函数mf,由于两者所调用的成员函数相同,凭借的对象也相同,那么行为也相同,是吗?在如下情况下,不会是这样:
mf是一个非虚函数,但是子类重写了这个mf的方法。
class D : publicB{
public:
void mf() ; //遮掩了B::mf
};
pB->mf(); //调用 B::mf
pD->mf(); //调用 D::mf
当mf被调用的时候,任何一个D对象可能表现出B或D的行为;决定因素不在对象自身,而在于指向该对象之指针,这就不合理。对于public继承,适用于B对象的每一件事情,都应该适用于D对象。上面违反了这个规定,D重新定义了mf,设计出现了矛盾。
- 在任何情况下都不该重新定义一个继承而来的non-virtual 函数
- 将一个子类对象的地址分别赋值给基类指针或者子类指针,分别调用重写的的函数,其结果不同。
继承体系下指针比较的含义
在C++中,一个对象可以有多个有效地址, 这样地址的比较不是关于地址相同的问题,而是对象同一性。
class Shape { ... };
class Subject { ... };
class ObservedBlob: public Shape, public Subject { .. };
ObservedBlob *b = new ObservedBlob;
Shape* s = ob;
Subject *subj = ob;
我们进行如下测试:
if( ob == s) ; // true
if (subj == ob) ; // true
实际上,我们发现,几种指针以及内存布局如下:
布局1中,s和subj分别指向了子类对应的父类部分。而obj则完整的指定了整个子类,s和subj具有和obj不同的地址。
不管哪种布局,上面的if语句的比较都是true,不能够拿s和sub比较,因为它们不具有继承关系,它们具有不同的地址,但是比较的结果相同(true),这是为什么呢?这是因为编译器调整了一定的偏移量来完成的。
ob == sub;
可能被翻译成:
ob? (ob + delta == subj) : (subj == 0);
也就是说,ob是空指针,它们就相等,否则ob调整为基类对象。这里可以得到一个非常重要的经验: 一般而言,当我们处理对象的指针和引用的时候,必须小心避免失去类型信息。
void* v = subj;
if (ob == v); // 不相等。
通过复合塑模出has-a或根据某物实现出
- 复合的意义和public继承完全不同
- 在应用领域:复合意味着has-a。在实现领域,复合意味着is-implemented-in-terms-of(根据某物实现出)
指针和引用的区别
在下列情况下,你应该使用指针
- 存在不指向任何对象的可能。
- 你需要在不同的时刻指向不同的对象。
- 重载某个操作符的时候,你应该使用引用。
尽量使用 C++风格的类型转换
使用C风格的指针,问题
- 过于粗鲁,能允许你在任何类型之间进行转换。
- 在程序语句中难以识别。
要求或者禁止对象分配在堆上。
要求对象分配在堆上
我们必须要找到一种方法能够通过new 来创建对象,而不能够通过其他的方式,非堆对象在它们定义的时候被会自动构造并且在生存期结束的时候,能够被自动析构,所以只有想办法将构造函数和析构函数设置为非法操作。如果将两者都设置为private,没有必要,我们只需将析构函数设置为私有既可。
class UPNumber {
public:
UPNumber();
UPNumber(int val);
void destroy() const {
delete this;
}
private:
~UPNumber();
};
UPNumber n;
UPNumber* p = new UPNumber();
delete p; // error;
p->destory(); // fine
还有一种方法是将所有的构造函数声明为私有,包含拷贝构造函数、默认构造函数等。因为要将每个构造函数声明为私有,所以还是将析构函数来声明为private简单。
禁止对象分配在堆上
禁止对象直接通过new来实例化对象,这种禁止很简单,因为总是通过new来创建,所以你可以通过某种某种方法来禁止调用new,new操作符的可用性,你不能够控制,但是你能够控制operator new 函数,你可以将这个函数声明为private。
class UPNumber {
priate:
void* operator new(size_t size);
void operator delete(void* ptr);
void operator* new[](size_t size);
void operator delete[](void* ptr);
};
UPNumber n1; //OK
static UPNumber n2; // OK
UPNumber* p = new UPNumber; //error
我们看到operator new和 operator delele **两者都声明在private中**,除非有具体的理由否则,我们不应该将其中一个放在private中,如果要禁止UPNumber数组分配在堆上,我们应该讲operator new[]和operator delete[] 放在private中。
强制使用堆对象‘
因为栈对象,在离开作用域的时候,会自动调用类的析构函数,所以我们将析构函数声明为private,这样在析构的时候,将会报错。从而强制用户使用堆对象。
class OnHeap {
private:
~OnHeap();
pubic:
void destroy() {
delete this;
}
};
名字空间
namespace org_semantics {
class String { .... };
String operator+(const string& s, const string& b);
}
上面的名字空间的代码是声明,我们可以重新打开该名字空间来定义。注意这样做,可能重新打开定义了一个名字空间。
namespace org_semantics {
String operator+(const String&a, const String& b);
}
为了避免上面提到的问题,我们可以使用全名来限定定义:
org_semantics::String org_semantcis::operator+(const semantics::String& a, const semantics::String& b);
如果希望使用某个名字空间的名称,我们可以使用using指令,
void aFunc() {
using namespace std;
vector<int> a{1, 2,3};
}
注意我们在这里在函数作用域内使用了using 指令,其作用域一直扩展到函数结束,其他的代码就看不到了std,有的程序员,喜欢将using指令放在程序的开头,这时不好的习惯,因为其作用范围太大,跟没有使用名字空间是一样的。
协变返回类型
我们知道,一般来说重写的函数需要和被重写的函数具有相同的原型。
class Shape {
public:
virtual double area() const = 0;
};
class Circle {
public:
virtual float area () const ; // 错误
};
因为重写的接口,在Circle中的返回值改成了float,所以不能够重写,但是有一类意外:就是返回的类型为子类的指针和引用,这种情况是允许的。
class Shape {
public:
virtual Shape* clone() const =0;
};
class Circle{
public:
virtual Shape* clone() const ; // OK
};
制造抽象基类
在正常情况下,我们可以定义一个纯虚函数来使一个类变成抽象类,也可以通过继承使得类获取纯虚函数,使得类变成抽象基类。然而,有时候,我们抽象的时候,找不到合适的纯虚函数。那么,我们应该怎样定义一个行为类似于抽象基类呢?
这里,可以通过确保类中没有公有的构造函数来模拟抽象基类的性质。这就意味着我们必须显示的声明一个构造函数,否则编译器会为我们生成默认构造函数。
class ABC {
public:
virtual ~ABC();
protected:
ABC();
ABC(const ABC&);
};
class D: public ABC {
};
这两个构造函数被声明为受保护的,是为了让派生类的构造函数能够使用,同时阻止创建独立的ABC对象。
void func1(ABC);
void func2(ABC&);
ABC a; // 错误
D d;
func1(d); // 错误,复制构造函数式受保护的。
func2(d); // 正确
另一种使一个类成为抽象类,可以声明一个virtual虚构函数为纯虚的,析构函数是最佳的候选者。
class ABC {
pubic:
virtual ~ABC() = 0;
};
placement new
placement new 是operator new的一个重载版本,和operator new不同的是,语言禁止用户替换placement new,而普通的opeator new **和opeartor delete是可以被替换掉的。placement new 直接忽视第一个表示大小的实参,直接返回第二个参数,它允许我们在特定的位置放置对象**。
// placement new
void* operator new(size_t size, void *p ) throw {
return p;
}
placement new 的使用
class SPort {..... };
const int comLoc = 0x004000;
void* comAddr = reinterpret_cast<void *>(comLoc);
SPort* com1 = new (comAddr) Sport; // placement new 的使用。
从上,我们知道placement new是不会分配任何存储,仅仅是返回一个指向已经分配好空间的指针。所以,不能够对其调用delete操作:
delete com1; // 错误
如果要销毁placement 实例化的对象,我们应该调用其析构函数:
com1->~SPort();
然而,自己主动的调用析构函数往往容易出错,常常导致一个对象析构多次,或者根本没有析构,只有在有必要的时候,才这样设计。
特定于类的内存管理
如果你不希望你的类使用全局的operator new和operator delete,那么你可以在自己的类中,显示的定义属于自己的operator new 和 delete。
class Hanle {
public:
void* operator new(size_t );
void operator delete(void *);
};
在new 操作符进行对象的分配的时候,首先查看Handle的作用域内是否定义了operator new,如果没有找到,那么就使用全局作用域operator new。你定义了operator new ,那么一般要定义operator delete。成员operator new 均为静态成员函数。因为如果不是静态成员函数,因为operator new需要调用在对象构造之前,operator delete 需要调用在对象析构之后,若是一般的成员函数,对象在构造之前,是不有效的。静态成员函数中没有this指针。operator new 和 operator delete 是可以被子类继承的,当然如果子类重写了它们,将调用子类的。
class MyHandle : public Handle {
};
Hanle* my = new MyHandle(); // use Handle::operator new
delete my; // use Handle::operator delete
当然,作为基类的Handle需要有一个虚析构函数,否则不会调用子类的析构函数和operator delete。一个常见的误解是使用new操作符就意味着在堆中分配内存,实际上不是如此,new操作符只是意味着operator new 将被调用,而且返回一个指向某块内存的地址。全局的operator new意味着在堆中申请内存,但是类中的operator new对分配的内存并没有任何限制。可以使静态分配的内存,也可以是容器内部。
重载operator++的前缀和后缀模式
class Number {
public:
Number& operator++ () // prefix ++ {
// Do work on this. (increment your object here)
return *this;
}
// You want to make the ++ operator work like the standard operators
// The simple way to do this is to implement postfix in terms of prefix. //
Number operator++ (int) // postfix ++ {
Number result(*this); // make a copy for result
++(*this); // Now use the prefix version to do the work
return result; // return the copy (the old) value.
}
};
使用带检查的STL实现
带检查的STL实现有哪些?
用算法代替手工编写的循环
调用算法时,应该考虑编写自定义函数对象来封装所需要的逻辑。要抛弃那种处理每个元素的循环式思路。算法的效率也可能比循环好,手写迭代器可能出现迭代器无效的情况。
几个例子:
deque<double>::iterator current = d.begin();
for (size_t i = 0; i < max; ++i) {
current = d.insert(current, data[i] + 41);
++current;
}
使用算法:
transform(data, data + max, insterter(d, d.begin()), _1 + 41);
我们看看inserter的定义:
和 std::instert_iterator的定义
使用正确的查找算法
对于查找算法有:
- find/ find_if
- count/count_if
- binary_search
- lower_bound
- upper_bound
- equal_range