C++ 基础入门篇(二):引用

文章目录

  • [一. 引用](#一. 引用)
    • [1.1 引用的概念和定义](#1.1 引用的概念和定义)
    • [1.2 引用的使用方法](#1.2 引用的使用方法)
    • [1.3 取别名的方式](#1.3 取别名的方式)
    • [1.4 引用的应用场景](#1.4 引用的应用场景)
      • [1.4.1 引用传参](#1.4.1 引用传参)
      • [1.4.2 传引用返回](#1.4.2 传引用返回)
      • [1.4.3 补充](#1.4.3 补充)
    • [1.5 引用的特性](#1.5 引用的特性)
      • [1.5.1 不开辟空间](#1.5.1 不开辟空间)
      • [1.5.2 引用在定义时必须初始化](#1.5.2 引用在定义时必须初始化)
      • [1.5.3 引用的专一性](#1.5.3 引用的专一性)
  • [二. const引用](#二. const引用)
    • [2.1 概念补充](#2.1 概念补充)
      • [2.1.1 权限](#2.1.1 权限)
      • [2.1.2 权限的放大与缩小](#2.1.2 权限的放大与缩小)
      • [2.1.3 临时对象和中间变量](#2.1.3 临时对象和中间变量)
      • [2.1.4 常量与常化](#2.1.4 常量与常化)
    • [2.2 const引用的使用方式](#2.2 const引用的使用方式)
      • [2.2.1 从常见的问题开始](#2.2.1 从常见的问题开始)
      • [2.2.2 const引用与常量](#2.2.2 const引用与常量)
      • [2.2.3 const与临时对象](#2.2.3 const与临时对象)
    • [2.3 const引用的应用价值](#2.3 const引用的应用价值)
  • 三.指针和引用的区别

一. 引用

1.1 引用的概念和定义

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一快内存空间,例如在生活中我们会给身边的同学起一个外号,水浒传中李逵外号黑旋风,林冲外号豹子头。 所以引用就是取别名

1.2 引用的使用方法

类型& 引⽤别名 = 引⽤对象;

C++中为了避免引入太多的运算符,会复用C语言的一些符号,比如前面的<<和>>,这里引用也和取地址使用了同一个符号&

例子:

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

int main()
{
	int a = 0;
	// 引⽤:b是a的别名
	int& b = a;

	//这⾥取地址我们看到是⼀样的
	cout << &a << endl;
	cout << &b << endl;
	return 0;
}

1.3 取别名的方式

1. 一个变量可以有多个别名

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

int main()
{
	int a = 0;
	// 引⽤:b和c是a的别名
	int& b = a;
	int& c = a;
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;

	//这⾥取地址我们看到是⼀样的
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	b++;
	
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;

	return 0;
}

此时a变量所指向的整型空间有2个别名。由于别名都指向同一块空间,改变别名就可以改变原值,对b++,同时会让a,b,c同时改变。

2. 别名的别名

可以给别名取别名。

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

int main()
{
	int a = 0;
	// 引⽤:b和c是a的别名
	int& b = a;
	int& c = a;
	//也可以给别名b取别名,d相当于还是a的别名
	int& d = b;
	
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
	cout << d << endl;

	//这⾥取地址我们看到是⼀样的
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	cout << &d << endl;
	b++;

	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
	cout << d << endl;
	return 0;
}

此时:b是a的别名,d是b的别名,d相当于a的别名。

1.4 引用的应用场景

引用在实践中主要是用于 引用传参 引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象。

1.4.1 引用传参

cpp 复制代码
//指针:
void inc(int *p) {
    (*p)++;
}

int main() {
    int x = 1;
    inc(&x);   // 要取地址
    // x == 2
}
//指针传参:
void swap(int* x, int* y) {
    int t = *x;
    *x = *y;
    *y = t;
}

int main() {
    int a = 5, b = 6;
    swap(&a, &b);   // 调用要取地址
}

//引用:
void inc(int &r) {
    r++;
}

int main() {
    int x = 1;
    inc(x);    // 直接传
    // x == 2
}

//引用传参
void swap(int& rx, int& ry) {
    int t = rx;
    rx = ry;
    ry = t;
}

int main() {
    int a = 5, b = 6;
    swap(a, b);     // 直接传变量
}
  • 指针传参

优点:灵活、可空、可重绑定、可做算术。调用处更明显。

缺点:取地址和解引用的繁琐步骤,使用不透明性------感受上并非操作变量本身,语法繁琐,容易空指针、野指针。安全性较低,需要手动检查。

  • 引用传参

优点:语法自然,安全,不能为空。避免拷贝,适合大对象和运算符重载。传递即取别名,直接操作别名不需要解引用,使用透明性------感受直接操作变量本身

缺点:仅 C++ 支持,不能为空,不能重绑定。调用处不显式,可能悬空。

总结:引用更安全简洁,适合大多数 C++ 修改实参和只读大对象的场景;指针更灵活可空,适合 C 接口、可空参数、重绑定和底层操作。两者不是简单的好坏关系,而是适用场景不同。

  • 另一种用法:与指针交替使用取代二级指针

C语言二级指针的写法:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef struct ListNode
{
    int val;
    struct ListNode* next;
} LTNode, *PNode;

void ListPushBack(LTNode** pphead, int x)
{
    assert(pphead);

    PNode newNode = (PNode)malloc(sizeof(LTNode));
    if (!newNode)
    {
        perror("malloc:fail");
        exit(1);
    }
    newNode->val = x;
    newNode->next = NULL;

    if (*pphead == NULL)
    {
        *pphead = newNode;
    }
    else
    {
        PNode tail = *pphead;
        while (tail->next)
        {
            tail = tail->next;
        }
        tail->next = newNode;
    }
}

int main()
{
    PNode list = NULL;
    ListPushBack(&list, 1); // 必须取地址
    ListPushBack(&list, 2);
    ListPushBack(&list, 3);
    return 0;
}

C语言二级指针的弊端:

1. 调用处必须取地址,容易忘

c 复制代码
ListPushBack(list, 1);    // 错,类型不匹配
ListPushBack(&list, 1);   // 对,必须取地址

问题在于:list 本身是 PNode,类型是 ListNode*,函数要的是 ListNode**,只有 &list 才是 ListNode**,新手最容易犯的错就是忘了 &,然后编译器报一堆类型错误,或者勉强通过但运行崩溃。

C++优化方法:直接对链表的指针引用取别名:修改链表指针的别名就是修改链表指针本身

cpp 复制代码
void ListPushBack(PNode& pHead, int x);

PNode list = nullptr;
ListPushBack(list, 1);   // 直接传,不需要 &

调用意图完全一样,但不需要取地址,也不会传错类型。

2. 函数内要解引用,语义不直观

c 复制代码
if (*pphead == NULL)
{
    *pphead = newNode;
}
else
{
    PNode tail = *pphead;
    while (tail->next)
    {
        tail = tail->next;
    }
    tail->next = newNode;
}

你要在脑子里做翻译:

C++ 引用版本:

cpp 复制代码
void ListPushBack(PNode& pHead, int x)
{
    if (pHead == nullptr)
    {
        pHead = newNode;
    }
    else
    {
        PNode tail = pHead;
        while (tail->next)
        {
            tail = tail->next;
        }
        tail->next = newNode;
    }
}

函数内 pHead 就是头指针本身,改 pHead 就是改外部 list,不需要 * ,不需要脑内翻译

3. assert(pphead) 是必须的

c 复制代码
void ListPushBack(LTNode** pphead, int x)
{
    assert(pphead);
    // ...
}

为什么必须断言?

pphead 是二级指针,它自己可能为 NULL,如果 pphead == NULL,那 * pphead 就是解引用空指针,直接崩溃所以必须先检查 pphead 本身

而引用版本:

cpp 复制代码
void ListPushBack(PNode& pHead, int x)

引用必须绑定有效对象,不存在"引用本身为空"的情况,所以不需要assert(&pHead)。当然,pHead 本身可以是 nullptr,但那是另一层语义,和"引用是否有效"无关。

4. 容易混淆"改指针"和"改节点"

假设你不小心写成:

cpp 复制代码
void ListPushBack(LTNode** pphead, int x)
{
    // ...
    pphead = newNode;   // 错误:只改了局部变量
}

这改的是函数内的 pphead 副本,外部的 list 完全没变。

正确写法必须是:

cpp 复制代码
*pphead = newNode;

再比如优先级问题:

c 复制代码
*pphead->next = newNode;    // 错误,等价于 *(pphead->next)
(*pphead)->next = newNode;  // 正确

二级指针下,*、->、\[\] 的优先级和结合性非常容易写错。

5. 类型签名不直观

c 复制代码
void ListPushBack(LTNode** pphead, int x);

看到 LTNode**,你需要想:哦,这是要修改 LTNode * 类型的变量,所以传的是 LTNode * 的地址,函数内用 * pphead 访问那个 LTNode*

而:

cpp 复制代码
void ListPushBack(PNode& pHead, int x);

一眼就能看出:pHead 是一个 PNode 的引用,函数会修改这个指针

1.4.2 传引用返回

引用返回值的场景相对比较复杂,我们在这里简单讲了一下场景,还有一些后续内容在类和对象章节中会继续深入讲解。

1. 引用返回的优势:

以一个栈为例,我们想要改变它的栈顶元素

当我们使用传值返回时:

为什么传值返回不能赋值?

因为STTop 函数的返回类型是 int(传值返回)时:函数内部计算 rs.ars.top 的值。根据C++规定,这个值会被拷贝出来,生成一个临时对象(纯右值,prvalue)。C++语法规定:+=、= 等赋值运算符的左边必须是一个左值(lvalue)。左值的定义是:有名字、有固定内存地址、可以被修改的实体。红框里的"临时对象"是右值(rvalue)------它没有名字,生命周期极短,只是一个临时的数据包。因此,编译器报错:+= 左边必须是左值。

当我们使用传引用返回时:

int& 表示函数返回的是一个引用(别名)。图中标注的 int& rx 是一个概念上的抽象,意思是:STTop(st1) 这个表达式的求值结果,就是 rs.ars.top - 1 的别名。因为它直接关联到栈内部的那个真实元素,所以它是一个左值(lvalue)。C++语法规定,赋值运算符(+=, = 等)左侧必须是左值。因此,STTop(st1) += 10; 完全合法。执行效果:编译器会将其翻译为 st1.ast1.top - 1 += 10;,直接修改了栈顶元素本身。

1.4.3 补充

1. 悬空引用:引用最初绑定了有效的内存,但那块内存后来被销毁了,导致引用指向了无效的内存。

cpp 复制代码
int& func()
{
    int a = 0;
    int& ra = a;
    return ra; // 返回时,a 被销毁,ra 变成了悬空引用
}

它和野指针的联系:
野指针:通常一运行就崩溃(段错误 Segmentation Fault),因为随机地址大概率是不可访问的。

悬空引用:非常隐蔽!那块栈内存虽然被回收了,但操作系统还没有把这块内存拿走(只是标记为可复用)。你访问它时,可能读到旧数据(看起来正常),也可能被接下来的函数调用覆盖(读到垃圾)。这会导致程序在离 Bug 很远的地方崩溃,极难排查。

1.5 引用的特性

1.5.1 不开辟空间

引用是不开辟空间的,只对现有的一块空间给他一个别名

1.5.2 引用在定义时必须初始化

由于引用不开辟空间的特性,与指针不同,引用必须初始化

1.5.3 引用的专一性

引用一旦引用一个实体,再不能引用其他任何实体

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

int main()
{
	int a = 10;
	// 编译报错:"ra": 必须初始化引⽤
	//int& ra;
	int& b = a;
	int c = 20;
	b = c;
	// 这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向,
 
	// 这⾥是⼀个赋值
 
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	return 0;
}

二. const引用

2.1 概念补充

在理解const引用之前,首先需要补充一些概念:

2.1.1 权限

非正式术语,指一个类成员基于其访问说明符所在类的继承关系以及访问上下文所拥有的可访问性 (accessibility)。它决定了程序的其他部分(如类的成员函数、派生类、友元或外部代码)是否能够访问该成员,以及访问时的范围例如,是完全无法访问、仅能读取,还是可以读取和修改

2.1.2 权限的放大与缩小

1. 权限放大:非正式术语,指在特定上下文通过特定机制扩大或提升了继承自基类的成员的可访问范围,使得原本访问受限的成员变得更容易被访问。(非法,编译报错)

简单理解就是:把"只读"的实体,交给"可读可写"的指针/引用去绑定。

cpp 复制代码
const int b = 20;    // b:只读
// int& rb = b;      //  报错:试图把只读的 b 交给可写引用(权限放大)
// int* pb = &b;     //  报错:权限放大

为什么危险? 如果允许这样,rb = 30; 就会修改一个本该只读的常量,彻底摧毁 const 的保护机制。

2. 权限缩小:非正式术语,指在特定上下文通过特定机制限制或降低了继承自基类的成员的可访问范围,使得原本可以访问的成员变得难以或无法被访问。(合法,安全降级)

简单理解就是:把"可读可写"的实体,交给"只读"的指针/引用去绑定。

cpp 复制代码
int a = 10;          // a:可读可写
const int& ra = a;   // ra:只读(权限缩小)
const int* pa = &a;  // pa:指向常量的指针(权限缩小)

// a = 20;           // 合法
// ra = 20;          // 报错:只读不可写

为什么安全? 你派了一个只读的保镖去管理房间,保镖不会破坏房间里的东西。

2.1.3 临时对象和中间变量

1. 临时对象

正式术语,这是 C++ 标准中的正式术语。临时对象是不具有名称的、生命周期短暂的对象,通常在表达式求值过程中为了保存中间结果而创建。在现代 C++ 中,它通常对应纯右值(prvalue)或将亡值(xvalue)。

临时对象产生场景:

1. 函数返回值(而不是引用)时。
2. 类型转换(如 double 转 int,或自定义类的隐式转换)。
3. 显式调用构造函数(如 std::string("hello"))。
4. 字面量(如 10, 3.14)。
5. 表达式计算结果(如 a + b)。

2. 中间变量

非正式术语,通常指在代码执行过程中,为了计算最终结果或完成一系列操作而创建的、具有名称的变量。它可以是用户显式声明的局部变量,也可以是编译器优化过程中引入的临时存储。

2.1.4 常量与常化

1. 常量

值在初始化后不能被修改的对象或表达式。

常量是语言层面认定的、不允许被修改的数据。它通常分为两类:

  • 字面量常量(天生):
    比如 10、3.14、"hello"。这些是代码里写死的,属于纯右值(prvalue),没有对应的内存地址让你去改。

  • 符号常量(被定义的):
    通过 const 或 constexpr 定义的变量。

cpp 复制代码
const int a = 10;      // a 是一个符号常量
constexpr int b = 20;  // b 是编译期常量

2. 常化:

常化,是指将一个原本具有"可读可写"权限的实体(变量、指针、引用),通过 const 修饰,降级为"只读"的过程。这完美契合我们之前讲的"权限只能缩小,不能放大"。

常化最典型的三个场景:

  • 变量常化:
cpp 复制代码
int a = 10;          // 可读可写
const int& ra = a;   // 常化过程:ra 是 a 的常引用(只读)
// ra = 20;          // 编译报错:通过 ra 无法修改 a
a = 20;              // 合法:a 本身没有被常化,依然是可写变量
  • 指针常化
cpp 复制代码
int x = 10;
int* p = &x;           // 可读可写
const int* cp = &x;    // 常化过程:指向常量的指针(不能通过 cp 改 x)
int* const pc = &x;    // 常化过程:常量指针(pc 不能指向别人,但能改 x)
  • 对象成员函数的常化(最容易被忽视的常化)
    在类中,成员函数末尾加 const,本质上是对 this 指针进行了常化(后面我们会学到这里做了解)
cpp 复制代码
class A {
    int val;
public:
    void setVal(int v) { val = v; }        // 普通函数:this 是 A*,可读可写
    int getVal() const { return val; }     // 常化函数:this 变成了 const A*,只读
};

2.2 const引用的使用方式

2.2.1 从常见的问题开始

cpp 复制代码
const int a = 10;
int& ra = a;

发生错误:error C2440: "初始化": 无法从"const int"转换为"int &"

这是经典的权限放大错误:const设置了a只能读不能写。所以不能用一个可以读可以写的别名来引用a,不然a权限就放大了。

解决方法:

  • 权限不可以放大:对于一个const限制的对象,需要一个const(相同权限)的别名来引用。
cpp 复制代码
const int a = 10;
const int& ra = a;
  • 权限可以缩小:别名ra的权限相对于a缩小了,但是a本身的权限不变,ra++会报错
cpp 复制代码
int a = 10;
const int& ra = a;
a++;
ra++;

2.2.2 const引用与常量

在 C++ 的学习中,"const 引用"和"常量"经常被放在一起讨论,导致初学者极易混淆。结合我们之前聊过的"常化"、"权限放大与缩小"以及"左值右值",我们可以用一句话切中要害:

常量(Constant):是一个实体。它天生或后天被设定为不可修改。

const 引用(Reference to const):是一个别名(视角)。它本身不是常量,只是它承诺不通过这个别名去修改绑定的对象。

准确来说,C++ 标准里并没有"常量引用"这个词,准确的叫法是"对常量的引用"(reference to const)。

下面我们通过三种绑定情况,彻底理清它们的关系。

1. const 引用绑定常量(权限一致,合法)

cpp 复制代码
const int a = 10;     // a 是常量(只读)
const int& ra = a;    //  合法:ra 是 a 的别名,权限同为只读
// ra = 20;           //  报错:不能通过 ra 修改 a

本质:引用绑定了常量,双方权限一致(只读)。

2. const 引用绑定普通变量(权限缩小,合法)

cpp 复制代码
int b = 20;           // b 是普通变量(可读可写)
const int& rb = b;    //  合法:将 b 的"可写"权限缩小为"只读",交给 rb
// rb = 30;           //  报错:不能通过 rb 修改 b
b = 30;               //  合法:b 本身没有被常化,依然可以改

本质:这就是我们之前讲的"常化"。你把一把可读可写的钥匙,降级成只读钥匙交给了保镖(rb)。b 本身并没有变成常量

3. 普通引用绑定常量(权限放大,非法)

cpp 复制代码
const int c = 30;     // c 是常量(只读)
// int& rc = c;       //  报错:试图把只读的 c 交给可写引用(权限放大)

本质:编译器绝对禁止你通过普通引用去修改一个常量。

2.2.3 const与临时对象

临时对象天生是右值(不可被修改)。普通引用无法绑定临时对象(权限放大);const 引用不仅能绑定临时对象,还能奇迹般地延长它的生命周期

1. 为什么普通引用不能绑定临时对象?(权限放大)

回顾之前的问题:"为什么 int& r = 10; 会报错?",因为 10 和传值返回产生的临时对象,都是纯右值(prvalue)。在 C++ 的语法设定中,右值是不具备"可写"属性的(你可以把它理解为天生的只读逻辑状态,虽然它在物理内存里)。普通引用 int&:权限是可读可写。临时对象(右值):权限是只读。如果你用 int& 去绑定临时对象,等于向编译器申请:"我要拿到这个临时对象的修改权"。这就构成了权限放大!编译器绝不允许你修改一个转瞬即逝的临时数据,所以直接报错。

2. const 引用做了什么?(权限缩小 + 生命周期延长)

当你写成 const int& r = 10; 或 const int& r = func(); 时,发生了两件极其伟大的事情:

1. 权限缩小(妥协)
你向编译器承诺:"我只读,不修改"。
const 引用把原本"可读可写"的权限,主动降级为"只读"。由于临时对象本来就是右值(只读),双方权限一致,权限放大被消除,编译通过。

2. 生命周期延长(魔杖)
这是 C++ 中最特殊的规定之一。正常情况下,临时对象在包含它的完整表达式结束时(也就是遇到分号时)就会销毁。

cpp 复制代码
cout << func() << endl; // func() 产生的临时对象在这一行的分号处销毁

但是,如果你用一个 const 引用来接住它:

cpp 复制代码
const int& r = func(); // 临时对象原本该在分号处销毁
// 现在,它的生命周期被延长了,活得和 r 一样久!

编译器会为这个临时对象在栈上分配一块空间(这就是之前图里画的"红框"),并让 r 绑定它,直到 r 离开作用域,它们才一起销毁。

2.3 const引用的应用价值

在 C++ 中,const 引用(const T&)在函数传参上的应用,可以说是现代 C++ 最经典、最常用的优化与安全设计。

结合我们之前讨论的"权限"、"临时对象"、"拷贝代价",我们可以用一句话概括它的核心价值:不拷贝(效率高)、不改源(安全)、左右通吃(兼容性极强)。

下面从四个维度,彻底展开它的应用价值。

1:避免拷贝,提升效率(对比传值传参)

如果参数是 std::string、std::vector 或自定义的大对象,传值传参(void func(Tval))会发生一次完整的拷贝构造。

cpp 复制代码
void badFunc(std::string s); // 传值:拷贝一份完整的字符串,代价极高
void goodFunc(const std::string& s); // const 引用:不拷贝,直接传别名

底层机制:const T& 传递的是对象的地址(底层实现是指针),无论对象多大,传参开销仅仅是一个指针的大小(通常 8 字节)。
性能提升:对于大对象,这能省去深拷贝(如字符串复制、动态数组分配)的巨大开销。

2:权限收缩,保证安全(对比普通引用传参)

如果使用普通引用(void func(T& val)),函数内部可以直接修改实参,这会破坏数据的安全性。

cpp 复制代码
void badFunc(int& val) {
    val = 100; // 函数内部把外部变量改了,调用者可能并不知情
}

void goodFunc(const int& val) {
    // val = 100; //  编译报错:权限收缩为只读,函数内部无法修改
}

防御性编程:const 引用向调用者承诺:"我只是借用看看,绝对不会改它"。

权限缩小:无论传入的是普通变量还是常量,const 引用都能安全绑定(因为权限只能缩小,不能放大)。

3:左右通吃,兼容临时对象(对比普通引用传参)

这是 const T& 最强大的特性,也是之前纠结"临时对象"和"权限放大"的最终答案。

普通引用只能绑定左值(普通变量),不能绑定右值(字面量、临时对象),否则会构成权限放大:

cpp 复制代码
void badFunc(int& val);
badFunc(10);          //  报错:10 是右值,普通引用权限放大
badFunc(a + b);       //  报错:a+b 是临时对象(右值)

const 引用则"左右通吃":

cpp 复制代码
void goodFunc(const int& val);
goodFunc(a);          //  传左值(普通变量)
goodFunc(10);         //  传字面量(右值)
goodFunc(a + b);      // 传临时对象(右值)

底层魔法:当 const 引用绑定右值时,编译器会悄悄为这个右值物化(Materialize)一个临时对象,并延长这个临时对象的生命周期,让它和 val 一样长久。

实战意义:你写 void print(const std::string& s),调用者不仅能传 std::string,还能直接传"hello"(字面量,触发隐式类型转换生成临时对象)。

4:支持隐式类型转换(传值的"平替")

如果函数参数是 std::string(传值),你传 "hello" 是可以的,因为 const char * 会隐式转换为 std::string。

但如果你用普通引用 std::string&,传 "hello" 会报错(因为转换会产生临时对象,普通引用无法绑定)。
只有 const std::string& 完美保留了这个能力:既能接收转换后的临时对象,又能避免拷贝。

三.指针和引用的区别

在 C++ 中,指针和引用是面试和工程中最常被拿来对比的两个概念。结合我们之前讨论的"临时对象"、"权限放大缩小"、"底层实现"和"链表二级指针",我们可以从语法、底层、权限、应用四个维度,彻底把它们的区别理清。

指针是实体(变量),保存的是地址;引用是别名(语法糖),是实体的另一个名字

下面我们逐一拆解:

1. 语法层面的八大核心区别

2. 底层实现层面的区别(编译器视角)

误区:很多初学者认为"引用不占内存"。

真相:在 C++ 标准层面,引用只是一个别名。但在底层汇编实现上,引用通常是用指针来实现的,也会占用一块指针大小的空间(存在栈或寄存器中)。但是,编译器会帮你做"自动解引用"。

指针:你要手动 * p 解引用,手动 &a 取地址。

引用:编译器底层虽然存的是地址,但在语法层面帮你自动屏蔽了 * 和 &。

唯一的例外(引用不开辟空间的场景):
当引用作为编译期常量被优化掉,或者作为函数参数直接被寄存器传递时,它可能真的不占用实际的栈内存。但这属于编译器优化的范畴,不需要你手动关心。

3. 权限与 const 结合的区别

这是 C++ 安全性的核心。

指针的 const 有三种形态:

  • const int * p:指向常量的指针(不能改 * p,但可以改 p 指向别人)。

  • int * const p:常量指针(可以改 * p,但不能改 p 的指向)

  • const int * const p:既不能改 * p,也不能改指向。

引用的 const 只有一种形态:

  • const int& r:指向常量的引用(不能通过 r 修改原对象)。

注意:int& const r 是语法错误!因为引用一旦初始化就不能重绑定(自带常量属性),再加 const 是多余的。

权限放大与缩小:

指针:const int * p = &a;(权限缩小,合法);int * p = &const_a;(权限放大,非法)。

引用:const int& r = a;(权限缩小,合法);int& r = const_a;(权限放大,非法)。

特例:const int& r = 10;(合法,延长临时对象生命周期);int& r = 10;(非法,权限放大)。

总结口诀:
指针是实体,引用是别名。
指针可空可换向,引用专一必绑定。
指针多级算术广,引用自动解引用。
底层实现皆指针,语法安全引用强。
传参优先常引用,修改指针用引用(指针的引用)。
动态内存用指针,返回局部勿引用。

选择原则:能用引用就用引用(更安全,语法更自然)。需要表达"空"或"重新绑定"时用指针(如动态内存、多态基类指针、可空参数)。需要修改指针本身时,用"指针的引用" T * &(替代二级指针 T * * )。

相关推荐
richard_yuu1 小时前
AOI 实战第六篇:.onnx 到 C++ 的最后一公里
c++·深度学习·yolo
wuminyu1 小时前
Kafka配置TLS/SSL加密传输时零拷贝失效分析
java·linux·c语言·jvm·c++
影视飓风TIM1 小时前
C++ 智能指针:auto_ptr / unique_ptr / shared_ptr / weak_ptr 原理与使用
开发语言·c++
一木 之林2 小时前
李沐《动手学深度学习》知识卡合集:从 Softmax 回归到房价实战(第24-50集)
开发语言·c++·人工智能
心中有你02142 小时前
Java Swing实现校园最短路径导航系统(Dijkstra算法完整源码+详细解析)
java·开发语言·算法
Alphapeople2 小时前
路径规划算法的python实现
开发语言·python·算法
重生之小比特2 小时前
【Java SE】数组的定义与使用
java·开发语言
旋生万物2 小时前
圈道裂痕——用 Python 验证素数分布的螺旋生成论判据
开发语言·python·数论·素数·cci·可计算数学
keyipatience2 小时前
5种IO模型与阻塞IO,select,poll,epoll,LT和ET模式
linux·服务器·网络·数据结构·c++·算法