C++语法细节复习(1)

MyString

最普通的String模仿, 没有追加功能函数 :

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <iostream>
#include <cstring>
using namespace std;

class MyString
{
public:
MyString(const char *str = nullptr)
{
if (!str)
{
mptr = new char[1];
*mptr = '\0';
}
else
{
mptr = new char[strlen(str) + 1];
memcpy(mptr, str, strlen(str) + 1);
}
}

~MyString()
{
if (!mptr)
delete[] mptr;
}

MyString(const MyString &cp)
{
cout << "触发左值拷贝" << endl;
mptr = new char[strlen(cp.mptr) + 1];
memcpy(mptr, cp.mptr, strlen(cp.mptr) + 1);
}

MyString(MyString &&cp)
{
cout << "触发右值拷贝" << endl;
mptr = cp.mptr;
cp.mptr = nullptr;
}

MyString &operator=(const MyString &cp)
{
cout << "触发左值赋值重载" << endl;
if (this == &cp)
return *this;

delete[] mptr;

mptr = new char[strlen(cp.mptr) + 1];
memcpy(mptr, cp.mptr, strlen(cp.mptr) + 1);
return *this;
}

MyString &operator=(MyString &&cp)
{
cout << "触发右值赋值重载" << endl;
if (this == &cp)
return *this;

delete[] mptr;

mptr = cp.mptr;
cp.mptr = nullptr;
return *this;
}

char *&getPtr() { return mptr; }
char *getPtr() const { return mptr; }

private:
char *mptr;
};

MyString operator+(const MyString &first, const MyString &second)
{
MyString tmp;
tmp.getPtr() = new char[strlen(first.getPtr()) + strlen(second.getPtr()) + 1];
strcpy(tmp.getPtr(), first.getPtr());
strcat(tmp.getPtr(), second.getPtr());
return tmp;
}

ostream &operator<<(ostream &out, const MyString &str)
{
out << str.getPtr();
return out;
}

int main()
{
MyString str("hello");
MyString str2(" world!");
str = move(str2);
MyString str3(MyString("222"));
MyString str4 = MyString("111");
cout << str4 << endl;
cout << str << endl;
return 0;
}

右值引用

  • 左值 : 有内存, 有名字 | 右值 : 没内存, 没名字
  • int&& d = 20;
    • 一个右值引用变量本身是一个左值, 只不过引用了一个右值而已.
    • 20是右值, d是左值, 也是右值引用变量.

引用折叠 / 完美转发

我们先引入一个例子 : vector的push_back, 其可以有两个版本, 分别对应接收左值和右值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void push_back(const T& val)
{
if(full()) expand();

_allocator.construct(_last, val);
_last++;
}

void push_back(T&& val)
{
if(full()) expand();

_allocator.construct(_last, std::move(val));
_last++;
}

其区别在于, 因为右值在使用时会降级为左值, 所以在右值版本必须通过move保证传递的依然是个左值. 而引用折叠 + 完美转发可以帮助我们合并这两个版本.

引用折叠

  • & + & = &

  • & + && = &

  • && + && = &&

完美转发

  • forward() : 使用这个函数可以保证右值不会降级.

函数模板类型推导 + 引用折叠 + 完美转发 实现左右值版本合并

1
2
3
4
5
6
7
8
template<typename Ty>
void push_back(Ty&& val)
{
if(full()) expand();

_allocator.construct(_last, std::forward<Ty>(val);
_last++;
}

这个版本等同于我们以上两个版本, 这里的逻辑是 :

  • 使用函数模板开启类型推导, 不再按照定死的类型判断, 也就是可以使用引用折叠了.
  • 如果传入左值, T& && -> T&.
  • 如果传入右值, T&& && -> T&&.
  • forward将val按照原来的类型完美转发出去, 不改变其左右值类型.

于是我们通过 函数模板类型推导 + 引用折叠 + 完美转发 实现了代码简化, 让其可以同时处理左右值情况, 这种操作适用于希望同时接受左右值同时对左右值没有操作上的区别的函数, 你可以认为这就是引用折叠和完美转发最主要的作用了.

当然完美转发的用途更加广泛一些, 比如 :

  • 在工厂函数中根据不同类型转发不同的参数.

    1
    2
    3
    4
    5
    template<typename T, typename... Args>
    T* create(Args&&... args)
    {
    return new T(std::forward<Args>(args)...);
    }

    create就在分析完T的类型后向T的构造函数中完美转发参数包, 也就是说, 他可以为任何类型在堆上开辟空间, 只用参数和其构造函数匹配.

  • 原地构造, 最经典的就是vector的emplace_back.

    1
    2
    3
    4
    template<typename... Args>
    void emplace_back(Args&&... args) {
    construct_at(end(), std::forward<Args>(args)...);
    }

    在以前push_back只可以传入对应类型的对象, 这样还要进行拷贝再填入到开辟的空间中, 而有了完美转发, 就可以直接在开辟的空间上用传入的参数直接调用对应类型的构造函数, 这样更加快速.

对象优化的原则

  • 函数参数传递过程中, 对象优先传引用传参, 不要传值传参.

  • 当函数返回对象时, 返回临时对象效率最优, 而非先定义局部对象然后返回局部对象.

    • 返回临时对象会直接在main函数栈帧上构建该对象, 不受这个函数影响. 如果是后者还要将原本构建的局部对象拷贝构造到main函数的栈帧上, 离开函数作用域还要调用局部对象的析构, 也就是说会多两个函数调用.
  • 当函数返回对象时, 优先按初始化方式接收, 而非初始化完了再赋值.

    • 因为这个返回的对象肯定是右值, 所以可以直接拿来用来构建, 都省去了再拷贝操作. 不过如果有了移动语义, 右值进行拷贝或赋值重载的消耗很小, 其实也可以忽略不计.
    • 在C++17中有RVO机制, 要求强制返回值优化, 也就说上面的行为在C++17以前可能算是编译器优化, 但在C++17以后就算是语言特性了.

C++语法细节复习(1)
http://example.com/2025/04/28/C++语法细节复习(1)/
作者
天目中云
发布于
2025年4月28日
许可协议