C++11新特性

文章目录

    • 列表初始化
    • decltype
    • nullptr
    • 右值引用
      • 左值引言与右值引用的关系
      • 左值引用与右值引用用处
        • 移动语义
        • 移动构造
        • 移动赋值
      • 完美转发
      • 引用折叠
    • default
    • final和override
    • 可变参数列表
    • lambda表达式
    • 包装器

列表初始化

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;
}

C++11新特性_第1张图片

这里的x如果直接用花括号初始化,默认是initializer_list< int >类型,这个类型其实相当于一个容器,也支持迭代器访问,只需要定义一个对象,即可遍历里面的元素。

可以理解成它就是一个固定的数组,支持遍历。

C++11中列表初始化也可以适用于new表达式中

int* pa = new int[4]{ 0 };

这里{“sort”, “排序”}会先初始化构造一个pair对象

map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };

而vector和list等各种容器能支持这样初始化,是因为它们都支持这样的构造函数

C++11新特性_第2张图片

decltype

关键字decltype将变量的类型声明为表达式指定的类型。

int main()
{
	const int x = 1;
	double y = 2.2;
	decltype(x * y) ret; // ret的类型是double
	decltype(&x) p;    // p的类型是int*

	return 0;
}

nullptr

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;
}

左值引言与右值引用的关系

  1. 左值引用不能用在右值身上(除非加上const)
double x = 1.1, y = 2.2;
double& k = x + y;		 //报错
const double& kk = x + y;//不报错

这个点可以用在参数传递上。例如C++11之前没有右值引用,所以形参都是定义成T& x这样的类型,这种左值引用是无法接收右值参数的,现在有了右值引用,就可以把参数改为const T& x,这样既可以接收左值又可以接收右值。

  1. 右值引用不能用在左值身上(除非move左值)
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;
}

左值引用与右值引用用处

  1. 左值引用

作为形参,减少拷贝;或者做输出型参数。

作为返回值,可以减少拷贝(如果是局部变量不可以用左值引用返回);或者可以修改返回值。

但是遇到局部变量作为返回值的时候(出了作用域就销毁),特别是一些占有空间较大的对象,只能传值返回,此时的左值引用无法做到减少拷贝。当然也可以返回值设置为void,利用输出型参数返回,但是这样有很多场景也不太符合。

例如to_string函数,返回的时候拷贝一次,接收返回值的话又拷贝一次,虽然编译器会优化成一次拷贝,但至少也有一次拷贝,效率相对低一点。

移动语义

  1. 右值引用

右值引用和移动语义解决上述问题。

在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

关键字 default 在C++中用于显式声明使用默认实现的特殊成员函数。当我们声明一个特殊成员函数(默认构造函数、析构函数、拷贝构造函数、移动构造函数或拷贝赋值运算符)时,可以使用关键字 default 来告诉编译器使用默认的实现。

class MyClass {
public:
    MyClass() = default; // 使用编译器生成的默认构造函数实现
    MyClass(const MyClass& other) = default; // 使用编译器生成的默认拷贝构造函数实现
    MyClass& operator=(const MyClass& other) = default; // 使用编译器生成的默认拷贝赋值运算符实现
	// 其他成员和函数声明
};

相反,delete就是禁止其生成默认的成员函数。

final和override

  • final关键字

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 关键字用于显式指示派生类重写基类的虚函数。当使用 override 关键字时,编译器会检查是否存在与基类函数对应的虚函数,以确保正确的重写。

class Base {
public:
    virtual void foo() {
        // ...
    }
};
class Derived : public Base {
public:
    void foo() override {
        // ...
    }
};

在上述示例中,Derived 类通过在 foo() 函数声明中使用 override 关键字,明确表明它想要重写 Base 类中的 foo() 虚函数。如果基类中没有名为 foo() 的虚函数,或者函数签名与基类中的虚函数不匹配,编译器将会报错。

可变参数列表

lambda表达式

Lambda表达式用作函数对象:

int main() {
    int factor = 5;
    auto multiply = [factor](int x) { return x * factor; };
    // 输出:50
    return 0;
}

Lambda表达式与算法函数一起使用:

int main() {
    std::vector&lt;int&gt; 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++中常用的设计模式,用于封装底层的实现或外部的库/组件,提供更友好和抽象的接口供上层代码使用。包装器通过各种手段,如封装、继承、组合等,将底层复杂的实现细节隐藏起来,提供简化和易于使用的接口。

  1. 封装复杂性:包装器的主要目标之一是隐藏底层实现的复杂性,使上层代码更易于理解和使用

  2. 提供高级接口:包装器使我们能够创建更高级别的接口,以满足特定需求。

  3. 增加可维护性:使用包装器可以将代码组织得更结构化和模块化,减少重复的代码。

  4. 促进代码重用:包装器可以提供通用和抽象的接口,使得底层实现更加易于重用。

你可能感兴趣的:(C++IE,c++,java,开发语言)