C++——初识(2)

一、引言

由于时间关系,上次我们只讨论了C++对C语言的部分改进。今天,让我们继续深入了解这位编程伙伴的其他特性。


二、inline

在学习inline之前,让我们先来回顾一下C语言里面的宏函数,因为这可是C++的祖师爷为了补宏函数的坑设计的。

下面我们来通过代码看看宏函数的坑和正确写法:

cpp 复制代码
#include<iostream>
using namespace std;

// 实现⼀个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))
// 为什么不能加分号? 
// 为什么要加外⾯的括号? 
// 为什么要加⾥⾯的括号? 
int main()
{
    int ret = ADD(1, 2);
    cout << ADD(1, 2) << endl;
    cout << ADD(1, 2)*5 << endl;
    int x = 1, y = 2;
    ADD(x & y, x | y);  // -> (x&y+x|y)

    return 0;
}

思考一下我提的三个为什么,平常宏替换是将函数名替换成后面的内容,第一如果加了分号,那cout <<ADD(1,2)<<endl; 不就等于cout <<ADD(1,2);<<endl;了吗;第二个如果不加外面外面的括号,那么大家看看ADD(1,2)*5,宏替换后是否还满足我们所需要的结果;第三个我们不加里面的括号,ADD(x&y,x|y)是否就变成了x&(y+x)|y,因为加法的运算符等级更高。

让我们仔细思考这三个问题:

  1. 如果宏定义末尾加了分号,那么cout << ADD(1,2) << endl;展开后就变成了cout << ADD(1,2); << endl;,这显然会导致语法错误。

  2. 如果不给宏定义加外层括号,例如ADD(1,2)*5展开后就会变成1+2*5,由于运算符优先级问题,结果将不符合预期。

  3. 如果宏参数不加括号,比如ADD(x&y,x|y)展开后会变成x&y + x|y,由于加法运算符优先级高于位运算,实际运算顺序会变成x&(y + x)|y,这将导致计算结果错误。

所以C++在设计的时候就发明了inline去避免宏函数造成的这么坑。

inline:

• 使用inline修饰的函数称为内联函数。在编译时,C++编译器会将内联函数在调用处直接展开,从而避免函数调用时的栈帧开销,提升执行效率。

inline关键字对编译器而言仅是建议性指令,编译器有权决定是否真正展开。不同编译器对内联展开的处理策略各异,因为C++标准未对此作强制规定。内联机制最适合短小且频繁调用的函数,而递归函数或较长的函数即使声明为内联也通常会被编译器忽略。

• C语言通过宏函数实现类似功能(预处理阶段展开),但宏函数编写复杂、易出错且难以调试。C++引入内联函数正是为了取代C的宏函数方案。

• 内联函数不建议将声明与定义分离到不同文件,否则会导致链接错误。由于内联展开后函数地址消失,链接阶段将无法正确解析引用。

额外告诉大家一个看到内联函数是否展开的小妙招哦:

cpp 复制代码
#include<iostream>
using namespace std;

inline int Add(int x, int y)
{
    int ret = x + y;
    ret += 1;
    ret += 1;
    ret += 1;
    return ret;
}
int main()
{
    // 可以通过汇编观察程序是否展开
    // 有call Add语句就是没有展开,没有就是展开了
    int ret = Add(1, 2);
    cout << Add(1, 2) * 5 << endl;

    return 0;
}

三、nullptr

NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:

cpp 复制代码
#ifndef NULL
    #ifdef __cplusplus
        #define NULL    
    #else
        #define NULL    
    #endif
#endif

• 在C++中,NULL可能被定义为字面量0,或在C中被定义为无类型指针(void*)的常量。无论采用哪种定义,使用空指针时都可能出现问题。例如,调用f(NULL)本意是想调用指针版本的f(int*),但由于NULL被定义为0,实际调用了f(int),导致与预期不符。而f((void*)NULL)调用则会报错。

• C++11引入的nullptr是一个特殊关键字,属于特定类型的字面量。它可以隐式转换为任意指针类型,但不会转换为整数类型。使用nullptr定义空指针能有效避免类型转换问题,确保代码意图的准确表达

cpp 复制代码
#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被定义成0,调⽤了f(int x),因此与程序的初衷相悖。
 
    f(NULL);    //结果为f(int x),与f(0)一样

    //f((int*)NULL);
    // 编译报错:error C2665: "f": 2 个重载中没有⼀个可以转换所有参数类型
 
    // f((void*)NULL);

    f(nullptr);

    return 0;
}

四、总结

总的来说,C++ 这两个新特性,其实都是在给 C 语言的老问题 "打补丁"------ 既保留了 C 语言的高效,又把那些容易踩的坑给填上了。

先说说 inline 内联函数,它就是宏函数的 "升级版"。C 语言的宏函数看着好用,不用函数调用的开销,但写的时候得小心翼翼加各种括号,稍不注意就因为运算符优先级搞出 bug,还没法调试。而 inline 就省心多了,写起来和普通函数一样,编译器会帮我们在调用的地方直接展开,既快又安全,还能支持类型检查和重载。不过也别随便用,要是函数体太长或者有递归,编译器根本不会给你内联,反而白忙活一场。

再看 nullptr,它就是来解决 NULL 的尴尬的。C 语言里 NULL 本质上就是 0,在 C++ 里写函数重载的时候,想传个空指针调用指针版本的函数,结果 NULL 被当成整数,直接调用了 int 版本的,完全违背初衷。而 nullptr 就专门用来表示空指针,只能传给指针类型,再也不会搞混,代码意图也更清晰。现在写 C++ 代码,建议直接把 NULL 换成 nullptr,尤其是用重载和智能指针的时候,能少踩很多坑。

如果大家在 inline 或者 nullptr 上遇到具体问题,比如不知道怎么判断内联有没有生效,或者在老项目里替换 NULL 怕出问题,欢迎在评论区一起讨论~ 编程就是这样,把细节里的坑都踩明白,技术才能慢慢提升,咱们继续在 C++ 的世界里探索吧!其实从宏函数到 inline从 NULLnullptr,能看出来 C++ 的设计思路 ------ 不是要把 C 语言推翻重来,而是在它的基础上,让我们写代码的时候更省心、更安全。

相关推荐
ECT-OS-JiuHuaShan2 小时前
麻烦是第一推动力,不厌其烦就是负熵流
开发语言·人工智能·数学建模·学习方法·量子计算
丶乘风破浪丶2 小时前
Vue项目中判断相同请求的实现方案:从原理到实战
前端·javascript·vue.js
why技术2 小时前
如果让我站在科技从业者的角度去回看 2025 年,让我选一个词出来形容它,我会选择“vibe coding”这个词。
前端·后端·程序员
worxfr2 小时前
CSS Flexbox 布局完全指南
前端·css
0思必得02 小时前
[Web自动化] JS基础语法与数据类型
前端·javascript·自动化·html·web自动化
Hy行者勇哥2 小时前
JavaScript性能优化实战:从入门到精通
开发语言·javascript·性能优化
Dreamcatcher_AC2 小时前
前端面试高频问题解析
前端·css·html
Kiyra2 小时前
八股篇(1):LocalThread、CAS和AQS
java·开发语言·spring boot·后端·中间件·性能优化·rocketmq
damo王2 小时前
how to install npm in ubuntu24.04?
前端·npm·node.js