Skip to content

1. 指针

  • 地址
    • 生活中的地址:

1.1 指针基础

1.1.1 指针概念

在计算机中所有数据都存储在内存单元中,而每个内存单元都有一个对应的地址,只要通过这个地址就能找到对应单元中存储的数据。

  • 在变量名前加&就可以看到其在内存中的地址了
cpp
#include <iostream>
int main(void)
{
    //对于我们创建类型的变量来说,在变量名前加&就可以看到其在内存中的地址了
    int a = 0;
    std::cout << "&a = " << &a << std::endl;

    int c = 1;
    //对于两个普通变量来说,他们在计算机中的地址是随机分配的
    int b = 0;
    std::cout << "&b = " << &b << std::endl;

    //对于数组来说,他们在内存中的排列是连续的
    int arr[5] = { 0 };
    for (int i = 0; i < 5; i++)
    {
        std::cout << "&arr[" << i << "] = " << &arr[i] << std::endl;
    }
    return 0;
}

1.1.2 指针变量

在C++中,允许用一个变量来存放其它变量的地址,这种专门用于存储其它变量地址的变量,我们称之为指针变量。简称指针。

cpp
int age;       // 定义一个普通变量
age = 10;
int *pnAge;    // 定义一个指针变量
pnAge = &age;
1.1.2.1 指针变量的定义
  • 定义:
    数据类型 * 指针变量名
    例如:int *p;
1.1.2.2 指针变量的初始化

C语言中提供了取地址运算符&来表示变量的地址。其一般形式为:
&变量名;

指针变量初始化的方法有两种:定义的同时进行初始化和先定义后初始化。

  • 定义的同时进行初始化:
    cpp
    int a = 1;
    int *pa = &a;
  • 先定义再初始化:
    cpp
    int a = 5;
    int *pa, *pb;
    pa = &a;

注意:

  • 多个指针变量可以指向同一个地址。
  • 指针的指向是可以改变的。
  • 指针的类型要和变量的类型一致。
cpp
int a = 1;
int b = 2;
int *pa = &a;
int *pb = &a; // 多个指针变量可以指向同一个地址
pb = &b;      // 指针的指向是可以改变的
// double *p_a = &a; // error,类型不一致
1.1.2.3 野指针和空指针

指针没有初始化里面是一个垃圾值,称为野指针。程序里不可以出现野指针。

cpp
int *p; // 野指针,不推荐

把指针初始化为NULL,即为空指针:

cpp
int *p = NULL; // 推荐
int *q = 0;    // 同样表示空指针

1.1.3 指针的大小

  • 无论什么类型的指针,得到的大小是:4或8
    • 在32位平台,所有的指针(地址)都是32位(4字节)
    • 在64位平台,所有的指针(地址)都是64位(8字节)
cpp
char ch = 'b';
int *a = NULL;
char *b = &ch;
float *c;

std::cout << "sizeof(a) = " << sizeof(a) << std::endl;
std::cout << "sizeof(b) = " << sizeof(b) << std::endl;
std::cout << "sizeof(c) = " << sizeof(c) << std::endl;

1.1.4 通过指针修改变量的值

  • 访问指针指向的存储空间:
    C++中提供了*来定义指针变量和访问指针变量指向的内存存储空间。
    • 在定义变量的时候,*是一个类型说明符,说明定义的这个变量是一个指针变量。
    • 定义完指针变量后,再在指针变量前加*,表示对指针变量解引用,即获取指针变量所指向区域的内容。
cpp
int a = 5;
int *p = &a; // 此处的*表示p为一个指针
std::cout << "*p = " << *p << std::endl; // 此处的*表示访问指针指向的存储空间(解引用)
  • 通过指针变量修改指针指向的存储空间:
cpp
int a = 1;
int *pa = &a;
*pa = 3; // 通过指针修改变量的值
std::cout << "*pa = " << *pa << std::endl;
std::cout << "a = " << a << std::endl;
  • 野指针和空指针不能解引用(因为没有绑定内存),但是可以重新绑定内存再解引用:
cpp
int *p; // 野指针
// *p = 1; // 错误,野指针不能解引用
int a = 1;
p = &a; // 重新绑定
std::cout << *p << std::endl; // 正确

int *p1 = NULL;
// *p1 = 1; // 错误,空指针不能解引用
p1 = &a;
std::cout << *p1 << std::endl; // 正确

练习:指针方式交换两个变量的值

cpp
#include <iostream>
int main()
{
    int a = 1;
    int b = 2;
    int tmp = 0;
    int* p_a = &a;
    int* p_b = &b;
    int* p_tmp = &tmp;

    *p_tmp = *p_a;
    *p_a = *p_b;
    *p_b = *p_tmp;

    std::cout << "a = " << a << ", b = " << b << std::endl;
    std::cout << "*p_a = " << *p_a << ", *p_b = " << *p_b << std::endl;
    return 0;
}

1.1.5 const修饰的指针变量

声明指针变量的时候可以使用const关键字。

  • 常量指针:声明指针变量的时候可以把const关键字写在类型名称前,不可以通过这种指针对捆绑存储区做赋值,但是可以对这种指针本身做赋值(不可以通过指针对存储区赋值,但可以对指针的地址赋值)。

    cpp
    int a = 1;
    int b = 2;
    const int *pa = &a; // 与 int const *pa = &a; 效果一样
    // *pa = 3; // error
    a = 3;      // right
    pa = &b;    // right
  • 指针常量:声明指针变量的时候可以把const关键字写在指针变量名称前,可以通过这种指针对捆绑存储区做赋值,但是不可以对这种指针本身做赋值(可以通过指针对存储区赋值,但不可以对指针的地址赋值)。

    cpp
    int a = 1;
    int b = 2;
    int * const pa = &a;
    *pa = 3;    // right
    // pa = &b; // error

*1.1.6 void 万能指针

void指针可以指向任意变量的内存空间。不应该在无类型指针前直接加*,必须先把无类型指针强制类型转换成有类型指针然后才能使用。

cpp
void *p = NULL;
int a = 10;
char c = 'a';

p = &c;
p = &a; // 指向变量时,最好转换为void *

// 使用指针变量指向的内存时,转换为int *
*( (int *)p ) = 11;
std::cout << "a = " << a << std::endl;

1.2 指针和数组

数组要通过循环的方式遍历,如果直接打印数组名arr,会发生什么呢?

cpp
#include <iostream>
using namespace std;
int main(void)
{
    int arr[] = { 1, 2, 3, 4, 5 };
    // cout << arr[] << endl; // 错误语法
    cout << arr[0] << endl;  // 1
    cout << arr << endl;     // 输出数组首元素的地址
    return 0;
}

1.2.1 数组指针

1.2.1.1 数组名

数组名字是数组的首元素地址。

cpp
#include <iostream>
int main()
{
    int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    std::cout << "a = " << a << std::endl;
    std::cout << "&a[0] = " << &a[0] << std::endl;
    return 0;
}
1.2.1.2 数组指针操作数组元素

由于数组名可以代表数组首元素地址,数组元素可以通过数组名[下标]访问,那么可以定义一个指针来存放数组的地址,并通过指针[下标]的方式去访问数组元素。

cpp
#include <iostream>
int main()
{
    int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int *pa = a;
    *pa = 10;
    std::cout << "a[0] = " << a[0] << std::endl;   // 10
    std::cout << "pa[0] = " << pa[0] << std::endl; // 10
    return 0;
}
1.2.1.3 数组指针加减运算

在指针指向数组元素时,允许以下运算:

  • 加一个整数(用+或+=),如p+1
  • 减一个整数(用-或-=),如p-1
  • 自加运算,如p++, ++p
  • 自减运算,如p--, --p

地址加减整数n实际上加减的是n个数组元素的大小(即n * sizeof(元素类型))。

注意:数组名不能参与加减计算(因为数组名是常量指针,不能修改)。

cpp
#include <iostream>
int main()
{
    int a[] = { 1, 2, 3, 4, 5, 6, 7 };
    int* pa = a;
    double a1[] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0 };
    double* pa1 = a1;

    std::cout << "pa = " << pa << std::endl;
    std::cout << "pa1 = " << pa1 << std::endl;
    std::cout << std::endl;

    pa++;   // 移动4字节(int大小)
    pa1++;  // 移动8字节(double大小)
    // a++; // error,数组名不能参与加减计算

    std::cout << "pa = " << pa << std::endl;   // 地址增加4
    std::cout << "pa1 = " << pa1 << std::endl; // 地址增加8
    std::cout << std::endl;

    pa += 2; // 移动8字节(2个int)
    // a += 2; // error
    std::cout << "pa = " << pa << std::endl;
    std::cout << std::endl;

    std::cout << "*(pa + 1) = " << *(pa + 1) << std::endl; // 下一个元素
    std::cout << "*pa = " << *pa << std::endl;              // 当前元素
    std::cout << "*(pa - 1) = " << *(pa - 1) << std::endl; // 上一个元素
    std::cout << "*pa - 1 = " << *pa - 1 << std::endl;     // 当前元素值减1,不是数组元素
    return 0;
}

通过数组指针遍历数组

cpp
#include <iostream>
int main()
{
    int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int i = 0;
    int n = sizeof(a) / sizeof(a[0]);

    // 方法1:下标法
    for (i = 0; i < n; i++)
    {
        std::cout << a[i] << " ";
    }
    std::cout << std::endl;

    // 方法2:数组名+偏移量
    for (i = 0; i < n; i++)
    {
        std::cout << *(a + i) << " ";
    }
    std::cout << std::endl;

    // 方法3:指针法(指针变量+偏移量)
    int* p = a;
    for (int i = 0; i < n; i++)
    {
        std::cout << *(p + i) << " ";
    }
    std::cout << std::endl;

    // 方法4:指针法(指针变量当数组名)
    for (int i = 0; i < n; i++)
    {
        std::cout << p[i] << " ";
    }
    std::cout << std::endl;

    // 方法5:指针移动
    for (p = a; p < a + n; p++)
    {
        std::cout << *p << " ";
    }
    std::cout << std::endl;
    return 0;
}

练习:求两个集合的交集

cpp
#include<iostream>
#include<cstdlib>
#include<ctime>
int main()
{
    srand(time(NULL));
    int arr1[10] = { 0 };
    int arr2[10] = { 0 };

    // 生成arr1并打印
    for (int i = 0; i < 10; i++)
    {
        arr1[i] = rand() % 20;
        std::cout << arr1[i] << " ";
    }
    std::cout << std::endl;

    // 生成arr2并打印
    for (int i = 0; i < 10; i++)
    {
        arr2[i] = rand() % 20;
        std::cout << arr2[i] << " ";
    }
    std::cout << std::endl;

    // 求交集
    std::cout << "两个集合的交集有:" << std::endl;
    for (int* p_arr1 = arr1; p_arr1 < arr1 + sizeof(arr1)/sizeof(int); p_arr1++)
    {
        for (int* p_arr2 = arr2; p_arr2 < arr2 + sizeof(arr2)/sizeof(int); p_arr2++)
        {
            if (*p_arr2 == *p_arr1)
            {
                std::cout << *p_arr2 << " ";
                break;
            }
        }
    }
    return 0;
}
1.2.1.4 指针和二维数组

在C++中,多维数组可以通过指针进行访问和操作。

  • 多维数组的名称代表数组首元素的地址。对于二维数组int arr[m][n]arr代表第一行第一个元素的地址(即第一行的地址)。
cpp
int arr[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
int *p1 = arr; // error,类型不匹配

正确的方式是使用数组指针:

cpp
int (*p)[4] = arr; // p指向一个包含4个整数的数组(即一行)

二维数组指针加减法
二维数组指针+1其实是加了内部一维数组的长度(即一行的大小)。

cpp
#include <iostream>
int main()
{
    int arr[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
    int (*p)[4] = arr;
    std::cout << "arr = " << arr << std::endl;
    std::cout << "p = " << p << std::endl;
    std::cout << "arr+1 = " << arr+1 << std::endl; // 增加16(一行4个int,4*4=16)
    std::cout << "p+1 = " << p+1 << std::endl;    // 同样增加16
    return 0;
}

指针遍历二维数组
可使用数组指针和两个下标的形式来访问,也可使用指针算术遍历。

cpp
#include <iostream>
int main()
{
    int arr[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
    int (*p)[4] = arr;

    // 方法1:使用数组指针和下标
    for (int i=0; i<3; i++)
    {
        for (int j=0; j<4; j++)
        {
            std::cout << p[i][j] << " ";
        }
        std::cout << std::endl;
    }

    // 方法2:使用指针算术(将二维数组视为一维)
    int *ptr = &arr[0][0];
    for (int i=0; i<3*4; i++)
    {
        std::cout << *ptr << " ";
        ptr++;
        if ((i+1) % 4 == 0) std::cout << std::endl;
    }
    return 0;
}

1.2.2 指针数组

数组的元素也可以是指针。当数组的元素是指针时,那么这个数组就叫指针数组,指针数组的本质是数组。

*int 类型指针数组

cpp
#include <iostream>
int main(void)
{
    int arr[5] = {1,2,3,4,5};
    int* p_arr[5] = { NULL }; // 指针数组,每个元素都是指针,初始化为NULL
    for (int i=0; i<5; i++)
    {
        p_arr[i] = &arr[i]; // 每个指针元素指向数组arr的对应元素
    }
    for (int i=0; i<5; i++)
    {
        std::cout << *p_arr[i] << std::endl;
    }
    return 0;
}

*char 类型指针数组
注意:打印字符地址时,需要转换为void*,否则cout会将其解释为字符串。

cpp
#include <iostream>
using namespace std;
int main(void)
{
    char p = 'c';
    cout << p << endl;          // 'c'
    cout << (void*)&p << endl;  // 地址

    char arr[] = "abc";
    cout << arr << endl;        // "abc"
    cout << (void*)arr << endl; // 地址

    return 0;
}

指针数组存储多个字符串:

cpp
#include <iostream>
int main(void)
{
    const char arr1[] = "abc";
    const char arr2[] = "def";
    const char arr3[] = "qwe";
    const char arr4[] = "wasd";
    const char arr5[] = "xyz";

    // 字符串数组:每个元素是指针,指向字符串的首地址
    const char* p_arr[5] = { arr1, arr2, arr3, arr4, arr5 };

    // 打印每个字符串的地址
    for (int i=0; i<5; i++)
    {
        std::cout << (void*)p_arr[i] << std::endl;
    }

    // 打印每个字符串
    for (int i=0; i<5; i++)
    {
        std::cout << p_arr[i] << std::endl;
    }

    // 访问单个字符
    for (int i=0; i<4; i++) // 假设第四个字符串"wasd"长度为4
    {
        std::cout << p_arr[3][i] << std::endl; // 输出w, a, s, d
    }
    return 0;
}

知识如风,常伴吾身