Effective C++ 读书心得

 

C转向C++

1尽量使用constinline而不用#define

 

2尽量用<iostream>而不用<stdio.h>

 

3尽量用newdelete而不用 mallocfree

因为 mallocfree不知道构造函数和析构函数

 

4尽量使用C++注释风格 // ,/* */C注释风格

 

5对应的newdelete要采用相同的形式

如果string  *stringPtr1 = new string[100]

delete [] stringPtr1

 

6析构函数里对指针成员调用delete

即使删除的是空指针也是安全的,类的每个指针成员要么指向有效的内存,要么就指向空,那么就可以在析构函数里简单的delete掉他们,而不用担心他们是否被new过。

delete p;

delete p之后,p变成没有定义,p所指向的内存也被释放,但仍然存放了它之前所指向的对象的地址。删除指针后,就变成悬垂指针(dangling pointer,指向曾经存放对象的地址。悬垂指针往往导致程序错误,而且很难检测出来。

因此最好的解决方法是,一旦删除了指针所指向的任何对象,立即将指针指为0

两种情况下会出现悬垂指针:

一、指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
 
二、指针pfree或者delete之后,没有置为NULL,让人误以为p是个合法的指针。别看freedelete的名字恶狠狠的(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。通常会用语句if (p != NULL)进行防错处理。很遗憾,此时if语句起不到防错作用,因为即便p不是NULL指针,它也不指向合法的内存块。

 

7预先准备好内存不够的情况

 

8operator new operator delete时要遵循常规

  实际做起来也就是:要有正确的返回值;可用内存不够时要调用出错处理函数;

   处理好0字节内存请求;避免不小心隐藏了标准形式的new

 

9避免隐藏标准形式的new

  一共有两个办法:

  1 在类里写一个支持标准 new调用方式的operator new

  2 为每一个增加到operator new的参数提供缺省值

 

10如果写了operator new就要同时写operator delete

 因为缺省operator new 是一种通用型的内存分配器,它得到的内存块是如下:

 内存块大小数据 + 对象的内存,operator delete总是通过传给它的内存块的包头信息来释放任意大小的内存块

而我们自己写的operator new不会返回带头信息的内存的指针,所以我们需要写自己的operator delete.

通用型的缺省operator new必须应付各种大小的内存请求,还要处理内部外部的碎片,所以效率低下。

所以我们可以写一个单项链表来做new或者delete操作。单项链表做成一个内存池,内存池不同于内存泄漏,因为内存池的大小决不会超过客户请求内存的最大值。

最好的办法是给出一个类来具体实现基于内存池的功能。

 

11为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符(exercised)

可以理解为只要类里有指针,就要也自己版本的构造函数

动态分配内存的类可以理解为类里包含指针

 缺省的赋值操作符执行的是逐位的copy,对指针来说就是逐位copy.有可能会产生内存泄漏

例如b = a,那么b指向的内存永远都不会被删除

a,b包含的指针指向同一对象,只要任意一个离开了生存空间,其析构函数就会删除掉另一个指针指向的内存。

另外对于拷贝构造函数而言,如果采用传对象值的方法调用函数,那么有一个通过默认拷贝函数初始化的过程,又造成了两个指针指向同一对象,只要任意一个离开了生存空间,其析构函数就会删除掉另一个指针指向的内存。

 因此只要类里有指针,就要写自己版本的拷贝构造函数和赋值操作符函数。

 

12尽量使用初始化列表而不要在构造函数里赋值(exercised)

特别是const和引用数据成员只能用初始化,不能被赋值

即使没有const和引用成员,用成员初始化列表还是比在构造函数里赋值要好,是出于效率的考虑:

因为对象的创建分为两部:

1 数据成员初始化(如果没有列表,将调用默认构造函数)

2执行被调用构造函数体内的动作。(赋值,将调用复制构造函数)

所以,尽量使用初始化列表

 

1. 为什么C++需要提供初始化列表?那些情况下必须实现初始化列表? (提示:有些情况下只能初始化不能赋值)
2.
构造函数可以是虚函数呢?在构造函数中调用虚函数会有什么样的结果? (提示:虚表指针是在构造函数的最开始初始化的)
3.
构造函数和赋值操作符operator=有什么区别? (提示:区分初始化和赋值)

 

13初始化列表中成员列出的顺序和它们在类中声明的顺序相同

 

初始化列表中成员列出的顺序应该和它们在类中声明的顺序相同。

因为类成员是按照它们在类里被声明的顺序进行初始化的,和它们在初始化列表中列出的顺序没有关系。

对一个对象的所有成员来说,它们的析构函数被调用的顺序总是和它们在构造函数里被创建的顺序相反。

 

14 确定基类有虚析构函数(exercised

这是因为派生类的object可以被基类的指针删除,所以基类必须有一个虚destructor来保证派生类的析构函数能被调用

如果某个类不包含虚函数,那一般是表示它将不作为一个基类使用

当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数

在想成为抽象类的类里声明一个纯虚析构函数

 

15operator=返回*this的引用 (exercise)

例如 string& operator=(const char *rhs)

         string& operator=(const string& rhs)

当定义自己的运算符时,必须返回赋值运算符左边参数的引用 *this,如果不这样做,就会导致不能连续赋值,或导致调用时的隐式类型转换不能进行,或两种情况同时发生。

如果operator = 返回 void,就妨碍了连续赋值操作

如果 operator = 返回 const对象的引用,以下情况就不行了(w1=w2=w3.

 

 

16operator=中对所有数据成员赋值

即使派生类的赋值运算符也必须处理它的基类成员的赋值

 

17operator=中检查给自己赋值的情况(exercise)

 

类和函数:设计与声明

 

18争取使类的接口完整并且最小

19分清成员函数,非成员函数和友员函数

20避免public接口出现数据成员

21尽可能使用const

对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或者二者同时指定为const

const可以指的是函数的返回值或某个参数,对于成员函数,还可以指的是整个函数

22尽量传引用而不用传值

通过值来传递一个对象的具体含义是由这个对象的类的拷贝构造函数定义的,使得传值成为一种非常昂贵的操作

因为引用不会调用构造函数获析构函数,因为没有新的对象创建。

引用的作用:

  1 使用引用形参返回额外的信息

  2利用const引用避免复制,在向函数传递大型对象时,需要使用引用形参.

如果使用引用形参的唯一目的是避免复制实参,需要定义为const引用

  3 应该将不需要修改的引用定义为const引用,普通的非const引用形参在使用时不灵活,这样的形参不能用const对象初始化,也不能用常量初始化

23必须返回一个对象时不要试图返回一个引用

一个函数只能有两种方法创建一个新对象:在堆栈里或堆上。

在堆栈里创建对象时伴随着一个局部变量的定义。

若要在函数里声明栈对象,返回一个引用,错误,因为当函数调用结束时,局部对象就被销毁,即不能返回局部对象的引用

若要在函数里声明类对象,返回一个引用,错误,谁来deletenew生成的对象?

见条款31,千万不要返回局部对象的引用,也不要返回函数内部用new初始化指针的引用

(此处可以总结为,返回值一般返回对象,传递参数一般用引用

 

24在函数重载和设定参数缺省值之间慎重选择

2 rules

1 确实有一个值可以作为缺省么?

2要用到多少种算法?

如果有合适的缺省,并且用到一种算法,就使用缺省参数,否则函数重载

 

25避免对指针和数字类型重载

26当心潜在的二义性

27如果不想使用隐式生成的函数就要显式的禁止它

具体方法是声明这个函数(一般是赋值运算符 =或者拷贝构造函数),并使之为private,防止别人调用它,并且不定义这个函数。

赋值运算符与拷贝构造函数在行为上具有相似性,所以一般禁止一个就等于禁止了另一个。

28划分全局名字空间

using namespace std;

 

类和函数:实现

29避免返回内部数据的句柄

句柄可以是指针或者引用

String& alsoB =const_cast<String&>(B);使得alsoB成为B的一个名字,但是不具有const属性

 

30避免这样的成员函数:其返回值是指向成员的非const指针或引用,但成员的访问级别比这个函数要低

因为这样privateprotected的变量就失去了被保护的意义

 

31千万不要返回局部对象的引用,也不要返回函数内部用new初始化指针的引用

若要在函数里声明栈对象,返回一个引用,错误,因为当函数调用结束时,局部对象就被销毁,即不能返回局部对象的引用。

若要在函数里声明类对象,返回一个引用,错误,谁来deletenew生成的对象?让调用这个函数的函数来做不现实,因为有可能返回对象的

名称没有被显式的使用。

 

32尽可能的推迟变量定义

    不要缺省一个构造一个对象然后对它赋值,因为效率比较低

要采用真正想要的值来初始化这个对象

 

33明智的使用内联

内联函数的基本思想将每个函数调用以它的代码体来替换。

过分的使用内联所产生的程序会因为有太大的体积而导致空间不够用,即使可以使用虚拟内存,内联造成的代码膨胀也可能会导致不合理的页面调度行为,过多的内联还会降低指令高速缓存的命中率,从而使取指令速度降低。

Inline就像register,只是对编译器的一种提示,而不是命令,也就是说编译器可以忽略掉指令。

构造函数不能够内联,表面上看起来没有代码,实际上有很多的代码

如果函数中包含有静态对象,通常要避免将它声明为内联函数

 

34将文件间的编译依赖性降至最低Forward Declaration

 尽量少在文件头中包含.h文件

当在Classdeclaration里只用到了只是被引用,就不用包括此类的头文件,只用包括 for example class switch;即可

将提供类定义(#include)的任务从函数声明头文件转交给包含函数调用的用户文件,就可以消除用户对定义的依赖

只要有可能,尽量让头文件不依赖于别的文件;如果不可能,就借助于类的声明。

也就是说:

如果可以使用对象的引用和指针,就要避免使用对象本身。定义某个类型的引用和指针只会涉及到这个类型的声明。

继承和面向对象设计

 

35使公有继承体现“是一个”的含义

 

36区分接口继承和实现继承

这两种类型的区别跟函数声明与函数定义间的区别一样

在基类中定义纯虚函数的目的在于,派生类仅仅只是继承函数的接口

声明一个除纯虚函数外什么也不包含的类叫做协议类 Protocol Class

 

37决不要重新定义继承而来的非虚函数

虚函数是动态绑定的,但非虚函数是静态绑定的

 

38决不要重新定义继承而来的缺省参数值

虚函数是动态绑定的,但缺省参数是静态绑定的,这意味着最终可能调用的是一个定义在派生类,但使用了基类的缺省参数值的虚函数。

 

39避免向下转换继承层次

从一个基类指针到一个派生类指针,被称为向下转换,因为它向下转换了继承的层次结构

避免的最好方法是将这种转换用虚函数替代

 

40通过分层体现“有一个”或“用来实现”

使某个类的对象称为另一个类的数据成员,从而实现将一个类构筑在另一个类之上,这一过程称为分层。

 

41区分继承和模板

42明智的使用私有继承

私有继承的特点:用什么来实现

1 和公有继承相反,如果两个类之间的继承关系为私有,编译器一般不会将派生类对象转换为基类对象

2从私有基类继承而来的成员都成为了派生类的私有成员,即使它们在基类中是保护或公有成员

 

因为分层也可以实现:用什么来实现,

规则是尽可能的使用分层,必须的时候采用私有继承。

(即有保护成员或虚函数介入的时候)

 

43明智的使用多继承

44说你想说的,理解你所说的

 

杂项

45弄清C++幕后为你所写所调用的函数

C++编译器在没有以下函数的时候会声明自己的版本:一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取值运算符,如果没有构造函数,则会有一个缺省构造函数

 

46宁可编译和链接时出错,也不要运行时出错

 

47确保非局部静态对象在使用前被初始化

 

48重视编译器警告

 

49熟悉标准库

50提高对C++认识

 

标准的String构造函数和析构函数

已知类String的原型为:

      class String

      {

        public:

            String(const char *str = NULL);      // 普通构造函数

            String(const String &other);        // 拷贝构造函数

            ~ String(void);                                  // 析构函数

            String & operate =(const String &other);      // 赋值函数

        private:

            char        *m_data;                      // 用于保存字符串

      };

            请编写String的上述4个函数。

标准答案:

 

// String的析构函数

            String::~String(void)               // 3

{

  delete [] m_data;                      

// 由于m_data是内部数据类型,也可以写成 delete m_data;

            }

 

            // String的普通构造函数            

            String::String(const char *str)      // 6

{

  if(str==NULL)                         

  {

    m_data = new char[1];    // 若能加 NULL 判断则更好

    *m_data = /0;                     

  }                                          

  else

  {

    int length = strlen(str);          

    m_data = new char[length+1];  // 若能加 NULL 判断则更好     

    strcpy(m_data, str);               

  }

}

// 拷贝构造函数

     String::String(const String &other)   // 3

     {   

  int length = strlen(other.m_data);

  m_data = new char[length+1];      // 若能加 NULL 判断则更好   

  strcpy(m_data, other.m_data);        

}

// 赋值函数

     String & String::operate =(const String &other)    // 13

     {   

         // (1) 检查自赋值                     // 4

          if(this == &other)

                return *this;

    

// (2) 释放原有的内存资源            // 3

          delete [] m_data;

          

          // 3)分配新的内存资源,并复制内容 // 3

  int length = strlen(other.m_data);

  m_data = new char[length+1];         // 若能加 NULL 判断则更好

          strcpy(m_data, other.m_data);

          

          // 4)返回本对象的引用            // 3

          return *this;

}

 

关于static_cast Operator const_cast Operator dynamic_cast Operator reinterpret_cast Operator

 

static_cast

 

The expression static_cast < type-id > ( expression ) converts expression to the type of type-id based solely on the types present in the expression. No run-time type check is made to ensure the safety of the conversion.

Syntax

static_cast < type-id > ( expression )

The static_cast operator can be used for operations such as converting a pointer to a base class to a pointer to a derived class. Such conversions are not always safe.

 

In contrast to dynamic_cast, no run-time check is made on the static_cast conversion of pb.

 

static_cast不能从表达式中去除const属性,因为另一个新的类型转换操作符const_cast有这样的功能。

 

const_cast

The const_cast operator can be used to remove the const, volatile, and __unaligned attribute(s) from a class.

Syntax

const_cast < type-id > ( expression )

 

const_cast用于类型转换掉表达式的constvolatileness属性。通过使用const_cast,你向人们和编译器强调你通过类型转换想做的只是改变一些东西的constness或者 volatileness属性。这个含义被编译器所约束。如果你试图使用const_cast来完成修改constness 或者volatileness属性之外的事情,你的类型转换将被拒绝。

 

 

dynamic_cast

MSDN:

The expression dynamic_cast<type-id>( expression ) converts the operand expression to an object of type type-id. The type-id must be a pointer or a reference to a previously defined class type or a “pointer to void”. The type of expression must be a pointer if type-id is a pointer, or an l-value if type-id is a reference.

Syntax

dynamic_cast < type-id > ( expression )

dynamic_cast,它被用于安全地沿着类的继承关系向下进行类型转换。这就是说,你能用dynamic_cast把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,而且你能知道转换是否成功。失败的转换将返回空指针(当对指针进行类型转换时)或者抛出异常(当对引用进行类型转换时)。

dynamic_casts
在帮助你浏览继承层次上是有限制的。它不能被用于缺乏虚函数的类型上,也不能用它来转换掉constness

 

reinterpret_cast Operator

MSDN:

The reinterpret_cast operator allows any pointer to be converted into any other pointer type. It also allows any integral type to be converted into any pointer type and vice versa. Misuse of the reinterpret_cast operator can easily be unsafe. Unless the desired conversion is inherently low-level, you should use one of the other cast operators.

使用这个操作符的类型转换,其的转换结果几乎都是执行期定义(implementation-defined)。因此,使用reinterpret_casts的代码很难移植。 reinterpret_casts的最普通的用途就是在函数指针类型之间进行转换。

比如转换函数指针的代码是不可移植的(C++不保证所有的函数指针都被用一样的方法表示),在一些情况下这样的转换会产生不正确的结果(参见条款M31),所以你应该避免转换函数指针类型。

 

Forward declaration

Header files may #include files only for classes that  are directly needed by the class declaration.Otherwise, forward declarations should be used.

A class is directly needed if objects of that class are used in the class declaration rather than just referenced.

So Include files needed by the class implementation will be included in the source files.

Forward declaration也就是说 前置声明

当在Classdeclaration里只用到了只是被引用,就不用包括此类的头文件,只用包括 for example class switch;即可

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值