CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
C++ 语言入门/课时/指针与引用
RawBlame
课时目录12 节

C++ 语言入门

  1. 01C++ 是什么
  2. 02基础语法
  3. 03指针与引用
  4. 04面向对象
  5. 05内存与 RAII
  6. 06STL 容器与算法
  7. 07模板基础
  8. 08现代 C++ 特性
  9. 09多线程
  10. 10构建与依赖
  11. 11调试与测试
  12. 12综合练习

指针与引用

pointers、references、raw vs smart 概念预览。

35 分钟

指针与引用

这一课你将能做到

  1. 用 & 取地址、用 * 解引用,说出指针变量本身存的是什么
  2. 写出指针传参与引用传参两种「被调函数改实参」的写法,并知道何时选哪种
  3. 用 nullptr 判空防段错误,解释 C++ 引用与 Java 引用的关键区别

为什么要学

Java 里你天天写 User u = new User(),从来不想 u 底下是什么——JVM 替你管了。C++ 没有这层遮罩:指针就是「内存地址」这个原始概念本身,你既可以拿它做高效的事(链表、零拷贝传参、和判题沙箱里的系统调用交互),也可能用它造出段错误。理解指针引用不是怀旧,是看懂一切 C++ 代码(包括 STL 内部实现)的前提。

对 Java 背景的你最反直觉的一点:Java 的「引用」更接近 C++ 的指针(可重指向、可为 null),而 C++ 的引用更像「对象的别名」,一旦绑定不可更改、永不独立存在。把这两个词的语义对齐,后面所有课程都顺。

先建立图像

内存就像一排带门牌号的储物柜:

地址    内容
0x1000  int score = 87        ← 变量 score 住在 0x1000
0x2000  0x1000                ← 指针 p 住在 0x2000,里面存的是 0x1000

int* p = &score;
   ↑      ↑
 类型    取地址运算符:拿到 score 的门牌号
 "指向 int 的指针"

*p        ← 解引用:顺着门牌号找到柜子里的东西(87)

引用则是另一回事:

int& r = score;   // r 不是新柜子,只是 score 的第二个名字

跟着做

步骤 1:取地址与解引用

新建 pointers.cpp:

#include <iostream>

int main() {
    int score = 87;
    int* p = &score;          // p 存的是 score 的地址

    std::cout << "score 的值:   " << score << "\n";
    std::cout << "score 的地址: " << &score << "\n";
    std::cout << "p 存的地址:   " << p << "\n";
    std::cout << "p 指向的值:   " << *p << "\n";

    *p = 95;                  // 通过指针修改 score
    std::cout << "改后 score:   " << score << "\n";   // 95
    return 0;
}
g++ -std=c++17 pointers.cpp -o pointers && ./pointers

输出(地址每次运行可能不同):

score 的值:   87
score 的地址: 0x7ffd8c2a
p 存的地址:   0x7ffd8c2a
p 指向的值:   87
改后 score:   95

步骤 2:空指针与安全检查

指针可以「不指向任何东西」:

#include <iostream>

int* findScore(int* scores, int n, int target) {
    for (int i = 0; i < n; ++i) {
        if (scores[i] == target) return &scores[i];
    }
    return nullptr;           // 没找到,返回空指针。C++11 前写 NULL 或 0
}

int main() {
    int scores[] = {60, 87, 95};
    int* hit = findScore(scores, 3, 100);

    if (hit != nullptr) {     // 也可以写 if (hit)
        std::cout << "找到,值为 " << *hit << "\n";
    } else {
        std::cout << "未找到\n";
    }
    // 若不判空直接 *hit,就是对空地址解引用 → 段错误(Segmentation Fault)
    // 判题沙箱里用户程序崩溃,最常见的死法就是解引用空指针/野指针
    return 0;
}

步骤 3:引用——对象的别名

#include <iostream>

int main() {
    int score = 87;
    int& r = score;           // r 与 score 是同一块内存的两个名字

    r = 100;                  // 改 r 就是改 score
    std::cout << score << "\n";   // 100
    std::cout << &r << " == " << &score << "\n";   // 地址相同!

    // 引用三条铁律:
    // 1. 声明时必须初始化(没有「空引用」)
    // 2. 绑定后不能改指别的对象(r = x 是赋值给 score,不是重新绑定)
    // 3. 永远不会为 null
    return 0;
}

步骤 4:传参实战——swap 的三种写法

#include <iostream>

void swapBroken(int a, int b) {       // 值传递:复制副本,改不动外面
    int t = a; a = b; b = t;
}

void swapByPointer(int* a, int* b) {  // 指针传递:传地址,C 风格
    int t = *a; *a = *b; *b = t;
}

void swapByRef(int& a, int& b) {      // 引用传递:C++ 首选
    int t = a; a = b; b = t;
}

int main() {
    int x = 1, y = 2;
    swapBroken(x, y);
    std::cout << x << " " << y << "\n";        // 还是 1 2
    swapByRef(x, y);
    std::cout << x << " " << y << "\n";        // 2 1
    swapByPointer(&x, &y);
    std::cout << x << " " << y << "\n";        // 1 2 → 又换回来了
    return 0;
}

调用体验上引用完胜:swapByRef(x, y) 不用取地址,语法干净;但指针能表达「可能没有对象」(nullptr),引用不能。参数一定有对象 → 引用;可能缺席 → 指针(现代 C++ 更多用 std::optional,见现代特性课)。

步骤 5:指针的经典战场——链表节点

判题沙箱维护「等待评测的提交队列」这类结构时,链表思想处处可见:

#include <iostream>

struct Submission {           // struct:默认 public 的类,先当「数据包」用
    int id;
    Submission* next;         // 指向下一个节点
    Submission(int i) : id(i), next(nullptr) {}
};

int main() {
    Submission a(101), b(102), c(103);
    a.next = &b;              // 手工串成链表:a → b → c
    b.next = &c;

    // 遍历:p 走到 nullptr 为止
    for (Submission* p = &a; p != nullptr; p = p->next) {
        std::cout << "提交 " << p->id << "\n";
    }
    // p->next 是 (*p).next 的语法糖:解引用再取成员
    return 0;
}

注意这里节点都在栈上、指针只做「连线」,不涉及堆内存分配——所以没有泄漏风险。一旦 new 出堆对象,内存管理规则就变了,那是第 5 课的主题。

步骤 6:const 与指针的组合

int score = 87;
const int* p1 = &score;    // 指向常量的指针:*p1 不可写,p1 可改指
int* const p2 = &score;    // 常量指针:p2 不可改指,*p2 可写
const int* const p3 = &score;  // 都不可改

void print(const int& x) { /* 只读参数,既快又安全 */ }

口诀:const 在 * 左边管「值」,在 * 右边管「指针」。

讲清楚要点

  1. 指针是变量,引用是别名:指针有自己的内存(8 字节)、可空、可重指、可做加减运算;引用无独立内存语义、不可空、绑定后不可改。函数参数默认优先引用。
  2. 段错误 = 访问了不该访问的内存:解引用空指针、野指针(指向已释放内存)、数组越界,都是 UB。判题沙箱捕获 SIGSEGV 信号就能把用户程序的崩溃转成 RE(Runtime Error)结果。
  3. & 和 * 是一对逆运算:&x 从对象到地址,*p 从地址回对象。声明里的 *、& 是类型的一部分(int* p),表达式里的 *、& 是运算符——同一符号两种身份,初学常混。
  4. -> 是「解引用 + 取成员」的糖:p->id 等价 (*p).id。对象用 .,对象指针用 ->,看符号就知道你手里是对象还是指针。
  5. 数组名会退化为指针:传数组进函数时 int arr[] 实际是 int*,长度信息丢失,所以 C 风格接口必须额外传 n。现代 C++ 用 std::vector/std::array 自带长度,能少一半这类 bug。

你可能会懵的地方

  • Java 的引用和 C++ 的引用是一回事吗? 不是。Java 引用本质是「受控指针」:可空、可重指、GC 管回收,但不能做地址运算。C++ 引用是对象别名,不可空、不可重指。Java 引用的灵活度更接近 C++ 指针(但被砍掉了指针运算和地址暴露)。

  • 什么时候必须用指针,引用替代不了? 三种场景:需要「可空」语义;需要改指针本身(如链表头插法 insert(Node** head));需要指针运算遍历内存。其余场景引用更安全。

  • int* p 还是 int *p? 语义完全相同,风格之争。int* p, q; 里 q 只是 int 不是指针,为避免误导,很多团队规范一行只声明一个变量。

  • 野指针和空指针什么区别? 空指针是「明确不指向」,判空即可防;野指针指向的内存已被释放或从未初始化,看起来正常用起来爆。防野指针三板斧:声明时就初始化(给 nullptr)、释放后立刻置空、优先用智能指针(第 5 课)。

  • 指针变量本身占多大内存? 与目标平台有关:64 位程序 8 字节、32 位程序 4 字节,与它指向什么类型无关。sizeof(int*)、sizeof(double*) 都是 8(64 位平台)。

错法对照

错法问题正解
int* p; *p = 5;野指针,写到随机地址,程序可能崩声明即初始化:int* p = nullptr; 或指向真实对象
int& r;引用必须初始化,编译不过int& r = score; 绑定时就给对象
解引用前不判空空指针解引用 → 段错误if (p) { ... } 先判空
return &localVar;返回局部变量地址,函数返回后内存失效返回值、引用参数回填,或返回堆/静态对象
用完 delete 后继续用指针悬垂指针(野指针的一种)delete 后置 nullptr,或直接用 unique_ptr

卡点急救

  1. 程序一运行就 Segmentation Fault:九成是空/野指针解引用。编译加 -g,用 gdb ./程序 跑,崩溃后 bt 看调用栈直接定位行号(第 11 课详解)。
  2. 改了形参外面没变:用了值传递。想改外面就传 T&;只读大对象传 const T&。
  3. 打印指针输出乱码地址而不是值:cout << p 打的是地址,要看值写 cout << *p;打印字符串指针时 cout 会自动输出内容,这是特例。

面试常问

  • 指针和引用的区别? 引用必须初始化且不可重绑、无空引用、sizeof 是目标对象大小;指针可空、可重指、可运算、自身占 8 字节。引用更安全,指针表达能力更强。底层实现上引用通常也是指针,但语言层面把它当别名。

  • 什么是野指针,如何避免? 指向无效内存的指针:未初始化、delete 后未置空、返回局部变量地址都会产生。避免手段:声明即初始化、delete 后置 nullptr、不返回局部对象地址、用智能指针接管生命周期。

  • const int* p、int* const p、const int* const p 分别什么含义? 第一个:指向常量的指针,值不可改、指向可改;第二个:常量指针,指向不可改、值可改;第三个:两者都不可改。记忆法:const 在 * 左修饰所指之物,在 * 右修饰指针本身。

自测三问

  1. p->id 等价于哪段写法?
  2. C++ 引用为什么不能表达「可能没有对象」?替代方案是什么?
  3. 数组传参时 int arr[] 实际退化成什么类型?

小结一句话

指针是存地址的变量,引用是对象的别名;参数默认用引用、可空语义用指针(或 optional)、链表和地址运算是指针的主场——掌握这对概念,C++ 内存模型的大门就开了。

延伸

  • cppreference:指针声明
  • cppreference:引用声明
  • 下一课:面向对象——类、构造析构与 RAII。
加载本节练习…
← 上一课基础语法下一课 →面向对象