目录

C++11简介:

1、统一的列表初始化:

std::initializer_list

2、自动类型推导:

auto:

decltype:

3、final 和 override

final:

override:

4、默认成员函数控制:

显示缺省函数:

删除默认函数:

5、左值和右值:

概念补充:

代码示例:

左值引用与右值引用:

两者的联系和区别:

右值引用的意义:

6、完美转发:

7、lambda表达式:

8、可变参数列表:

9、包装器:

function包装器:

bind(绑定):


C++11简介:

C++11,也被称为C++0x,是C++编程语言的一个重要更新版本,它于2011年正式被ISO标准委员会批准。相比于C++98,C++11带来了大量的新特性和改进,其中包含了约140个新特性,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。本篇主要介绍一些使用的多且实用的一些语法:

1、统一的列表初始化:

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。例如:

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自 定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

struct Student
{
	int _x;
	int _y;
};
int main()
{
	// 两种写法一样的
	int x1 = 1;
	int x2{ 2 };

	// 可以省去 = 
	int array1[]{ 1, 2, 3, 4, 5 };
	int array2[5]{ 0 };

	Student s{ 1, 2 };

	return 0;
}

创建对象时也可以使用列表初始化方式调用构造函数初始化

class A
{
public:

    // 如果不想让隐式类型转换发生可以加关键字: explicit
    // explicit A(int x, int y)

    A(int x, int y)
        :_x(x)
        , _y(y)
    {}


    // 单参数
    A(int x)
        :_x(x)
        , _y(x)
    {}

private:
    int _x;
    int _y;
};


int main()
{

    // 多参数的隐式类型转换
    // 本质是构造一个A对象,然后用这个对象去拷贝构造(编译器会进行优化, 就变成了直接构造)
    A a1 = { 1,2 };

    // 例如: A& a3 = {2, 2}; 这样子的就不行,但转换成下面的就可以
    const A& a3 = { 2,2 }; // 因为右边会产生临时变量,但临时变量具有常性

    // 当然也可以不加 = ,如 A a1{ 1,2 }; 但是不太推荐这么写 

    return 0;
}

std::initializer_list

initializer_list 是 C++11 引入的一个特性,用于表示某种类型对象的数组。它允许使用花括号 {} 包围的初始值列表来初始化对象或函数参数。initializer_list 通常与构造函数结合使用,以提供一种灵活的方式来初始化集合(如数组、向量等)或执行基于多个值的初始化。

(本质就是一个常量数组)

是个容器,但是没有新开空间,里面有两个指针,一个指向第一个元素(常量数组的开始),一个指向最后一个元素的后一个位置(常量数组的结束),所以是8/16字节,因为是两个指针(first, last)

2、自动类型推导:

auto:

auto 关键字用于自动类型推导。编译器会根据初始化表达式自动推断出变量的类型。使用 auto 可以让代码更加简洁,例如STL容器迭代器、函数返回类型等。

缺陷:

        auto虽然用起来非常方便,但也不能滥用哦,例如在层层嵌套的函数当中,用起来是爽了,但是代码的可读性会大大下降,这时维护起来也会特别麻烦,一个变量可能得剥好几层才知道类型,还有在涉及到函数重载以及模板元编程时,容易引发错误和意外的行为。

decltype:

decltype 关键字用于查询表达式的类型。与 auto 不同,decltype 在编译时解析表达式并得到其类型,但不实际计算表达式的值。这意味着可以使用 decltype 来获得几乎任何表达式的类型,包括那些没有定义(或不可计算)的表达式。

缺陷:

        decltype的语法相对复杂一点,且在某些情况下,decltype导出的类型可能非常冗长,特别是当表达式涉及到模板类型或复杂函数时。如果在函数声明中使用decltype时,如果函数的返回类型依赖于模板参数或函数参数的类型,那么可能需要使用尾置返回类型(trailing return type)语法,这可能会使函数声明的复杂性增加。

3、final 和 override

final:

final修饰类的时候,表示这个类无法被继承、修饰虚函数时,表示这个虚函数不能被重写。

修饰类:

修饰虚函数:

override:

override 关键字用于检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写编译报错。

4、默认成员函数控制:

显示缺省函数:

当你不为类定义任何构造函数时,编译器会为你生成一个默认构造函数。如果你显式地定义了其他构造函数(例如拷贝构造、移动构造还是其他自定义构造函数),但没有定义默认构造函数,编译器不会自动生成默认构造函数。如果还想编译器生成默认构造函数的话,可以在类定义中显式地使用 default 关键字来请求编译器生成默认构造函数。

删除默认函数:

如果想要一个类禁止被拷贝,在C++98当中的做法是将这个类的拷贝构造以及赋值重载直接声明为私有(private),在C++11中则更为简单一些,只需在该函数声明加上delete关键字即可

5、左值和右值:

概念补充:

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用的返回)等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。

简记:左值能出现在右边,右值不能出现在左边。

代码示例:

左值引用与右值引用:

可以先简短的记着:左值引用就是给左值取别名,右值引用就是给右值取别名!

先来谈谈简单的:左值引用:

int main()
{
	int a = 10;
	int& b = a; // b就是a的别名,其实就是a

	// 属于同一块地址空间:
	cout << &a << endl;
	cout << &b << endl;

	return 0;
}

再来谈谈右值引用:

int main()
{
	int x = 1, y = 2;
	int&& z = (x + y);

	string&& s1 = string("111111");
	
	string&& s2 = to_string(1234);

	int&& a = 10;

	return 0;
}

两者的联系和区别:

右值引用的本身是左值,因为只有这样才能实现移动构造和移动赋值,以此来实现资源转移,因为普通左值是没法引用右值的,必须加const,但是加了又没法实现资源的转移。

所以右值引用是有地址的:

	string&& s1 = string("111111");
	cout << &s1 << endl;

如果右值引用的属性是右值,那么移动构造和移动赋值,要转移资源的语法逻辑是矛盾的,右值是不能被改变的,可以理解为右值带有const属性。

诶?那再回头来看看,既然右值引用是给右值取别名,那么右值引用有地址,那么是不是右值本身是有地址的???

其实还真是,像刚才的那种右值的底层是有地址的,但是只有编译器知道,为了语法的逻辑自洽,他不会让你取得到这个地址,右值引用s1其实就是变相获取到这个地址,所以能不能取到地址不是关键,关键是资源的转移(匿名对象和临时对象都带有const属性)

一般情况下左值引用只能引用左值,不能引用右值。但是加了const的左值引用既能引用左值也能引用右值。

一般情况下右值引用只能引用右值,不能引用左值。但是右值引用可以引用move以后的左值。

move函数:将一个左值对象转化为右值引用,从而允许使用移动语义来优化资源的管理和程序的性能。

右值引用的意义:

右值引用的意义主要体现在两个方面:移动语义、完美转发。

移动语义:

1、资源所有权转移:在C++中,类的右值通常是一个临时对象,如果在表达式结束时没有被绑定到引用,就会被废弃。通过右值引用,可以在对象被废弃之前移走其资源,实现资源的再利用,再需要时,就可以避免无意义的拷贝复制操作,提高效率。

2、减少开销:被移走资源的右值在废弃时已经成为空壳,其析构的开销会大大降低。这有助于提升程序的性能,特别是在处理大型对象或资源密集型操作时。

完美转发:

这里简单讲一下,下一标题再详细叙述。

完美转发的实现依赖于右值引用。右值引用(T&&)用于绑定到即将被销毁的对象上,从而允许资源的移动而非拷贝。

在模板函数中,通过使用 T&& 作为参数类型,能够接受左值和右值作为参数。

6、完美转发:

完美转发(Perfect Forwarding)是C++11中引入的一种编程技巧,其目的是在编写泛型函数时能够保留参数的类型和值类别(左值或右值),从而实现更为高效且准确地传递参数。

完美转发允许函数模板将其接收到的参数“完美”地转发给内部调用的其他函数,这里的“完美”指的是不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。

省流:

完美转发就是在函数模板中,将参数转发给其他函数时,可以锁定参数的左右值属性和值类别。

因为右值引用的对象在作为实参传递时,属性会退化为左值,会直接匹配左值引用,使用完美转发可以保持它的右值属性。

这里来认识两个东西:forward函数和万能引用(T &&)

forward函数:是一个模板函数,用于实现完美转发

万能引用(T &&):既能接收左值也能接收右值。

直接上一段代码来直观感受一下:

7、lambda表达式:

lambda表达式从C++11标准开始引入的一种定义匿名函数对象的简洁方式。它可以捕获它所在作用域的变量,并可以在需要函数对象的任何地方使用,包括作为算法的一部分或作为回调函数。

基本语法:

[capture](parameters) mutable -> return_type {  
    // 函数体  
}
  • capture捕获列表,指定哪些外部变量在lambda函数体内是可用的。捕获列表可以是按值捕获(例如[x])或按引用捕获(例如[&x]),也可以混合使用(例如[&, x = this->x]),其中&表示按引用捕获所有外部变量,除了显式按值捕获的。=表示按值捕获所有外部变量,除了显式按引用捕获的。
  • parameters参数列表,与普通函数相同。如果lambda不接受任何参数,则可以省略括号。
  • mutable这是一个可选的说明符,表示lambda函数体内的代码可以修改按值捕获的变量。默认情况下,这些变量是只读的。
  • return_type返回类型,也是可选的。如果lambda体只有一个返回语句,且编译器能够从该语句推断出返回类型,则可以省略。如果lambda没有返回语句,则其返回类型为void

也可以就简单的记为:[](){}

其实本质就是仿函数,编译器在编译的时候就会转成仿函数,原理类似范围for

举个小例子:我给这个lambda表达式传两个值,让它返回这两个值的和给我:

补充:

int main()
{
    int a = 1, b = 2;
    // 捕捉列表

    auto swap1 = [a, b]() mutable // 还是传值捕捉
    {
        int tmp = a;
        a = b;
        b = tmp;
    };
    swap1();
    // 如果不加mutable的话a, b捕捉到的默认是const的,无法对其做出修改操作,而且·此ab非局部的那俩ab
    // 也就是就算带mutable修改内部也并不会改变外部
    printf("%d %d\n", a, b);


    // 引用捕捉可修改:
    auto swap2 = [&a, &b]() {
        int tmp = a;
        a = b;
        b = tmp;
    };
    swap2();
    printf("%d %d", a, b);

    // 捕捉方式还有 =, &
    // = 为传值捕捉所有父作用域中的变量,包括this
    // & 为引用捕捉所有父作用域中的变量,包括this

    // 还可以混合捕捉: 表示为&捕捉全局,唯独b为传值捕捉(也可以反着来)
    auto func = [&, b]() {};

    return 0;
}

8、可变参数列表:

省流:可变参数列表一种特殊的函数参数机制,允许函数接收数量不确定的参数

先来段简单的代码看看使用方法:

既然可变参数列表可以接收不确定数量的参数,那么仔细想想,好像printf函数也是如此,那么是不是就可以简单的模拟实现一下printf函数呢?

总结:可变参数列表提供了极高的灵活性和类型安全性,且支持泛型编程,但复杂性较高,可能会导致编译时间的增加和额外的性能开销。

9、包装器:

function包装器:

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。于头文件#include <functional>

其实包装器的本质就是函数指针,可调用对象有:函数、函数对象(仿函数)、lambda表达式、绑定表达式等。

先来看一个例子:来看看下面这段代码中的函数模板会被实例化几次?

template<class F, class T>
T Test(F f, T t)
{
	static int x = 0;
	cout << "x: " << ++x << endl;
	cout << "x: " << &x << endl;

	return f(t);
}

double f(double i)
{
	return i / 2;
}

// 仿函数
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};

int main()
{
	// 函数:
	cout << Test(f, 11.11) << endl;

	// 函数对象(仿函数):
	cout << Test(Functor(), 11.11) << endl;

	// lambda表达式:
	cout << Test([](double d) {return d / 4; }, 11.11) << endl;

	return 0;
}

通过结果我们可以看出来,函数模板实例化了三次(静态变量x的地址都不一样)

对于同一个模板类或模板函数,使用多种不同的类型参数去实例化它时。每次使用不同的类型参数实例化模板,编译器都会生成一份独立的模板代码。这可能导致编译时间增加、二进制文件大小膨胀,以及潜在的运行时性能问题(如指令缓存未命中增加)。

但是我只想让这个函数模板实例化一次怎么办呢?

这时就可以掏出function包装器来解决:

function用法解析:

代码结果:

这样实例化出来的就是同一份模板了(x依次递增,且为同一地址),这样通过function包装器就可以减少不必要的模板实例化,因为模板实例化多了容易导致代码膨胀,降低效率。

最重要的是:好用(滑稽)

bind(绑定):

省流:调整可调用对象的参数个数或者顺序:

完整概念:

std::bind 用于生成一个新的可调用实体(function object),这个新的可调用实体可以把它的某些(或全部)参数绑定到给定的值上。这样,当你调用这个新的可调用实体时,就不需要再为那些已经被绑定的参数提供值了。(头文件:#include <functional>

基本语法:

auto newCallable = bind(callable, arg_list, placeholders::_1, placeholders::_2, ...);
  • callable:你想要调用的函数、函数对象、成员函数指针或成员对象指针。
  • arg_list:你希望预先绑定到 callable 的参数列表。这些参数在后续调用 newCallable 时将不再需要。
  • placeholders::_1, placeholders::_2, ...:占位符,用于表示 newCallable 被调用时,需要接收的参数位置。允许保留一些参数位置以供将来调用时提供。

上个简单的代码看看:

再来看一组:也可以绑定类中的函数:

注意:

        绑定成员函数时,需要传递成员函数的地址(使用&运算符),并且还需要传递一个指向对象实例的指针(对于非静态成员函数)或对象的引用(但通常期望指针,因为bind需要能够存储这个引用所指向的对象的状态,而局部引用在函数返回后可能不再有效)。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部