【现代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

相关文章

IDEA如何将String类型转json格式

《IDEA如何将String类型转json格式》在Java中,字符串字面量中的转义字符会被自动转换,但通过网络获取的字符串可能不会自动转换,为了解决IDEA无法识别JSON字符串的问题,可以在本地对字... 目录问题描述问题原因解决方案总结问题描述最近做项目需要使用Ai生成json,可生成String类型

使用C#代码计算数学表达式实例

《使用C#代码计算数学表达式实例》这段文字主要讲述了如何使用C#语言来计算数学表达式,该程序通过使用Dictionary保存变量,定义了运算符优先级,并实现了EvaluateExpression方法来... 目录C#代码计算数学表达式该方法很长,因此我将分段描述下面的代码片段显示了下一步以下代码显示该方法如

C++中实现调试日志输出

《C++中实现调试日志输出》在C++编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助... 目录1. 使用 #ifdef _DEBUG 宏2. 加入时间戳:精确到毫秒3.Windows 和 MFC 中的调试日志方法MFC

五大特性引领创新! 深度操作系统 deepin 25 Preview预览版发布

《五大特性引领创新!深度操作系统deepin25Preview预览版发布》今日,深度操作系统正式推出deepin25Preview版本,该版本集成了五大核心特性:磐石系统、全新DDE、Tr... 深度操作系统今日发布了 deepin 25 Preview,新版本囊括五大特性:磐石系统、全新 DDE、Tree

Python中lambda排序的六种方法

《Python中lambda排序的六种方法》本文主要介绍了Python中使用lambda函数进行排序的六种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们... 目录1.对单个变量进行排序2. 对多个变量进行排序3. 降序排列4. 单独降序1.对单个变量进行排序

SpringBoot基于MyBatis-Plus实现Lambda Query查询的示例代码

《SpringBoot基于MyBatis-Plus实现LambdaQuery查询的示例代码》MyBatis-Plus是MyBatis的增强工具,简化了数据库操作,并提高了开发效率,它提供了多种查询方... 目录引言基础环境配置依赖配置(Maven)application.yml 配置表结构设计demo_st

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

Mysql 中的多表连接和连接类型详解

《Mysql中的多表连接和连接类型详解》这篇文章详细介绍了MySQL中的多表连接及其各种类型,包括内连接、左连接、右连接、全外连接、自连接和交叉连接,通过这些连接方式,可以将分散在不同表中的相关数据... 目录什么是多表连接?1. 内连接(INNER JOIN)2. 左连接(LEFT JOIN 或 LEFT

Redis的Hash类型及相关命令小结

《Redis的Hash类型及相关命令小结》edisHash是一种数据结构,用于存储字段和值的映射关系,本文就来介绍一下Redis的Hash类型及相关命令小结,具有一定的参考价值,感兴趣的可以了解一下... 目录HSETHGETHEXISTSHDELHKEYSHVALSHGETALLHMGETHLENHSET

在 VSCode 中配置 C++ 开发环境的详细教程

《在VSCode中配置C++开发环境的详细教程》本文详细介绍了如何在VisualStudioCode(VSCode)中配置C++开发环境,包括安装必要的工具、配置编译器、设置调试环境等步骤,通... 目录如何在 VSCode 中配置 C++ 开发环境:详细教程1. 什么是 VSCode?2. 安装 VSCo