《Effective C++》
1 让自己习惯C++
1 视C++为一个语言联邦
u C++并不是一个带有一组守则的一体语言,它是由4个次语言组成的,每个次语言都有自己的规约。C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。例如,对于内置类型而言(C),传值比传引用更加高效,对于用户自定义类型(O),由于构造函数和析构函数的存在,传const引用更加高效。
C++是一个支持多重范式编程语言:过程形式(procedural)、面向对象(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming)。
C++包含的4次语言:
1)C
区块、语句、预处理、内置数据类型、数组、指针等都来自C,相较于C++,C没有模板、异常、重载;
2)Object-Oriented C++
classes(包括构造函数和析构函数)、封装、继承、多态、虚函数等。
3)Template C++
4)STL
2 尽量以const/enum/inline替换#define
u 对于单纯常量,最好以const对象或enums替换#defines
u 对于形似函数的宏(macros),最好改用inline函数替换#defines
1)使用宏定义的变量名称可能没有进入记号表(symbol table);
2)相较于const变量,宏定义的简单替换会导致更多的目标码;
3)宏定义不强调作用域,不提供任何封装;
4)宏并非完全不必要,#include仍然是必需品,#ifdef/#ifndef
3 尽可能使用const
u const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体,声明为const可帮助编译器侦测出错误用法
u 当const和non-const成员函数有着实质等价的实现时,令non-cast版本调用const版本可以避免代码重复
u 编译器强制实施bitwise constness,编写程序应该使用概念上的常量性(conceptual constness)
//两个成员函数如果只是常量性不同,可以被重载
//当const和non-const成员函数有着实质等价的实现时,令non-cast版本调用const版本可以避免代码重复
class TextBlock
{
public:
const char& operator[] (std::size_t position) const
{
return text[position];
}
char& operator[] (std::size_t position)
{
//return text[position];
return const_cast<char&>(static_cast<const TextBlock>(*this)[position]);
}
private:
std::string text;
};
//使用mutable释放掉non-static成员变量的bitwise constness约束
class CTextBlock
{
public:
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
}
std::size_t CTextBlock::length() const
{
if (!lengthIsValid)
{
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
4 确定对象被使用前已被初始化
u 为内置型对象进行手工初始化,因为C++不保证初始化它们
u 构造函数最好使用成员初值列(member initialzation list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同;
u 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。
u 对于内置类型成员变量,其初始化与赋值的成本相同。如果成员变量是const或reference,它们就只能初始化不能被赋值。最简单的做法就是使用成员初值列(member initialization list),这样做有时候绝对必要,且又往往比赋值更高效。
u C++有着十分固定的“成员初始化次序”,基类在派生类之前初始化,类的成员变量以其声明次序被初始化。
u C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。
为避免在对象初始化之前过早使用它们,需要以下三个步骤:
1)手动初始化内置型non-member对象
2)使用成员初值列初始化对象的所有成分
3)在“初始化次序不确定”的情形下,加强设计(引入单例)
static对象
其寿命从被构造出来直到程序结束为止,包括global对象、定义于namespace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象,不包括stack和heap-based对象。函数内的static对象称为local static对象,其他的对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。
编译单元(translation unit)
指产出一个目标文件的那些源码,基本上它是单一源码加上其所含入的头文件。
2 构造/析构/赋值运算
5 了解C++默默编写并调用了哪些函数
u 定义一个空类Empty,编译器会自动声明一个默认构造函数、拷贝构造函数、拷贝赋值函数、析构函数,这些函数都是public且inline。只有当这些函数被调用,它们才会被编译器创建出来。
编译器创建的析构函数是non-virtual,除非这个类的基类自身声明有virtual析构函数。
如果类中已声明构造函数,编译器不会再创建默认构造函数。
对于拷贝赋值函数,必须保证生成的代码合法且有适当机会证明它有意义。
template<class T>
class NamedObject
{
public:
NamedObject(std::string& name, const T& value);
private:
std::string& nameValue; //引用不允许赋值
const T objectValue;
};
std::string newDog("Persephone");
std::string oldDlog("Satch");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDlog, 36);
p = s;
在上面这种情况下,由于引用不允许赋值,因此编译器不会自动生成相应的拷贝赋值函数。类似的情况还有const成员以及基类的拷贝赋值函数被声明为private。
6 若不想使用编译器自动生成的函数,就该明确拒绝
u 为驳回编译器自动提供的机能(暗自生成拷贝构造函数、拷贝赋值函数),可将相应的函数声明为private并且不予实现,也可以使用Uncopyable作为基类。
class Uncopyable
{
protected:
Uncopyable(){} //允许派生类对象构造和析构
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
7 为多态基类声明virtual析构函数
u 带多态性质的基类应该声明一个虚析构函数,如果类中带有任何虚函数,它就应该拥有一个虚析构函数;
u 如果类的设计目的不是作为基类使用,或不是为了具备多态性,就不应该声明虚析构函数;
8 别让异常逃离析构函数
u 析构函数绝不要吐出异常。如果一个析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或者结束程序
u 如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作
有两种方法避免析构函数抛出异常:
1)如果close抛出异常就结束程序,通常通过调用abort完成
DBConn::~DBConn()
{
try
{
db.close();
}
catch (...)
{
std::abort(); //制作运转记录,记下对close的调用失败
}
}
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 (...)
{
//制作运转记录,记下对close的调用失败
}
}
}
private:
DBConnection db;
bool closed;
};
9 绝不在构造和析构过程中调用virtual函数
在构造和析构期间不要调用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
{
public:
virtual void logTransaction() const;
};
重新设计之后
class Transaction
{
public:
explicit 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))
{
}
private:
static std::string createLogString(parameters);
}
10 令operator=返回一个*this的引用
内置类型以及STL容器都支持连锁赋值形式,如:
int x, y, z;
x = y = z = 15;
为了实现“连锁赋值”,赋值操作符必须返回一个指向操作符左侧实参的引用,该原则也适用于赋值相关运算符,如+=、*=等。
class Widget
{
public:
Widget& operator+=(const Widget& rhs)
{
...
return *this;
}
Widget& operator=(const Widget& rhs)
{
...
return *this;
}
};
11 在operator=中处理“自我赋值”
u 确保当对象自我赋值时operator=有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址(证同测试)、精心周到的语句顺序以及copy-and-swap
u 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确
重载operator=函数时,存在的两个问题:自我赋值安全和异常安全
class Bitmap{...}
class Widget
{
private:
Bitmap* pb;
};
Widget& Widget::operator =(const Widget& rhs)
{
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
//copy-and-swap技术
class Bitmap{...}
class Widget
{
public:
void swap(Widget& rhs);
private:
Bitmap* pb;
};
Widget& Widget::operator =(const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
Widget& Widget::operator =(const Widget rhs)
{
swap(rhs);
return *this;
}
12 复制对象时勿忘其每一个成分
u Copying函数应该确保复制“对象内的所有成员变量”以及“所有base class成分”
u 不要尝试以某个copying函数实现另一个copying函数,应该将共同技能放进第三个函数中,并由两个copying函数共同调用
3 资源管理
13 以对象管理资源
u 为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源
u 两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null
class Investment;
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
}
以对象管理资源:
1)获得资源后立刻放进管理对象
2)管理对象运用析构函数确保资源被释放
std::auto_ptr<Investment> pInv1(createInvestment());//pInv1指向createInvestment返回对象
std::auto_ptr<Investment> pInv2(pInv1);//pInv2指向对象,pInv1被置为null
pInv1 = pInv2; //pInv1指向对象,pInv2被置为null
受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它,不适用于STL容器。auto_ptr的替代方案是RSCP(reference-counting smart pointer,引用计数型智慧指针),RSCP存在一个弱点,即无法处理环状引用。
auto_ptr和tr1::shared_ptr两者在其析构函数内做delete而不是delete[]动作,这就意味着不能在动态分配而得的array身上使用auto_ptr和tr1::shared_ptr。
14 在资源管理类中小心coping行为
u 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
u 普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法。不过其他行为也都可以被实现。
class Lock
{
public:
explicit Lock() : mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
};
void f()
{
Mutex m;
{
Lock m1(&m);
} //释放资源
}
void f2()
{
Mutex m;
{
Lock m1(&m);
Lock m2(m1); //
}
}
对于RAII对象被复制,有以下处理途径:
1)禁止复制
class Lock : public Uncopyable
{
public:
explicit Lock() : mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
};
2)对底层资源使用“引用计数法”
tr1::shared_ptr第二个参数定制删除器(引用次数为时执行)
class Lock
{
public:
explicit Lock(Mutex* pm) : mutexPtr(pm, unlock)
{
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
3)复制底部资源(深拷贝)
4)转移底部资源的拥有权
使用auto_ptr
15 在资源管理类中提供对原始资源的访问
u APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理的资源”的办法
u 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便
class FontHandle;
void releaseFont(FontHandle fh);
class Font
{
public:
explicit Font(FontHandle fh) : f(fh)
{
}
~Font()
{
releaseFont(f);
}
//获取原始资源的两种方法
FontHandle get() const
{
return f;
}
//隐式转换
operator FontHandle() const
{
return f;
}
private:
FontHandle f;
};
Font f1(getFont());
FontHandle f2 = f1; //f1隐式转换为FontHandle
16 成对使用new和delete时要才去相同形式
u 如果在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果在表达式中不使用[],一定不要在相应的delete表达式中使用[]。
使用vector<string>替换这里的数组定义
typedef std::string AddressLines[4];
std::string *pa1 = new AddressLines;
//delete pa1;//error
delete [] pa1;
17 以独立语句将newed对象置入智能指针
u 以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以觉察的资源泄露。
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
int f()
{
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority()); //error
//对于调用函数的参数处理没有明确顺序
//1 调用“new Widget”
//2 调用priority
//3 调用tr1::shared_ptr构造函数
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
}
4 设计与声明
18 让接口容易被正确使用,不容易被误用
u 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质
u “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容
u “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
u tr1::shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题,可被用来自动解除互斥锁等待
std::tr1::shared_ptr<Investment> createInvestment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);
retVal = ...;
return retVal;
}
19 设计class犹如设计type
class的设计就是type的设计。
20 宁以pass-by-reference-to-const替换pass-by-value
u 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可以避免切割问题(slicing problem)
u 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言pass-by-value往往比较合适
21 必须返回对象时,别妄想返回其reference
u 绝不要返回一个local stack对象的指针或引用,或者返回一个heap-allocated对象的引用,或者返回一个local static对象的指针和引用(有可能同时需要多个这样的对象)
22 将成员变量声明为private
u 切记将成员变量声明为private。这可赋予客户方位数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供类作者以充分的实现弹性
u protected并不比public更具封装性
public意味着不封装,几乎可以说不封装意味着不可以改变,尤其是对于那些被广泛使用的类。比如说,删除一个public成员变量,所有使用它的客户代码都会被破坏,如果是一个protected成员变量,则所有使用它的派生类都会被破坏,因此protected成员变量和public成员变量一样缺乏封装性。实际上只有两种访问权限:private(提供封装)和其他(不提供封装)。
23 宁以non-member、non-friend替换member函数
u 宁可拿non-member、non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。
24 若所有参数皆需类型转换,请为此采用non-member函数
u 如果需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
class Rational
{
...
};
const Rational operator* (const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rsh.numerator(),
lhs.denominator() * rsh.denominator());
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2;
result = 2 * oneFourth;
25 考虑写出一个不抛出异常的swap函数
u 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常
u 如果你提供一个member swap,也该提供一个non-member swap用来调用前者
u 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“名空间资格修饰”
u 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些std而言全新的东西
swap函数是实现异常安全性编程、自我赋值的一种常见机制。
5 实现
26 尽可能延后变量定义式的出现时间
u 尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。
//反例
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted;
if (password.length() < MinimumPasswordLength)
{
throw logic_error("Password is too short");
}
...
return encrypted;
}
//范例
void encrypt(std::string& s);
std::string encryptPassword(const std::string& password)
{
using namespace std;
if (password.length() < MinimumPasswordLength)
{
throw logic_error("Password is too short");
}
//string encrypted;
std::string encrypted(password);
encrypt(encrypted);
return encrypted;
}
27 尽量少做转型动作
u 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
u 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
u 宁可使用C++式转型,不要使用旧式转型。前者很容辨识出来,而且也比较有分门别类的职掌。
C++提供四种新式转型:
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
const_cast:通常被用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的C++式的转型操作符;
dynamic_cast:主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属于继承体系中的某个类型(将基类指针或引用转换为派生类)。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作;
reinterpret_cast:意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个*int转型为int。
static_cast:用来强迫隐式转换(implicit conversions),例如将non-const对象转为const对象,将int转为double,将void*指针转为typed指针,将基类指针转为派生类指针,但它无法将const转为non-const(这个只有const_cast才办得到)。
class Base{};
class Derived: public Base{};
void f()
{
Derived d;
Base* pb = &d;
}
基类指针pb指向派生类对象,有时候上述两个指针并不相同。这种情况下会有个偏移量在运行期被施加于派生类指针身上,用以取得正确的基类指针值。单一对象(例如派生类对象)可能拥有一个以上的地址(例如“以Base*”指向它时的地址和“以Derived*”指向它时的地址)。
class Window{};
class SpecialWindow: public Window
{
public:
void blink();
}
typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs;
for (VPW::iterator iter = winPtrs.begin();
iter != winPtrs.end(); ++iter)
{
if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
{
psw->blink();
}
}
//避免转型的一种可行方案
typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPW;
VPW winPtrs;
for (VPW::iterator iter = winPtrs.begin();
iter != winPtrs.end(); ++iter)
{
(*iter)->blink();
}
//避免转型的另一种处理方法
class Window
{
public:
virtual void blink();
};
class SpecialWindow: public Window
{
public:
virtual void blink();
}
28 避免返回handles指向对象内部成分
u 避免返回handles(包括引用、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
class Point
{
public:
Point(int x, int y);
void setX(int newVal);
void setY(int newVal);
};
struct RectData
{
Point ulhc;
Point lrhc;
};
class Rectangle
{
Point& upperLeft() const
{
return pData->ulhc;
}
Point& lowerRight() const
{
return pData->lrhc;
}
private:
std::tr1::shared_ptr<RectData> pData;
};
void f()
{
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2);
rec.upperLeft().setX(50); //修改了私有变量,破坏了封装
}
处理方法:
class Rectangle
{
const Point& upperLeft() const
{
return pData->ulhc;
}
const Point& lowerRight() const
{
return pData->lrhc;
}
private:
std::tr1::shared_ptr<RectData> pData;
};
但仍然存在问题空悬指针问题。
class GUIObject{};
const Rectangle boundingBox(const GUIObject& obj);
void f()
{
GUIObject* pgo;
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());
}
boundingBox(*pgo)函数调用返回一个临时的Rectangle对象。pUpperLeft 指向一个标示temp的Points,在整个调用语句结束之后,temp内的Points被析构。pUpperLeft 变成空悬、虚吊的指针。
29 为“异常安全”而努力是值得的
u 异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
u “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
u 函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全保证”中的最弱者。
“异常安全”有两个条件:
1)不泄露任何资源
2)不允许数据败坏
异常安全函数(Exception-Safe Functions)提供以下三个保证之一:
1)基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。
2)强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态。
3)不抛掷保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如ints,指针等等)身上的所有操作都提供nothrow保证。
struct PMImpl
{
std::tr1::shared_ptr<Image> bgImage;
int imaeChanges;
};
class PrettyMenu
{
private:
Mutex mutex;
std::tr1::shared_ptr<PMImpl> pMImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
using std::swap;
Lock ml(&mutex);
std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pMImpl));
pNew->bgImage.reset(new Image(imgSrc));
++pNew->imaeChanges;
swap(pMImpl, pNew);
}
30 透传了解inlining的里里外外
u 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最小化。
u 不要只因为function templates出现在头文件,就声明它们为inline。
inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于类定义内。
大部分编译器拒绝将太过复杂的函数内联,如带有循环、递归、对虚函数的调用(毕竟虚函数要到运行期才能确定调用哪个函数,而内联是在编译期进行替换)等。有时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体,如果程序需要取某个inline函数的地址,编译器通常必须为此函数生成一个outlined函数本体。
inline void f(){}
void (*pf)() = f;
f(); //这个调用将被inlined,因为它是一个正常调用
pf(); //这个调用或许不被inlined,因为它通过函数指针达成
内联函数无法随着程序库的升级而升级。如果f是程序库的一个内联函数,客户将f函数本体编进其程序,一旦f被改变,所有用到f的客户程序都需要重新编译。
31 将文件间的编译依存关系降至最低
u 支持“编译依赖性最小化”的一般构想是:相依与声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。
u 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用
class Person
{
public:
Person(const std::string& name, const Data& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
上面这段代码一旦Data、Address类被修改,Person类以及使用Person类的代码都需要重新编译。
#include <string>
#include <memory>
class PersonImpl;
class Date;
class Address;
class Person
{
public:
Person(const std::string& name, const Data& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::tr1::shared_ptr<PersonImpl> pImpl;
};
#include "Person.h"
#include "PersonImpl.h"
Person::Person(cosnt std::string& name, const Date& birthday, const Address& addr)
: pImpl(new PersonImpl(name, birthday, addr))
{
}
std::string Person::name() const
{
return pImpl->name();
}
通过通过“将接口从实现中分离”来减少依赖。其中一种方法是pimpl idiom(也称编译防火墙)。这个分离的关键在于以“声明的依存性”替换“定义的依存性”,这正是编译依存性最小化的本质。像Person这样使用Pimpl idiom的类称为Handle类(Handle classes)
1)如果使用指针或引用可以完成任务,就不要使用对象。
你可以只靠一个类型声明式就定义出指向该类型的引用和指针,但如果定义某类型的对象,就需要用到该类型的定义式。
2)如果能够,尽量以类声明式替换类定义式。
当你声明一个函数而它用到某个类时,你并不需要该类的定义,即使该函数以传值方式传递该类型的参数。
另一种处理方法是Interface classes
class Person
{
public:
~Person();
std::string name() const = 0;
std::string birthDate() const = 0;
std::string address() const = 0;
static std::tr1::shared_ptr<Person>
create(const std::string& name, const Date& birthday, const Address& addr);
};
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;
};
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));
}
6 继承与面向对象设计
32 确定你的public继承塑模出is-a关系
u “public继承”意味着is-a。适用于基类身上的每一件事情一定也适用于派生类身上,因为每一个派生类对象也都是一个基类对象。
33 避免遮掩继承而来的名称
u 派生类内的名称会遮掩基类内的名称,在public继承下从来没有人希望如此。
u 为了让遮掩的名称再见天日,可使用using声明式或转变函数。
34 区分接口继承和实现继承
u 接口继承和实现继承不同。在public继承之下,派生类总是继承基类的接口
u 纯虚函数只具体制定接口继承
u 简朴的非纯虚函数具体指定接口继承及缺省实现继承
u 非虚函数具体指定接口继承以及强制性实现继承
35 考虑virtual函数以外的其他的选择
virtual函数的替代方案包括NVI手法以及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式
将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法方位class的non-public成员
tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定的目标签名式兼容”的所有可调用物。
class GameCharacter
{
public:
virtual int healthValue() const; //派生类可以重新定义
};
virtual函数的替代方案:
1)使用non-virtual interface(NVI)手法,那是模板方法(Template Method)设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。
class GameCharacter
{
public:
int healthValue() const
{
//做一些事前工作
int retVal = doHealthValue();
//做一些事后工作
return retVal;
}
private:
virtual int doHealthValue() const
{
}
};
优点:healthValue函数称为doHealthValue的外覆器(wrapper),NVI手法使得注释部分在调用virtual函数前后完成一些工作。
2)将virtual函数替换为“函数指针成员变量”,这是策略(Strategy)设计模式的一种分解表现形式
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc()) : healthFunc(hcf)
{
}
int healthValue() const
{
return healthFunc(*this);
}
void setHealthCalculator(HealthCalcFunc hcf)
{
healthFunc = hcf;
}
private:
HealthCalcFunc healthFunc;
};
优点:
a)同一个人物类型的不同实体可以有不同的健康计算函数
b)某个已知人物的健康指数计算函数可以在运行期通过setHealthCalculator进行变更
缺点:
a)降低GameCharacter封装性,因为non-member函数要访问类的non-public函数,需添建立friend关系或者新增public接口供non-member函数调用。
3)以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
typedef std::tr1::function<int (const GameCharacter&)> HealthCalFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
{
}
int healthValue() const
{
return healthFunc(*this);
}
void setHealthCalculator(HealthCalcFunc hcf)
{
healthFunc = hcf;
}
private:
HealthCalcFunc healthFunc;
};
short calcHealth(const GameCharacter&);
struct HealthCalculator
{
int operator()(const GameCharacter&) const
{
}
};
class GameLevel
{
public:
float health(const GameCharacter&) const;
};
class EvilBadGuy: public GameCharacter
{
};
class EyeCandyCharacter: public GameCharacter
{
};
EvilBadGuy ebg1(calcHealth); //人物:使用某个函数计算健康指数
EyeCandyCharacter ecc1(HealthCalculator()); //人物:使用某个函数对象计算健康指数
GameCharacter currentLevel;
EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health, currentLevel, _1)); //人物,使用某个成员函数计算健康指数
4)将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是策略(Strategy)设计模式的传统实现手法
class GameCharacter;
class HealthCalcFunc
{
public:
virtual int calc(const GameCharacter& gc) const
{
}
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter
{
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
: pHealthCalc(phcf)
{
}
int healthValue() const
{
return pHealthCalc->calc(*this);
}
private:
HealthCalcFunc* pHealthCalc;
};
可调用对象(callable entity)
函数指针、函数对象、成员函数指针
36 绝不要重新定义继承而来的non-virtual函数
class Base
{
public:
void mf()
{
cout << "Base::mf()" << endl;
}
};
class Drived : public Base
{
public:
void mf()
{
cout << "Drived::mf()" << endl;
}
};
Drived d;
Base *pb = &d;
pb->mf(); //Base::mf()
Drived *pd = &d;
pd->mf(); //Drived::mf()
37 绝不要重新定义继承而来的缺省参数值
通常继承主要两类函数:虚函数和非虚函数。重新定义一个继承而来的非虚函数永远是错误的。虚函数是动态绑定的,但是其缺省参数值确实静态绑定的。
38 通过复合塑模出has-a或根据“根据某物实现出”
public继承带有“is-a”的含义,复合意味着“has-a”或者“is-implemented-in-terms-of”,在应用域,复合意味着“has-a”,在实现域,复合意味着is-implemented-in-terms-of(根据某物 实现出)。真实世界中的某些事物,例如人、汽车等等对象属于应用域,其他对象则纯粹是实现细节上的人工制品,如缓冲区、互斥器、查找树等,这些对象属于实现域。
39 明智而审慎地使用private继承
u private继承意味is-implemented-in-terms-of(根据某物实现出)。它通常比复合(composition)的级别低。但是当派生类需要访问基类的保护成员或需要重新定义继承而来的虚函数时,这样设计是合理的。
u 和复合不同,private继承可以造成空基类最优化。这对致力于“对象尺寸最小化”的程序库而言,可能很重要。
如果类之间的继承关系是private,编译不会自动将一个派生类对象转换为一个基类对象;其次,由private基类继承而来的所有成员,在派生类中都会变成private属性,即使它们在基类中原本是protected或者public属性。
private继承意味着“is-implemented-in-terms-of”,只有实现部分被继承,接口部分应略去。如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意涵。private继承在软件“设计”层面没有意义,其意义只及于软件实现层面。
private继承和复合都意味着“is-implemented-in-terms-of”,尽可能使用复合,必要时才使用private继承(涉及protected成员和virtual函数,或者处于空间考虑)
class Timer
{
public:
explicit Timer(int tickFrequency);
virtual void onTick() const;
};
class Widget : private Timer
{
private:
virtual void onTick() const; //查看Widget的数据
};
Widget需要重定义虚函数onTick,Widget就必须继承自Timer,但是Widget并不是一个Timer。通过private继承可以解决这个问题。
class Widget
{
private:
class WidgetTimer : public Timer
{
public:
virtual void onTick() const;
};
WidgetTimer timer;
};
private继承无法阻止onTick被Widget 的派生类修改,另外将timer声明为WidgetTimer指针,可降低编译依存性。
class Empty{};
class HoldsAnInt : private Empty
{
private:
int x;
Empty e;
};
sizeof(HoldsAnInt) > sizeof(int)
class HoldsAnInt : private Empty
{
private:
int x;
};
sizeof(HoldsAnInt) = sizeof(int)
EBO(empty base optimiszation)空基类优化,EBO一般只在单一继承下才可行,对于拥有多个基类的派生不适用。
40 明智而审慎地使用多重继承
u 多重继承比单一继承赋值。它可能导致新的歧义,以及对virtual继承的需要。
u virtual继承会增加大小、速度、初始化(以及赋值)复杂度等等成本。如果virtual基类不带任何数据,将是最具实用价值的情况
u 多重继承的确有正当用途。其中一个情节涉及“public继承某个接口类(Interface class)”和“private继承某个协助实现的类”的两相组合。
多重继承会带来以下两个问题:
1)从一个以上的基类继承相同名称(函数、typedef等),造成歧义
class BorrowableItem
{
public:
void checkOut();
};
class ElectronicGadget
{
private:
bool checkOut() const;
};
class Mp3Player:
public BorrowableItem,
public ElectronicGadget
{
};
void f()
{
Mp3Player mp3;
mp3.checkOut(); //歧义
}
即使两个checkOut函数的返回值、const、封装属性不同,仍然会造成歧义。重载解析时,C++首先确认这个函数对此调用而言是最佳匹配,找出最佳匹配之后才检验其可取用性(封装属性),上述两个checkOut具有相同的匹配程度。
7 模板与泛型编程
41 了解隐式接口和编译器多态
类和模板都支持接口和多态
对类而言接口是显式的,以函数签名为中心。多态则是通过虚函数发生于运行期
对模板参数而言,接口是隐式的,奠基于有效表达式。多态则是通过模板具现化和编译期的函数重载解析(function overloading resolution)
42 了解typename的双重意义
声明模板参数时,前缀关键字class和typename可互换
请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符
template<typename T>
class Derived : public Base<T>::Nested //base class list不允许typename
{
public:
explicit Derived(int x)
: Base<T>::Nested(x) //member initialization list不允许typename
{
typename Base<T>::Nested temp; //不在基类、初始化列中需加上typename
}
};
template<typename IterT>
void workWithIterator(IterT iter)
{
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
}
43 学习处理模板化基类内的名称
class CompanyA
{
public:
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
};
class CompanyB
{
public:
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
};
class MsgInfo{};
template<typename Company>
class MsgSender
{
void sendClear(const MsgInfo& info)
{
std::string msg;
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info)
{
std::string msg;
Company c;
c.sendEncrypted(msg);
}
};
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
//传送前logging
sendClear(info);
//传送后logging
}
};
以上代码会出现编译错误,原因是编译器编译器并不知道LoggingMsgSender继承的是哪个基类(直到LoggingMsgSender被具现化)。也就无法知道MsgSender<Company>的实现,是否包含sendClear函数。
假设CompanyZ坚持使用加密通讯(一般的MsgSender模板对CompanyZ并不合适,因为其中包含一个sendClear函数),为矫正这个问题,可以针对CompanyZ特化(全特化)一个MsgSender模板。
class CompanyZ
{
public:
void sendEncrypted(const std::string& msg);
};
template<>
class MsgSender<CompanyZ>
{
public:
void sendSecret(const MsgInfo& info)
{
}
};
如果以MsgSender<CompanyZ>作为LoggingMsgSender的基类,同样会出现编译错误,因为基类并未提供sendClear函数。
这也就是C++拒绝这个调用的原因,它知道基类模板有可能被特化,而特化版本可能不提供和一般性模板相同的接口,因此它往往拒绝在模板化基类(templatized base classes)(例如MsgSender<Company>)内寻找继承而来的名称(SendClear)。
解决以上问题由三种处理方法:
1)使用this指针
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
//传送前logging
this->sendClear(info);
//传送后logging
}
};
2)使用using声明
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
using MsgSender<Company>::sendClear;
void sendClearMsg(const MsgInfo& info)
{
//传送前logging
sendClear(info);
//传送后logging
}
};
3)指出被调用的函数位于基类中
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
//传送前logging
MsgSender<Company>::sendClear(info);
//传送后logging
}
};
44 将与参数无关的代码抽离模板
模板生成多个类和多个函数,所以任何模板代码都不该与某个造成膨胀的template参数产生相依关系
因非类型模板参数而造成代码膨胀,往往可消除,做法是以函数参数或类成员变量替换模板参数
因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型
template<typename T, std::size_t n>
class SquareMatrix
{
public:
void invert();
};
SquareMatrix<double, 5> sm1;
sm1.invert();
SquareMatrix<double, 10> sm2;
sm2.invert();
以上代码会具现化两份invert,造成代码膨胀。
template<typename T>
class SquareMatrixBase
{
protected:
SquareMatrixBase(std::size_t n, T* pMem)
:size(n), pData(pMem)
{
}
private:
std::size_t size;
T* pData;
};
template<typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T>
{
public:
SquareMatrix() : SquareMatrixBase<T>(n, data)
{
}
private:
T data[n*n];
};
通过动态分配内存,可以减少对象自身大小。
template<typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T>
{
public:
SquareMatrix() : SquareMatrixBase<T>(n, 0), pData(new T[n*n])
{
this->setDataPtr(pData.get());
}
private:
boost::scoped_arry<T> pData;
};
45 运用成员函数模板接受所有兼容类型
请使用成员函数模板(member function templates)生成“可接受所有兼容类型”的函数
如果你声明member templates用于“泛化copy构造”或者“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符
template<class T>
class shared_ptr
{
public:
shared_ptr(shared_ptr const& r); //copy构造函数
template<class Y>
shared_ptr(shared_ptr<Y> const& r); //泛化copy构造函数
shared_ptr& operator=(shared_ptr const& r); //copy赋值函数
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const& r); //泛化拷贝赋值函数
};
46 需要类型转换时请为模板定义非成员函数
当我们编写一个class template,而它所提供的“与此template相关的”函数支持“所有参数的隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”
template<typename T>
class Rational
{
public:
Rational(const T& numerator = 0, const T& denominator = 1);
const T numerator() const;
const T denominator() const;
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{
}
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; //error:编译错误
以oneHalf 进行推导很容易得到T为int,operator*的第二个参数为Rational<T>,但实际传递的参数类型为int,虽然Rational<int>的non-explicit构造函数可以将2转换为Rational<int>,进而将T推导为int,但是编译器并不会那么做,模板实参推导过程中从不将隐式类型转换函数纳入考虑。
template<typename T>
class Rational
{
public:
Rational(const T& numerator = 0, const T& denominator = 1);
const T numerator() const;
const T denominator() const;
friend const Rational operator*(const Rational& lhs, const Rational& rhs);
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{
}
上面代码可以通过编译,当oneHalf被声明为一个Rational<int>,class Rational<int>于是被具现化出来,作为过程的一部分,friend函数operator*(接受Rational<int>模板参数)也就被声明出来(作为一个函数而非模板函数),但是这个函数并没有被定义出来。
template<typename T>
class Rational
{
public:
Rational(const T& numerator = 0, const T& denominator = 1);
const T numerator() const;
const T denominator() const;
friend const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator(), rhs.denominator());
}
};
这里使用friend函数与传统用途访问类的非公有成员不同。为了让类型转换可能发生于所有实参身上,我们需要一个non-member函数,为了让这个函数被自动具化,我们需要将它声明在类内部,而在类内部声明非成员函数的唯一办法就是:令它成为一个friend函数。
template<typename T> class Rational;
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational<T>(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
template<typename T>
class Rational
{
public:
Rational(const T& numerator = 0, const T& denominator = 1);
const T numerator() const;
const T denominator() const;
friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{
return doMultiply(lhs, rhs);
}
};
doMultiply本身不支持混合式运算,但是operator*支持。
47 请使用traits classes表现类型信息
traints classes使得“类型相关信息”在编译期可用。它们以templates和“templates特化”完成实现
整合重载技术后,traits classes有可能在编译期对类型执行if...else测试。
C++程序标准库中迭代器的tag struct定义:
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{};
template<typename T>
class deque
{
class iterator
{
public:
typedef random_access_iterator_tag iterator_category;
};
};
template<typename T>
class list
{
class iterator
{
public:
typedef bidirectional_iterator_tag 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::bidirectional_iterator_tag)
{
if (d < 0)
{
throw std::out_of_range("Negative distange");
}
while(d--) ++iter;
}
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}
48 template元编程
模板元编程(TMP,Template metaprograming)可将工作由运行期移往编译期,因而得以实现早期编译错误侦测和更高的执行效率
TMP可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if (typeid(typename std::iterator_traits<IterI>::iterator_category)
== typeid(std::random_access_iterator_tag))
{
iter += d;
}
else
{
if (d >= 0)
{
while(d--) ++iter;
}
else
{
while(d++) --iter;
}
}
}
这里使用的typeid-based方法效率比traits低,因为类型测试发生于运行期而非编译期,运行期类型测试代码会出现在(或者说被链接于)可执行文件中。
std::list<int>::iterator iter;
advance(iter, 10);
等同于
void advance(std::list<int>::iterator& iter, int d)
{
if (typeid(typename std::iterator_traits<std::list<int>::iterator>::iterator_category)
== typeid(std::random_access_iterator_tag))
{
iter += d; //error
}
else
{
if (d >= 0)
{
while(d--) ++iter;
}
else
{
while(d++) --iter;
}
}
}
虽然typeid测试会失败从而不会执行“iter += d;”,但是编译器必须保证所有源码都有效,即使是不会执行起来的代码。
8 定制new和delete
49 了解new-handler的行为
template<typename T>
class NewHandlerSupport
{
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 currentHandler;
};
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));
return ::operator new(size);
}
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
class Widget: public NewHandlerSupport<Widget>
{
};
50 定制new和delete
有许多理由需要改写自定的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息
51 编写new和delete时需固守常规
operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0字节申请。class专属版本则还应该处理“比正确大小更大的(错误)申请”。
operator delete应该在收到null指针时不做任何事。class专属版本则还应该处理“比正确大小更大的(错误)申请”
52 写了placement new也要写placement delete
当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时续的内存泄露
当你声明placement new和placement delete,请确定不要无意识(非故意)地遮掩了它们的正常版本
《Effective C++》相关推荐
- [.NET] 《Effective C#》快速笔记 - C# 中的动态编程
<Effective C#>快速笔记 - C# 中的动态编程 静态类型和动态类型各有所长,静态类型能够让编译器帮你找出更多的错误,因为编译器能够在编译时进行大部分的检查工作.C# 是一种静 ...
- 《Effective C++》第8章 定制new和delete-读书笔记
章节回顾: <Effective C++>第1章 让自己习惯C++-读书笔记 <Effective C++>第2章 构造/析构/赋值运算(1)-读书笔记 <Effecti ...
- 《Effective C++》读书笔记(第一部分)
有人说C++程序员可以分为两类,读过Effective C++的和没读过的.世界顶级C++大师Scott Meyers 成名之作的第三版的确当得起这样的评价. 本书并没有你告诉什么是C++语言,怎样使 ...
- 《Effective STL》学习笔记(第一部分)
本书从STL应用出发,介绍了在项目中应该怎样正确高效的使用STL.本书共有7个小节50个条款,分别为 (1) 容器:占12个条款,主要介绍了所有容器的共同指导法则 (2) vector和string: ...
- 《Effective Java2》笔录
转自:http://www.verydemo.com/demo_c89_i134810.html Joshua Bloch在国内出版的书包括<Effective Java2><Jav ...
- 《Effective C++》item25:考虑写出一个不抛异常的swap函数
std::swap()是个很有用的函数,它可以用来交换两个变量的值,包括用户自定义的类型,只要类型支持copying操作,尤其是在STL中使用的很多,例如: int main(int argc, _T ...
- 《Effective C#》Part I:第一部分总结
第一部分是语言使用的基础,而这是使用语言的基本功,也是平常最不注意的,养成好的习惯,有利于后期水平提高. 这部分包括前面11个单元,分别如下: Item 1: Always Use Propert ...
- 《Effective C#》的读书笔记
突然看到的一篇关于阅读 Bill Wagner先生的<Effective C#>的读书笔记,觉得写的不错,就在这里进行了链接.如果谁有这本书的中文版,希望可以给我发一下先谢谢了 <E ...
- 【Python 2.7】str和unicode的互相转换,摘自《Effective Python》
str 和 unicode 以下摘自<Effective Python> python3 有两种表示字符序列的类型:bytes 和 str.前者的实例包含原始的8位值:后者的实例包含Uni ...
- 《Effective Java》读书笔记 Item 1:考虑静态工厂方法,而不是构造器
众所周知,要想能获取一个类的实例,该类得要提供一个public的构造器.但是<Effective Java>书中说还有一个方法,那就是提供静态工厂方法(static factory met ...
最新文章
- Go 学习笔记(33)— Go 自定义类型 type(自定义结构体、结构体初始化、结构体内嵌、自定义接口)
- poj 1904 King's Quest
- 践行科技向善,腾讯Light 把光引向厦门
- 截取视图某一段另存为部分视图(Partial View)
- mysql5.7导入数据的权限问题
- window下php5.6-x64-ts可用php_redis.dll文件
- 获取输出最大角标 php,html实现消息按钮上的数量角标的实例详解
- android中fragment如何保存edittext文本,如何在Android中使用DialogFragment进行文本输入?...
- 将稍大文件存储到远程SQL Server服务器
- Win11右建没有刷新怎么办?Win11右建没有刷新的解决方法
- git SSL certificate problem: unable to get local issuer certificate
- C# 如何设置 richTextBoxr的边距
- 智能指针是一种类,别名称为句柄类
- 集成随机惯性权重和差分变异操作的樽海鞘群算法
- 基础平台系列-1-第三方服务
- 腾讯软件测试笔试题西安,腾讯软件测试笔试题题库
- rows是横着的还是cols_rows和cols到底哪个是列哪个是行
- 13.罗马数字转整数
- 盘点Scratch少儿编程的实用性
- (翻译)Apache Kafka 官方文档:开始
热门文章
- 华为OD机试真题:分糖果【2022 Q1 Q2 |200分】
- 【数据结构】克鲁斯卡尔(Kruskal)算法 —PK— 普里姆(Prim)算法
- IOS第三方框架集合
- 微信企业邮更换管理者微信号
- spring(三) spring与mybatis整合
- ubuntu16.04安装jdk以及详细jdk环境配置教程,亲测没毛病。
- Arduino 眨眼睛(定时器版本)
- 使用MATLAB绘制二元函数图像
- 微信小程序页面上面的名字怎么改_微信小程序如何动态修改页面标题——已解决...
- Ubunt下使用Alsa以及Alsa和PulseAudio