myworld-深入理解C++11-1

本文详细介绍了C++11中的新特性,包括如何处理右尖括号的二义性问题,类型推导的使用以及auto关键字的增强功能。此外,还深入探讨了lambda函数的概念、语法和应用场景,包括其对变量捕获、常量性和性能的影响。最后,文章提到了块作用域、仿函数以及在不同上下文中变量的使用规则。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

右尖括号的编译

c98中会把连续的两个右尖括号识别为一个右移操作符,在c++11中支持了连续两个右尖括号的写法,例子:
#include <iostream>
using namespace std;

template X<int i> class X{};
template Y<class T> class Y{};

int main() {
	X<1> X1;
	Y<X<1>> X2;  //此处使用c98会编译错误,原因时c98把>>识别成了右移操作符。
	Y<X<1> > X3; //C98中此种尖括号的书写方法能够正确识别。
	/* 如果gcc带有了c++11,则能够正确编译X2. g++ --std=c++11 -c a.c */
	return 0;
}

auto类型推导

	静态类型和动态类型的主要区别在与对变量进行类型检查的时间点。对于所谓的静态类型,类型检查主要发生在编译阶段;动态类型对于变量的类型检查发生在运行阶段。
	c和c++为静态类型语言,python为动态类型语言。python等这些动态类型语言的变量“拿来就用”得益于这些动态类型语言中的“类型推导”技术。
	类型推导也可以用于静态类型语言,在C++11中的类型推导的实现方式之一就是重定义了auto关键字。(auto关键字原来就有,只不过在c++11中重新定义了。auto的原义是具有自动存储器的局部变量,在c98中这个关键字几乎没有被使用到,因为函数内没有声明为static的变量都默认为具有自动存储器的局部变量。)
	在c++中,auto关键字不再作为一个存储类型指示符,而是作为一个类型指示符来指示编译器。auto声明的变量的类型必须由编译器在编译时期推导而得。(auto的类型推导也是在编译时期确定,而不是在运行时,这也是静态类型和动态类型的区别。)
		存储类型标识符:static, extern, thread_local等;类型标识符:int, float等。
	auto声明的变量必须被初始化,因为如果没有初始化,编译器的类型推导就没法推导出类型了。编译器在编译时期会将auto替换为变量实际的类型。
	auto关键字的优势:
		1. 简化代码。
		2. 避免在类型声明时出现的错误。
		3. 自适应性能够在一定程度上支持泛型编程。
#一个判断最大值的宏定义。
#define MAX1(a,b) (((a) > (b)) ? (a) : (b))
#define MAX2(a,b) ({ \
	auto _a = (a);
	auto _b = (b);
	(_a > _b) ? _a : _b;
})
第二种比第一种好的点在于:
	1. 第二中宏定义中a,b只需计算一次;而在第一种宏定义中,a和b都需要计算多次。所以第二次的宏定义比第一次的宏定义性能更好。
	2. 第二种定义更好的解决了宏定义中a++的问题。在第一个宏定义中,a++会被计算两次。在第二个宏中,a只被计算一次,因此更好的解决了a++的问题,第二种的宏定义更符合人们的理解。
    int a = 4;
    auto * ap = &a;  //ap的类型为int *
    auto ap1 = &a;   //ap1的类型为int *, 声明为auto *或者auto 都是一样的,没有区别。
    auto a1 = a;     //a1的类型为int
    auto & a_ref = a;  //a_ref的类型为int &,如果要声明为引用,则必须使用auto &
double foo();
float * bar();
const auto a = foo();  //a的类型为const double
const auto & b = foo();  //b的类型为const double &
volatile auto * c = bar(); //c的类型为volatile float *

auto d = a;   //d的类型为double,无法带走常量性或者易失性。
auto & e = a;  //e的类型为const double &,引用的话可以带走常量性或者易失性。
auto f = c;    //f的类型为float *,无法带走常量性或易失性。
volatile auto & g = c;   //c的类型为volatile float * &。
volatile和const在c++中代表了变量的两种不同的属性:易失的和常量的。在c++标准中,他们常常被一起叫做cv限定符。声明为引用的变量可以保持其引用对象的cv属性,非引用对象则不能。
在同一个赋值语句中,auto可以用来声明多个变量的类型,不过这些变量的类型必须相同。

初始化列表和new也可以用auto,代码如下:
auto x = 1;
auto x1(1);
auto y{1};  //使用初始化列表的auto.
auto z = new auto(1);  //在new中使用auto。

不能使用auto的4中情况

1. 形参中不能使用auto。
2. 结构体的非静态成员变量不能使用auto。
3. 不能声明auto数组,例如auto z[3]是不允许的。
4. 在实例话模板的时候用auto作为模板参数是不允许的。例如:vector<auto> z;

auto判断对错

auto int i = 1;是对的吗?
答:在c++98中,此语法是对的,auto作为一个自动局部变量关键字的说明;在c++11中则是错误的,携程auto i = 1;才是对的,加上了int就不对了。

long long类型

long long可以在不同平台上有不同的长度,但是至少有64位。
要了解平台上long long大小的方法就是查看<climits>中的宏。与long long相关的宏一共有三个:LLONG_MIN,LLONG_MAX,ULLONG_MAX。
#include <climits>
using namespace std;

void test7() {
    cout << LLONG_MIN << endl;
    cout << LLONG_MAX << endl;
    cout << ULLONG_MAX << endl;
}
/* 结果如下:
-9223372036854775808
9223372036854775807
18446744073709551615
*/

lambda函数

lambda用来表示一种匿名函数,这种匿名函数代表了一种所谓的lambda演算。

目前几种流行的编程泛型

命令式编程/过程式编程
面向对象编程
函数式编程

lambda的语法

[捕捉列表](参数列表) mutable -> 返回类型{语句列表}

[]是lambda函数的引出符,编译器根据此函数判断接下来的代码是否是lambda函数。
lambda函数与普通函数可见的最大区别之一,就是lambda函数可以通过捕捉列表访问一些上下文中的数据。具体地,捕捉列表中描述了上下文中哪些数据可以被lambda使用,以及使用的方式(以值传递的方式或者以引用的方式。)

  • [var] 表示以值传递方式捕捉上下文中的变量var。
  • [=] 表示通过值传递的方式捕捉所有父作用域的变量,包括this。
  • [& var] 表示通过引用传递的方式捕捉上下文中的变量var。
  • [&] 表示通过引用的方式捕捉所有父作用域的变量,包括this。
  • [this] 表示通过值传递的方式捕捉当前的this指针。

块作用域

所谓块作用域,可以理解为任务{}内的代码都是属于块作用域的。

在块作用域之外的lambda函数,其捕捉列表必须为空,因此这样的函数除了语法上不同之外,与普通函数并没有区别。
对于块作用域之内的lambda函数,仅能捕捉父作用域之内的局部变量,捕捉任何非此作用域的变量或者此作用域内的非局部变量,如静态变量等,都会导致编译器报错。

仿函数

仿函数简单的说,就是重定义了成员函数operator()的一种自定义类型的对象。这样的对象有一个特点,就是其在使用层面感觉跟函数的使用并无二样。但是究其本质却并非函数。
仿函数的一个例子如下:

class _functor {
public:
	int operator(int x, int y) { return x+y; }
};

int main() {
	int girls = 3, boys = 4;
	_functor totalChild;
	return totalChild(5,6);
}
lambda和仿函数有着相同的内涵--都可以捕捉一些变量作为初始状态,并接受参数进行运算。仿函数通过参数列表捕捉变量的初始状态,lambda函数通过捕获列表获得参数的初始状态。
事实上,仿函数是编译器实现lambda的一种方式。在现阶段,通常编辑器都会把lambda函数转化为一个仿函数对象。因此,在c++11中,lambda可以视为仿函数的一种等价形式,或者更动听的说,lambda是仿函数的语法甜点。
相比与传统的函数,lambda函数在这里更加直观,使用起来也更加方便。代码可读性很好,效果上,lambda函数则等同于一个“局部函数”。(C/C++语言中不允许局部函数的存在。)
lambda函数的代码可读性更好,尤其对于小函数而言。
  • 对于按值传递的捕捉列表,其传递的值在lambda函数定义的时候就已经决定了。而按引用传递的捕捉列表变量,其传递的值则等于lambda函数调用时的值。
  • 从c++11标准的定义中可以发现,lambda的类型被定义未一个闭包的类,而每个lambda表达式则会产生一个闭包类型的临时对象(右值)。

lambda函数的const特性

c++11中,默认情况下,lambda函数是一个const函数。按照规则,一个const的成员函数是不能在函数体中改变非静态成员变量的值的。例子如下:

int test1() {
    int val = 1;
    //auto const_val_lambda = [=]() {val = 3};
	//lambda函数默认是const函数,不能改变捕捉列表中通过传值方式传递进来的变量,编译报错。

    auto mutable_val_lambda = [=]() mutable {val = 3; return val;};
    cout << "case1==>" << mutable_val_lambda() << endl; // 3
    cout << val << endl;  // 1
    //显示设置lambda函数为mutable函数,此时在函数内部可以改变变量的值,但是其实父域中的变量并没有被改变。其实改变的是函数内变量的副本。
    //因此,捕捉列表中传值方式的变量不能在lambda函数中被改变。

    auto const_ref_lambda = [&]() {val = 3; return val;};
    cout << "case2==>" << const_ref_lambda() << endl;  // 3
    cout << val << endl;  // 3

    auto mutable_ref_lambda = [&]() mutable {val = 3; return val;};
    cout << mutable_val_lambda() << "  " << val << endl;  //3  3

    int val2 = 1;
    auto const_param_lambda = [&](int v) {v = 3; return v;};
    cout << "val2==>" << const_param_lambda(val2) << endl;  // 3
    cout << val2 << endl;  // 1

    int val1 = 1;
    auto const_param_lambda1 = [=](int v) {v = 3; return v;};
    cout << "val1==>" << const_param_lambda1(val1) << endl;  // 3
    cout << val1 << endl; // 1
    //通过参数列表以值传递方式传递进来的变量,不会改变父域中的值。与捕捉列表没关系。

    int val3 = 1;
    auto const_param_lambda2 = [&](int & v) {v = 3; return v;};
    cout << "val3==>" << const_param_lambda2(val3) << endl;  // 3
    cout << val3 << endl;  // 3
    
    int val4 = 1;
    auto const_param_lambda3 = [&](int & v) mutable {v = 3; return v;};
    cout << "==>" << const_param_lambda3(val4) << endl; // 3
    cout << val4 << endl;  // 3

    int val5= 1;
    auto const_param_lambda4 = [&](int * v) mutable {*v = 3; return *v;};
    cout << "==>" <<const_param_lambda4(&val5) << endl; // 3
    cout << val5 << endl;  // 3
    //(连续打印结果未3和1,会引起误导。)

    int val6= 1;
    auto const_param_lambda5 = [&]() {val6 = 3; return val6;};
    cout << const_param_lambda5(val6) << "  " << val6<< endl;  // 3  1,此种方式打印的结果是3和1,容易误导,实际是能够被改变的。
    cout << "==>" << const_param_lambda5() << endl; //3
    cout << val6 << endl;  // 3
    //通过引用方式捕获的变量,可以在lambda函数中改变父域的值,同时父域中的值的改变也会影响lambda函数。
}

综合上述的case总结出的结论:

  • 按值传递方式捕捉的变量是lambda函数中不可更改的常量。
  • 绝大多数时候,临时变量只是用于lambda函数的输入,如果需要输出结果到上下文,我们可以使用引用,或者通过让lambda函数返回值来实现。
  • 有人认为这算是闭包类型名称的体现,即在复制了上下文中的变量之后关闭了变量与上下文中变量的联系,变量至于lambda函数运算本身有关系,不会影响lambda函数(闭包)之外的任务内容。

关于捕捉列表的讨论

  • 首先,在[=]中,所有捕捉的变量在lambda声明一开始就被拷贝,且拷贝的值不可被更改。正因为传值的方式会发生拷贝,所以,如果不想带来很大的开销,那么可以通过传递引用的方式来传递参数。
  • 在[&]中使用的变量与父作用域是有联系的,使用的时候也需要格外注意。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值