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

相关文章

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝