C++第四十三弹---C++11新特性深度解析:让你的代码更现代、更高效(下)
新的类功能:默认成员函数,类成员变量初始化,强制生成默认函数的关键字default,禁止生成默认函数的关键字delete;可变参数模板:递归函数方式展开参数包,逗号表达式展开参数包,数组构造的过程展开参数包,函数返回值展开参数包
✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】
目录
1 新的类功能
1.1、默认成员函数
原来C++类中,有6个默认成员函数:
- 1. 构造函数
- 2. 析构函数
- 3. 拷贝构造函数
- 4. 拷贝赋值重载
- 5. 取地址重载
- 6. const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
代码演示
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
// 写一个析构就不会默认生成移动构造,移动赋值
//~Person()
//{}
private:
lin::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
// 没有写拷贝构造,赋值操作符重载,析构函数中的任意一个,会默认生成移动构造和移动赋值
// 默认生成的构造,内置类型按字节拷贝,自定义类型有移动调用移动,没移动调用拷贝
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
运行结果
注意:前面有string的模拟实现。
1.2、类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个我们在类和对象默认就讲了,这里就不再细讲了。
1.3、强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
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(Person&& p) = default;
private:
lin::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
1.4、禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁而已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即
可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
// 禁止生成拷贝构造
Person(const Person& p) = delete;
private:
lin::string _name;
int _age;
};
int main()
{
Person s1;
// s2为不存在的对象,此时会调用拷贝构造,没有拷贝构造因此报错
Person s2 = s1;
return 0;
}
修改main函数
int main()
{
Person s1;
// s2为已经存在的对象,调用赋值重载
Person s2;
s2 = s1;
return 0;
}
2 可变参数模板
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 Cpp_Printf(Args... args)
{
// 计算参数包的数据个数
cout << sizeof...(args) << endl;
// error C3520 : “args”: 必须在此上下文中扩展参数包
// 不支持
//for (size_t i = 0; i < sizeof...(args); i++)
//{
// cout << args[i] << endl;
//}
//cout << endl;
}
int main()
{
Cpp_Printf(1);
Cpp_Printf(1, 'A');
Cpp_Printf(1, 'A', std::string("sort"));
return 0;
}
运行结果
2.1、递归函数方式展开参数包
方式一
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
运行结果
方式二
void _Cpp_Printf()
{
cout << endl;
}
template<class T,class ...Args>
void _Cpp_Printf(const T& val, Args... args)
{
cout << val << endl;
_Cpp_Printf(args...);
}
template<class ...Args>
void Cpp_Printf(Args... args)
{
_Cpp_Printf(args...);
}
int main()
{
Cpp_Printf(1);
Cpp_Printf(1, 'A');
Cpp_Printf(1, 'A', std::string("sort"));
return 0;
}
运行结果
2.2、逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在 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)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在
2.3、数组构造的过程展开参数包
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;
}
运行结果
2.4、函数返回值展开参数包
代码演示
template<class T>
int PrintArg(T t)
{
cout << t << " ";
return 0;
}
template<class ...Args>
void Cpp_Printf(Args... args)
{
int arr[] = { PrintArg(args)... };
cout << endl;
}
int main()
{
Cpp_Printf(1);
Cpp_Printf(1, 'A');
Cpp_Printf(1, 'A', std::string("sort"));
return 0;
}
运行结果
STL容器中的empalce相关接口函数:
template <class... Args>
void emplace_back (Args&&... args);
template <class... Args>
void emplace_front (Args&&... args);
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和emplace系列接口的优势到底在哪里呢?
代码一
int main()
{
// 没区别
list<lin::string> lt;
lin::string s1("11111111");
lt.push_back(s1);
lt.push_back(move(s1));
cout << endl;
lin::string s2("11111111");
lt.emplace_back(s2);
lt.emplace_back(move(s2));
return 0;
}
运行结果
代码二
int main()
{
list<pair<lin::string,int>> lt;
pair<lin::string, int> kv1("xxxxx", 1);
lt.push_back(kv1);
lt.push_back(move(kv1));
cout << endl;
// 与前面没区别
//pair<lin::string, int> kv2("xxxxx", 1);
//lt.emplace_back(kv2);
//lt.emplace_back(move(kv2));
// 直接构造pair的参数包,参数包一直传递下去,底层是构造
lt.emplace_back("xxxxxx",1);
return 0;
}
运行结果
从上图我们可以看到,使用emplace_back是直接构造,而使用push_back需要先构造再拷贝构造,证明有些情况下emplace_back效率更高。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)