3. const
-
成员函数后面的const也会导致重载
-
如果const和非const成员函数代码重复,可以在const函数中实现一次,然后在非const函数中通过cast调用const版本的函数
char & operator[] (int i) { return const_cast<char&>( static_cast<const tb&>(*this) [i] ); } const char & operator[] (int i) const { return txt[i]; }
4. 初始化
- 类成员初始化顺序总是以声明顺序初始化
- 定义于不同编译单元中的non-local static对象初始化顺序不确定,可以通过将此类对象放到函数中,让函数返回对象的引用供用户访问该对象(C++单例模式实现方法)
5. 编译器自动生成的函数
如果类中含有应用、const值或者基类赋值函数为私有,则不会自动生成赋值函数
8. 析构函数中的异常
如果在析构函数中抛出异常,会导致内存泄漏,应该提供一个非析构函数用来执行会抛出异常的操作,或者让析构函数吞下异常而不抛出(abort或者自己catch)
13 用对象管理资源
类似于智能指针
shared_ptr和unique_ptr如果用来管理动态分配的数组,需要将模版类型参数写成T []
, 或者设置自定义删除器:
shared_ptr<A> p(new A[5], default_delete<A[]>());
unique_ptr<A, decltype(default_delete<A[]>())> q(new A[5], default_delete<A[]>());
14 资源管理类中要小心coping行为
解决方案:
-
禁止复制,将copy声明为delete或者放到
-
使用引用计数,由最后一个释放资源,借助shared_ptr可以很简单实现
class Lock { private: shared_ptr<mutex> ptr; public: explicit Lock(mutex * m) :ptr(m, [](mutex * m){m->unlock();}) { m->lock(); } };
-
进行深拷贝
-
copy时转移资源所有权
17 以独立语句将动态分配的对象放入智能指针
f(shared_ptr<int>(new int()), g());
上面这条语句可能会导致内存泄漏,在调用f之前,有三个步骤需要执行:
- new int()
- shared_ptr()
- g()
三个步骤执行顺序由编译器决定,如果先执行new,再执行g(),此时g()抛出异常,步骤1中动态分配的内存就遗失了。
正确做法是用独立语句将动态分配内存的对象放入智能指针:
shared_ptr<int> ptr(new int());
f(ptr, g());
23 使用non-member, non-friend函数代替member函数
将一个函数作为member函数会导致更多代码能访问类中的private成员,导致封装破坏,当改变类中某些实现时,需要修改更多代码,而使用non-member, non-friend函数就不会导致这样的问题。
可以设置一个工具类,将函数作为其中的static函数,也可以将该函数和类放到同一个命名空间中。
24 如果所有参数都需要类型转换,使用non-member函数
例如对于实数类Rational,可以由整型隐式转换,那么对于乘法运算符,如果将其定义为member函数
const Rational operator * (const Rational & rhs) const;
他能够处理rational*10
, 而对于10*rational
这样的使用方式就会编译错误
如果使用non-member函数
const Rational operator * (const Rational & lhs, const Rational & rhs);
则能够处理两种调用方法。
27 尽量少做转型动作
dynamic_cast效率比较低,如果需要在派生类中调用基类函数,可以使用Base::f()
无法将派生类指针或引用使用dynamic_cast转换为protected或private继承来的基类
将派生类指针强制转换为基类指针,他们的位置可能会不一样,例如对于多重继承:
class Base1 {char x;};
class Base2 {double y;};
class Derive : public Base1, public Base2 {};
Derive d;
cout << &d << " " << dynamic_cast<Base1*>(&d) << " " << dynamic_cast<Base2*>(&d) << endl;
30 inline
定义在类声明里的函数默认inline
inline函数通常一定头文件或者同一个cpp中
inline导致程序膨胀,占用内存增多,可能还会导致虚拟内存换页行为致使运行效率降低
inline只是一个建议,如果成功inline,编译器依旧可能会生成一个函数本体,因为可能有代码会要求取得函数地址指针
inline函数无法随着程序库一起升级,修改了inline函数,必须重新编译其它调用了inline函数的模块。
33 派生中的重载(overload),覆盖(override),隐藏(hide)
重载(overload),覆盖(override),隐藏(hide)
可以使用using或者转交函数让被隐藏的基类函数可用
在派生中的override函数后面加override关键字,防止参数错误导致hide
35 virtual的替代方法
37 不要override继承而来的缺省参数值
虚函数是动态链接的,使用了虚函数表的方式实现。
虚函数的缺省参数值却是静态链接的,也就是说如果使用指向派生类的基类指针调用虚函数,会调用派生类的虚函数但却会使用基类虚函数中的缺省参数值。因为如果要让缺省参数也表现的和虚函数一样,那就需要为参数也定义一个类似虚函数表的结构,相对于虚函数表每个表项只是一个函数指针,“虚缺省参数表 ”却可能很大,为了效率C++选择了让缺省参数采用静态链接。
42 typename
使用typename表明其后的嵌套从属名称是一个类型而不是变量
template <typename T>
void f(T a)
{
typename T::value_type x = a;
}
基类列表和构造函数初始化列表中不需要也不能使用typename
同时使用typename和typedef
template <typename Ite>
typename iterator_traits<Ite>::value_type f(Ite a)
{
typedef typename iterator_traits<Ite>::value_type value_type;
value_type temp(*a);
return temp;
}
vector<string> s = {"abc", "def"};
cout << f(s.begin()) << endl;
43 处理模板化基类内的名称
当基类和派生类都是模版时,在派生类中直接调用基类的名称时,会报错,即使基类模版中存在这个名称。这是因为基类可能会被全特化成一个没有该名称的类。
例如对于下面代码:
class CompanyA {};
class CompanyB {};
class CompanyZ {};
class MsgInfo {};
template <typename Company>
class MsgSender
{
public:
void sendClear(const MsgInfo& info);
};
template<>
class MsgSender<CompanyZ> {};
template <typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
senderClear(info);
}
};
当模版参数为CompanyZ时,基类被全特化,全特化的基类没有senderClear。所以模版基类中含有该名称并不保证该模版基类实例化后一定有该名称,所以编译器不会进入模版基类寻找名称。
如果可以保证基类中含有该名称,可以用以下三种方法向编译器承诺:
-
使用
this->
:template <typename Company> class LoggingMsgSender: public MsgSender<Company> { public: void sendClearMsg(const MsgInfo& info) { this->senderClearMsg(info); } };
-
使用
using MsgSender<Company>::sendClear;
template <typename Company> class LoggingMsgSender: public MsgSender<Company> { public: using MsgSender<Company>::sendClear; void sendClearMsg(const MsgInfo& info) { sendClear(info); } };
-
使用
MsgSender<Company>::sendClear(info);
,但这种行为将名称限定在了基类中,对于虚函数相当于关闭了动态绑定template <typename Company> class LoggingMsgSender: public MsgSender<Company> { public: void sendClearMsg(const MsgInfo& info) { MsgSender<Company>::sendClear(info); } };
47 Type Traits
Traits编程方式可以获取用户自定义类型和内置类型的一些信息,对于用户自定义类,我们可以通过在类中内嵌信息(例如使用typedef或者一个变量等方式),而对于内置类型,我们无法在其中内嵌任何信息。traits编程方式使用一个模版类解决这个问题,对于用户自定义类型,模板类直接使用该类型的内嵌信息,而对于无法内嵌信息的内置类型,则通过为它定义一个特化版本的模版来达到和自定义类型一样的效果。
最典型的例子就是STL的iterator_traits:
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 Iterator>
struct iterator_traits
{
typedef typename Iterator::iterator_category iterator_category;
typedef typename Iterator::value_type value_type;
typedef typename Iterator::difference_type difference_type;
typedef typename Iterator::pointer pointer;
typedef typename Iterator::reference reference;
};
template <typename T>
struct iterator_traits<T*>
{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef T& reference;
};
template <typename T>
struct iterator_traits<const T*>
{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef const T* pointer;
typedef const T& reference;
};
就此我们可以通过iterator_traits无差别的获取内置指针和自定义迭代器的一些属性,并应用到advance函数(将迭代器向前或向后移动若干个位置)中:
// 获取迭代器类型,返回一个迭代器类型tag对象
template <typename Iterator>
inline typename iterator_traits<Iterator>::iterator_category
iterator_category(const Iterator &)
{
typedef typename iterator_traits<Iterator>::iterator_category category;
return category();
}
// 单向迭代器,通过++步进
template <typename InputIterator, typename Distance>
inline void __advance(InputIterator &i, Distance n, input_iterator_tag)
{
while(n--) ++i;
}
// 双向迭代器,通过++步进
template <typename BidirectionalIterator, typename Distance>
inline void __advance(BidirectionalIterator &i, Distance n, bidirectional_iterator_tag)
{
if(n>=0)
while(n--) ++i;
else
while(n++) --i;
}
// 随机迭代器,直接前进n步
template <typename RandomAccessIterator, typename Distance>
inline void __advance(RandomAccessIterator &i, Distance n, random_access_iterator_tag)
{
i += n;
}
// 上层函数,根据迭代器类型调用相应的下层函数
template <typename InputIterator, typename Distance>
inline void advance(InputIterator &i, Distance n, input_iterator_tag)
{
__advance(i, n, iterator_category(i));
}
STL中还有其它地方也是用了traits编程方式,例如判断trivial等等
杂项
构造函数参数与成员变量重名
构造函数参数和成员变量重名时,可以使用初始化列表:
class A
{
private:
int x;
public:
A(int x = 0) :x(x) {}
};
这样时正确的,因为对于x(x)
,左边的x只能从成员变量中寻找,而对于右边的x,编译器是优先使用局部变量的,也就是函数参数。
如果不使用初始化列表,则必须使用this->x = x
显式表明两边的x是哪一个x。
unordered_set/map
template < class Key, // unordered_set::key_type/value_type >
class Hash = hash<Key>, // unordered_set::hasher
class Pred = equal_to<Key>, // unordered_set::key_equal
class Alloc = allocator<Key> // unordered_set::allocator_type
> class unordered_set;
对于一些内置类型,unordered并没有提供hash函数,需要自己实现,例如:
struct pairHash
{
size_t operator ()(const pair<int, int> & p) const
{
return hash<int>()(p.first) ^ hash<int>()(p.first);
}
};
int main()
{
unordered_map<pair<int, int>, int, pairHash> mp;
return 0;
}
智能指针
定义一个智能指针时应该保证它是使用使用动态分配内存生成的指针,并且保证只有一个智能指针管理它
shared_ptr<V> pi(new V());
cout << pi.use_count() << endl;
auto pi1 = pi;
cout << pi.use_count() << endl;
shared_ptr<V> pi2(pi.get());
shared_ptr<V> pi3(pi.get());
cout << pi.use_count() << endl;
上面这个对象被析构了两次