您的位置:首页 > 编程语言 > C语言/C++

Effective C++学习笔记1

2012-06-19 23:36 295 查看
一. 让自己习惯C++

1. 视C++为一个语言联邦

  C++主要包括以下四个部分:

    C:C++以C为基础,并兼容C。

    Object-Oriented C++:面向对象编程,主要包括:类,封装,继承,多态。

    Template C++:C++泛型编程(C++ Generic Programming)。

    STL:主要包括:容器,迭代器,算法,函数对象。

  C++高效编程守则视情况而变化,取决于你使用哪一部分C++。

2. 尽量使用const,enum,inline替换define

  (1)const定义的常量在编译期确定,因此可以作为数组定义时使用。

  (2)在class中只能初始化static const常量。

     在class中不能初始化static成员。只能在类中声明,然后在类的外部定义。

     const成员只能在类的构造函数初始话列表中初始化。

     示例代码如下:     

      class my_Class
      {
      public:
         my_Class(int cnumber=0):c_number(cnumber){}
      private:
         static const int sc_number=10;
        static int s_number;
         const int c_number;
      };

      int my_Class::s_number=90;

  (3)在vs2008中是可以取enum 类型的地址的,和书上说的不一样。

  (4)对于类似函数的#define宏,最好用inline函数代替。

  (5)虽然有了上述的一些#define替代品,但是#ifdef/#ifndef依然扮演控制编译的重要角色。现在还不到预处理器全面隐退的时候。

3. 尽可能使用const

  (1)STL迭代器相当于T*,所以const vectot<int>::iterator iter; 相当于 int const *iter;iter指针指向不能改变,但是指针指向的内容可以改变。要想让内容不改变需要使用:vector<int>::const_iterator iter;相当于const int* iter。

  (2)const成员函数的目的是为了确认该成员函数可以作用于const对象身上。const对象不能改变类成员的值,不管这个成员是const还是non-const都不能改变。如果想在const成员函数中改变non-const成员的值,这个成员前面需要加上关键字:mutable,如:mutable int count;。对const对象的操作,仅限于读,并且只有const成员函数可以操作const对象。

  (3)当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码的重复。示例如下:

class TextBlock
{
public:
const char& operator[](std::size_t position) const
{
...
...
...
return text[position];
}
char& operator[](std::size_t position)
{
return const_cast<char&>(static_cast<const TextBlock&>(*this))[position];
}
};


  首先将非const的对象通过static_cast转换成const对象,然后调用const成员函数operator[],之后对返回的const类型通过const_cast转换成非cosnt类型。

  注意这种写代码的方式。

4. 确定对象被使用前都先被初始化

  (1)C++规定,对象的成员变量的初始化动作发生在进入构造函数之前。构造函数最好使用成员初始化列表的形式,而不要在构造函数内使用赋值操作。成员初始化顺序是先初始化基类的成员变量,然后再初始化派生类的成员变量,class的成员初始化顺序总是按照其声明次序被初始化。所以为了安全起见,成员初始化类表顺序应该和成员声明顺序相同。

  (2)为免除“跨编译器单元之初始化顺序”,请以local static对象代替non-local static 对象。

[b]二. 构造/析构/赋值运算[/b]

5. 了解C++默默编写并调用哪些函数

  (1)如果自己没有写,编译器为类声明下面四个函数:一个copy构造函数,一个copy assignment操作符,一个默认无参数构造函数和一个析构函数。如果用户定义了任意 一个构造函数,编译器不再提供默认构造函数。

  (2)函数声明形式代码如下:

class Empty
{
public:
Empty(){...}
Empty(const Empty&){...}
~Empty(){...}
Empty& operator=(const Empty&){...}
};


6. 若不想使用编译器自动生成的函数,就该明确拒绝。

  (1)如果一个对象是独一无二的,不能被拷贝和赋值,可以通过将拷贝和赋值函数声明为私有的。

  (2)注意这里一定要使用private继承,代码如下:

class UnCopyable
{
protected:
UnCopyable(){}
~UnCopyable(){}
private:
UnCopyable(const UnCopyable&);
UnCopyable& operator=(const UnCopyable&);
};

class HomeForSale:private UnCopyable
{
...
};


7. 为多态基类声明virtual析构函数

  (1)带多态性质的基类(polymorphic base class)应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。

  (2)class设计的目的如果不是作为base class使用,或者不具备多态性,就不该声明virtual析构函数。因为virtual函数会占用额外的一个指针空间。

8. 别让异常逃离构造函数

  (1)析构函数绝不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获任何异常,然后吞掉它们(不传播)或终止程序。

  (2)如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

9. 绝不在构造和析构函数过程中调用virtual函数。因为这类函数从不下降至derived class(比起当前执行构造函数和析构函数的那一层)。

10. 另operator=返回一个reference to*this。这样是为了实现“连锁赋值”。

11. 在operator=中处理“自我复制”

  (1)确保当前对象自我赋值有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址,精心周到的语句顺序,以及copy-and-swap技术。

  (2)确保任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然是正确的。

  (3)实现方式的代码书写,一般有下面几种形式:

//下面的类将copy的公共部分代码封装在一个init函数中,然后在copy和赋值拷贝中调用公共代码
//但是在复制赋值语句中没有考虑到自身赋值。这里也不需要考虑自身赋值,因为这里没有存在heap
//分配的对象
class Teacher{
public:
Teacher(string _name,int _age):name(_name),age(_age){}
Teacher(const Teacher& rhs){
init(rhs);
}
Teacher& operator=(const Teacher& rhs){
init(rhs);
return *this;
}
string getName(){
return this->name;
}
int getAge(){
return this->age;
}
private:
void init(const Teacher& rhs){
this->name=rhs.name;
this->age=rhs.age;
}
string name;
int age;
};

//考虑"自身赋值"的四种方法
class School{
public:
School(){
teacher=new Teacher("Julia",22);
}
School(Teacher* _teacher){
this->teacher=_teacher;
}
//对象交换方法,只需将对象中的所有属性交换即可
void swap(School& rhs){
School tmp(rhs);
tmp.teacher=rhs.teacher;
rhs.teacher=this->teacher;
this->teacher=tmp.teacher;
}
School(const School& rhs){
this->teacher=rhs.teacher;
}
//方法一,通过条件判断
//这种方法存在异常安全性,如果new语句申请内存
//没有成功,那么teacher指针指向将不确定
School& operator=(const School& rhs){
if (this==&rhs){
cout<<"自身赋值"<<endl;
return *this;
}
delete teacher;
teacher=new Teacher(*rhs.teacher);
return *this;
}
//方法二,拷贝然后复制
School& operator=(const School& rhs){
School *tmp=this->teacher;
this->teacher=new Teacher(*rhs.teacher);
delete tmp;
return *this;
}
//方法三,拷贝然后交换,先将要赋值的内容做一份拷贝(生成一个局部临时对象),注意这里是pass by reference
School& operator=(const School& rhs){
School temp(rhs);
swap(temp);
return *this;
}
//方法四,是对方法三的进一步改进,采用pass by value方式,因为值传递会拷贝一份临时对象,就不用像三那样
//写一条语句进行对象拷贝了
School& operator=(School rhs){
swap(rhs);
return *this;
}
void print(){
cout<<"this->teacher->name:"<<this->teacher->getName()<<endl<<"this->teacher->age:"<<this->teacher->getAge()<<endl;
}
private:
Teacher *teacher;
};


12. 复制对象时勿忘其每一个部分

  (1)当编写一个copying函数,请确保:a. 复制所有local成员变量;b.调用所有base class内适当的copying函数。

  (2)不要用某个copying函数实现另一个copying函数。应该将共同机能放在第三个函数中,这个函数一般是private的,命名为init,如上面11的代码Teacher所示。

  (3)调用基类copying函数的方式如下面代码所示:

//赋值勿忘其每一个部分
class Student{
public:
Student(string _name,int _age):name(_name),age(_age){}
Student(const Student& rhs){
init(rhs);
}
Student& operator=(const Student& rhs){
init(rhs);
return *this;
}
string getName(){
return this->name;
}
int getAge(){
return this->age;
}
private:
void init(const Student& rhs){
this->name=rhs.name;
this->age=rhs.age;
}
string name;
int age;
};

class HighStudent:public Student{
public:
HighStudent(string _name,int _age,bool _girlFriend):Student(_name,_age),girlFriend(_girlFriend){
}
//如果这里将Student(rhs)注释掉会出现下面的错误
//error C2512: 'Student' : no appropriate default constructor available
//这是因为回去调用基类的默认构造函数,而基类并没有提供默认的构造函数
HighStudent(const HighStudent& rhs):Student(rhs),girlFriend(rhs.girlFriend){}
HighStudent& operator=(const HighStudent& rhs){
Student::operator=(rhs);//如果将这一句注释掉,基类的属性值不会改变,改变的只有派生类的值
girlFriend=rhs.girlFriend;
return *this;
}
bool getGirlFriend(){
return girlFriend;
}
void print(){
cout<<"Name:"<<getName()<<"\tAge:"<<getAge()<<"\tHas GirlFriend?: "<<getGirlFriend()<<endl;
}
private:
bool girlFriend;
};


[b] 三.资源管理[/b]

13. 以对象管理资源

  (1)把资源放进对象内,我们便可以依靠C++的“析构函数自身调用机制”确保资源被释放。

  (2)资源取得时机便是初始化时机(RAII Resource Acquisition is Initialization)。

  (3)auto_ptr,若通过copy构造函数或copy assignment操作符复制它们,被复制物便会变为null,而复制所得的指针将取得资源的唯一拥有全。

  (4)使用方式,std::tr1::shared_ptr<Investment> pInv(createInvestment);

          std::auto_ptr<Investment> pInv(createInvestment);

     注意它们只能指向单个对象,不能给它们分配对象数组。

14. 在资源管理中心小心copying行为

  (1)复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。

  (2)普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法。不过其它行为也都可能被实现。

15. 在资源管理类中提供对原始资源的访问

  (1)RAII classes并不是为了封装某物而存在;它们的存在是为了确保一个特殊行为----资源释放----会发生。

  (2)APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。一般是提供一个get()函数。

  (3)对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但是隐式转换对客户端比较方便。

16. 成对使用new和delete时要采用相同的形式

  (1)注意下面的这种形式:

    typedef std::string AddressLines[4];

    std::string* pal=new AddressLines;

    delete pal;  //这么写是错误的

    delete[] pal;  //这么写才是正确的

17. 以独立语句将newed对象置入智能指针

  (1)给出一个出现异常的例子:

    int priority();

    void processWidget(std::tr1::shared_ptr<Widget> pw,int priority);

    现在考虑调用:

    processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());

    这里编译器必须创建代码做下面三件事:

    》调用priority

    》new 一个widget对象

    》调用tr1::shared_ptr构造函数

    但是对于C++来说这三件事执行的顺序是不确定的,确定的是new 肯定在shared_ptr构造函数之前执行。如果执行顺序如下:

    》new 一个widget对象

    》调用priority

    》调用tr1::shared_ptr构造函数

    这时如果priority出现异常,将导致widget对象得不到释放。分开写成下面的形式:

    std::tr1::shared_ptr<Widget> pw(new Widget);

    processWidget(pw,priority);

    就不会出现上述说的问题了。   

四. 设计与声明

18. 让接口容易被使用,不易被误用。

19. 设计class犹如设计type。

20. 宁以pass-by-reference-to-const替换pass-by-value

  (1)一般而言,可以合理假设“pass-by-value并不昂贵”的唯一对象就是内置类型和STL的迭代器和函数对象。对它们而言,pass-by-value比较合适。

  (2)pass-by-reference-to-const可以避免切割问题(slicing problem)

21. 必须返回对象时,别妄想返回其reference

  (1)绝不要还回pointer或reference指向一个local stack对象,或返回一个reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象。

  (2)这一条书中提供的示例很好,充分证明了作者的论点。

22. 将成员变量声明为private。

23. 宁以non-member、non-friend替换member函数。

  (1)这里个人的理解是:和类密切相关的函数要写出成员函数,如果一些复杂的功能可以通过组合几个成员函数来完成,则完成这个功能的函数最好使用non-member函数。

24. 若所有参数皆需要类型转换,请为此采用non-member函数

  (1)这个个人理解:对于一些二元运算符(+-×/等运算)的重载要使用non-member函数,必要时可以是friend。

  (2)只有当参数被列于参数列(parameter list)时,这个参数才是隐式类型转换的合格参与者。地位相当于“被调用之成员函数所隶属的那个对象”----即this对象----的那个隐喻参数,绝不是隐式转换的合格参与者。

25. 考虑写出一个不抛出异常的swap函数。

     
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: