跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

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

C++ 引用作为变量别名可避免拷贝提升效率,内联函数通过编译期展开减少调用开销,C++11 引入的 auto 关键字简化类型推导,基于范围的 for 循环优化遍历语法,nullptr 提供安全的指针空值表示。内容涵盖引用特性、内联机制及 C++11 核心新特性的详细解析与对比。

PhpPioneer发布于 2026/2/9更新于 2026/5/2724 浏览
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)的生命周期结束。

注意:是右值具有常性,不是临时变量具有常性,虽然 fun 函数返回的是局部的静态变量,但是由于是值返回,所以依旧返回的是临时变量。

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;
}

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

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

【面试题】:引用和指针的对比

引用概念上定义一个变量的别名,指针存储一个变量地址。

引用在定义时必须初始化,指针没有要求。

引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。

没有 NULL 引用,但有 NULL 指针。

在 sizeof 中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占 4 个字节)。

引用自加即引用的实体增加 1,指针自加即指针向后偏移一个类型的大小。

有多级指针,但是没有多级引用。

访问实体方式不同,指针需要显式解引用,引用编译器自己处理。

引用比指针使用起来相对更安全。

二、内联函数

2.1 内联函数是啥?

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

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

在 release 模式下,查看编译器生成的汇编代码中是否存在call Add

在 debug 模式下,需要对编译器进行设置,否则不会展开 (因为 debug 模式下,编译器默认不会对代码进行优化,以下给出 vs2022 的设置方式)

1、右键项目,点击属性

2、展开 C/C++,把常规选项中的调试信息格式化改成程序数据库

3、优化中的内联函数拓展修改成只适用于_inline(/Ob1)

2.3 内联函数特性

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

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

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

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

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

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

内联函数在编译时会将函数体复制到每个调用处,多次调用会导致代码膨胀。

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

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

【面试题】:宏的优缺点?

宏的优点增强代码复用性提升程序性能

宏的缺点调试困难(预编译阶段已完成替换)降低代码可读性、可维护性,易误用无类型安全检查

C++ 替代宏的技术常量定义:用 const/enum 替代短小函数定义:用内联函数替代

【面试题】:内联函数的优缺点?

优点减少函数调用开销(栈帧创建 / 销毁等),提升执行效率兼具类型安全、可调试性,易读易维护(优于宏函数)无需为函数分配独立内存空间,语法与普通函数一致

缺点以空间换时间,频繁调用会导致代码膨胀,增大目标文件 / 内存占用 inline 是编译器建议而非强制,大函数 / 递归函数会被忽略,无法实现内联声明与定义不可分离(跨文件分离会引发链接错误),灵活性受限

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

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

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

更多推荐文章

查看全部
  • 平衡二叉树判断:从 O(n²) 暴力递归到 O(n) 自底向上优化
  • 医疗 AI 场景下的 k-均值算法深度解析与实践
  • Clawdbot 源码部署实战:从环境搭建到 WebChat 验证
  • 二分查找实战:山峰数组峰顶索引与寻找峰值
  • 县域烟花禁燃监管 GIS 实践:Java 结合高德地图 API 盘点销售点
  • Bright Data AI Scraper Studio:自然语言自动生成企业级爬虫架构
  • JDK 主流版本现状与选型建议
  • 数字图像处理与 FPGA 实现:搭建算法与硬件思维的桥梁
  • Java 实现决策树算法:从原理到代码实战
  • 数据结构:二叉树与堆的 C 语言实现详解
  • Python AI 大模型部署指南:本地运行、API 服务与 Docker 封装
  • Python 基础语法完全指南:变量、类型与运算符
  • AI 大模型冲击下,职场人如何保持竞争力?
  • Xcode 原生集成 AI 大模型本地化配置指南
  • C++ 多线程同步:互斥锁(mutex)实战
  • C++ 哈希表全家桶:unordered_map/set 底层实现与位图布隆过滤器
  • Python 爬虫基础教程:请求、解析与数据存储
  • Python 打造 AI 三剑客:文档总结、代码生成与资料检索
  • Spring Boot 集成 MyBatis-Plus 数据库操作与完整 CRUD 示例
  • C++与Linux 文件操作:系统接口 open write read 详解

相关免费在线工具

  • 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