一、简介
- C++11是C++的第二个主要版本,也是自C++98以来最重要的更新。其中引入了大量的更改来标准化现有实践并改进C++程序员可用的抽象。
- C++11在2011年8月12日最终获得ISO批准之前,使用了C++0x这个名称,因为它预计在2010年之前发布。从C++03到C++11花了8年时间。所以,它已经成为迄今为止版本之间最长的间隔。从那时起,目前C++每3年定期更新一次。
- C++11的出现如下,在2003年,C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。
- 相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛化和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多。
- C++11增加的语法特性篇幅非常多,本文主要讲解实际中比较实用的语法。
二、统一的列表初始化
1、概念
- 在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
- C++11则扩大了用花括号{}括起的列表(列表初始化)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用列表初始化时,可添加等号(=),也可不添加。
- 创建对象时可以使用列表初始化方式调用构造函数初始化。
2、示例代码
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
int arr1[]{ 1,2,3,4 };
int arr2[2]{ 0 };
Test test{ 5,6 };
int i = 0;
int j = { 0 };
int k{ 0 };
Date d1(2024, 8, 3);
Date d2 = { 2024,8,3 };
Date d3{ 2024,8,3 };
const Date& d4 = { 2024,8,3 };
Date* p1 = new Date[3]{ d1,d2,d3 };
Date* p2 = new Date[3]{ {2024,8,1}, {2024,8,2},{2024,8,3} };
return 0;
}
三、initializer_list
1、概念
- 此类型用于访问C++初始化列表中的值,该列表是const T类型的元素列表。这种类型的对象由编译器从初始化列表声明中自动构造,初始化列表声明是用大括号括起来的,逗号分隔的元素列表。
- initializer_list对象会自动构造,就像分配了一个T类型的元素数组一样,使用任何必要的非缩窄隐式转换,将列表中的每个元素复制初始化为数组中的相应元素。
- initializer_list对象引用此数组的元素,但不包含它们。即复制initializer_liist对象会产生另一个引用相同底层元素的对象,而不是引用它们的新副本。
- 只接受一个此类参数的构造函数是一种特殊的构造函数,称为initializer_list构造函数。当使用initializer_list构造函数语法时,initializer_list构造器优先于其他构造函数。
- initializer_list一般是作为构造函数的参数,C++11中的STL有许多容器有用initializer_list作为参数的构造函数,这样初始化容器对象就很方便。
- initializer_list可以作为赋值运算符重载函数的参数,这样就可以用大括号对对象进行赋值。
2、示意图
3、示例代码
int main()
{
vector<int> v1 = { 1,2,3,4 };
auto il1 = { 17,18,19,20 };
cout << typeid(il1).name() << endl;
map<string, string> dict = { {"snow", "雪"}, {"dragon", "龙"}, {"snow dragon", "雪龙"} };
for (auto& e : dict)
{
cout << e.first << ":" << e.second << endl;
}
return 0;
}
4、运行结果
四、声明
1、auto
(1)概念
- 在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto没什么价值。
- 在C++11中,废弃了auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
(2)示例代码
int main()
{
int i = 0;
auto p = &i;
map<string, string> dict = { {"snow", "雪"}, {"dragon", "龙"} };
auto it = dict.begin();
cout << typeid(p).name() << endl;
cout << typeid(it).name() << endl;
return 0;
}
2、decltype
(1)作用
- 将变量的类型声明为表达式指定的类型。
(2)示例代码
int main()
{
int i = 1;
double d = 2.3;
auto ret = i * d;
cout << typeid(ret).name() << endl;
decltype(ret) k;
cout << typeid(k).name() << endl;
return 0;
}
(3)运行结果
五、左值引用与右值引用
1、左值与左值引用
- 左值是一个表示数据的表达式,如变量名或解引用的指针,可以获取它的地址和对它赋值。又如在左值定义时用const修饰符修饰,虽然不能给他赋值,但是可以获取它的地址。另外,左值可以出现在赋值符号的两边。
- 左值引用是对左值的引用,可以将左值绑定到左值引用上。左值引用通过使用 & 符号声明,并表示对某个具名对象的别名。
2、示例代码
//左值
int* p = new int(1);
int b = 1;
const int c = 2;
//对上面的左值进行引用的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
3、右值与右值引用
- 右值是一个表示数据的表达式,如字面常量、表达式返回值,函数返回值(不能是左值引用返回)等等。
- 右值只能出现在赋值符号的右边,不能出现在赋值符号的左边,且不能获取右值的地址。
- 内置类型的右值称为纯右值,自定义类型的右值称为将亡值。
- 右值引用是对右值的引用,可以将右值绑定到右值引用上。右值引用是不具名(匿名)变量的别名。
- 右值引用给右值取别名后,会导致右值被存储到特定位置,即可以取到该位置(右值引用)的地址。如果不想该右值引用被修改,可以使用const修饰它。
4、示例代码
double x = 1.2, y = 3.4;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
//rr2 = 5.5; //加const修饰不能修改
int z = 5;
//int&& rr3 = z;
int&& rr4 = move(z);
5、move
(1)函数
(2)作用
- 这是一个辅助函数,用于强制对值执行移动语义,即使它们有名称,直接使用返回的值会导致arg被视为右值。
- 标准库的许多组件实现了移动语义,允许在参数为右值时直接转移对象的资产和属性的所有权,而无需复制它们。
- 在标准库中,移动意味着被移出的对象处于有效但未指定的状态。这意味着,在这样的操作之后,从对象中移动的值只应被销毁或分配一个新值;否则访问它会产生一个未指定的值。
- move函数并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。但这个左值需要是将亡值,即它不会再被使用,因为它之前拥有的资源会被转移走。
(3)示例代码
string s1("snowdragon");
string s2("dragonsnow");
s2 = move(s1);
cout << s1 << "\n" << s2 << endl;
(4)运行结果
6、总结
- 左值引用只能引用左值,不能引用右值。但用const修饰后的左值引用既可引用左值,也可引用右值。
- 右值引用只能右值,不能引用左值。但右值引用可以引用move以后的左值。
- 左值引用和右值引用都是属于引用类型,所以,无论是声明左值引用还是右值引用,都必须立即进行初始化。
六、右值引用和移动语义
1、左值引用的短板
- 虽然左值引用做参数和返回值都可以提高效率,但是当函数的返回对象是一个局部变量时,这个局部变量出了该函数作用域就会被销毁,不能使用左值引用返回,只能传值返回。
- 传值返回会导致多次拷贝构造,如果这个传值返回的数据是一个很大的自定义类型,就会导致资源的浪费。
2、移动语义
- 移动语义是C++中的一个概念,允许程序员显式地指示编译器进行资源的移动而非复制。它通过利用临时对象的右值引用来实现,将对象的状态或所有权从一个对象转移到另一个对象,而不进行内存拷贝。
3、示例代码
namespace snowdragon
{
class string
{
public:
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)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s)-- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
snowdragon::string to_string(int x)
{
snowdragon::string ret;
while (x)
{
int val = x % 10;
x /= 10;
ret += ('0' + val);
}
reverse(ret.begin(), ret.end());
return ret;
}
}
void TestMobCon()
{
snowdragon::string s2;
s2 = snowdragon::to_string(123456789);
}
4、示意图
七、万能引用与完美转发
1、万能引用
(1)基本概念
- 概念:万能引用主要用于模板编程中,它允许函数模板的参数既能接受左值引用,也能接受右值引用。
- 定义:在模板函数中,当参数类型被声明为 T&& 时,这个参数就被称为万能引用。这里的T是一个模板类型参数,而&&并不直接表示右值引用,而是与T结合形成万能引用的特性。
- 特性:万能引用可以接受左值或右值作为实参。当传入左值时,它会被视为左值引用;当传入右值时,它会被视为右值引用。这种灵活性使得万能引用在模板编程中非常有用。
(2)用途
- 实现完美转发:万能引用常与forward一起使用,以实现完美转发。完美转发允许函数模板将其参数以原来的值类别(左值或右值)转发给另一个函数。这在编写泛型代码时非常有用,因为它可以确保参数的类型和值类别在传递过程中保持不变。
- 提升代码灵活性:通过使用万能引用,模板函数可以更加灵活地处理不同类型的参数,包括左值和右值。这有助于编写更加通用和高效的代码。
(3)万能引用与右值引用的区别
- 定义不同:万能引用是模板参数的一种特殊用法(T&&),而右值引用是变量类型的一种(X&&,其中X是确定的类型)。
- 用途不同:万能引用主要用于模板编程中,以实现完美转发等高级功能;而右值引用则主要用于实现移动语义,以提高资源管理的效率。
(4)参数转发属性变化
- 万能引用只是提供了能够同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都会退化成左值。即传递一个右值作为实参,但在万能引用中用这个右值作为实参转发(调用其他函数)时,它的属性就会变成左值。
- 万能引用的右值引用参数属性改变是因为右值不能被修改,但是右值被右值引用后,如果需要实现移动构造和移动赋值,被转移资源的对象又是自定义类型的,此时就需要转发的右值引用的属性是左值。
2、完美转发
(1)概念
- 完美转发(Perfect Forwarding)是C++11中引入的一种重要的编程技术和概念,主要用于在泛型编程中确保参数能够以最准确、最高效的方式传递给其他函数。其核心思想是在传递参数时,不仅要准确地转发参数的值,还要保持参数的左值(lvalue)或右值(rvalue)属性不变。
(2)函数
(3)目的
- 在C++中,传统的函数参数传递方式在处理右值(如临时对象)时往往会导致不必要的拷贝操作,降低了程序的性能。完美转发的目的是通过优化参数传递机制,避免这种不必要的拷贝,从而提升程序的执行效率。
3、示例代码
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);
//Fun(move(t));
Fun(forward<T>(t));
}
int main()
{
int a;
const int b = 8;
PerfectForward(10); // 右值
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
4、运行结果
- 以下为依次放开PerfectForward中注释后的运行结果。
八、后续文章
本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请点赞、收藏加关注支持一下💕💕💕