构造函数

构造函数

构造函数是类的一种特殊的函数,他的任务是用来初始化类的数据成员。无论何时,只要类的对象被创建,就会执行构造函数。构造函数的结构也很简单,如下所示:

1
2
3
4
5
6
7
8
9
10
11
class SalesData {
public:
SalesData() {}
SalesData(const string& s) : bookName(s) {}
SalesData(const string& s, int id, double p) : bookName(s), bookId(id), price(p) {}

private:
string bookName;
int bookId;
double price;
}

默认的构造函数

当我们没有声明任何的构造函数的时候,编译器会为我们生成默认的构造函数

default

当我们在构造函数后面添加了default,就意味着我们显示要求编译器生成默认的构造函数。这是因为如果我们给定了任何一个构造函数后,编译器便不会再为我们生成默认构造函数了

1
2
3
4
class SalesData {
public:
SalesData() = default;
}

委托构造函数

委托构造函数是c++11的新标准。所谓的委托构造函数就是一个构造函数可以使用另一个构造函数来初始化,如下所示

1
2
3
4
5
6
7
8
9
10
11
class SalesData {
public:
SalesData() = default;
SalesData(const string& s, int id, double p) : bookName(s), bookId(id), price(p) {} // 非委托构造函数
SalesData(const string& s) : SalesData(s, 0, 0) {} // 委托构造函数

private:
string bookName;
int bookId;
double price;
}

实例

1
2
3
4
5
6
string foo1("hello");  // 直接初始化
string foo1 = "hello"; // 拷贝初始化

// 定义并初始化一个空的字符串,然后对其进行赋值
string foo2;
foo1 = "world;

我们看上面的三种方式最终的结果都是一样的,但是这三种行为可是有着本质的不同的,下面我们会来分析

拷贝构造函数

拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SalesData {
public:
SalesData() = default;
SalesData(const string& s, int id, double p) : bookName(s), bookId(id), price(p) {} // 非委托构造函数
SalesData(const string& s) : SalesData(s, 0, 0) {} // 委托构造函数
SalesData(const SalesData&); // 拷贝构造函数

private:
string bookName;
int bookId;
double price;
}

SalesData::SalesData(const SalesData& src) : bookName(src.bookName), bookId(src.bookId), price(src.price) {}

合成拷贝构造函数

如果我们没有写拷贝构造函数,那么编译器会为我们生成一个默认的拷贝构造函数,即合成拷贝构造函数。

例子

1
string foo1 = "hello";  // 拷贝初始化

上面的这种方式,就会通过拷贝构造函数来初始化的。

拷贝构造函数通常用于:

  • 通过使用另一个同类型的对象来初始化新创建的对象。
  • 复制对象把它作为参数传递给函数。
  • 复制对象,并从函数返回这个对象。

拷贝赋值函数

拷贝复制函数实际上是对“=”运算符的重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SalesData {
public:
SalesData() = default;
SalesData(const string& s, int id, double p) : bookName(s), bookId(id), price(p) {} // 非委托构造函数
SalesData(const string& s) : SalesData(s, 0, 0) {} // 委托构造函数
SalesData& operator=(const SalesData&);

private:
string bookName;
int bookId;
double price;
}

SalesData& SalesData::operator=(const SalesData& rhs)
{
bookName = rhs.bookName;
bookId = rhs.bookId;
price = rhs.price;
return *this;
}

合成拷贝赋值函数

如果我们没有写拷贝赋值函数,那么编译器会为我们生成一个默认的拷贝赋值函数,即合成拷贝赋值函数。

例子

1
2
string foo2;
foo1 = "world;

上面的操作实际上是分了两步,首先是利用默认初始化将其设置为空字符串,然后再通过拷贝赋值将其设置为“world”

移动构造函数

移动构造函数做的是移动操作,而不是拷贝操作。

移动构造函数基于右值引用的技术。参数一般为该类类型的引用,如下所示:

1
2
3
4
5
6
7
StrVec::StrVec(StrVec&& s) noexcept : 
elements(s.elements),
firstFree(s.firstFree),
capacity(s.capacity)
{
s.elements = s.firstFree = s.capacity = nullptr;
}

移动构造函数从参数对象中窃取了资源,而不是拷贝,因此经过移动构造函数后,类接管了内存,而参数已经丧失了资源的控制,因此在函数内,我们将其置nullptr。而资源的管理则有类接管。

移动赋值运算符

跟移动构造类似,移动赋值函数也是基于右值引用。

1
2
3
4
5
6
7
8
9
10
StrVec& StrVec::operator=(StrVec&& rhs) noexcept {
if (this != &rhs) {
free();
elements = rhs.elements;
firstFree = rhs.firstFree;
capacity = rhs.capacity;
rhs.elements = rhs.firstFree = rhs.capacity = nullptr;
}
return *this;
}

同样的,如果不存在移动构造函数/移动赋值运算符,编译器会为我们合成移动构造函数/移动赋值运算符,但是并不会为所有的类都合成默认的移动构造函数/移动赋值运算符。只有当一个类没有任何自己定义的拷贝控制函数,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数/移动赋值运算符。

移动和拷贝

如果一个类既有移动构造函数/移动赋值,又有拷贝构造函数/拷贝赋值,那么会调用那个呢?其实很简单,因为移动构造函数/移动赋值使用过的是右值,而拷贝构造函数/拷贝赋值,使用过的是左值。因此可以根据传入的参数来决定具体调用哪一个。如下所示:

1
2
3
strVec v1, v2;
v1 = v2; // 这里使用的拷贝赋值,因为v2是左值
v2 = getVec(v1) // 这里使用的是移动赋值,因为getVec(v1) 是一个右值。

如果类没有定义移动构造/移动赋值,那么右值也将会被拷贝/赋值。

析构函数

析构函数用于在对象生命周期结束时,用来释放所申请的内存资源。具体的格式如下所示:

1
2
3
4
class Foo {
public:
~Foo() {}
}

合成析构函数

如果我们没有写析构函数,那么编译器会为我们生成一个默认的析构函数,即合成析构函数。

但是但是如果我们在类中使用了动态内存等,一定不要依赖合成的析构函数,因为这样会造成内存泄露,除非用的智能指针。

析构函数也可以使用default关键字,用于通知编译器生成默认的析构函数

自定义的类的行为

行为像值的类

所谓行为像值的类是指当我们赋值的时候,执行的是拷贝操作,而不是指针共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "string"
using namespace std;
class HasPtr
{
private:
string* ps;
int i;
public:
HasPtr(const string& s = string()) : ps(new string(s)), i(0) {}
HasPtr(const HasPtr& p) : ps(new string(*(p.ps))), i(p.i) {}
HasPtr& operator=(const HasPtr& rhs) {
auto newP = new string(*rhs.ps); // 这一行和下一行的顺序不行颠倒,否则当p = q,p和q是同一对象时会挂掉。
delete ps;
ps = newP;
i = rhs.i;
return *this;
}
~HasPtr();
};

行为像指针的类

行为像指针的类是指当我们对类的实例执行下面的赋值操作时(p = q),并不是拷贝操作,而是他们两个指向同一个地址

下面的这段代码使用引用计数的方式来实现行为像指针的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class HasPtr
{
private:
string* ps;
int i;
size_t* use;
public:
HasPtr(const string& s = string()) : ps(new string(s)), i(0), use(new size_t(1)) {}
HasPtr(const HasPtr& p) : ps(new string(*(p.ps))), i(p.i), use(p.use) { ++*use; }
HasPtr& operator=(const HasPtr& rhs) {
++*rhs.use++;
if (--*use == 0) {
delete ps;
delete use;
}
ps = rhs.ps;
i = rhs.i;
use = rhs.use;
return *this;
}
~HasPtr() {
if (--*use == 0) {
delete ps;
delete use;
}
}
};

也可以使用智能指针的格式来重写上面的小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class HasPtr
{
private:
shared_ptr<string> ps;
int i;
public:
HasPtr(const string& s = string()) : ps(make_shared<string>(s)), i(0){}
HasPtr(const HasPtr& p) : ps(make_shared<string>(p.ps)), i(p.i) {}
HasPtr& operator=(const HasPtr& rhs) {
ps = rhs.ps;
i = rhs.i;
return *this;
}
~HasPtr() {}
};

显示 Gitment 评论