C++中的构造,赋值,引用,移动

C++中的构造,赋值,引用,移动

Tue Feb 04 2025
tech
Tag: C++ move ref copy

一、(拷贝)构造,(拷贝)赋值,引用

1.(拷贝)构造

构造即为一个实例对象分配空间以及初始化的过程

C
1
2
3
4
5
6
7
8
9
class Type
{
pubilc:
	int i;
	Type(int input)
	{
		i = input;
	}
}

类中仅含有i一个成员,Type的构造函数接收一个int参数并赋值给i。

注:这里的i不是严格意义的初始化,严格意义的初始化应为Type(int input):i(input){}

构造一个Type实例对象:

直接调用构造函数构造
C
1
2
3
4
5
Type inst(6);
//或:
Type inst{6};
//或:
Type inst = Type(6);

注:第三种初始化很像Type函数返回的临时对象创建后被拷贝或移动到 inst。在C++17以前语义确实如此,但大多数编译器会优化为Type inst(6)。C++17及以后该过程(即优化为Type inst(6))写入标准被保证,即两者严格等价。

从已有的对象(拷贝)构造

为此添加(拷贝)构造函数,它接收一个Type类对象作为参数

C
1
2
3
4
Type(Type other)
{
	this -> i = other.i;
}

使用:

C
1
2
Type a(6);
Type b(a);

第二行调用了b的(拷贝)构造

更进一步的,我们将(拷贝)构造函数优化为

C
1
2
3
4
Type(const Type other)//添加const确保被调用者的安全
{
	this -> i = other.i;
}

2.(拷贝)赋值

类对象的赋值是指将一个对象的值赋给另一个对象。

为实现该过程需要为类添加(拷贝)赋值函数(即重载”=“运算符)

C
1
2
3
4
void operator=(const test &other)
{
    this->i = other.i;
}

使用:

C
1
2
3
Type a(6);
Type b(0);
b = a;

在第三行触发了b的的赋值函数,将a的i赋值给了b的i

进一步的,为实现链式赋值(即a=b=c=d=……),优化为返回自身的引用

C
1
2
3
4
5
Type& operator=(const test &other)
{
    this->i = other.i;
    return *this;
}

注:赋值函数不强制使用“重载=”的形式,它既可以是全局函数也可以是成员函数,只要能达到“赋值”的目的即为赋值函数。

3. 引用

引用分为左值引用和右值引用,关于左值和右值参见C++的那些事——左值、纯右值和将亡值 - 知乎

引用即为指针常量的包装,本质上是指针的语法糖,目的是简化c语言中多级指针满天飞的场景,引用同样会有指针相似的问题(如悬空引用等),也可以理解为变量的“别名”

引用常用于替代指针、不可复制类型的传参、对象复用等

引用为C++内置语法,无需增添额外函数

创建引用时必须初始化

左值引用

C
1
2
Type a(6);
Type& b = a;

其中b为a的(左值)引用。

a,b操作同一变量,对a(b)的操作等价于对b(a)的操作

C
1
2
3
4
5
Type a(6);
Type& b = a;
std::cout<< a.i << " " << b.i <<"\n";//输出"6 6"
b.i++;
std::cout<< a.i << " " << b.i <<"\n";//输出"7 7"

引用不支持更改绑定对象(参照常量指针)

C
1
2
3
4
5
6
7
8
9
10
int main()
{
    T a(9);
    T b(100);
    T &c = a;
    c = b;//不是将c绑定的对象改为b而是将b赋值给c
    std::cout << a.i<<" " << b.i<<" " << c.i << '\n';
    return 0;
}
//输出:"100 100 100"

注:虽然引用本质是指针,但引用在 C++ 中是语言级的特性,并不会调用重载的 operator&。因此,即使你在类中禁用了 operator&(例如将其删除),引用仍能正确工作,不受影响。

注:Type& b = a(即绑定一个左值,这里是a)中不会为b分配{int i}的空间,也不会调用b的构造函数,同时也不会调用a的任何函数(如析构函数),左值引用引用左值不会更改被引用对象(这里是a)的生命周期,c++规定创建引用时必须初始化在一定程度上是为了确保引用的生命周期(即b)小于等于被引用的(即a)生命周期,以避免悬空引用。而指针不要求创建时初始化,使得经常出现悬空指针。引用相较于指针更安全,指针相较于引用更灵活(如动态绑定,晚绑定等)

右值引用

对右值进行的引用:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
T fun()//函数的返回值为右值
{
    return T(1);
}

int main()
{
    T &&a = fun();//绑定一个右值,整个流程只会创建一次内存
    T b = fun();//创建并赋值,整个流程会创建两次内存
    std::cout<<a.i<<'\n';//合法,a的绑定延长了fun返回值临时变量的生命周期
    return 0;
}

注:同Type inst = Type(6),T b = fun()在C++17以前会大概率被优化为T&& b = fun(),c++17以后必定被优化

右值引用会延迟被引用的右值的生命周期至与该引用一致,如T &&a = fun()中a绑定了fun的临时返回值变量,按照标准fun的临时返回值变量应在该语句后析构,但由于a是该右值的引用,该临时对象的生命周期被提升为与a一致,使得对其操作合法

常量左引用绑定左值

被const修饰的引用不可修改所绑定的对象

C
1
2
3
4
Type a(6);
const Type& b = a;
a.i =10;//合法
b.i =10;//不合法

常量左引用绑定右值

常量左引用既可以绑左值,也可以绑右值,绑定右值时等价于常量右引用

C
1
2
3
4
5
6
7
8
9
10
11
void fun(const T& a)//a既可以绑右值又可以绑左值
{
    std::cout << a.i << '\n';
}
int main()
{
    T a(1);
    fun(a);//a为左值
    fun(T(2));//T(2)为右值
    return 0;
}

常量右引用

不可修改的右引用

C
1
2
3
4
5
6
7
8
9
10
11
12
T fun()
{
    return T(1);
}

int main()
{
    const T && a = fun();
    a.i = 10;//非法
    return 0;
}

注:常量右引用只能绑右值,常量左引用既能绑右值又能绑左值

二、移动

0.移动的意义

移动适用于含堆上内存类的资源复用

考虑一个类:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class T
{
private:
    char *data;
    size_t len;
public:
    T(const char* input)
    {
        len = strlen(input);
        data = static_cast<char*>(malloc(len));
        strcpy(data, input);
    };
    ~T()
    {
        free(data);
    }
};

当对其进行拷贝时会发生三个问题:

一、堆内存的重复释放:

对其进行拷贝:

C
1
2
3
4
5
6
int main()
{
    T a("hello");
    T b = a;
    return 0;
}

注:如果类中没有定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,即分别调用其成员拷贝构造函数(浅拷贝)

运行出错:

错误原因:

当b被赋值时,b中的指针与a中的指针相同,当a,b离开作用域时分别调用其析构函数导致同一内存被析构两次产生错误

解决方案:

在赋值构造函数单独开辟空间然后赋值堆上数据(深拷贝)

C
1
2
3
4
5
6
T(const T& other)
{
    this->len = other.len;
    this->data = static_cast<char *>(malloc(other.len));
    memcpy(this->data, other.data, sizeof(char) * other.len + 1);
}

二、堆内存的浪费

考虑一个场景:

C
1
2
3
4
5
6
7
8
9
T fun()
{
	return T("hello");
}
int main()
{
    T b = fun();//假设编译器不进行返回值优化c
    return 0;
}

由fun产生的返回值临时变量仅仅作为数据的传递,却需要一次深拷贝,非常浪费。我们有没有办法在保证性能(只使用浅拷贝)的情况下又能保证内存安全呢?

三、不可拷贝的类型

某些类(如std::thread)不允许拷贝,如何将其资源“移动”到另一个对象中呢?

1.移动构造

考虑一个类:

C
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
class T
{
private:
    char *data;
    size_t len;

public:
    T(const char *input)
    {
        len = strlen(input);
        data = static_cast<char *>(malloc(len));
        strcpy(data, input);
    };
    T(const T &other)
    {
        this->len = other.len;
        this->data = static_cast<char *>(malloc(other.len));
        memcpy(this->data, other.data, sizeof(char) * other.len + 1);
    }
    ~T()
    {
        free(data);
    }
};

我们实现一个功能:传入一个不会在被使用的对象(右值,以区分拷贝构造的左值,实现不同重载对应不同构造方式),得到其堆上资源(浅拷贝),同时保证内存安全(不会多次释放)

我们可以为其添加一个“标记”flag,用true代表该对象未被”移动“,data指向的内存由自己使用、释放。用false标识已经被移动,data指向的资源不属于自身,不应释放data.

为此添加移动标记和构造函数

C
1
2
3
4
5
6
7
bool flag = 1;
T(T &&other)
{
    this->len = other.len;
    this->data = other.data;
    other.flag = 0;
}

同时重写析构函数以确保内存安全

C
1
2
3
4
5
6
7
~T()
{
    if(flag==1)
    {
    	free(data);
    }
}

2.移动拷贝

同拷贝构造,拷贝赋值也会导致以上三点问题

为此我们引入移动赋值

C
1
2
3
4
5
6
void operator=(T&& other)
{
    this->data = other.data;
    this->len = other.len;
    other.flag = 0;
}

同样的,为实现链式赋值,我们返回自身引用,同时我们可以将”data指针是否为空“作为是否被移动的标志,最后的代码为

C
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class T
{
private:
    char *data;
    size_t len;

public:
    T(const char *input)
    {
        len = strlen(input);
        data = static_cast<char *>(malloc(len));
        strcpy(data, input);
    };
    T(const T &other)
    {
        this->len = other.len;
        this->data = static_cast<char *>(malloc(other.len));
        memcpy(this->data, other.data, sizeof(char) * other.len + 1);
    }
    T(T &&other)
    {
        this->len = other.len;
        this->data = other.data;
        other.data = nullptr;
    }
    T& operator=(const T &other)
    {
        this->len = other.len;
        this->data = static_cast<char *>(malloc(other.len));
        memcpy(this->data, other.data, sizeof(char) * other.len + 1);
        return *this;
    }
    T& operator=(T&& other)
    {
        this->data = other.data;
        this->len = other.len;
        other.data = nullptr;
        return *this;
    }
    ~T()
    {
        if(data)
        {
            free(data);
        }
    }
};