尽量用const,emum,inline替换#define
换句话说,“宁可用编译器替换预处理器”,因为或许#define不被视为语言的一部分。我们知道,编译器必须在编译期间知道数组的大小,所以大小可以通过static const int
变量或者enum
指定。如:
class GamePlayer {
private:
static const int NumTurns = 5;
int scores[NumTurns];
...
}
或者
class GamePlayer {
private:
emun { NumTurns = 5};
int scores[NumTurns];
...
}
有时候会用宏定义调用函数,如:
选择a和b的较大值传给函数f:
#define CALL_WITH_MAX(a,b) f((a)>(b) ? (a) : (b))
但这种方式是很容易出错的,更好的方式是使用template inline函数:
template<typename T>
inline void callWithMax(const T& a,const T&b)
{
f(a>b?a:b);
}
记住
对于单纯的常量,最好以const对象或enums替换#define。
对于类似函数的宏,最好该用inline函数替换#define。
4种类型转换
static_cast
用法:static_cast <typeid> (expression)
说明:该运算符把expression转换为typeid类型,但没有运行时类型检查来确保转换的安全性。
用途:
编译时进行类型转换。
a) 用于类层次结构中基类和派生类之间指针或者引用的转换。
up-casting (把派生类的指针或引用转换成基类的指针或者引用表示)是安全的;
down-casting(把基类指针或引用转换成子类的指针或者引用)是不安全的。
b) 用于基本数据类型之间的转换,如把int转换成char,这种转换的安全性也要由开发人员来保证。
c) 可以把空指针转换成目标类型的空指针(null pointer)。
d) 把任何类型的表达式转换成void类型。
注意: static_cast不能转换掉expression的const、volitale或者__unaligned属性。
const_cast
用法:const_cast<typeid>(expression)
说明:这个类型操纵传递对象的const属性,或者是设置或者是移除。如:
Class C{…}
const C* a = new C;
C* b = const_cast<C*>(a);
dynamic_cast
用法:dynamic_cast <typeid> (expression)
该运算符把exdivssion转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;
如果type-id是类指针类型,那么exdivssion也必须是一个指针,如果type-id是一个引用,那么exdivssion也必须是一个引用。
运行时类型转换。
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
up-casting (把派生类的指针或引用转换成基类的指针或者引用表示)时,dynamic_cast和static_cast的效果是一样的;
down-casting(把基类指针或引用转换成子类的指针或者引用)时,dynamic_cast具有类型检查的功能,比static_cast更安全。
另外要注意:进行dynamic_cast时,基类要有虚函数,否则会编译出错;static_cast则没有这个限制。
这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数。
reinterpret_cast
用法:reinterpret_cast <typeid>(expression)
说明:转换一个指针为其他类型的指针,也允许将一个指针转换为整数类型,反之亦然。这个操作符能够在非相关的类型之间进行转换。操作结果只是简单的从一个指针到别的指针的值的二进制拷贝,在类型之间指向的内容不做任何类型的检查和转换。这是一个强制转换。使用时有很大的风险,慎用之。
注意:reinterpret _cast不能转换掉expression的const、volitale或者__unaligned属性。
尽可能使用const
const成员函数
可以处理const对象。
如果两个成员函数只是常量性(constness)不同,可以被重载。
mutable关键字
mutalbe的中文意思是“可变的,易变的”,跟const是反义词。
在C++中,mutable可以突破const的限制。被mutable修饰的变量,将永远处于可变的状态,在被const修饰的函数里面也能被修改。
在const和non-const成员函数中避免代码重复
方法是在non-const成员函数中调用const成员函数。
但反过来,如果用const成员函数调用non-const成员函数,则会存在风险,因为对象可能被non-const函数改变。
构造/析构/赋值运算
所有编译器产出的函数都是public。如果你想要阻止这些函数被创建出来,你可以将拷贝构造函数或赋值操作符=声明为private。
这样一来,藉由明确声明一个成员函数,你阻止了编译器暗自创建;而通过令这些函数为private,使你得以成功阻止人们调用它。
虚函数的实现
为了实现出虚函数,对象必须携带某些信息,主要用来在运行期决定哪一个虚函数该被调用。这份信息通常是由一个所谓vptr指针指出。
vptr指向一个由函数指针构成的数组,称为虚函数表。
纯虚函数会导致一个抽象类,也就是不能被实体化的类。
成员初始化列表
在初始化类的成员的时候,我们经常会有两种选择,其一是类构造函数的成员初始化列表,其二是构造函数的函数体。那么这两者的区别又是什么,成员初始化列表的具体行为到底是什么呢?
成员初始化列表和构造函数体的区别
成员初始化列表和构造函数的函数体都可以为我们的类数据成员指定一些初值,但是两者在给成员指定初值的方式上是不同的。成员初始化列表使用初始化的方式来为数据成员指定初值,而构造函数的函数体是通过赋值的方式来给数据成员指定初值。也就是说,成员初始化列表是在数据成员定义的同时赋初值,但是构造函的函数体是采用先定义后赋值的方式来做。这样的区别就造成了,在有些场景下,是必须要使用成员初始化列表的。
例如:
- 初始化一个引用成员变量
- 初始化一个const变量
- 当我们在初始化一个子类对象的时候,而这个子类对象的父类有一个显示的带有参数的构造函数
- 当调用一个类类型成员的构造函数,而它拥有一组参数的时候
另外性能上,初始化列表也比函数体要开销小。
对于类的赋值,函数体使用的是拷贝赋值运算符来给数据成员赋初值,平白无故多了很多步骤和花销。
而初始化列表是使用类的拷贝构造函数来赋初值。
成员初始化列表的行为
成员初始化列表是可以初始化类的数据成员,那么他是如何操作的呢?是通过一系列的函数调用么,不是的。
成员初始化列表是按照数据成员的声明顺序,将初始化操作安排在构造函数所有usercode的前面。
尽量不要用次序较后的成员来初始化次序较前的成员,这样就会出问题,这也是成员初始化列表的一个弊端。
RAII惯用法
注意到,智能指针的实现就是一种典型的RAII的应用。
C++中的RAII全称是“Resource acquisition is initialization”,直译为“资源获取就是初始化”。RAII利用构造函数和析构函数定义一个类来完成对资源的分配和释放,提供了一种资源自动管理的方式,当产生异常、回滚等现象时,RAII可以正确地释放掉资源。
举个常见的例子:
void Func()
{
FILE *fp;
char* filename = "test.txt";
if((fp=fopen(filename,"r"))==NULL)
{
printf("not open");
exit(0);
}
...
// 如果 在使用fp指针时产生异常 并退出
// 那么 fp文件就没有正常关闭
fclose(fp);
}
在资源的获取到释放之间,我们往往需要使用资源,但常常一些不可预计的异常是在使用过程中产生,就会使资源的释放环节没有得到执行。此时,就可以让RAII惯用法大显身手了。
RAII的实现原理很简单,利用stack上的临时对象生命期是程序自动管理的这一特点,将我们的资源释放操作封装在一个临时对象中。
class Resource{};
class RAII{
public:
RAII(Resource* aResource):r_(aResource){} //获取资源
~RAII() {delete r_;} //释放资源
Resource* get() {return r_ ;} //访问资源
private:
Resource* r_;
};
比如文件操作的例子:
class FileRAII{
public:
FileRAII(FILE* aFile):file_(aFile){}
~FileRAII() { fclose(file_); }//在析构函数中进行文件关闭
FILE* get() {return file_;}
private:
FILE* file_;
};
void Func()
{
FILE *fp;
char* filename = "test.txt";
if((fp=fopen(filename,"r"))==NULL)
{
printf("not open");
exit(0);
}
FileRAII fileRAII(fp);
...
// 如果 在使用fp指针时产生异常 并退出
// 那么 fileRAII在栈展开过程中会被自动释放,析构函数也就会自动地将fp关闭
// 即使所有代码是都正确执行了,也无需手动释放fp,fileRAII它的生命期在此结束时,它的析构函数会自动执行!
}
RAII免除了对需要谨慎使用资源时而产生的大量维护代码。在保证资源正确处理的情况下,还使得代码的可读性也提高了不少。
scope lock (局部锁技术)
RAII可以控制的资源不局限于内存,也可以是文件句柄、网络连接、互斥量等。
在很多时候,为了实现多线程之间的数据同步,我们会使用到 mutex,critical section,event,singal 等技术。但在使用过程中,由于各种原因,有时候,我们会遇到一个问题:由于忘记释放(Unlock)锁,产生死锁现象。
采用RAII 就可以很好的解决这个问题,使用着不必担心释放锁的问题. 示例代码如下:
My_scope_lock 为实现 局部锁的模板类.
LockType 抽象代表具体的锁类 .如基于 mutex 实现 mutex_lock 类。
template<class LockType>
class My_scope_lock
{
public:
My_scope_lock(LockType& _lock):m_lock(_lock)
{
m_lock.occupy();
}
~My_scope_lock()
{
m_lock.relase();
}
protected:
LockType m_lock;
}
使用的时候:
//全局变量
int counter = 0;
void routine();
mutex_lock m_mutex_lock;
void routine()
{
My_scope_lock l_lock(m_mutex_lock);
counter++;
...
//others...
}
typename和traits技法
typename的用法
typename用在模板定义里,标明其后的模板参数是类型参数。
例如:
template<typename T, typename Y>
T foo(const T& t, const Y& y){//....};
templace<typename T>
class CTest
{
private:
T t;
public:
//...
}
其实这里可以使用关键字class,而且其功能和typename完全相同。这里的class和定义类时的class完全是两回事,C++当时就是为了减少关键字,才使用了class。但最终却不得不引入了typename,究竟是什么原因呢?请看第二条,也就是typename的第二个用法:在模板中标明“内嵌依赖类型名”
template <class _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;
};
内嵌是指它是定义在类名的定义中的。以上difference_type和value_type都是定义在iterator_traits中的。
依赖是指依赖于一个模板参数。typename iterator_traits<_InputIter>::difference_type中difference_type依赖于模板参数_InputIter.
类型名是指这里最终要指出的是个类型名,而不是变量。例如iterator_traits<_InputIter>::difference_type完全有可能是类iterator_traits<_InputIter>
类里的一个static对象。而且当我们这样写的时候,C++默认就是解释为一个变量的。所以,为了和变量区分,必须使用typename告诉编译器。
traits
在 C++ 中,traits 习惯上总是被实现为 struct ,但它们往往被称为 traits classes。Traits classes 的作用主要是用来为使用者提供类型信息。
traits 使用的关键技术是:模板的特化与偏特化。
特化
我们先来看下一函数模板的通用定义:
template<typename T>
struct my_is_void {
static const bool value = false;
};
然后,针对 void 类型,我们有以下的特化版本:
template<>
struct my_is_void<void> {
static const bool value = true;
};
my_is_void<bool> t1;
cout << t1.value << endl; // 输出0
my_is_void<void> t2;
cout << t2.value << endl; // 输出1
当声明 my_is_void<void> t2;
时,使用的是特化版本,故其 value 值为 1。
偏特化
模板特化时,可以只指定一部分而非所有模板参数,或者是参数的一部分而非全部特性,这叫做模板的偏特化。一个类模板的偏特化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。
我们以另一个例子来说明模板的偏特化。先来看通用的原始模板。
template<typename T>
struct my_is_pointer {
static const bool value = false;
};
我们对模板参数T进行限制,要求其为一个指针的类型:
template<typename T>
struct my_is_pointer<T*> {
static const bool value = true;
};
my_is_pointer<int> p1;
cout << p1.value << endl; // 输出 0,使用原始模板
my_is_pointer<int*> p2;
cout << p2.value << endl; // 输出 1,使偏特化模板,因为指定了 int * 类型的参数
实现 Traits Classes
我们知道,在 STL 中,容器与算法是分开的,彼此独立设计,容器与算法之间通过迭代器联系在一起。那么,算法是如何从迭代器类中萃取出容器元素的类型的?没错,这正是我们要说的 traits classes 的功能。
迭代器所指对象的类型,称为该迭代器的 value_type。我们来简单模拟一个迭代器 traits classes 的实现。
template<class IterT>
struct my_iterator_traits {
typedef typename IterT::value_type value_type;
};
my_iterator_traits 其实就是个类模板,其中包含一个类型的声明。有上面 typename 的基础,相信大家不难理解 typedef typename IterT::value_type value_type;
的含义:将迭代器的value_type 通过 typedef 为 value_type。
对于my_iterator_traits,我们再声明一个偏特化版本。
template<class IterT>
struct my_iterator_traits<IterT*> {
typedef IterT value_type;
};
即如果 my_iterator_traits 的实参为指针类型时,直接使用指针所指元素类型作为 value_type。
接下来,考虑这样的写法:
my_iterator_traits<vector<int>::iterator>::value_type a;
我们来解释 这面这句的含义。
vector<int>::iterator
为vector<int>
的迭代器,该迭代器包含了 value_type 的声明,由 vector 的代码可以知道该迭代器的value_type 即为 int 类型。
接着,my_iterator_traits<vector<int>::iterator>
会采用 my_iterator_traits
的通用版本,也就是使用 typename IterT::value_type
这一类型声明,这里 IterT 为 vector<int>::iterator
,故整个语句萃取出来的类型为 int 类型。
而如果是 my_iterator_traits<char*>::value_type
则使用 my_iterator_traits 的偏特化版本,直接返回 char 类型。