1. 指针基础:内存的导航系统

1.1 什么是指针?

指针是C++中一种特殊的变量,它存储的不是实际的数据值,而是另一个变量的内存地址。可以将指针理解为内存的导航系统或地址纸条。

int x = 10;         // 实际存储数据的变量
int* ptr = &x;      // 指针变量,存储x的地址

在这里插入图片描述
在 C 和 C++ 中,直接打印指针变量本身,输出的结果是否为 x 的地址,取决于你使用的编程语言以及打印方式——核心区别在于“是否指定了正确的格式化规则”。


ptr直接打印会是什么内容?


1)C 语言中:直接打印需用 %p 占位符

C 语言中没有“自动识别指针并打印地址”的机制,必须通过 printf 函数配合 %p 格式化占位符,才能正确输出指针存储的地址(即 x 的地址)。

1. 正确用法(输出 x 的地址)

使用 %p 明确告诉 printf:“待打印的变量是指针,需要输出它存储的内存地址”。

#include <stdio.h>

int main() {
    int x = 10;
    int* ptr = &x;  // ptr 存储 x 的地址
    
    // %p 占位符:输出 ptr 存储的地址(即 x 的地址)
    printf("ptr 存储的地址(x 的地址):%p\n", ptr);  
    printf("直接取 x 的地址:%p\n", &x);  // 与上面输出完全一致
    
    return 0;
}

输出结果(地址是内存中的随机位置,仅作示例):

ptr 存储的地址(x 的地址):0x7ffd8b2b7a2c
直接取 x 的地址:0x7ffd8b2b7a2c
2) 错误用法(不会输出 x 的地址)

如果误用其他占位符(如 %d 打印整数、%c 打印字符),会导致“未定义行为”——输出的是无意义的数字,而非 x 的地址。

// 错误示例:用 %d 打印指针
printf("错误用法(%d):%d\n", ptr);  // 输出随机数字,不是 x 的地址

3 )C++ 中:两种打印方式的区别

C++ 兼容 C 语言的 printf,同时提供了 cout 输出流。两种方式对指针的处理不同:

1. 用 cout 直接打印(输出 x 的地址)

C++ 的 cout自动识别指针类型,直接打印指针变量存储的内存地址(无需额外占位符)。

#include <iostream>
using namespace std;

int main() {
    int x = 10;
    int* ptr = &x;
    
    // cout 直接打印指针:输出 x 的地址
    cout << "ptr 存储的地址(x 的地址):" << ptr << endl;
    cout << "直接取 x 的地址:" << &x << endl;  // 与上面输出完全一致
    
    return 0;
}

输出结果(示例):

ptr 存储的地址(x 的地址):0x7ffd8b2b7a2c
直接取 x 的地址:0x7ffd8b2b7a2c
2. 用 printf 打印(同 C 语言,需 %p

如果在 C++ 中混用 printf,仍需遵循 C 语言的规则,用 %p 才能正确输出地址。

// C++ 中用 printf 打印指针
printf("ptr 存储的地址:%p\n", ptr);  // 正确,输出 x 的地址

关键总结

编程语言 打印方式 是否输出 x 的地址? 核心原因
C printf("%p", ptr) %p 专门用于输出指针存储的地址
C printf("%d", ptr) 占位符与指针类型不匹配,行为未定义
C++ cout << ptr cout 自动识别指针,输出地址
C++ printf("%p", ptr) 兼容 C 语言的格式化规则

简单说:在正确使用打印规则的前提下(C 用 %p,C++ 用 cout%p),直接打印指针变量 ptr,输出的就是 x 的地址——因为 ptr 的本质就是“存储 x 地址的变量”。

在这里插入图片描述


1.2 基本指针操作

操作 语法 含义 比喻
取地址 &var 获取变量地址 记录房子门牌号
声明指针 Type* ptr 声明指向Type的指针 准备一张地址纸条
解引用 *ptr 访问指针指向的值 按地址找到房子
成员访问 ptr->member 访问指针所指对象的成员 进入房子查看房间

1.3 指针与对象的关系

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

ListNode node(10);      // 创建节点对象
ListNode* ptr = &node;  // 指针指向该节点

// 三种等价的访问方式:
node.val = 20;          // 直接访问
ptr->val = 20;          // 通过指针访问(推荐)
(*ptr).val = 20;        // 解引用后访问

关键理解:无论用哪种方式,操作的都是同一个内存位置!


2. 链表归并排序中的指针应用

2.1 算法框架

自底向上的归并排序是理解指针应用的完美案例:

ListNode* sortList(ListNode* head) {
    if (!head || !head->next) return head;
    
    // 计算链表长度
    int length = 0;
    ListNode* curr = head;
    while (curr) {
        length++;
        curr = curr->next;
    }
    
    ListNode dummy(0);   // 虚拟头节点
    dummy.next = head;
    
    // 自底向上归并
    for (int step = 1; step < length; step *= 2) {
        ListNode* prev = &dummy;  // 连接指针
        curr = dummy.next;        // 处理指针
        
        while (curr) {
            // 分割和合并操作
            ListNode* left = curr;
            ListNode* right = split(left, step);
            curr = split(right, step);
            
            ListNode* merged = mergeTwoLists(left, right);
            prev->next = merged;
            
            // 移动prev到末尾
            while (prev->next) {
                prev = prev->next;
            }
        }
    }
    
    return dummy.next;
}

2.2 三大关键指针的作用

指针 作用 比喻
dummy 固定的头节点锚点 大地基,永远标记起点
prev 移动的连接器 胶水,负责连接各个段落
curr 移动的分割器 剪刀,负责切割待处理段落

3. 核心子函数:指针操作的精华

3.1 分割函数 split()

ListNode* split(ListNode* head, int step) {
    if (!head) return nullptr;
    ListNode* curr = head;
    
    // 移动到第step个节点(或链表末尾)
    for (int i = 1; i < step && curr->next; i++) {
        curr = curr->next;
    }
    
    ListNode* rest = curr->next;  // 保存剩余部分
    curr->next = nullptr;         // 切断链表
    return rest;
}

指针操作解析

  1. curr 遍历链表,找到分割点
  2. curr->next = nullptr 执行实际的分割操作
  3. 返回剩余部分的头指针

示例

原链表: 1→2→3→4→5→null
调用 split(head, 2) 后:
left部分: 1→2→null
返回: 3→4→5→null

3.2 合并函数 mergeTwoLists()

ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    ListNode dummy(0);        // 虚拟头节点
    ListNode* curr = &dummy;  // 移动的构建指针
    
    while (l1 && l2) {
        if (l1->val < l2->val) {
            curr->next = l1;   // 连接l1节点
            l1 = l1->next;     // 移动l1指针
        } else {
            curr->next = l2;   // 连接l2节点
            l2 = l2->next;     // 移动l2指针
        }
        curr = curr->next;     // 移动构建指针
    }
    
    curr->next = l1 ? l1 : l2; // 连接剩余部分
    return dummy.next;         // 返回实际头节点
}

指针操作解析

  1. dummy 提供稳定的起始点
  2. curr 逐步构建新链表
  3. l1l2 遍历两个输入链表
  4. 最后返回 dummy.next 获取真正的头节点

关键技巧curr 开始时指向 dummy,通过 curr->next 修改 dummy.next,然后移动 curr 继续构建。


4. 常见问题与调试技巧

4.1 常见指针错误

  1. 野指针:未初始化的指针

    ListNode* ptr;      // ❌ 危险!
    ptr->val = 10;      // 💥 段错误
    
  2. 空指针解引用

    ListNode* ptr = nullptr;
    cout << ptr->val;   // 💥 运行时错误
    
  3. 内存泄漏

    ListNode* ptr = new ListNode(10);
    // 忘记 delete ptr;
    

4.2 调试建议

  1. 可视化绘图:在纸上画出链表和指针位置
  2. 使用调试器:单步执行,观察指针值的变化
  3. 添加打印语句
    cout << "curr at: " << curr << ", value: " << (curr ? curr->val : -1) << endl;
    

5. 总结与最佳实践

5.1 指针使用原则

  1. 总是初始化指针

    ListNode* ptr = nullptr;  // ✅ 安全
    
  2. 优先使用栈对象

    ListNode dummy(0);        // ✅ 自动内存管理
    
  3. 明确指针所有权:谁创建,谁释放

5.2 链表操作模式

// 模式1:虚拟头节点 + 移动指针
ListNode dummy(0);
ListNode* curr = &dummy;
// ... 构建操作 ...
return dummy.next;

// 模式2:双指针技巧
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
    slow = slow->next;
    fast = fast->next->next;
}

参考文献:【指针】内含超多图解!通俗易懂!(入门必看!)
参考文献:关于指针的基本知识,什么是指针?(小白版)

更多推荐