目录
1.C++11简述
2.统一的初始化列表
2.1{}的初始化
2.2std::initializer_list
3.声明
3.1auto
3.2decltype
3.3nullptr
4.智能指针
5.右值引用和移动语义
5.1左值引用和右值引用内容
5.2左值引用和右值引用比较
5.3左值引用和右值引用总结
6.类的新功能
6.1default
6.2delete
7.可变参数模板
8.Lambda表达式
8.1引入
8.2Lambda表达式
8.3Lambda语法
C++11是在2011年完成了一项C++语言标准,较于C++98/03,C++11在数量上带来了更多可观了变化。其中包括140多种新特性,以及对C++03标准中600多个缺陷的修正。
总的而言,C++11能够更好的用于系统的开发和库的设计,其语法更加的简洁、稳定和安全。不仅功能更加的强大,而且提高了程序设计者开发的效率。
所以对于其中一些标志性内容,我们需要进行一个简单的了解和认识,这也正是本篇博客的内容所在,即对C++11当中的部分重要特性进行讲解。
C++11当中扩大了花括号内的列表(初始化列表)的适用范围,使其能够适用于所有内置类型和用户自定义类型的初始化,并且使用时可以添加=号,也可以不添加。具体的使用案例如下:
#include
using namespace std;
struct Point {
int _x;
int _y;
};
class Data {
public:
Data(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{
cout << "is Data()\n";
}
private:
int _year;
int _month;
int _day;
};
int main() {
//old:
Point p1 = { 1,2 };
Data d1(2022, 1, 1);
//new:
Point p2{ 1,2 };
Data d2{ 2022,1,1 };//调用构造函数进行初始化
exit(0);
}
我们也可C++11当中的初始化列表对new表达式当中的内容进行初始化。
initializer_lis是一种数据类型,具体的类型内容我们可以通过下述代码来得到。
int main() {
auto li = { 1,2,3 };
cout << typeid(li).name();
exit(0);
}
然后执行代码,便可与得到{1, 2, 3}的数据类型为:class std::initializer_list
这样一种类型的提出是让我们在可以使用初始化列表{}来初始化对象,在C++11当中对STL当中的许多容器添加了initializer_list作为参数的构造方法,这样我们在初始化容器时便可与直接使用{}来进行初始化。
例如在vector当中的声明便有:
auto是C++11当中用于在变量声明时自动推导变量的类型,所以auto类型的变量在声明时必须显示的初始化,可以让编译器推断出变量的类型。
auto x = 10; // x的类型将被推导为int
auto y = 3.14; // y的类型将被推导为double
auto z = "hello"; // z的类型将被推导为const char*
不过值得注意的内容是:auto关键字的使用是在编译时进行类型推导,而不是在运行时进行动态类型推断。因此,一旦变量的类型被推导出来,它将保持不变,不能再改变为其他类型。
我们也可以将auto与容器当中的迭代器一起使用,便于声明迭代器变量,如下述代码:
std::vector numbers = {1, 2, 3, 4, 5};
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
// 使用迭代器访问元素
std::cout << *it << " ";
}
C++11当中关键字decltype可以将变量类型声明为一种表达式指定的类型。如下述代码所示:
int x = 5;
decltype(x) y; // y的类型将被推导为int
值得注意的是:auto类型并不能在函数参数列表中声明,而decltype计算得到的类型可以。
并且decltype还可以用于获取类成员的类型。例如:
struct MyClass {
int x;
double y;
};
MyClass obj;
decltype(obj.x) a; // a的类型将被推导为int
decltype(obj.y) b; // b的类型将被推导为double
不过decltype是在编译过程中进行类型推导,仅类型推导,不会执行表达式或求值。只用于获取表达式的静态类型信息,并将其用于类型声明或类型推导。
在C++中NULL被定义成为字面常量0,但这样直接的定义可能会带来一些问题,因为0既能代表指针常量,又能表示整形常量。所以处于安全和清晰考虑,在C++11当中引入nullptr,仅用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以参考我之前的文章。
在C++11中,引入了右值引用(R-value reference)和左值引用(L-value reference),这两个引用类型对于理解C++语言中的值类别和移动语义至关重要。
左值引用(L-value reference):
例如:
int x = 10;
int& lref = x; // 左值引用绑定到左值表达式x
右值引用(R-value reference):
例如:
int&& rref = 5; // 右值引用绑定到右值表达式5
右值引用的引入主要为了支持移动语义和完美转发(perfect forwarding),它使得在某些情况下可以避免不必要的对象拷贝和内存分配,提高性能和效率。
需要注意的是,通过使用`std::move()`函数,可以将左值转换为右值引用,以便在适当的情况下触发移动语义的操作。
int x = 10;
int&& rref = std::move(x); // 将x转为右值引用
在C++11中,左值引用(L-value reference)和右值引用(R-value reference)是两种不同的引用类型,它们在语义和使用上有一些关键区别:
1. 绑定对象类型:
- 左值引用主要用于绑定到具有名称的左值表达式,例如变量、对象。
- 右值引用主要用于绑定到临时对象、字面量、表达式等右值。
2. 可修改性:
- 左值引用可以修改所引用对象的值。
- 右值引用也可以修改所引用对象的值,但通常用于实现移动语义和完美转发,而不是直接修改对象的值。
3. 生命周期:
- 左值引用的生命周期通常与所引用的对象的生命周期相同。当引用超出作用域或被重新绑定时,引用将变为悬空引用(dangling reference)。
- 右值引用的生命周期通常短暂,常用于临时对象或作为函数返回值,它们的生命周期在表达式求值后结束。
4. 移动构造:
- 右值引用引入了移动构造,它允许高效地将资源(如动态分配的内存)从一个对象转移到另一个对象,而无需执行昂贵的拷贝操作。
- 移动构造通过移动构造函数和移动赋值运算符实现,它们接受右值引用参数并将资源从源对象“移动”到目标对象。
例如在vector中的push_back方法便提供了右值引用带来的移动构造,避免资源的过分开辟。
当我们调用vector当中push_back时,采用不同的方法,会发现调用的具体函数内容也不相同,我们可以打开vector文件进行调试,发现同上图一样的规律。
当然我们也可以如同下面代码的书写方式(std::move()),来将左值引用转化为右值引用,然后实现移动语义。
int main() {
std::string str1("ni hao");
std::string str2(str1);//拷贝构造
std::string str2(std::move(str1));//移动构造
return 0;
}
不过要注意,经过move转化为右值引用之后,str1当中的内容便不能再修改了,因为它的资源已经转移给str2了,它本身已经不存在存储内容了。
5. 完美转发:
- 右值引用还支持完美转发(perfect forwarding),它允许函数将参数按原样转发给其他函数,保留参数的值类别和常量性。
- 完美转发通过使用模板和右值引用参数来实现,它在实现通用代码和函数包装器时非常有用。
左值引用和右值引用在C++11中的引入提供了更多的灵活性和性能优化的机会。它们的使用要根据具体的情况来选择,以实现代码的高效性、可读性和正确性。
左值引用本质上来说是对一个变量起别名,我们在修改左值引用内容时,实际上也是在修改原有变量内容,并且左值引用和原有变量代表的是同一份内存,如下所示:
对于右值引用看似是对常值进行的引用,但是右值引用是可以改变的。其本质上是对常值引用内容开辟了一份内存空间(空间的别名),我们对这份空间对应的内容进行了修改,这是可行的,如下图所示:
可以看出通过对常值10进行右值引用之后,x被分配到了一份内存空间,我们可以对这份内存空间代表的内容进行修改。
我们对于左值引用的期待便是对变量进行统一管理,尤其是在函数传参当中,对于右值引用的期待便是尽量少的创建资源,提高程序的运行效率。
在之前的C++版本当中,当我们创建类的时候,编译器会默认生成6个成员函数来供我们进行使用,它们分别是:构造函数、析构函数、拷贝构造函数、拷贝赋值运算符重载、取地址运算符重载、const取地址运算符重载。
而在C++11当中为其新增了2个默认构造函数,分别是:移动构造函数和移动拷贝赋值运算符重载。
C++11当中提供了一些关键字,可以帮助我们更好的控制类的默认构造函数。default便是其中之一,使用default关键字可以指定默认的构造函数,让其一定会被生成。
class Test {
public:
Test() = default;
Test(int t)
: _t(t)
{}
private:
int _t;
};
int main() {
Test t1;
return 0;
}
类似于上述代码,如是不采用default关键字修饰默认的构造函数,则会产生报错,即不存在构造函数对t1初始化。通常我们解决此类问题的方式是为自己构造函数的参数列表中的参数加入默认值,或是显式的写出编译器提供的构造函数。default关键字为我们提供了更多的选择。
delete关键字我们在C++智能指针博客中模拟实现unique_ptr提到过,它的功能是删除默认的构造函数,即禁止编译器生成被delete修饰的默认函数。
这样能够防止我们不经意间调用类中默认生成的函数,将程序问题暴露在编译阶段。
C++11引入了可变参数模板的功能,使得函数或类模板能够接受可变数量的参数。可变参数模板允许您在编写代码时不确定参数的数量,并根据需要处理这些参数。
#include
// 递归终止条件:没有参数时停止递归
void printArgs() {
std::cout << std::endl;
}
// 使用可变参数模板打印参数
template
void printArgs(T first, Args... args) {
std::cout << first << " ";
printArgs(args...); // 递归调用printArgs函数
}
int main() {
printArgs(1, 2, 3, "Hello", 4.5);
return 0;
}
在上面的示例中,我们定义了一个名为`printArgs`的函数模板。它有两个重载版本:一个是没有参数的版本作为递归终止条件,另一个是带有模板参数`T`和`Args...`的版本,用于打印参数并递归调用自身来处理剩余的参数。
在`main`函数中,我们调用`printArgs`函数,并传递了不同类型和数量的参数。函数将逐个打印这些参数,直到没有剩余参数为止。
在C++98当中,当我们面对将一份数据集合及进行排序的时候,我们可以使用的方法是std::sort,注意包含文件将#include
并且,sort默认是按照降序排列元素内容的,当我们需要按照升序排序时,需要在方法后面加入greater关键字来实现。我们给出如下实例:
#include
#include
int main() {
int arry[] = { 2,5,1,6,7 };
//默认降序排列
std::sort(arry, arry + sizeof(arry) / sizeof(arry[0]));
int array[] = { 2,9,7,8,6 };
//greater,升序排列
std::sort(array, array + sizeof(array) / sizeof(array[0]), std::greater());
return 0;
}
如果需要排列的内容是自定义的数据类型,则还需我们自己给出排序规则,具体情况可以复习运算符重载。
不过这样繁琐的工程量显然是我们不愿意看到的,每次当我们实现一次algorithm算法,都需要重新去封装一个类。并且如果每次比较逻辑不一致的话,我们还需去封装多个类来满足每一次的比较逻辑。因此,在C++11当中,提供了lambda表达式来对其进行优化。
#include
#include
#include
#include
using namespace std;
class Student {
public:
Student(string name, int score)
: _name(name)
, _score(score)
{}
string _name;
int _score;
};
int main() {
vector s = { {"张三", 90}, {"李四", 89}, {"王五", 70} };
//Lambda表达式
sort(s.begin(), s.end(), [](Student& s1, Student& s2) { return s1._score < s2._score; });
return 0;
}
最终上述代码执行得到的结果如下图所示:
我们可以看出,Lambda表达式实际上便是一个匿名函数,以[]为开始,输入形参和对应的返回值。
Lambda表达式的书写格式如下:
[capture list] (parameter list) -> return type {
Function Body// 函数体
}
其中每个部分的解释如下:
1.Capture List (捕获列表): 用于在Lambda表达式中捕获外部变量,使得Lambda可以在其函数体中访问这些变量。捕获列表可以为空,也可以使用以下方式进行捕获:
[ ]
: 空捕获列表,Lambda函数体无法访问任何外部变量。[
var1, var2, ...]
: 按值捕获指定的外部变量。[
&var1, &var2, ...]
: 按引用捕获指定的外部变量。[
=]
: 以值方式捕获所有外部变量,Lambda函数体可以访问当前作用域内的所有变量,但不能修改它们。[
&]
: 以引用方式捕获所有外部变量,Lambda函数体可以访问并修改当前作用域内的所有变量。[
this]
: 捕获当前对象(用于访问成员变量和成员函数)。2.Parameter List (参数列表): 指定Lambda函数的参数,与普通函数的参数列表一样。
3.Return Type (返回类型): 指定Lambda函数的返回类型。使用auto
关键字可以让编译器自动推断返回类型。
4.Function Body (函数体): 包含Lambda函数的执行代码。