欢迎光临
我们一直在努力

C++ 基础入门篇(三):内联函数与空指针

文章目录

  • 一. 内联函数
    • 1.1 内联函数的定义
    • 1.2 宏函数的缺陷与问题
    • 1.3 内联函数的使用
    • 1.4 内联函数的底层细节
    • 1.5 内联函数的优缺点
      • 1.5.1 内联展开的问题
      • 1.5.2 声明与定义分离
  • 二. nullptr(c++11新增内容)
    • 2.1 为什么需要 nullptr ?
    • 2.2 nullptr 是什么 ?
    • 2.3 重载解析中的表现

在这里插入图片描述

一. 内联函数

1.1 内联函数的定义

用 inline 修饰的函数叫做内联函数,编译时 C++ 编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率。 内联函数的出现在一定程度上取代了 C 语言的宏函数。

1.2 宏函数的缺陷与问题

宏函数(带参数的宏)本质上是预处理阶段的文本替换,而不是语言层面的函数。这一根本特性决定了它有一系列难以回避的缺陷。 1. 直接文本替换

宏函数编译采用直接文本替换,缺乏类型检查,类型不安全。

例如:

// 实现一个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))

这里我们需要解答关于宏的一些问题

为什么宏函数不能加分号? 为什么要有外括号? 为什么要有内括号?

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

#define MAX(a,b) ((a)>(b)?(a):(b));
if (cond)
x = MAX(a,b);
else
x = 0;

//展开后:
/*if (cond)
x = ((a)>(b)?(a):(b));;
else
x = 0;
*/

  • 外括号指的是把整个宏体包起来它的作用是:保证整个宏展开结果作为一个整体参与外部表达式,不被外部运算符拆开。

#define ADD(a,b) a+b
int r = 2 * ADD(1,2);

//展开后:
//int r = 2 * 1 + 2;

结果是 4,而不是期望的 6。因为 * 优先级高于 +,宏体被拆开了。

//加上外括号:
#define ADD(a,b) ((a)+(b))
int r = 2 * ADD(1,2);

//展开:
int r = 2 * ((1)+(2));

结果就是 6。所以外括号保护的是:宏整体与外部运算符之间的优先级关系。

  • 内括号指的是把每个参数在宏体内部都包起来:#define MUL(a,b) ((a)*(b)),这里的 (a) 和 (b) 就是内括号。它的作用是:保证实参表达式作为一个整体参与宏体内部的运算。

#define MUL(a,b) (a*b)
int r = MUL(1+2, 3);

//展开:
int r = (1+2*3);

结果是 7,而不是期望的 9。因为 2*3 先算了

//加上内括号:
#define MUL(a,b) ((a)*(b))
int r = MUL(1+2, 3);

//展开:
int r = ((1+2)*(3));

加上内括号结果才是 9。 所以内括号保护的是:实参表达式与宏体内部运算符之间的优先级关系。

括号只能解决优先级和结合性问题,不能解决宏函数的根本缺陷:参数可能被多次求值,例如 MAX(i++, j);没有类型检查;调试困难;命名污染;代码膨胀。

所以现代 C/C++ 中,能不用函数式宏就不用。优先考虑:C:static inline 函数;C++:inline 函数、constexpr 函数、模板、lambda。宏只保留给那些语言机制无法替代的场景,比如条件编译、字符串化、记号拼接等。

1.3 内联函数的使用

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

inline Add(int x,int y)
{
int a = b + c ;
return z ;
}

在 VS 编译器的 Debug 版本下默认不展开 inline,这样方便调试。若 Debug 版本想展开,需要设置以下两个地方。 在项目中右键单击项目,进入项目属性: 在这里插入图片描述 C/C++ -> 调试信息格式 -> 程序数据库(/Zi)

在这里插入图片描述 C/C++ -> 优化 -> 内联函数扩展 -> 只适用于_inline(/Ob1)

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

1.4 内联函数的底层细节

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

1.5 内联函数的优缺点

1.5.1 内联展开的问题

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

展开能提升效率,但展开不是绝对好。编译器并不会对 inline 言听计从。设想一个极端场景:某内联函数有 100 条语句,项目中有 10000 个调用点。若每条语句编译后平均 6 字节,全部展开将增加约 100 万条指令、约 6 MB 机器码,对安装包和指令缓存都是灾难。但现实中,编译器几乎不会内联这样的函数:成本模型会因函数体过大、调用点过多而拒绝。inline 只是提示,不是命令。真正能造成这种膨胀的,通常是 __forceinline、always_inline 等强制手段。合理使用下,只对短小热函数内联,缺点可以小到可忽略。编译器会帮助你合理使用内联,但前提是:你给它决策空间,而不是替它做决定。

1.5.2 声明与定义分离

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

内联函数不能像普通函数那样进行跨翻译单元的声明与定义分离。普通函数只需在头文件中声明,定义可以放在某个 .cpp 中,由链接器解析符号;但内联函数需要在调用点展开函数体,编译器在编译每个翻译单元时都必须看到完整定义。C++ 标准也明确要求:内联函数在每个使用它的翻译单元中都必须有定义。因此,内联函数通常直接定义在头文件中,而不是只在头文件里放声明、在 .cpp 里放定义。若声明和定义写在同一个翻译单元内,语法上可以分开;但定义必须对使用它的翻译单元可见。类内定义的成员函数隐式内联,天然满足这一点。C 语言中则常用 static inline 把头文件内联函数定义为内部链接,以避免链接和 ODR 问题。

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

nullptr 是 C++11 引入的空指针常量,用来替代 C/C++ 中长期使用的 NULL 和字面量 0。它的目标很明确:让“空指针”在类型系统里有自己独立的身份,而不是伪装成整数。

2.1 为什么需要 nullptr ?

在 C++11 之前,表示空指针通常有两种方式:

int* p = NULL;
int* q = 0;

但 NULL 本质上是一个宏,在 C++ 中通常被定义为 0 或 0L。这意味着它首先是一个整数,只是恰好能隐式转换成指针。这会带来重载解析问题:

void f(int);
void f(void*);

f(NULL); // 可能调用 f(int),也可能歧义,取决于 NULL 的定义
f(0); // 一定调用 f(int)

程序员想表达“空指针”,编译器却可能理解成“整数 0”。nullptr 解决了这个问题

f(nullptr); // 明确调用 f(void*)

2.2 nullptr 是什么 ?

nullptr 是一个关键字,类型是 std::nullptr_t,定义在 <cstddef> 中。

#include <cstddef>
std::nullptr_t n = nullptr;

它的关键特性:

  • 是空指针常量,不是整数;

  • 可以隐式转换为任意指针类型;

  • 可以隐式转换为任意成员指针类型;

  • 可以转换为 bool,值为 false;

  • 不能隐式转换为整数类型。

例如:

int* p = nullptr; // OK
double* q = nullptr; // OK
void (*fp)() = nullptr; // OK
int A::*mp = nullptr; // OK,成员指针

bool b = nullptr; // OK,b == false
int x = nullptr; // 错误:不能把 nullptr 当整数

2.3 重载解析中的表现

这是 nullptr 最直观的价值:

void f(int);
void f(void*);

f(0); // 调用 f(int)
f(NULL); // 可能调用 f(int),也可能歧义
f(nullptr); // 调用 f(void*)

因为 nullptr 不能转成 int,但可以转成 void*,所以它天然偏向指针重载。

总结:nullptr 不是指针,而是空指针常量,类型为 std::nullptr_t。它可以隐式转换为任意指针和成员指针,转换为 bool 时为 false,但不能转换为整数。它的出现让空指针在重载解析、模板推导和类型安全上都有了明确语义,是 C++11 中替代 NULL 和 0 的标准做法。写现代 C++ 时,表示空指针,就用 nullptr。

内联函数与空指针,一个谈编译期的展开与取舍,一个谈运行时的空值边界。它们看似不相干,却都在提醒我们:C++ 的效率与安全,从来不是自动获得的,而是建立在对底层语义的理解之上。至此,这两个话题告一段落。

接下来,我们将走进类和对象。那是 C++ 封装、抽象与多态的起点,也是 this 指针、构造与析构、拷贝与移动等新问题的舞台。内联与空指针不会消失,它们会以成员函数、对象生命周期等新的形式再次出现。让我们继续。

在这里插入图片描述

赞(0)
未经允许不得转载:171主机测评 » C++ 基础入门篇(三):内联函数与空指针
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址