【现代C++】C++11新特性 nullptr、constexpr、auto、decltype、范围for、尾置返回类型、lambda表达式、复杂类型阅读

本文主要是介绍【现代C++】C++11新特性 nullptr、constexpr、auto、decltype、范围for、尾置返回类型、lambda表达式、复杂类型阅读,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 1. 新的空指针字面量 `nullptr`
  • 2. 区分 `constexpr` 与 `const`
  • 3. `auto` 类型指示符——自动推断类型和值初始化
  • 4. `decltype` 类型指示符——自动推断类型
  • 5. 范围遍历的 `for` 语句
  • 6. 尾置返回类型
  • 7. lambda表达式
    • 7.1 值捕获
    • 7.2 引用捕获
    • 7.3 隐式捕获
    • 7.4 混合捕获
    • 7.5 返回类型

C++最标准的参考资料之一,虽然有点难度:
在这里插入图片描述


1. 新的空指针字面量 nullptr

关键词 nullptrstd::nullptr_t 类型的纯右值,表示空指针字面量,从 nullptr 到任意指针类型和任意成员指针类型都存在隐式转换——这一转换同样适用于任何空指针常量,空指针常量包括 std::nullptr_t 类型的值和空指针宏常量 NULL

在C++14的头文件 <type_traits> 中有一个类模板 is_null_pointer ,可以检查类型是否为
std::nullptr_t

#include <iostream>
#include <type_traits> int main() { 	std::cout << std::boolalpha 	<<std::is_null_pointer<decltype(nullptr)>::value << ' '  	<<std::is_null_pointer<std::nullptr_t>::value << ' ' 	<<std::is_null_pointer<volatile std::nullptr_t>::value << ' ' 	<<std::is_null_pointer<const volatile std::nullptr_t>::value << '\n' 	<<std::is_null_pointer<int*>::value << ' ';  	return 0; 
}

运行结果如下:
在这里插入图片描述
类似的还有 is_void, is_array, is_pointer, is_enum, is_union, is_class, is_function, is_object ,全部都是用于类型检查的类模板。

原先初始化一个空指针要赋值为 NULL ,但是C++中的 NULL 是一个宏常量,其值实际上为 0 (C中的 NULL 是转换为 void * 类型的 0 ;不过C++中不能像C一样把 void * 隐式转换成其他类型的指针,为了解决空指针的表示问题,C++干脆引入了 0 来表示空指针),相关的宏定义如下:

#ifndef __cplusplus //如果没有定义__cplusplus宏,说明正在编译C语言
#define NULL ((void *)0)
#else   /* C++ */
#define NULL 0
#endif  /* C++ */

这样问题就来了,一个 int 型、值为 0 的字面量,在函数重载时必定会出现非预期的结果。考虑一段代码如下:

#include <iostream>
int f(int x) {std::cout << "int x" << std::endl;
}
int f(int *x) { //希望调用的函数std::cout << "int *x" << std::endl;
}
int main() {f(NULL); //此处会输出"int x",与我们的想法不同,因为NULL既可以转换为指针,也相当于0
}

nullptr 解决了 NULL 指代空指针时的二义性问题,下面的代码会调用 int f(int *x)

#include <iostream> 
int f(int x) {std::cout << "int x" << std::endl;
}
int f(int *x) {std::cout << "int *x" << std::endl;
}
int main() {f(nullptr); 
}

总得来说,条件允许的前提下尽量使用 nullptr

此处涉及到的问题有:

  • C和C++中 NULL 的区别;
  • C++中 NULLnullptr 的区别;
  • C++中 nullptr 的用处

2. 区分 constexprconst

我们所想的常量不一定是程序意义上的“常量”,因此可能引发一些意想不到的错误。考虑以下代码:

#include <iostream>
int main() {int a;std::cin >> a;const int b = a + 233; //或者 int b = a + 233;  int c[b];  //这两行代码,在以前的编译器中会报错,现在倒是能够用变量或者//const常量定义数组长度了,即VLA(variable length array)return 0;
}

个人认为,为了避免误解,C++中的 const 关键字应该改为 readonly ,表示只读变量,即变量的值在程序执行期间不会被修改,但是每次执行程序时 const 类型的值不一定相同constexpr 才是真正的常量类型,或者现在称为常量表达式类型,即每次执行程序时都为同一个值,且程序执行期间无法被修改,限定更加严格。

#include <iostream>
int main() {int a;std::cin >> a;const int b = a + 10;constexpr int c = b + 10; //错误! b为常量,但不是常量表达式return 0;
}
#include <iostream>
int main() {const int a = 10; //int a = 10; 也会报错 const int b = a + 10;constexpr int c = b + 10; //正确return 0;
}

当然,这方面的水很深,比如C++20新出的 consteval, constinit 关键字……反正我现在是把握不住的。


3. auto 类型指示符——自动推断类型和值初始化

遇到太长的类型名,难以拼写,浪费时间;遇到不记得的函数返回类型,无法保存返回值……这些问题都可以用 auto 解决。比如:

//原来要写
vector<int> v;
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) {//do something
}
//现在可以写成
vector<int> v;
for (auto it = v.begin(); it != v.end(); ++it) {//do something
}

auto 的作用,一方面根据给出的值推断变量类型(所以 auto 变量必须要有初始值),另一方面使用给出的值初始化变量。因此 auto 需要实际执行表达式:

#include <iostream>
int f() { std::cout << "Hello auto!" << std::endl;return 111;
}
int main() { auto b = f(); //b的值为111,会输出"Hello auto!",因为f被实际执行 std::cout << b << std::endl;return 0;
}

在这里插入图片描述

auto 的水也很深,这里只讲一点和引用相关的。当引用被用于 auto 变量初始化时,真正参与初始化的是引用对象的值引用即别名),编译器将以引用对象的类型作为 auto 变量的类型,以引用对象的值作为 auto 变量的初始值。因此下面的代码中,p 的类型是 int ,而非 i 变量的引用,改变 p 的值不会影响到 i

#include <iostream>
int main() {int i = 1, &r = i; //变量i和i的引用rauto p = r; //p的类型为int,值为1,不是i的引用std::cout << "i=" << i << ", " << "p=" << p << std::endl;p = 3; std::cout << "i=" << i << ", " << "p=" << p << std::endl;return 0;
}

在这里插入图片描述
如果想要用 auto 得到一个引用,需要添加引用修饰符,此时修改 p 的值会影响到 i

#include <iostream>
int main() {int i = 1, &r = i; //变量i和i的引用rauto &p = r; //p的类型为int,值为1,不是i的引用std::cout << "i=" << i << ", " << "p=" << p << std::endl;p = 3; std::cout << "i=" << i << ", " << "p=" << p << std::endl;return 0;
}

在这里插入图片描述


4. decltype 类型指示符——自动推断类型

如果我们只想要用表达式的类型定义一个变量,却不想用表达式的值初始化这个变量,就可以使用 decltype 。用法如下:

int a = 100;
decltype(a) b;
b = 233;  

注意,decltype 只会用表达式的返回值进行类型推断不会执行表达式

#include <iostream>
int f() {std::cout << "Hello delctype!" << std::endl;return 111;
}
int main() { decltype(f()) b = 233; //b的值为233,不是111,也不会输出"Hello delctype!",因为f没有实际执行 std::cout << b << std::endl;return 0;
}

在这里插入图片描述
decltypeauto 都可以用于类型推断,两者之间除了是否使用表达式的值初始化是否实际执行表达式这两个区别以外,还有以下不同之处:

  • 处理引用:decltype 根据引用推断出的类型,包含引用修饰符,此时必须初始化,不然无法编译通过;而 auto 推断出的类型,会忽视掉引用,为引用对象的类型。可以说,除了在 decltype下,其他情况的引用都可以视为引用对象本身。

    #include <iostream>
    int main() {int i = 1, &r = i; //变量i和i的引用rdecltype(r) p = r; //p的类型为int&,即为int的引用; 由于是引用,必须初始化! 值为1std::cout << "i=" << i << ", " << "p=" << p << std::endl; //i=1, p=1p = 3; std::cout << "i=" << i << ", " << "p=" << p << std::endl; //i=3, p=3return 0;
    }
    
  • 处理顶层 const :所谓顶层 const 指的是对象本身是 const 的;与之相对的是底层 const ,指的是对象所指向的对象是 const 的,一个例子是 const int, int const 类型和 int *const 类型(注意,const 限定词适用于紧靠左侧的类型,除非左侧没有任何内容,才适用于紧靠右侧的类型)。以指针为示例,区分顶层和底层 const (阅读时从右往左读):

    • int const* :底层 constpointer to const int
    • int const *const :顶层和底层 constconst pointer to const int
    • int *const :顶层 constconst pointer to int
    • int *const * :底层 constpointer to const pointer to int
    • ……

    auto 会忽视掉顶层 const 但是保留底层 const ,如果要让 auto 变量的类型为顶层 const ,需要自行添加 const 修饰符。 而 decltype 会返回表达式返回值的类型,包括引用和顶层 const

    #include <iostream>
    int main() {int const i = 0, &r = i; //顶层constauto a = i; //a为int,忽略顶层constauto b = r; //b为int,忽略顶层const和引用auto c = &i; //c为int const*,即指向常量int的指针,保留底层const
    //	*c = 2; //错误,对只读变量*c赋值! auto const d = i; //添加const修饰符,d为int const类型,即常量intdecltype(i) x = 2; //保留顶层const,x为int const,必须初始化decltype(r) y = x; //保留顶层const和引用,y为int const&,是对常量的引用,引用必须初始化  
    //	decltype(r) z;	//错误,z是一个对常量的引用,引用必须初始化! return 0;
    }
    

5. 范围遍历的 for 语句

即使有了 auto 类型指示符,遍历容器和数组也有点麻烦。现在有了范围 for 语句,就可以写得很简洁了:

//有了auto
vector<int> v;
for (auto it = v.begin((); it != v.end(); ++it) cout << *it << ' ';//现在有了范围for
vector<int> v;
for (auto val : v) cout << val << ' ';

6. 尾置返回类型

普通函数基本上不需要尾置返回类型,不过如果函数的返回类型很复杂,尾置返回类型的用处就很大了。比如说 int (*func(int i))[10] 这种类型。关于如何阅读这些复杂类型,可见此处。

不过有了尾置返回类型,就可以像寻常定义变量的写法一样写返回类型了:

int (*func(int i))[10] {//do something
}
//代替一维数组
auto func(int i) -> int(*)[10] { //返回一维数组指针//do something
}
//代替二维数组
auto func(int i) -> int(*)[10][10] {//do something
}
//代替二重指针
auto func(int i) -> int ** { //这个可以不使用//do something
}

如果出现了更加复杂的返回类型,就可以用 decltype + auto 的双重组合拳。一个简要示例如下:

auto func(int a, int b) -> decltype(a + b) {//do somethingreturn a + b; 
}

在C++14以后,连尾置返回类型都可以省略了,直接返回 auto 就可以了,编译器会解决的。只要不过分挑剔,现代C++的写法完全可以像Python一样简洁优美!(正论)


7. lambda表达式

lambda表达式即所谓的匿名函数,会生成一个 function object ,常用作C++ STL中函数模板、类模板的谓词,比如 sort, transform, partial_sum 等等。一个完整的lambda表达式的格式如下:

[capture list] (params list) mutable exception -> return type { function body }

各个部分的具体格式为:

  • [capture list] :捕获外部变量列表,可以为空的 []不可以省略
  • (params list) :形式参数列表,可以为空的 ()不可以省略
  • mutable 指示符:说明是否可以修改捕获的变量可以省略
  • exception :异常设定,可以省略
  • return type :尾置返回类型,可以省略
  • function body :函数体,可以为空的 {}不可以省略

此处我们重点关注捕获列表和返回类型。

7.1 值捕获

类似于普通函数参数的值传递,被捕获变量的值将在lambda表达式创建时,通过值拷贝方式传入,外部对该变量的修改不会影响到lambda表达式内部的值,更重要的是,lambda表达式内部不能修改值捕获变量的值,或者说,值捕获的变量均为 read-only 变量

#include <iostream> 
int main() {int t = 123;auto func = [t]() {
//		t = 233; //错误!lambda不能修改值捕获变量 std::cout << t << std:: endl; }; t = 233; func(); //输出233return 0;
}

如果要做到和普通函数+值传递的行为一致,就需要允许修改值捕获变量,我们要在函数体前加上 mutable 关键字。此时对值捕获变量的修改,理所当然也不会影响到lambda表达式外部

#include <iostream> 
int main() {int t = 123;auto func = [t]() mutable {t = 233; std::cout << t << std:: endl; };  func(); //输出233std::cout << t << std::endl; //输出123 return 0;
}

7.2 引用捕获

类似于普通函数参数的引用传递,外部对该变量的修改会影响到lambda表达式内部的值,lambda表达式内部对该变量的修改会影响到外部的值。

#include <iostream> 
int main() { int t = 123;auto func = [&t]() mutable { std::cout << t << std:: endl; };  func(); //输出123t = 233; //外部修改变量func(); //输出233  return 0;
}
#include <iostream> 
int main() {int t = 123;auto func = [&t]() mutable { t = 233;};  std::cout << t << std::endl; //输出123 func(); //lambda表达式内部修改变量 std::cout << t << std::endl; //输出233  return 0;
}

7.3 隐式捕获

不想写太多的捕获变量,就可以指定一种捕获类型,要么全是值捕获,要么全是引用捕获,让编译器推断需要捕获哪些变量。

#include <iostream> 
int main() {int a = 11, b = 22, c = 33; auto func = [=]() { //全是值捕获 std::cout << a << ' ' << b << ' ' << c << ' ' << std::endl; };  func(); //输出11 22 33 return 0;
}
#include <iostream> 
int main() {int a = 11, b = 22, c = 33; auto func = [&]() { //全是引用捕获 a = 1, b = 2, c = 3; };  func();   std::cout << a << ' ' << b << ' ' << c << ' ' << std::endl;  //输出1 2 3 return 0;
}

7.4 混合捕获

如果有些变量需要值捕获,有些需要引用捕获,就需要混合起来:

auto func = [&a, b]() { //a引用捕获, b值捕获//do something
}
auto func = [&, a]() { //a值捕获, 其余变量引用捕获//do something
}
auto func = [=, &a]() { //a引用捕获, 其余变量值捕获//do something
}

7.5 返回类型

上述的lambda表达式都没有指定尾置返回类型,却能够通过编译,也是由于编译器的作用——根据 return 语句推断出了lambda表达式的返回类型,不过这种推断的能力有限:

auto func = [](int t) {if (t == 1) return 233;return 332.5; //[Error] inconsistent types 'int' and 'double' deduced for lambda return type
}

此时编译报错。因此对于有多个 return 语句的情况,需要自行指定lambda表达式的返回类型

auto func = [](int t) -> double {if (t == 1) return 233;return 332.5;
}

lambda表达式看似复杂,其实也有点复杂,不过不深究就可以用得很爽,特别是用于函数式编程

这篇关于【现代C++】C++11新特性 nullptr、constexpr、auto、decltype、范围for、尾置返回类型、lambda表达式、复杂类型阅读的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/282769

相关文章

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

Python中Json和其他类型相互转换的实现示例

《Python中Json和其他类型相互转换的实现示例》本文介绍了在Python中使用json模块实现json数据与dict、object之间的高效转换,包括loads(),load(),dumps()... 项目中经常会用到json格式转为object对象、dict字典格式等。在此做个记录,方便后续用到该方

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Java实现复杂查询优化的7个技巧小结

《Java实现复杂查询优化的7个技巧小结》在Java项目中,复杂查询是开发者面临的“硬骨头”,本文将通过7个实战技巧,结合代码示例和性能对比,手把手教你如何让复杂查询变得优雅,大家可以根据需求进行选择... 目录一、复杂查询的痛点:为何你的代码“又臭又长”1.1冗余变量与中间状态1.2重复查询与性能陷阱1.

python中的显式声明类型参数使用方式

《python中的显式声明类型参数使用方式》文章探讨了Python3.10+版本中类型注解的使用,指出FastAPI官方示例强调显式声明参数类型,通过|操作符替代Union/Optional,可提升代... 目录背景python函数显式声明的类型汇总基本类型集合类型Optional and Union(py

MySQL中查询和展示LONGBLOB类型数据的技巧总结

《MySQL中查询和展示LONGBLOB类型数据的技巧总结》在MySQL中LONGBLOB是一种二进制大对象(BLOB)数据类型,用于存储大量的二进制数据,:本文主要介绍MySQL中查询和展示LO... 目录前言1. 查询 LONGBLOB 数据的大小2. 查询并展示 LONGBLOB 数据2.1 转换为十

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

C++ vector越界问题的完整解决方案

《C++vector越界问题的完整解决方案》在C++开发中,std::vector作为最常用的动态数组容器,其便捷性与性能优势使其成为处理可变长度数据的首选,然而,数组越界访问始终是威胁程序稳定性的... 目录引言一、vector越界的底层原理与危害1.1 越界访问的本质原因1.2 越界访问的实际危害二、基

基于Python实现数字限制在指定范围内的五种方式

《基于Python实现数字限制在指定范围内的五种方式》在编程中,数字范围限制是常见需求,无论是游戏开发中的角色属性值、金融计算中的利率调整,还是传感器数据处理中的异常值过滤,都需要将数字控制在合理范围... 目录引言一、基础条件判断法二、数学运算巧解法三、装饰器模式法四、自定义类封装法五、NumPy数组处理

MyBatis的xml中字符串类型判空与非字符串类型判空处理方式(最新整理)

《MyBatis的xml中字符串类型判空与非字符串类型判空处理方式(最新整理)》本文给大家介绍MyBatis的xml中字符串类型判空与非字符串类型判空处理方式,本文给大家介绍的非常详细,对大家的学习或... 目录完整 Hutool 写法版本对比优化为什么status变成Long?为什么 price 没事?怎