Open v4if opened 7 years ago
被声明为explicit的构造函数,禁止编译器执行非预期的类型转换,除非有一个好的理由允许构造函数被用于隐式类型转换,否则一般声明为explicit
copy构造函数用来以同类型对象初始化自我对象,copy assignment操作符用来从另一个对象中拷贝其值到自我对象
以同类型对象初始化自我对象
从另一个对象中拷贝其值到自我对象
Pass-by-value意味着调用copy构造函数,Pass-by-reference-to-const往往是比较好的选择
Pass-by-value
Pass-by-reference-to-const
lhs和rhs,分别代表left-hand side左手端和right-hand side右手端
left-hand side
right-hand side
通常将指向一个T型对象的指针命名为pt,意思是pointer to T
指向一个T型对象
pointer to T
const double PI = 3.14;
在class内旧式编译器不支持static const int Magic = 0x5;static成员在其声明式上获得初值 enum hack补偿做法:一个属于枚举类型的数值可权充int被使用enum {Magic = 0x5};
static const int Magic = 0x5;
enum {Magic = 0x5};
temlate inline 函数可以代替宏
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) // 可预料行为和类型安全检查 template<typename T> inline void CallWithMax(const T& a, const T& b) { f(a > b ? a : b); }
有了const、enum和inline,对预处理器(特别是#define)的需求降低了,但并非完全消除。#include仍然是必需品,而#ifdef/#ifndef仍然是控制编译的重要角色
不该被改动
const出现在左边,表示被指物是常量;如果出现在右边,表示指针自身是常量
令函数返回一个常量值const T opertor*(const T& lhs, const T& rhs);
const T opertor*(const T& lhs, const T& rhs);
const成员函数 两个成员函数如果只是常量性不同,可以被重载
void print(const TextBlock& ctb) { std::cout << ctb[0]; // 调用const char& operator[](std::size_t position) const }
mutable可以释放掉non-static成员变量的const成员函数约束
// 令non-const operator[]调用其const —— 需要转型操作 // 反向做法,令const版本调用non-const版本是不应该做的事 // 因为const成员函数承诺绝不改变其内部对象,non-const却没有 char& operator[](std::size_t position) { return const_cast<char&>( static_cast<const T&>(*this)[position] ); }
读取未初始化的值会导致不明确的行为,更可能的情况是读入一些半随机bits
半随机
C++规定,对象的成员变量的初始化动作发生在进入构造函数之前,因此在构造函数内的操作都是赋值,而非初始化,初始化的发生时间更早 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作
对于成员初始化列表,C++有着十分固定的成员初始化次序,base classes更早于derived classes被初始化,而class的成员变量总是以其声明次序被初始化
成员初始化次序
copy构造函数、copy assignment操作符、析构函数、default构造函数,唯有当这些函数被需要(被调用),它们才会被编译器创建出来 所有这些函数都是public且inline的
如果在一个内含reference成员的class内支持赋值操作(assignment),必须自己定义copy assignment操作符,C++并不允许让reference改指向不同的对象
内含reference成员
让reference改指向不同的对象
可以将copy构造函数或copy assignment操作符声明为privete 但这个做法并不绝对安全,因为member函数和friend函数还是可以调用private函数 因此可以只声明,而且故意不去实现它们,调用时会得到一个连接错误(linkage error)
T(const T&); T& operator=(const T&);
给base classes一个virtual析构函数,这个规则只适用于带多态性质的base classes身上,这种base classes的设计目的就是为了使用指向base的指针,用来通过base class接口处理derived class对象 某些classes的设计目的是作为base classes使用,但不是为了多态用途,因此它们不需要virtual析构函数
基类不添加虚析构函数会造成内存泄漏,局部销毁 给析构函数加上virtual之后,就等于告诉编译器应该调用的是哪个析构函数的信息了,这样一来,当你的派生对象经由基类指针删除时,就会非常顺利的调用到派生类的析构函数,而不是调用到基类析构函数。 虚函数的副作用是有的,类中只要有大于等于一个虚函数时就会生成虚表 vptr,这个虚表会占用内存空间,所以,我认为当你完全了解了这个机制后,可以自己度量这个性能得失,并且做好注释工作。不过,一点点的性能损失来换取绝对的安全,这是值得的。
许多人的心得是:只有当class内含至少一个virtual函数,才为它声明virtual析构函数
不要试图继承一个不带non-virtual析构函数的class
以下代码会造成内存泄露,只是局部销毁了Base的成分
// g++ -O -g -fsanitize=address virtual_base.cpp #include <vector> class Base { public: ~Base(){}; }; class Derived : public Base { private: std::vector<int> m_data; }; int main() { Base *obj = new Derived(); delete obj; return 0; }
抽象class
class AWOV { public: virtual ~AWOV() = 0; };
析构函数的运作方式是,最深层派生的那个class其析构函数最先被调用,然后是其每一个base class的析构函数被调用
抢先制不明确行为于死地
不明确行为
DBConn::~DBConn() { try {db.close();} catch (...) { // 记录下对close的调用失败 std::abort(); } }
或者吞掉异常不处理
自我赋值
潜在的自我赋值a[i] = a[j] *px = *py
a[i] = a[j]
*px = *py
// 不能处理new异常 Widget& Widget::operator=(const Widget& rhs) { if (this == &rhs) return *this; delete pb; pb = new Bitmap(*rhs.pb); return *this; } // 如果new Bitmap抛出异常,pb会保持原状 // 对原bitmap做了一份复件,删除原bitmap,指向新制造的bitmap Widget& Widget::operator=(const Widget& rhs) { Bitmap* pOrig = pb; pb = new Bitmap(*rhs.pb); delete pOrig; return *this; }
如果为class添加一个成员变量,必须同时修改copying函数 为派生类撰写copying函数,必须很小心的也复制其base class成分 Base::opreator=(rhs);
Base::opreator=(rhs);
常见的资源包括动态内存分配、文件描述器、互斥锁、数据库连接、网络socket
把资源放进对象内,依赖C++的析构函数自动调用机制确保资源被释放
析构函数自动调用机制
// not good // 工厂函数,返回动态分配对象 Investment* pInv = createInvestment(); ... delete pInv;
// 智能指针 std::auto_ptr pInv(createInvestment());
注意不要让多个auto_ptr同时指向同一对象 为了预防这个问题,auto_ptr有一个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权! auto_ptr的底层条件是:受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它 shared_ptr会持续追踪共有多少个对象指向某笔资源,并在无人指向它时自动删除该资源。但无法打破环状引用,比如两个其实已经没有被使用的对象彼此互指,因而好像还处在被使用的状态 auto_ptr和shared_ptr两者都在其析构函数内做delete,而不是delete[]动作,意味着不要在动态分配的array身上使用它们 vector和string几乎总是可以取代动态分配而得的数组 2. 管理对象运用析构函数确保资源被释放,当对象离开作用域 ### 在资源管理类中小心copy行为 资源取得时机便是初始化时机 RAII —— 资源获取即初始化,利用对象的声明周期进行底层的资源管理 可以采取的策略: 1. 禁止复制 2. 对底层资源祭出`引用计数法` ```c++ class Lock{ public: // 以某个mutex初始化shared_ptr,并以unlock函数为删除器 explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) { lock(mutexPtr.get()); } private: shared_ptr<Mutex> mutexPtr; };
shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,也就是返回智能指针内部的原始指针(的复件) 就像(几乎)所有的智能指针一样,shared_ptr和auto_ptr也重载了指针取值操作符(operator->和operator* )它们允许 隐式转换至底部原始指针
delete[] ptr;
当使用new时,有两件事情发生,一是内存被分配出来,二是针对此内存会有构造函数被调用 使用delete时,也有两件事情发生,针对此内存会有析构函数被调用,然后内存才被释放
delete最大的问题在于:即将被删除的内存之内究竟存有多少对象 单一对象的内存布局一般而言不同于数组的内存布局。更明确的说,数组的内存布局通常还包括数组大小的记录,以便delete知道需要调用多少次析构函数
数组大小
// 在单独语句内以智能指针存储new的对象 // 如果不这样,一旦异常抛出,有可能导致难以察觉的资源泄露 shared_ptr<Widget> pw(new Widget); processWidget(pw, priority());
对string使用length,对list使用size(告诉调用者目前容器内有多少对象)
Boost的shared_ptr是原始指针的两倍大,以动态分配内存作为簿记用途和删除之专属数据,以virtual形式调用删除器,并在多线程程序修改引用次数时蒙受线程同步化的额外开销
合法值
以pass-by-value的形式传递需要带来临时副本的拷贝构造和析构的额外开销 避免derived class对象切割问题
但是对于内置类型来说(如int),pass by value往往比pass by reference(通常意味着传递的是指针)的效率更高些 可以合理假设pass-by-value并不昂贵的唯一对象就是内置类型和STL的迭代器和函数对象
pass-by-value并不昂贵
const Rational& operator*(const Rational& lhs, const Rational& rhs) { // 更糟糕的写法,谁来为new出来的对象实施delete Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); return *result; } // 欲令诸如operator*这样的函数返回reference,只是浪费时间而已
从封装的角度看,其实只有两种访问权限:private(提供封装)和其他(不提供封装)
将所有便利函数放在多个头文件内但隶属于同一个命名空间,意味着对于客户来说可以轻松扩展这一组函数 因为class定义式对客户而言是不能扩展的
class Rational{...}; // 不包括operator* // non-member const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); } Rational oneHalf(1, 2); Rational result = 2 * oneHalf;
namespace WidgetStuff{ template<typename T> class Widget{...}; // 内含swap成员函数 template<typename T> void swap(Widget<T>& a, Widget<T>& b) { a.swap(b); } }
如果可以,尽量避免转型,尤其是dynamic_cast 许多程序员相信,转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型,这是错误的观念。 任何一个类型转换(包括通过转型操作而进行的显示转换,或通过编译器完成的隐式转换)往往真的另编译器编译出运行期间执行的代码
class SpecialWindow: public Window { public: virtual void onResize() { Window::onResize(); // 而不是 static_cast<Window>(*this).onResize(); } };
异常安全
void PrettyMenu::changeBackground(std::iostream& imgSrc) { // 互斥器作为并发控制 lock(&mutex); delete bgImage; ++imageChanges; bgImage = new Image(imgSrc); unlock(&mutex); }
从异常安全性的观点看,这个函数很糟。
new Image(imgSrc)
void PrettyMenu::changeBackground(std::iostream& imgSrc) { // 使用对象管理资源 Lock ml(&mutex); Image* pIOrig = bgImage; bgImage = new Image(imgSrc); delete pIOrig; ++imageChanges; }
将bgImage也交给资源管理类
class PrettyMenu{ shared_ptr<Image> bgImage; }; void PrettyMenu::changeBackground(std::iostream& imgSrc) { // 使用对象管理资源 Lock ml(&mutex); bgImage.reset(new Image(imgSrc)); ++imageChanges; }
还有一个一般化的策略:copy and swap。为你打算修改的对象做出一份副本,然后在副本身上做一切必要修改,若有任何修改动作抛出异常,原对象保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)
免除函数调用成本,inline只是对编译器的一个申请,不是强制命令
所有对virtual函数的调用也都会使inline落空,因为virtual意味着等待,直到运行期才确定调用哪个函数,而inline意味着执行前,先将调用动作替换为被调用函数的本体 如果编译器无法将你要求的函数inline化,会给你一个警告信息
等待,直到运行期才确定调用哪个函数
适用于base class身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也是一个base class对象
class Derived: public Base { public: using Base::mf1; // 让Base class内 名为mf1的所有东西可见 };
using 声明式被放在derived class的public区域:base class内的public名称在public derived class内也应该是public using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见
non-virtual函数都是静态绑定的
Shape* ps; Shape* pc = new Circle; Shape* pr = new Rectangle;
ps、pc、pr都被声明为pointer-to-shape类型,所有它们的静态类型都是Shape* 所谓的对象的动态类型是指目前所指对象的类型,也就是说,动态类型可以表现出一个对象将会有什么行为 virtual函数是动态绑定,而缺省参数值却是静态绑定
pointer-to-shape
目前所指对象的类型
virtual函数的缺省参数值来自base class而非derived class virtual函数,唯一应该覆写的东西就是动态绑定
public继承意味着,如果D是一种B,对B为真的每一件事情对D也都应该为真
C++用来解析(resolving)重载函数调用的规则是:首先确定这个函数是否是最佳匹配,然后找出最佳匹配函数后才检验其可取用性
template<typename T> void doProcessing(T& w) { if (w.size() > 10 && w != someNastyWidget) { T temp(w); temp.normalize(); temp.swap(w); } }
以不同的template参数具现化function template
哪一个重载函数该被调用(发生在编译期)
哪一个virtual函数该被绑定(发生在运行期)
typename只被用来检验嵌套从属类型名称,其他名称不该有它存在
template<typename C> // 允许使用typename或class void f(const C& container, // 不允许使用typename typename C::iterator iter); // 一定要使用typename
typename不可出现在base class list内的嵌套从属类型名称之前,也不可在成员初始化列表中作为base class修饰符
template<> class MsgSender<CompanyZ> { public: void sendSecret(const MsgInfo& info){} }
class定义式最前头的template<>语法象征这既不是template也不是标准class,而是个特化版的MsgSender template,在template实参是CompanyZ时被使用
template<>
唯有non-member函数才有能力在所有实参身上实施隐式类型转换
在所有实参身上实施隐式类型转换
new出来的内存前后安排signiture签名 C++要求所有的operator new返回的指针都有适当的对齐(取决于数据类型)
导读
被声明为explicit的构造函数,禁止编译器执行非预期的类型转换,除非有一个好的理由允许构造函数被用于隐式类型转换,否则一般声明为explicit
copy构造函数用来
以同类型对象初始化自我对象
,copy assignment操作符用来从另一个对象中拷贝其值到自我对象
Pass-by-value
意味着调用copy构造函数,Pass-by-reference-to-const
往往是比较好的选择命名习惯
lhs和rhs,分别代表
left-hand side
左手端和right-hand side
右手端通常将
指向一个T型对象
的指针命名为pt,意思是pointer to T
让自己习惯C++
尽量以const、enum、inline替换#define
const double PI = 3.14;
在class内旧式编译器不支持
static const int Magic = 0x5;
static成员在其声明式上获得初值 enum hack补偿做法:一个属于枚举类型的数值可权充int被使用enum {Magic = 0x5};
temlate inline 函数可以代替宏
有了const、enum和inline,对预处理器(特别是#define)的需求降低了,但并非完全消除。#include仍然是必需品,而#ifdef/#ifndef仍然是控制编译的重要角色
尽可能使用const
不该被改动
const修饰常量
const出现在左边,表示被指物是常量;如果出现在右边,表示指针自身是常量
const面对函数声明时的应用
令函数返回一个常量值
const T opertor*(const T& lhs, const T& rhs);
const成员函数 两个成员函数如果只是常量性不同,可以被重载
mutable可以释放掉non-static成员变量的const成员函数约束
在const和non-const成员函数中避免代码重复
确定对象被使用前已先被初始化
读取未初始化的值会导致不明确的行为,更可能的情况是读入一些
半随机
bitsC++规定,对象的成员变量的初始化动作发生在进入构造函数之前,因此在构造函数内的操作都是赋值,而非初始化,初始化的发生时间更早 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作
对于成员初始化列表,C++有着十分固定的
成员初始化次序
,base classes更早于derived classes被初始化,而class的成员变量总是以其声明次序被初始化构造/析构/赋值运算
了解C++默默编写的函数
copy构造函数、copy assignment操作符、析构函数、default构造函数,唯有当这些函数被需要(被调用),它们才会被编译器创建出来 所有这些函数都是public且inline的
如果在一个
内含reference成员
的class内支持赋值操作(assignment),必须自己定义copy assignment操作符,C++并不允许让reference改指向不同的对象
若不想使用编译器自动生成的函数,就该明确拒绝
可以将copy构造函数或copy assignment操作符声明为privete 但这个做法并不绝对安全,因为member函数和friend函数还是可以调用private函数 因此可以只声明,而且故意不去实现它们,调用时会得到一个连接错误(linkage error)
为多态基类声明virtual析构函数
给base classes一个virtual析构函数,这个规则只适用于带多态性质的base classes身上,这种base classes的设计目的就是为了使用指向base的指针,用来通过base class接口处理derived class对象 某些classes的设计目的是作为base classes使用,但不是为了多态用途,因此它们不需要virtual析构函数
基类不添加虚析构函数会造成内存泄漏,局部销毁 给析构函数加上virtual之后,就等于告诉编译器应该调用的是哪个析构函数的信息了,这样一来,当你的派生对象经由基类指针删除时,就会非常顺利的调用到派生类的析构函数,而不是调用到基类析构函数。 虚函数的副作用是有的,类中只要有大于等于一个虚函数时就会生成虚表 vptr,这个虚表会占用内存空间,所以,我认为当你完全了解了这个机制后,可以自己度量这个性能得失,并且做好注释工作。不过,一点点的性能损失来换取绝对的安全,这是值得的。
许多人的心得是:只有当class内含至少一个virtual函数,才为它声明virtual析构函数
不要试图继承一个不带non-virtual析构函数的class
以下代码会造成内存泄露,只是局部销毁了Base的成分
抽象class
析构函数的运作方式是,最深层派生的那个class其析构函数最先被调用,然后是其每一个base class的析构函数被调用
别让异常逃离析构函数
抢先制
不明确行为
于死地或者吞掉异常不处理
绝不在构造和析构过程中调用virtual函数
令operator=返回一个reference to *this
在operator=中处理
自我赋值
潜在的自我赋值
a[i] = a[j]
*px = *py
复制对象时勿忘其每一个成分
如果为class添加一个成员变量,必须同时修改copying函数 为派生类撰写copying函数,必须很小心的也复制其base class成分
Base::opreator=(rhs);
资源管理
常见的资源包括动态内存分配、文件描述器、互斥锁、数据库连接、网络socket
以对象管理资源
把资源放进对象内,依赖C++的
析构函数自动调用机制
确保资源被释放// 智能指针 std::auto_ptr pInv(createInvestment());
在资源管理类中提供对原始资源的访问
shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,也就是返回智能指针内部的原始指针(的复件) 就像(几乎)所有的智能指针一样,shared_ptr和auto_ptr也重载了指针取值操作符(operator->和operator* )它们允许 隐式转换至底部原始指针
成对使用new和delete时要采取相同的形式
delete[] ptr;
当使用new时,有两件事情发生,一是内存被分配出来,二是针对此内存会有构造函数被调用 使用delete时,也有两件事情发生,针对此内存会有析构函数被调用,然后内存才被释放
delete最大的问题在于:即将被删除的内存之内究竟存有多少对象 单一对象的内存布局一般而言不同于数组的内存布局。更明确的说,数组的内存布局通常还包括
数组大小
的记录,以便delete知道需要调用多少次析构函数以独立语句将new的对象置入智能指针
设计与声明
让接口容易被正确使用,不易被误用
对string使用length,对list使用size(告诉调用者目前容器内有多少对象)
Boost的shared_ptr是原始指针的两倍大,以动态分配内存作为簿记用途和删除之专属数据,以virtual形式调用删除器,并在多线程程序修改引用次数时蒙受线程同步化的额外开销
设计class犹如设计type
合法值
:构造、赋值操作符、setter函数必须进行有效的错误检查宁以pass-by-reference-to-const替换pass-by-value
以pass-by-value的形式传递需要带来临时副本的拷贝构造和析构的额外开销 避免derived class对象切割问题
但是对于内置类型来说(如int),pass by value往往比pass by reference(通常意味着传递的是指针)的效率更高些 可以合理假设
pass-by-value并不昂贵
的唯一对象就是内置类型和STL的迭代器和函数对象必须返回对象时,别妄想返回其reference
将成员变量声明为private
从封装的角度看,其实只有两种访问权限:private(提供封装)和其他(不提供封装)
宁以non-member、non-friend替换member函数
将所有便利函数放在多个头文件内但隶属于同一个命名空间,意味着对于客户来说可以轻松扩展这一组函数 因为class定义式对客户而言是不能扩展的
若所有参数皆需类型转换,请为此采用non-member函数
考虑写出一个不抛异常的swap函数
实现
尽可能延后变量定义式的出现时间
尽量少做转型动作
如果可以,尽量避免转型,尤其是dynamic_cast 许多程序员相信,转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型,这是错误的观念。 任何一个类型转换(包括通过转型操作而进行的显示转换,或通过编译器完成的隐式转换)往往真的另编译器编译出运行期间执行的代码
为
异常安全
而努力是值得的从异常安全性的观点看,这个函数很糟。
new Image(imgSrc)
导致异常,对unlock的调用就绝不会执行,于是互斥器就永远被锁住了new Image(imgSrc)
抛出异常,bgImage就是指向一个已被删除的对象,imageChanges也已被累加将bgImage也交给资源管理类
还有一个一般化的策略:copy and swap。为你打算修改的对象做出一份副本,然后在副本身上做一切必要修改,若有任何修改动作抛出异常,原对象保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)
透彻了解inline的里里外外
免除函数调用成本,inline只是对编译器的一个申请,不是强制命令
所有对virtual函数的调用也都会使inline落空,因为virtual意味着
等待,直到运行期才确定调用哪个函数
,而inline意味着执行前,先将调用动作替换为被调用函数的本体 如果编译器无法将你要求的函数inline化,会给你一个警告信息继承与面向对象设计
确定你的public继承塑模出is-a关系
适用于base class身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也是一个base class对象
避免遮掩继承而来的名称
using 声明式被放在derived class的public区域:base class内的public名称在public derived class内也应该是public using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见
区分接口继承和实现继承
绝不重新定义继承而来的non-virtual函数
non-virtual函数都是静态绑定的
绝不重新定义继承而来的缺省参数值
ps、pc、pr都被声明为
pointer-to-shape
类型,所有它们的静态类型都是Shape* 所谓的对象的动态类型是指目前所指对象的类型
,也就是说,动态类型可以表现出一个对象将会有什么行为 virtual函数是动态绑定,而缺省参数值却是静态绑定virtual函数的缺省参数值来自base class而非derived class virtual函数,唯一应该覆写的东西就是动态绑定
通过复合塑模出has-a
public继承意味着,如果D是一种B,对B为真的每一件事情对D也都应该为真
C++用来解析(resolving)重载函数调用的规则是:首先确定这个函数是否是最佳匹配,然后找出最佳匹配函数后才检验其可取用性
模板与泛型编程
了解隐式接口和编译器多态
以不同的template参数具现化function template
会导致调用不同的函数,这便是所谓的编译器多态 类似于哪一个重载函数该被调用(发生在编译期)
和哪一个virtual函数该被绑定(发生在运行期)
了解typename的双重意义
typename只被用来检验嵌套从属类型名称,其他名称不该有它存在
typename不可出现在base class list内的嵌套从属类型名称之前,也不可在成员初始化列表中作为base class修饰符
class定义式最前头的
template<>
语法象征这既不是template也不是标准class,而是个特化版的MsgSender template,在template实参是CompanyZ时被使用需要类型转换时请为模板定义非成员函数
唯有non-member函数才有能力
在所有实参身上实施隐式类型转换
定制new和delete
new出来的内存前后安排signiture签名 C++要求所有的operator new返回的指针都有适当的对齐(取决于数据类型)