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