C++11新增了花括号初始化方式:
int main()
{
vector<int> v1 = { 1,2,3 };
vector<int> v2{ 1,2,3 };
list<int> lt1 = { 1,2,3 };
list<int> lt2{ 1,2,3 };
auto x = { 1,2,3 };
cout << typeid(x).name() << endl;
return 0;
}
这里的x如果直接用花括号初始化,默认是initializer_list< int >类型,这个类型其实相当于一个容器,也支持迭代器访问,只需要定义一个对象,即可遍历里面的元素。
可以理解成它就是一个固定的数组,支持遍历。
C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
这里{“sort”, “排序”}会先初始化构造一个pair对象
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
而vector和list等各种容器能支持这样初始化,是因为它们都支持这样的构造函数
关键字decltype将变量的类型声明为表达式指定的类型。
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是int*
return 0;
}
NULL最开始在C语言中是0值,但是这会引发一些问题。比如:重载了两个函数,参数分别是int和int*,当调用函数的时候,NULL就不会匹配到int*,而是匹配到int,这就是C语言宏定义NULL值带来的问题。所以C++引入了新的空指针:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
引用最大的价值是减少拷贝。
先看左值:左值是一个表示数据的表达式。如:变量名或解引用的指针。左值最重要的一个特征是可以取地址,并且可以对它赋值。对于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;
}
double x = 1.1, y = 2.2;
double& k = x + y; //报错
const double& kk = x + y;//不报错
这个点可以用在参数传递上。例如C++11之前没有右值引用,所以形参都是定义成T& x这样的类型,这种左值引用是无法接收右值参数的,现在有了右值引用,就可以把参数改为const T& x,这样既可以接收左值又可以接收右值。
int x = 1;
int&& r = x; //报错
int&& rr = move(x);//不报错
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。
也就是说,右值引用之后的值,可以当成左值来用。
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // 报错
return 0;
}
作为形参,减少拷贝;或者做输出型参数。
作为返回值,可以减少拷贝(如果是局部变量不可以用左值引用返回);或者可以修改返回值。
但是遇到局部变量作为返回值的时候(出了作用域就销毁),特别是一些占有空间较大的对象,只能传值返回,此时的左值引用无法做到减少拷贝。当然也可以返回值设置为void,利用输出型参数返回,但是这样有很多场景也不太符合。
例如to_string函数,返回的时候拷贝一次,接收返回值的话又拷贝一次,虽然编译器会优化成一次拷贝,但至少也有一次拷贝,效率相对低一点。
右值引用和移动语义解决上述问题。
在string类中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
// 移动构造
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
int main()
{
bit::string ret2 = bit::to_string(-1234);
return 0;
}
再运行上面to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。
需要注意的一点是,什么时候会调用移动构造是通过调用的对象来决定的,如果移动构造和拷贝构造同时存在,就看这个赋值对象是左值/右值,进行匹配拷贝/移动构造。这种机制叫做移动语义。
不仅仅有移动构造,还有移动赋值:
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
int main()
{
string ret1;
ret1 = to_string(1234);
return 0;
}
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义
同样的道理,此时会在赋值构造和移动赋值之间选出最合适的,由于其返回值是右值,那么自然也就匹配到移动语义。
值得注意的是:右值引用并没有延长变量的生命周期,只是将其资源全部转移走了。
完美转发指的是在参数传递过程中,参数的引用类型能一直保持。有些时候引用折叠会导致应该是右值引用的变成了左值引用(基于引用折叠和forward())。
一般格式如下:
// std::forward(t)在传参的过程中保持了t的原生类型属性。
// Fun()
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
完美转发是基于引用折叠的,引用折叠是完美转发的基础之一。
前面说过了,对于一个右值引用来说,其本身的属性依旧是一个左值。
#include
#include
void fun(int& arg) {
std::cout << "Lvalue reference: " << arg << std::endl;
}
void fun(int&& arg) {
std::cout << "Rvalue reference: " << arg << std::endl;
}
template <typename T>
void foo(T&& arg) {
fun(arg);
}
int main() {
int x = 42;
foo(x); // Lvalue reference: 42
foo(10); // Lvalue reference: 10
foo(std::move(x)); // Lvalue reference: 42
return 0;
}
上述的三个调用都将会变成左值类型,为什么呢?因为右值引用本身也是一个左值,它作为参数的时候,当然就是以左值的方式去匹配函数。
所以此时需要保持右值引用原有的特性,需要用到forward。所以引用折叠和forward()结合可以完成完美转发。
template <typename T>
void foo(T&& arg) {
fun(std::forward<T>(arg));
}
对于常属性来说,引用折叠也会保持其常属性。
关键字 default 在C++中用于显式声明使用默认实现的特殊成员函数。当我们声明一个特殊成员函数(默认构造函数、析构函数、拷贝构造函数、移动构造函数或拷贝赋值运算符)时,可以使用关键字 default 来告诉编译器使用默认的实现。
class MyClass {
public:
MyClass() = default; // 使用编译器生成的默认构造函数实现
MyClass(const MyClass& other) = default; // 使用编译器生成的默认拷贝构造函数实现
MyClass& operator=(const MyClass& other) = default; // 使用编译器生成的默认拷贝赋值运算符实现
// 其他成员和函数声明
};
相反,delete就是禁止其生成默认的成员函数。
final 关键字用于修饰类、成员函数或虚函数,表示它们不能被继承或重写。
修饰类:final 关键字用于禁止派生类继承某个类,即该类为最终类,不能被继承。
class Base final {
// ...
};
class Derived : public Base // 错误,Base 是 final 类,不能被继承
{
// ...
};
修饰成员函数:final 关键字用于禁止派生类重写某个虚函数或普通虚函数。
class Base {
public:
virtual void foo() final {
// ...
}
};
class Derived : public Base {
public:
void foo() override; // 错误,基类的 foo() 是 final 函数,不能被重写
};
使用 final 关键字可以提供代码的可靠性,确保某些类或函数不会被修改或继承,从而提高程序的安全性和可靠性。
override 关键字用于显式指示派生类重写基类的虚函数。当使用 override 关键字时,编译器会检查是否存在与基类函数对应的虚函数,以确保正确的重写。
class Base {
public:
virtual void foo() {
// ...
}
};
class Derived : public Base {
public:
void foo() override {
// ...
}
};
在上述示例中,Derived 类通过在 foo() 函数声明中使用 override 关键字,明确表明它想要重写 Base 类中的 foo() 虚函数。如果基类中没有名为 foo() 的虚函数,或者函数签名与基类中的虚函数不匹配,编译器将会报错。
Lambda表达式用作函数对象:
int main() {
int factor = 5;
auto multiply = [factor](int x) { return x * factor; };
// 输出:50
return 0;
}
Lambda表达式与算法函数一起使用:
int main() {
std::vector<int> numbers = {3, 1, 5, 2, 4};
// 使用lambda表达式作为排序函数
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a < b;
});
// 输出:1 2 3 4
return 0;
}
按引用捕获的Lambda表达式:
int main() {
int x = 10;
auto lambda = [&]() {
x++;
};
lambda();
// 输出:11
return 0;
}
在此示例中,我们定义了一个使用按引用捕获的lambda表达式,它通过引用捕获了外部变量x,并递增它。
包装器是一种在C++中常用的设计模式,用于封装底层的实现或外部的库/组件,提供更友好和抽象的接口供上层代码使用。包装器通过各种手段,如封装、继承、组合等,将底层复杂的实现细节隐藏起来,提供简化和易于使用的接口。
封装复杂性:包装器的主要目标之一是隐藏底层实现的复杂性,使上层代码更易于理解和使用。
提供高级接口:包装器使我们能够创建更高级别的接口,以满足特定需求。
增加可维护性:使用包装器可以将代码组织得更结构化和模块化,减少重复的代码。
促进代码重用:包装器可以提供通用和抽象的接口,使得底层实现更加易于重用。