《Effective C++》读书笔记一
1、C++中std是什么意思?
摘自:https://blog.****.net/calvin_zhou/article/details/78440145
在程序中像vector ,cout,这类东西都是在std内,有时会忽略std::,你得自己认清
2、
3、尽量以Const、enum、inline替换#define
举例:定义常量可以用define方式
#define ASPECT_RATIO 1.653
不好之处:
ASPECT_RATIO无类型,在进行预处理阶段只有ASPECT_RATIO被替换成1.653的过程,不会进行类型安全检查
改成:
const double AspectRatio = 1.653
好处:
AspectRatio有类型double,在编译阶段进行类型安全检查
如果定义常量指针,记得*前后都要加const
const char* const authorName = "Scott Meyers";
用inline函数代替宏函数:
#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))
改成(至于原因有点复杂,查看https://blog.****.net/u013517637/article/details/51154422)
template<typename T>
inline void callWithMax(const T& a,const T& b)
{
f(a>b?a:b);
}
建议传参采用const 引用形式,提高效率
最后请记住:
1、对于单纯常量,最好以const对象或enums替换#defines
2、对于形似函数的宏,最好改用inline函数替换#define
总结:
有了consts, enums、inlines 我们对预处理器(特别是#define)的需求降低了,但并非完全消除,#include仍然是必需品,#ifdef/#ifndef也继续扮演控制编译的重要角色。
最后了解下预处理器和编译器:
从源代码到获取到可执行程序大致流程如下所示:
Step1:源代码(source code)
Step2:预处理器(preprocessor)
Step3:编译器(compiler)
Step4:目标代码(object code)
Step5:链接器(Linker)
Step6:可执行文件(executables)
预处理器:
早于 编译器。
工作内容:删除注释、包含(include)其他文件以及宏替换等
常用的预处理有:
#include 功能:包含头文件、#if 功能:条件、#else 功能:否则、#elif 功能:否则如果、#endif 功能:结束条件、#ifdef 功能如果定义了一个符号,就执行操作,等效#if !defined、#define 功能:定义一个符号、#undef 功能:删除一个符号
编译器
工作时间:晚于预处理器
工作任务:语法分析、语义分析等,最后生成目标文件
摘自:https://blog.****.net/u012421852/article/details/51167668?utm_source=copy
4、尽可能使用const
(1)const修饰函数(函数内不能修改成员的值且不能调用非const函数)
int get() const
{
//get函数返回i的值,不需要对i进行修改,则可以用const修饰。防止在函数体内对i进行修改。
return i;
}
将成员函数设为const又分为两个概念:
第一种:bitwise constness,认为成员函数设为const,那么不能改变对象的任何成员变量(static除外)
第二种:logical constness,一个const成员函数可以修改它所处理的对象的某些bits,但要保证在用户使用中侦测不出。
对于第一种bitwise constness,有个特殊情况,虽然将operator[]声明为const成员函数,如下:
但是外部可以通过指针指向的值可以改变,与常量对象矛盾,如下:
解决方法:
可使用mutable关键字解除bitwise constness,例如 mutable char *mPointer;
在程序中:
(2)const修饰函数参数(在函数内不能修改指针所指内容,起到保护作用)
void fun(const char * src, char * des){ //保护源字符串不被修改,若修改src则编译出错。
strcpy(des,src);
}
(3)如果参数是引用,为了避免函数通过引用修改实参的内部数据成员,加const来保护实参
void h(const A & a){
}
(4)const修饰函数返回值
也是用const来修饰返回的指针或引用,保护指针指向的内容或引用的内容不被修改,也常用于运算符重载。归根究底就是使得函数调用表达式不能作为左值。
#include <iostream>
using namespace std;
class A
{
private:
int i;
public:
A(){i=0;}
int & get()
{
return i;
}
};
void main()
{
A a;
cout<<a.get()<<endl; //数据成员值为0
a.get()=1; //尝试修改a对象的数据成员为1,而且是用函数调用表达式作为左值。
cout<<a.get()<<endl; //数据成员真的被改为1了,返回指针的情况也可以修改成员i的值,所以为了安全起见最好在返回值加上const,使得函数调用表达式不能作为左值
}
摘自:https://blog.****.net/my_mao/article/details/22872149
最后请记住:
5、确定对象被使用前已被初始化
(1)对于变量初始化:
一定在声明的时候先初始化
(2)对于构造函数初始化:
不好:
不好之处:c++规定,对象的成员变量初始化动作发生在进入构造函数本体之前
好:
好的地方:通过初始化列表,效率比赋值更高,且注意,初始化列表的成员变量排列次序和他们在类中声明的次序相同
5、知道c++默认调用了哪些函数
如果写一个空类,c++默认会声明一个默认构造函数、拷贝构造函数和一个析构函数,这些函数都是public且inline
因此你写下:
class Empty{};
等同于写下:
只有当这些函数被调用,才会被编译器创建出来,下面代码造成每一个函数被编译器产出:
对于拷贝构造函数:
类NamedObject的构造函数这样:
(1)如果调用了拷贝构造函数:
在将第一个参数no1的string传给no2时,会调用string的拷贝构造函数并以"Smallest Prime Number"为实参,第二个参数类型int,所以会拷贝no1中value的每一个bits完成初始化
(2)如果调用赋值操作:
编译器拒绝执行,因为引用没法赋值,无法改变,编译器拒绝编译
NamedObject<int> p("Persephone", 2);
NamedObject<int> s("Satch", 36);
p=s;
6、为多态基类的析构函数声明为virtual
父类析构函数如果不设为virtual,那么下列程序
CEmployee *oper = new COperator() ; //CEmployee是父类,COperator是子类
delete oper;
执行顺序为:
父类构造->子类构造->父类析构
后果:子类析构没有调用,内存泄露
如果父类析构加virtual,子类析构不加virtual,执行顺序为:
父类构造->子类构造->子类析构->父类析构
注意点:
1、既不能将所有类析构函数都声明为virtual,因为Point class内含virtual函数,对象体积会增加百分之50-100,也不能所有类析构都不声明为virtual
2、给基类一个virtual析构函数只适用于带多态性质的基类
3、如果类带有任何virtual函数,析构函数就应该设为virtual
4、如果该类设计不是作为基类使用,或不是为了具备多态性,析构函数不该设为virtual
7、别让异常逃离析构函数
8、在构造和析构期间不要调用virtual函数,因为调用从不下降至子类
9、在赋值操作符operator= 返回一个*this,类型为对象本身
10、在赋值操作符operator= 中处理“自我赋值”
11、复制对象勿忘其每一个成分
不对做法:
正确做法:
12、以对象管理资源
1、创建了一个资源后立即交给管理manager类管理
2、管理对象运用析构函数确保资源被释放
13、成对使用new和delete采用相同的形式
14、用传const 引用代替传值
通过对象传值的后果:
15、返回对象时,别返回对象的引用
方式一:如果返回一个引用
friend const Rational& operator* (const Rational& lhs,const Rational& rhs);
那么这个返回的引用在两个数相乘*之前就存在,也即调用该函数返回的对象就存在,这不合理。
方式二:
那么该对象存在栈上,是个局部变量,在函数退出前就销毁了
方式三:
new出来对象在堆上,不仅付出了一个调用构造函数的代价,同时有一个疑问:谁来对这个new出来的对象Delete?如果
这里x*y*z调用两次operate*函数,也即new了两个对象,但是只能用一个对象接受,第二个对象无法delete,导致绝对的内存泄露。
正确写法:
虽然得牺牲返回值的构造和析构成本,但是值得的,返回的是一个对象的值
16、将成员变量声明为private
为什么要将成员变量声明为private而不是public:
1. 能够提供语法一致性,写代码的时候能够马上区分要不要加函数括号;
2. 提供更好的访问控制,只读、只写还是可读可写,都容易控制;
3. 封装,减少与外界接触的机会,方便修改和维护。
为什么protected不行?
如果不存在继承关系,protected的作用与private等同,除了本类之外,其他类或者类外都不能访问,但如果存在继承关系时,protected标识的成员变量,在它的子类中仍可以直接访问,所以封装性就会受到冲击。这时如果更改父类的功能,相应子类涉及到的代码也要修改,这就麻烦了。而如果使用private标识,则根据规则,所有private标识的变量或者函数根本不被继承的,这样就可以保护父类的封装性了,仍可以在子类中通过父类的成员函数进行访问。
书上说了这样一句话:“从封装的角度观之,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。
最后总结一下:
1. 切记将成员变量声明为private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性
2. protected并不比public更具封装性
17、尽可能延后变量定义的出现时间
只要定义了一个变量且类型带有一个构造和析构函数,那么程序到达这个变量定义式时,便要承受构造成本,这个变量离开作用域后,便要承受析构成本
因此将这句话放在抛异常之后,然而仍然不够好,string encrypted;没有设初值会调用默认构造函数,但是通过默认构造函数构造对象然后赋值要比直接在构造时指定初值效率低,
因此将
改为
如果在循环中:
18、尽量少做转型动作
旧式类型转换:
c++风格新式转型:
const_cast被用作将const去除
dynamic_cast用作向下转型,决定某对象是否归属继承中的某个类型
static_cast将int转为double,将一种指针转为另一种