c++ effective总结(一)

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

  c++可以认为是由C,Object-Oriented C++(面向对象),Template C++(模板),STL(c++标准模板库)四种次语言组成的。

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

  c++中推荐使用其他的方法替换一些宏定义操作,如常量定义,推荐使用const int MAX = 10 替换#define MAX 10。而对于#define定义的函数,也推荐使用inline(内联函数)替换。另外如在类中要声明成员属性的值。必须声明成静态常量,或者使用enum。如

class Stack{
public:
    ...
private:
    static const int size = 10; // 或者 enum { size = 10 };
    int members[size];
}

条款三:尽可能使用const

  const在c++里面可以说是用的非常多的一个修饰词,const可以修饰值,指针,函数,函数参数,函数返回值等。

  如const char* p = "hello"; 指针指向的值为常量。char* const p = "hello";指向值的指针为常量。可以记为const在*左边值为常量,const在*右边指针为常量。如果*两边都有const,则值和指针都为常量。

  当你在使用stl容器时,使用迭代器访问容器中的元素,而又不希望通过迭代器修改容器中的值,可以使用const_iterator,或者是遍历常量容器对象,使用const_iterator。

  const修饰成员函数,该成员函数不可以修改成员属性值,另外const常用来传递的引用参数,当你不希望修改引用参数中的内容时,可以用const修饰。

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

  为内置对象进行手工初始化,因为c++不保证初始化它们,即你想初始化一个int对象,最好将int x;改写成int x = 0;当然大部分情况下c++会为int x;分配 x = 0。

  构造函数初始化成员变量时,最好使用成员初值列,这样效率比赋值要高一些,尤其是对非内置对象,内置对象两者没影响。如

//推荐使用
class Student{
public:
    Student(string& name, int age):name_(name),age_(age) {};
private:
    string name_;
    int age_;
}

//不推荐
class Student{
public:
    Student(string& name, int age) {
        name_ = name;
        age_ = age;
    };
private:
    string name_;
    int age_;
}

  上面第一种只有对于string对象只有copy操作,而第二种既有copy操作,还有赋值操作。

条款五:了解c++默默编写并调用哪些函数

  你定义一个空类,编译器会为它分配default构造函数,copy构造函数,copy assignment操作符合一个析构函数。如

class Empty {};

  实际上上面的类等价于

class Empty {
public:
    Empty() {...};
    Empty (const Empty& e) {...};
    ~Empty() {...};
    Empty& operator=(const Empty& e) {...};  
}

   如果你自己定义了构造函数,copy构造函数,copy assignment,析构函数,则编译器不会再分配这些函数。

   编译器分配的copy assignment有时候存在问题,如string& name这样的成员属性,编译器分配的copy assignment是不可以赋值的,你需要自己重新定义copy assignment。

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

  如条款五所说编译器会自动分配如copy构造函数,copy assignment这类的函数,如果你不想要这些函数,可以在private下声明这些函数就行了。如

class Student{
private:
     Student (const Student&);
     Student& operator=(const Student&);
}

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

  带有多态性质的基类,或者类中有带vitural的成员函数,则其析构函数都应该定义为virtual析构函数。但如果该类不是设计为基类,则不要声明virtual析构函数。如果基类中的析构函数没有定义为vitural,则会出现一些问题,如在工厂模式中。有以下代码

class Person{
public:
    Person();
    ~Person();
}

class Student : public Person {...};

Person* getPerson();

Person* student = getPerson();
delete student;

  假设当前的getPerson()获取一个派生类Student对象,当使用完之后delete student时因为指针是基类指针,所以会销毁基类中的成员,但是不会销毁派生类中的成员,所以会存在”部分销毁“的现象,但是若基类中的析构函数是vitural,则会删除派生类中的成员。

  另外如果一个类不是基类,不要声明vitural函数,因为vitural会引入虚表指针和虚表,会占用一部分内存。

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

  这种通常指在析构函数中调用了一些函数,而这些函数可能引入异常,此时需要做一些处理,尽量保证不要让析构函数的异常传递出来,或者说尽量确保析构函数中不要发生异常。

条款九:绝对不要在构造函数和析构函数中调用virtual函数

  因为上述这类调用绝对不会下降至下一层,即派生层,这种情况避免就好了。一般也不会这样去调用

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

  这是一种固定的assignment 操作符写法,如

class Student{
public:
    Student& operator=(const Student& s){
        name_ = s.name_;
        age_ = s.age_;
        return *this;
    }
private:
    string name_;
    int age_;
}

  除了operator= 如operator+=等都可以写成这样。

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

  自我赋值,即a = a;当然一般这种情况不会发生,但是这种*a = *b,而指针a和b都指向同一个值,这样就存在问题。如果operator= 是下面这种写法

class Bitmap {};

class Widget {
public:
    Widget& operator=(const Widget& rhs){
        delete pb;
        pb = new Bitmap(*rhs.pb);
        return *this;
    }
private:
    Bitmap* pb;
}

  如果rhs == this,则delete pb时也删除了rhs中的pb。所以可以改写成

class Bitmap {};

class Widget {
public:
    Widget& operator=(const Widget& rhs){
        Bitmap* pOrig = pb;
        pb = new Bitmap(*rhs.pb);
        delete pOrig;
        return *this;
    }
private:
    Bitmap* pb;
}

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

  copy构造函数和copy assignment函数中不要忘了每一个成员变量,忘记了编译器也不会报错。如

class Student{
public: 
    Student (const Student& s):name_(s.name_) {};
private:
    string name_;
    int age_;
}

  派生类中的copy构造函数也不要忘了基类中的成员变量,可以直接调用基类的构造函数。

class Person{
public:
    Person (const Person& p):name_(p.name_), age_(p.age_) {};
private:
    string name_;
    int age_;
}

class Student : public Person {
public:
    Student (const Student& s) : Person(s), grade_(s.grade_){};
private:
    int grade_;
}

条款十三:以对象管理资源

  在c++中new和delete必须是同时存在的,但很多时候会忘记delete,或者说你很仔细的没有忘记delete,但是在new和delete之间的代码可能会存在return,continue等这类操作,而跳过了delete,为了防止这种内存泄漏的情况发生,所以也就有了以对象管理资源,当对象呗释放时,对象的析构函数会自动释放这些资源,如auto_ptr就是这种的资源管理对象。而c++11中的unique_ptr,shared_ptr也是这一类对象,只不过unique_ptr不能多个对象共享一块内存,而shared_ptr通过引用计数机制,可以多个对象共享一块内存,只有当引用计数为0才会释放内存。

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

  RAII(Resource Acquisition Is Initailization,资源取得时便是初始化时,也是以对象管理资源的概念)对象复制时要一并复制它所管理的资源。而普遍常见的RAII class copying行为是:抑制copying,施行引用计数法等。

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

  APIs中往往需要访问原始资源,所以每一个RAII class应该提供一个”取得其所管理之资源“的方法,如get成员函数可以获得原始指针,重载了指针取值运算符,转换到原始指针并取值。

  对原始资源的访问可能经由显示转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

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

  如下例子  

string* stringArray = new string[100];
...
delete stringArray;

  上面的new和delete并不是一个很好的应用,因为new出来的是一个数组,而delete很可能只释放了一个元素,标准的用法是

string* stringArray = new string[100];
...
delete [] stringArray;

  所以说new和delete,new [] 和delete []。针对此问题要注意typedef的使用,最好不要对数组使用typedef,否则很容易在delete时忘了带[]。

条款十七:以独立语句将newed对象置入智能指针

  以独立语句将newed对象存储于智能指针内。如果不这样做,一旦异常抛出,有可能导致难以察觉的资源泄漏。如

int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);

//调用
processWidget(std::shared_ptr<Widget> (new Widget), priority());

  在c++中上述执行的顺序并不严格,可能先执行new Widget,再执行priority(),最后才执行shared_ptr的调用。这样一旦priority()调用报错,就不会发生new Widget的内存泄漏。所以最好独立语句将对象放入智能指针。

std::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

条款十八:让接口容易被正确使用,不易被误用

  总结成一句话就是在设计接口时,尽量简洁明了,约束性强,不容易发生误用。

条款十九:设计class犹如设计type

条款二十:宁以pass-by-reference-to-const替换pass-by-value

  在函数的参数传递过程中对于非内置类型对象,尽量以引用或指针传递,推荐引用,为了避免函数中修改传递的对象,可以加上const修饰符。对于非内置类型,引用的传递要比传值高效很多,因为传值的过程中相当于传递副本,是需要调用copy构造函数,而函数执行完之后还会调用析构函数销毁copy的对象,而引用传递不存在这一问题。

条款二十一:必须返回对象时,别妄想返回其reference

  在函数的返回值时切勿返回reference、pointer,尤其是指向函数中的局部对象,直接返回值即可,虽然这样会耗时(调用构造函数),但至少是正确的。

条款二十二:将成员变量声明为private

条款二十三:宁以non-member,non-friend替换member函数

条款二十四:若所有参数皆需类型转换,请为此采用non-member函数

条款二十五:考虑写出一个不抛异常的swap函数

条款二十六:尽可能延后变量定义式的出现时间

  定义的变量即使没有被使用,也会存在构造和析构的操作,也就是或会存在构造和析构的成本,所以在定义变量时尽量在使用时定义。

条款二十七:尽量少做转型动作

  如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。

  如果转型是必要的,试着将他隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放入到他们自己的代码内。

  宁可使用c++风格的新式转型,不要使用旧式转型。

 条款二十八:避免返回handles指向对象内部成分

  对于下面的例子,返回引用(handles,指针,迭代器也可以归为这一类)会导致内部数据的不安全.

class Point {
public:
    Point(int x, int y);
    void setX(int newVal);
    void setY(int newVal);
}

struct RectData{
    Point ulhc;
    Point lrhc;
}

class Rectangle{
public:
    Point& upperLeft () const {return pData->ulhc};
    Point& lowerRight () const {return pData->lrhc};
private:
    std::shared_ptr<RectData> pData;
}

  上面的Rectangle类中,虽然upperLeft函数是const函数,但是返回的结果是Point的引用,通过该引用是可以修改Point内部的值,包括返回指针和迭代器都是会发生类似的情况,所以如果硬要返回引用这类handles,可以返回const references

class Rectangle{
public:
    const Point& upperLeft () const {return pData->ulhc};
    const Point& lowerRight () const {return pData->lrhc};
private:
    std::shared_ptr<RectData> pData;
}

条款二十九:为”异常安全“而努力是值得的

条款三十:透彻了解inlining的里里外外

  总之只有在小型且频繁被调用的函数身上才使用inlining。这样可以保证代码膨胀的问题最小化,程序的速度提升最大化,如max函数。

条款三十一:将文件间的编译依存关系降至最低

条款三十二:确定你的public继承塑模出is-a关系

  基类和派生类之间一定要有is-a关系,即基类的属性和方法,在派生类中一定是合理存在的。

条款三十三:避免遮掩继承而来的名称

  只要熟悉作用域,根据作用域去定义变量就可以了,知道在使用变量时作用域的先后顺序。

条款三十四:区分接口继承和实现继承

  这里涉及到基类中的纯虚函数,虚函数和非虚函数以及继承的概念。

  纯虚函数:含有纯虚函数的类是抽象类,不可以被new出来。对于纯虚函数,子类只继承了接口,子类中必须声明和实现纯虚函数,当然父类中也可以定义纯虚函数,但要指定父类名称才可以调用。

  虚函数:子类会继承虚函数的接口和缺省实现,子类中也可以重写虚函数。

  非虚函数:子类会继承非虚函数的接口和强制实现,不建议子类中重写非虚函数。如果硬要重写,调用时基类指针会调用基类中的函数,想调用子类中的重写函数,必须使用子类指针。

条款三十五:考虑virtual函数以外的其他选择

  在子类继承父类的功能时,可以引入一些设计模式来使得继承更灵活。如使用模板模式,策略模式。

条款三十六:绝不重新定义继承而来的非虚函数

  正如上面所说如果子类重写了父类的非虚函数,父类指针即使是指向子类,当调用非虚函数时,还是会调用父类的非虚函数,所以非虚函数的调用和指针类型绑定。所以为了避免出现这样的迷惑行为,还是不要重写非虚函数。

条款三十七:绝不重新定义继承而来的缺省参数值

  对于继承而来的虚函数或纯虚函数,如果函数中有定义的缺省参数值,继承时不要修改缺省参数值,因为缺省参数值是静态绑定(发生在编译期)的。而虚函数或纯虚函数时动态绑定(发生在运行期)的。

条款三十八:通过复合塑模出has-a或”根据某物实现出“

条款三十九:明智而审慎地使用private继承

  private并不会继承接口,而是继承基类的实现,总之尽量使用public,除非特殊实现上,因为private继承本质上不属于is-a关系。

条款四十:明智而审慎地使用多重继承

  总之能使用单一继承的尽量使用单一继承,多重继承太过复杂,容易引入歧义,且应用于多重继承的虚继承既要提前设计也会引入大小、速度等成本。

条款四十一:了解隐式接口和编译器多态

  c++中的模板创造了隐式接口和编译期的多态。

条款四十二:了解typename的双重意义

  在声明模板时,template<typename T> 和 template<class T>的含义是一样的,但是typename可以表示嵌套从属类型,如T::setX到底是一个类型还是成员名称,如果加上typename就很明确是一个类型了,typename T::setX。

posted @ 2020-08-27 20:23  微笑sun  阅读(1045)  评论(3编辑  收藏  举报