Re:从零开始学C++(二)基础精讲·下篇:内联函数与空指针

◆ 博主名称: 晓此方-CSDN博客

大家好,欢迎来到晓此方的博客。

⭐️个人专栏:

◆数据结构系列

专治数据结构与算法疑难杂症_晓此方的博客-CSDN博客

此方玩转算法与数据结构_晓此方的博客-CSDN博客

◆C语言系列

专治C语言疑难杂症_晓此方的博客-CSDN博客

◆C++系列

此方带你玩转C++_晓此方的博客-CSDN博客

⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰


目录

0.1概述&前言

一,内联函数

1.1内联函数的定义

1.2宏函数的问题所在

1.2.1直接文本替换

1.2.1.1情况一

1.2.1.2情况二

1.2.1.3情况三

1.2.2无法调试

1.3如何进行内联展开

1.3.1内联函数的使用方式

1.3.2准备工作

1.4内联展开的底层剖析

1.4.1编译器是否展开内联函数

1.4.1.1编译器没有展开内联函数时:

1.4.1.2编译器展开内联函数时:

1.5内联函数的优劣

1.5.1展开的好处

1.5.2展开的代价

1.5.3声明与定义分离

二,nullptr(c++11新增内容)

2.1NULL的坑


0.1概述&前言

C语言的宏函数和NULL空指针存在许多缺陷,在C++中,引入了inline函数和nullptr来代替他们,解决了定义不严谨等众多问题。同时也提升了编译语言的编写体验。本篇此方将从底层出发,为大家带来内联函数和nullptr的详细内容。讲解深入骨髓,细节无微不至。以真诚换真心,倾尽全力做到最好。现在,让我们开始吧。


一,内联函数

1.1内联函数的定义

inline修饰 的函数叫做内联函数 ,编译时C++编译器会在调用的地方展开内联函数 ,这样调用内联函数就不需要建立栈帧 了,就可以提高效率。

内联函数的出现,直接取代了C语言中的宏函数。

1.2宏函数的问题所在

1.2.1直接文本替换

宏函数编译采用直接文本替换 ,**缺乏类型检查,**存在潜在的安全问题

cpp 复制代码
// 实现一个ADD宏函数的常见问题
//#define ADD(int a, int b) return a + b;
//#define ADD(a, b) a + b;
//#define ADD(a, b) (a + b)

// 正确的宏实现
#define ADD(a, b) ((a) + (b))
  1. 为什么宏函数不能加分号?
  2. 为什么要有外括号?
  3. 为什么要有内括号?
cpp 复制代码
printf("%d",Add(1,2));
1.2.1.1情况一

宏函数在使用时会进行宏替换 ,将宏名 部分全部替换为宏体,如果宏体有分号,会被一块儿替换进去,产生编译错误。

cpp 复制代码
cout <<Add(1,2)*5<<endl;
1.2.1.2情况二

优先级错误一:此时宏替换,如果没有外扩号,就会变成(a)+(b)*5,先计算5*b,再计算加减。导致结果错误。

cpp 复制代码
Add(x&y,x|y);
1.2.1.3情况三

优先级错误二:此时宏替换,如果没有内括号,就会变成x&(y+x)|y,先加减,再与或,导致计算结果错误。

1.2.2无法调试

宏函数不是一个真正意义上的函数,在预处理阶段 就会直接进行文本替换,而调试器看到的是预处理后的代码 。此外,即使在宏函数所在行设置断点也无意义,调试器无法在断点处找到一个有效的函数

1.3如何进行内联展开

1.3.1内联函数的使用方式

直接在一个函数前面加上inline关键字即可。

cpp 复制代码
inline Add(int x,int y)
{
    int z = x + y ;
    return z ;
}

1.3.2准备工作

vs编译器debug版本 下面默认是不展开 inline的,这样方便调试,debug版本想展开需要设置一下以下两个地方。

项目 处右击进入项目属性:

这样,你的编译器就可以将内联函数展开以提升运行效率了。

1.4内联展开的底层剖析

inline对于编译器而言只是一个建议 ,也就是说,你加了inline编译器也可以选择 在调用的地方不展开 ,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定 这个。inline适用于频繁调用的短小函数 ,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略

关键点: inline是对编译器的"建议 ",不是强制命令

1.4.1编译器是否展开内联函数

1.4.1.1编译器没有展开内联函数时:
cpp 复制代码
inline int Add(int a, int b)
{
	int ret = a + b;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	ret++;
	return ret;
}
int main()
{
	cout << Add(4, 9)<<endl;
	return 0;
}
  • 使用了 call 指令 → 表示跳转到函数地址执行。
  • 执行完后返回。
  • 这说明:虽然写了 inline,但编译器没展开,而是当作普通函数调用。

详细调用过程:

  1. RCX <- 4 (第一个参数),RCX<- 9 (第二个参数)
  2. call指令 下一条指令的地址(即main函数中call之后的下一行代码地址)压入当前线程的栈顶。
  3. 跳转到jmp指令jmp指令 又跳转到函数的第一条指令,调用完成
1.4.1.2编译器展开内联函数时:

如果编译器决定展开,则不会出现 call ,而是在调用位置直接插入函数体的代码

cpp 复制代码
inline int Add(int a, int b)
{
	int ret = a + b;
	ret++;
	ret++;
	ret++;
	ret++;
	return ret;
}
int main()
{
	cout << Add(4, 9)<<endl;
	return 0;
}

1.5内联函数的优劣

1.5.1展开的好处

根据上文从底层的剖析,我们知道,展开可以省去函数调用等一系列复杂操作 。从而提升函数的运行效率。虽然展开同时会影响编译速度,但是"对函数运行效率的提升"对比"对函数编译速度的下降"的影响始终要大。

1.5.2展开的代价

展开来提升效率固然不错。但是展开也不是绝对好。

编译器并不会 无条件展开,言听计从

设想一个场景 : 如果一个内联函数在项目中存在10000个调用点 ,该内联函数本身有100条语句 ,每条语句的大小大概在2 到 10 字节(假设6字节) ,那么全部展开将增加100万条语句 ------也就是600万字节 ,相当于:**5.722 MB,**一个内联函数让一个安装包平白无故增加6MB的空间,显然是不值得的。

但是合理使用可以将内联函数的缺点缩小到极小(可忽略不计)。编译器会帮助你合理使用。

1.5.3声明与定义分离

内联函数不可以进行声明和定义分离。如果分离,内联函数的调用点将找不到内联函数本体。

原理:由于C++的一些规定,内联函数无法像普通函数那样被使用函数的声明发现

或者将定义直接放在调用点所在文件,或者将内联函数定义连同声明一同放在头文件中。


二,nullptr(c++11新增内容)

传统C语言头文件stddef.h中,可以看到如下代码:

cpp 复制代码
1  #ifndef NULL
2      #ifdef __cplusplus
3          #define NULL    0
4      #else
5          #define NULL    ((void *)0)
6      #endif
7  #endif

解释:

  • C++ 中:NULL被定义为 0(整数常量)
  • C 中:NULL被定义为(void*)0,即一个指向空的void*指针.

但是这个设计不是很ok。c语言中还好,c++中有很大的 ,所以C++不使用NULL,取而代之的是nullptr ,意思是空指针

2.1NULL的坑

cpp 复制代码
void f(int x)
{
    cout << "f(int x)" << endl;
}

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

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

猜猜看打印结果是什么?结果是。

cpp 复制代码
f(int x)
f(int x)

由于以上的这种定义特性 ,C++中的NULL无法表示实际的空指针 。那么**强转成void***不就可以了?还是不行:

C++有比C语言更加严格的类型检查,俩个都不匹配,这个时候就要进行隐式类型转换。但是不知道转成什么。

那么C语言为什么可以这么定义?实际上C语言和c++的这种定义确实都是不好的。C语言的定义中,C语言自己这么定义问题不大。因为C语言定义允许void*类型的指针给任意类型的其他指针不需要强转 (这让C语言malloc之后完全可以不需要强转直接给,因为malloc结果是void*类型的指针),但是C++不允许 (C++兼容C语言的绝大多数语法,但是也会有像这样的小差异),如以下代码必须强制类型转换才能生效

cpp 复制代码
void* p1 = NULL;
int* p2 = p1;

C++11中引入nullptr,nullptr是一个特殊的关键字 ,nullptr是一种特殊类型 的字面量,它可以转换成任意其他类型的指针类型 。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能 被隐式地转换为指针类型而不能被转换为整数类型

nullptr也是0 ,但是对比NULL更好,解决了以上的特殊情况。**在C++以后都用nullptr!**C语言还是原来那套没问题。


以上就是本篇的全部内容了,C++的全部基础内容到此为止 。此刻,你我正式步入了C++的世界。如果本篇对你有帮助,不要忘了点赞+投币+收藏三联一波哦!我是此方,我们下期再见。

相关推荐
心.c2 小时前
初步了解Next.js
开发语言·前端·javascript·js
桃花岛主702 小时前
go-micro,v5启动微服务的正确方法
开发语言·后端·golang
Kiri霧2 小时前
Go 结构体高级用法
开发语言·后端·golang
csdn_life182 小时前
Rustrover 如何像Java一样直接 进行调试和运行
java·开发语言·rust
草莓熊Lotso3 小时前
C++11 核心特性实战:列表初始化 + 右值引用与移动语义(附完整代码)
java·服务器·开发语言·汇编·c++·人工智能·经验分享
初夏睡觉4 小时前
从0开始c++,但是重置版,第1篇(c++基本框架)
开发语言·c++
草莓熊Lotso4 小时前
GCC/G++ 编译器完全指南:从编译流程到进阶用法(附实操案例)
linux·运维·服务器·网络·c++·人工智能·自动化
workflower9 小时前
时序数据获取事件
开发语言·人工智能·python·深度学习·机器学习·结对编程
CoderYanger10 小时前
C.滑动窗口-求子数组个数-越长越合法——2799. 统计完全子数组的数目
java·c语言·开发语言·数据结构·算法·leetcode·职场和发展