📃博客主页: 小镇敲码人
💚代码仓库,欢迎访问
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞
【C++进阶】之C++11的简单介绍(二)
- 新的类功能
- 移动构造和移动赋值
- 当某个类没有显式实现移动赋值和移动构造
- 右值的自定义成员是否为右值
- 强制生成默认函数的关键字--- default
- 强制生成默认函数的关键字--- delete
- 继承和多态中的final与override关键字
- 可变参数模板的语法
- 递归展开参数包
- 可变参数模板的底层原理
- 逗号表达式展开参数包
- C++11中的emplace_back系列函数
- emplace_back与push_back效率比较
新的类功能
移动构造和移动赋值
移动构造我们前面已经详细的讲述过了,这里不再重复的赘述,这里主要就移动赋值做一下阐述。
看下面的代码,思考有没有出现不必要的拷贝,如何优化?
#include"string.h"
int main()
{
my_string::string ret1;
ret1 = my_string::to_string(12345);
cout << ret1 << endl;
return 0;
}
利用移动拷贝优化,减少这一次不必要的拷贝:
string& string::operator= (string&& str)
{
cout << "string& string::operator= (string && str)" << endl;
swap(str);
return *this;
}
我们把临界资源转移减少了不必要的拷贝,同时把ret1
的资源交给临界资源去释放。
- 只有需要深拷贝的类需要移动构造和移动赋值方法,因为浅拷贝没有需要申请空间的资源,所耗时间不多。
当某个类没有显式实现移动赋值和移动构造
- 如果你实现了移动构造或者移动赋值,编译器不会再提供拷贝构造和拷贝赋值。
实现任何一个,编译器都会把拷贝构造和拷贝赋值删除。
- 如果你没有实现移动构造,且没有实现析构函数 、拷贝构造、拷贝赋值重载、移动赋值中的任
意一个。那么编译器会默认生成一个移动构造,对于内置类型它是逐字节赋值;对于自定义类型,如果这个自定义类型自己实现了移动构造,则会去调用它的移动构造,如果没有,就调用拷贝构造。
- 当
A
中有自定义类型变量,且实现了A
的移动构造,需要在初始化列表显示掉该自定义类型的移动构造:
这里会有人质疑,可能你的x
接收了一个右值,但是你能保证这个右值里面的自定义成员也是右值吗?这里答案是显然的,右值的所有成员都是右值,我们可以来验证一下:
右值的自定义成员是否为右值
#include<iostream>
using namespace std;
class B
{
public:
B()
{}
B(B&& x)
{
cout << "B(B&& x)" << endl;
}
};
class A
{
public:
A()
{}
A(A&& x)
:b(move(x.b))
{
cout << "A(A&& x)" << endl;
}
public:
int a = 3;
B b;
};
int main()
{
A a;
A b(move(a));
move(a).b = 3;
cout << &move(a).b << endl;
}
这里我们将a move
后,它返回一个右值,如果它的成员是左值,应该可以修改和取地址,最关键的是看取地址,因为const
属性的变量也不允许修改:
- 如果你没有实现移动赋值重载,且没有实现析构函数 、拷贝构造、拷贝赋值重载、移动构造中的任
意一个。那么编译器会默认生成一个移动赋值重载函数,对于内置类型它是逐字节赋值;对于自定义类型,如果这个自定义类型自己实现了移动赋值重载,则会去调用它的移动赋值重载,如果没有,就调用拷贝赋值。
如果显式的实现了移动赋值重载函数,类中有自定义类型成员,需要在函数中显式的调用它,减少拷贝。
强制生成默认函数的关键字— default
当我们在写一些方法的时候,会出现写了部分方法,部分默认方法被删除的情况,这种情况常见于构造函数和赋值构造函数中,比如当你显式的写了带参的构造函数,不带参的默认构造函数就会被删除,这个时候我们可以使用default
,强制它生成。
class A
{
public:
A(int data):
data_(data)
{
}
private:
int data_;
};
int main()
{
A a;
}
报错:
强制生成默认构造函数:
强制生成默认函数的关键字— delete
当我们在实现某些类的时候,如果不希望它被拷贝,通常有两种方式:
- 将其拷贝函数私有化:
#include<iostream>
using namespace std;
class A
{
public:
A(int data) :
data_(data)
{
}
A() = default;
private:
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
A(A&& a)
{
cout << "A(A&& a)" << endl;
}
A& operator=(const A& a)
{
cout << "A& operator=(const A& a)" << endl;
return *this;
}
A& operator=(A&& a)
{
cout << "A& operator=(A&& a)" << endl;
return *this;
}
int data_;
};
int main()
{
A a;
A b(3);
a = b;
a = move(b);
A tmp1(b);
A tmp2(move(b));
return 0;
}
2.C++11中更简单,直接将相应的默认函数删除。
#include<iostream>
using namespace std;
class A
{
public:
A(int data) :
data_(data)
{
}
A() = default;
A(const A& a) = delete;
A& operator=(const A& A) = delete;
private:
int data_;
};
int main()
{
A a;
A b(3);
/*a = b;*/
a = move(b);
/*A tmp1(b);*/
A tmp2(move(b));
return 0;
}
同类型的左值和右值函数,我们删除一个就可以了,另外一个默认函数就不会生成了。
继承和多态中的final与override关键字
final
在继承中有两个作用:- 阻止基类被继承。
class Base final {
public:
void func() {
// 实现
}
};
// 下面的代码会编译失败,因为Base是final的
class Derived : public Base {};
int main()
{
return 0;
}
- 还有一个作用是C++14出现的,是阻止虚函数重写,这里我们不具体介绍了。
2.override
override
明确表示一个子类重写了基类中的虚函数。
这样做有以下好处:
- 编译器检查:如果你标记了一个函数为override,但基类中并没有一个同名的虚函数供其重写,编译器会报错。这有助于防止一些由于拼写错误或类型不匹配而导致的潜在问题。
- 提高代码可读性:通过override关键字,其他开发者可以更容易地理解这个函数的意图和作用。
class Base {
public:
virtual void func() {
// 实现
}
};
class Derived : public Base {
public:
void func() override {
// 重写实现
}
};
基类如果没有对应虚函数就会报错(构成重写的要求见多态和继承部分博客)。
可变参数我们在C语言部分就已经接触过,它们我们再熟悉不过,就是scanf
和printf
函数:
这里我们的printf
就是可变参数,我们可以给它传很多东西进去,要想把printf
的原理完全弄清楚,需要学习系统部分的知识,在操作系统部分,我们会再谈到printf
,这里就不详谈了。
在C++11前,我们的模板参数都是固定的个数,C++11出现了可变模板参数,这无疑是一个巨大的改进,但是可变模板参数比较抽象,使用起来没有那么容易:
可变参数模板的语法
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的args
前面有省略号,所以它就是一个可变模板参数,我们把可变模板参数又叫做参数包。参数包里面有0~N个模板参数(N >= 0),我们无法直接获取参数包中的参数,这既是参数包的特点,也是难点,因为语法上不支持args[i]
的形式,我们只能通过一些其它方法来获取参数包中的参数和值。
递归展开参数包
#include<iostream>
#include<vector>
using namespace std;
void Func_()//0个参数
{
cout << "参数解析完毕" << endl;
}
template<class T, class ... Args>
void Func_(T& data, Args... args)
{
cout << data << " data的类型为:" << typeid(data).name() << endl;
Func_(args...);
}
template<class ...Args>
void Func(Args... args)
{
Func_(args...);
}
int main()
{
Func(0, 1, 2, "xxxxxx", 'c');
return 0;
}
运行结果:
我们还写了一个递归终止函数,即参数为0时的情况。
可变参数模板的底层原理
所谓的可变参数模板,无非是函数模板实例化之后推演生成匹配的函数,如果有现成的就不需要实例化:
逗号表达式展开参数包
使用这种方式展开参数包,不再需要写递归返回函数。
#include<iostream>
#include<vector>
#include<string.>
using namespace std;
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
运行结果:
我们对代码关键部分int arr[] = { (PrintArg(args), 0)... };
做一下解释
- 参数包展开:在函数模板
ShowList
中,Args... args
是一个参数包,它可以接受任意数量和类型的参数。在int arr[] = { (PrintArg(args), 0)... };
中的使用是尝试对参数包中的每个元素进行展开,并对每个元素执行(PrintArg(args), 0)
操作。 - 逗号表达式:逗号运算符用于顺序执行两个表达式,并返回最后一个表达式的值。在我们的例子中,
PrintArg(args), 0
会先调用PrintArg
函数(该函数接受一个参数并打印它),然后表达式的结果为 0(因为逗号运算符返回其右侧表达式的值)。
但是由于我们是使用的初始化列表来初始化的数组,没有指定数组的大小,编译器只能通过初始化列表来推断数组的大小,但是这里初始化列表是通过模板参数包展开的,在很多编译器上无法推断数组大小,可能会报错。
#include<iostream>
#include<vector>
using namespace std;
template <class ...Args>
void ShowList(Args... args)
{
vector<int> arr = {args... };
for (auto num : arr)
cout << num << " ";
cout << "\nvector的大小:" << arr.size();
cout << endl;
}
int main()
{
ShowList(1, 2, 3, 4, 4, 5, 6, 7, 8, 9);
return 0;
}
运行结果:
在我们的编译器上可以正常运行。(VS2019)
C++11中的emplace_back系列函数
这个函数也是用来插入元素的。
它支持模板的可变参数和万能引用,万能引用也是引用,可以减少拷贝,我们看它的底层肯定也会走完美转发:
那它和insert
系列的函数有什么区别呢?
特征 | emplace_back | insert (非emplace 版本) | emplace (insert 的一个版本) | push_back |
---|---|---|---|---|
位置 | 容器末尾 | 指定位置 | 指定位置 | 容器末尾 |
元素构造 | 直接在容器内存构造 | 先构造再复制/移动 | 直接在容器内存构造 | 先在容器外部构造(可能需要复制或移动) |
效率 | 高效(避免复制/移动) | 可能较低(涉及复制/移动) | 高效(避免复制/移动) | 可能较低(涉及复制或移动) |
灵活性 | 较低(仅限于末尾) | 高(任意位置) | 高(任意位置) | 较低(仅限于末尾) |
使用示例 | vec.emplace_back("Hello", 5, '!'); | vec.insert(vec.begin() + 1, "x"); | vec.emplace(vec.end(), "Hello", 5, '!'); | std::string temp = "Hello" + std::string(5, '!'); vec.push_back(temp); (先构造字符串,再复制或移动到容器) |
我们用代码来验证一下上面的内容:
#include<iostream>
#include<list>
#include"string.h"
using namespace std;
int main()
{
list<my_string::string> l1;
l1.emplace_back("xxxxxxxxx");
cout << "------------------------------------" << endl;
l1.push_back("xxxxxxxxxxxx");
return 0;
}
运行结果:
emplace_back
减少了一次移动构造,因为它是直接在容器内存构造。emplace_back
可以在内存中直接构造所依赖的两个条件:
- 定位
new
:允许我们对已经分配内存,但还没有创建对象的内存初始化并创建对象,也就是显式的调用构造函数。 - 参数包,允许我们传多个不同类型的值来作为定位
new
表达式的参数(构造函数的参数)。push_back
不行,只能传T
作为参数,T
是容器里元素的类型,但是这个元素可能是自定义类型,它里面还有其它的成员。
我们通过阅读源码,发现vs2019的库里面,emplace_back
确实使用到了定位new。
因为它使用到了定位new
,所以我们猜测,vector
的erase
系列的方法里面,并不直接释放对象的空间,而是要显式的去释放该对象的资源(去调用T
的析构函数):
和我们想的一致,这样那片空间又变成没有创建对象的空间了。
emplace_back与push_back效率比较
- 插入内置类型。
#include<iostream>
#include<list>
#include<vector>
#include"string.h"
#include<time.h>
using namespace std;
void test1()
{
vector<int> v1;
const int N = 100000000;
size_t begin1 = clock();
for (int i = 0; i < N; ++i)
{
v1.push_back(i);
}
size_t end1 = clock();
vector<int> v2;
size_t begin2 = clock();
for (int i = 0; i < N; ++i)
{
v2.emplace_back(i);
}
size_t end2 = clock();
cout << "push_back: " << end1 - begin1 << endl;
cout << "emplace_back: " << end2 - begin2 << endl;
}
int main()
{
test1();
return 0;
}
运行结果:
2.插入浅拷贝的自定义类型对象(直接给插入对象参数的情况下)。
emplace_back
可以减少一次拷贝。
- 注意:由于
emplace_back
是可变模板参数的形式,它不支持{}
初始化列表对象作为它的参数,这和它的设计初衷是有关系的(可变模板参数(如模板函数中的…)主要是为了提供一种灵活的方式来处理未知数量和类型的参数。它们允许开发者编写能够接受任意数量和类型参数的模板代码,但并未直接关联到特定的数据结构或初始化方式,如初始化列表。)
#include<list>
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
std::list<Date> lt1;
lt1.push_back({ 2024,3,30 });
cout << "----------------------------" << endl;
// 不支持
//lt1.emplace_back({2024,3,30 });
// 推荐
lt1.emplace_back(2024, 3, 30);
return 0;
}
运行结果:
测试效率:
#include<list>
#include<iostream>
#include<time.h>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
//cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
//cout << "Date(const Date& d)" << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
void test2()
{
const int N = 10000000;
list<Date> l1;
size_t begin1 = clock();
for (int i = 0; i < N; ++i)
{
l1.push_back({ rand() + i,rand() + i,rand() + i });
}
size_t end1 = clock();
list<Date> l2;
size_t begin2 = clock();
for (int i = 0; i < N; ++i)
{
l2.emplace_back(rand() + i, rand() + i, rand() + i);
}
size_t end2 = clock();
cout << "push_back效率:" << end1 - begin1 << endl;
cout << "emplace_back效率:" << end2 - begin2 << endl;
}
int main()
{
srand(time(NULL));
test2();
return 0;
}
运行结果:
emplace_back
也没有快多少。
3.插入深拷贝的自定义类型对象(有移动构造和移动赋值重载函数)。
#include<list>
#include<iostream>
#include<time.h>
#include"string.h"
using namespace std;
void test2()
{
const int N = 10000000;
list<my_string::string> l1;
size_t begin1 = clock();
for (int i = 0; i < N; ++i)
{
l1.push_back("XXXXXXXXXXXXXXXXXX");
}
size_t end1 = clock();
list<my_string::string> l2;
size_t begin2 = clock();
//cout << "----------------------" << endl;
for (int i = 0; i < N; ++i)
{
l2.emplace_back("XXXXXXXXXXXXXXXXXXXXXXX");
}
size_t end2 = clock();
cout << "push_back效率:" << end1 - begin1 << endl;
cout << "emplace_back效率:" << end2 - begin2 << endl;
}
int main()
{
srand(time(NULL));
test2();
return 0;
}
运行结果:
push_back
每次只用多调用一次移动构造。
- 深拷贝的自定义类型作为其成员但是没有实现移动拷贝和移动赋值重载。
还是刚刚的代码,我们把我们自己实现的string
的移动构造给注释。
此时的区别就是push_back
比emplace_back
多了一次拷贝构造(深拷贝),但是emplace_back
不需要,因为它是使用const char*
直接在内存里初始化它的元素,调用对应的构造函数。
此时push_back
慢了1秒,计算机的1s可以计算很多东西,可以看出移动构造还是很重要的,在需要深拷贝的类中,我们尽量要实现移动构造和移动赋值。
总结:emplace_back
这种插入的效率很客观,我们可以多使用它。尤其是一些没有实现深拷贝的类,使用emplace_back
(给插入对象参数情况下),可以减少一次深拷贝。
- 本人知识、能力有限,若有错漏,烦请指正,非常非常感谢!!!
- 转发或者引用需标明来源。