Effective C++ 学习笔记
Dec 28, 2013
1. C++编程范型 #
Programming Paradigms,也就是编程的风格和模式
- Procedural-based,面向过程编程
- Object-based,基于对象
- Object-Oriented,面向对象编程
- Generics,范型编程
- Template MetaProgramming,TMP,模板元编程
一般认为是四种:面向过程OP(C风格),面向对象OO(使用类),范型编程GP(使用模板),模板元编程TMP(使用模板和递归,获得编译期间执行的代码)
2. 函数签名(signature) #
函数的参数和返回类型。函数在重载时利用函数签名区分不同函数。
int add(int a,int b)
{
return a+b;
}
add函数的签名为:
int (int,int)
3. Explicit关键字:避免隐式转换 #
如下代码
class A{
public:
A(int n){ cout<<"A:"<<n<<endl; }
};
int main(){
A a = 10;
return 0;
}
输出:A:10
这里就发生了隐式转换。将10转换(构造)为A类型,再赋值给a。
如果使用
explicit A(int n){ cout<<"A:"<<n<<endl; }
就不允许这种转换,编译器会报错。
原则:尽量使用explicit关键字修饰(含参)构造函数,避免意外转换
4. 拷贝构造函数 #
如果没有显示声明一个拷贝构造函数,系统会提供一个默认拷贝构造函数,实现位拷贝,即浅拷贝。
class A{
public:
int m_id;
A(int id):m_id(id){}
};
int main(){
A a1(1);
A a2 = a1; //拷贝构造
cout<<a2.m_id<<endl; //输出 1
return 0;
}
显示声明一个拷贝构造函数,可以手动编码实现深拷贝。
class A{
public:
int m_id;
A(int id):m_id(id){}
A(const A& b){m_id=b.m_id+1;}//拷贝构造函数
};
int main(){
A a1(1);
A a2 = a1; //调用自定义拷贝构造
cout<<a2.m_id<<endl; //输出 2
return 0;
}
5. pass-by-reference-to-const 替换 pass-by-value #
函数传递参数时,使用传递参数的const型引用(const T&)代替值传递(T),以提高效率。
pass-by-value
当以一个对象作为参数传递时,函数调用过程中,存在“一次COPY构造函数和一次析构函数”。
void Foo(MyClass a);
MyClass cls;
Foo(cls);
Foo函数调用开始时:调用cls对象的拷贝构造函数,初始化a对象
Foo函数调用结束时:调用a对象的析构函数
pass-by-reference-to-const
void Foo(const MyClass& a);
MyClass cls;
Foo(cls);
上述代码就没有了对象的COPY构造函数、析构函数的调用
原则:对于自定义数据类型,使用pass-by-reference-to-const 替代pass-by-value 对于内置数据类型,STL的迭代器和函数对象,pass-by-value更合适
6. Item2:使用const,enum,inline替换#define #
即以编译器替换预处理器。
预处理器将#define在预处理阶段符号替换,没有进入编译器的符号表,不利于后续调试查错。
7. Enum枚举 #
C和C++中的枚举大致相同,也有一些区别。C++中的枚举名可以直接作为类型,不需要加enum修饰。
C中
enum Week{Monday,Tuesday};
enum Week w1 = Monday;
printf("%d\n",w1);
C++
enum Week{Monday,Tuesday}; //Week可以直接作为类型
enum Week w1 = Monday;
Week w2 = Tuesday;
cout<<w1<<w2<<endl;
C++ 类内的枚举
class Shape{
public:
enum Color{Red,Green,Blue,Yellow};
Shape(Color color){}
};
int main(){
Shape s(Shape::Blue);//通过类名::枚举值
return 1;
}
8. 类成员变量初始化 #
区分赋值(Assignment)和初始化(Initialization)
赋值
class C{
public:
C(){ id = 1; }//给id赋值
~C(){};
private:
int id;
};
初始化
class C {
public:
//初始化,通过初始化成员列表 Member Initialization List
C():id(1){}
~C(){};
private:
int id;
};
C++规定,对象成员变量的初始化动作发生在进入构造函数本体(也就是{}内的代码)之前。
9. 单例模式 #
方法:构造函数私有化,static函数GetInstance返回static成员变量
(1). 通常实现方法
class Singleton{
private:
Singleton(){}//构造函数私有化
~Singleton(){}
public:
//静态成员函数返回对象
static Singleton* GetInstance(){
if(NULL==_instance){
_instance = new Singleton();
}
return _instance;
}
private:
static Singleton* _instance;
};
Singleton* Singleton::_instance=NULL;//静态成员变量初始化
这种方法的不足是需要手动释放内存(_instance需要手动delete)。
(2). 利用auto_ptr实现方法,借助智能指针自动释放内存
class Singleton{
private:
Singleton(){}
~Singleton(){}
public:
static Singleton* GetInstance(){
if(NULL==_instance.get()){
_instance.reset(new Singleton());
}
return _instance.get();
}
void Print(){cout<<"Hello World"<<endl;}
private:
friend class auto_ptr<Singleton>;//友元
static auto_ptr<Singleton> _instance;
};
auto_ptr<Singleton> Singleton::_instance;
10. 类的默认构造 #
空类
class Empty{}
相当于
class Empty{
public:
Empty(){}
Empty(const Empty& rhs){} //copy构造函数
~Empty(){}
Empty& operator =(const Empty& rhs) //copy assignment 操作符
}
用户没有声明构造函数\拷贝构造函数\重载copy assignment 操作符,编译器会自动生成默认的。
11. 析构函数的virtual #
如下代码:如果~A不声明为virtual,输出结果“~A()”;如果声明,则为“~B(),~A()”
class A{
A(){}
virtual ~A() {cout<<"~A()\n";}
};
class B: public A{
B(){}
~B() {cout<<"~B()\n";}
};
int main(){
A* p = new B;
delete p;
return 0;
}
类如果会被派生,则定义其析构函数为virtual,主要不是防止内存泄露,而是为了正确的析构。如果不会派生,则不需要定义为virtual的,因为使用虚函数会耗费资源的、降低效率。
原则:如果类含有一个virtual函数,则将其析构函数声明为virtual
12. 禁止编译器提供的默认copy构造函数和copy assignment操作符 #
如果我们希望禁止类的copy构造函数和copy assignment操作(如下例)
class Unique{};
Unique a;
Unique b(a); //copy 构造函数
a = b; //copy assignment操作符
则可将其声明为private并且只声明不实现。(这样系统就不会提供默认的copy 构造函数 和copy assignment操作符)
class Unique{
private:
Unique(const Unique&);
Unique operator =(const Unique&);
};
13. 虚函数、纯虚函数、抽象类、虚基类 #
(1). 虚函数
为了使基类指针能调用子类的成员函数
当基类函数声明为virtual,运行时当基类指针调用该虚函数时,会首先查看派生的子类中有没有实现该函数,有则调用子类的函数,无则调用基类的函数。
作用:实现运行时的多态性(动态绑定)
(2). 纯虚函数 pure virtual
要求在子类中必须实现的虚函数,它在基类中没有定义。
virtual 返回类型 函数名(参数表)=0;
(3). 抽象类 abstract class
至少含有一个纯虚函数的类,称为抽象类
(4). 虚基类
多继承时,出现重复继承现象,使用虚基类消除。如下例中D会有两个A类的副本
class A {int a;…};
class B: public A {…};
class C: public A {…};
class D: public B, public C {…};
把A定义为B、C的虚基类,D就只有一个A类的副本
class A {int a;…};
class B: virtual public A {…};
class C: virtual public A {…};
class D: public B, public C {…};
原则:不要继承一个带有 non-virtual 析构函数的类
14. RAII #
RAII,Resource Acquisition Is Initialization(资源获取即初始化)。详细资料
一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
不需要显式地释放资源。 采用这种方式,对象所需的资源在其生命期内始终保持有效。 C++中没有垃圾回收机制RAII为了(GC),RAII成为了弥补该机制的一种方法。
15. Item23:将成员变量声明为private #
可以对成员变量进行精确地访问控制;
提供封装性,方便后续升级代码
16. 使用shared_ptr代替 普通指针 #
使用智能指针std::tr1::shared_ptr
C++11 已经将shared_ptr纳入了std了,即std::shared_ptr
#include <iostream>
#include <memory> //gcc #include <tr1/memory>
int main(){
std::tr1::shared_ptr<int> a(new int(1));
std::cout<<*a<<endl; //输出1
return 1;
}
17. 继承时的覆盖(item 33) #
(1). 子类和父类的作用域 #
(2). 重载和覆写 #
例如:Derived类继承自Base类,Base类有两个重载的foo函数(一个无参,一个有参),Derived类覆写了foo的无参数版本。
这时,Base类的foo(int n)有参函数就不能被Derived调用了,因为它也被掩盖了。
class Base{
public:
//基类有两个重载的函数,无参和有参
void foo(){cout<<"Base::foo()"<<endl;};
void foo(int n){cout<<"Base::foo(int n)"<<endl;};
};
class Derive:public Base{
public:
//派生类只覆写了其中的一个无参,则有参的不会被继承了
void foo(){cout<<"Derived::foo()"<<endl;}
};
int main(){
Derive d;
d.foo(); //输出Derived::foo()
//d.foo(1); //报错,没有此函数
return 1;
}
如果子类不复写,则父类中两个foo重载,子类均可使用 即便父类中两个foo重载均声明为virtual,上面的问题仍然存在
(3). 使用Using解决上述问题 #
使用using Base::foo; 可以使父类中的foo函数在子类的作用域类可见
class Base{
public:
void foo(){cout<<"Base::foo()"<<endl;};
void foo(int n){cout<<"Base::foo(int n)"<<endl;};
};
class Derive:public Base{
public:
using Base::foo;
void foo(){cout<<"Derived::foo()"<<endl;}
};
int main(){
Derive d;
d.foo(); //输出Derived::foo()
d.foo(1); //输出 Base::foo(int n)
return 1;
}
(4). 转交函数:只继承父类重载函数中的一个 #
上述Using方法,使得Base::foo的两个重载函数在Derived类中都可见。
如果只想让其中一个可见(比如无参的),另一个不可见,则要使用转交函数(forwarding function)
class Base{
public:
void foo(){cout<<"Base::foo()"<<endl;};
void foo(int n){cout<<"Base::foo(int n)"<<endl;};
};
class Derive:public Base{
public:
virtual void foo(){Base::foo();} //转交函数
};
int main(){
Derive d;
d.foo(); //输出Base::foo()
//d.foo(1); //错误,该函数没有被继承下来
return 1;
}
原则:避免在派生类中重新定义基类的non-virtual函数
18. 基类:pure virtual , impure virtual, non-virtuals #
- 声明一个pure virtual 函数的目的是让Derived Classes只继承函数接口
- 声明一个impure virtual 函数(即virtual函数)的目的是让Derived Classes 继承该函数的接口和缺省实现
- 声明一个non-virtual 函数的目的Derived Classes继承该函数的接口和一份强制性实现
[non-virtual 函数代表的意义是invariant不变性凌驾于specialization特异性,它不该在Derived Classes中被重新定义]
19. 不要重新定义继承而来的缺省参数(item37) #
如下代码所示:
class Shape{
public:
enum Color{Red,Green,Blue,Yellow};//Red=0,Blue=1,Yellow=2
virtual void Draw(Color color = Red){//缺省参数
cout<<"Shape:"<<color<<endl;
}
};
class Circle:public Shape{
public:
void Draw(Color color = Green){//缺省参数
cout<<"Circle:"<<color<<endl;
}
};
int main(){
Shape* ps = new Circle();
//【我们期望输出的是Circle:1,但实际为Circle:0】
ps->Draw(); //输出Circle:0
return 1;
}
基类Shape::Draw 虚函数缺省参数为Red,派生类Circle::Draw 函数缺省参数为Green,
当基类指针指向派生类对象时,调用Draw()函数,由虚函数的性质可知,调用的是派生类Circle::Draw。
Shape* ps = new Circle();
ps->Draw(); //输出Circle:0
但是此处的缺省参数却不是派生类Circle的Green,而是基类Shape的Red
即:调用一个Derived class的virtual函数,使用的却是Base class为它指定的缺省参数
这是为什么?
因为:virtual是动态绑定,缺省参数是静态绑定 ps的动态类型是Circle,但是它的静态类型是Shape,
所以ps调用的是Circle::Draw,但是缺省参数却是Shape::Draw的缺省参数
怎么解决?
使用替代方案NVI(Non-Virtual Interface):令base class的一个public non-virtual 函数(如Shape::Draw)调用private virtual 函数(Shape::doDraw),后者可被derived class重新定义(Circle::doDraw)。这样保障了默认参数只有一个(Red)。
class Shape{
public:
enum Color{Red,Green,Blue,Yellow};
void Draw(Color color = Red) const {doDraw(color);}//默认参数
public:
virtual void doDraw(Color color )const =0;
};
class Circle: public Shape{
private:
void doDraw(Color color)const {//不要指定默认参数
cout<<"Circle:"<<color<<endl;
}
};
int main(){
Shape* ps = new Circle();
ps->Draw(); //输出Circle:0
return 1;
}
20. private继承 #
private继承时,编译器不会自动将一个derived class转换为base class;public继承则会自动转换。
class Person{};
class Student:private Person{}; // private继承
void eat(Person p){}
int main(){
Person p; Student s;
eat(p); //没问题
//eat(s); //出错
//error: 'Person' is an inaccessible base of 'Student'
return 1;
}
21. 模板元编程 #
模板元编程利用template和递归机制,编制出在编译期间(compile-time)可执行代码。
即程序结果在编译期间由编译器产生,而不是运行时得到。
主要使用C++的静态语言成分,风格类似函数式编程;不能使用变量,if、else、for等run-time 控制语句;
C++的模板机制(或静态语言机制,包括typedef,enum等)是图灵完备的(Turing-Complete),理论上可以实现任何可实现算法。
产生于数值计算,但最大用途在于类型计算(type computation)(及相关领域)【比如判断类型,IfPointer()…】;用来编写库,如Loki,boost。
经典例子 #
计算Fibonacci数列第N项
// 主模板 用于处理一般的逻辑
template<int N>
struct Fib
{
enum { Result = Fib<N-1>::Result + Fib<N-2>::Result };
};
// 完全特化版 处理N=1的情况
template <>
struct Fib<1>
{
enum { Result = 1 };
};
// 完全特化版 处理N=0的情况
template <>
struct Fib<0>
{
enum { Result = 0 };
};
// 示例
int main(){
int i = Fib<10>::Result; //mingw 最多支持到Fib<47>
std::cout << i << std::endl;
return 1;
}
一个阶乘的例子
template<int n>
struct Factorial
{
enum { val = Factorial<n-1>::val*n};
};
template<>
struct Factorial<0>
{
enum { val = 1};
};
int main(){
cout<<Factorial<6>::val<<endl;//720
return 1;
}
两个例子的基本思想相同:
利用模板特化机制实现编译期条件选择结构,利用递归模板实现编译期循环结构,模板元程序则由编译器在编译期解释执行。
优点
当被编译器解释时,模板元程序可以生成高效的代码,从而可以大幅提高最终应用程序的运行效率。通过将计算从运行期转移至编译期,在结果程序启动之前做尽可能多的工作,最终获得速度更快的程序。
缺点
代码可读性差;难以调试;可移植性差(不同编译器支持程度不一样)