C++ 指针和引用

指针和引用是 C++ 中用于间接访问变量的重要机制,两者都能提高代码的灵活性和效率,但在语法、语义和使用场景上有显著区别。

1、指针

指针是存储另一个变量内存地址的变量 ,其本质是一个独立的变量,拥有自己的内存空间,存储的内容是目标变量的地址(通常为 4 字节或 8 字节,取决于系统位数)。

1.1 基本概念与语法

cpp 复制代码
int main() {
    int number = 42;     // 一个普通的整型变量
    int* ptr = &number;  // 一个指针变量,存储了number的地址

    std::cout << "Value of number: " << number << std::endl;       // 输出: 42
    std::cout << "Address of number: " << &number << std::endl;    // 输出: 0x7ffd2d5e7a4c (类似的值)
    std::cout << "Value of ptr: " << ptr << std::endl;             // 输出: 0x7ffd2d5e7a4c (和&number一样)
    std::cout << "Value pointed to by ptr: " << *ptr << std::endl; // 输出: 42 (解引用)

    return 0;
}
  • & (取地址运算符):获取变量的内存地址。
  • * (解引用运算符):访问指针所指向地址中存储的值。
  • 声明:type* pointer_name; (例如 int* p;, double* dPtr;)

1.2 指针的运算

指针支持有限的算术运算:++, --, +, -。运算的单位是其所指向类型的大小。

cpp 复制代码
int arr[] = {10, 20, 30, 40};
int* arrPtr = arr; // 数组名可视为指向其第一个元素的指针

std::cout << *arrPtr << std::endl; // 输出: 10
arrPtr++; // 指针向前移动 sizeof(int) 个字节
std::cout << *arrPtr << std::endl; // 输出: 20
arrPtr += 2;
std::cout << *arrPtr << std::endl; // 输出: 40

1.3 指针与常量 (const)

const 和指针的组合容易混淆,关键在于 const 修饰的是什么。

cpp 复制代码
int value = 10;
int anotherValue = 20;

// 1. 常量指针 (Pointer to constant)
// 指针指向的值不能通过该指针修改,但指针本身可以指向别的地址。
const int* ptr1 = &value;
// *ptr1 = 30; // 错误:不能修改指向的值
ptr1 = &anotherValue;     // 正确:可以修改指针的指向

// 2. 常量指针 (Constant pointer)
// 指针本身的值(存储的地址)不能改变,但它指向的值可以改变。
int* const ptr2 = &value;
*ptr2 = 30;               // 正确:可以修改指向的值
// ptr2 = &anotherValue;  // 错误:不能修改指针的指向

// 3. 指向常量的常量指针 (Constant pointer to constant)
// 指针的指向和指向的值都不能改变。
const int* const ptr3 = &value;
// *ptr3 = 30;            // 错误
// ptr3 = &anotherValue;  // 错误

总结

  • 根据英文翻译记名字,const :常量指针; const:指针常量
  • 根据const修饰记哪些不能改,const int* ptr1:const修饰指针,则不能给*ptr1赋值; int* const ptr2: const修饰ptr2,则不能给ptr2赋值

1.4 动态内存管理

指针是 C++ 中手动进行动态内存分配和释放的关键。

cpp 复制代码
// 动态分配单个int
int* dynamicInt = new int(100);
std::cout << *dynamicInt << std::endl; // 输出: 100

// 动态分配数组
int size = 5;
int* dynamicArray = new int[size]{1, 2, 3, 4, 5}; // C++11 初始化

// 使用后必须手动释放,否则会导致内存泄漏
delete dynamicInt;      // 释放单个对象
delete[] dynamicArray;  // 释放数组
dynamicInt = nullptr;   // 良好实践:释放后立即将指针置为空,防止"悬空指针"
dynamicArray = nullptr;

1.5 多级指针

指针可以指向另一个指针。

cpp 复制代码
int a = 5;
int* ptr = &a;
int** pptr = &ptr; // 指向指针的指针

std::cout << **pptr << std::endl; // 输出: 5 (需要两次解引用)

1.6 指针的常见问题

  1. 空指针 (Null Pointer)int* ptr = nullptr; (C++11+) 或 NULL (C)。指向地址 0,表示"不指向任何对象"。解引用空指针会导致未定义行为(通常是程序崩溃)

  2. 悬空指针 (Dangling Pointer)指针指向的内存已被释放 。解引用它会导致未定义行为。

    cpp 复制代码
    int* ptr = new int(10);
    delete ptr; // 内存被释放
    // ptr 现在是一个悬空指针
    // *ptr = 20; // 危险!未定义行为
    ptr = nullptr; // 正确的做法:释放后置空
  3. 野指针 (Wild Pointer)未被初始化的指针 。其值是随机的垃圾地址。绝对不要解引用野指针

    cpp 复制代码
    int* wildPtr; // 野指针
    // *wildPtr = 10; // 极度危险!未定义行为

2、引用

引用是已存在变量的别名 ,它不占用独立的内存空间,与被引用的变量共享同一块内存。引用必须在定义时初始化,且一旦绑定到某个变量,就不能再改为绑定其他变量 。它本质上是一个解引用的、不可为空的、不可重新绑定的常量指针

2.1 基本概念与语法

cpp 复制代码
int main() {
    int original = 50;
    int& ref = original; // ref 是 original 的引用(别名)

    std::cout << original << std::endl; // 输出: 50
    std::cout << ref << std::endl;      // 输出: 50

    ref = 100; // 通过引用修改原变量的值

    std::cout << original << std::endl; // 输出: 100
    std::cout << ref << std::endl;      // 输出: 100

    // int& badRef; // 错误!引用必须在声明时初始化。
    return 0;
}
  • 必须在创建时初始化 ,且一旦初始化绑定到一个变量,就不能再成为其他变量的引用
  • 对引用的所有操作都是在操作它所引用的原始变量。

2.2 常量引用

常量引用常用于函数参数,以避免不必要的拷贝,同时保证不会意外修改传入的实参。它可以绑定到临时对象(右值)。

cpp 复制代码
void printValue(const int& value) { // 使用const引用作为参数
    std::cout << value << std::endl;
    // value = 10; // 错误:不能通过const引用修改值
}

int main() {
    int a = 5;
    printValue(a);    // OK
    printValue(10);   // OK!常量引用可以绑定到临时对象(字面量10)
    return 0;
}

2.3 引用与函数

引用最重要的用途之一是在函数参数传递和返回值中。

2.3.1 按引用传递

允许函数修改实参的值,且避免大对象拷贝的开销。

cpp 复制代码
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    swap(x, y);
    std::cout << x << ", " << y; // 输出: 20, 10
    return 0;
}
2.3.2 按引用返回

可以返回函数调用者可以修改的左值。**绝不能返回局部变量的引用!**核心原因是局部变量的内存生命周期与引用的有效性不匹配,会导致 "悬空引用",引发未定义行为。

局部变量(如函数内定义的变量)存储在栈区 ,其生命周期仅限于定义它的代码块 (如函数执行期间)。当代码块执行结束(如函数返回),栈帧会被销毁 ,局部变量的内存会被释放(变为 "无效内存")。

若此时返回该变量的引用,这个引用会指向一块 "已失效的内存"(即 "悬空引用")。后续对该引用的任何操作(如读取、修改)都会触发未定义行为(可能读取到垃圾值、程序崩溃、或出现不可预测的结果)。

cpp 复制代码
int& getElement(int arr[], int index) {
    return arr[index]; // 返回数组元素的引用是安全的
}

int main() {
    int array[] = {1, 2, 3};
    getElement(array, 1) = 50; // 修改array[1]为50
    std::cout << array[1]; // 输出: 50
    return 0;
}

3、常见问题

  1. 指针和引用的本质区别是什么?

    • 指针是存储地址的独立变量 ,有自己的内存空间;引用是变量的别名,无独立内存空间,与被引用变量共享内存。
    • 指针可以为 nullptr,引用必须绑定有效变量(无空引用)。
    • 指针可以重新赋值指向其他变量,引用一旦绑定则不可改向。
  2. 为什么引用必须初始化?

    引用的本质是"别名",语法上要求必须从定义时就明确它是谁的别名。如果允许未初始化的引用,就无法确定它指向哪个变量,会导致无法预测的内存操作(未定义行为)。这是 C++ 语法对引用的强制约束,以保证安全性。

  3. 函数参数传递时,何时用指针,何时用引用?

    • 用引用

      • 需修改实参的值,且能保证参数非空(无需检查 nullptr)。
      • 传递大型对象(避免复制,与指针效率相同,但语法更简洁)。
      • 运算符重载(如 operator+operator= 通常用引用返回)。
    • 用指针

      • 允许参数为空(需在函数内检查 nullptr,如 delete 操作)。
      • 需要传递多级间接地址(如 int** 用于修改指针本身)。
      • 与 C 语言兼容(C 中无引用,只能用指针)。
  4. 什么是"野指针"?如何避免?

    "野指针"是指向无效内存 (如已释放的内存、未初始化的内存)的指针 ,访问野指针会导致未定义行为(崩溃、数据错误等)。

    避免方法:

    • 指针定义时初始化(如 int* p = nullptr)。
    • 释放内存后及时置空(delete p; p = nullptr)。
    • 不返回局部变量的指针。
    • 函数接收指针参数时,先检查是否为 nullptr
  5. const int*, int* const, 和 const int* const 有什么区别?

    • const int* - 指向常量的指针(值不能改);
    • int* const - 常量指针(指针本身不能改);
    • const int* const - 指向常量的常量指针(值和指针都不能改)。
  6. 什么是悬空指针?如何避免?

    指向已被释放的内存 的指针。
    避免方法:

    • 释放后立即将指针置为 nullptr
    • 使用智能指针。
  7. void* 指针是什么?

    是一种可以指向任何数据类型(未指定类型)的通用指针。不能直接解引用,必须先强制转换为具体的类型。

相关推荐
感哥13 小时前
C++ 多态
c++
沐怡旸19 小时前
【底层机制】std::string 解决的痛点?是什么?怎么实现的?怎么正确用?
c++·面试
River4161 天前
Javer 学 c++(十三):引用篇
c++·后端
感哥1 天前
C++ std::set
c++
侃侃_天下1 天前
最终的信号类
开发语言·c++·算法
博笙困了1 天前
AcWing学习——差分
c++·算法
青草地溪水旁1 天前
设计模式(C++)详解—抽象工厂模式 (Abstract Factory)(2)
c++·设计模式·抽象工厂模式
青草地溪水旁1 天前
设计模式(C++)详解—抽象工厂模式 (Abstract Factory)(1)
c++·设计模式·抽象工厂模式
感哥1 天前
C++ std::vector
c++