Effective C++ 学习笔记

Effective C++ 学习笔记

Dec 28, 2013
Coding
C++, 笔记

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). 子类和父类的作用域 #

clip_image002.jpg

(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;
}

两个例子的基本思想相同:

利用模板特化机制实现编译期条件选择结构,利用递归模板实现编译期循环结构,模板元程序则由编译器在编译期解释执行。

优点

当被编译器解释时,模板元程序可以生成高效的代码,从而可以大幅提高最终应用程序的运行效率。通过将计算从运行期转移至编译期,在结果程序启动之前做尽可能多的工作,最终获得速度更快的程序。

缺点

代码可读性差;难以调试;可移植性差(不同编译器支持程度不一样)