⚡【C++要笑着学】(4) 内联函数:inline 关键字 | 内联函数特性 | 改版后的 auto 关键字 | 范围 for 的用法 | 指针空值 nullptr

  ​​​🔥 订阅量破千的火热 C++ 教程
👉 火速订阅
《C++要笑着学》 

 🔥 CSDN 累计订阅量破千的火爆 C/C++ 教程的 2023 重制版,C 语言入门到实践的精品级趣味教程。
了解更多: 👉 "不太正经" 的专栏介绍 试读第一章
订阅链接: 🔗《C语言趣味教程》 ← 猛戳订阅!

💭 写在前面:大家好,我是 亦优叶子。本文将讲解内联函数的概念、范围 for 循环和指针空值 nullptr。我们将逐一解释这些概念,讨论它们的用途和优缺点,并提供使用建议和注意事项。希望读者通过本章的学习,能够更好地掌握 C++ 编程的基础知识。

📜 本章目录:

Ⅰ.  内联函数(Inline Function)

0x00 引入:栈帧消耗问题的优化

0x01 内联函数的概念

0x02 内联函数的特性 —— 空间换时间

0x03 什么函数建议成为内联函数?

0x03 编译器角度下的 inline

0x04 inline 声明与定义不建议分离

0x03 浅谈宏的优缺点和替代方法

Ⅱ.  auto关键字(C++11)

0x00 改版前的auto

0x01 改版后的auto

0x02 auto 的使用场景

0x03 使用auto的注意事项

Ⅲ. 范围 for(C++11新增)

0x00 引入:语法糖

0x01 范围 for 的用法

0x02 范围 for 的使用条件

Ⅳ.  指针空值 nullptr

0x00 引入:C 中的 NULL

0x01 nullptr 关键字

0x01 引入 nullptr 的原因


Ⅰ.  内联函数(Inline Function)

0x00 引入:栈帧消耗问题的优化

 我们知道,调用函数需要建立栈帧,栈帧中要保留一些寄存器,结束后又要恢复。

❓ 这就可以看出这些都是有消耗的,对于频繁调用的小函数,有没有方法可以优化呢?

💬 代码演示:比如下面这个两数相加的函数:

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

我们可以尝试写一个两数相加的宏:

#include<iostream>
using namespace std;

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

// 写一个两数相加的宏
#define ADD(X, Y) ((X) + (Y))

int main(void)
{
    cout << "函数: " << Add(1, 2) << endl;
    cout << "宏: " << ADD(1, 2) << endl;
    // 写宏的技巧:记住宏原理是替换,你替换一下看看对不对
    // cout << "M: " << ((1) + (2)) << endl;

    cout << "宏: " << 10 * ADD(3, 4) << endl;
    return 0;
}

🚩 运行结果如下:

这是在 C 语言里学到的知识,但是 C++ 中有什么更好的解决方法吗?

 本章我们就来学习传说中的 —— 内联函数 (Inline Funciton) 。

0x01 内联函数的概念

 宏有时候用起来似乎比较复杂,也容易出错。

所以 C++ 新增了内联函数,我们可以使用内联函数来解决上述问题。

📚 概念: inline 修饰的函数叫做内联函数

inline 数据类型 [函数名]

💬 代码演示:内联函数

#include <iostream>
using namespace std;

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

int main(void)
{
    cout << "内联函数: " << Add(1, 2) << endl;

    return 0;
}

我们在 Add 函数前面增加了 inline表示 "建议" 将 Add 函数设置为内联函数。

 注意我这里用了 "建议",为什么是 "建议" 呢?我们继续往下看!

0x02 内联函数的特性 —— 空间换时间

"内联函数是一场空间换时间的交易..."

  内联函数的特性:以空间换时间,省去了调用函数的开销。

编译时 C++ 编译器会在调用内联函数的地方展开,是没有函数压栈的开销的。

为什么 inline 要叫 "内联" 函数呢?内联 —— 即 "内部关联",所以它会在编译的时候展开。

内部关联展开后,代码就会很长,我们举个例子。

💭 例子:假设有 10 行代码

inline void func() {
    // 假设有10行代码
}
  • 如果不展开:我们假设有 1000 个调用,编译后台就会有 10 + 1000 条指令。
  • 如果展开:编译后台合计会有 10 * 1000 条指令。

 因此,这是一场以空间换取时间的交易!

因为节省了函数压栈的开销,所以能提高程序运行的效率。

0x03 什么函数建议成为内联函数?

inline 既然是空间换时间,所以如果是 "轻量级" 的小函数,比如一个简单的 Add 相加函数。

我们就可以考虑将它标上 inline,向编译器举荐将 Add 设置为内联函数,以提高效率。

 但是,如果一个函数的代码很长,比如循环或递归等函数,就不适宜成为内联函数。

0x03 编译器角度下的 inline

inline 对于编译器而言 只是一个建议,编译器会自动优化。

也就是说,我们使用 inline 关键字,仅仅是向编译器举荐而已!具体同不同意要看编译器。

如果定义为 inline 函数的函数体内有循环或递归等等,编译器优化时会忽略掉内联。

所以,并不是你给函数标了 inline 想让它成为内联函数,它就一定能成为内联函数的。

0x04 inline 声明与定义不建议分离

inline 申明和定义不建议分离!内联函数会认为在调用的地方展开,导致不生成地址。

test.cpp -> test.o  符号表中不会生成 Func 函数的地址,

因为 inline 函数是不需要地址的 (都在调用的地方展开了还要个毛线地址) ,我们来验证一下:

💬 Add.h:

#include <iostream>
using namespace std;

inline int Func(int x, int y);

💬 Add.cpp:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Add.h"

int Func(int x, int y) {
	return x + y;
}

💬 test.cpp:

#include "Add.h"

int main(void) 
{
	Func(1, 2);

	return 0;
}

🚩 运行结果如下:

因此,不建议将 inline 申明和定义分离。

0x03 浅谈宏的优缺点和替代方法

❓ 宏的优缺点是什么?

优点:

① 宏可以增强代码的复用性

② 宏有助于提高性能

缺点:

① 宏调试起来很不方便(因为宏在程序预编译阶段进行替换)。

② 宏的大量使用可能会导致代码的可读性差,可维护性差,容易误用。

③ 宏没有类型安全的检查。

❓ 有哪些技术可以替代宏?

① 常量定义:换用 const

② 函数定义:换用内联函数

Ⅱ.  auto关键字(C++11)

0x00 改版前的auto

改版前的 auto 指的是在早期 C/C++ 中 auto 关键字的含义。

📚 旧的含义:使用 auto 修饰的变量,是具有自动存储器的局部变量。

 遗憾的是,大家都懒得去用它。这是为什么呢?

auto int a = 0;   // 表示a是一个自动存储类型,会在函数结束后自动销毁。

当使用 auto 修饰后,表示 a 是一个自动存储类型,它会在函数结束以后自动销毁。

但是因为后来 C 把标准给改了,不加是自动销毁:

int a = 0;   // 标准改了之后,不加也是自动销毁。

 这么一来,这个 auto 关键字就没有意义了,因为都是自动销毁。

auto:这就尴尬了,我的存在没有意义了,用和不用都一样。

0x01 改版后的auto

C++标准委员会觉得这 auto 也太尴尬了,我们得给它来一波加强。

为了缓解 auto 的尴尬,C++ 标准委员会把 auto 原来的功能给废弃了。

并赋予了 auto 全新的含义!

📚 游戏更新补丁(bushi):auto 现在不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器。auto 声明的变量必须由编译器在编译时推导而得。

💬 也就是说,它可以自动推导出数据的类型:

int a = 0;
auto c = a;  // C++11给auto关键字赋予了新的意义:自动推导c的类型

你的右边是什么,它就会推导出相应的类型

任何类型都可以实现,包括但不限于:

auto ch = 'A';
auto e = 10.11;
auto pa = &a;

为了方便测试,我们来打印一下对象的类型看看:

#include<iostream>
using namespace std;

int main()
{
    int a = 0;
    auto c = a;  // 自动推导c的类型

    auto ch = 'A';
    auto e = 10.11;
    auto pa = &a;

    // typeid - 打印对象的类型
    cout << typeid(c).name() << endl;   // i
    cout << typeid(ch).name() << endl;  // c
    cout << typeid(e).name() << endl;   // d
    cout << typeid(pa).name() << endl;  // Pi

    return 0;
}

🚩 运行结果如下:

 emmm... 确实

 这时候可能有人会觉得,这一波操作好像也没啥意义啊,

直接写数据类型不香吗?  int c = a;

我们继续往下看~

0x02 auto 的使用场景

处理又臭又长的数据类型 

💬 遇到这种场景,就能体会到 auto 的香了:

#include <iostream>
#include <map>

int main(void) 
{
    std::map<std::string, std::string> dict = {{"sort", "排序"}, {"insert", "插入"}};
    std::map<std::string, std::string>::iterator it = dict.begin();
    // 这个类型又臭又长,写起来太麻烦了。。。
    
    auto it = dict.begin();   // 可以改成这样,爽!
    // ↑ 根据右边的返回值去自动推导it的类型,写起来就方便多了

    return 0;
}

像遇到这种又臭又长的类型,而且还要经常使用,

这时候使用 auto 帮你自动推到类型,就很爽了!

auto 与指针结合起来使用:

📚 auto 非常聪明,它在推导的时候其实是非常灵活的:

int main(void)
{
    int x = 10;
    auto a = &x;  // int*
    auto* b = &x; // int*
    auto& c = x;  // int

    return 0;
}

在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型!

否则编译器将会报错,因为编译器实际只对第一个类型进行推导,

然后用推导出来的类型定义其他变量。

auto a = 1, b = 2;
auto c = 3, d = 4.0;  ❌ 该行代码会编译失败,因为c和d的初始化表达式类型不同

0x03 使用auto的注意事项

 注意!

使用 auto 是必须要给值的!

int i = 0;
auto j;  ❌

auto j = i;  必须给值!!

这就意味着,auto 是不能做参数的!

auto 不能作为函数的参数

void TestAuto(auto a); ❌

此处代码编译失败,auto 不能作为形参类型,因为编译器无法对a的类型进行推导!

auto 不能直接用来声明数组

 auto b[3] = {4,5,6};   ❌

📌 为了避免与 C++98 中的 auto 发生混淆,C++11 只保留了 auto 作为类型指示符的用法。

 auto 在实际中最常见的优势用法就是 C++11 提供的新式 for 循环,

还有 lambda 表达式等进行配合使用。我们可以继续往下看~

Ⅲ. 范围 for(C++11新增)

0x00 引入:语法糖

📚 范围 for,即 —— 基于范围的 for 循环。

 范围 for 可以说是一颗 "语法糖" ,什么是语法糖?

"语法糖就是用起来会让人觉得很甜,很爽的东西"

以前,我们要遍历一个数组,一般会按照以下方式进行:

int main()
{
    int arr[] = { 1, 2,3,4,5 };
    int sz = sizeof(arr) / sizeof(arr[0]);  // 计算数组大小

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

    return 0;
}

 对于一个有范围的集合而言,让程序员来说明循环的范围是多余的,

有时候还会容易犯错误……

 因此,C++11 中引入了基于范围的 for 循环,语法如下:

 for ( auto 变量名 : 数组)

for 循环后的括号由冒号分为两部分:

  • 第一部分:范围内用于迭代的变量
  • 第二部分:表示被迭代的范围

0x01 范围 for 的用法

💬 代码演示:适用范围 for 遍历数组

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

    for (auto e : arr) {
        printf("%d ", e);
    }
    printf("\n");

    return 0;
}

🚩 运行结果如下:

💬 代码演示:试着使用范围 for,把数组中的每个值 +1   (1 2 3 4 5 → 2 3 4 5 6)

#include<iostream>
using namespace std;

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

    // ++
    for (auto& e : arr) {
        e++;
    }

    // 打印
    for (auto e : arr) {
        cout << e << " ";
    }
    cout << endl;

    return 0;
}

🚩 运行结果如下:

🔑 解读:使用引用来对数组中的每个值进行修改。

(这里的 & 不是取地址,是 "引用" 。如果你不知道,可以看完下一章,再回来看)

📌 注意事项:

和普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环。

0x02 范围 for 的使用条件

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

对于数组而言,就是数组中第一个元素和最后一个元素的范围。

对于类而言,应该提供 beginend 的方法,begin end 就是 for 循环迭代的范围。

❌ 错误演示:下面的代码就是 for 循环范围不确定!

void TestFor(int arr[]) {    // 传递过来的是首元素地址
    for (auto& e : arr) {    // 无法确定范围
        cout << e << endl;
    }
}

这里传递过来的是数组的首元素地址,并不是数组,它会不知道范围是多少,所以会 报错

不仅如此,并且迭代的对象要实现 ++ == 的操作

(关于迭代器这个问题,后期会讲,现在了解一下留个印象即可)

Ⅳ.  指针空值 nullptr

0x00 引入:C 中的 NULL

在良好的 C/C++ 变成习惯中,声明一个变量时最好给该变量一个合适的初始值,

否则可能会出现不可预料的错误,比如未初始化的指针。

如果一个指针没有合法的指向,我们就需要手动给它置为空。

在之前的 C 语言教程里,我们都是用 NULL 来解决的:

#include<iostream>
using namespace std;

int main(void)
{
    // C++ 98/03
    int* p1 = NULL;
    int* p2 = 0;

    return 0;
}

🔑 这里的 NULL 其实是一个宏,而 C++ 空指针推荐使用 nullptr 来处理。

0x01 nullptr 关键字

 这是 C++11 新增了 nullptr 关键字,专门用来表示空 NULL:

int* p3 = nullptr;

有了 nullptr 以后,我们就不再推荐使用 NULL 了,因为 nullptr 更专业更安全!

📌 注意事项:

① 使用 nullptr 表示指针空值时,因为它是关键字,所以不需要包含头文件。

② 为了 提高代码的健壮性,在后续表示指针空值的时建议最好使用 nullptr 

③ C++11 中,sizeof( nullptr ) 与 sizeof( (void*)0 ) 所占的字节数相同 (4/8) :

sizeof(nullptr) == sizeof((void*)0) 

0x01 引入 nullptr 的原因

正如之前所说,NULL 其实是一个宏。

我们打开传统的 C 头文件 (stddef.h) 中可以看到如下代码:

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

可以看到,NULL可能被定义为字面常量 0,或者被定义为无类型指针 (void*) 的常量。

不论采取何种定义,在使用空值和指针时,都不可避免地会遇到一些麻烦,比如:

void Func(int)
{
	cout << "Func(int)" << endl;
}
void Func(int*)
{
	cout << "Func(int*)" << endl;
}
int main()
{
	Func(0);
	Func(NULL);
	Func((int*)NULL);

	return 0;
}

该程序的本意是想通过 Func(NULL) 调用指针版本的 Func(int*) 函数,

但是由于 NULL 被定义成0,这么一来就不符合程序的初衷了。

在 C++98 中,字面常量 0 既可以是一个整型数字,也可以是无类型的指针 (void*) 常量,

但是编译器默认情况下会将其看成一个整型常量,

如果要将其按照指针方式来使用,必须对其进行强制类型转换 (void*)0 。

 后来 C++11 引入了指针空值 nullptr 就缓解了这一尴尬现象……

🔺 总结:nullptr 其实就是 0,所以有了 C++11之后,就不再推荐大家使用 NULL 了。

📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   2022.2.23 | 2023.9.20(重制)
❌ [ 勘误 ]   Zoffan:内联函数"空间/时间"的逻辑关系写错了【已修正】
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. C++[EB/OL]. 2021[2021.8.31]. .

程序员面试宝典[M]. 5. .

  • 84
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 27
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平渊道人

喜欢的话可以支持下我的付费专栏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值