搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第7章 指针基础

第7章 指针基础

入门 👤 已掌握变量和数组的初学者 🔧 理解数据在内存中的位置与间接访问
本章你将学到
  • 指针是什么,如何声明和使用指针
  • 指针与数组、函数的关系
  • 指针运算与空指针的安全使用
核心概念指针变量解引用

想象一下,你有一张写着朋友家地址的纸条。纸条本身不是房子,但通过它你能找到房子。在C++中,指针就是这张“地址纸条”,它存储的是另一个变量在内存中的位置(地址),而不是数据本身。理解指针是解锁C++高效编程和复杂数据结构(如链表、树)的关键第一步。

7.1 指针是什么:从地址到变量

每个变量在内存中都有一个唯一的地址,就像每个房子有唯一的门牌号。指针变量专门用来存放这些地址。声明指针时,需要在类型后加上星号(*)。

int num = 42;        // 一个普通的整型变量
int *pNum = #   // pNum是一个“指向int的指针”,用&取num的地址赋值给它

关键操作符

  • &(取地址运算符):获取变量的内存地址。例如 &num。
  • *(解引用运算符):通过指针访问它指向的地址里存储的值。例如 *pNum 得到 42。

下面这个表格清晰地展示了普通变量与指针变量的区别和联系:

元素描述示例代码类比
变量 (num)存储实际数据的内存单元int num = 42;房子本身
地址 (&num)变量在内存中的位置cout << # (输出如0x7ffeed6a5c)房子的门牌号
指针变量 (pNum)存储地址的变量int *pNum = #写着门牌号的纸条
解引用 (*pNum)通过指针访问其指向的值*pNum = 100; (将num改为100)根据纸条找到房子并进去

我在早期项目中踩过一个坑:声明了指针却忘记初始化。未初始化的指针指向一个随机的、未知的内存地址,解引用它会导致程序崩溃(段错误)。声明指针后,立即将其指向一个有效的地址或设为nullptr,这是一个必须养成的好习惯。

7.2 指针的声明、初始化与使用

指针的声明语法是 类型* 指针变量名。星号(*)靠近类型还是变量名是风格问题,但必须保持一致。我推荐 int* p; 这种写法,强调“指向int的指针”是一个类型。

常见误区

  • 混淆声明中的*和解引用中的*:int *p; 里的*是声明符,表示p是指针。*p = 5; 里的*是运算符,表示操作p指向的内存。
  • 对未初始化的指针进行解引用:这是导致程序崩溃的最常见原因之一。如果你看到 Segmentation fault (core dumped) 这类错误,首先检查指针是否有效。
  • 类型不匹配:double d = 3.14; int *p = &d; 是错误的。指针类型必须与其指向的变量类型严格一致(void*指针除外)。

正确的初始化流程如下:

声明指针变量
→
初始化为nullptr或有效地址
→
使用前检查有效性
→
通过解引用访问或修改数据
// 正确示例
int value = 10;
int* ptr1 = &value;      // 方式1:指向已有变量
int* ptr2 = nullptr;     // 方式2:初始化为空(C++11推荐)
// int* ptr3;           // 危险!未初始化,绝对避免

if (ptr1 != nullptr) {  // 使用前检查是好习惯
    *ptr1 = 20;          // 解引用并修改,value现在变为20
    int another = *ptr1; // 解引用获取值,another为20
}

编者提示:关于 nullptr(C++11引入)与旧的 NULL 宏的详细对比,以及为什么现代C++推荐使用 nullptr,我们将在第31章《C++11/14新特性》中深入探讨。

7.3 指针与数组的紧密关系

数组名在大多数情况下会被编译器自动转换为指向其首元素的指针。这是C/C++中一个极其重要且常用的特性。

int arr[5] = {1, 2, 3, 4, 5};
int* p = arr; // 等价于 int* p = &arr[0];

// 通过指针访问数组元素
int first = *p;       // first = 1,访问arr[0]
int second = *(p + 1); // second = 2,访问arr[1]
*(p + 2) = 30;         // arr[2] 被修改为 30

这里引入了指针算术:对指针进行加/减整数操作,移动的距离是以其指向类型的大小为单位的。对于 int* p,p+1 意味着地址增加 sizeof(int) 个字节(通常是4字节),从而指向下一个int。

指针 vs. 数组名

虽然数组名在很多地方可以当作指针用,但有一个关键区别:数组名是常量指针,其值(地址)不可改变。而指针变量本身的值可以改变。

int arr[3];
int* p = arr;

p++;      // 合法:p现在指向arr[1]
// arr++; // 非法!数组名arr是常量,不能自增。

7.4 指针作为函数参数(传址调用)

回忆第6章的函数参数传递,普通的传值是“复制一份”。如果想在函数内部修改外部变量的值,就需要传递该变量的地址,即使用指针参数。这被称为“传址调用”。

主调函数
→
传递变量地址
→
被调函数
↓
原始变量
⇄
通过指针直接修改
// 一个交换两个整数值的函数
void swap(int* a, int* b) {
    int temp = *a; // 解引用a,获取其指向的值
    *a = *b;       // 将b指向的值,赋给a指向的内存
    *b = temp;     // 将temp的值,赋给b指向的内存
}

int main() {
    int x = 5, y = 10;
    swap(&x, &y); // 传递x和y的地址
    // 现在 x == 10, y == 5
}

实际开发中,对于需要修改的简单内置类型(如int, double)或大型结构体,传递指针比传递整个对象的副本更高效。但你必须时刻注意指针的有效性,并在函数内做好空指针检查。

场景:遍历数组

  • 使用指针算术 p++ 遍历数组比用下标 arr[i] 在某些历史代码中更常见。
  • 性能上在现代编译器优化后差异极小,但指针形式能更直观体现“移动”的概念。

场景:函数返回多个值

  • C++函数只能直接返回一个值。
  • 通过传递多个指针参数,函数可以将结果写回调用者提供的变量中,实现“返回”多个值的效果。

场景:操作动态内存

  • 这是指针的核心用途之一。
  • 使用 new 运算符在堆上分配内存,返回的是该内存块的地址,必须用指针来接收和管理。
  • 这将在第9章《动态内存管理》中详细展开。

动手试一试

1. 指针追踪练习:在纸上画出以下代码执行过程中,变量、指针及其值的内存关系图。

int a = 1, b = 2;
int *p1 = &a, *p2 = &b;
*p1 = *p1 + *p2; // a 变成多少?
p2 = p1;
*p2 = 10; // 现在 a 和 b 的值分别是多少?

2. 编写一个函数:名为 findMax,接收一个整型数组和其长度(int)作为参数,返回数组中最大值的地址(即一个 int*)。在main函数中测试它。

3. 调试挑战:下面这段代码有什么问题?如何修正?

void setToZero(int* ptr) {
    ptr = new int(0);
}
int main() {
    int value = 42;
    int* p = &value;
    setToZero(p);
    cout << *p; // 期望输出0,实际呢?
}

检验你的理解

  1. 给定 double d = 3.14; double *pd = &d;,表达式 *pd 和 &d 的类型和值分别是什么?
  2. 如果指针 int* ptr = nullptr;,执行 *ptr = 100; 会发生什么?为什么?
  3. 写出通过指针 int* p 访问整型数组 arr 第三个元素(下标为2)的两种不同表达式。
  4. “传址调用”相比“传值调用”的主要优势是什么?它有什么潜在风险?

本章小结

  • 指针是存储内存地址的变量,通过 & 取地址,通过 * 解引用访问目标数据。
  • 指针必须初始化,应指向有效内存地址或初始化为 nullptr,对无效指针解引用会导致程序崩溃。
  • 数组名可视为指向首元素的常量指针,指针算术(p++)是基于所指向类型的大小进行内存地址移动。
  • 指针作为函数参数可以实现“传址调用”,允许函数修改外部变量的值,也用于高效传递大型数据。
  • 理解指针与内存地址的关系,是后续学习动态内存分配、数据结构以及高级C++特性的基石。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注