zoukankan      html  css  js  c++  java
  • 一、让自己习惯C++

    写在前面

    第一遍看《Effective C++》时,在准备暑期实习生的招聘,没有时间好好地捋一下,将一些要点记录下来。现在实习回来,重读此书,并记录一些要点,为今后的复习亦或是学习铺垫。

    这篇介绍第一章的4个条款。

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

    1. C++是一个多重范型编程语言:
    • 支持过程形式
    • 支持面向对象形式
    • 支持函数/泛型形式
    • 支持元编程形式
    1. 理解C++,必须首先认识其主要的次语言:
    • C语言。 C++ 以C为基础,区块,语句,预处理器,内置数据类型,数组,指针等全部都来自于C。C++是C的高级解法揭露了C语言的局限性:
      • 没有模板。
      • 没有异常处理。
      • 没有重载。
      • ...(以及封装,继承等面向对象的特性等其它)
    • Object-Oriented C++: 简单总结就是面向对象的特点。
      • 类。每个类都有构造函数,析构函数。
      • 封装,继承,多态,虚函数(动态绑定).
    • Template C++: C++范型编程的部分。有了模板可以带来崭新的编程范型。
    • STL: Standard Template Library。对容器,迭代器,算法以及函数对象的规约有极佳的机密配合与协调。
    1. 对于内置类型,pass-by-value通常比pass-by-reference更加高效,但是对于用户自定义(user-defined)类型,由于构造函数和析构函数的存在,pass-by-reference-to-const往往更好。

    作者总结:

    C++的高效编程守则视状况而变化,取决于你使用C++的哪一部分。

    个人总结:

    此条款介绍了C++的组成部分,在开发过程中,要高效利用C++的这几个“次语言”特性,在使用不同的“次语言”的时候,要选择相应的高效的编程方式。

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

    首先要明白使用#define的缺点:

    • 如#define NUM 1.2,调试的时候出现的是1.2而不是NUM,如果这个宏定义不是自己写的就更难定位问题了。

    • 宏定义#define MAX(a,b) ((a) > (b) ? (a) : (b))看似可行的一个宏定义函数,但是考虑以下情况:

      int a = 5,b = 0;
      MAX(a++,b); // a被累加两次
      MAX(++a,b + 10); // a被累加一次
      a的累加次数取决于a和b的大小,显然不是调用者所期待的情形。

    class的专属常量

    假定我们在GamePlayer类中有个常量成员,有个数组,数组大小使用该常量表示。

    class GamePlayer
    {
    public:
        static const int iNum = 5;  //常量声明式
        int iScores[iNum];
        。。。              // 其它成员
    }
    

    要明确一点:上述const是一个声明式,并不是一个定义式。

    为什么要声明为static?

    如果不是static,在该类还未构造时,iNum是不存在的,编译器也就无法知道数组iScores的大小。编译器会坚持要求知道数组的大小。

    旧式的编译器中,不允许static在声明的时候不允许被赋初值。如果不支持声明时候赋初值,就应该改为:

    class GamePlayer
    {
    public:
        static const int iNum;
        ...
    }
    

    在函数体外再赋初值:

    int GamePlayer::iNum = 5;
    

    但是采取这种写法就无法在类中定义一个常量大小的数组。

    采用enum解决

    声明enum常量,就可以防止不同编译器对const能否赋初值所带来的不便之处。

    class GamePlayer
    {
    public:
        enum { iNum = 5 };
        int iScores[iNum];
    }
    

    使用enum的更多好处

    enum声明的常量是一个右值。如果不想别人用一个pointer或者reference指向一个整型常量,使用enum即可。引用和指针都无法绑定在一个枚举常量上。

    enum
    {
    	first = 1,
    };
    //int &First = first;   无法通过编译
    //int *pFirst = &first; 无法通过编译
    

    作者总结

    对于单纯常量,最好以const对象或enum替换#define.

    对于形似函数的宏,最好改用inline function替换#define.

    条款03:尽可能使用const

    先写一下老生常谈的const和pointer的不同组合的效果。

    char hello[] = "hell0";
    char *p1 = hello;               // non-const pointer,non-const data
    const char *p2 = hello;         // non-const pointer,const data
    char* const p3 = hello;         // const pointer,non-const data
    const char* const p = hello;    // const pointer,const data
    

    初学者不容易记住,其实只要记住const后面是什么(数据类型不看),什么就不变就对了。

    比如说,const char *p,const 后面是 *p, p是指针所指向的数据,所以是data不变。又比如 char const p; const后面是p,p是一个指针,所以是个const pointer,指针指向的地址不能改变。

    由此延申出const在STL迭代器中的使用

    假设我们用一个迭代器指针去操作一个vector容器:
    如果用const显式修饰:

    vector<int> vct(10,1);
    const vector<int>::iterator it = vct.begin(); // 此迭代器指针是一个non-const data,const pointer类型。
    *it = 9;    // 正确。
    ++it;       // 错误,const pointer不能改变指向的位置。
    

    上述代码中,const 修饰的迭代器指向的地址不可变,所以只能指向vct.begin()位置。

    此外,STL的迭代器中有一个const_iterator,是一个non-const pointer,const data类型的迭代器。

    vector<int> vct(10,1);
    vector<int>::const_iterator cIt;
    *cIt = 10;      // 错误,const data,不可改变其值。
    ++cIt;          // 正确。non-const pointer.可以改变其指向。
    

    令函数返回一个常量值可以降低错误发生的概率

    const Rational operator *(const Rational &lhs, const Rational &rhs);
    

    如果返回的不是const,那么很可能写成

    if(a * b = c)
    

    这个是程序员写错的时候的情形,如果返回const那么就会提示报错,就能立即定位错误。而如果不是const类型,那么这个错误就可能很难被发现。

    const修饰成员函数

    const修饰的成员函数,在函数体内不能修改任何一个成员变量。如果是可能被修改的成员变量,那么这些成员变量应该是使用mutable关键词来修饰。mutable关键词可以去掉non-static成员变量的bitwise constness约束

    注意:两个成员函数的常量性不同,是可以被重载的。

    例如:

    class TextBlock
    {
    public:
        const char &operator[](std::size_t position) const
        {
            ... // 记录数据1
            ... // 记录数据2
            ... // 记录数据3
            return text[position];
        }
        char & operator[](std::size_t position)
        {
            ... // 记录数据1
            ... // 记录数据2
            ... // 记录数据3
            return text[position];
        }
    }
    

    如果这两个版本实现了相同的函数体,只是返回值的常量性不同,那么可以将non-const版本改成以下版本:

    char & operator[](std::size_t position)
    {
        return const_cast<char &>(
        static_cast<const TextBlock&>(*this)
        [position])
        );
    }
    

    这语句有两个转型的动作:

    (1) static_cast<const TextBlock&>.将当前对象转成const的对象。

    (2) const_cast<char &>是去掉const属性,恢复成原来的非const对象。

    我们重载了[]运算符,const和非const版本都有。当对象为const 属性的时候调用的是const版本,非const属性对象就调用非const版本。

    这样写可以避免代码冗余。

    注意:只能用非const去调用const,如果使用const去调用非const,那么就先要将const属性去掉,那么原本const函数体中的数据就不被保证不会被修改,也就失去了我们一开始使用const修饰的初衷。

    作者总结

    将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于在任何作用域内的对象,函数参数,函数返回类型,成员函数本体。

    编译器强制实施bitwise constness,但你编写程序的时候应该使用“概念上的常量性”。

    当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

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

    先搞清楚赋值和初始化是不一样的:
    假设有一个类ABEntry:

    class ABEntry
    {
    public:
        ABEntry(const string name,const string addr);
    private:
        string theName;
        string theAddr;
    }
    

    以下的构造函数中是赋值给私有成员变量,而不是初始化私有成员变量。

    ABEntry::ABEntry(const string name,const string addr)
    {
        theName = name;     // 赋值
        theAddr = addr;     // 赋值
    }
    

    真正的初始化:

    ABEntry::ABEntry(const string name,const string addr)
    :theName(name),theAddr(addr)
    {
        
    }
    

    二者的不同:
    第一个构造函数中:

    (1) 在赋值之前,theName和theAddr先执行了它们各自的默认构造函数,也就是string类中的默认构造函数,有了一个初值(为空)。

    (2) 进行赋值的时候,调用了copy assignment操作符。将name和addr复制给theName和theAddr.

    所以它充其量只是一个赋值,并不能说是初始化,第一小步就已经初始化成一个空string了。

    而在第二个构造函数中,只调用了一个copy构造函数去构造初始值。《C++ Primer》中将这种初始化方式叫做成员列表初始化。

    单单使用一个copy构造函数显然是比较高效的。在内置类型中,不需要调用默认构造函数,二者的效率是差不多的。

    const和reference初始化

    由于const和reference一定需要初值,而不能被赋值改变,所以需要采用成员列表初始化的方式来进行初始化操作。

    成员变量的初始化顺序

    在C++中,成员变量的初始化顺序严格遵守变量的声明顺序。

    class Text
    {
    public:
        ...
    private:
        string strAddr;
        string strName;
        int iCall;
    }
    

    在上述类之中,如果采用成员列表初始化,那么初始化顺序依此为strAddr,strName,iCall.如果需要使用strName的值去初始化strAddr, 那么是错误的做法,因为strAddr先于strName初始化,strName这个时候尚未有值。

    作者总结:

    为内置型对象进行手工初始化,因为C++不保证初始化它们。
    构造函数最好使用成员初始列,而不要在构造函数本体内使用赋值操作,初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
    为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

  • 相关阅读:
    Java 多线程系列02
    Java 多线程系列01
    java io流03 字符流
    java JDBC系列
    java io流02 字节流
    Helidon使用心得
    camel 解析
    Spring 源码分析
    java代码实现分页功能
    SpringBoot Tomcat启动报错
  • 原文地址:https://www.cnblogs.com/love-jelly-pig/p/9612936.html
Copyright © 2011-2022 走看看