【C++修炼之路】C++入门(下)

news2024/12/23 14:27:19

👑作者主页:@安 度 因
🏠学习社区:StackFrame
📖专栏链接:C++修炼之路

文章目录

  • 一、前言
  • 二、内联函数
    • 1、概念
    • 2、特性
  • 三、auto(C++ 11)
    • 1、概念
    • 2、价值
    • 3、三个不能
  • 四、范围for循环(C++11)
    • 1、基本使用
    • 2、使用条件
  • 五、指针空值nullptr(C++11)
  • 六、结语

如果无聊的话,就来逛逛 我的博客栈 吧! 🌹

一、前言

大家好,我是 a n d u i n anduin anduin . 这篇文章是 C++ 入门的最后一篇。内容主要为 内联函数、auto、范围 for 循环和 nullptr 空指针 ,对于后三个都是 C++11 的特性。内容不多,也不难,让我们开始学习吧!

二、内联函数

调用函数需要建立栈帧,栈帧中要保存寄存器,结束后就要恢复,这其中都是有 消耗 的:

int add(int x, int y)
{
	int ret = x + y;
	return ret;
}

int main()
{
	add(1, 2);
	add(1, 2);
	add(1, 2);
	add(1, 2);
	add(1, 2);

	return 0;
}

而针对 频繁调用小函数,可以用 优化,因为宏是在预处理阶段完成替换的,并没有执行时的开销,并且因为代码量小,也不会造成代码堆积。

例如,代码就可以写成这样:

#define ADD(x, y) ((x) + (y)) 

int main()
{
	cout << ADD(1, 2) << endl;

	return 0;
}

但是宏也有缺点:

  1. 不能调试
  2. 没有类型安全的检查
  3. 有些场景下非常复杂

就拿 add 函数来说,可能一不小心就会写成 #define ADD(x + y) x + y 的样子;所以写宏时出错,要么是替换出错,要么是因为优先级出错,所以宏并不友好。

而 C++ 针对为了减少函数调用开销,又可以在一定程度上替代宏,避免宏的出错,从而设计出了内联函数

内联函数的关键字为 inline

inline int add(int x, int y)
{
	int ret = x + y;
	return ret;
}

int main()
{
	int ret = add(1, 2);
	cout << ret << endl;

	return 0;
}

1、概念

在 release 版本下,inline 内联函数会直接在调用部分展开;对于 debug 则需要 主动设置 (debug 下编译器默认不对代码做优化);但是 release 版本下其他版本优化的太多,可能就不太好观察,所以我们设置一下编译器,在 debug 下看:

打开解决方案资源管理器,右击项目名称,选中属性并打开,在 C/C++ 区域常规部分,在调试信息一栏设置格式为程序数据库

image-20230130103426993

在 C/C++ 优化一栏,将内联函数扩展部分选中只适用于 _inline

image-20230130103641075

设置完毕后,点击应用。

在设置前、后,分别启动调试,查看反汇编代码:

修改前:

image-20230130104123005

修改后:

image-20230130104227711

两段反汇编代码最大的区别就是 call 消失了 ,call 就是函数调用的指令,它的消失就说明第二段代码没有进行调用。内联函数直接在局部展开了,在 main 函数中完成了操作。有了内联,我们就不需要去用 c 的宏了,因为宏很容易出错。

2、特性

  1. inline是一种以 空间换时间 的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
  2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。下图为《C++prime》第五版关于inline的建议:
    image-20230130115508988
  3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

要点讲解

1)空间换时间是因为反复调用内联函数,导致编译出来的可执行程序变大

inline void func()
{
    // 假设编译完成为 10 条指令
}

若不用内联函数,不展开,若1000次调用 func,每次调用的地方为 call 指令的形式,总计 1010 行指令。若用内联函数,则展开,若一千次调用,每次调用的地方为都会展开为 10 条指令,总计 10 * 1000 行指令。

展开会让编译后的程序变大,如果递归函数作内联,后果可想而知。所以长函数和递归函数不适合展开

2)编译器可以忽略内联请求,内联函数被忽略的界限没有被规定,一般10行以上就被认为是长函数,当然不同的编译器不同

基于上面的解释,所以编译器会决策是否使用内联函数。因为如果函数太大导致的结果很糟糕。

3)内联函数声明和定义不可分离

// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
	cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
	f(10);
	return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

由于内联函数在调用的地方展开,所以内联函数无地址(这里的地址指的是call 指令调用函数的地址,通过这个地址会跳到 jmp 指令处,再根据 jmp 处指令跳转到函数执行的部分) ,即 f.cpp->f.o 符号表中,不会生成 f 的地址。

当编译时,由于头文件要被包含,但是这时只有函数声明,但是没有函数的定义,所以只能在链接时展开,这里只能变为 call + 地址的指令,但是内联函数并没有地址,链接不到,就报错了。

所以当声明和定义分离,调用函数时,由于内联函数无地址,编译器链接不到,所以就会报错,为链接错误

结论:简短,频繁调用的小函数建议定义成 inline .

三、auto(C++ 11)

1、概念

在 c 语言中,也存在 auto 关键字。

int main()
{
    auto int a = 0;
}

auto 关键字修饰后,a变为自动存储类型,即变量会在函数结束以后自动销毁。但是这个语法完全多此一举,因为后来,对于局部变量默认就是自动存储类型,当函数结束后也会自动销毁。

于是 C++ 委员会废弃了 auto 的用法,赋予了新的意义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

int main()
{
	int a = 0;
	int b = 0;
	auto c = a; // 自动推导 c 的类型
	auto d = 'A';
	auto e = 10.11;

	return 0;
}

auto 会自动推导变量的类型。

typeid 可以看对象类型,用法为 typeid(c).name() ,由此可以打印变量的类型:

image-20230130132744135

对于 auto 来说,并不会保留 const 属性,例如 e ,10.11 是常量,就丢弃了 const 属性,而保留 double .

image-20230130133028869

对于 auto ,如果要加上 const 属性,则需要主动加上:

int main()
{
	int x = 10;
	const auto y = x;
	cout << typeid(y).name() << endl; // 这里不会打印出,需要调试看

	return 0;
}

image-20230130141526876

2、价值

auto 具有两种针对场景:

  1. 类型难于拼写
  2. 含义不明确导致容易出错

上面的使用仅仅是样例,而它真正的价值体现在 stl 部分范围遍历 中。

在 stl 中,有些类型名字很长,例如:

#include <map>

int main()
{
	map<string, string> dict;
	map<string, string>::iterator it = dict.begin();

	return 0;
}

当要取其迭代器进行遍历时,很复杂,这时 auto 就派上用场了。

auto it = dick.begin();

它会根据右边的返回值类型,自动推导 it 的类型,写起来十分方便。

对于这里使用 typedef 进行重命名也可以

int main()
{
	map<string, string> dict;
	typedef map<string, string>::iterator Dict;
	Dict it = dict.begin();

	return 0;
}

虽然 typedef 可以使用,但是 typedef 有一个缺点,看这个例子:

image-20230202194331558

p1 是失败的

因为 typedef 这里不是展开为 const char* p1 ,实际上这里 typedef 之后是 char* const p1 ,const 修饰的是 p1 ,但是 p1 没有初值,之后也改不了了,所以就失败了;但是 p2 展开是 char* const *p2 ,修饰的是 *p2 ,此刻 p2 是能改的,所以没问题。

验证一下:

typedef char* pstring;

int main()
{
	const pstring p1 = nullptr; // 需要一个初值
	const pstring* p2;
}

image-20230202194825557

auto 很灵活,甚至可以在后面附上其他属性,附上属性后,就显示表现它的类型:

image-20230130140727872

auto& c 的意思就是,c 为 x 的别名,但是类型还是 int ,不过显示认为,它就是推导出来类型的别名。

3、三个不能

  1. auto不能独立定义

image-20230130142147801

  1. auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a) {}

因为编译器无法推导 auto 的类型,没有根据。

  1. auto不能定义数组
void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {456};
}

四、范围for循环(C++11)

1、基本使用

之前对于数组的遍历,需要使用下标遍历:

int main()
{
	int arr[] = { 1, 2, 3, 4,5 };

	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) 
	{
		printf("%d ", arr[i]);
	}

	return 0;
}

而 C++ 中效仿新语言,加入了范围遍历:

int main()
{
	int arr[] = { 1, 2, 3, 4,5 };

	for (auto num : arr) 
	{
		cout << num << endl;
	}

	return 0;
}

其中就用到了 auto 关键字,当复杂类型遇到auto和范围遍历,就是天堂。

而范围for循环的原理就是自动取遍历目标的每一个元素,再放到给定的临时变量中。在上方就是取 arr 的元素放到 num 中,并自动判断结束。auto 会根据遍历目标的元素类型自动推导,当然直接写类型 int 也对

如果要使用范围遍历修改遍历目标,则可以使用引用

for (auto num : arr) { num ++ ;}

若以这种写法,是不会改变的,因为 num 是局部变量,只是值和 arr 中元素相等,本身并没有关联。

但是如果加上引用,意义就变了:取遍历目标每个元素的值给 num ,让 num 变为那些元素的别名

image-20230130144633348

而对于 num 的生命周期,则可以认为仅在每次范围遍历中(某一次循环)才存在。

范围 for 会根据遍历目标的元素类型来取出元素,例如上方例子就是 int ,如果这时用指针接收,就是错误的

image-20230130144836539
因为取出来的每一个元素是 int ,类型不匹配。而判断结束我们并不用担心,其实和普通遍历类似。

2、使用条件

for循环迭代的范围必须是确定的

对于数组的范围就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

以下代码就有问题,因为for的范围不确定,因为函数传参,数组就会退化为指针:

void TestFor(int array[])
{
	for (auto& e : array)
    {
        cout << e << endl;
    }
}

是错误的。

五、指针空值nullptr(C++11)

对于 c 来说,空指针为 NULL,是一个宏。

在 C++98/03 时,只能使用 NULL ;而 C++11 后,推荐使用 nullptr 。

NULL

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

实际上 NULL 就是个宏,所以说写成 int* p = 0 ,也可以;而j绝大多数情况下,这样写都没问题。

但是对于极端场景:

void f(int) // 这边由于不使用形参,不给形参名也可以
{
	cout << "f(int)" << endl;
}

void f(int*)
{
	cout << "f(int*)" << endl;
}

int main()
{
	f(0);
	f(NULL);
	return 0;
}

按道理,对于第一次调用,应该匹配第一个,对于第二次调用,应该匹配第二个。

但是实际上它们都匹配了第一个,原因是 NULL 是一个宏,本质为 0 .

image-20230130151237088

在C++98中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针(void* )常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void* )0,例如:(int*)NULL ,所以在 C++11 后,使用 nullptr 是明智的选择。

注意点

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

六、结语

到这里本篇博客就到此结束了。

对于 C++ 杂七杂八的知识点,我们就讲到这里。下一篇文章,我们开始讲解类和对象,做好准备哦!

如果觉得 a n d u i n anduin anduin 写的不错的话,可以点赞 + 收藏 + 评论支持一下哦!

那么我们下期见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/193672.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL基础(3)—— MySQL数据类型

文章目录数值类型1、整数类型2、浮点数类型3、定点数类型日期和时间类型1、YEAR2、DATE、TIME、DATETIME3、TIMESTAMP字符串类型1、CHAR(M)2、VARCHAR(M)3、各种 TEXT 类型4、ENUM 类型和 SET 类型二进制类型1、BIT 类型2、BINARY(M) 与 VARBINARY(M)3、BLOB 类型MySQL说到底就…

移动出行2023:聊以新颜待今朝

兔年春节期间&#xff0c;城市再现浓浓烟火气。预订全满的年夜饭、排不到号的奶茶店以及火爆的电影票房等&#xff0c;证明着“吃、游、购、娱”等需求集中释放的“威力”。根据国家税务总局发布的最新数据&#xff0c;今年春节假期&#xff0c;全国消费相关行业销售收入与上年…

最长上升子序列问题(LIS问题)与最长不上升子序列问题的四种方法(c++ 模板代码)

文章目录动态规划树状数组线段树二分查找最大上升子序列问题也叫做LIS问题&#xff0c;与最大公共子序列LCS问题是一类经典问题&#xff0c;在本章我们将总结一下求解LIS最大上升子序列的几种方法&#xff0c;同时也会给出对应的最大不上升子序列的求解方法。 关于LCS问题&…

【表格单元格可编辑】vue-elementul简单实现table表格点击单元格可编辑,点击单元格变成输入框修改数据

前言 这是最近遇到的功能&#xff0c;经常会需要一个表格可以编辑数据 类似于excel那种点击一下单元格就可以编辑数据&#xff0c;修改后鼠标移动出去 光标消失就会保存数据给后台 这里记录一下实现方法&#xff0c;其实也比较简单 就是通过角标来判断显示隐藏的 效果图 代码…

[Android开发基础4] 点击事件的响应与处理

文章目录 方法一&#xff1a;控件的onClick属性 方法二&#xff1a;内部类 方法一&#xff1a;控件的onClick属性 利用控件自带的onClick属性&#xff0c;指定事件处理函数名称即可实现控件点击事件的处理 这里有个小技巧就是当设置完控件的onClick属性后&#xff0c;它会报没…

XXE漏洞常见利用点总结

目录 知识点小结 常用payload 本地文件读取 SSRF 引入外部实体 dtd 信息探测 XXE漏洞攻击 案例演示 案例一&#xff08;有回显&#xff09; 案例二&#xff08;无回显读取本地敏感文件(Blind OOB XXE)&#xff09; XXE 防御 使用语言中推荐的禁用外部实体的方法 知…

08-linux网络管理-iftop命令详解

文章目录1. 安装2. 基本使用2.1 命令2.2 输出2.3 说明3. 选项3.1 选项说明3.2 几个示例-n&#xff08;不查找主机名&#xff09;-i &#xff08;查看指定网卡流量&#xff09;-P&#xff08;显示主机端口&#xff09;-t&#xff08;不使用ncurses 界面&#xff09;4. ncurses界…

IDEA集成Docker插件实现一键自动打包部署

一. 概述 大家部署项目的时候&#xff0c;动辄十几个服务&#xff0c;每次修改逐一部署繁琐不说也会浪费越来越多时间&#xff0c;所以本篇整理通过一次性配置实现一键部署微服务&#xff0c;直接上教程。 二. 配置服务器 1、Docker安装 服务器需要安装Docker&#xff0c;如…

iOS单元测试怎么写 ?

iOS单元测试怎么写 ? 什么是单元测试 ? 针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。对于面向对象编程&#xff0c;最小单元就是方法 iOS 集成了自己的测试框架 OCUnit 和 UITests 为什么单元测试 ? 执行单元测试&#x…

网络基础2-3 ---传输层的UDP协议:DP的特点,UDP的协议格式,UDP的应用

目录 一、tcpdump命令 二、UDP协议 前言 2.1、UDP协议的特点&#xff1a; 2.2、UDP的协议格式 16位的UDP长度&#xff1a; 结合抓包工具&#xff1a;分析一下UDP协议&#xff0c;就利用我们之前写的udp_socket编程 16位的校验和&#xff1a; 2.3、UDP的应用&#xff1…

Python的序列结构及常用操作方法,学完这一篇你就彻底懂了

上一篇&#xff1a;Python流程控制语句之跳转语句 文章目录前言一、索引二、切片三、序列相加四、乘法五、检查某个元素是否是序列的成员六、计算序列的长度、最大值和最小值总结前言 序列是一块用于存放多个值的连续内存空间&#xff0c;并且按一定顺序排列&#xff0c;每个值…

【论文阅读】Cleanits: A Data Cleaning System for Industrial Time Series

论文来源 标题: Cleanits (Xiaoou Ding,2019) 作者: Xiaoou Ding, Hongzhi Wang, Jiaxuan Su, Zijue Li, Jianzhong Li, Hong Gao 期刊: Proceedings of the VLDB Endowment 研究问题 工业时间序列数据清洗系统 1&#xff09;缺失值插补&#xff0c;2&#xff09;匹配不一致…

Maven可选依赖与排除依赖

可选依赖——指的是对外隐藏当前所依赖的资源&#xff08;不透明&#xff09; 可选依赖的作用&#xff1a;是隐藏所使用的依赖&#xff0c;用于控制当前依赖资源能否被别人发现 可选依赖的含义&#xff1a;当前工程所依赖的资源&#xff0c;不被其他项目所调用此依赖 pom文件…

DolphinScheduler 3.1.0 海豚集群运维使用问题记录

文章目录海豚常见问题1. 认证问题2. 时区问题3. jdk问题导致的认证问题4. 海豚调度sqoop任务问题(1. 海豚不允许脚本有空行出现(2. 脚本调脚本:权限不足(3. 直接执行某个表的sqoop同步任务:(4. sudo权限不足5. 海豚配置hive/impala数据源问题(1.海豚连接hive数据源配置(2. 配置…

[架构之路-97]:《软件架构设计:程序员向架构师转型必备》-7-需求分析与业务需求领域建模

前言&#xff1a;需求分析工程师工作中业务领域&#xff0c;而业务领域有很多业务领域专有的概念&#xff1b;程序员主要工作在计算机领域&#xff0c;他们没有足够的业务领域的知识识别业务领域的过于专业化的业务需求。为了确保业务需求能够被软件工程师正确无误地实现&#…

《MFC编程》:第一个MFC程序

《MFC编程》&#xff1a;第一个MFC程序《MFC编程》&#xff1a;第一个MFC程序设置开发环境如何把一个win32程序改成MFC程序&#xff1f;代码书写《MFC编程》&#xff1a;第一个MFC程序 设置开发环境 头文件为<afxwin.h>&#xff1b;在设置中勾选“使用MFC库”。 注&…

53.Isaac教程--ZED相机

ZED相机 ISAAC教程合集地址文章目录ZED相机Codelets支持的固件下载出厂校准文件通过本地校准提高相机精度为相机校准文件指定自定义位置Isaac SDK 支持 StereoLabs ZED 和 ZED Mini (ZED-M) 以及 ZED2 立体相机。 使用本节中的程序下载出厂校准文件或在相机上执行本地校准。 …

看涨期权与看跌期权

目录 1. 看涨期权多头 2. 看涨期权空头 3. 看跌期权多头 4. 看跌期权空头 买进期货合约者称为多头&#xff0c;卖出股指期货合约者称为空头。 1. 看涨期权多头 买入沪深 300 指数的看涨期权&#xff0c;行权价 2000 点&#xff0c;期限 1 个月期权费 100 点1 点 100 元初…

PMP和ACP哪个更有用?

PMP证书和ACP证书都是项目管理类的证书&#xff0c;但是方向不一样&#xff0c;ACP特别验证了从业者在项目工作中理解及实施敏捷管理原则与实践的能力&#xff0c;PMP则认证了从业者所表现出的领导和引导项目团队的能力。 PMP是传统的项目管理模式&#xff0c;适合各行各业&am…

日常小工具之:不花一分钱,不限制视频大小,用 python 和 ffmpeg 批量视频转格式,并保存到 iphone / ipad

应用背景 2008 年左右买的一个系列视频&#xff0c;全都是 .rmvb 的格式&#xff0c;想移到 iphone 里面&#xff0c;但是显示解码格式不支持上 知乎 看格式转换的工具发现这些工具需要把视频上传上去处理&#xff0c;而且很慢&#xff0c;而且有些还限制视频大小 我觉得有必要…