指针的介绍

C++指针是一种特殊的变量,其本质是内存地址的“容器”——它不直接存储数据值,而是记录某块内存空间的起始位置;通过解引用操作符**(*)**可以访问或修改该地址上的实际数据。这一特性使指针能直接与计算机内存交互,成为动态内存管理(如通过new分配、delete释放资源)、高效操作复杂数据结构(如链表、树的节点链接)以及优化程序性能的核心工具。

指针的引入

函数一般是只能返回一个值,即使我们函数里面可以写多个return,但只要执行到一个return,函数就会结束。而数组虽能返回多个值(数组里面包含多个元素),但是数组内元素的数据类型是一致的,对于不同的数据类型,也不能做到返回多个值。

因此为了解决这一问题,可以使用指针,使用指针可以想返回几个值就返回几个值,想要返回什么类型就返回什么类型。

在程序设计过程中,存入数据还是取出数据都需要与内存单元打交道,计算机通过地址编码来表示内存单元。

指针类型就是为了处理计算机地址数据的,计算机将内存划分为若干个存储空间大小的单元,每个单元大小就是一个字节,即计算机将内存换分为一个一个的字节,然后为每一个字节分配唯一的编码,这个编码即为这个字节的地址。

指针就是用来表示这些地址的,即指针型数据不是什么字符型数据,而存的是我们内存中的地址编码。

指针可以提高程序的效率,更重要的是能使一个函数访问另一个函数的局部变量,指针是两个函数进行数据交换必不可少的工具。

地址与指针的概念

在计算机系统中,内存是由大量的存储单元组成的,每个存储单元都有一个唯一的编号,这个编号就是内存地址。内存地址的作用类似于日常生活中家庭的门牌号,通过它可以准确找到对应的存储位置。

在32位系统中,内存地址通常用32位二进制数表示,这意味着系统最多可以寻址4GB的内存空间;而在64位系统中,内存地址由64位二进制数表示,理论上可以支持更大的内存空间(约18EB)。

指针本质上就是一个用于存储内存地址的变量。与普通变量不同,指针变量中存储的不是具体的数据值,而是某块内存空间的地址。通过这个地址,程序可以间接地访问或修改该地址对应的内存单元中的数据。

在C++中,指针的定义需要指明其指向的数据类型。例如:

1
2
int *p;  // 定义一个指向整型数据的指针变量p
char *q; // 定义一个指向字符型数据的指针变量q

这里的*符号表示这是一个指针变量,而intchar则指定了该指针可以指向的数据类型。指针的类型决定了通过该指针访问内存时的寻址范围和数据解释方式。

需要注意的是,指针变量在定义后如果没有初始化,它的值是不确定的(通常称为野指针),直接使用这样的指针可能会导致程序崩溃或产生不可预期的结果。因此,在使用指针前,应该始终确保它指向了有效的内存空间。

指针的定义形式和含义

在C++中,指针的定义遵循一定的语法规则,正确理解这些语法对于掌握指针的使用至关重要。

指针定义的基本语法

指针定义的基本形式为:

1
数据类型 *指针变量名;

其中:

  • 数据类型:表示该指针可以指向的数据类型,也称为指针的基类型
  • *:星号是一个指针声明符,表明这是一个指针变量
  • 指针变量名:遵循标识符命名规则的变量名称

需要注意的是,在C++中,星号*实际上是与变量名绑定的,而不是与数据类型绑定的。这意味着在同一行定义多个指针时,每个指针变量前都需要添加星号:

1
2
int *p1, *p2;  // 正确:p1和p2都是指向int类型的指针
int* p3, p4; // 错误:p3是指向int类型的指针,但p4是普通int变量

不同类型指针的定义示例

以下是一些常见数据类型的指针定义示例:

1
2
3
4
5
6
int *p_int;       // 指向整型数据的指针
char *p_char; // 指向字符型数据的指针
float *p_float; // 指向浮点型数据的指针
double *p_double; // 指向双精度浮点型数据的指针
int (*p_array)[5]; // 指向包含5个int元素的数组的指针
int (*p_func)(int, int); // 指向返回int、接受两个int参数的函数的指针

指针的类型含义

指针的基类型决定了通过该指针访问内存时的行为:

  1. 寻址范围:指针的类型决定了从指针指向的地址开始,一次可以访问的内存字节数。例如,char*指针一次访问1个字节,int*指针(在32位系统中)通常一次访问4个字节。

  2. 指针运算:当对指针进行加减运算时,指针移动的字节数等于基类型的大小。例如,int*指针加1,实际地址值增加4(假设int占4字节)。

  3. 数据解释:通过指针访问内存时,系统会根据指针的类型来解释读取到的二进制数据。同样的二进制数据,用int*float*访问可能会得到完全不同的数值。

指针的占用内存大小

在相同的编译环境下,无论指针指向的数据类型是什么,指针变量本身占用的内存大小是相同的。这是因为所有指针存储的都是内存地址,而地址的长度由系统的寻址能力决定:

  • 在32位系统中,指针变量通常占用4个字节
  • 在64位系统中,指针变量通常占用8个字节

可以通过sizeof运算符来验证这一点:

1
2
3
cout << "sizeof(int*) = " << sizeof(int*) << endl;
cout << "sizeof(char*) = " << sizeof(char*) << endl;
cout << "sizeof(double*) = " << sizeof(double*) << endl;

在同一系统中,以上三个输出结果应该是相同的。

特殊指针类型

C++中还有一些特殊的指针类型,它们具有特定的含义和用途:

  1. 空指针(nullptr):表示不指向任何有效内存地址的指针。在C++11及以后的标准中,推荐使用关键字nullptr来表示空指针,而不是使用NULL宏。

    1
    int *p = nullptr; // 正确:使用nullptr初始化指针
  2. const指针:指针本身的值不能被修改(即不能改变指针的指向)。

    1
    2
    3
    4
    int a = 10, b = 20;
    int *const p = &a; // p是一个const指针,始终指向a
    // p = &b; // 错误:不能改变const指针的指向
    *p = 30; // 正确:可以通过const指针修改所指对象的值
  3. 指向const的指针:指针所指向的对象的值不能通过该指针修改。

    1
    2
    3
    4
    5
    const int a = 10;
    const int *p = &a; // p是一个指向const的指针
    // *p = 20; // 错误:不能通过p修改a的值
    int b = 20;
    p = &b; // 正确:可以改变p的指向

指针的特殊用法

指针在C++中有许多特殊而强大的用法,这些用法使得指针成为C++中灵活而高效的工具。下面介绍几种常见的指针特殊用法。

1. 指针与数组

在C++中,指针和数组有着密切的关系。数组名实际上是一个指向数组首元素的常量指针。这意味着我们可以使用指针来访问数组元素:

1
2
3
4
5
6
7
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 等价于 int *p = &arr[0];

// 使用指针访问数组元素
cout << *p << endl; // 输出:10,访问arr[0]
cout << *(p + 1) << endl; // 输出:20,访问arr[1]
cout << *(p + 2) << endl; // 输出:30,访问arr[2]

需要注意的是,当指针进行加减运算时,指针移动的字节数等于其基类型的大小。例如,对于int*指针,加1操作会使指针地址增加4字节(假设int占4字节)。

2. 指针数组

指针数组是一个数组,其每个元素都是一个指针。指针数组常用于存储多个字符串或多个数组的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 存储多个字符串的指针数组
const char *names[] = {"Alice", "Bob", "Charlie", "David"};

// 访问指针数组中的元素
cout << names[0] << endl; // 输出:Alice
cout << names[1] << endl; // 输出:Bob

// 存储多个整型数组地址的指针数组
int arr1[] = {1, 2, 3};
int arr2[] = {4, 5, 6};
int arr3[] = {7, 8, 9};
int *arrays[] = {arr1, arr2, arr3};

// 通过指针数组访问各个数组的元素
cout << arrays[0][1] << endl; // 输出:2,访问arr1[1]
cout << arrays[2][2] << endl; // 输出:9,访问arr3[2]

3. 多级指针

多级指针是指向指针的指针。在某些复杂的数据结构中,我们可能需要使用多级指针来间接访问数据:

1
2
3
4
5
6
7
8
9
10
int a = 10;
int *p = &a; // 一级指针,指向a
int **pp = &p; // 二级指针,指向p
int ***ppp = &pp; // 三级指针,指向pp

// 通过多级指针访问a的值
cout << "a = " << a << endl; // 直接访问,输出:10
cout << "*p = " << *p << endl; // 通过一级指针访问,输出:10
cout << "**pp = " << **pp << endl; // 通过二级指针访问,输出:10
cout << "***ppp = " << ***ppp << endl; // 通过三级指针访问,输出:10

在实际编程中,二级指针较为常见,而三级及以上的指针则较少使用,因为它们会使代码变得复杂难懂。

4. void指针

void*指针是一种特殊类型的指针,可以指向任何类型的数据,但不能直接通过它访问所指向的数据(需要先进行类型转换):

1
2
3
4
5
6
7
8
9
10
void *vp;
int a = 10;
char c = 'A';

vp = &a; // void指针指向整型变量
// cout << *vp << endl; // 错误:不能直接解引用void指针
cout << *((int*)vp) << endl; // 正确:先将void指针转换为int指针,再解引用,输出:10

vp = &c; // void指针指向字符型变量
cout << *((char*)vp) << endl; // 正确:先将void指针转换为char指针,再解引用,输出:A

void指针常用于函数参数和返回值,以实现通用的数据处理功能。例如,C语言中的malloc函数返回的就是void*类型的指针。

5. 野指针和悬空指针

  • 野指针:未初始化的指针,其指向的内存地址是不确定的。使用野指针可能会导致程序崩溃或数据损坏。

    1
    2
    int *p; // 野指针,未初始化
    // *p = 10; // 危险:可能导致程序崩溃或数据损坏
  • 悬空指针:指针曾经指向有效的内存,但该内存已被释放(例如通过delete操作)。使用悬空指针同样危险。

    1
    2
    3
    int *p = new int(10);
    delete p; // 释放p指向的内存,但p本身并未置空
    // *p = 20; // 危险:p现在是悬空指针,指向已释放的内存

为了避免野指针和悬空指针的问题,应该始终:

  1. 指针定义后立即初始化(即使初始化为nullptr
  2. 指针释放内存后将其设为nullptr

6. 指针与函数

指针在函数中的应用主要包括:

  • 函数参数传递:通过指针传递参数,可以在函数内部修改实参的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
    }

    int main() {
    int x = 10, y = 20;
    swap(&x, &y); // 通过指针传递参数
    cout << "x = " << x << ", y = " << y << endl; // 输出:x = 20, y = 10
    return 0;
    }
  • 函数返回指针:函数可以返回指针,但需要确保返回的指针指向的内存是有效的(不是局部变量的地址)

    1
    2
    3
    4
    int *createArray(int size) {
    int *arr = new int[size]; // 在堆上分配内存
    return arr; // 返回指向堆内存的指针
    }
  • 函数指针:指向函数的指针,可以用于实现回调函数等功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    int add(int a, int b) {
    return a + b;
    }

    int subtract(int a, int b) {
    return a - b;
    }

    int main() {
    int (*operation)(int, int); // 声明一个函数指针

    operation = add; // 函数指针指向add函数
    cout << operation(5, 3) << endl; // 调用add函数,输出:8

    operation = subtract; // 函数指针指向subtract函数
    cout << operation(5, 3) << endl; // 调用subtract函数,输出:2

    return 0;
    }

7. 智能指针(C++11及以后)

为了解决手动内存管理容易导致的内存泄漏问题,C++11引入了智能指针,它可以自动管理动态分配的内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <memory>

int main() {
// unique_ptr:独占所有权的智能指针
std::unique_ptr<int> up1(new int(10));
// std::unique_ptr<int> up2 = up1; // 错误:unique_ptr不允许拷贝
std::unique_ptr<int> up2 = std::move(up1); // 正确:可以通过move转移所有权

// shared_ptr:共享所有权的智能指针
std::shared_ptr<int> sp1(new int(20));
std::shared_ptr<int> sp2 = sp1; // 正确:shared_ptr允许多个指针共享同一块内存

// weak_ptr:不增加引用计数的共享指针,用于解决shared_ptr的循环引用问题
std::weak_ptr<int> wp = sp1;

return 0;
}

智能指针是现代C++中推荐的内存管理方式,可以大大减少内存泄漏和悬空指针的问题。

指针使用的注意事项

  • 始终初始化指针(即使初始化为nullptr)
  • 避免使用未初始化的指针(野指针)
  • 释放内存后将指针设为nullptr,避免悬空指针
  • 注意指针的作用域和生命周期
  • 理解指针运算的含义,避免越界访问

总结

C++指针是一种强大而灵活的工具,它允许程序直接与内存交互,是C++语言的核心特性之一。

  • C++11引入了智能指针(unique_ptrshared_ptrweak_ptr),它们可以自动管理动态分配的内存,大大减少了内存泄漏和悬空指针的问题
  • 在现代C++编程中,推荐优先使用智能指针而非原始指针进行内存管理

总之,掌握指针的使用是学习C++的重要环节。指针既可以提高程序的效率和灵活性,也可能带来安全隐患和难以调试的错误。因此,在使用指针时,我们应该始终保持谨慎,遵循良好的编程实践,确保代码的安全性和可靠性。随着对指针理解的深入,我们将能够更加灵活地运用C++语言的强大功能,编写出高效、优雅的程序。