本篇记录一下首次研读Effective C++的知识点和自己的理解,会分条款逐一阐述。
条款01:视C++为一个语言联邦
C++有四个次语言,次语言之间相互协调,当你在编写程序的时候,最有可能运用到的就是次语言之间的相互转换,所以可以分开来学习,熟悉了每一块之后再联合起来使用。
- C:C++任然是以C为基础,区块、语句、预处理器、内置数据类型、数组、指针等都是延续C的传统。
- 面向对象:这部分就是C with Classes。主要包括classes,封装,继承,多态,虚函数等,对这一块的理解是C++的核心所在。
- Template C++:这部分是C++泛型编程,这一部分在实际编程中运用的较多,也是判断一个程序员是否有经验的标准。
- STL:是一个template程序库,他对容器、迭代器、算法以及函数对象有极好的协调,是我们学习C++的好例子之一。
条款02:尽量以const,enum,inline替换 #define
首先说明原因:#define无法创建一个域的专属常量,一旦一个宏被定义,在其后的编译过程中都将有效,除此之外,也不能提供任何封装性。const却可以实现。使用enum的好处在于当你不想让别人获取一个reference或pointer指向你的某个整数常量,你可以用enum,其不能取定义好的对象的地址,这点与#define类似。所以对于单纯常量,最好以const对象或者enums替换define
当用要定义形似函数的宏时,调用其时候有可能会出现无法预料的行为(很容易出错),你可以用template inline来替换宏。
条款03:尽可能使用const
const的语义是指定一个不能被改动的对象。const关键字出现在星号左边,表示被指物是常量,如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。const可以被施加于任何作用域内的对象,函数参数,函数返回类型,成员函数本体。
一个例子,在对于*的操作符重载中,为什么要定义成如下形式?首先将参数定义为const+引用的形式是没有问题的,回传值定义为const是未来降低因客户错误而造成的意外,为了避免(a*b)=c,这样的赋值
const Rational operator*(const Rational& lhs,const Rational& rhs);
如果两个成员函数如果只是常量性不同,可以被重载。但是为了避免const和non-const成员函数中的重复,可以用non-const调用const函数来避免,而反过来调用则是错误的,因为const成员函数承诺不改变其对象的逻辑状态,non-const成员函数却没有这样的承诺。
条款04:确定对象被使用前已经被初始化
因为读取未初始化的值会导致不明确的行为。对于内置类型你必须手工完成(int x=0;),对于内置类型之外的其他对象,就要通过构造函数来进行初始化。原则就是:要确保每一个构造函数都将对象的每一个成员都初始化。
还有要区分赋值和初始化这两个概念。C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前(即通过成员初始化列表进行初始化),并且在初值列中要对所有的成员变量进行初始化,初始列列出的成员变量,其排列次序应该和在class中的声明次序相同。
static对象,其寿命是从构造出来直到程序结束为止。由于c++对于定义在不同编译单元(指的是产出单一目标文件的源码)内的non-local static对象的初始化次序并无明确定义,除此之外,在多线程环境下,其为共享变量。解决方法:把non-local static对象放在一个函数中返回其引用的方式,用local static对象替换non-local static对象,可解决static对象初始化顺序问题。
条款05:了解C++默默编写并调用哪些函数
如果用户没有申明,编译器会自动声明一个copy构造函数,一个拷贝赋值操作符,一个析构函数和一个默认构造函数。如果一个class内有内含reference的成员,或内含const的成员,编译器对其无能为力,因为更改const成员是不合法的,编译器不知道怎么生成默认函数,所以需要自己定义拷贝赋值运算符。
如果某个base class将拷贝赋值运算符声明为private,编译器则拒绝为其derived class生成一个拷贝赋值运算符(因为默认情况下,derived class的拷贝赋值运算符能处理base class的部分,如果base声明为private则没法调用)
条款06:若不想使用编译器自动生成的函数,则该明确拒绝
有时候在一些类中,你并不想让编译器为你自动声明函数,你的做法如下:
- 将拷贝构造函数或拷贝赋值运算符声明为private,但是并不推荐这个做法
- 创造一改基类,在基类中将拷贝构造函数或拷贝赋值运算符声明为private,因此派生类则不会定义新的拷贝构造函数和拷贝赋值运算符
条款07:为多态基类声明virtual析构函数
为什么基类的析构函数要声明为virtual。https://blog.csdn.net/puliao4167/article/details/85060629
条款08:别让异常逃离析构函数
首先要明白,析构函数是绝对不能吐出异常(抛出异常),因为析构函数会传播该异常,会造成很大的麻烦,解决方法
- 用try..catch捕获异常并调用abort()终止程序,“强迫程序结束”
- 吞下异常并记录下来,这个方法也许不是最佳方法,但是也比传播出去好
- 最佳的方法,将可能产生异常的函数的使用权交给用户,然后自己在程序中设置一个双保险。首先让用户的动作调用函数,如果用户没有执行这个动作,这时析构函数再来替用户执行。
条款09:绝不在构造函数和析构函数过程中调用virtual函数
这个问题和类的构造析构顺序有关,如果在基类的构造函数中调用virtual函数。在派生类对象初始化过程中,会先调用基类的构造函数,而此时派生类中继承的虚函数尚未初始化,所以此时调用的是基类的虚函数。这样会造成严重的不确定性。析构函数也是一样。
总之,在派生类对象的基类成员构造期间,对象的类型是基类而不是派生类。所以不要在构造函数或析构函数中调用虚函数。下面例子中,初始化一个派生类对象,却在构造期间调用基类的虚函数,容易造成误解。
#include <iostream>
using namespace std;
class BASE{
public:
BASE(){
cout<<"base con-func\n";
virtual_func();
}
~BASE(){
cout<<"base des-func\n";
virtual_func();
}
virtual void virtual_func() const{
cout<<"base virtual func\n";
}
};
class Derived:public BASE{
public:
Derived(){
cout<<"der con-func\n";
}
~Derived(){
cout<<"der des-func\n";
}
virtual void virtual_func() const{
cout<<"derived virtual func\n";
}
};
int main(int argc, char const *argv[])
{
Derived d;
d.virtual_func();
return 0;
}
条款10:令赋值运算符operator=返回一个*this的引用(为了应付连续赋值)
条款11:在operator=中处理“自我赋值”
在operator=中我们要注意“自我赋值安全性”和“异常安全性”。自我赋值安全性可以用if(this==&other) return *this;来保证;异常安全性就是要注意在动态内存在复制前,别对其删除。也可以用copy and swap技术同时两个的安全性。
条款12:复制对象时勿忘每一个成分
当编写一个拷贝赋值运算符时,请确保复制了所有的local成员变量,调用所有base class内适当的拷贝函数。
除此之外,不要尝试以某个拷贝构造函数调用其拷贝赋值运算符,或者由拷贝赋值运算符函数调用拷贝构造函数,都是没有意义的。如果两函数中有冗余的代码,可以共同放到第三个函数中,共同调用。
条款13:以对象管理资源
RAII(Resource Acquisition Is Initialization,资源获取即初始化),以智能指针为代表,当获得资源后立刻放进管理对象中,通过管理对象的构造函数和析构函数保证资源的初始化和释放。
条款14:在资源管理类中小心copying行为
复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
深拷贝与浅拷贝。简单理解:浅拷贝即拷贝值,深拷贝是拷贝地址。默认的拷贝构造函数都是浅拷贝。但是当类中有动态分配空间时就会出现问题,析构时将删除多次资源。
条款15:在资源管理类中提供对原始资源的访问
最典型的例子就是shared_ptr中的get(),其可以显示转换(返回智能指针内部的原始指针的副本),也可以采用隐式转换(对于用户而言较方便)
条款16:成对使用new和delete时要采取相同形式
如果你在new表达式中使用[ ],必须在相应的delete表达式中也是用[ ]。如果你在new表达式中不使用[ ],一定不要在相应的delete表达式中使用[ ]。
当使用new时候,会发生两件事:1.内存被分配处理 2.针对此内存会调用一个或多个构造函数。当使用delete时,也会有两件事:1. 针对此内存会有一个或多个析构函数调用 2.然后内存才会被释放。
我们使用两种对应的new-delete就是为了在释放内存空间时候,告诉其删除的是一个对象还是数组,如果是数组的话会先读取内存中的数组大小,然后多次调用析构函数,如果只是单一对象就直接析构即可。
条款17:以独立语句将newed对象置入智能指针
条款20:宁以pass-by-reference-to-const替换pass-by-value
首先传递引用的方式会使得效率高很多,因为没有任何构造函数或析构函数被调用,也没有任何新对象被创建,而以by value的方式传值则会多次构造函数和析构函数调用。
除此之外,传引用还可以避免对象切割问题(在一个函数中,如果是传值,当一个派生类对象实参传递到一个基类对象行参中,只会保留其基类对象,派生类对象特性被切割),如果用传引用则不会,因为reference底层是以指针来实现的。
条款21:必须返回对象时,别妄想返回其引用
这个条款说的其实是不能返回引用指向某个local对象,无论local对象是在stack中(函数结束便自动销毁)或者new出来在heap中(new出来的如果传出函数没有delete),都不能返回其引用。
条款22:将成员变量声明为private(封装性)
条款23:宁以non-member、non-friend替换member函数(也是为了提供更好封装)
条款24:若所有参数皆需类型转换,请采用non-member函数
以operator*为例,当其要满足类对象和一个int相乘时候,需要构建non-member函数,且利用构造函数隐式类型转换,即可变成两个类对象之间的操作。
未完待续...