More Effective C++ 笔记

基础议题
1 当你知道你必须指向一个对象并且不想改变其指向时,或者在
  重载操作符并为防止不必要的语义误解时,你不应该使用指针。
    1 必须指向对象,不能引用空
    2 始终指向相同的内存块
    3 避免 *v[5]=10

2 static_cast<double>(var)  同c类型的转换
  const_cast                去掉const
  dynamic_cast              基类指针转换为子类指针(否则返回NULL)
 
  另:void *指针不能进行运算,不能将函数指针转换为void *类型。
 
3 多态和指针运算不能混合在一起用。
 (基类类型数组,编译器已经决定了各元素间间隔的大小,
   如果传入子类对象,a[i]结果就混乱了。指针运算需要sizeof(typename))
 
 
4 使用这种(没有缺省构造函数的)类的确有一些限制(对象数组必须在{}里逐个传参,
  堆上的需要定义指针并逐个new,最后delete), 但是当你使用它时,
  它也给你提供了一种保证:你能相信这个类被正确地建立和高效地实现。
 
  在《深入c++对象模型》四种情况下(成员变量有构造函数,基类有构造函数),编译器会生成缺省构造函数。
  综上,应尽量定义一个可以完全初始化对象的构造函数(无论是否带参数,不带最好)。
   
运算符
5 不要定义隐式类型转换(  operator double() const; )
  可以换个其他名字 或者 最好不定义。
  explict: 拒绝单构造函数的隐式转换,  explict ctor(int a)
 
  就像go语言,函数可以只为参数指定类型,而不用给形参名字(避免无用参数,编译器告警)。
   
6 i++++; // same as i.operator++(0).operator++(0);
  它发现从第一个operator++函数返回的const对象又调用operator++函数,
  然而这个函数是一个non-const成员函数,所以const对象不能调用这个函数
  因此 后缀应该返回const(后缀比前缀多一个int参数)
 
  后缀的实现没办法 返回*this(因为要返回运算前的数据)
  当处理用户定义的类型时,尽可能地使用前缀increment,因为它的效率较高。
  在后缀里使用前缀操作,这样就只用维护前缀函数了。
    
7 逗号表达式整个表达式的结果是逗号右边表达式的值。
  尽量不去重载运算符,特别是&&, ||, 和 ,  因为函数对参数求值顺序不固定。
  因此&&和||的短路特性就没了,逗号运算符也不能保证先计算左边。
     
8 就象malloc一样,operator new的职责只是分配内存。它对构造函数一无所知。
  new操作符才知道ctor。可以自己定制operator new。
  placement new 指定地址给对象(已经分配的内存)
 
  delete是对应关系,operator delete只负责释放内存,不调用构造函数。
 
异常
9 运用ctor和dtor避免内存泄漏,(包装一层,类似auto_ptr)

10 因为C++确保删除空指针是安全的。在构造函数里捕获异常,如果是const指针,
   则在初始化列表里调用函数。

   初始化列表里初始某成员变量时可以调用成员函数,但是函数使用的成员变量应该
   是先于被初始的成员变量构造的。
   也就是说函数所用到的变量在声明时,顺序都在使用这个函数做初始化的变量的前面。
   这也是类的对象存储模型所支持的(函数是独立存储的,定义类时即已实现)。
   
   还可使用类似auto_ptr(有缺陷)
   然后收集了关于auto_ptr的几种注意事项:
   1、auto_ptr不能共享所有权。
   2、auto_ptr不能指向数组
   3、auto_ptr不能作为容器的成员。
   4、不能通过赋值操作来初始化auto_ptr
   std::auto_ptr<int> p(new int(42));     //OK
   std::auto_ptr<int> p = new int(42);    //ERROR
   这是因为auto_ptr 的构造函数被定义为了explicit
   5、不要把auto_ptr放入容器    
     
   不要使用auto_ptr。
   
11 析构函数不要传出异常  catch(...){}  1 导致teminal 2 析构函数不能完成
   
12 异常抛出静态类型,如基类指针引用子类对象,抛出的是基类对象拷贝。
   异常总是拷贝对象,不同于函数参数的引用。
   
   一般来说,你应该用throw来重新抛出异常,因为这样不会改变被传递出去的异常类型,
   而且更有效率,因为不用生成一个新拷贝。

   catch (Widget w) ... // 通过传值捕获
   会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,
   第二个是把临时对象拷贝进w中(WQ加注,重要:是两个!)。
   同样,当我们通过引用捕获异常时,
   catch (Widget& w) ... // 通过引用捕获
   catch (const Widget& w) ... //也通过引用捕获
   这仍旧会建立一个被抛出对象的拷贝:拷贝同样是一个临时对象(可能抛出对象的生命期结束了,所以必须拷贝)。
   相反当我们通过引用传递函数参数时,没有进行对象拷贝。
   如果catch( Widget *pW )  则要求pW指向的是全局或堆上的对象,这个和函数参数一致。
   
   基类子类转换(基类引用能捕获抛出的子类类型异常)、void *捕获所有指针。
   按代码顺序进行捕获。
   
   综上,多使用引用类型来catch。

 1 #include <iostream>
 2 #include <string>
 3 
 4 using namespace std;
 5 
 6 
 7 class testclass
 8 {
 9 public:
10     testclass(){ cout << "base ctor." << endl;}
11     testclass(const testclass &t)
12     {
13         cout << "base copy" << endl;
14     }
15     ~testclass(){};
16     
17     virtual void show()
18     {
19         cout << "base." << endl;
20     }
21     
22 };
23  
24  
25 class test: public testclass
26 {
27 public:
28        test()
29        {
30              cout << "child ctor" << endl;
31        }
32        
33            test(const test &t)
34         {
35             cout << "child copy" << endl;
36         }
37     
38        void show()
39        {
40             
41             cout << "child " << endl;
42        }
43 
44 };
45 //抛出异常时总会拷贝,而且是静态拷贝,即拷贝引用或指针的声明类型(base类) ,throw的是对象引用或者
46 //*pointer(指向全局空间或者堆上);
47 //   如果直接抛出指针,外面catch指针的话,不存在该情况(即只是指针的拷贝)。 
48 int main()
49 {
50     try
51     {
52     
53         testclass *tc = new test();
54         throw *tc;
55     }
56     catch( testclass t )
57     {
58         t.show();
59     }
60     return 0;
61 }
62 
63 //int main()
64 //{
65 //    try
66 //    {
67 //    
68 //        testclass *tc = new test();
69 //        
70 //        testclass &reference = *tc;
71 //        reference.show();                //通过引用实现多态 ,(调用了子类的虚函数) 
72 //         
73 //        cout << "***********" << endl;
74 //        throw tc;
75 //    }
76 //    catch( testclass *t )
77 //    {
78 //        t->show();
79 //    }
80 //    return 0;
81 //}

 


   
13 通过引用捕获异常,因为12的缺点(本身抛出子类,基类捕获)

14 不要使用异常规格,即指定异常类型,可能导致直接终止.
   (可以重置unexpected处理函数)

15 异常的开销(忽略)

效率
16 2/8 原则

17 懒惰计算,需要时才计算。懒惰定义,减少局部对象的生命期(《代码大全》)。

18 热情法,缓存预估会被频繁访问的数据。
   iterator是一个对象,不是指针,所以不能保证”->”被正确应用到它上面。
   不过STL要求”.”和”*”在iterator上是合法的,所以(*it).second在语法上
   虽然比较繁琐,但是保证能运行。
   
19 临时对象。
   在任何时候只要见到常量引用(reference-to-const) 参数,就存在建立
   临时对象而绑定在参数上的可能性。
   在任何时候只要见到函数返回对象就会有一个临时对象被建立(以后被释放)。


20 使用inline函数, return时直接调用构造函数,这样有的编译器能避免构造
   一个临时对象, 而是直接使用命名对象c (Rational c=a*b)     
    [!named] return value optimization (为返回值优化, 不同与nrvo)
   这种特殊的优化――通过使用函数的return 位置(或者在函数被调用位置用一
   个对象来替代)来消除局部临时对象――是众所周知的和被普遍实现的。
   
   
21 通过重载显示实现函数,避免隐式类型转换产生临时对象
   在C++中有一条规则是每一个重载的operator必须带有一个用户定义类型
   (user-defined type)的参数
   const UPInt operator+(const UPInt& lhs, int rhs);
      
22 比大多数编译器希望进行的返回值优化更复杂。
   上面第一个函数实现也有这样的临时对象开销,就象你为使用命名对象 result
   主要的一点是 operator的赋值形式(operator+=)比单独形式(operator+)效率更高。
   做为一个库程序设计者,应该都支持。

   因此当我们面对在命名对象和临时对象间进行选择时,用临时对象更好一些。
   它使你耗费的开销不会比命名的对象还多,特别是使用老编译器时,它的耗费会更少。
 
   return value 优化 和 nrvo 是两种优化,第二种需要copy ctor,可以暂不关心
 
23 stdio 在效率上比iostream要高,如果必要的情况下,可以更换
 
24 type_info的指针可能存放在vtbl的第一个元素上《深入探索c++对象模型》
 
技巧
25 被派生类重定义的虚拟函数不用必须与基类的虚拟函数具有一样的返回类型。
   如果函数的返回类型是一个指向基类的指针(或一个引用),那么 派生类的
   函数可以返回一个指向基类的派生类的指针(或引用)。
   
   这个特性很容易实现返回各个子类的对象的指针。如实现虚拟拷贝构造函数clone
   通过在拷贝构造外封装一层虚函数实现。
 
 
26 使用一个静态变量来记录类的对象数,当超过时,构造函数抛出异常。
   private构造函数,增加一个friend 方法来调用(可以选择new一个成员变量或多个)
   使用一个对象计数基类。
   
27 只建立堆上对象   (让栈对象和全局对象不合法,private 析构函数)
   禁止建立堆对象
     static void *operator new(size_t size);
     static void operator delete(void *ptr);
    
28 smart pointer
29 引用计数(实现string类对象如果对应的串相同,只保留一份)
   少量的值被大量的对象共享,对象/值的比例越高,越是适宜使用引用计数

30 代理类 (二维数组中的一维数组类)  

31 使用typeid(xx).name 和函数指针来实现不同类对象调用不同方法(多个对象需要虚函数)

33 非尾端类应该是抽象类(A<--B,如果A也能产生对象,则让AB共同继承一个虚基类)


 
 
 

posted @ 2016-09-30 16:16  navas  阅读(200)  评论(0编辑  收藏  举报