跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C++

C++ 入门:引用、内联函数与 C++11 新特性详解

C++ 引用为变量别名,不占用额外内存;内联函数在编译期展开以减少调用开销但可能增加体积;C++11 引入 auto 进行类型推导简化声明,范围 for 循环优化遍历语法,nullptr 提供类型安全的空指针表示。涵盖引用特性、内联机制、auto 使用细则及 nullptr 优势,对比传值与传引用效率,解析宏与内联函数区别,并提供相关面试题解答。

FrontendX发布于 2026/3/28更新于 2026/4/264 浏览
C++ 入门:引用、内联函数与 C++11 新特性详解

C++ 入门:引用、内联函数与 C++11 新特性详解

一、引用

1.1 引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。比如:李逵,在家称为**"铁牛",江湖上人称"黑旋风"**

void TestRef() {
    int a = 10;
    int& ra = a; // 定义引用类型
    printf("%p\n", &a);
    printf("%p\n", &ra);
}

**注意:**引用类型必须和引用实体是同种类型的。

1.2 引用的特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体
#include <iostream>
using namespace std;
int main() {
    int a = 10;
    // 1. 编译报错:'ra': 必须初始化引用
    // int& ra;
    
    // 2. 一个变量可以有多个引用
    int& b = a;
    int& d = a;
    
    // 3. 这里并非让 b 引用 c,因为 C++ 引用不能改变指向,这里是一个赋值
    int c = 20;
    b = c;
    return 0;
}

1.3 常引用

**概念:**const 引用就是常引用。

常引用场景:

const 引用(常引用)绑定临时变量时,会延长临时变量的生命周期,使其与 const 引用的生命周期一致,直到该引用(如 rii)的生命周期结束。

1.4 使用场景

引用做参数:

void Swap(int& left, int& right) {
    int temp = left;
    left = right;
    right = temp;
}

**输出型参数:**传参时不用担心值拷贝问题,因为形参和实参都指向同一实体。 **提高效率:**传递大对象时,只需传递对象的引用(而非拷贝整个对象),避免了大对象拷贝带来的性能开销,从而提高程序运行效率。

引用做返回值:

int& Add(int a, int b) {
    int c = a + b;
    return c;
}
int main() {
    int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1, 2) is :" << ret << endl;
    return 0;
}

上面代码有个很明显的问题:返回了局部变量的引用,局部变量出了函数就销毁了,这里返回的 ret 的值有两种可能性,如果函数结束栈帧销毁,但是没有清理栈帧,那么 ret 引用可能指向被引用的局部变量,如果栈帧被清理,那么 ret 指向的就是随机值了。

正确做法:

int& Count() {
    static int n = 0;
    n++;
    // ...
    return n;
}

返回个静态区的局部变量,静态区局部变量的生命周期是整个程序,所以无论用 int 接收还是用 int& 接收都没有任何问题。

  1. 接收返回引用的两种情况:
    • 用普通变量接收:会拷贝静态变量当前的值;
    • 用引用变量接收:这个引用会成为静态变量的别名。
  2. 返回引用的优势:
    • 避免对象拷贝(尤其对大对象),提升性能;
    • 可直接修改原始对象,支持链式操作。
  3. 返回引用的风险:
    • 若被引用对象(比如栈区局部变量)被销毁,会形成'悬空引用',此时访问或修改会引发崩溃、数据错乱等未定义行为。

1.5 传引用、传值效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。下面我用代码测试一下传引用和传值,让大家更直观感受效率差别。

测试引用做参数:

#include <time.h>
struct A {
    int a[10000];
};
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue() {
    A a;
    // 1. 以值作为函数参数
    size_t begin1 = clock();
    for (size_t i = 0; i < 1000000; ++i) TestFunc1(a);
    size_t end1 = clock();
    // 2. 以引用作为函数参数
    size_t begin2 = clock();
    for (size_t i = 0; i < 1000000; ++i) TestFunc2(a);
    size_t end2 = clock();
    cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
    cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}

编译器默认单位是毫秒,从运行结果来看,效率差距是很大的。

测试引用做返回值:

#include <time.h>
struct A {
    int a[10000];
};
A a;
// 值返回
A TestFunc1() {
    return a;
}
// 引用返回
A& TestFunc2() {
    return a;
}
void TestReturnByRefOrValue() {
    // 1. 以值作为函数的返回值类型
    size_t begin1 = clock();
    for (size_t i = 0; i < 100000; ++i) TestFunc1();
    size_t end1 = clock();
    // 2. 以引用作为函数的返回值类型
    size_t begin2 = clock();
    for (size_t i = 0; i < 100000; ++i) TestFunc2();
    size_t end2 = clock();
    cout << "TestFunc1 time:" << end1 - begin1 << endl;
    cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main() {
    TestReturnByRefOrValue();
    return 0;
}

1.6 指针和引用的区别

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

int main() {
    int a = 10;
    int& ra = a;
    cout << "&a = " << &a << endl;
    cout << "&ra = " << &ra << endl;
    return 0;
}

在底层实现上实际是有空间的,因为引用可能是按照指针方式来实现的(不同编译器的实现细节肯定不一样)。vs2022 下指针和引用的底层对比:

从汇编代码来看,底层是差不多的,但是也不能一概而论,毕竟每个编译器的实现不一样。

【面试题】:引用和指针的不同点
  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
  2. 引用在定义时必须初始化,指针没有要求。
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
  4. 没有 NULL 引用,但有 NULL 指针。
  5. 在 sizeof 中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占 4 个字节)。
  6. 引用自加即引用的实体增加 1,指针自加即指针向后偏移一个类型的大小。
  7. 有多级指针,但是没有多级引用。
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
  9. 引用比指针使用起来相对更安全。

二、内联函数

2.1 内联函数是啥?

以inline 修饰的函数叫做内联函数,编译时C++ 编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

2.2 如何判断是否为内联函数?

在 release 模式下,查看编译器生成的汇编代码中是否存在 call Add。 在 debug 模式下,需要对编译器进行设置,否则不会展开(因为 debug 模式下,编译器默认不会对代码进行优化)。

  1. 右键项目,点击属性。
  2. 展开 C/C++,把常规选项中的调试信息格式化改成程序数据库。
  3. 优化中的内联函数拓展修改成只适用于_inline(/Ob1)。

2.3 内联函数特性

核心:以空间换时间 —— 编译阶段用函数体替换调用,优势是减少调用开销、提升效率;缺陷是可能增大目标文件。

编译器的建议性:inline 是建议而非强制,编译器通常仅处理规模小、非递归、频繁调用的函数(如《C++ Prime》所述),大函数(如 75 行)或递归函数会被忽略。

声明定义不分离:分开写会导致链接错误(跨源文件调用时更明显),因为内联函数无独立地址。

内部链接属性:不同源文件可定义同名内联函数,不会引发链接冲突。

替换时机:编译阶段完成。

【问题】: 为啥内联函数可能会导致目标文件变大

内联函数在编译时将函数体复制到每个调用处,若函数被多次调用,代码体积会显著增加。

【问题】:递归不能内联的核心原因

内联需编译时确定展开次数,而递归调用层次由运行时动态决定(依赖输入或状态),编译器无法预知,故无法安全展开。

【面试题】:宏的优缺点?
  • 优点:预处理阶段展开,无函数调用开销;可定义常量或复杂逻辑。
  • 缺点:无类型检查;易产生副作用;调试困难;代码膨胀。
【面试题】:内联函数的优缺点?
  • 优点:消除函数调用开销;编译器可进行更多优化。
  • 缺点:可能增加代码体积;过度使用影响编译速度;无法像普通函数那样隐藏实现细节。

三、auto 关键字 (C++11)

3.1 auto 简介

在 C++11 中,auto 关键字被赋予了全新的含义 —— 作为编译期类型推导指示符。它不再表示'自动存储期的变量',而是让编译器根据变量的初始化表达式,自动推导出变量的实际类型。这一特性极大简化了复杂类型的声明,尤其在处理冗长的 STL 容器迭代器、模板类型等场景时,能显著提升代码的简洁性和可读性。

使用 auto 的核心要求是变量必须初始化,因为编译器需要通过初始化表达式才能完成类型推导。例如:

int a = 10;
auto b = a; // 编译器推导出 b 的类型为 int
auto c = 'c'; // 推导出 c 的类型为 char

3.2 auto 的使用细则

3.2.1 auto 与指针和引用结合起来使用

声明指针类型时,auto 和 auto* 的效果完全一致,均会推导出指针类型:

int x = 20;
auto* p1 = &x; // p1 推导为 int*
auto p2 = &x; // p2 同样推导为 int*

声明引用类型时,必须显式添加 &,否则 auto 会推导出被引用对象的类型而非引用:

int y = 30;
auto& r = y; // r 推导为 int&(y 的引用)
auto r2 = y; // r2 推导为 int(y 的值拷贝)
3.2.2 在同一行定义多个变量

当在同一行使用 auto 声明多个变量时,所有变量必须能被推导为相同类型,否则会编译报错。这是因为 auto 仅能推导出一种类型,无法同时适配多种不同类型:

auto a = 10, b = 20; // 正确,a 和 b 均推导为 int
// auto c = 10, d = 3.14; // 错误,c 推导为 int,d 推导为 double,类型不一致

3.3 auto 不能推导的场景

3.3.1 auto 不能作为函数的参数

编译器无法在编译期根据函数调用情况推导出参数的实际类型,因此 auto 不能用于函数形参的声明:

// 编译失败:auto 不能作为函数参数类型
void func(auto param) { // ... }
3.3.2 auto 不能直接用来声明数组

auto 无法推导出数组类型,因此不能直接用于数组的声明。若需简化数组相关的类型声明,可结合指针或引用间接实现:

int arr[] = {1, 2, 3};
// auto arr2[] = {4, 5, 6}; // 错误,auto 不能直接声明数组
auto* p = arr; // 正确,p 推导为 int*(指向数组首元素)

四、基于范围的 for 循环 (C++11)

4.1 范围 for 的语法

在 C++98 中遍历数组需手动控制循环范围,如:

void TestFor() {
    int array[] = {1, 2, 3, 4, 5};
    for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
        array[i] *= 2;
    for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
        cout << *p << endl;
}

而 C++11 的范围 for 循环语法简洁,由冒号':'分为迭代变量和被迭代范围两部分,示例:

void TestFor() {
    int array[] = {1, 2, 3, 4, 5};
    for (auto& e : array)
        e *= 2;
    for (auto e : array)
        cout << e << " ";
    return 0;
}

它支持 continue 结束本次循环、break 跳出整个循环,与普通循环逻辑一致。

4.2 范围 for 的使用条件

4.2.1 for 循环迭代的范围必须是确定的

对于数组,范围是数组第一个元素到最后一个元素;对于类,需提供 begin 和 end 方法来界定迭代范围。如下代码因范围不确定会出问题:

void TestFor(int array[]) {
    for (auto& e : array)
        cout << e << endl;
}
4.2.2 迭代的对象要实现 ++ 和 == 的操作

迭代过程依赖这些操作来控制迭代逻辑(此部分涉及迭代器知识,后续会详细讲解,现阶段了解即可)。

五、指针空值 nullptr (C++11)

问题 1:NULL 的问题?

NULL 本质是宏,在传统 C 头文件 stddef.h 中定义如下:

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

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

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

这里 f(0) 调用 f(int) 没问题,但 f(NULL) 由于 NULL 定义的模糊性(既像 0 又像指针),可能导致编译器匹配混乱,而 f((int)NULL)* 虽然明确转化为指针类型调用 f(int*),但这种写法不够简洁直观。

问题 2:为啥引入 nullptr?

无需额外头文件:

nullptr 是 C++11 引入的新关键字,专门表示指针空值。使用它时,无需包含额外头文件,代码简洁性提升。

字节数特性:

在 C++11 中,sizeof(nullptr) 与 sizeof((void)0)* 所占字节数相同。这意味着 nullptr 在内存占用等底层特性上,和传统表示空指针的方式在字节层面有对应关系。

提升代码健壮性:

相比 NULL 可能带来的歧义,nullptr 明确表示指针空值。在函数重载等场景下,能让编译器准确匹配函数,减少错误发生概率,使代码更健壮。例如之前的 f 函数调用,使用 nullptr 就很明确。

总结来说,nullptr 作为 C++11 的新特性,解决了 C++98 中 NULL 表示指针空值的一些弊端,让指针空值的表达更清晰、准确,有助于写出更可靠的代码。

问题 3:nullptr 类型?

nullptr 的类型是 std::nullptr_t,它可以隐式转换为任何指针类型(包括对象指针、函数指针等),但不能转换为整数类型(这一点和 NULL 不同,NULL 可能被解析为整数 0)。

目录

  1. C++ 入门:引用、内联函数与 C++11 新特性详解
  2. 一、引用
  3. 1.1 引用概念
  4. 1.2 引用的特性
  5. 1.3 常引用
  6. 1.4 使用场景
  7. 1.5 传引用、传值效率比较
  8. 1.6 指针和引用的区别
  9. 【面试题】:引用和指针的不同点
  10. 二、内联函数
  11. 2.1 内联函数是啥?
  12. 2.2 如何判断是否为内联函数?
  13. 2.3 内联函数特性
  14. 【问题】: 为啥内联函数可能会导致目标文件变大
  15. 【问题】:递归不能内联的核心原因
  16. 【面试题】:宏的优缺点?
  17. 【面试题】:内联函数的优缺点?
  18. 三、auto 关键字 (C++11)
  19. 3.1 auto 简介
  20. 3.2 auto 的使用细则
  21. 3.2.1 auto 与指针和引用结合起来使用
  22. 3.2.2 在同一行定义多个变量
  23. 3.3 auto 不能推导的场景
  24. 3.3.1 auto 不能作为函数的参数
  25. 3.3.2 auto 不能直接用来声明数组
  26. 四、基于范围的 for 循环 (C++11)
  27. 4.1 范围 for 的语法
  28. 4.2 范围 for 的使用条件
  29. 4.2.1 for 循环迭代的范围必须是确定的
  30. 4.2.2 迭代的对象要实现 ++ 和 == 的操作
  31. 五、指针空值 nullptr (C++11)
  32. 问题 1:NULL 的问题?
  33. 问题 2:为啥引入 nullptr?
  34. 问题 3:nullptr 类型?
  • 💰 8折买阿里云服务器限时8折了解详情
  • 💰 8折买阿里云服务器限时8折购买
  • 🦞 5分钟部署阿里云小龙虾了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 双指针算法解决三数之和与四数之和问题
  • SpringAI 接入 DeepSeek 大模型示例
  • Webgal 自定义动画编写指南
  • PyModbus 安装与配置指南:Python 工业通信协议实现
  • Python 打包 exe 实现软件授权码与注册机方案
  • Python 2026 年发展局势:AI 时代的通用基础设施语言
  • 基于 ESP32-C5 的 Moji 2.0 AI 桌面机器人技术解析
  • SPI 通信读取 255 故障排查:C++ spidev0.0 原理分析
  • C++ 基础语法与核心编程知识点总结
  • 苹果 macOS 26 Tahoe 内存占用过大问题及解决方案
  • Linux Shell 简易实现:原理、设计与实践
  • Generative UI 如何重塑 AI 时代的前端交互
  • MiniMax 海螺 AI 视频:图片与文本生成高质量视频
  • Docker 项目部署实战:后端、前端与数据库配置
  • 微软 Edge Webview2 v144 升级导致 SAP GUI 白屏故障及解决方案
  • OpenClaw 飞书机器人配置指南
  • JavaScript 原子读和写操作详解
  • 强化学习:策略梯度定理与 REINFORCE 算法
  • Visual C++ 运行库修复工具使用指南
  • 利用 AI 工具快速构建电商系统核心模块(商品、购物车、订单)

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online