【C++】C++入门—(下)
前言:上一篇文章我们着重介绍了C++的函数重载和引用,我们探寻了函数重载的奥妙以及引用相较于指针的便捷,高效。那么这一篇文章我们就来讲讲最后的两个内容:内联函数和nullptr。话不多说,赶紧开始吧!
订阅C++的篇目请点击:C++专栏
文章目录
- 一,内联函数
- 1.1宏
- 1.2内联函数
- 1.2.1内联函数的概念
- 1.2.2内联函数的特性
- 1.3内联函数与宏的对比总结
- 1.3.1代码示例对比
- 二,nullptr
一,内联函数
1.1宏
在正式讲内联函数之前不妨想一想内联优化的是C语言的哪个部分?像上一篇文章所讲述的函数重载优化的就是C语言函数重名的问题,引用优化的就是指针的复杂度。
那么本章节我们所要讲的内联其实优化的就是C语言的宏。
所以我们不妨先从宏讲起:
//回忆一下在C语言实现一个宏函数
#include<iostream>
using namespace std;
//这里先来看看错误的几种宏
//#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);//这才是宏函数正确的写法!
#define Add(a,b) ((a)+(b));
int main()
{int a=1,b=2;int ret=Add(a,b);//宏替换后变成 ret=return 1+2; 语法报错//第二种宏函数在下面这种情况下就会报错if(Add(a,b));//替换后变成 if(a+b;) if里面有语句 语法报错//第三种情况由于优先级问题没法得到我们想要的结果int ret2=Add(a,b)*5//我们想要的结果是(1+2) 但替换后是ret2=1+2*5结果不符//第四种情况可以解决第三种乘除问题 但是解决不了逻辑运算符问题int x=10,y=20;int ret3=Add(a|x,b&y);//替换后就是ret3=(1|10+2&20) 加法优先级更高先执行加法 不符合我们的预期return 0;
}
从上面这个例子我们可以看出来实现一个宏函数要考虑方方面面,如果考虑不周就会出现上面那几种错误一样有很多的坑,导致我们的程序各种报错。那我们为什么还要使用宏呢?
因为宏有很多优点比如:
1. 宏的本质就是替换所以宏可以简化重复性代码的编写,提高效率。
2.宏在预处理阶段就展开,不占用运行时的开销比如上面的宏函数是不用创建栈帧的,执行效率较高。
3.宏还支持条件编译,#ifdef和#endif 可以通过宏启用或禁用调试输出
宏也有很多缺点:
- 宏在预处理阶段直接文本替换,可能导致意料之外的副作用(如多次计算参数)
- 缺乏类型检查,容易引发难以调试的错误。展开后的代码可能变得冗长,增加编译后文件体积(代码膨胀)。
C++祖师爷本贾尼想着说能不能搞出一个既能自动检查类型,调试,又能像宏函数一样展开减少运行上的消耗(即不用开辟空间直接替换)。为了解决这些问题内联函数应运而生!
1.2内联函数
1.2.1内联函数的概念
内联函数(Inline Function)是一种通过编译器优化机制减少函数调用开销的技术。在函数声明前添加关键字inline,建议编译器将函数体直接插入调用处,避免传统函数调用的压栈、跳转和返回等操作。
1.2.2内联函数的特性
接着上面的代码,我们使用内联看看与宏有什么本质区别:
#include<iostream>
using namespace std;
inline int Add(int x, int y)
{int ret = x + y;return ret;
}
int main()
{int a=1,b=2;int ret=Add(a,b);return 0;
}
我们通过转到反汇编代码调试起来让大家看看:
这时候有就会有人问?内联函数不是没有达到宏替换的效果吗?而且还有栈帧的消耗。这反而是内联函数支持调试的体现,我们想让内联函数像宏一样展开只需设置一下我们的vs编译器就好了。
下面我们来看看,展开后的内联函数:
- 从上面我们知道vs编译器默认情况下是不展开的,这是为了方便调试如果像让他展开就要开启上面的设置。
- 使用
inline函数
内联函数,可以有效的减少运行时的开销不需要再建立栈帧,只是像宏一样替换,提高了效率。
内联函数既然这么好那么能不能在所有的情况都是用内联呢?那当然是不行的,下面举个例子来直观感受一下:
还是这段代码,我们稍作改动:
#include<iostream>
using namespace std;
inline int Add(int x, int y)
{int ret = x + y;ret+=2;ret+=2;ret+=2;ret+=2;ret+=2;ret+=2;ret+=2;return ret;
}
int main()
{int a=1,b=2;int ret=Add(a,b);return 0;
}
> 当我们不断的增加内联函数里边的代码行数时,展开的指令就不断增加!直到增加到8行此时我们调试代码转到反汇编的时候发现内联函数又不展开了,乖乖去call地址创建栈帧了,这是为什么呢?
因此在使用内联函数的时候,选择适合小型、频繁调用的函数使用内联,尤其是涉及类成员函数或模板的情况。
- 此外还有一点要注意的是:内联函数不建议声明和定义分离,因为如果内联函数展开那么就会找不到定义导致链接错误! 来看下面的例子:
//在F.h文件中定义一个普通函数声明
void func1(int x);
//在F.cpp文件中定义
void func1(int x)
{cout << x << endl;
}//void func2(int x);
#include<iostream>
using namespace std;
//在test.cpp中使用
int main()
{func1(10);return 0;
}
运行上面的代码结果会顺利的打印出来,这是因为func1函数创建了栈帧有函数地址在链接的时候就会去找地址所以即使分离在不同文件但它们可以互相链接所以没有问题,接下来看看内联函数:
//这次我们在F.h文件中声明内联函数
inline void func2(int x);
//在F.cpp文件中实现
inline void func2(int x)
{cout<<x<<endl;
}
#include<iostream>
using namespace std;
int main()
{func2(10);return 0;
}
这时运行代码我们就可以看到是链接错误了,为什么会报链接错误呢?我们来对比一下两段代码:
1.3内联函数与宏的对比总结
1.3.1代码示例对比
内联函数:
inline int add(int a, int b) { return a + b; }
宏:
#define ADD(a, b) ((a) + (b))
特性 | 内联函数 | 宏 |
---|---|---|
处理阶段 | 编译时 | 预处理时 |
类型检查 | 支持 | 不支持 |
调试支持 | 完整 | 不可用 |
作用域 | 遵守函数作用域 | 全局替换 |
适用场景 | 高性能小函数 | 文本替换、条件编译 |
以上就是关于内联函数的介绍了,下面来看最后一个小语法nullptr。
二,nullptr
C++创造nullptr就为解决传统的NULL问题,举个例子:
#include<iostream>
using namespace std;
void f(int x)
{cout << "f(int x)" << endl;
}
void f(int* ptr)
{cout << "f(int* ptr)" << endl;
}
int main()
{f(0);f(NULL);//f((int*)NULL);// f((void*)NULL);f(nullptr);return 0;
}
我们想通过null来调用形参为指针的的函数,但是null被默认转化成了数值0从而调用了第一个传值的函数,这明显与我们的初衷相违背,就算我们将他强制类型转化也还会报错。所以C++就创造了nullptr:它可以转换成任意其他类型的指针类型。
总结:
nullptr
:用于表示空指针,替代传统的 NULL 或 0。它的主要作用是提供类型安全的空指针表示,避免传统方式可能导致的类型混淆问题。
以上就是本章的全部内容啦!
最后感谢能够看到这里的读者,如果我的文章能够帮到你那我甚是荣幸,文章有任何问题都欢迎指出!制作不易还望给一个免费的三连,你们的支持就是我最大的动力!