指针与引用

引用必须要初始化。

指针会根据编译器不同而变化,32位4字节,64位8字节

引用根据被引用的数据类型变化

int*& 指针的引用 以指针来判断。

右值引用 c++11

  1. 左值:可以长时间保存,可以存在于=左边的值,可以取地址;
  2. 右值:临时值,不能存在于=左边的值,不可以取地址。

左值引用,实际上是取地址赋给新的变量。必须初始化。

常引用,用于引用部分右值,不可进行更改。实际上是使用一个临时变量与一块临时内存进行存储,必须初始化。可以引用左与右。

image-20230318004324533

右值引用原理相近,临时内存的地址无法获取,但是可以对临时内存里面的内容进行修改。

1
2
int&& v1 = 10;
v1++;

右值引用是C++11新特性,之所以引入右值引用,是为了提高效率。如下面所示:

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
class A
{
public:
A(size_t N):m_p(new char[N])
{
}
A(const A & a)
{
if (this != &a)
{
delete[]m_p;
m_p = new char[strlen(m_p) + 1];
memcpy(m_p, a.m_p, strlen(m_p) + 1);
}
}
~A()
{
delete []m_p;
}

private:
char *m_p = nullptr;
};

A createA(size_t N)
{
return A(100);
}

void func(A a)
{
//
}

int main()
{
func(createA(100));

system("pause");
return 0;
}

这里会导致大量得调用A得构造函数,不考虑编译优化,原本执行如下:

1
2
3
4
5
6
createA(100),执行A(100)调用A(size_t)构造函数一次;
退出createA,临时构造得A(100),释放调用析构函数一次;
赋给返回值会调用一次拷贝构造函数一次;
返回值传入func中形参会调用拷贝构造函数一次;
func运行完成后形参释放,调用A析构函数一次;
返回值使用完成释放,调用A析构函数一次;

从上面可以看出有大量得构造、析构调用 ,但是我们做的工作无非就是临时构造一个A(100)给func使用而已。那么可否将临时A(100)始终一份给到func使用呢?答案就是右值引用。如下:

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
class A
{
public:
A(size_t N):m_p(new char[N])
{
}
~A()
{
delete []m_p;
}

private:
char *m_p = nullptr;
};

A&& createA(size_t N)
{
return (A&&)A(100);
}

void func(A&& a)
{
//
}

int main()
{
func(createA(100));

system("pause");
return 0;
}

我们将临时A(100)强制转换为了右值引用,同时func形参也是右值引用,也就是将临时对象延长到了func中,中间避免了其他构造和析构调用,提高了效率。

​ 注意到我们将A得拷贝构造函数去掉了,因为已经用不到。如果原版写法,去掉拷贝构造函数会崩溃,因为会自动调用默认拷贝构造函数,是浅拷贝,中间临时对象会提前删除公共内存,后面对象再次释放是就会重复删除内存导致崩溃。

这就是移动。它可以让你将一个对象的资源(如内存、文件句柄等)从一个临时的右值转移给另一个对象,而不需要进行深拷贝这样可以提高性能,避免不必要的内存分配和释放

image-20230319210844838

std::move可以转换左值引用为右值引用。实现原理实际上就是强制转换

1
2
3
4
5
6
7
8
9
int main()
{
int a = 3;
int &&t = std::move(a);
int &&t2 = std::move(3);

system("pause");
return 0;
}
1
2
3
4
5
6
7
8
9
10
int main()
{
int a = 3;
int &&t = (int &&)a;
t = 9;
cout << a << endl; // a = 9

system("pause");
return 0;
}

std::unique_ptr不能相等,因为他们是不可以拷贝的,因此不可以左值赋给左值。使用移动,把左值转换成右值,就可以让二者相等。

image-20230319211406922

通用引用

通用引用就是根据接受值类型可以自行推导是左值引用还是右值引用。

如果声明变量或参数具有T&&某种推导类型的类型 T,则该变量或参数为通用引用,否则就是右值引用(无法传入左值)。

也就是传入的参数在编译时需要推导,如果不需要推导,则不是通用引用。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
class B
{
public:
void print(T &&) {}
};

int main()
{
B<int> b;
b.print(3); // 为右值引用

system("pause");
return 0;
}

因为在编译print之前print中的参数已经由B b确定了,所以在print编译时无需推导,故B中的T&&为右值引用。如果改为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T>
class B
{
public:
template<typename Arg>
void print(Arg &&) {}
};

int main()
{
B<int> b;
b.print(3); // 为右值引用

system("pause");
return 0;
}

因为print时函数模板形参和类模板形参类型时独立的,故在编译print时是需要推导的,故Arg&&为通用引用。

引用折叠

引用虽然形式上是右值引用,但是却可以接受左值,这是怎么实现的呢?这就是引用折叠。

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
void print(T&& t)
{
}

int main()
{
int a = 9;
print(a);
print(9);
system("pause");
return 0;
}

print(a)时,因为a为左值,会被推导成print(int& &&t)形式,int& &&t 会被折叠为int &,所以最终形式为print(int &)。(左值被推导为左值引用)

print(9)时,为9为右值,所以被推导为print(int&& &&)形式,而int&& &&会被折叠为int&&,所以最终形式为print(int&&)。(右值被推导为右值引用)

引用类型只有两种,所以折叠形式就是4中,为:T& &,T& &&,T&& &,T&& &&。引用折叠规则概况为两种:

T&& &&折叠为T&&;

其他折叠为T&.

完美转发

通用引用既可以接受左值也可以接受右值,但是通用引用本身是左值。如果在函数模板中继续传递该值给其他函数,势必会改变该值的属性,即都为左值引用。

使用std::forward(a)可以进行完美转发,使值属性和之前保持一致。某个功能对左值和右值处理情况不一致,如果将左值和右值引用当作同一种情况使用,可能会会有性能损失。例如左值进行深拷贝,右值进行移动。

原理是使用了引用折叠。具有推导类型的T&&转换会进行引用折叠。而int&&类型是确定的,不能进行折叠。

有两套,传入的为左或右,用右值进行强制类型转换,左右转化为左,右右转化为右