Effective C++学习笔记(条款1-34)
1. 开场白
《Effective C++》这本书一直觉得有些难度,已经反复看了好几次了,每次看都能发现一些以前没有注意到的知识点。建议在看这本书前先看看《C++编程思想》或者是《C++ Primer》,另外,如果看过一些《设计模式》或者《敏捷设计开发》的会稍微好些,书中一些条款涉及了设计模式,虽然并不是在讲设计模式,但是一些设计模式中的思维模式影响了这些条款的内容。
最后,我不知道是我个人语文水平不行的原因,还是说侯捷大大翻译的原因。我总觉得这本书翻译的有些拗口,不像我以前读《STL源码剖析》时候那么有感觉,我后面也读了《More Effective C++》,也是侯捷大大翻译的,感觉理解起来也是很吃力,一些语句得停下来一点点的抠才知道他说的是什么意思。额,不提这些了,接下来就整理一些条款的心得体会。
2. 记录
条款1. 视C++为联邦语言
条款2. 尽量以const,enum,line替换#define
- #define没有作用域的概念,也就是说namespace和class的作用域限定对于#define定义的宏是不起作用的,可以看下面的这个代码。
namespace Foo {
#define TEMP "hello world"
}class Base {
#define NAME "Base"
};int main() {cout << NAME << TEMP << endl;return 1;
}
- #define在预处理过程中处理,不利于调试。#define一旦宏展开,宏名称再也找不到了,那调试的时候并不会告诉我们是宏定义出现了错误。例如下面的这个例子,:
#define MAX(a, b) (a > b ? a : b)int main() {int a = 1, b = 1;cout << MAX(++a, b) << endl;return 1;
}
这里的++a被替换了两次,所以宏展开以后的结果是:(++a > b ? ++a : b),这样一看就知道有什么问题了,但是没有调试的时候却很不容易发现这种问题。
条款3. 尽可能使用const
- 它修饰的对象是谁?
- 它的作用域范围?
- 它的语义是什么?
const int a = 1;
const static int b = 2;
class Base {
public:const string &getName(const string &index) const {const int tmp = 10;}const static string name = "Base";const int idx;const int * const foo;
};
使用const的理由有:
- 避免作为等式左值,虽然稍微熟悉点的程序员都会避免这么做,但是不排除在写条件语句的时候不小心将“==”错误的输入成了“=”,当然我也见过初学者直接将这个作为函数左值的情况出现。
- 告诉调用者以及自己,使用const修饰的这个对象希望被保护起来,我们不愿意其被修改。虽然在这些情况下不使用const也是可以的,但是问题就在于总有那么些人和自己捣乱,与其一次次的和他们说这里不能动,那里不能改,不如事先用const修饰一下,然后其用的时候就知道“哦,这里用const修饰了,我不应该去改它”。
条款4. 确定对象使用前已被初始化
- 如果对一个类对象说初始化,意思主要是指这个类已经“准备完毕”,可以正常工作,那这里的准备完毕就有很多含义了,比如tcp客户端的套接字的“准备完毕”--可能指的就是已经与服务器端完成链接,也已经完成服务器端的地址信息配置,也可能是指已经套接字已经申请完成,这里的初始化语义就很丰富了,总体来说,只要这个对象在后面的代码中可以正常调用了,那就是已经初始化完成了;
- 但是对一个对象内部的成员属性的初始化定义就十分精细:在成员初始化列表中完成初始化过程。
- 对于类对象内部的成员属性的初始化,第一个需要注意的就是构造函数内部的过程并不是”初始化“,初始化这时候已经完成,确切的初始化应该是在初始化成员列表中的初始化;第二个需要注意的就是,内置对象(int、char、long、double等)的初始化值是不明确的,根据具体的编译器相关,所以最好在初始化列表中进行初始化,对于引用,const变量只能在初始化列表中进行初始化,对于是一个类对象的成员属性,如果不希望调用默认构造函数,那也需要在初始化列表中初始化(为了保证效率);第三个需要注意的就是初始化的顺序问题,基类首先被初始化(如果存在的话,如果是多重继承,那先初始化虚继承,然后再依次初始化),然后就是依据声明的次序依次初始化,如果初始化列表中存在定义,则调用初始化列表中的方法进行初始化,如果没有在初始化列表中初始化,那对于基本类型就根据编译器而定,类对象则调用其默认构造函数进行初始化(如果存在的话,如果不存在就必须在初始化列表中初始化)。
- 第二种情况是类对象相互依赖的情况,作者想说明情况是使用”单例模式“保证获取的一个静态变量是被初始化的。
条款5. 了解C++默认编写并调用那些函数
- C++默认为我们生产了默认构造函数、拷贝构造函数、赋值操作符、析构函数这4个函数,第一个注意的是,这些方法都是非虚的,条款7中就讨论了非虚对析构函数造成了什么影响,然后就是构造函数不能声明为虚函数。第二个需要注意的就是,虽然C++中如果不使用public和protected修饰的属性和方法都默认为private的,但是唯独这4个方法,除非显示使用private修饰,否则它们都是public的。
class Base {
public:// 以下方法是默认提供的Base() {}Base(const Base &b) {}~Base() {}Base &operator=(const Base &b) {}
};
- 然后需要注意的就是,什么时候调用哪个方法,这样我们在初始化的时候才能心中有数,可以参考下面的几种写法,清晰的知道自己正在做什么:
int main() {Base b1; // 默认构造函数Base b2 = b1; // 拷贝构造函数Base b3(b1); // 拷贝构造函数Base b4; // 默认构造函数b4 = b1; // 赋值操作符Base *b5 = new Base; // 默认构造函数Base *b6 = new Base(b1); // 拷贝构造函数delete b6; // 析构函数Base *b7 = (Base *)malloc(sizeof(Base)); // 不会调用构造函数free(b7); // 不会调用析构函数return 1;
}
条款6. 若不想使用编译器自动生成函数,就应该明确拒绝
条款7. 为多态基类声明virtual析构函数
class Base {};
class Derive : public Base {};int main() {Base *b = new Derive;delete b;return 1;
}
如果出现上面这种情况就容易发生局部析构的危险,当然在上面这个例子中是不会出现局部析构的危险了,但是如果在Derive中有一个指针指向了堆中的一块内存,此时如果仅仅调用Base类中的析构函数,则就不会执行这个Derive中的析构,这块内存就泄露了。这就是它最大的一个危害。
class Base {
public:virtual ~Base() {} // 虚析构函数,确保了可以调用派生类中的析构
};
class Derive : public Base {};int main() {Base *b = new Derive;delete b;return 1;
}
那什么时候需要使用virtual析构函数就很明显了:只要这个类有可能被作为基类而存在,那就它的析构函数必须是virtual的。当然书中也给出了一个相对容易判断的准则:只要出现了virtual关键字修饰的方法,就必须将析构设置为virtual的(这个道理很容易解释,因为我只要设计了virtual函数,那我就是希望可以有一个派生类继承这个类,所以析构就需要为virtual的了)。
class Base {
public:virtual ~Base() = 0;
};
Base::~Base() {}class Derive : public Base {};int main() {Base *b = new Derive;delete b;return 1;
}
条款8. 别让异常逃离析构
条款9. 绝不在构造和析构过程中调用virtual函数
条款10. 另optertor=返回一个reference to *this
条款11. 在operator=中处理自我赋值
int main() {Base b;b = b;return 1;
}
最容易出现的一种意外情况就是:
class Base {
public:Base &operator=(const Base &in) {delete name;name = new string(*in.name);return *this;}private:string *name;
};
一旦出现自我赋值的情况下,这个代码必然出现问题了。
条款12. 复制对象时勿忘记其每一个成分
条款13. 以对象管理资源
- 智能指针的生命周期:这直接决定了我们可以在哪里申请我们的智能指针。
- 对象的生命周期:这个直接决定了我们希望使用哪种类型智能指针。
- 智能指针管理下的对象生命周期:不同的智能指针管理的对象的生命周期是不一样的,虽然智能指针被析构了,但是它所管理的对象不一定会被释放,所以弄清楚这点很重要。
- 指针使用的注意事项:这个和相关智能指针的实现相关,智能指针在使用的时候总有各种各样的限制,如不能在容器中使用auto_ptr,auto_ptr无法进行值传递操作,shared_ptr无法解决环状引用问题,auto_ptr和shared_ptr不能用于管理数组对象等等。要弄明白为什么会有这些限制,看看这些智能指针内部的机制或许是一个更好的选择。
- 对象在函数内部分配(当然必须是在堆上分配),但是函数尾部不释放而是希望将其传出给外部(工厂方法就是常见的一种情况),这时候可以考虑使用shared_ptr指针。
- 对象在函数内部分配(当然必须是在堆上分配),但是希望这个对象在函数返回的时候自动释放,这种时候可以考虑使用auto_ptr,使用auto_ptr还有一个好处,就是即使函数内部存在异常抛出的情况,这个对象也可以自动的被析构。
- 在类中的成员属性,但是这个属性成员指向的数据是被多个多想共享的(例如数据库连接),那可以考虑使用shared_ptr。
条款14. 在资源管理中小心copying行为
条款15. 在资源管理类中提供对原始资源的访问
条款16. 成对使用new和delete是要采取相同型式
条款17. 以独立语句将newed对象置入智能指针
条款18. 让接口容易被正确使用,不易被误用
“接口”这个术语在不同的场景中表示的含义也有一些微妙的区别,条款18中的接口的定义和条款34中对于接口的定义是不一样的,前者侧重描述一个函数方法的参数与返回值的设计,而后者则侧重于由多个函数方法封装后的整体。
条款18的目标就是要告诉我们如何去设计一个函数——除去函数内部逻辑部分以外——输入参数、输出参数、函数名(一个函数其实就是由这三部分+访问权限+函数逻辑构成),最后这个条款得到的结论就是:
- 函数名:保持函数名的一致性,也就是说具有相同功能的函数最好命名是一致的,这样设计的理由就是让别人(包括自己)用起来不会因为命名混乱而错误使用。
- 输入参数:语义清晰、尽可能保证参数不会被错误调用、保证传入参数的效率。做到这点并不容易,如要求语义清晰,我们可以使用精确的命名,让调用者可以直接明白参数的含义,也可以利用const关键字标识传输参数是否会被修改;此外要保证参数不会被错误调用,比如如果输入的整数是特定的几个值,就可以利用枚举实现;要保证传输参数的效率,那就是最好采用引用的方式,这样可以避免1次的构造和析构操作。此外还有很多,比如采用默认参数,告诉调用者某些参数是可选的;
- 返回值:返回值也很重要,如果希望返回一个函数内部生成的对象,那就要想办法避免用户忘记释放内存而造成内存泄露,一种比较好的方法就是利用shared_ptr指针。
个人的感觉就是要设计一个好的接口(函数)其实挺难的,要考虑的事情还是相对比较多。
条款19. 设计class犹如设计type
条款20. 宁以pass-by-reference-to-const替换pass-by-value
- 对于内置对象(int、char、long、double等),如果不希望在函数内部进行修改,那就直接使用值传递的方式,它的效率反而比引用传递的效率更好(更确切的说是在函数内部访问的时候效率更高);如果希望在函数内部对其进行修改,那使用引用传递,但是不要加入const描述,这就是涉及形参和实参的区别了。
- 对于类对象,使用引用传递的好处就是它传递的只是一个指针(在系统看来引用和指针就是一个东西,对程序员会存在一些语义上的区别),这种方式避免了传入时调用类的拷贝构造函数,以及在函数返回时调用析构函数。至于加入const关键字是为了告诉外部人员,我内部不会对这个类对象实例进行任何修改。
- 而且使用值传递的方式传递还有一点很重要的就是它可以避免“对象切割”问题,这个问题也很很好理解,就是因为函数向上转型导致了派生类中的内容被切割了。
- 最后还有一个需要注意的就是,对于STL这样的容器而言,其还是使用值传递的方式,所以平时得知道自己每次调用容器到底执行了哪些操作了。
条款21. 必须返回对象时,别妄想返回其reference
条款22. 将成员变量声明为private
条款23. 宁以non-member、non-friend替换member函数
条款24. 若所有参数皆需类型转换,请为此采用non-member函数
条款25. 考虑写出一个不抛出异常的swap函数
条款26. 尽可能延后变量定义式的出现时间
- 将变量的定义延迟到使用前一刻
- 合理评估构造与析构的时间,例如书上给出的例子,方法A需要1次构造+1次析构+n次赋值,方法B需要n次构造+n次析构,个人感觉方法A在大多数情况下比方法B效率要高,当然也不排除例外了(书上说有可能B更好,我是想不出来了)
// 方法A
Base b;
for (int i = 0; i < n; i++) b = ...// 方法B
for (int i = 0; i < n; i++)Base b = ...
条款27. 尽量少做转型动作
- C风格的转型:适合内置对象类型和指针
- const_cast:去除类型的常量性
- dynamic_cast:向下类型转型
- reinterpret_cast:低级转型,一般不用,因为不具有平台的可移植性
- static_cast:类似C风格的转型
- 第一点就是要根据需要选择合理的转型方式,例如内置对象类型可以使用static_cast,去除常量性使用const_cast等。
- 第二点就是尽可能保持风格统一,这个是为了便于代码阅读而提出的,所以如果决定采用C++风格的转型,那就尽量不用C风格的转型了。
条款28. 避免返回handles指向对象内部成分
- 因为只要让外部可以获取到内部对象的一个“句柄”,那我们就无法预估外部会做出什么样的操作,即使加入const进行修饰,他们也可能通过很多手段绕过const的限制,进行对内部成分的修改。
- 第二种危害就是生命周期获取会不同,对象内部成分的生命周期很可能比外部希望使用的时间要短,那一旦内部的对象死亡了,外部却无法获取到内部对象死亡的信息。
条款29. 为“异常安全”而努力是值得的
条款30. 透彻了解inline的里里外外
条款31. 将文件间的编译依存关系降至最低
class Base {
public:Base(); // 这个是声明int key; // 这个是声明void fun() { // 这个是定义,并且隐含内联cout << "hello world\n";}
};Base::Base(){ // 这个是定义cout << "base" << endl;
}
为什么需要区分这么详细呢?我们一步步来看,我们将Base的声明以及定义放在Base.h文件中。
#ifndef BASE_H
#define BASE_Hclass Base {
public:void fun();
};void Base::fun() {cout << "hello world";
}#endif // BASE_H
然后是main函数的代码:
#include <iostream>
using namespace std;#include "Base.h"int main() {Base b;b.fun();return 1;
}
这时候执行编译,可以得到如下的编译输出:
g++ -c -g -frtti -fexceptions -mthreads -Wall -DUNICODE -DQT_LARGEFILE_SUPPORT -I'../test' -I'.' -I'c:/QtSDK/Desktop/Qt/4.8.1/mingw/mkspecs/win32-g++' -o debug/main.o ../test/main.cpp
g++ -Wl,-subsystem,console -mthreads -o debug/test.exe debug/main.o
如果对编译工具链有所了解,就知道这里编译的时候使用 -I 用于指定Base.h的目录,这样在编译的时候导入Base.h文件。
#include "Base.h"
#include <iostream>void Base::fun() {std::cout << "hello world";
}
这时候重新编译,编译信息就完全不一样了。
g++ -c -g -frtti -fexceptions -mthreads -Wall -DUNICODE -DQT_LARGEFILE_SUPPORT -I'../test' -I'.' -I'c:/QtSDK/Desktop/Qt/4.8.1/mingw/mkspecs/win32-g++' -o debug/main.o ../test/main.cpp
g++ -c -g -frtti -fexceptions -mthreads -Wall -DUNICODE -DQT_LARGEFILE_SUPPORT -I'../test' -I'.' -I'c:/QtSDK/Desktop/Qt/4.8.1/mingw/mkspecs/win32-g++' -o debug/Base.o ../test/Base.cpp
g++ -Wl,-subsystem,console -mthreads -o debug/test.exe debug/main.o debug/Base.o
这时候首先会编译生成Base.o文件,最后再进行链接。
条款32. 确定你的public继承塑模出is-a关系
条款33. 避免遮掩继承而来的名称
- 情况1、基类中的方法是非虚的,但是在派生类中覆写了这个方法
class Base {
public:void name() { cout << "base" << endl; }
};class Derive : public Base {
public:void name() { cout << "derive" << endl; }
};int main() {Base *base = new Derive;base->name(); // 其实我们是希望获取derive的名字return 1;
}
我们使用基类指针指向一个派生类的对象实例,我们希望调用name方法的时候调用的其实是派生类中的name方法。但是很不幸,这时候调用的却是基类中的name方法。
- 情况2、派生类中重新声明基类中含有的成员属性
class Base {
public:int key;Base() : key(100) {}
};class Derive : public Base {
public:int key;Derive() : key(200) {}
};int main() {Base *base = new Derive;cout << base->key << endl; // 其实我们是希望获取derive中的keyreturn 1;
}
这时候,我们或许会认为这里的key值应该是200,但是很不幸结果却是100。我们不妨看看这个派生类的大小:
int main() {cout << sizeof(Derive) << endl; // 8return 1;
}
可以得到派生类大小为8,基类和派生类的int各占4字节,但是最后获取的是哪个key值,则需要看指向key的指针是Base还是Derive类型的了。
- 情况3、 在派生类中重载基类中的同名方法(这里不关心基类中同名方法是否为虚方法)
class Base {
public:virtual void name() { cout << "base" << endl; } // 这里用不用virtual关键字都要出问题
};class Derive : public Base {
public:void name(int) { cout << "derive" << endl; }
};int main() {Derive *base = new Derive;base->name(); // 这里编译就不通过了return 1;
}
- 情况4、 基类中函数如果出现重载,在派生类中也要将所有重载方法覆写,否则下面的这种调用就无效了
class Base {
public:virtual void name() { cout << "base" << endl; }virtual void name(int) { cout << "base.int\n"; }
};class Derive : public Base {
public:void name(int) { cout << "derive" << endl; }
};int main() {Derive *base = new Derive;base->name(); // 这里编译就不通过了return 1;
}
class Derive : public Base {
public:using Base::name; // 这个关键,让Derive可以见到Base中的所有name方法void name(int) { cout << "derive" << endl; }
};
整理一下,关于,上述的几种情况想说的就是:
- 不要在派生类中声明与基类中同名的成员变量
- 不要覆写基类中任何非虚方法
- 派生类中如果要覆写基类中的虚方法,最好将该方法的所有重载覆写
条款34. 区分接口继承和实现继承
- 纯虚函数:表明派生类必须重新实现该方法,基类中不提供相应实现
- 虚函数:表明基类提供了默认实现,但是派生类中可以覆写它,进行自定义
- 非虚函数:表明基类不希望派生类对其进行覆写,原因参考条款33
条款35. 考虑virtual函数以外的其它选择
条款36.绝不重新定义继承而来的non-virtual函数
条款37. 绝不重新定义继承而来的缺省参数值
条款38. 通过复合塑模出has-a或“根据某物实现出”
条款39. 明智而审慎地使用private继承
条款40. 明智而审慎的使用多重继承
Effective C++学习笔记(条款1-34)相关推荐
- Effective c++学习笔记条款20:宁以 pass-by-reference-to-const替换pass-by-value
Prefer pass-by-reference-to-const to pass-by-value 这个问题在在C++是非常常见的.传值和传引用巨大的差别在于你所使用的参数是其本身还 ...
- Effective C++ 学习笔记 条款05 了解C++默默编写并调用了哪些函数
当写下一个空类时,编译器会为你合成一个拷贝构造函数.一个拷贝赋值运算符.一个析构函数,如没有声明其他的构造函数,编译器会合成一个默认构造函数.这些都是inline的public成员. 当类有一个引用成 ...
- Effective C++ 学习笔记 第七章:模板与泛型编程
第一章见 Effective C++ 学习笔记 第一章:让自己习惯 C++ 第二章见 Effective C++ 学习笔记 第二章:构造.析构.赋值运算 第三章见 Effective C++ 学习笔记 ...
- Java:Effective java学习笔记之 考虑实现Comparable 接口
Java 考虑实现Comparable 接口 考虑实现Comparable 接口 1.Comparable接口 2.为什么要考虑实现Comparable接口 3.compareTo 方法的通用约定 4 ...
- 《Effective C++》学习笔记——条款26
***************************************转载请注明出处:http://blog.csdn.net/lttree************************** ...
- 《Effective C++》学习笔记——条款45
七.模板与泛型编程 条款45:运用成员函数模板接受所有兼容类型 让智能指针隐式转换 智能指针: 它是"行为像指针"的对象,并提供指针没有的机能. 真实指针的优点: 支持隐式转换,比 ...
- 【Effective C++ 学习笔记】
条款02:尽量以const,enum,inline替换 #define #define定义的常量也许从未被编译器看见,也许在编译器开始处理源码之前它就被预处理器移走了: #define不重视作用域,不 ...
- Effective C++学习笔记(Part Five:Item 26-31)
2019独角兽企业重金招聘Python工程师标准>>> 最近终于把effectvie C++仔细的阅读了一边,很惊叹C++的威力与魅力.最近会把最近的读书心得与读书笔记记于此, ...
- Effective C++学习笔记——构造/析构/拷贝运算
条款9:决不再构造和析构过程中调用virtual函数,包括通过函数间接调用virtual函数. 应用:想在一个继承体系中,一个derived class被创建时,某个调用(例如生成相应的日志log)会 ...
- Effective C++学习笔记(Part One:Item 1-4)
2019独角兽企业重金招聘Python工程师标准>>> 最近终于把effectvie C++仔细的阅读了一边,很惊叹C++的威力与魅力.最近会把最近的读书心得与读书笔记记于此,必备查 ...
最新文章
- 赠票 | 来智源大会,聆听张钹院士、Michael I. Jordan等大咖分享!
- 获取SQL Server 2000数据库和表空间使用信息
- react native报错:Expected a component class,got[object object]
- Android8.0适配那点事(二)
- 计算机系统配置有哪些,查看电脑配置方法有哪些
- 怎样关闭vivo的HTML查看器,vivo安全模式在哪儿关闭?
- 软件测试学习视频 分享
- app页面html制作工具,app页面设计制作软件(最好用的6款设计软件)
- stc单片机id加密c语言,STC单片机使用加密芯片SMEC98SP的加密实例源码
- (SCI分区)查SCI期刊JCR分区的图解步骤
- 轻量级pdf查看阅读工具Sumatra PDF
- word2013、word2016、word2019标题序号变黑色竖线解决方法
- 放大镜原理分析及jquery实现
- 行为型设计模式(二)
- VCC、 VDD、VEE、VSS 电压理解
- SQLyog 64位破解版 v12.09
- 机器学习入门:准备知识笔记(pandas)之一
- 运营App渠道推广中,如何统计推广效果?
- 【转载·SCTP协议】浅析 - SCTP协议
- 百度图像识别 API
热门文章
- C# Spire.Pdf 无限制 使用教程
- 题解 P2919 【[USACO08NOV]守护农场Guarding the Farm】
- 武汉市查询社保电脑号及公积金账号的方法(湖北省其他市也适用)
- securecrt 终端VIM配色
- Mac OS X 平台有哪些优秀应用可以将视频转成 GIF?Mac视频转gif软件推荐
- GPS在ROS中的测试和使用
- Android中给图片加边框
- 推荐10 个短小却超实用的 JavaScript 代码段
- html图片指定refere,前端解决第三方图片防盗链的办法 - html referrer 访问图片资源 403 问题...
- 【中间件技术】第四部分 Web Service规范(10) Web Service规范