(5~12)Effective C++ 改善程序与设计的55个具体做法

5. 了解 C++ 默认编写并调用哪些函数

  • 编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符, 以及析构函数。

    //创建空类
    class Empty
    {  
    };
    
    //空类等价于如下类
    class Empty
    {
    public:
        Empty(){} //默认生成无参构造函数
        ~Empty(){} //默认生成析构函数
        
        Empty(const Empty& rhs){} //默认生成拷贝构造函数
        Empty& operator=(const Empty& rhs){ return *this;} //默认生成赋值操作符
    };
    
    Empty e; //调用默认构造函数
    Empty e1(e); //调用拷贝构造函数
    Empty e1 = e; //调用拷贝构造函数
    e2 = e1; // 调用赋值操作符
    
    //1、若类中声明了有参构造函数, 则编译器不帮助生成默认构造函数, 不影响生成拷贝构造和赋值操作符。
    
    
    template <typename T>
    class NamedObjected
    {
    public:
    	NamedObjected(std::string& name, const T& value) : name_value(name), object_value(value)
    	{
    
    	}
    
    private:
    	std::string& name_value;
    	const T object_value;
    };
    
    std::string s;
    NamedObjected<int> e(s, 1); //ok
    NamedObjected<int> e2(e); //ok
    e2 = e; //error
    //2、 如果类中含 const 或 引用成员变量,则编译器拒绝自动生成赋值操作符,因为违反了引用和 const 的不可变性。
    
    //3、如果父类将赋值操作符声明为 private, 那么编译器也将拒绝为其派生类生成一个赋值操作符, 因为派生类的赋值操作符可以处理父类的赋值操作符(见条款12)。
    
    

6. 若不想使用编译器自动生成的函数,就该明确拒绝

  • 为驳回编译自动提供的功能,可将相应的成员函数声明为 private 并且不予实现。使用像 Uncopyable 这样的 base class 也是一种做法。

    class Uncopyable
    {
    protected:
        Uncopyable(){}
        ~Uncopyable(){} //不一定得是 virtual,因为此基类没有多态性质(见条款7) 
    
    private:
        Uncopyable(const Uncopyable&);  //阻止生成拷贝构造函数和赋值操作符
        Uncopyable& operator=(const Uncopyable&); 
    }
    
    //为什么继承 Uncopyable 会阻止子类自动生成 拷贝构造和赋值操作符 (见条款5和条款12)
    class HomeForSale : private Uncopyable //class 不再声明 拷贝构造和赋值操作符
    {
        ...  
    }
    

7. 为多态基类声明 virtual 析构函数

  • polymorphic(带多态性质的) base class 应该声明一个 virtual 析构函数。 如果 class 带有任何 virtual函数, 他就应该拥有一个 virtual 析构函数。
  • class 的设计目的如果不是作为 base class 使用, 或不是为了具备多态性, 就不该声明 virtual 析构函数。
  1. 当一个具有多态性质的派生类对象经由一个基类指针被删除时, 应该将基类析构函数加上 virtual,使得子类能析构。

    //带有多态性质的 People 类
    class People
    {
    public:
        People(){}
        ~People(){}
        
        virtual void Speak(){}
    };
    
    class Student : public People
    {
    public:
        Student(){}
    	Student(const char* name)
        {
            int len = strlen(name) + 1;
        	m_name = (char*)malloc(len);
            strcopy(m_name, name);
        }
        
        virtual void Speak(){}
        
        ~Student(){
            free(m_name); 
        }
        
    private:
        char* m_name;
    };
    
    People* p = new Student("s");
    delete p; // 若 People 的析构函数未声明为 virtual, delete后,则不会调用 Student 的析构函数,从而导致 m_name 内存泄漏。
    
    //声明为 virtual 就可解决
    class People
    {
    public:
        People(){}
        virtual ~People(){}
        virtual void Speak(){}
    };
    
  2. 一个类不意图作为一个具有多态性质类时,无需将该类析构函数声明为 virtual

    //如果一个类不包含 virtual 函数,那么这个类的析构函数无需声明为 virtual, 否则是个馊主意
    //因为当其声明为 virtual 后, 类内会维护一个 vptr( virtual table pointer) 指针, 其指向一个由函数指针构成的数组, 称为 vtbl( vitual table).
    //这就导致该类体积会变大, 无意义的内存。
    
  3. 不要企图继承 STL 容器。

    class SpecialString : public std::string
    {
    	...
    };
    
    std::string* pss = new SpecialString("12");
    delete pss; //同样的问题,可能导致 SpecialString 类内存泄漏
    
  4. 当带有多态性质的基类无需实例化时,使用纯虚函数声明, 并提供一份定义。

    class AWOV
    {
    public:
        virtual ~AWOV() = 0;
    };
    
    AWOV::~AWOV(){} //若不定义,编译不通过,因为派生类对象销毁时,会向上逐一调用父类析构函数, 发现没有定义,连接器就会发生抱怨。
    
    

8. 别让异常逃离析构函数

  • 析构函数绝对不要吐出异常, 如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序。

  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应, 那么 class 应该提供一个普通函数(而非析构函数中)执行该操作。

    class Widget
    {
    public:
        
        ~Widget(){}
        
    };
    
    void doSomething()
    {
        std::vector<Widget> v; //若vector销毁到第二个Widget时,析构函数抛出异常将会导致剩余Widget未析构,可能导致资源泄漏。
    }
    
    class DBConn
    {
    public:  
        void close() //供客户使用的新函数,给客户一次捕获异常的机会。
        {
            db.close();
            closed = true;
        }
        
        ~DBConn()
        {
            if(!closed) //若客户未调用新函数close(),则利用析构函数调用 db.close(),并且捕获其异常, 记入日志。
            {
                try
                {
                    db.close();
                }
                catch(...)
                {
                    //记录日志
                }
            }
        }
        
    private:
        DBConnection db;
        bool closed;
    };
    

9.绝不在构造和析构过程中调用 virtual 函数

  • 在构造和析构期间不要调用 virtual 函数,因为调用从不下降至 derived class (比起当前执行构造函数和析构函数的那层)。

class Transaction
{
public:
    Transaction()
    {
        init();
    }
    
    virtual void LogTransaction() const = 0; //日志记录
private:
    void init()
    {
        //如果LogTransaction为纯虚函数,那么当父类钩爪
        
        LogTransaction(); //调用 virtual 函数
    }
};

class BuyTransaction : public Transaction
{
public:
    virtual void LogTransaction() const
    {
        ...
    }
};

BuyTransaction b;
//问题1、当对象 b 构造时不会调用子类的 LogTransaction, 因为基类比子类先构造, 无法调用子类的成员函数。
//问题2、由于基类比子类先构造, 当基类 LogTransaction 为纯虚函数时,编译通过, 但运行时会报错。当为非纯虚函数时,会调用基类的 LogTransaction(), 也会和你的预期不一致
//令派生类将必要的构造信息向上传递至基类构造函数解决
class Transaction
{
public:
    explicit Transaction(const std::string& logInfo)
    {
        logTransaction(logInfo);
    }
    void logTransaction(const std::string& logInfo) const; //变为非虚函数
};

class BuyTransaction : public Transaction
{
public:
    explicit BuyTransaction( parameters) : Transaction(CreateLogString( parameters ))//通过子类参数将log信息传递给基类构造函数
    {
    }
    void logTransaction(const std::string& logInfo) const; //变为非虚函数
    
private:
    static std::string createLogString(parameters); //令此函数为 static, 避免函数中使用子类中未初始化的成员变量。(静态成员函数无法使用非 static 成员变量)
};

10. 令 operator= 返回一个 reference to *this


  • *令 operator= 返回一个 reference to this

    //此协议并非强制性, 未遵循编译一样通过, 只是使得自定义类型与内置类型、标准程序库保持统一, 避免不必要的麻烦。
    
    class Widget
    {
    public:
        Widget& operator=(const Widget& rhs)
        {
            return *this;
        }
        
        Widget& operator+=(const Widget& rhs) // 此协议也适用于 +=,-=,*=,等待
        {
            return *this;
        }
        
        Widget& operator=(int rhs) //此操作符的参数类型也使用,即使不符合规定
        {
            return *this;
        }
    };
    
    
    

11. 在operator= 中处理自我赋值

  • 确保当对象自我赋值时 operator= 有良好行为。其中技术包括比较 “来源对象” 和 “目标对象” 的地址,精心周到的语句顺序、以及 copy-and-swap

  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

    //1、一份不安全的 operator= 实现版本
    class Widget
    {
    	Widget() : data_(NULL), len_(0)
    	{
    	}
    	
    	Widget(const Widget& rhs){
    		...
    	}
    	
    	Widget& operator=(const Widget& rhs)
    	{
    		delete pb; //如果 rhs.pb 和 类内 pb 指向同一个对象,则此对象会 delete 两次
    		pb = new Bitmap(*rhs.pb);
    		return *this;
    	}
    	
    	~MyString(){}
    	
    private:
    	Bitmap* pb;
    };
    
    //此问题解决方法有三个。
    //1、证同测试
    Widget& operator=(const Widget& rhs)
    {
        //虽然行的通, 但是如果 new Bitmap 异常(不管是因为内存不足还是构造函数异常),Widget 最终会指向变成野指针。
        if(*this == rhs) return *this;
        
    	delete pb;
    	pb = new Bitmap(*rhs.pb); 
    	return *this;
    }
    
    //2、改进,精心周到的语句
    Widget& operator=(const Widget& rhs)
    {
        Bitmap* ptemp = pb; //记住原先的pb
        pb = new Bitmap(*ths.pb); //令pb指向 *pb 的一个副本(即使 new Bitmap 失败, pb仍保持原状。)
        delete ptemp; // 删除原先的pb
        return *this;
    }
    
    //3、方案2替代使用 copy-and-swap
    class Widget
    {
      void swap(Widget& rhs);   //交换*this和rhs数据,详见条款29
    };
    
    Widget& operator=(const Widget& rhs)
    {
    	Widget temp(rhs); //为 rhs 数据制作一份
        swap(temp);
        return *this;
    }
    
    

12. 复制对象时勿忘其每一个成分

  • copying 函数应该确保复制 ”对象内的所有成员变量“ 及 ”所有 base class 成分。“
  • 不要尝试以某个 copying 函数实现另一个 copying 函数。 应该将共同机能放进第三个函数中,并由两个 coping 函数共同调用。
//copying 函数应该确保复制 ”对象内的所有成员变量“ 及 ”所有 base class 成分, 否则会导致某些成员变量未初始化,从而产生不预期的结果。
void logCall(const std::string& funcName);
class Customer
{
public:
    Customer(){}
    ~Customer(){}
    
    Customer(const Customer& rhs) : name_(rhs.name_)
    {
        logCall("Customer copy Constructor");
    }
    Customer& operator=(const Customer& rhs)
    {
        logCall("Customer copy assignment operator");
        name_ = rhs.name_;
        return *this;
    }
    
private:
    std::string name_;
};

// copying时,未初始化父类的成员变量
class PriorityCustomer : public Customer
{
public:
    PriorityCustomer(const PriorityCustomer& rhs) : priority_(rhs.priority) 
    {
        logCall("PriorityCustomer copy Constructor");        
	}
    
     PriorityCustomer& operator=(const PriorityCustomer& rhs)
    {
        logCall("PriorityCustomer copy assignment operator");
        priority_ = rhs.priority_;
        return *this;
    }
private:
    int priority_;
}

// copying时,初始化父类的成员变量
class PriorityCustomer : public Customer
{
public:
    PriorityCustomer(const PriorityCustomer& rhs) : Customer(rhs),priority_(rhs.priority) 
    {
        logCall("PriorityCustomer copy Constructor");        
	}
    
     PriorityCustomer& operator=(const PriorityCustomer& rhs)
    {
        logCall("PriorityCustomer copy assignment operator");
        Customer::operator=(rhs); //对基类成分进行赋值动作
        priority_ = rhs.priority_;
        return *this;
    }
private:
    int priority_;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值