Effective C++读书笔记

本篇记录一下首次研读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:若不想使用编译器自动生成的函数,则该明确拒绝

    有时候在一些类中,你并不想让编译器为你自动声明函数,你的做法如下:

  1. 将拷贝构造函数或拷贝赋值运算符声明为private,但是并不推荐这个做法
  2. 创造一改基类,在基类中将拷贝构造函数或拷贝赋值运算符声明为private,因此派生类则不会定义新的拷贝构造函数和拷贝赋值运算符

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

    为什么基类的析构函数要声明为virtual。https://blog.csdn.net/puliao4167/article/details/85060629

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

    首先要明白,析构函数是绝对不能吐出异常(抛出异常),因为析构函数会传播该异常,会造成很大的麻烦,解决方法

  1. 用try..catch捕获异常并调用abort()终止程序,“强迫程序结束”
  2. 吞下异常并记录下来,这个方法也许不是最佳方法,但是也比传播出去好
  3. 最佳的方法,将可能产生异常的函数的使用权交给用户,然后自己在程序中设置一个双保险。首先让用户的动作调用函数,如果用户没有执行这个动作,这时析构函数再来替用户执行。 

条款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函数,且利用构造函数隐式类型转换,即可变成两个类对象之间的操作。

 

未完待续...

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值