c++11
- 1. {}初始化
- 2. 范围for循环
- 3. final与override
- 4. 右值引用
- 4.1 左值引用和右值引用
- 4.2 左值引用与右值引用比较
- 5. lambda表达式
- 6. 声明
- 6.1 auto
- 6.2 decltype
- 6.3 nullptr
- 7. 可变参数模版
1. {}初始化
在C++中,使用花括号初始化的方式被称为列表初始化。列表初始化可以用于数组、结构体、类等类型的初始化。在C++11之前,列表初始化仅能用于数组和POD类型的初始化。C++11新标准将列表初始化应用于所有对象的初始化。以下是一些使用列表初始化的例子:
struct Point
{
int _x;
int _y;
};
class foo
{
public:
foo(int i, double d) :_i(i), _d(d) {} // 构造函数
private:
int _i;
double _d;
};
int main()
{
int arr[]{ 1, 2, 3, 4, 5 }; // 数组
Point p{ 1,2 }; // 结构体
foo f{ 1, 3.14 }; // 类
return 0;
}
以上代码中,arr 是一个整型数组,使用列表初始化方式进行了初始化;p 是一个结构体,使用列表初始化方式对其成员变量进行了初始化;f 是一个类对象,使用列表初始化方式对其成员变量进行了初始化。
2. 范围for循环
在C++中,范围for循环是一种用于遍历数组、容器、初始化列表等类型的语法结构。它的语法格式如下:
for (declaration : expression)
{
// 循环体
}
其中,declaration 表示遍历声明,在遍历过程中,当前被遍历到的元素会被存储到声明的变量中。expression 是要遍历的对象,它可以是表达式、容器、数组、初始化列表等。
以下是一个使用范围for循环的例子:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto value : v)
{
std::cout << value << " ";
}
std::cout << std::endl;
return 0;
}
以上代码中,v 是一个整型向量,使用范围for循环方式进行了遍历。
需要注意的是,在使用范围for循环遍历容器时,循环会自动以容器为范围展开,并且循环中也屏蔽掉了迭代器的遍历细节,直接抽取容器中的元素进行运算,使用这种方式进行循环遍历会让编码和维护变得更加简便。
其中还有一个点需要注意,上述value相当于一个形参,也就是说value改变,不影响数组v的改变,那么怎么在遍历的时候又能修改数组v?其实可以使用引用访问元素,如下:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
cout << "没有使用引用访问:";
for (auto value : v)
{
value *= 2;
}
for (auto value : v)
{
std::cout << value << " ";
}
std::cout << std::endl;
cout << "使用引用访问:";
for (auto &value : v)
{
value *= 2;
}
for (auto value : v)
{
std::cout << value << " ";
}
std::cout << std::endl;
return 0;
}
在C++中,使用范围for循环时,如果使用引用访问元素,可以避免对元素进行拷贝,从而提高程序的效率。
当使用值访问元素时,会对元素进行一次拷贝,而使用引用访问元素时,则不会进行拷贝。因此,在不改变元素的情况下,使用引用访问元素可以减少一次拷贝,提高程序的效率。
3. final与override
在C++11中,override 和 final 是两个新的关键字,用于增强代码的安全性和可读性。
override 用于在派生类中重写基类的虚函数时,显式地告诉编译器此函数是重写基类的虚函数。如果重写时函数名、参数列表和返回类型都和基类的虚函数一致,但是没有加上 override 关键字,那么编译器无法判断是否是故意的重写,容易导致程序出错。加上 override 关键字后,编译器会在编译时检查是否真的重写了基类的虚函数,如果没有则会报错,从而避免了这种错误。
final 用于修饰类、函数或者变量,表示它们是终态的,不能被派生类、重写或者修改。使用 final 关键字可以防止子类再覆写父类的虚函数。如果一个虚函数被声明为 final,则派生类不能再重写它。
4. 右值引用
4.1 左值引用和右值引用
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时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);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
//10 = 1;
//x + y = 1;
//fmin(x, y) = 1;
return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。
4.2 左值引用与右值引用比较
- 左值引用是用符号 & 声明的,它只能绑定到左值,即可以取地址、有名字、非临时的对象。左值引用可以用来修改或读取所绑定的对象的值。
- 右值引用是用符号 && 声明的,它只能绑定到右值,即不能取地址、没有名字、临时的对象。右值引用可以用来延长所绑定的对象的生命周期,或者实现移动语义,即将对象的资源从一个所有者转移到另一个所有者,而不需要进行拷贝。
- 一个例外是,const左值引用可以绑定到右值,这样可以实现对右值的只读访问,而不改变其生命周期。
- 另一个例外是,在函数重载时,如果有一个参数既可以接受左值引用,又可以接受右值引用,那么编译器会优先选择左值引用。这是为了避免对左值进行不必要的移动操作。
左值引用和右值引用的优缺点如下:
- 左值引用的优点是可以对所引用的对象进行修改或读取,而不需要拷贝或移动。左值引用的缺点是不能绑定到右值,如果需要绑定到右值,必须使用常量左值引用,但这样就不能修改所引用的对象了。
- 右值引用的优点是可以实现移动语义,减少拷贝或赋值操作的开销,提高程序的效率。右值引用的缺点是不能修改所引用的对象,而且会改变所引用对象的状态,使其失去资源的所有权。
5. lambda表达式
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法,但是如果排序的是自定义类型元素,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
在C++11中,Lambda表达式是一种用于定义匿名函数的语法结构。Lambda表达式可以用于任何需要函数对象的地方,例如函数参数、返回值、STL算法等。Lambda表达式的语法格式如下:
[capture-list] (parameters) mutable -> return-type { statement };
lambda表达式各部分说明:
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[] {};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=] {return a + 3; };
// 省略了返回值类型,无返回值类型
auto fun1 = [&](int c) {b = a + c; };
fun1(10);
cout << a << " " << b << endl;
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int {return b += a + c; };
cout << fun2(10) << endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;
return 0;
}
int main()
{
int x = 10, y = 20;
auto func1 = [](int x = 1,int y = 2) //当捕捉列表和参数列表都有x,y,优先用参数列表中的值。
{
cout << x + y << endl; // 3
};
func1();
return 0;
}
int main()
{
int x = 0, y = 1;
int m = 0, n = 1;
auto swap1 = [](int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
};
swap1(x, y);
cout << x << " "<< y << endl;
// 引用捕捉
auto swap2 = [&x, &y]()
{
int tmp = x;
x = y;
y = tmp;
};
swap2();
cout << x << " " << y << endl;
// 混合捕捉
auto func1 = [&x, y]()
{
//...
};
// 全部引用捕捉
auto func2 = [&]()
{
//...
};
// 全部传值捕捉
auto func3 = [=]()
{
//...
};
// 全部引用捕捉,x传值捕捉
auto func4 = [&, x]()
{
//...
};
return 0;
}
lambda表达式的优点是可以使代码更加简洁紧凑,并且可以避免定义不必要的函数对象。Lambda表达式的缺点是可能会降低代码的可读性和可维护性。
6. 声明
6.1 auto
auto是C++11引入的一个关键字,用于在声明变量时自动推导变量的类型。auto的使用可以让编译器在编译期间自动推算出变量的类型,这样就可以更加方便的编写代码了。auto还可以用于定义函数返回值类型,但此时auto仍然使用的是模板实参推断的机制,因此返回类型为auto的函数如果返回一个初始化列表,则会出错。
6.2 decltype
在C++11中,decltype 是一种用于推导表达式类型的关键字。decltype 可以用于推导变量、函数返回值、表达式等的类型。decltype 的语法格式如下:
decltype(expression)
其中,expression 是要推导类型的表达式。
以下是一个使用 decltype 的例子:
#include <iostream>
int main()
{
int i = 42;
decltype(i) j = i + 1;
std::cout << "i = " << i << ", j = " << j << std::endl;
return 0;
}
以上代码中,使用了 decltype 推导了变量 j 的类型。
decltype 的优点是可以在编译期间推导出表达式的类型,从而提高程序的效率。decltype 的缺点是可能会降低代码的可读性和可维护性。
6.3 nullptr
C++11中,nullptr是一个用于表示空指针的关键字,它可以替代C++03中的0或NULL。nullptr的类型是std::nullptr_t,它可以隐式转换为任意类型的指针或成员指针,但不能转换为整数类型或布尔类型。nullptr的优点是可以避免一些类型推导的歧义,例如在函数重载或模板参数推导时。nullptr的缺点是可能会与一些旧代码不兼容,例如使用NULL作为整数常量的代码。
在C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
7. 可变参数模版
在C++11中,可变参数模板是一种用于定义可变数量参数的模板。它允许模板函数或类接受任意数量的参数,包括类型、非类型和模板参数。可变参数模板的语法格式如下:
template<typename... Args>
void foo(Args... args)
{
// 函数体
}
其中,Args 是一个模板参数包,可以接受任意数量的模板参数。在函数体中,可以使用 args… 来展开参数包,以便对每个参数进行操作。
可变参数模版中有一点需要注意,在使用sizeof()求可变参数的个数时,应该这样书写:sizeof…(args),//错误格式sizeof(args…);如下所示:
template <class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl; //求可变参数的个数
}
int main()
{
ShowList(1);
ShowList(1, 2.2);
ShowList(1, 2.2, "hello");
return 0;
}
如何解析可变参数包?这里使用递归来解决。以下是一个使用可变参数模板的例子:
void ShowList() //函数重载,当参数个数为0时,没有该函数,就会找不到匹配的函数
{
cout << endl;
}
template <class T, class ...Args>
void ShowList(const T& val, Args... args) //每次从参数包中解析一个
{
cout << val << " ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 2.2);
ShowList(1, 2.2, "three");
return 0;
}
可变参数模板的优点是可以使代码更加灵活和通用,可以接受任意数量和类型的参数。可变参数模板的缺点是可能会降低代码的可读性和可维护性。