首页 > 代码库 > c++笔记:const、初始化、copy构造/析构/赋值函数

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;
 }

 

参考资料:《Effective C++》

 

c++笔记:const、初始化、copy构造/析构/赋值函数