Special Member Functions

区别于定义类的行为的普通成员函数,类内有一类特殊的成员函数,它们负责类的构造拷贝移动销毁

构造函数

构造函数控制对象的初始化过程,具体来说,就是初始化对象的数据成员。构造函数的名字与类名相同,且没有返回值。构造函数也可以有重载,重载区别于参数数量或参数类型。与其他成员函数不同的是,构造函数不能被声明为const,对象的常量属性是在构造函数完成初始化之后获得的。

默认构造函数

默认构造函数的工作是:如果在类内定义了成员的初始值,那么用初始值初始化成员;否则,默认初始化成员。

默认初始化是指定义变量时不赋予初始值时被赋予默认值的动作。定义于函数体外的内置类型如果没有被显式初始化,则赋值0;在函数体内定义的变量不会被初始化。

class LiF {
public:
LiF(int _lif = 0) { lif = _lif; } // 指定了lif的初值,这是一个默认构造函数
private:
int lif;
} LiF l; // 调用默认构造函数,此时l.lif值为0

再看下面这种情况:

class LiF1 {
public:
LiF1(int _lif = 0) { lif = _lif; }
int lif;
}; class LiF2 {
public:
LiF1 lif1;
}; LiF2 l2;
std::cout << l2.lif1.lif << std::endl; // 输出结果是0

在上面的例子中,我们并没有为LiF2定义默认构造函数,但它又执行了默认构造。这是因为,当类没有定义任何构造函数,而程序又需要用到构造函数时,编译器会自动生成一个合成的默认构造函数(synthesized default constructor)。需要注意的是,只有在类内所有成员都具有类内初始值的时候,编译器才能合成默认构造函数。

class LiF {
public:
void print() { cout << lif << endl; }
private:
int lif;
}; LiF l;
l.print();

在上面的代码中,看似是我们需要一个默认构造函数来完成对l的初始化,理所应当地,编译器应该为我们生成一个合成的默认构造函数,但实际运行时,发现l.lif的值是未定义的。再看下面这种情况:

class LiF {
public:
int lif;
}; LiF l;
if (l.lif) {
cout << l.lif << endl;
}

这次连编译都没有通过,报错信息指出程序正在试图访问一个未初始化变量,即l.lif。通过上面这两种情况可以看出,编译器并不会因为程序“需要”默认构造函数,就自动生成一个合成的默认构造函数。事实上,只有下面几种情况,编译器会生成合成的默认构造函数:

  1. 类含有类对象成员,且该对象类型有默认构造函数(对应第一个例子)。
  2. 类继承自带有默认构造函数的类。
  3. 类内带有虚函数,由于虚函数表指针的存在,每个对象的构造都需要赋予该指针正确的值,而这个工作由默认构造函数完成。
  4. 类虚继承自另一个类,虚继承的派生类包含一个指向虚基类的指针,该指针同样需要正确的值,这个工作同样由默认构造函数完成。

C++11提供了default关键字,可以通过指定 = default来显式生成默认构造函数。

class LiF {
public:
LiF() = default;
void print() { cout << lif << endl; }
private:
int lif;
}; LiF l;
l.print(); // l.lif的值是未定义的

此外,如果类内包含一个无法默认初始化的const成员,那么编译器也会拒绝生成默认构造函数。

class LiF {
public:
LiF() = default;
void print() { cout << lif << endl; }
private:
const int lif;
}; LiF l; // 编译无法通过,提示默认构造函数被禁用

《C++ Primer》也建议,不要依赖编译器提供的合成的特殊成员函数

构造函数初始值列表

构造函数还可以包括一部分特殊的内容,这部分称为构造函数初始值列表(constructor initialize list),C++建议,在列表内完成成员的初始化。相比在构造函数体内初始化,初始值列表可以初始化常量成员。

class LiF {
public:
LiF(int _lif) { lif = _lif; } // 编译报错,提示常成员lif没有初始化
LiF(int _lif): lif(_lif) {} // 通过编译
private:
const int lif;
};

使用初始值列表初始化一个对象时,列表参数的顺序并不影响成员的初始化顺序,决定初始化顺序的是成员的定义顺序。良好的编程规范是,初始化列表的成员顺序尽量与成员的定义顺序保持一致

class LiF {
public:
LiF(int val): b(val), a(b) {}
private:
int a;
int b;
};

委托构造函数

C++11扩展了构造函数初始值列表的功能,允许定义委托构造函数(delegating constructor)。委托构造函数可以通过初始化列表把初始化任务委托给之前已经定义过的构造函数。

class LiF {
public:
LiF(int _a, int _b): a(_a), b(_b) {} // 普通构造函数
LiF(): LiF(0, 0) {} // 通过委托定义了默认构造函数
private:
int a;
int b;
};

转换构造函数

如果一个类存在一个只接受单个参数的构造函数,那么这个函数就定义了一个从参数类型向类类型隐式转换的规则,这个函数也被称为转换构造函数(converting constructor)。这种隐式转换无法嵌套,即编译器只会自动做一次隐式转换。

class LiF {
public:
LiF(int _lif = 0) : lif(_lif) {}
void doNothing(const LiF &l) {} // doNothing()函数需要一个LiF对象的引用
private:
int lif;
}; LiF l1;
l1.doNothing(1); // 这里执行了隐式转换,用参数1生成了一个LiF对象

隐式转换可能带来一些无法预知的后果,有时我们并不希望隐式转换的发生。C++提供了explicit关键字以禁用这种隐式转换。explicit只允许出现在函数声明处,且只适用于单参数构造函数,多参数构造函数并不存在隐式转换规则。

class LiF {
public:
explicit LiF(int _lif = 0) : lif(_lif) {}
void doNothing(const LiF &l) {} // doNothing()函数需要一个LiF对象的引用
private:
int lif;
}; LiF l1;
l1.doNothing(1); // 编译无法通过,因为隐式转换已被禁用

拷贝构造函数

class LiF {
public:
LiF();
LiF(const LiF& l): lif(l.lif) {}
int lif;
};

在翻阅原码的时候,经常能见到形如上面的类。其中第二个构造函数就是拷贝构造函数(copy constructor)。最常见的拷贝构造函数往往是:形参列表只包含一个自身类类型的引用,由于拷贝过程中并不会改变被拷贝的对象,这个引用一般也是按const属性传递。在定义一个对象时,如果采用=的方式初始化,那么执行的就是拷贝初始化。

string s1("s"); // 直接初始化
string s2(s1); // 直接初始化
string s3 = s1; // 拷贝初始化
string s4 = "s"; // 隐式拷贝初始化
string s5 = string("s"); // 显式拷贝初始化(等价于s4)

为什么是按引用传递呢?如果按值传递,在传递过程中会隐式调用拷贝构造函数生成函数实参,引发无限循环调用。

通常情况下,拷贝构造函数都是被隐式调用的,因此一般不声明为explicit。和默认构造函数类似,如果程序没有定义拷贝构造函数,在需要时,编译器会自动生成一个合成的拷贝构造函数(synthesized copy constructor),这个函数会拷贝对象的所有成员,但不同的是,即使我们定义了其他(非拷贝)构造函数,编译器也会生成。拷贝构造函数也可以使用初始化列表。需要注意的是,合成的拷贝构造函数进行的是浅拷贝

调用拷贝构造函数的场景:

  1. =定义对象
  2. 把对象作为实参传递给非引用类型的形参(这也解释了为什么拷贝构造函数要按引用传递)
  3. 返回一个非引用类型的对象
  4. 用花括号列表初始化数组元素或聚合类成员
string a;
void doNothing(string a);
doNothing(a); // 对应2 string doNothing();
doNothing(); // 对应3 string s[2] = {"1", "2"}; // 对应4.1 struct LiF {
string a;
};
LiF lif = {"a"}; // 对应4.2

移动构造函数

在C++11中,出现了对象移动的特性。在某些情况下,我们拷贝的对象会被立即销毁,如:使用函数调用的返回值给对象赋值。这种拷贝是不必要的,在这种情况下,更好的方法是移动(move)对象。为了支持移动操作,C++11提供了move语义以及右值引用move被定义在标准库中,用于把一个左值转换为右值引用,所谓右值引用即绑定到临时对象的引用。有了右值和move,就可以轻松定义移动构造函数:

#include <iostream>

class LiF {
public:
LiF(int _lif = 0) : lif(_lif) { std::cout << "default" << std::endl; } // 默认构造函数
LiF(const LiF& l) : lif(l.lif) { std::cout << "copy" << std::endl; } // 拷贝构造函数
LiF(LiF&& l) : lif(l.lif) { std::cout << "move" << std::endl; } // 移动构造函数
private:
int lif;
}; int main() {
LiF l1; // 调用默认构造函数
LiF l2 = l1; // 调用拷贝构造函数
LiF l3 = std::move(l1); // 调用移动构造函数
return 0;
}

当一个类同时存在拷贝和移动构造时,编译器会通过参数是否是右值判断应该使用哪种构造函数。当参数是右值时,编译器会选择移动构造,在移动构造函数中,表面上是把右值引用的对象赋值给待构造的对象,实际上,资源的所有权已经发生了改变,待构造的对象“窃取”了资源。

运算符重载

拷贝赋值运算符重载

通过重载赋值运算符=,类也可以控制对象的赋值。类似地,如果一个类没有定义拷贝赋值运算符,编译器会生成一个合成拷贝赋值运算符(synthesized copy-assignment operator)。回顾一般的赋值操作:首先给=左侧的对象赋予右侧对象的值,然后返回整个表达式的值(左侧对象)。为了与一般的赋值操作对应,在重载赋值运算符时,通常把返回类型置为对象引用并返回左侧对象(*this)。同样地,合成的拷贝赋值运算符进行的也是浅拷贝

class LiF {
public:
LiF(int *_lif): lif(_lif) {}
LiF(const LiF &l): lif(l.lif) {} // 显式定义拷贝构造函数
LiF& operator= (const LiF& l) {
lif = l.lif;
return *this;
} // 显式定义拷贝赋值运算符重载
int *lif;
}; int a;
LiF l1(&a);
LiF l2 = l1; // 此时l1、l2中的lif成员指向的都是a的地址

拷贝构造函数与拷贝赋值运算符的行为很相似,但通过他们的名字可以很好地理解它们的工作:拷贝构造函数负责拷贝一个对象并构造另一个对象,拷贝赋值运算符负责拷贝一个对象的内容并赋值给一个已存在的对象,即两者的区别为有无新对象生成。

移动赋值运算符重载

类似地,我们还可以重载=进行移动赋值。

#include <iostream>
using std::string;
using std::cin;
using std::cout;
using std::endl; class LiF {
public:
LiF(string _lif = "lif") : lif(_lif) { cout << "default" << endl; } // 默认构造函数
LiF(const LiF& l) : lif(l.lif) { cout << "copy" << endl; } // 拷贝构造函数
LiF(LiF&& l) noexcept : lif(l.lif) { cout << "move" << endl; } // 移动构造函数
LiF& operator= (const LiF &l) { // 拷贝赋值
cout << "copy=" << endl;
lif = l.lif;
return *this;
}
LiF& operator= (LiF &&l) noexcept { // 移动赋值
cout << "move=" << endl;
if (this != &l) {
std::swap(lif, l.lif);
}
return *this;
}
~LiF() { cout << "destruct" << endl; }
void print() { cout << lif << endl; }
private:
string lif;
}; int main() {
LiF l1; // 调用默认构造函数
LiF l2 = l1; // 调用拷贝构造函数
LiF l3 = std::move(l1); // 调用移动构造函数
return 0;
}

同样,移动赋值运算符接受的参数也是一个右值引用。编译器也会在需要时合成移动构造函数和移动赋值运算符。但只有当一个类没有定义任何拷贝控制,且其所有成员都是可移动构造或移动赋值时,编译器才能合成。

析构函数

最后一类特殊成员函数叫做析构函数(destructor),与构造函数相反,析构函数负责释放对象占用的资源,销毁对象的数据成员。同样地,当一个类没有定义自己的析构函数时,编译器会生成一个合成析构函数(synthesized destructor)

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

析构函数的名字为~加上类名,且不接受任何参数。每个类类型的对象被销毁都会执行自己的析构函数,内置类型没有析构函数。与构造函数不同的是,析构函数的析构部分是隐式的,并不会出现在函数体内。析构函数的函数体用于进行一些额外的操作,如:销毁相关对象,打印debug信息等。真正的析构发生在析构函数函数体之后,且成员按初始化顺序的逆序销毁。特殊地,析构函数不能被声明为delete

总结

在编写一个类时,无论是否需要,都应该显式地定义以上特殊成员函数(移动构造和移动赋值可以视情况而定)。

C++:Special Member Functions的更多相关文章

  1. C++ Member Functions的各种调用方式

    [1]Nonstatic Member Functions(非静态成员函数) C++的设计准则之一就是:nonstatic member function至少必须和一般的nonmember funct ...

  2. virtual member functions(单一继承情况)

    virtual member functions的实现(就单一继承而言): 1.实现:首先会给有多态的class object身上增加两个members:一个字符串或数字便是class的类型,一个是指 ...

  3. C++ 之const Member Functions

    Extraction from C++ primer 5th Edition 7.1.2 The purpose of the const that follows the parameter lis ...

  4. Some interesting facts about static member functions in C++

    Ref http://www.geeksforgeeks.org/some-interesting-facts-about-static-member-functions-in-c/ 1) stati ...

  5. [EffectiveC++]item23:Prefer non-member non-friend functions to member functions

    99页 导致较大封装性的是non-member non-friend函数,因为它并不增加“能否访问class内之private成分”的函数数量.

  6. 《理解 ES6》阅读整理:函数(Functions)(七)Block-Level Functions

    块级函数(Block-Level Functions) 在ES3及以前,在块内声明一个函数会报语法错误,但是所有的浏览器都支持块级函数.不幸的是,每个浏览器在支持块级函数方面都有一些细微的不同的行为. ...

  7. 《理解 ES6》阅读整理:函数(Functions)(六)Purpose of Functions

    明确函数的双重作用(Clarifying the Dual Purpose of Functions) 在ES5及更早的ES版本中,函数调用时是否使用new会有不同的作用.当使用new时,函数内的th ...

  8. 《理解 ES6》阅读整理:函数(Functions)(五)Name Property

    名字属性(The name Property) 在JavaScript中识别函数是有挑战性的,因为你可以使用各种方式来定义一个函数.匿名函数表达式的流行使用导致函数调试困难,在栈信息中难以找出函数名. ...

  9. 《理解 ES6》阅读整理:函数(Functions)(四)Arrow Functions

    箭头函数(Arrow Functions) 就像名字所说那样,箭头函数使用箭头(=>)来定义函数.与传统函数相比,箭头函数在多个地方表现不一样. 箭头函数语法(Arrow Function Sy ...

随机推荐

  1. 基于python的selenium常用操作方法(1)

    1 selenium定位方法    Selenium提供了8种定位方式. ·         id ·         name ·         class name ·         tag ...

  2. 【MySQL】MySQL 8.0的SYS视图

    MySQL的SYS视图 MySQL8.0的发展越来越趋同与Oracle,为了更好的监控MySQL的一些相关指标,出现了SYS视图,用于监控. 1.MySQL版本 (root@localhost) [s ...

  3. 【计算机网络】WebSocket实现原理分析

    1.介绍一下websocket和通信过程? 1.1 基本概念 [!NOTE] Websocket是应用层第七层上的一个应用层协议,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 T ...

  4. 深度剖析各种BloomFilter的原理、改进、应用场景

    Bloom Filter是由Bloom在1970年提出的一种多哈希函数映射的快速查找算法.通常应用在一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合. 一. 实例  为了说明B ...

  5. python基础(2):python的安装、第一个python程序

    1. 第一个python程序 1.1 python的安装 自己百度,这是自学最基本的,安装一路确定即可,记得path下打钩. 1.2 python的编写 python程序有两种编写方式: 1.进入cm ...

  6. Java 实践:生产者与消费者

    实践项目:生产者与消费者[经典多线程问题] 问题引出: 生产者和消费者指的是两个不同的线程类对象,操作同一个空间资源的情况. 需求引出: —— 生产者负责生产数据,消费者负责取走数据 —— 生产者生产 ...

  7. 区块链社交APP协议分析预告

    2017年,比特币的火热,直接导致了代币市场的繁荣: 2018年,作为信用体系的未来解决方案,区块链引发了互联网原住民的淘金热. 作为风口上的引流神器,区块链技术与社交网络结合起来,产生了一系列区块链 ...

  8. 推荐一款适合Dynamics 365/Dynamics CRM 2016 使用的弹出窗插件AlertJs

    Github地址: https://github.com/PaulNieuwelaar/alertjs 目前有两个版本,3.0版本(30天免费试用)以及2.1版本(完全免费) ------------ ...

  9. Firebase-config 在android中的使用

    说明 firebase-config提供远程配置方案,可以通过远程控制app的基本配置方案更换工作.如在特定时间更换不同的App基础配色反感,更换基础显示图标等. firebase-config fi ...

  10. 传入一个Map<String,Long> 返回它按value排序后的结果

    //传入一个Map<String,Long> 返回它按value排序后的结果 sort为正序还是倒序(-1倒序),size为要几条数据 private static Map<Stri ...