文章目录
- 系列文章目录
- 前言
- 1:多语言结合体——C++
- 2:尽量以const, enum, inline替换 #define
- 3:尽可能使用const
- 4:确定对象被使用前已先被初始化
- 5:了解C++默认编写并调用哪些函数
- 6:若不想使用编译器自动生成的函数,明确拒绝
- 7:为多态基类声名virtual析构函数
- 8:Prevent exceptions from leaving destructors
- 9:绝不在构造和析构过程中调用virtual函数
- 10:令operator=返回一个reference to *this
- 11:在operator=中处理“自我赋值”
- 12:copy all parts of an object
- 资源管理
- 13:Use objects to manage resources
- 14:Think carefully about copying behavior in resource-managing classes.
- 15:在资源管理类中提供对原始资源的访问
- 16:成对使用new和delete时要采取相同形式
- 17:Strore newed objects in smart pointers in standalone statements
- 设计与声名
- 18:Make interfaces easy to use correctly and hard to use incorrectly.
- 19:Treat class design as type design
- 20:Prefer pass-by-reference-to-const to pass-by-value
- 21:Don't try to return a reference when you must return an object
- 22:Declare data members private.
- 23:Prefer non-member non-friend functions to member functions
- 24:若所有参数皆需类型转换,请为此采用non-member函数
- 25:考虑写出一个不抛出异常的swap函数
- 实现 Implementations
- 26:尽可能延后变量定义式的出现时间
- 27:Mimimize casting
前言
前言
术语
声名:告诉编译器某个东西的名称和类型,但略去细节。
定义:提供编译器一些声名所遗漏的细节。
初始化:“给对象初始”的过程。
- C
- Object-Oriented C++
- Template C++
- STL
class GamePlayer
{
private:
enum {NumTurns = 5};
int scores[NumTurns];
}
若编译器不允许“int-class 初始值设定”
- 对于单纯常量,最好以const对象或enums替换#define
- 对于形似函数的宏,最好改用inline函数替换#define
class TextBlock
{
public:
TextBlock(string s):text(s) {}
const char& operator[](std::size_t position) const
{
return text[position];
}
char& operator[](std::size_t position)
{
return text[position];
}
private:
std::string text;
};
int main()
{
TextBlock tb("Hello");
cout << tb[0] << endl; //调用non-const TextBlock::operator[]
const TextBlock ctb("WORLD");
cout << ctb[0] << endl; //调用const TextBlock::operator[]
tb[0] = '5'; //✔
ctb[0] = '5'; //❌
}
static_cast, const_cast
4:确定对象被使用前已先被初始化class PhoneNumber {...};
class ABEntry {
public:
ABEntry(const std::string&name);
private:
std::string theName;
};
ABEntry::ABEntry(const std::string&name):theNmae(name) //这叫初始化
{
theName = name; //着叫赋值,非初始化
}
- 为内置类型对象进行手动初始化,C++不保证初始化它们
- 构造函数最好使用成员初始值列表,而不要再构造函数本体内使用赋值操作。初始值列出的成员变量,其排列次序应该和它们再class中的声名次序相同。
- 为免除“跨编译单元之初始化次序”问题,以local static 对象替换non-local static对象。
默认声名:
一个默认构造函数
copy构造函数
copy assignment操作符
析构函数
just like this
class Empty
{
public:
Empty() {}
Empty(const Empty& rhs) {}
Empty& operator=(const Empty& rhs) {}
~Empty() {}
};
Empty e1; //default 构造函数
//析构函数
Empty e2(e1); //copy构造函数
e2 = e1; //copy assignment 操作符
- 编译器可自动为class创建default constructor, copy constructor, copy assignment operator, deconstructor。
class Empty
{
public:
Empty() = delete;
Empty(const Empty&) = delete;
Empty& operator=(const Empty&) = delete;
~Empty() = delete;
};
7:为多态基类声名virtual析构函数
class SpecialString: public std::string { // std::string有个 non-virtual析构函数
...
};
SpecialString* pss = new SpecialString("sdf");
std::string *ps;
ps = pss; //静态类型与动态类型不一致
delete ps; //❌
- polymorphic(带多态性质的) base classes 应该声名一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
- classes的设计目的如果不是作为base classes使用,或不是为了具备多态特性(polymorphically),就不该声明virtual析构函数。
构造函数内的virtual函数还不是virtual的
10:令operator=返回一个reference to *thisclass Widget{
public:
...
Widget& oeprator=(const Widget& rhs){
...
return *this;
}
};
11:在operator=中处理“自我赋值”
隐式的自赋值
a[i] = a[j]; *px = *py
Widget& operator=(const Widget& rhs)
{
if (this = &rhs) return *this;
delete pb; //先把原来的对象删除
pb = new Bitmap(*rhs.pb); //再new一个
return *this;
Bitmap* pOrig = pb;
pb = new Bitmap(rhs->pb);
delete pOrig;
return *this;
Bitmap* tmp = new Bitmap(rhs->pb);
delete pb ;
pb = tmp;
return *this
Widget tmp(rhs);
swap(tmp);
return *this;
}
Widget& Widget::operator=(Widget rhs)
{
swap(rhs);
return *this;
}
- 确保当对象自我赋值时operator= 有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
- Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
使用智能指针管理内存
注意: shared_ptriptr(new int[5]); //不报错,但默认还是用delete 释放内存,会导致内存泄露
- 为防止资源泄露,使用RAII对象,它们再构造函数中获得资源并在析构函数中释放资源。
- 使用shared_ptr
- 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
- 普遍而常见的RAII class copying行为是:抑制copying、引用计数法。
class Investment{
public:
bool isTaxFree() const;
...
};
Investment* createInvestment(); //假定返回一个堆内存指针
std::shared_ptr pInv(createInvestment());
int daysHeld(const Investment* pi);
daysHeld(pInv.get());
pInv->isTaxFree();
(*pInv).isTaxFree();
- APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理的资源”的方法。
- 对原始资源的访问可能经由显示转换或隐式转换。一般来说显式转换比较安全,隐式转换方便。
std::string* stringArray = new std::string[100]; ... delete stringArray; //❌ , delete [] stringArray
- new 与 delete
- new …[] 与 delete []
int priority(); void processWidget(std::shared_ptrpw, int priority); 考虑 processWidget(std::shared_ptr (new Widget), priority()); 其执行顺序可能为: 1. new Widget 2. priority() 3. shared_ptr构造函数 若priority()发生异常,则会造成内存泄露 auto pw = std::shared_ptr (new Widget); processWidget(pw, priority());
- 将new出的对象存放在智能指针时需要单独的一行语句。否则,可能导致内存泄露
struct Day{
explicit Day(int d): val(d) {}
int val;
};
struct Month{
explicit Month(int m):val(m) {}
int val;
};
struct Year{
explicit Year(int y):val(y) {}
int val;
};
class Date{
public:
Date(const Month&, const Day&, const Year&);
};
尽量与内置类型的行为相同,a=b, a*b = c报错等
shared_ptr要比原始指针大且慢,然而其“降低客户错误”的成效却是较为显著和方便的
- 好的接口很容易被正确使用,不容易被误用。应该在所有接口中达到。
- “促进正确使用”的方法,接口一致,以及与内置类型的行为兼容。
- “阻止误用”的方法,建立新类型、限定类型上的操作,数傅对象值,消除用户的资源管理责任。
- shared_ptr支持定制型删除器(custom deleter)。可放置DLL问题,可被用来自动解除互斥锁等。
设计class时需要考虑:
- 新type的对象应该如何被创建和销毁?
- 对象的初始化和对象的赋值该有什么样的差别?
- 新type的对象如果被 passed by value,意味着什么?copy constructor函数用来定义一个type的pass-by-value该如何实现。
- 什么是新type的“合法值”?
- 新type需要配合某个继承图系(inheritance graph)吗?
- 新的type需要什么样的转换?
- 什么样的操作符和函数对此新type而言是合理的?
- 什么样的标准函数应该驳回?(private?)
- 谁该取用新type的成员
- 什么是新type的“未声名接口”(undeclared interface)?
- 新type有多么一般化?
- 真的需要一个新type?
- class的设计就是type的设计。
class Person{
public:
Person();
virtual ~Person();
...
private:
std::string name;
std::string address;
};
class Student: public Person{
public:
Student();
~Student();
...
private:
std::string schoolName;
std::string schoolAddress;
};
- 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。
- 以上规则并不适用与内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较妥当。
- 禁止返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
论点:
- 接口一致性,接口都是函数
- 使用函数可以实现“不准访问”、“读写访问”等
- 封装
如果改变public,所有设计到public的接口倒要改变,如果改变protected,所有用到protected的子类都要变。从封装的角度看,只有private(提供封装)和其它(步提供封装)。
- 将成员变量声名为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者较高的弹性。
- protected并不比public更具封装性。就当他不存在。
class WebBrowser
{
public:
...
void clearCacke();
void clearHistory();
void removeCookies();
...
# 一:member函数
void clearEverything();
};
#二:non-member函数
void clearBrowser(WebBrowser &wb){
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
// 头文件“webbrowser.h”这个头文件针对class WebBrowser自身及WebBrowser核心机能
namespace WebBrowserStuff{
class WebBrowser{...};
... //核心机能,如几乎所有客户都需要的 non-member函数
}
// 头文件"webbrowserbookmarks.h"
namespace WebBrowserStuff{
... //与书签相关的便利函数
}
// 头文件“webbrowsercookies.h”
namespace WebBrowserStuff{
... //与cookie相关的便利函数
}
namespace可以跨头文件,而class必须定义在一个头文件中
将多个“提供便利的函数”放在多个头文件内但隶属同一个namespace,用户可以轻松扩展这一组“提供便利的函数”。如,WebBrowser客户要添加些与影响下载相关的“提供便利的函数”,只需要再WebBrowserStuff命名空间内建立一个头文件,内含那些函数的声名即可。(若在WebBrowser命名空间中声名“提供便利的函数”,那么定义放在哪?)
- 用non-member non-friend函数替换member函数。这样可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。
- 如果你需要为某个函数的所有参数(包括this指针所指的的哪个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
swap多少与异常安全性编程(exception-safe programming)相关
默认swap行为:(如果ab都在堆中该如何处理?a,b都为内置类型,先不考虑堆的情况)
namespace std {
template void swap(T& a, T& b)
{
T temp(a); //a,b都为内置类型
a = b;
b = temp;
}
}
提供public swap成员
将std::swap特例化
class WidgetImpl
{
public:
...
private:
int a, b, c; //可能有许多数据
std::vector v; //意味复制时间很长
...
};
class Widget
{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{
...
*pImpl = *(rhs.pImpl);
...
}
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl);
}
private:
WidgetImpl* pImpl;
};
namespace std{
template<> void swap(Widget& a, Widget& b)
{
a.swap(b);
}
}
std的内容完全由C++标准委员会决定,标准禁止我们膨胀那些已经声名好的东西。即,不要添加任何新东西到std里头。
所以:
namespace WidgetStuff{
...
template class Widget {...};
...
// non-member swap function, 这里不属于std命名空间
template 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 swap, member swaps, non-member swaps, std::swap特例化版本
- 如果swap的默认实现代码对class或class template的效率较高,使用默认即可,调用swap会使用默认版本,并且效率较好。
- 如果swap默认版本效率低下(即,class或class template使用了pimpl“pointer to implementation”)尝试以下行为:
- 提供一个public swap成员函数,让它高效的swap类型的两个对象。这个函数绝对不该抛出异常。
- 在class或class template所在命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
- 如果你正在编写一个class(而非class template),为你class特例化std::swap。并令它调用你的swap成员函数。
remember
- 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
- 如果你提供一个member swap,也提供一个non-member swap用来调用前者。对于classes(而非templates),也请特例化std::swap。
- 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间”。
- 为“用户定义类型”进行std templates特例化是好的,但不要在std内加入某些对std而言全新的东西。
通过default构造函数构造出一个对象然后对它赋值比直接在构造时指定初值效率差。
以下哪个好?
//方法A Widget w; for (int i=0; i//方法B
for (int i=0; i
Widget w(取决于i的某个值);
…
}27:Mimimize casting
- 做法A:1个构造函数 +1 个析构函数 +n 个赋值操作
- 做法B:n个构造函数 +n 个析构函数
- 如果classes的一个赋值成本低于一组构造+析构成本,做法A比较高效,否则B比较高效。除非(1)知道赋值成本比“构造+析构”成本低,(2)正在处理代码中效率高度敏感的部分,否则用做法B。
- 尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。
C风格的转型动作:
(T) expression
函数风格的转型动作:
T(expression)C++四种新式转型:
const_cast
( expression )
将对象的常量属性移除。它也是唯一由此功能的C+±style转型操作符。dynamic_cast
( expression )
将父类转换为子类
用于“安全向下转型”,用于决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。reinterpret_cast
( expression )
意图执行低级转换,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。这一类转型在低级代码以外很少见。static_cast
( expression )
用来强迫隐式转换implicit conversions,例如将non-const对象转为const对象,或将int转为double等。它也可用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const——这个只有const_cast才能做。class Widget{ public: explicit Widget(int size); ... }; void doSomeWork(const Widget& w); doSomeWork(Widget(15)); //以一个int加上“函数风格”的转型动作创建一个Widget doSomeWork(static_cast(15)); //以一个int加上“C++风格”的转型动作创建一个Widget int x, y; ... double d = static_cast (x) / y; //x除以y,使用浮点数除法 建立一个base class指针指向一个derived class对象,但有时候上述两个指针值并不相同。这种情况下会有个偏移量(offset)在运行期被施行于Derived指针上,用以取得正确的Base指针值
class Base { ... }; class Derived: public Base { ... }; Derived d; Base* pb = &d; //隐喻的将Derived*转换为Base*
- 如果可以,尽量避免类型转换,尤其是在注重效率的代码中避免dynamic_casts。如果有个设计需要类型转换,尝试无需类型转换的代替设计。
- 如果转换时必要的,尝试将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
- 宁可使用C+±style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职称。



