作者:@阿亮joy.
专栏:《吃透西嘎嘎》
座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
传统的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址 + 可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时 const 修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 下面的语句都会编译报错,左操作数必须为左值
//10 = 1;
//x + y = 1;
//fmin(x, y) = 1;
return 0;
}
左值可以引用右值吗?右值可以引用左值吗?
// x既能接受左值,又能接受右值
template <class T>
void Func(const T& x)
{
//...
}
int main()
{
// 左值引用可以引用右值吗?const的左值引用可以
double x = 1.1, y = 2.2;
//double& rr1 = x + y; // 编译报错
const double& rr2 = x + y; // 可以
// 右值引用可以引用左值吗?不可以,可以引用move以后的左值
int a = 10;
//int&& rr3 = a; // 编译报错
int&& rr5 = move(a);
return 0;
}
左值引用与右值引用总结:左值引用只能引用左值,不能引用右值。但是 const 左值引用既可引用左值,也可引用右值。右值引用只能引用右值,不能引用左值,但是右值引用可以 move 以后的左值。
需要注意的是:右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。也就是说,不能取字面量 10 的地址,但是 rr1 引用后,可以对 rr1 取地址,也可以修改 rr1。如果不想 rr1 被修改,可以用 const int&& rr1 去引用。注:rr1 和 rr2 都是左值。
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
//rr2 = 5.5; // 报错
cout << &rr1 << endl;
cout << &rr2 << endl;
return 0;
}
左值引用解决的问题:
- 做参数:a. 减少拷贝,提高效率。b. 做输出型参数
- 做返回值:a. 减少拷贝,提高效率。b. 引用返回,可以修改返回对象(比如:
operator[]
)
左值引用既可以引用左值和又可以引用右值,那为什么C++11 还要提出右值引用呢?其实左值引用无法解决一些场景的问题,所以就提出了右值引用。
C++11 的右值引用的一个重要功能就是要解决上面的问题,但右值引用并不是直接作为返回值起作用的。
注:只有在一个表达式里,编译器才能够优化,上图的场景无法优化。对象已经存在,只能用to_string
生成的临时对象调用赋值运算符重载赋值给ret
。
那么在string
类里添加移动构造和移动赋值就能够解决上面的问题了。
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
添加移动构造和移动赋值后,就不会存在深拷贝了。在bit::string中增加移动构造和移动赋值,移动构造和移动赋值本质是将参数右值的资源转移到指定的对象中,那就不需要深拷贝了。
注:右值有两类,第一类是纯右值,即内置类型右值;第二类是将亡值,即自定义类型右值。右值将亡值的资源可以转移到指定的对象,而左值不能。移动构造和移动赋值是延长了资源的生命周期。
完整代码
namespace Joy
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
// 现代写法
//string tmp(s._str);
//swap(tmp);
// 传统写法
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
reverse(str.begin(), str.end());
return str;
}
}
STL 中的容器都是增加了移动构造和移动赋值的。
STL 容器的插入接口函数也增加了右值引用版本。
模板中的 && 万能引用
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用:既能引用左值,也能引用右值
// 引用折叠
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
可以看到,上面的引用通通被折叠成左值引用。其实这可以用上面的一个知识点来解释,当右值引用右值时,那么这个引用的属性也是左值,其有自己的地址。
std::forward 完美转发在传参的过程中保留对象原生类型属性。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用:既能引用左值,也能引用右值
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
注:如果想要一直保持对象的元素类型,就要一直完美转发。注:只有模板参数采用万能引用,确定的类型没有万能引用。
原来C++类中,有 6 个默认成员函数:构造函数、析构函数、拷贝构造函数,赋值运算符重载、取地址重载和 const 取地址重载。重要的是前 4 个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个:移动构造函数和移动赋值运算符重载。拷贝构造函数和赋值运算符重载是针对左值的拷贝,而移动构造和移动赋值时针对右值的拷贝。不需要深拷贝的类,也就不需要自己写移动构造和移动赋值。拷贝对象需要深拷贝是,自己写移动构造和移动赋值。比如:string、vector 和 list 等。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行浅拷贝,对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行浅拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用赋值运算符重载。
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。如果没有移动构造和移动赋值,才会去调用拷贝构造和赋值运算符重载。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
// 会默认生成拷贝构造和移动构造
/*Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}*/
/*Person& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}*/
/*~Person()
{}*/
private:
Joy::string _name;
int _age;
};
int main()
{
Person s1("张三", 18);
Person s2 = s1; // 拷贝构造
Person s3 = std::move(s1); // 移动构造
Person s4;
s4 = std::move(s2); // 移动赋值
return 0;
}
注释掉 string 的移动构造和移动赋值
类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化。这个在类和对象就讲了,这里就不再细讲了。
强制生成默认函数的关键字 default
C++11 可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成。
禁止生成默认函数的关键字 delete
如果能想要限制某些默认函数的生成,在 C++98 中,可以将该函数设置成 private,这样只要其他人想要调用就会报错。在 C++11 中更简单,只需在该函数声明加上 = delete 即可,该语法指示编译器不生成对应函数的默认版本,称 = delete 修饰的函数为删除函数。
要求使用 delete 关键字实现一个类只能在堆上创建对象
// 只能在堆上创建对象
class HeapOnly
{
public:
void Destroy()
{
delete[] _str;
// delete this 也要调用析构函数
operator delete(this);
}
HeapOnly()
{
_str = new char[10];
}
~HeapOnly() = delete;
private:
char* _str;
};
int main()
{
//HeapOnly hp1;
//static HeapOnly hp2;
HeapOnly* ptr = new HeapOnly;
ptr->Destroy();
//delete ptr; // 编译报错
return 0;
}
注:delete 主要做两件事:1. 调用对象的析构函数 2. 调用 operator delete 回收对象的内存。operator delete 底层也是调用 free 来释放空间的。
继承和多态中的 final 与 override 关键字
final 可以修饰一个类,表示这个类不能被继承;也能修饰一个虚函数,表示这个虚函数不能被重写。override 修饰子类的虚函数,如果子类的虚函数没有完成重写,就会编译报错。
在 C 语言中,我们就已经接触过可变参数了,只是没有深入了解过。其实可变参数底层是用一个数组来接收这些参数的,要用时将这些参数从数组取出来。
C++11 将可变参数扩展到模板,C++11 的新特性可变参数模板能够让你创建可以接受可变参数的函数模板和类模板。相比 C++98 / 03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,以后大家如果有需要,可以再深入学习。
下面就是一个基本可变参数的函数模板
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数 args 前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为参数包,它里面包含了 0 到 N(N>=0)个模版参数。我们无法直接获取参数包 args 中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用 args[i] 这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
计算可变参数的个数
// 可变参数的模板
template <class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
string str("hello");
ShowList();
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', str);
return 0;
}
// 模板参数包不支持以下玩法
for (size_t i = 0; i < sizeof...(args); ++i)
{
cout << args[i] << " ";
}
cout << endl;
递归函数方式展开参数包
// 递归终止函数
void ShowList()
{
cout << "ShowList()" << endl;
}
// 展开函数,参数包args包含N个参数(N>=0)
template <class T, class ...Args>
void ShowList(const T& val, Args... args)
{
cout << "ShowList(" << val << ", 参数包args有 " << sizeof...(args) << " 个参数)" << endl;
ShowList(args...); // 递归调用
}
int main()
{
string str("hello");
ShowList(1, 'A', str);
return 0;
}
逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在ShowList
函数体中展开的, Printarg
不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式,逗号表达式会按顺序执行逗号前面的表达式。
ShowList
函数中的逗号表达式(printarg(args), 0)
是先执行printarg(args)
,再得到逗号表达式的结果 0。同时还用到了C++11 的另外一个特性始化列表,通过初始化列表来初始化一个变长数组,{(printarg(args), 0)...}
将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... )
,最终会创建一个元素值都为 0 的数组int arr[sizeof...(Args)]
。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分Printarg(args)
打印出参数,也就是说在构造数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
template <class T>
void PrintArg(const T& val)
{
cout << val << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
for (size_t i = 0; i < sizeof...(args); ++i)
{
cout << arr[i] << ' ';
}
cout << endl;
}
int main()
{
string str("hello");
ShowList(1, 'A', str);
return 0;
}
函数调用展开参数包
template <class T>
int PrintArg(const T& val)
{
cout << val << " ";
return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { PrintArg(args)... };
cout << endl;
for (size_t i = 0; i < sizeof...(args); ++i)
{
cout << arr[i] << ' ';
}
cout << endl;
}
int main()
{
string str("hello");
ShowList(1, 'A', str);
return 0;
}
当一个函数需要传多个参数,但你并不知道要传多少个参数,就可以将这个函数弄成可变参数函数模板。
STL 容器中的 empalce 相关接口函数
首先我们看到的 emplace 系列的接口,支持模板的可变参数,并且万能引用。那么相对 insert 和 emplace 系列接口的优势到底在哪里呢?
push_back 是先构造一个对象,再用这个对象拷贝构造到对应空间中去;而 emplace_back 是将参数包往下传,然后直接调用构造函数将对象构造出来。所以,在某些情况下 emplace 的接口更加高效。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
Date& operator=(const Date& d)
{
cout << "Date& operator=(const Date& d))" << endl;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 没有区别
vector<int> v1;
v1.push_back(1);
v1.emplace_back(2);
// 有区别
list<Date> lt1;
lt1.push_back(Date(2022, 11, 16));
cout << "---------------------------------" << endl;
lt1.emplace_back(2022, 11, 16);
return 0;
}
本篇博客主要讲解了什么是左值引用和右值引用、左值引用和右值引用的区别、完美转发、类的新功能和可变参数模板等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!❣️