zoukankan      html  css  js  c++  java
  • c++:const、初始化、copy构造/析构/赋值函数

    构造函数

    Default构造函数:可被调用而不带任何实参的构造函数,没有参数或每个参数都有缺省值。如:

    class A {
    public:
        A();
    };

    将构造函数声明为explicit,可阻止它们被用来执行隐式类型转换,但仍可用来进行显示类型转换。如:

    class B {
    public:
        explicit B(int x = 0, bool b = ture);
    };

    copy构造函数:用于以同型对象初始化自我对象,以passed by value的方式传递对象;·

    copy assignment操作符:用于从另一个同型对象中拷贝其值到自我对象。如:

    class Widget {
      public:
          Widget();
          Widget(const Widget& rhs); //copy构造函数
          Widget& operator=(contst Widget& rhs);//copy assignment操作符
    };
    Widget w2(w1); //调用copy构造函数
    w1 = w2; //没有新对象被定义 调用copy assignment(copy赋值)操作
    Widget w3 = w2; //有新对象被定义 调用copy构造函数

    其它笔记

    TR1:Technical Rpeort1,描述加入C++标注你程序库的诸多新机能的规范,机能以新的class templates和function templates形式体现,TR1组件置于命名空间std内嵌套的命名空间tr1中;

     Boost:组织/网站,提供可移植的开源C++程序库,大多数TR1机能以Boost为基础。

     

    C++可主要分为4个sublanguage部分:

     1. C,以C为基础,包含blocks, statements, preprocessor, built-in data types, arrays, pointers等内容。

     2. Object-oriented C++,包含C with classes的面向对象诉求,如classes(构造函数和析构函数), encapsulation, inheritance, polymorphism(多态), virtual函数(动态绑定)等。

     3. Template C++,包含泛型编程(generic programming)部分。

     4. STL,template程序库,包含containers, iterators, algorithms, function objects等内容。

     C++高效编程守则的变化取决于使用的是C++的哪一部分。

    注意:尽量以const, enum, inline替换#define,降低对预处理器的需求

    如:以const常量替换#define

    /*#define不被视为语言的一部分,记号名称ASPECT_RATIO可能没进入记号表(symbol table)内;*/
    #define ASPECT_RATIO 1.653; //错误
    
    /*语言常量Aspect肯定会被编译器看到,进入记号表内:*/
    const double AspectRatio = 1.653; //正确

    定义常量指针时,因为常量定义是通常被放在头文件内,有必要将指针声明为const:

    const char* const authorName = “Scott Meyers”;

    另:string对象通常比char*-based更合适:

    const std::string authorName(“Scott Meyers“); 

    用static const定义class专属常量 

    定义class专属常量时,为确保此常量至多只有一份实体,将其作为static成员:

    class GamePlayer {
      private:
          static const int NumTurns = 5;//常量声明式
          int scores[NumTurns];//使用常量
    };

     如果需要取class专属常量的地址,需要为编译器提供class专属常量的定义式;

    定义式需放入实现文件,而非头文件;

    class常量在声明时获得初值,定义时不可再设初值:

    cons tint GamePlayer::NumTurns;//常量定义式

    如果使用旧式编译器,则不允许static成员在声明时获得初值,另in-class初值设定也只允许对整数常量进行,可采用: 

    class CostEstimate {
      private:
          static const double FudgeFactor;
    };
    //常量定义位于实现文件内,赋初值
    const double ConstEstimate::FudgeFactor = 1.35;

    若编译器不允许in class初值设定,用enum hack补偿

    Enum hack:类似#define,取#define和enum地址不合法,而取const地址合法;

    Enum和#define不会导致非必要的内存分配;

    可约束使别人无法获得指向某个指针常量的指针或引用;

    如果编译器错误地不允许in class初值设定,可改用enum hack补偿做法,使该常量成为一个记号名称:

    class GamePlayer {
      private:
          enum {NumTurns = 5};
          int socres[NumTurns];
    };

    使用template inline函数替代宏:

    template<typename T>
    inline void callWithMax(const T& a, const T& b) {
        f(a>b?a:b);
    }

    尽可能使用const

    用const来修饰指针,可以指出指针自身和其所指物是否为const。

    如果关键词const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两遍,表示被指物和指针两者都是常量:

    char* p = greeting; //non-const pointer, non-const data
    const char* p = greeting; //non-const pointer, const data
    char* const p = greeting; //const pointer, non-const data
    const char* const p = greeting;//const pointer, const data

    如果被指物是常量,关键词const写在类型或写在类型时候星号之前的意义都相同:

    void f1(const Widget* pw);
    void f2(Widget const * pw);//两种都表示获得一个指针,指向常量Widget对象

    STL中迭代器的作用相当于T*指针,声明迭代器为const相当于声明一个T* const指针,表示该迭代器不得指向不同的东西,但所指东西的值可以改动:

    const std::vector<int>::iterator iter = vec.begin();//声明迭代器为const
    *iter=10; //可行

    如果希望迭代器所指东西不可改动,即模拟const T*指针,则声明const_iterator:

    std::vector<int>::const_iterator cIter = vec.begin();
    *iter=10;//不可行

    确定对象被使用前已经被初始化

    C++规定对象的成员变量初始化动作发生在进入构造函数本体之前。如果按照以下操作,只是在构造函数体内赋值,而不是初始化:

    ABEntry::ABEntry(const std::string& name, const std::string& address,
                     const std::list<PhoneNumber>& phones) {
        theName = name;
        theAddress = address;//赋值(assignments)
        thePhones = phones;  //而不是初始化(initializations) 
        numTimesConsulted = 0;
    }

    完成初始化更好的做法是使用成员初值列(member initialization list:

    ABEntry::(const std::string& name, const std::string& address,
              const std::list<PhoneNumber>& phones)
        :theName(name),
        :theAddress(address),
        :thePhones(phones),
        :numTimesConsulted(0) {}

    C++有固定的成员初始化次序:base classes初始化早于其derived classes,class成员变量初始化顺序按照声明次序(即使在成员初值列中次序不同,但最好以声明次序写成员初值列。

    有关“不同编译单元内定义的non-local static对象”的初始化顺序

    Static对象的寿命从被构造出来直到程序结束为止,包括global对象、定义于namespace作用域内的对象、classes内、函数内、file作用域内被声明为static的对象;

    函数内的static对象称为local static对象,其他static对象称为non-local static对象;

    程序结束时static对象会被自动销毁;

    编译单元(translation unit):产出单一目标文件(single object file)的源码;

    问题:如果某个编译单元内某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,因为C++对“定义于不同编译单元内的non-local static对象的初始化次序”无明确定义,它所用到的这个对象可能尚未被初始化。

    解决办法:将每个non-local static对象搬到自己专属函数内,该对象在此函数内被声明为static,函数返一个指向它所含对象的reference;

    用户调用这些函数,而不直接指涉这些对象(用local-static替换non-local static);

    原理:Singleton模式的一个实现收发,保证函数内的local static对象会在函数调用期间首次遇上该对象定义式时被初始化,如:

    FileSystem& tfs() {
        static FileSystem fs; //替换local static tfs
        return fs;
    }
    Directorry::Directory(params) {
        std::size_t disks = tfs(). numDisks();//原先使用reference to tfs
    }
    Directory& tempDir() {
        static Directory td; //替换local static tempDir
        reutrn td;
    }

    C++自动编写并调用copy构造/析构/copy赋值运算

    如果没有声明copy构造函数、copy assignment操作符和析构函数,C++处理后编译器将自动声明copy构造函数和析构函数;

    编译器自动声明的copy构造函数单纯将来源对象的每个non-static成员变量(调用成员变量的copy构造函数)拷贝到目标对象;

    编译器自动声明的copy assignment操作符与copy构造函数类似,但如果生成代码不合法,编译器会拒绝自己生成operator=编译赋值行为,如以下三种情况:

    1. 如果要在内含reference成员的class内支持赋值操作,编译器拒绝生成copy assignment,必须自己定义copy assignment操作符;

    2. 如果要在内含const成员的class内支持赋值操作,更改const成员不合法,编译器拒绝生成copy assignment;

    3. 如果base class将copy assignment操作符声明为private,编译器拒绝为其derived classes生成copy assignment操作符,因为所生成的copy assignment操作符将无法调用derived class无权调用的成员函数;

    如果没有声明任何构造函数,编译器也会声明一个default构造函数;

    这些函数都是public且inline的,当需要调用这些函数时才会被编译器创建;

    编译器调用的析构函数是non-virtual的,除非该class的base class自身声明有virtual析构函数;

    若不想使用编译器自动生成的函数需明确阻止

    特定对象可能需要阻止对其进行copy操作,如果需要阻止编译器自动生成的函数(如copy构造函数或copy assignment操作符),可以将这些函数声明为private且不添加定义;

    例:C++标准程序库iostream实现码中的ios_base, basic_ios, sentry的copy构造函数和copy assignment操作符都被声明为private且没有定义;

    防止编译器暗自生成及他人调用,可将copy构造函数和copy assignment操作符声明为private,客户企图拷贝对象时会被编译器阻止:

    class HomeForSale {
      public:
          ...
      private:
          ...
          HomeForSale(const HomeForSale&); //只有声明
          HomeForSale(const HomeForSale&); 
    };

    防止member和friend函数调用,可不为copy构造函数和copy assignment操作符添加定义,但报错会发生在连接器。为将连接期错误移至编译器,可为对象创建base class,在base class内将copy构造函数和copy assignment操作符声明为private:

    class Uncopyable { //base class
      protected:
          Uncopyable(){} //允许derived对象构造和析构
          ~Uncopyable(){}
      private:
          Uncopyable(const Uncopyable&); //但阻止copying
          Uncopyable operator=(const Uncopyable&);
    };
    
    class HomeForSale : private Uncopyable { //derived class
      ... //不再声明copy构造函数或copy assignment操作符
    }

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

    注意将多态基类的析构函数声明为virtual,因为如果base class有non-virtual的析构函数,derived class对象的析构函数未能执行,derived成分无法经由base class指针销毁,形成资源泄露;

    给base class声明virtual析构函数,删除derived class对象时能够完整销毁整个对象:

    class TimeKeeper {
      public:
          TimeKeeper();
          virtual ~TimeKeeper();
          ...
    };
    TimeKeeper* ptk = getTimeKeeper();//从TimeKeeper继承体系获得一个动态分配对象
    ...
    delete ptk;//释放避免资源泄露

    Virtual函数的实现细节:每个带有virtual函数的class都有相应的由函数指针构成的数组vtbl(virtual table)。对象携带vptr(virtual table pointer)指针,指向vtbl。当对象调用某一virtual函数时,实际被调用的函数取决于该对象的vptr所指的vtbl,编译器在vtbl中寻找适当的函数指针。

    如果class不需要作为base class(class内往往不含有virtual函数),则不需要令其析构函数为virtual。

    标准容器如string,STL容器如vector, list, set, trl::unordered_map等都不含有virtual析构函数,应避免继承此类带有non-virtual析构函数的class导致析构函数未有定义出现资源泄露问题。

     

    如果需要创建抽象(abstract)对象,可为其声明pure virtual函数并提供空白定义:

    class AWOV {
      public:
          virtual ~AWOV() = 0;
    };
    AWOV::~AWOV() {}

    析构函数的运作方式:最深层派生(most derived)的class的析构函数最先被调用,每个base class逐渐被调用,为derived classes的析构函数提供定义,创建对~baseclass的调用动作(否则连接器报错)。

    析构函数绝对不要吐出异常

    注意:析构函数绝对不要吐出异常,否则会导致程序过早结束或不明确行为。如果一个被析构函数调用的函数可能抛出异常,析构函数内应该捕捉任何异常,吞下或结束程序。

    如果客户需要对某个操作函数运行期间抛出的异常做出反应,class应提供一个普通函数(在析构函数之外)执行该操作。如:

    class DBConnection {
      public:
          ...
          static DBConnection cereate();
          void close();//关闭连接,失败则抛出异常
    }
    
    class DBConn { //用于管理DBConnection对象
      public:
          ...
          void close() {//供客户对可能出现的问题做出反应
              db.close(); //确保数据库连接关闭
              close = true;
          }
          ~DBConn() {
              if(!closed) {
                  try {
                      db.close();
                  } catch(...) {
                      ...//如果关闭失败,记录下来并吞下异常,或结束程序
                  }
              }
          }
      private:
          DBConnection db;
          bool closed;
    };
    
    //客户使用
    { DBConn dbc(DBConnection::create());
        ...}//区块结束,DBConn销毁

    绝对不要在构造函数和析构函数内调用virtual函数

    由于无法在构造和析构期间无法使用virtual函数从base class向下调用到derived class,如果需要确保每次base class的继承体系上的对象被创建时都会有适当版本的成员函数被调用,可采用将该函数改为non-virtual,要求derived class构造函数传递必要信息给base class构造函数,构造函数从而安全调用non-virtual函数的做法:

    class Transaction {//base class
      public:
          explicit Transaction(const std::string& logInfo);
          void logTransaction(const std::string& logInfo) const; //根据不同类型做出不同的日志记录
          ...
    };
    
    Transaction::Transaction(const std::string& logInfo) {
        ...
        logTransaction(logInfo);
    }
    
    class BuyTransaction : public Transaction { //derived class
      public:
          BuyTransaction(parameters) 
          : Transaction(createLogString(parameters)) 
          {...} //将log信息传给base class构造函数
         ...
      private:
          static std::string createLogString(parameters);
    };

    利用辅助函数static std::string createLogString(parameter)创建一个值传给base class构造函数,比在成员初值列内给予base class所需数据更方便、可读;

    此函数为static,避免了意外指向“初期未成熟的derived class对象内尚未初始化的成员变量”。

    operator=应返回*this的引用

    标准赋值oprator=及其他赋值如+=,-=,*=等应遵守协议返回reference to *this(指向左侧【当前】对象的reference),从而实现“连锁赋值”(如x=y=z=15,解析为右结合律);

    class Widget {
      public:
          ...
      Widget& operator=(const Widget& rhs) {
          ...
          return *this; //返回类型为指向当前对象的reference
      }
    }

    非强制性,违反协议一样可通过编译,但该协议被所有内置类型和标准程序库共同遵守;

     

    在编写operator=时需考虑自我赋值操作的情况,如:

    w=w;

    a[i] = a[j] (i=j时)

    *px = *py; (px和py指向同一处)忽略自我赋值问题指针可能指向已被删除的对象;

    处理方法:提前做证同测试(identity test),检验自我赋值情况:

    Widget& Widget::operator(const Widget& rhs) {
    if (this == &rhs) return this; //identity test
    delete pb; //pb为对象持有的指针
    pb = newBitmap(*rhs.pb);
    return *this;
    }

    仍存在不具备异常安全性的问题,如果因为分配时内存不足或copy构造函数抛出异常导致new()异常,持有指针可能仍会指向已被删除的对象;

    让operator具备异常安全性的做法:在复制指针所指对象前先别delete该指针,如果new()抛出异常,指针和指针原指的对象能够保持原状。

    Widget& Widget::operator=(const Widget& rhs) {
        If(this==&rhs) return *this;
        Bitmap* pOrig = pb; //记住原先持有的指针
        pb = new Bitmap(*rhs.pb) //令pb指向*pb的一个副本
        delete pOrig; //删除原先的pb
        return this;
    }

    复制对象时需注意复制所有成分

    为derived class编写copying函数时需要记得除local成员变量外,还需复制base class成分,其中base class的private成分无法直接访问,应在derived class的copying函数内调用相应的base class函数:

    PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
     : Customer(rhs), PriorityCustomer(rhs.priority) { //调用base class Customer的构造函数
         logCall("PriorityCustomer copy constructor");
     }
     PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) {
         logCall("PriorityCustomer copy assignment operator");
         Customer::operator=(rhs); //对base class成分进行赋值
         return *this;
     }
  • 相关阅读:
    winform 异步更新ui
    定时器的写法 winform
    延迟加载
    使用VS分析程序性能
    win7 C/C++,QT安装环境总结
    论文总结
    天舟一号
    硬盘 SMART 检测参数详解[转]
    碧桃花
    在C的头文件中定义的结构体,如何在cpp文件中引用
  • 原文地址:https://www.cnblogs.com/RDaneelOlivaw/p/7350184.html
Copyright © 2011-2022 走看看