Effective C++(上):Scott Meyers著

目录

Accustoming Yourself to C++:

条款01:视C++为一个联邦

条款02:尽量以const,enum,inline替换#define

条款03:尽可能使用const

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

Constructors,Destructors,and Assignment Operators:

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

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

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

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

条款09:绝不在构造和析构过程中调用virtual函数

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

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

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

Resource Management:

条款13:以对象管理资源

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

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

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

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


前言

        在深入探讨Effective C++前,请允许我先向还未学习或深入学习C++的同学介绍一下:该书适合以及了解C++基本语法,清楚类间的关系,virtual,lambda表达式,模板元编程,STL容器,多重继承MI,智能指针以及RTTI等知识点的同学学习。我自认为该篇文章是真正入门C++必读的一本书,这本书没有太多深入知识点,但是每一个条款都可能让你对C++有着全新的认识,所以为了真正入门C++,进一步了解C++,所以我写了该篇文章,巩固基础的同时也是当作我课余生活的一部分乐趣。

        本文章将各个条款统一整理总结,将原书生硬的翻译尽量通俗易懂的进行讲解,并且将各个条款尽量分为四部分,第一部分:为什么?第二部分:不怎么做会怎么样?第三部分:代码,第三部分:总结。通过这四步让你更了解每个条款存在的理由,以及直观的看到不遵守条款会造成什么后果,很多同学认为学习条款或规范可以遵守可以不遵守,我觉得这个想法是十分危险的,就形如C++中的内存泄露,遵守条款可能不能让你避免所有的内存泄露,但是能避免大多数情况下的内存泄露以及未定义行为。本文章或多或少可能存在错误,还望各位读者指出,受教了。


Accustoming Yourself to C++:

  条款01:视C++为一个联邦

        C++每一个模块都相互串通,基本的int类型定义在类中会有,基本的类型声明在模板中会存在,模板的嵌套和使用在STL容器源码中会出现。也可以单独将C++分为四个子语言,面向对象,模板等不同编程风格和技术组成,这就是为什么要将C++视为一个联邦?只为更好的对知识点串通,也为了更好的将对应的技术进行区分

#include<iostream>
using namespace std;

int main(){
    cout << "Hello World" << endl;
    return 0;
}

        总结:学习C++中的各个知识点,将它们视为联邦,因为每一个知识点之间都相互串通,但每个技术都有对应的编程风格


  条款02:尽量以const,enum,inline替换#define

        在阅读本条款之前,我将确定你熟悉const,enum等语法。对于#define宏定义我需要补充以下几点:        

                1.记号表:对于使用#define宏定义的标识符,会被记录到编译器的记号表中,后续调用该标识符时将在符号表中调用对应的值

                2.#undef:由于#define并不在乎作用域,一旦标识符被定义,那么除非在某处使用#undef取消定义,否则后续代码中都可以调用,对此失去了一定的封装性

        为什么尽量以const,enum和inline替换#define?对此有以下理由:

                1.使用const定义常量替换#define定义标识符,会产生较小的目标码

                2.为了在类内对使用static修饰的变量进行初始化,可以使用enum替换static,还不需要担心使用#define宏定义导致其失去封装性

class A {
private:
    static int value_A;     //static修饰的变量只能类外初始化
};
int A::value_A = 10;

class B {
private:
    #define value_B 10;     //宏定义变量导致失去封装性
};

                3.使用#deinf只是对所定义的值进行替换,会产生不可预料的行为

#define CALL_MAX(a,b) (a > b ? a : b);

inline void fun(int a,int b) { (a > b) ? a : b; }

int main() {
    int a_1 = 5, a_2 = 5, b = 0;
    CALL_MAX(++a_1, b);    //调用#define宏定义的函数,这里a会自加两次
    cout << "a_1" << a_1;
    fun(++a_2, b);         //调用inline定义的函数,这里a会自加一次
    cout << "a_2" << a_2;
    return 0;
}

         总结:对于单纯的常量,可以使用const和enum替换#define。对于函数的宏,改用inline替换#define


  条款03:尽可能使用const

        对于const关键字,如何确定指针为const还是指向的值为const,我有一个口诀:左定值,右定向,const定义不变量

const char* Ptr = Wild_Pointer;
//指针Ptr指向的值为const

char* const Ptr = Wild_Pointer; 
//指针Ptr为const

const char* const Ptr = Wild_Pointer; 
//指针Ptr指向的地址不变,地址所存储的值不变

        如何对const修饰的变量或参数进行修改?有以下两种方法:

                1.使用const_cast<>将指定的const指针转换为非const(只能用于指针)

                2.对变量使用mutable关键字修饰,即可在const函数中进行修改

        为什么尽可能使用const?对此有以下理由:

                1.使用const定义变量能防止if判断条件中出现赋值行为(这个理由给我的感觉很emmm...)

const int A = 10;
const int B = 20;
if (A = B) {
    //报错!表达式必须为可修改的左值
    //出现const变量被赋值为非const对象
}

                2.使用const可以对相同返回值,变量数相同,类型相同的函数进行重载,编译器会根据传入的实参调用最匹配的函数

                3.类中的const函数能访问所有成员变量,但是只能访问const成员函数

class A {
public:
    void fun_1() const { 
        cout << "void fun_1() const" << endl;
        fun_2(); 
    }
    void fun_2() const { 
        cout << "void fun_2() const" << endl;
        fun_3();    //报错!fun_3函数并不是const函数
    }
    void fun_3() { cout << "void fun_3()" << endl; }
};

        总结:使用const能预防赋值行为,能对函数进行重载,能对函数施加更多的性质,但是使用时应注意隐式转换


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

        为什么确定对象被使用前已被初始化?对此有以下理由:

                1.对于变量,未初始化的对象不能被调用(部分编译器不会指出该错误)。对于指针,未初始化的指针为野指针,可能指向的地址并不是你所需要的地址,也可能指向一个脏数据

                2.对于类内变量,应尽可能的使用初始化列表(也叫初成员初值列),因为C++不保证初始化它们

        总结:对于不同的对象,要使用之前都需要确保已被初始化


Constructors,Destructors,and Assignment Operators:

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

        对于本条款所述的内容,在已经了解C++基本语法后,应该明白构造函数,析构函数,基类的构造和析构函数和赋值构造函数是C++默认编写的函数,以及C++会在何时调用这些函数,对此我希望补充一点:

        针对类的成员中存在引用变量时,编译器不会提供默认的赋值构造函数

class class_A{
private:
    int value;
    int& rederencs = value;
};

int main() {
    class_A B;
    class_A C;
    C = B;      
    //报错!因为类的成员中存在引用变量时,编译器不会提供默认的赋值构造函数
    return 0;
}

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

        阅读本条款之前,你应该清楚哪一些函数会因为未定义,将使C++提供默认的该函数,对此本条款将着重对复制构造函数和赋值重载运算符进行分析。

        如何明确拒绝不想使用编译器自动生成的函数?对此有以下做法:

                1.针对复制构造函数和赋值重载运算符,我们可以在类中将其声明为私有方法,并不对其提供定义

class A {
private:
    A(const A& Object);     //这是一个复制构造函数,尽可能的使用const
    A& operator=(const A& Object);  //这是一个赋值重载运算符
    //以上两个函数只有声明,未提供定义是会将链接期报错提升至编译期
};

         总结:大多数使用C++的同学对复制构造函数和赋值重载运算符的使用理解太浅,但是不能忽视它们是导致内存泄漏的其中一个原因。对此,如果我们不想使用或防止使用时未意识到调用的是复制构造函数和赋值重载运算符,我们可以在类中将其声明为私有方法,并不对其提供定义(未定义的原因是时报错被更早的发现:链接期的报错提升至编译期发现)


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

        为什么要为多态基类声明virtual析构函数?对此有以下理由:

                1.如果基类未将析构函数声明为virtual,将会导致指向子类的基类指针被delete时,只会调用基类的析构函数,而子类多余的内存将未被析构,最后导致内存泄漏

class Base {
public:
    Base() { std::cout << "Base 构造函数" << std::endl; }
    ~Base() { std::cout << "Base 析构函数" << std::endl; }    //未声明为virtual
};

class Derived : public Base {
public:
    Derived() { std::cout << "Derived 构造函数" << std::endl; }
    ~Derived() { std::cout << "Derived 析构函数" << std::endl; }
};

int main() {
    Base* base = new Derived();    //基类指针指向子类对象
    delete base;    //delete基类指针
    //只会调用Base析构函数,并不会调用Derived析构函数
    return 0;
}

        总结:通常析构函数未被声明为virtual的类,不推荐定义为基类。对此有一个一举两得的方法,就是声明虚基类。将析构函数声明为纯虚函数,这样就不会导致内存泄漏。

class Base {
public:
    Base() { std::cout << "Base 构造函数" << std::endl; }
    virtual ~Base() =0 { std::cout << "Base 析构函数" << std::endl; } 
};

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

        在阅读本条款之前,我将认为你以及对C++的异常有所了解,嗯?只是了解try,catch和throw关键字?我觉得你更应该清除当程序中抛出一个异常时,会暂停当前的执行流程,并查找一个匹配的异常处理代码块来处理该异常。如果没有找到匹配的异常处理代码块,程序会调用默认的异常处理函数,并终止程序的执行。但是请允许我对你提个醒:对于C++而言,两个异常同时存在的情况下,会导致程序结束执行或不明确行为(如果程序中同时存在两个异常,那么编译器和运行环境可能会选择其中一个异常进行处理,而忽略另一个异常。这种情况下,程序的行为就会变得不确定,可能会导致程序崩溃或者产生错误的结果)

        为什么别让异常逃离析构函数?对此有以下理由:

                1.对于C++而言,两个异常同时存在的情况下,会导致程序结束执行或不明确行为(条款前以及说了,但是这正是别让异常逃离析构函数的重要原因)

        什么时候异常会存在两个或两个以上?我们应该如何做才能防止异常在析构函数内捕获?

                1.什么时候异常会存在两个或两个以上?假如有一个类A,类A的析构函数会抛出异常,但是异常并不使用try和catch进行捕获,对此该异常将被传播。此时你声明了一个vector容器,容器的元素含有多个类A对象,该vector容器被析构时将依次调用类A的析构函数,此时便会存在多个异常,且这些异常将被传播

                2.如何做才能防止异常在析构函数内被捕获?这很简单,是时候使用try和catch关键词对其异常进行捕获,防止逃出析构函数

        总结:对于可能要抛出异常的析构函数,应使用try和catch关键字对异常进行捕获,防止传播。如果要对异常进行反应和操作,应该在析构函数外提供函数对其进行操作,而不是在析构函数中。


  条款09:绝不在构造和析构过程中调用virtual函数

        为什么绝不在构造和析构过程中调用virtual函数?有以下理由:

                1.在类的继承中,声明一个子类对象其基类构造函数最先被调用,若其构造函数中存在virtual函数,将会调用基类的virtual函数而不是子类对应的函数,难以理解?请看代码示例

class Base {
public:
    void fun(){ std::cout << "Base fun()" << std::endl; }
    Base() { fun(); }
    
};

class Derived : public Base {
public:
    virtual void fun(){ std::cout << "Derived  fun()" << std::endl; }
    Derived() { fun(); }
};

int main(){
    Derived Object;
    //先调用Base的构造函数,而Base构造函数将调用fun()
    //在Derived中fun()为virtual函数,你可能会认为此时将调用Derived中的fun()
    //但是这里将调用Base的fun()函数,这会背离你的想法
    return 0;
}

                 2.对象在子类构造函数开始执行之前不会成为一个子类对象(在子类对象的基类构造期间,对象类型是基类而不是子类)

        总结:这不仅仅是对构造和析构函数的要求,而对于它们所调用的所有函数应该也遵守这一约束


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

        本条款并不会造成什么影响,只是将operator=返回值设置为引用可以连续调用该函数,不懂连续调用函数?看看下面的例子:

int A,B,C;
A = B = C = 1202;

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

        本条款只是针对operator=进行约束,防止白痴行为的发生(就好像所有规范的产生总是因为一些白痴的天马行空的操作而产生的,我想你应该理解我的这句话)

        为什么在operator=要防止自我赋值?有以下理由:

                1.自我赋值可能会导致多个对象指向同一个地址,假如该对象是指针,再假如你将某个指针delete后还有指针指向该指针指向的地址,这样会造成未定义行为

                2.确定如何函数如果接受的形参为一个以上的对象,而其中多个对象是同一个对象时,其函数内的行为仍然正确

        总结:在operator=中防止自我赋值是必要的,最起码不会导致未定义行为。什么?你问我要如何防止自我赋值?加上一个if判断就好啦!

int& operator=(const int& Object){
    if (this == &Object){    //防止自我赋值
        return *this;
    }
}

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

        本条款,emmm。。。感觉这是常识,在对类进行复制或者调用重载赋值运算符时,应该要把类中的所有成员将其复制,不遵守该条款可能会导致其未定义行为或对象未初始化等错误(对象调用先得确保已被初始化)


Resource Management:

  条款13:以对象管理资源

        在阅读本条款之前,请允许我想你介绍RAII思想:RAII是C++中的一种资源管理的技术,其核心思想是在对象的构造函数中获取资源,而在对象的析构函数中释放资源。为什么要引入以对象管理资源的想法?我想很大概率是因为对象没有被析构而导致内存泄漏(听说大名鼎鼎的某m路由器还得定时重启防止内存泄漏)

        以对象管理资源是指什么?有以下两种解释:

                1.在获得对象获得资源后立刻放进管理对象内

                2.管理对象运用析构函数确保资源的是否

        PS:通俗来说,就是在构造函数中对类成员进行初始化,在析构函数中对类成员进行释放内  存等操作

        要实现以对象管理资源要怎么做?有以下方式:

                1.使用shared_ptr或auto_ptr智能指针,确保最后一个对象被析构,释放内存(通常使用shared_ptr智能指针,因为当存在复制操作时会导致auto_ptr指向nullptr)

        总结:本条款皆在让你清楚的意识到要在构造函数中对成员进行初始化,在析构函数中对成员进行delete,也提供了解决方法:使用shared_ptr或auto_ptr智能指针,实现以对象管理资源的操作


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

        在阅读本条款时,请允许我再对资源管理类进行讲解:资源管理类可以是一个自定义的class,用于管理程序中使用的资源,但因为满足构造函数中初始成员,析构函数中delete成员,故称为资源管理类

        为什么要在资源管理类中小心copying行为?有以下理由:

                1.资源管理类通常对成员的定义要求特别高,不允许成员出现副本,限制资源管理类中的成员单独一份(不太理解?好比如一个类中有两个对象,这两个对象都有单独的工资成员,一天某个对象的该成员被另一个对象不小心覆盖,那么原本是代表两个人不一样的工资,最后导致两个人工资都一样了)

                2.为什么要定义资源管理类?资源管理类用于管理程序中使用的资源,例如动态分配的内存、文件句柄、网络连接等。资源管理类的主要目的是确保资源在使用后被正确地释放,以避免资源泄漏和程序崩溃等问题

        怎么对资源管理类中的copying行为进行有效的限制?有以下操作: 

                1.源管理类进行禁止复制,如何禁止复制?将其copy函数定义为私有的且未定义的

                2.对资源管理类所管理的资源进行引用计数(类似于shared_ptr智能指针,我想你应该清楚它的工作方式,这里就不再赘述了)

                3.对资源管理类所管理的资源进行拥有权的转移(类似于auto_ptr智能指针,这里我也不再对其赘述)

                4.对资源管理类进行深度复制(深复制需要new一块新的内存,而浅复制不需要)

        PS:原书中所提到互斥器对象(Mutex),对应的解释是:标准库中的Mutex类用于实现互斥器的对象,类提供了锁定和解锁互斥器的操作,防止多个线程同时访问同一个资源,以避免数据的竞争和不一致状态

        总结:明白资源管理类的作用,理解四个对资源管理类中的copying行为的限制:禁止复制,进行引用计数,进行拥有权转移,深度复制


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

        在阅读本条款之前,如果你还不清楚什么是原始资源?请听我解释:原始资源就是资源管理类中的成员

        为什么要对资源管理类中提供对原始资源的访问?有以下理由:

                1.通常在使用函数或接口时,都需要传入参数,该参数可能会要求传入的实参为原始资源

        如何对资源管理类中提供原始资源访问?有以下操作:

                1.提供显式的函数返回资源管理类的原始资源

class A{    //假设这是一个资源管理类
private:
    int value;    //原始资源
public:
    int get_Value(){ return value;}    //返回原始资源
}

                2.对资源管理类提供隐式转换函数,但通常不这么做,会导致虚吊的对象(何为虚吊的对象?指针A指向指针B,指针B指向一块内存地址,当该地址被delete后,指针A成为虚吊的指针)

class Base{    //假设这是一个资源管理类
private:
    int value;    //原始资源
public:
    operator Base(){ return value;}    //隐式转换函数
}

         总结:对资源管理提供显示的函数还是提供隐式转换函数返回原始资源,取决于个人。隐式转换函数会产生虚吊对象,但是方便。显示函数能确切表达编写代码的意思,但是并不方便(你也不想每次获取原始资源的时候都写一遍函数名吧?)

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

        本条款很简单,对于学习了C++基础语法的同学来说。如果你调用new时使用[],那你必须在对应调用的delete中也使用[],如果你调用new时没有使用[],那么也不该在对应调用delete中使用[]

void fun_1(){
    int* p = new int[10];  // 使用[]动态分配一个包含 10 个整数的数组
    delete[] p;  // 使用[]释放动态分配的数组内存
}

void fun_2(){
     int* p = new int;  // 没有使用[]动态分配一个整数
    delete p;  // 没有使用[]释放动态分配的单个整数内存
}

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

        为什么使用独立语句将newed对象置于智能指针中?理由很简单:防止内存泄漏,不理解?请看代码:

int fun_1();
void fun_2(shared_ptr<widget>(new widget),fun_1());

//在使shared_ptr初始化之前,必须先new widget
//假如存在执行顺序为new widget,其次fun_1(),最后初始化shared_ptr
//其中fun_1()调用异常,就会导致new widget的地址未使shared_ptr初始化
//而且new widget指针会遗失,最后导致内存泄漏

        总结:对于智能指针的初始化,请将它单独设为一条语句,这样能防止内存泄漏。不这样做,产生异常会导致难以察觉的内存泄漏


        结尾

        以上便是《Effective C++(上)》的全部内容,内容很简单。无需多想,也无需多虑,有什么问题大多数都是我总结的不够清楚,或思维不够严谨,如果存在跟我想法有差异的同学也可以在评论区指出。请允许我有部分私心,编写该文章主要是自己巩固知识,也方便后续查阅浏览。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wild_Pointer.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值