DJの小站

记录生活,发现自己

0%

Effective C++ 从C到Cpp

一、将C++视为一个语言联邦

C++ ≈ {C, C with class, Template C++, STL},对于内置类型而言,Cpp采用以值传递的方式更高效,而对于用户自定义类型而言,一般采用以常引用传递的方式更好(这其中的成因与底层指令相关,所以学一学汇编是很重要的),在使用CPP哪个部分时,遵循那个部分。

二、尽量不要使用C中的define语句

尽量不要使用define,尽管define很高效,但他只是忠实地替换了原始代码,会造成很多问题比如:

  1. 定义#define ABC 114514时,由于编译器在预处理阶段移走了ABC而用114514替代,之前ABC的标记已经没有了,若是出了问题,打印的错误信息很可能是error: 114514……,而不是error ABC...,在带项目中难以定位,建议用const int ABC = 114514替代,出错也好debug

  2. 考虑使用宏:

    1
    2
    3
    4
    #define max(a, b) (a) > (b) ? (a) : (b)
    int a = 5, b = 0;
    max(++a, b); //a会自增两次
    max(++a, b + 10); //a自增一次

    这显然非常扯淡,一般而言,使用inline函数模板代替宏,更安全,更省心

    tips: 内联函数与宏的区别

    1、内联函数在编译时展开,而宏在预编译时展开

    2、在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。

    3、内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。

    4、宏不是函数,而inline是函数

    5、宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函数不会出现二义性。

    6、inline可以不展开,宏一定要展开。因为inline指示对编译器来说,只是一个建议,编译器可以选择忽略该建议,不对该函数进行展开。

    7、宏定义在形式上类似于一个函数,但在使用它时,仅仅只是做预处理器符号表中的简单替换,因此它不能进行参数有效性的检测,也就不能享受C++编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。

  3. 想在编译期使用宏定义类似的功能,可使用enum hack大法,enum的行为很像加了限制区间的宏定义

三、尽可能多地使用const:

区别const int *int * const,前者修饰的是指向的东西,后者修饰的是指针

对于STL迭代器,其规则与主语言又有所不同:

1
2
const vector<int>::iterator iter;	//相当于int * const,迭代器指向不能改变
vector<int>::const_iterator citer; //相当于const int *,不能通过迭代器修改指向的元素

对于运算符重载,最好将运算数声明为const-reference,并在重载时,考虑与基本类型运算符行为保持一致。

const对象只能调用const成员函数,这是符合const语义(不修改)的,同理,const成员函数内部只能访问成员变量,但无法修改,也无法访问普通成员函数(普通成员函数可能修改成员变量)

  1. 将一些函数,变量加const可帮助编译器检查错误用法

  2. const和non-const版本的函数除了返回以外无差别时,可使用强转令non-const调用const

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const char& operator[](std::size_t position) const{
    ........
    }

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

四、确定对象使用前被初始化:

  1. 为确保对象的行为如你预计般地可靠,必须确保每个对象使用前被初始化

  2. 对于成员对象,通常采用初始化列表的形式构造:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class People
    class Society{
    private:
    People p;
    int nums;
    const string rule;
    public:
    Society(const People& tp, const string& s, int n):
    p(tp),
    rule(s)
    {nums = n};
    }

    通常,写为初始化列表的成员对象直接调用自己的构造函数进行构造,而采若用赋值的方式,其行为往往是使用default构造函数构造成员对象,再使用重载的”=”进行赋值。

    类型为引用和常量的成员变量必须使用初始化列表,不能采用赋值的形式

    对于一些初始化和赋值表现一样好的成员,可考虑写一个init函数,供多个不同的构造函数使用

  3. 对于non-local static对象,即多个文件(作用域,对象……)共享的静态对象,由于我们无法确定初始化顺序,而在一个对象未被初始化前调用它是很危险的,所以我们一般采用单例模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class test{
    private:
    test(){}
    public:
    test& get_test(){
    static test t;
    return t;
    }
    }