Effective C++学习笔记1

本文深入探讨了C++编程中的关键技巧,包括习惯C++语言特性、构造与析构原则、资源管理策略以及设计与声明的注意事项。文章强调了const、inline与enum的正确使用,构造函数与析构函数的自定义,以及如何有效管理资源。
摘要由CSDN通过智能技术生成

 

一. 让自己习惯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版本可避免代码的重复。示例如下:

 

 1 class TextBlock
 2 {
 3 public:
 4     const char& operator[](std::size_t position) const
 5     {
 6         ...    
 7         ...
 8         ...
 9         return text[position];
10     }
11     char& operator[](std::size_t position)
12     {
13         return const_cast<char&>(static_cast<const TextBlock&>(*this))[position];
14     }
15 };

 

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

  注意这种写代码的方式。

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

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

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

二. 构造/析构/赋值运算

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

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

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

1 class Empty
2 {
3 public:
4     Empty(){...}
5     Empty(const Empty&){...}
6     ~Empty(){...}
7     Empty& operator=(const Empty&){...}
8 };

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

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

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

 1 class UnCopyable
 2 {
 3 protected:
 4     UnCopyable(){}
 5     ~UnCopyable(){}
 6 private:
 7     UnCopyable(const UnCopyable&);
 8     UnCopyable& operator=(const UnCopyable&);
 9 };
10 
11 class HomeForSale:private UnCopyable
12 {
13     ...
14 };

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)实现方式的代码书写,一般有下面几种形式:

 

 1 //下面的类将copy的公共部分代码封装在一个init函数中,然后在copy和赋值拷贝中调用公共代码
 2 //但是在复制赋值语句中没有考虑到自身赋值。这里也不需要考虑自身赋值,因为这里没有存在heap
 3 //分配的对象
 4 class Teacher{
 5 public:
 6     Teacher(string _name,int _age):name(_name),age(_age){}
 7     Teacher(const Teacher& rhs){
 8         init(rhs);
 9     }
10     Teacher& operator=(const Teacher& rhs){
11         init(rhs);
12         return *this;
13     }
14     string getName(){
15         return this->name;
16     }
17     int getAge(){
18         return this->age;
19     }
20 private:
21     void init(const Teacher& rhs){
22         this->name=rhs.name;
23         this->age=rhs.age;
24     }
25     string name;
26     int age;
27 };
28 
29 
30 //考虑"自身赋值"的四种方法
31 class School{
32 public:
33     School(){
34         teacher=new Teacher("Julia",22);
35     }
36     School(Teacher* _teacher){
37         this->teacher=_teacher;
38     }
39     //对象交换方法,只需将对象中的所有属性交换即可
40     void swap(School& rhs){
41         School tmp(rhs);
42         tmp.teacher=rhs.teacher;
43         rhs.teacher=this->teacher;
44         this->teacher=tmp.teacher;
45     }
46     School(const School& rhs){
47         this->teacher=rhs.teacher;
48     }
49     //方法一,通过条件判断
50     //这种方法存在异常安全性,如果new语句申请内存
51     //没有成功,那么teacher指针指向将不确定
52     School& operator=(const School& rhs){
53         if (this==&rhs){
54             cout<<"自身赋值"<<endl;
55             return *this;
56         }
57         delete teacher;
58         teacher=new Teacher(*rhs.teacher);
59         return *this;
60     }
61     //方法二,拷贝然后复制
62     School& operator=(const School& rhs){
63         School *tmp=this->teacher;
64         this->teacher=new Teacher(*rhs.teacher);
65         delete tmp;
66         return *this;
67     }
68     //方法三,拷贝然后交换,先将要赋值的内容做一份拷贝(生成一个局部临时对象),注意这里是pass by reference
69     School& operator=(const School& rhs){
70         School temp(rhs);
71         swap(temp);
72         return *this;
73     }
74     //方法四,是对方法三的进一步改进,采用pass by value方式,因为值传递会拷贝一份临时对象,就不用像三那样
75     //写一条语句进行对象拷贝了
76     School& operator=(School rhs){
77         swap(rhs);
78         return *this;
79     }
80     void print(){
81         cout<<"this->teacher->name:"<<this->teacher->getName()<<endl<<"this->teacher->age:"<<this->teacher->getAge()<<endl;
82     }
83 private:
84     Teacher *teacher;
85 };

 

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

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

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

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

 1 //赋值勿忘其每一个部分
 2 class Student{
 3 public:
 4     Student(string _name,int _age):name(_name),age(_age){}
 5     Student(const Student& rhs){
 6         init(rhs);
 7     }
 8     Student& operator=(const Student& rhs){
 9         init(rhs);
10         return *this;
11     }
12     string getName(){
13         return this->name;
14     }
15     int getAge(){
16         return this->age;
17     }
18 private:
19     void init(const Student& rhs){
20         this->name=rhs.name;
21         this->age=rhs.age;
22     }
23     string name;
24     int age;
25 };
26 
27 class HighStudent:public Student{
28 public:
29     HighStudent(string _name,int _age,bool _girlFriend):Student(_name,_age),girlFriend(_girlFriend){
30     }
31     //如果这里将Student(rhs)注释掉会出现下面的错误
32     //error C2512: 'Student' : no appropriate default constructor available
33     //这是因为回去调用基类的默认构造函数,而基类并没有提供默认的构造函数
34     HighStudent(const HighStudent& rhs):Student(rhs),girlFriend(rhs.girlFriend){}
35     HighStudent& operator=(const HighStudent& rhs){
36         Student::operator=(rhs);//如果将这一句注释掉,基类的属性值不会改变,改变的只有派生类的值
37         girlFriend=rhs.girlFriend;
38         return *this;
39     }
40     bool getGirlFriend(){
41         return girlFriend;
42     }
43     void print(){
44         cout<<"Name:"<<getName()<<"\tAge:"<<getAge()<<"\tHas GirlFriend?: "<<getGirlFriend()<<endl;
45     }
46 private:
47     bool girlFriend;
48 };

 三.资源管理

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函数。

 

 

     

转载于:https://www.cnblogs.com/ZJUKasuosuo/archive/2012/06/19/EffectiveCPP1.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值