Effective C++总结

1060人阅读

前言:

effective C++ 是一本经典之作,其翻译较为贴合原著,但读起来未免有些僵硬而让人摸不着头脑,所以,我会以更为贴近中国人的理解,对此书进行一些阐释。

条款01:把 C++ 看成一个语言联邦

C++由几个重要的次语言构成

C语言:区块,语句,预处理器,数组,指针等等。

:class,封装,继承,多态......(动态绑定等等)

模板:涉及泛型编程,内置数种可供套用的函数或者类。

STL:STL是个模板库,主要涉及容器,算法和迭代器

在不同情况下使用适合的部分,可以使 C++ 实现高效编程

条款02:用const, enum, inline 替换 #define

1、#define 修饰的记号,在预处理的时候,已经全部被替换成了某个数值,如果出错,错误信息可能会提到这个数值,而不会提到这个记号。在纠错方面很花时间,因为其他程序员不知道这个数值代表什么。我们可以用 const 和 enum 解决这个问题。

 
  1. //enum hack 补偿做法:

  2. enum 枚举量{para1 = value1, para2 = value2,......}

  3. //将一个枚举类型的数值当作 int 类型使用

  4. //和 #define 很像,都不能取地址,但它没有 #define 的缺点

2、#define 不能定义类的常量,因为被 #define 定义的常量可以被全局访问,它不能提供任何封装性。

3、#define 修饰的宏书写繁琐且容易出错,inline 函数可以避免这种情况:
 

 
  1. #define MY_COMPARE(a, b) f((a) > (b) ? (a) : (b))

  2. //这是一个三目运算符,如果 a > b,则返回 a,否则返回 b

  3. //宏中的每一个实参都必须加上小括号

  4.  
  5. //调用:

  6. int a = 5, b = 0;

  7. MY_COMPARE(++a, b);//1

  8. MY_COMPARE(++a, b + 10);//2

  9.  
  10. /*

  11. 1式中,++a => a = 6 => 6 > b = 0 => return ++a;

  12. a 的值竟然增加了两次!

  13. */

  14.  
  15. //定义 inline:

  16. #define MY_MAX(a, b) (a) > (b) ? (a) : (b)

  17.  
  18. template<class T>

  19. inline int MY_COMPARE(const T&a, const T&b)

  20. {

  21. a > b ? a : b;

  22. }

  23. //inline 将函数调用变成函数本体

  24. //传入的是 ++a 的值

  25.  
  26. int main()

  27. {

  28. int a = 2;

  29. int b = 2;

  30. MY_COMPARE(++a, b);

  31. cout << a << endl;

  32. //此时 a = 3

  33.  
  34. MY_MAX(++a, b);

  35. cout << a << endl;

  36. //此时 a = 5

  37.  
  38. system("pause");

  39. return 0;

  40. }

条款03:尽可能使用 const

const 允许我们指定一个语义约束,使某个值应该保持不变

1、const 修饰 变量,指针,函数,函数返回值等,可以使程序减少错误,或者更容易检测错误:

        指针常量:int* const p;//指针地址不可变,指针指向值可变

        常量指针:const int* p;//指针指向值不可变,指针地址可变

        常量指针常量:const int* const p;//都不可变

const 修饰迭代器

        iterator 相当于 T* const //指针常量

        const_iterator 相当于 const T* //常量指针

const 修饰函数返回值

 
  1. const int max(int a, int b)

  2. {

  3. a > b ? a : b;

  4. }

  5.  
  6. int c = 6;

  7. max(a,b) = c;

  8. //将 c 的值赋给 max(a, b) 是没有意义的,const 防止这种操作的发生

2、const 修饰成员函数

        如果两个成员函数只是常量性不同(其他相同)则可以发生重载

                const 类对象调用 const 成员函数

                non-const 类对象调用普通成员函数

bitwise:

        const 成员函数不能改变(除 static)成员变量的值,因为常函数里 this 指针指向的值不可改变。同理,const 对象不可以调用 non-const 函数,因为函数有能力更改成员属性的值。

        但是若成员变量是一个指针,仅仅改变指针指向的值却不改变指针地址(地址是 this 指向的值),则不算是 const 函数 ,但能够通过 bitwise 测试。

        使用 mutable 可以消除 non-static 成员变量的 bitwise constness 约束。

 
  1. class person

  2. {

  3. public:

  4.  
  5. person(int a)

  6. {

  7. m_id = a;

  8. }

  9.  
  10. int& func() const

  11. {

  12. m_id = 888;

  13. }

  14.  
  15. mutable int m_id;

  16. };

  17.  
  18. int main()

  19. {

  20. const person p(666);

  21. p.func();

  22.  
  23. cout << p.m_id << endl;

  24.  
  25. system("pause");

  26. return 0;

  27. }

3、当 const 和 non-const 成员函数有实质的等价实现时,利用两次转型,令 non-const 调用 const 可以避免代码重复。

 
  1. const char& operator[](int pos) const

  2. {

  3. //...

  4. //...

  5. return name[pos];

  6. }

  7.  
  8. char& operator[](int pos)

  9. {

  10. return

  11. const_cast<char&>//移除第一次转型添加的 const

  12. (

  13. static_cast<const classname>(*this)[pos]

  14. //把 classname 类型数据转换为 const classname

  15. //使得能够调用 const operator[]

  16. );

  17. }

条款04:确定对象被使用前已经被初始化

1、内置数据类型

 
  1. int a = 0;

  2. double b = 0;

  3. char* c = "A C-style string";

2、自定义数据类型

使用成员初值列替换有参构造,且次序和class声明次序相同:
 

 
  1. class person

  2. {

  3. public:

  4. person(int age, int id, string name)//这是一个有参构造

  5. {

  6. this->m_age = age;

  7. this->m_id = id;

  8. this->m_name = name;

  9. }

  10.  
  11. int m_age;

  12. int m_id;

  13. string m_name;

  14. };

  15.  
  16. //这是成员初始列

  17. class person

  18. {

  19. public:

  20. person(int age, int id, string name):

  21. m_age(age),

  22. m_id(id),

  23. m_name(name)

  24. {}

  25.  
  26. int m_age;

  27. int m_id;

  28. string m_name;

  29. };

  30.  
  31. /*

  32. 成员初始列格式:

  33. classname(parameter1, parameter2,...):

  34. member1(value1),

  35. member2(value2),

  36. ...

  37. membern(value)

  38. {}

  39. */

有参构造是先执行默认构造,再给成员变量赋值。这有一个坏处,const 修饰的量不可以被赋值,但是可以初始化。

使用成员初值列则省去了默认构造部分。成员初值列在不传参的情况下,可以看成默认构造。

 
  1. int m_size;

  2. char m_array;

  3.  
  4. //这两个成员变量在初始化的时候是有次序的,m_array 的大小是由 m_size 指定的

  5. //所以必须先初始化 m_size

  6. //实际上,成员初值列中变量的次序可以和声明次序不同

  7. //但为防止这类错误出现,成员初值列的次序应当和声明次序相同

3、使用 local-static 对象替换 non-local-static 对象

函数内 static 对象是 local-static 对象,函数外 static 对象是 non-local-static 对象。

 
  1. class teacher

  2. {

  3. ......

  4. string tname(){...};

  5. };

  6. extern teacher t;

  7. //声明一个 teacher 对象 t, 预留给用户使用

  8. //不定义是因为我们不知道什么时候用它

  9.  
  10. class student

  11. {

  12. ......

  13. student(params)

  14. {

  15. string a = t.tname();

  16. //使用 t 对象

  17. }

  18. };

这里就出现问题了,teacher 对象必须在 student 对象之前初始化,但是 student 构造函数中使用的是未初始化的 teacher 对象。

解决办法:利用一个函数,定义并初始化本 static 对象,并返回它的引用。

 
  1. class teacher

  2. {

  3. ......

  4. string pname(){...};

  5. };

  6. teacher& teach()

  7. {

  8. static teacher t;//定义一个 local-static 对象

  9. return t;

  10. }

  11.  
  12. class student

  13. {

  14. ......

  15. student(params)

  16. {

  17. string a = t().tname();

  18. //使用 t() 函数返回的引用,引用期间,teacher对象被初始化

  19. }

  20. };

  21. student& stu()

  22. {

  23. static student s;

  24. return s;

  25. }

  26.  

extern 声明的对象对于 teacher 而言是一个 non-local-static 对象, teacher& 函数内声明的对象对于 teacher 而言是一个 local-static 对象。C++保证,local-static 对象在包含它的函数被调用期间(或者说首次遇到这个对象),会被初始化。也就是说,如果用返回引用的函数访问这个对象,就没有调用未初始化对象的困扰。

条款05:了解C++默默编写并调用哪些函数 

在创建类时,如果自己不定义默认构造,拷贝构造(拷贝运算符),析构函数,那么编译器会自动生成这些函数。

 
  1. //拷贝运算符:

  2. classname& operator=(const classname& cn){......}

  3.  

拷贝运算符注意事项:

若成员变量中有引用,或者被 const 修饰等等,拷贝运算符不可被调用。

 
  1. class person

  2. {

  3. ......

  4. const int m_age;

  5. string& m_name;

  6. ......

  7. }

  8.  
  9. person p1("lisa", 18);

  10. person p2("luna", 19);

  11. p1 = p2;//error!

  12. //const 值不可以修改,引用的指向不可以修改

条款06:若不想使用编译器自动生成的函数,就该明确拒绝

任何人都应该是天地间独一无二的,很难有理由有两个一摸一样的人。所以对于类中拷贝构造函数,我们应当阻止他们。但若是不声明,编译器也会自动生成拷贝构造函数。

 
  1. class person

  2. {

  3. private:

  4. person(const person&);

  5. person& operator=(const person&);

  6. //参数是不必要写的,毕竟这个函数不会被实现

  7. public:

  8. ......

  9. };

编译器自动生成的函数都是 public 函数,所以我们将 public 改为 private,就可以防止对象调用拷贝构造。

注:private 只有成员函数和友元函数可以调用。

同时也产生了一个问题,如何防止拷贝在成员函数或友元函数中被调用?

答案是建立一个父类,在父类中定义 private 拷贝函数,子类( person 等等)继承父类。因为子类不可以调用父类的 private 函数:

 
  1. class uncopyable

  2. {

  3. private:

  4. uncopyable(const uncopyable&);

  5. uncopyable operator=(const uncopyable&);

  6. };

  7.  
  8. class person{......};

条款07:给多态基类声明 virtual 析构函数

多态把父类当作一个接口,用以处理子类对象:利用父类指针,指向一个在堆区开辟的子类对象。

 
  1. class person

  2. {

  3. public:

  4. person();

  5. ......

  6. ~person();

  7. };

  8.  
  9. class teacher: public person{......};

  10.  
  11. person* p = new teacher(...);

  12. ...

  13. delete p;

  14. //在堆区开辟的数据要手动删除

上述代码是有问题的。

我们知道,在普通类继承里,删除子类对象会先调用子类的析构,再调用父类的析构。但在多态里情况有所不同。我们删除的是父类指针,调用的只是父类的析构函数,子类析构不会被调用,也就是说,子类对象没有被删除,而指针却没了。这是局部销毁,会造成资源泄漏等错误。

幸运的是,我们可以通过虚函数来解决这个问题。

在多态里,虚函数可以让子类重写父类的函数,同时在虚函数表中生成一个指针,找到子类重写函数的地址,从而让我们可以通过父类访问子类重写的函数。

 
  1. class person

  2. {

  3. public:

  4. person();

  5. ......

  6. virtual ~person();

  7. };

  8.  
  9. class teacher: public person{......};

  10.  
  11. person* p = new teacher(...);

  12. ...

  13. delete p;

  14. //删除 p 的时候调用 virtual ~person();

  15. //virtual 找到子类析构函数的地址,导致子类也可以被删除

纯虚函数使得父类更像一个接口,这里不用多说。

注:多态里父类应该至少有一个虚函数(virtual 析构),若不用做多态,则类里不应该有虚函数。

条款08:别让异常逃离析构函数

释义:在析构函数内部处理异常

我们来看以下案例:

 
  1. class person

  2. {

  3. public:

  4. ...

  5. ~animal(){...};

  6. //假设执行这个析构函数会抛出一个异常

  7. };

  8.  
  9. void test()

  10. {

  11. vector<person> v;//假设容器中存储了几个对象

  12. ...

  13. v.clear();//现在清空容器

  14. ...

  15. add();//摧毁容器之后,剩余的其他操作

  16. ...

  17. }

  18.  

由于对象属于 person 类,在删除的时候会调用析构函数,而 person  的析构函数会抛出一个异常,且未进行异常处理,所以异常会到 test 函数里。 v 中存储了不止一个对象,所以会同时抛出多个异常到test函数里。

在C++程序中,若是同时存在两个异常,则要么结束程序,要么导致不确定行为。结束程序,剩余的操作就无法完成,这对于程序员来说是一个麻烦。

首先介绍一下异常处理的办法

 
  1. try

  2. {...}

  3. //try 内部写可能产生异常的语句,没有产生异常,则catch语句不执行,产生则一一匹配

  4. //catch 用于捕获并处理异常,和 case 有异曲同工之妙

  5. catch(...)

  6. {

  7. 1、可以使用 abort(); 函数终止程序

  8. 2、可以吞下这个异常,在 catch 内部做一些处理

  9. }

 了解如何处理异常之后,我们就可以实现如条款所说,在析构函数内部处理异常

 
  1. class employee

  2. {

  3. public:

  4. ...

  5. void furlough(); //这是一个放假函数,不放假则抛出一个异常

  6. ...

  7. };

  8.  
  9. class manager

  10. {

  11. public:

  12. ...

  13. ~manager()

  14. {

  15. try

  16. {

  17. ep.furlough();

  18. }

  19. //用于确保工人在劳碌后总是可以放假

  20. catch(...)

  21. {

  22. //内部实现

  23. }

  24. }

  25. ...

  26.  
  27. private:

  28. employee ep;

  29. };

但我们对此还有一个想法:

假设员工对不让放假的处理是抗议,如果员工很在意这份工作且不想007,显然,在辞职的情况下抗议是没有意义的,因为他们已经失去了这份工作了。所以他们需要在失去工作之前,对不让放假进行处理。

也就是说,我们需要对函数运行期间抛出的异常做出反应,以防止影响到后面的操作。对此我们可以在类里添加一个普通函数

 
  1. class manager

  2. {

  3. ...

  4. void holiday()

  5. {

  6. ep.furlough();//如果没有抛出异常,则析构中 if 不会执行

  7. //如果抛出异常,则异常回滚到上级函数中

  8. //我们在上级函数中,holiday 函数之后可以定义一个 try catch 语句对异常进行处理

  9. fangjia = true;

  10. }

  11. ...

  12. ~manager()//析构函数处理异常作为二重保证

  13. {

  14. if(!fangjia)// if fangjia == false

  15. {

  16. try

  17. {

  18. ep.furlough();

  19. }

  20. //用于确保工人在劳碌后总是可以放假

  21. catch(...)

  22. {

  23. //内部实现

  24. }

  25. }

  26. }

  27. ...

  28.  
  29. private:

  30. employee ep;

  31. bool fangjia;//bool 的默认值依赖编译器,前文有提到过,一定要初始化。令 fangjia = false

  32. };

条款09:绝不在构造和析构函数中使用虚函数

众所周知,在类的操作中,父类比子类先构造,而子类也比父类先析构(多态也是如此,多态先通过 virtual 找到子类析构,再析构父类),所以在构造父类的时候,子类对象还未进行初始化,在析构父类的时候,子类已经被销毁。

此时,如果父类的构造和析构函数中有 virtual ,则该函数无法找到子类的地址(或者说无视子类,因为子类被销毁/未被初始化),使程序发生不明确的行为。

所以 virtual 函数的调用无法下降至子类,但是子类可以将必要的构造信息向上传递到父类:

 
  1.  
  2. class teacher{

  3. public:

  4. explicit teacher(int score);//父类的构造

  5. void score_record(const int& score) const;//non-virtual 函数

  6. ......

  7. };

  8. teacher::teacher(const int& score)

  9. {

  10. ......

  11. score_record(score);//构造执行记录分数的操作

  12. }

  13.  
  14.  
  15. class student: public teacher{

  16. public:

  17. student(pare):

  18. teacher(get_score(para))//将信息传入父类的构造函数,使其记录一个分数

  19. {......}

  20. ......

  21. private:

  22. static int get_score(para);//利用一个 static 函数传递分数的值,static 不会传入未初始化的变量

  23. };

条款10:令 operator= 返回一个 reference to *this

释义:让赋值运算符重载版本返回一个自身,以便实现链式编程。

 
  1. class employee{

  2. public:

  3. int m_salary;

  4.  
  5. employee(int a)//有参构造,赋工资初值

  6. {

  7. this->m_salary = a;

  8. }

  9.  
  10. employee& operator=(const employee& ep)

  11. {

  12. this->m_salary = ep.m_salary;

  13. return *this;

  14. }

  15. //返回其本身

  16. };

  17.  
  18.  
  19.  
  20. employee e1(5000);

  21. employee e2(50000);

  22. employee e3(123456);

  23.  
  24. e1 = e2 = e3;

  25. //链式编程

条款11:在 operator= 中处理自我赋值

我们来看一段代码:

 
  1. class person{...};

  2. person p;

  3. p = p;

这是自我赋值,这种操作看起来有点愚蠢,但是并不很难发生。

比如,一个对象可以有很多种别名,客户不经意间让这些别名相等;

或者如之前所说,父类的指针/引用指向子类的对象,也会造成一些自我赋值的问题。

自我赋值往往没有什么意义,还会有不安全性。

 
  1. class student{...};

  2. class teacher

  3. {

  4. ...

  5. private:

  6. student* s;

  7. };

  8.  
  9. teacher& teacher::operator=(const teacher& teach)

  10. {

  11. if(s != NULL)

  12. {

  13. delete s;

  14. s = NULL;

  15. }

  16. s = new student(*teach.s);

  17. return *this;//便于链式操作

  18. }

上述代码是不安全的。如果 *this 和 teach 是同一个对象,那么客户在删除 *this 的时候,也把 teach 删除了,s 就会指向一个被删除的对象。这是不允许的。

我们提供三种方法以解决这个问题:

1、证同检测:

 
  1. teacher& teacher::operator=(const teacher& teach)

  2. {

  3. if (this == &teach)

  4. return *this;

  5. //证同检测

  6.  
  7. if (s != NULL)

  8. {

  9. delete s;

  10. s = NULL;

  11. }

  12. s = new student(*teach.s);

  13. return *this;//便于链式操作

  14. }

遗憾的是,证同检测可以保证自我赋值的安全性,但是不能保证“异常安全性”。即,如果 new student 抛出异常,则 s 就会指向一个被删除的对象,这是一个有害指针,我们无法删除,甚至无法安全读取它。

2、记住原指针:

 
  1. teacher& teacher::operator=(const teacher& teach)

  2. {

  3. student* stu = s; //记住原指针

  4.  
  5. if(s != NULL)

  6. {

  7. delete s;

  8. s = NULL;

  9. }

  10.  
  11. s = new student(*teach.s); //如果抛出异常,s 也可以找回原来地址

  12. delete stu; //删除指针

  13.  
  14. return *this;//便于链式操作

  15. }

3、copy and swap:

 
  1. void swap(const teacher& teach)

  2. {......}

  3.  
  4. teacher& teacher::operator=(const teacher& teach)

  5. {

  6. teacher temp(teach); //拷贝一个副本

  7. swap(temp); //将副本和 *this 交换

  8. return *this;//便于链式操作

  9. }

交换操作不要考虑原本指针内容,可以保证赋值安全性,同时也能保证异常安全性。

条款12:复制对象时勿忘其每一个成分

释义:自定义拷贝函数时,要把类变量写全(子类拷贝不要遗漏父类的变量)。

父类变量通常存储在 private 里,子类不能访问父类 private 对象,所以应该调用父类的构造函数:

 
  1. class animal

  2. {

  3. public:

  4. animal(const animal& an)

  5. {......}

  6. animal& opeartor=(const animal& an)

  7. {......}

  8. ......

  9. private:

  10. string typename;

  11. };

  12.  
  13. class cat: public animal

  14. {

  15. public:

  16. cat(const cat& c);

  17. cat& operator=(const cat& c);

  18.  
  19. private:

  20. string cat_type;

  21. };

  22.  
  23. cat::cat(const cat& c)

  24. :cat_type(c.cat_type),

  25. //为了不遗漏父类变量,调用父类函数

  26. animal(c)

  27. {}

  28.  
  29. cat::cat& operator=(const cat& c)

  30. {

  31. //为了不遗漏父类变量,调用父类函数

  32. animal::operator=(c);

  33. this->cat_type = c.cat_type;

  34. return *this;

  35. }

值得注意的是,上面代码 copy函数和 "="运算符调用的都是和本身一样的函数。究其原因,copy函数是创建一个新的对象,operator= 是对已经初始化的对象进行操作。

我们不能用 copy调用operator=, 以为这相当于用构造函数初始化一个新对象(父类尚未构造好)

同理,也不能用 operator= 调用 copy, 这相当于构造一个已经存在的对象(父类已经存在了)

条款13:以对象管理资源 

所谓资源,就是不再使用它时,将其还给系统。

周所周知,堆区开辟的数据需要程序员手动释放,否则会在程序结束的时候由系统释放。在此前提下,我们来看一段代码:

 
  1. class employee{...};

  2.  
  3. void func()

  4. {

  5. employee* emp = new employee();//动态分配一个对象

  6. ...

  7. delete emp;//手动释放,否则emp跑出 func() 函数,造成资源泄露

  8. }

可以预见,如果在 delete 之前,执行了诸如 return, 抛出异常等等,会导致程序跳过 delete ,使 emp 在堆区开辟的对象未被手动释放,造成资源泄露。(删除的是指针,指针指向的数据没有被删除。)

因为在子函数结束时,其中的类对象会发生析构,所以,我们需要建立一个资源管理类,来防止上述情况的发生。

 
  1. class employee{...};

  2.  
  3. class manager

  4. {

  5. ...

  6. private:

  7. public:

  8. employee* empPrt;

  9. ...

  10.  
  11. manager(employee* emp):

  12. empPrt(emp)

  13. {}

  14.  
  15. ~manager()

  16. {

  17. ...

  18. delete empPrt;

  19. }

  20. };

  21.  
  22.  
  23. employee* createmp()

  24. {...}//在堆区创建一个对象

  25.  
  26. void func()

  27. {

  28. manager m(createmp());

  29. ...

  30. }

利用 manager 资源管理类创建员工对象,在 func 函数结束的时候,manager析构释放了员工对象总结:建立资源管理类—>管理类存储资源的地址变量—>管理类的构造函数为变量初始化,析构函数手动释放变量在堆区开辟的内存。

条款14:在资源管理类中小心 copying 行为

有的时候,对于资源管理类的 copy 是不必要的,比如管理员工类,每个员工都是单独的个体。但在另一些情况下,copy 也是被程序员需要的。

我们对资源管理类中的 copy 有三种常用的处理方式:

1、禁止 copy 行为:当我们不想某个对象被复制时

 
  1. class manager

  2. {

  3. private:

  4. //将copy和operator=设为私有

  5. ...

  6. };

2、深拷贝:当我们想要多个这个对象的复件时

所谓深拷贝就是在堆区重新开辟一个地址,存储和这个对象相同的数据

(相信同学们都熟练掌握了深拷贝)

3、深拷贝之后删除原件:当我们只想要一个这个对象时

补充一点:引用计数法

创建一个对象时,其引用数+1,这个对象指向一个新的对象时,其引用数-1,被指向对象引用数+1,当引用数为 0 的时候,delete 这个对象。

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

管理类存放的是资源的指针,我们无法从管理类直接得到一个资源对象(只能得到一个指针,通过指针找到对象)。所以我们最好用显式转化或者隐式转换(自动类型转换)来得到一个资源对象:

 
  1. class employee{...};

  2.  
  3. class manager

  4. {

  5. ...

  6. private:

  7. public:

  8. employee* e;

  9. ...

  10. employee get() const

  11. {

  12. return *e;

  13. }

  14. //这是显示转化

  15.  
  16. operator employee() const

  17. {

  18. return *e;

  19. }

  20. ...

  21. };

  22.  
  23. manager m(...);

  24. employee emp = m.get();//调用显式

  25.  
  26. employee m1 = m;

  27. //隐式,manager 对象转换成了 employee 对象

条款16:成对使用 new 和 delete 时要采取相同形式

众所周知,数组名表示数组第一个地址

 
  1. string* s = new char[20];

  2. delete s;

所以执行 delete 时,删除的只是第一个对象,后面19个往往不大可能被删除。

对堆区数组的释放可以使用以下方式:

 
  1. string* s = new char[20];

  2. delete[] s;

总结:

new <=> delete

new [] <=> delete[]

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

首先介绍一下什么是智能指针:

C++提供智能指针来方便客户对资源进行管理,相当于一个资源管理类。常见的智能指针有:

tr1::shared_ptr<>

auto_ptr<>

它是一个格式像容器的变量类型:

 
  1. //举例:

  2. tr1::shared_ptr<employee> m(createmp());

  3. manager m(createmp());

  4. //这两个效果差不多

  5. //manager 是自定义的一个资源管理类

以上两个智能指针的主要区别在于 copy 行为上:

 tr1::shared_ptr<>在拷贝上允许深拷贝

auto_ptr<>在拷贝上允许拷贝之后删除原件

我们来看一段代码:

 
  1. int func();//这是一个普通的函数

  2.  
  3. //创建一个函数,调用智能指针

  4. useemployee(tr1::shared_ptr<employee> (new employee), func());

  5.  
  6. //tr1::shared_ptr<employee> (new employee)语句的执行顺序:

  7. //先执行 new employee

  8. //再将 new 的地址存放到 shared_ptr 中

C++对于函数参数的运算顺序有很大的弹性,在其他语言中,是先执行tr1::shared_ptr<employee> (new employee),再执行 func()。

但 C++ 不是,func() 函数可能插在 tr1::shared_ptr<employee> (new employee) 中:

 
  1. //其他语言

  2. new employee

  3. tr1::shared_ptr

  4. func()

  5.  
  6. //C++

  7. new employee

  8. func()

  9. tr1::shared_ptr

这时候,如果 func() 抛出异常之类,则 new 的地址就无法置入 shared_ptr 中,造成资源泄露。

所以,我们需要一条独立语句将 new 置入 tr1::shared_ptr 中:

 
  1. tr1::shared_ptr<employee> emp(new employee);//独立语句存放地址

  2.  
  3. useemployee(emp, func());//调用

条款18:让接口容易被正确使用,不易被误用

1、保证参数一致性:

 
  1. void print_date(int year, int month, int day)

  2. {......}

  3.  
  4. print_date(2022, 28, 9);//1

  5. print_date(2022, 9, 28);//2

在这样一个打印时间的函数接口中,我们按照年月日的顺序输出,但是1式却输出年日月。错误的参数传递顺序造成了接口的误用。

解决办法:

 
  1. class day{...};

  2. class month{...};

  3. class year{...};

  4.  
  5. void pd(const year& y, const month& m, const day& d){...}

  6.  

当然,传递某个有返回值的函数也是可以解决的,但这种方法看起来很奇怪。

2、保证接口行为一致性:

内置数据类型(ints, double...)可以进行加减乘除的操作,STL中不同容器也有相同函数(比如size,都是返回其有多少对象),所以,尽量保证用户自定义接口的行为一致性。

3、如果一个接口必须有什么操作,那么在它外面套一个新类型:

employee* createmp();//其创建的堆对象要求用户必须删除

如果用户忘记使用资源管理类,就有错误使用这个接口的可能,所以必须先下手为强,直接将 createmp() 返回一个资源管理对象,比如智能指针share_ptr 等等:

tr1::share_ptr<employee> createmp();

如此就避免了误用的可能性。

4、有些接口可以定制删除器,就像 STL 容器可以自定义排序,比较函数一样

 
  1. tr1::share_ptr<employee> p(0, my_delete());//error! 0 不是指针

  2.  
  3. tr1::share_ptr<employee> p(static_cast<employee*>(0), my_delete());//定义一个 null 指针

第一个参数是被管理的指针,第二个是自定义删除器。

条款19:设计 class 犹如设计 type

对于每一个 class 都要精心设计,要考虑其构造析构函数,初始化和赋值,继承,类型转换,运算符重载,值传递等问题。

条款20:宁以 pass-by-reference-to-const 替换 pass-by-value

释义:用 const引用传递替换值传递

值传递是新创建一个对象,将这个对象和原对象相等,如果用在类里面,当类中成员变量数目较少的时候,也许问题不大(在类里值传递先调用构造,再调用析构)。但当类中成员变量数目过大时,每一次值传递就会造成时间浪费。

引用传递是生成一个别名指向这个地址,其本身是个指针,无论原对象有多少个成员变量,都能在一瞬间找到某一个。用上const令其成为常量指针,即“只读”。

 
  1. class Number

  2. {

  3. public:

  4. int m_a;

  5. ...

  6. int m_n;//有 n 个变量

  7. ...

  8. };

  9.  
  10.  
  11. void print1_num(Number num);

  12. void print1_num(const Number& num);

  13.  
  14. Number num1;

  15. print1_num(num1);//构造,析构一个Number对象

  16. print2_num(num1);//传地址

此外,值传递在类里还有一个问题:容易造成切割问题。

比如一个子类继续父类,传递子类对象的时候,可能只创建了一个父类的对象,子类部分缺失了:

 
  1. class base_class

  2. {

  3. virtual void func() const;

  4. ...

  5. };

  6.  
  7. class derived_class

  8. {

  9. virtual void func() const;

  10. ...

  11. };

  12.  
  13. void print_class(base_class b);//这是一个打印函数

  14.  
  15. derived_class d;

  16. print_class(d);

当我们把 d 传入后,参数 b 被构造成了一个父类对象,调用 virtual 函数的时候不会调用子类函数。但我们传入的是子类对象。

条款21:必须返回对象时,别妄想返回其 reference

 
  1. class number{

  2. public:

  3. number(int a);

  4.  
  5. const number operator+(const number& n1, const number& n2); //创建一个新对象,返回它

  6. //const number& operator+(const number& n1, const number& n2);

  7.  
  8. private:

  9. int m_value;

  10. };

  11.  
  12. number n1(1);//n1 = 1

  13. number n2(2);//n2 = 2

  14.  
  15. number n3 = n1 + n2;

  16.  

周所周知,return 返回的是一个浅拷贝副本,返回一个对象是没有问题的,但如果返回一个引用,原对象被销毁之后,引用的指向也被销毁了,也就是引用指空,出错。

我们当然可以用 new 解决这个问题,但是当变量数目多的时候,程序员往往不知道怎么使用 delete:

n3 = n1 + n2 + n4 + ...

或许有人想到创建一个 static 对象,但这也是有问题的,我们每次调用都是对同一个 static 操作:

 
  1. const number& operator+(...){static number result;...;return result;}

  2.  
  3. bool operator==(const number& n1, const number& n2);

  4.  
  5. number n1, n2, n3, n4;

  6.  
  7. (n1 + n2) == (n3 + n4);//true

上述对result 进行了两次操作,第一次 n1+n2, 第二次 n3+n4 

注:这不同于链式编程,在上述中,我们并不想改变 n1 或者 n2 的值,否则我们直接返回 n1 或者 n2 的引用就可以了。

总结:虽然返回一个对象需要构造,析构等操作而产生一些代价,但是如果我们不想改变已有的值,就最好不要返回一个引用,而是支付这些代价。(这在时间上会多一点,但创建的对象会随运算符的结束而被销毁。这比“未定义行为(返回一个新建对象的引用)”,“资源泄漏”,“结果出错”要好得多了。)

条款22:将成员变量声明为 private

public:所有都能访问

protected:类对象不可以访问

private:只有类成员函数可以访问

private 优点:

1、使成员变量的控制更为精准:

 
  1. class person

  2. {

  3. public:

  4. void setage(...);

  5. void setname(...);

  6. void setid(...);

  7. private:

  8. int m_age;

  9. string m_name;

  10. int m_id;

  11. };

 用户通过某一函数控制一个私有变量,防止被误用。

2、使类更有封装性:

让我们看看不封装是什么后果:

 
  1. void func1()

  2. {

  3. person p;//创建一个 person 对象

  4. p.m_id;//调用其中一个变量

  5.  
  6. func2();//这个函数也调用了 p.m_id

  7. }

咋一看好像没什么,但如果 func2() 嵌套一个和自己同类型的函数呢?(可以看成递归)

那么 p.m_id 就被无限调用,当代码出错想要修改的时候,想想你的头发,那真是一个灾难。

而封装起来,修改只要改一小部分代码就可以了。

protected不比private有封装性,因为protected子类也可以实现上述代码的操作。

条款23:宁以 non-member, non-friend 替换 member 函数 

释义:如果一个成员函数调用了其他的成员函数,那么就要用一个非成员函数替换这个成员函数。

根据条款22,对类变量的操作只能通过类成员函数实现(因为它是私有变量),那么如果一个成员函数内部实现是调用其他的成员函数,则一个非成员函数也可以做到这样的效果:

 
  1. class preson

  2. {

  3. public:

  4. void func1();

  5. void func2();

  6.  
  7. void func3()

  8. {

  9. func1();

  10. func2();

  11. }

  12. };

  13.  
  14. void use_all(const person& p)

  15. {

  16. p.func1();

  17. p.func2();

  18. }

 func3() 和 use_all() 的效果是一样的,但这时候我们倾向于选择 use_all 函数,因为func3()作为一个成员函数,其本身也是个可以访问私有变量的函数。use_all() 函数其本身不可以访问私有变量。所以 use_all() 比 func3() 更有封装性。(能够访问私有变量的函数越少越好)

在了解这点之后,我们做一些更深层次的探讨:

我们称 use_func()(func3()的非成员函数版本)为便利函数。假设一个类有多个诸如 func1() 的函数,根据排列组合,也就有很多便利函数。为了让这些便利函数和它的类看上去更像一个整体,我们把便利函数和类放在一个 namespace 中。于是,我们可以更为轻松地拓展这些便利函数——多做一些排列组合。

总结若一个成员函数调用其他成员函数,那么这个成员函数的非成员函数版本比之拥有更多的封装性,和机能扩充性

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

举例:有理数类和整数的运算

 
  1. class Rational

  2. {

  3. public:

  4. Rational(int numerator = 0, int denominator = 1)//分子与分母

  5. ...

  6. const Rational operator*(const Rational& right_number) const;

  7. ...

  8. };

  9.  
  10. Rational oneEighth(1, 8);

  11. Rational onehalf(1, 2);

  12. Rational result1 = onehalf * oneEighth;

  13. Rational result2 = onehalf * 2;

  14. Rational result3 = 2 * onehalf;//error!

  15.  

onehalf*2 相当于 onehaf.operator*(2)

首先创建了一个临时对象 const Rational temp(2);

再让两个 Rational 对象运算。

2*onehalf 是 2 调用了operator*。因为 2 是需要被转换的参数,而 2 的位置和 this(调用operator *) 对象的位置是一样的,所以无法将 2 转换成 Rational 类型,也就无法调用 operator* 函数。

解决办法:使用 non-member 函数,让左右参数的地位平等:

 
  1. const Rational operator*(const Rational& left_number, const Rational& right_number)

  2. {...}

总结:如果所有参数(运算符左边或者右边的参数)都需要类型转换,用 non-member 函数。

条款25:考虑写一个不抛异常的 swap 函数 

周所周知,swap 可以交换两个数的值,标准库的 swap 函数是通过拷贝完成这种运算的。想想,如果是交换两个类对象的值,如果类中变量的个数很少,那么 swap 是有一定效率的,但如果变量个数很多呢?

你一定联想到了之前提过的,引用传递替换值传递。没错,交换两个类对象的地址就可以很有效率地完成大量变量的 swap 操作。不幸的是,标准库的 swap 并无交换对象地址的行为,所以我们需要自己写 swap 函数。

 
  1. class person{...};

  2. void my_swap(person& p1, person& p2)

  3. {

  4. swap(p1.ptr, p2.ptr);

  5. }

这个函数无法通过编译,因为类变量是 private,无法通过对象访问。所以要把它变成成员函数。

 
  1. class person

  2. {

  3. public:

  4. void my_swap(person& p)

  5. {

  6. swap(this->ptr, p.ptr);

  7. }

  8. ...

  9. };

如果你觉得 p1.my_swap(p2) 的调用形式太low了,你可以设计一个non-member 函数(如果是在同一个命名空间那就再好不过了),实现swap(p1, p2),这里不做演示。你还可以特化 std 里的 swap 函数:

 
  1. namespace std

  2. {

  3. template<>

  4. void swap<person> (person& p1, person& p2)

  5. {

  6. p1.my_swap(p2);

  7. }

  8. }

值得注意的是,如果你设计的是类模板,而尝试对swap特化,那么会在 std 里发生重载,这是不允许的,因为用户可以特化 std 的模板,但不可以添加新的东西到 std 里。

还有一点:在上面工作全部完成后,如果想使用 swap ,请确定包含一个 using 声明式,一边让 std::swap 可见,然后直接使用 swap。

 
  1. template<class T>

  2. void do_something(T& a, T& b)

  3. {

  4. using std::swap;

  5. ...

  6. swap(a, b);

  7. ...

  8. }

 其中过程:

如果T在其命名空间有专属的 swap,则调用,否则调用 std 的swap。

如果在 std 有特化的 swap,则调用,否则调用一般的 swap。(也即是拷贝)

\\这一点虽然看着很奇怪......

总结:

1、当 std::swap 效率不高时,考虑自定义一个成员函数 swap

2、为成员函数提供非成员函数版本

3、类模板不要特化 swap,类特化 swap

\\4、使用 swap 前要写 std::swap,以便在更多的语境下使用

 

c++

发布于2022-09-24著作权归作者所有

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值