MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 指针数组和指向指针的指针

2024-04-195.3k 阅读

C++ 指针数组

指针数组的基本概念

在 C++ 中,指针数组是一个数组,其数组元素都是指针类型。也就是说,该数组的每一个元素都存储着一个内存地址。

声明指针数组的一般形式为:数据类型 *数组名[数组大小]。例如,声明一个包含 5 个 int 类型指针的数组:int *ptrArray[5];

这里需要注意的是,由于 [] 的优先级高于 *,所以 ptrArray 首先是一个数组,然后数组中的元素类型为 int *,即 int 类型的指针。

指针数组的初始化

  1. 静态初始化 可以在声明指针数组时进行初始化。假设我们有几个 int 型变量,然后用指针数组来指向它们:
#include <iostream>
int main() {
    int num1 = 10;
    int num2 = 20;
    int num3 = 30;
    int *ptrArray[3] = {&num1, &num2, &num3};
    for (int i = 0; i < 3; i++) {
        std::cout << "ptrArray[" << i << "] 指向的值为: " << *ptrArray[i] << std::endl;
    }
    return 0;
}

在上述代码中,ptrArray 是一个包含 3 个 int 类型指针的数组,并且在声明时分别初始化为 num1num2num3 的地址。通过循环,我们可以输出每个指针所指向的值。

  1. 动态初始化 也可以动态地为指针数组分配内存。比如,我们要创建一个指针数组,每个指针指向一个动态分配的 int 型变量:
#include <iostream>
int main() {
    int *ptrArray[5];
    for (int i = 0; i < 5; i++) {
        ptrArray[i] = new int(i * 10);
    }
    for (int i = 0; i < 5; i++) {
        std::cout << "ptrArray[" << i << "] 指向的值为: " << *ptrArray[i] << std::endl;
    }
    for (int i = 0; i < 5; i++) {
        delete ptrArray[i];
    }
    return 0;
}

在这个例子中,首先声明了一个包含 5 个 int 类型指针的数组 ptrArray。然后通过循环动态地为每个指针分配内存,并赋予相应的值。最后,记得要释放动态分配的内存,以避免内存泄漏。

指针数组的应用场景

  1. 处理字符串数组 在 C++ 中,字符串通常用字符数组来表示,而一个字符串数组就可以用指针数组来方便地管理。例如:
#include <iostream>
int main() {
    const char *strArray[3] = {"Hello", "World", "C++"};
    for (int i = 0; i < 3; i++) {
        std::cout << "strArray[" << i << "]: " << strArray[i] << std::endl;
    }
    return 0;
}

这里 strArray 是一个指针数组,每个元素都是一个指向 const char 类型的指针,实际上就是指向字符串常量的起始地址。通过遍历指针数组,我们可以轻松输出每个字符串。

  1. 实现二维数组的灵活访问 指针数组可以模拟二维数组,并且在某些情况下提供更灵活的内存管理方式。例如,我们要创建一个二维数组,其每一行的长度可以不同:
#include <iostream>
int main() {
    int *twoDArray[3];
    twoDArray[0] = new int[2];
    twoDArray[1] = new int[3];
    twoDArray[2] = new int[4];
    int value = 1;
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < i + 2; j++) {
            twoDArray[i][j] = value++;
        }
    }
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < i + 2; j++) {
            std::cout << twoDArray[i][j] << " ";
        }
        std::cout << std::endl;
    }
    for (int i = 0; i < 3; i++) {
        delete[] twoDArray[i];
    }
    return 0;
}

在上述代码中,twoDArray 是一个指针数组,每个指针指向一个动态分配的一维数组。通过这种方式,我们可以创建一个行长度不同的二维数组,并对其进行初始化和访问。最后,别忘了释放动态分配的内存。

指向指针的指针

指向指针的指针的概念

指向指针的指针,也称为二级指针,是一个指针,它存储的是另一个指针的地址。也就是说,它指向的对象本身也是一个指针。

声明指向指针的指针的一般形式为:数据类型 **指针名。例如,int **doublePtr; 这里 doublePtr 是一个指向 int 类型指针的指针。

指向指针的指针的初始化和使用

  1. 初始化过程 首先需要有一个普通指针,然后让指向指针的指针指向这个普通指针。例如:
#include <iostream>
int main() {
    int num = 10;
    int *ptr = &num;
    int **doublePtr = &ptr;
    std::cout << "num的值: " << num << std::endl;
    std::cout << "通过ptr访问num的值: " << *ptr << std::endl;
    std::cout << "通过doublePtr访问num的值: " << **doublePtr << std::endl;
    return 0;
}

在这段代码中,num 是一个 int 型变量,ptr 是指向 num 的指针,而 doublePtr 是指向 ptr 的指针。通过 *ptr 可以访问 num 的值,通过 **doublePtr 同样可以访问 num 的值。

  1. 动态内存分配下的使用 当涉及到动态内存分配时,指向指针的指针也有重要的应用。比如,动态分配一个 int 类型的指针,然后让指向指针的指针指向它:
#include <iostream>
int main() {
    int **doublePtr;
    int *ptr = new int(20);
    doublePtr = &ptr;
    std::cout << "通过doublePtr访问的值: " << **doublePtr << std::endl;
    delete ptr;
    return 0;
}

在这个例子中,首先动态分配了一个 int 类型的变量,并让 ptr 指向它。然后将 ptr 的地址赋给 doublePtr。通过 **doublePtr 可以访问动态分配的 int 变量的值。最后,记得释放动态分配的内存。

指向指针的指针在函数参数中的应用

  1. 传递指针数组作为参数 当函数需要接收一个指针数组作为参数时,就可以使用指向指针的指针。例如,有一个函数要打印指针数组中每个指针所指向的值:
#include <iostream>
void printValues(int **ptrArray, int size) {
    for (int i = 0; i < size; i++) {
        std::cout << "ptrArray[" << i << "] 指向的值为: " << *ptrArray[i] << std::endl;
    }
}
int main() {
    int num1 = 10;
    int num2 = 20;
    int *ptrArray[2] = {&num1, &num2};
    printValues(ptrArray, 2);
    return 0;
}

在上述代码中,printValues 函数的参数是一个指向 int 类型指针的指针 ptrArray 和数组大小 size。在 main 函数中,创建了一个指针数组 ptrArray 并传递给 printValues 函数。在函数内部,通过 *ptrArray[i] 来访问每个指针所指向的值并输出。

  1. 动态内存管理与函数参数 当函数需要动态分配内存并通过指向指针的指针返回结果时,也会用到这种技术。例如,有一个函数要动态分配一个 int 类型的数组,并通过指向指针的指针返回这个数组的指针:
#include <iostream>
void allocateArray(int **result, int size) {
    *result = new int[size];
    for (int i = 0; i < size; i++) {
        (*result)[i] = i * 10;
    }
}
int main() {
    int *ptr;
    allocateArray(&ptr, 5);
    for (int i = 0; i < 5; i++) {
        std::cout << "ptr[" << i << "]: " << ptr[i] << std::endl;
    }
    delete[] ptr;
    return 0;
}

在这个例子中,allocateArray 函数的参数是一个指向 int 类型指针的指针 result 和数组大小 size。在函数内部,动态分配了一个 int 类型的数组,并通过 *result 来操作这个数组。在 main 函数中,通过 &ptrptr 的地址传递给 allocateArray 函数,从而使得 ptr 可以接收动态分配的数组的指针。最后,记得释放动态分配的内存。

指针数组与指向指针的指针的对比

数据结构特点

  1. 指针数组 指针数组是一个数组,其元素为指针。它可以看作是一种线性的数据结构,每个元素存储一个内存地址。例如,在处理字符串数组时,指针数组中的每个元素指向一个字符串的起始地址,方便对多个字符串进行管理和操作。指针数组在内存中的布局是连续的,每个元素占用相同大小的内存空间(指针大小,通常在 32 位系统为 4 字节,64 位系统为 8 字节)。

  2. 指向指针的指针 指向指针的指针是一个二级指针,它指向的对象是另一个指针。它更像是一种层次化的数据结构,通过两次间接寻址来访问最终的数据。例如,当我们需要在函数间传递指针数组时,就可以使用指向指针的指针来接收这个指针数组的地址。它的内存布局相对更复杂一些,需要考虑多级指针所指向的地址。

应用场景差异

  1. 指针数组的特定场景

    • 字符串处理:如前文所述,指针数组非常适合处理字符串数组。因为每个字符串可以看作是一个字符数组,而指针数组中的指针可以方便地指向每个字符串的起始位置,使得对字符串的操作,如排序、查找等更加高效。
    • 二维数组模拟:在需要创建行长度不同的二维数组时,指针数组是一种很好的选择。通过动态分配每个指针所指向的一维数组,可以灵活地控制二维数组的形状。
  2. 指向指针的指针的特定场景

    • 函数参数传递:当函数需要接收一个指针数组作为参数时,使用指向指针的指针作为函数参数可以方便地传递指针数组的地址。同时,当函数需要动态分配内存并返回一个指针时,通过指向指针的指针作为函数参数可以将动态分配的指针返回给调用者。
    • 链表结构扩展:在一些复杂的链表结构中,如双向链表或多级链表,指向指针的指针可以用于更灵活地管理链表节点的指针。例如,在双向链表的插入或删除操作中,可能会涉及到修改指针的指针,以正确地维护链表的结构。

内存管理差异

  1. 指针数组的内存管理

    • 如果指针数组中的指针指向的是静态分配的变量,那么不需要额外的内存释放操作,因为这些变量的生命周期由其作用域决定。例如,int num1 = 10; int num2 = 20; int *ptrArray[2] = {&num1, &num2}; 这里 num1num2 是静态分配的,在其作用域结束时会自动销毁。
    • 当指针数组中的指针指向动态分配的内存时,需要手动释放这些内存。例如,int *ptrArray[3]; for (int i = 0; i < 3; i++) { ptrArray[i] = new int(i * 10); } 之后需要在适当的时候通过循环 for (int i = 0; i < 3; i++) { delete ptrArray[i]; } 来释放内存,以避免内存泄漏。
  2. 指向指针的指针的内存管理

    • 如果指向指针的指针指向的是静态分配的指针,同样不需要额外的内存释放操作,因为静态分配的指针在其作用域结束时会自动销毁。例如,int num = 10; int *ptr = &num; int **doublePtr = &ptr; 这里 ptr 是静态分配的,在其作用域结束时会自动销毁。
    • 当指向指针的指针涉及到动态分配的指针时,需要更加小心地管理内存。例如,int **doublePtr; int *ptr = new int(20); doublePtr = &ptr; 最后需要先 delete ptr; 来释放动态分配的 int 变量,而 doublePtr 本身如果是静态分配的,不需要额外的内存释放操作,如果 doublePtr 也是动态分配的(例如 int **doublePtr = new int*; *doublePtr = new int(20);),则还需要 delete doublePtr; 来释放 doublePtr 本身的内存。

指针数组和指向指针的指针的实际案例

案例一:字符串排序

  1. 使用指针数组实现字符串排序 我们可以利用指针数组来实现字符串的排序。下面是一个简单的实现,使用冒泡排序算法对字符串数组进行排序:
#include <iostream>
#include <cstring>
void swap(char **a, char **b) {
    char *temp = *a;
    *a = *b;
    *b = temp;
}
void bubbleSort(char **strArray, int size) {
    for (int i = 0; i < size - 1; i++) {
        for (int j = 0; j < size - i - 1; j++) {
            if (std::strcmp(strArray[j], strArray[j + 1]) > 0) {
                swap(&strArray[j], &strArray[j + 1]);
            }
        }
    }
}
int main() {
    const char *strArray[4] = {"banana", "apple", "cherry", "date"};
    bubbleSort(const_cast<char**>(strArray), 4);
    for (int i = 0; i < 4; i++) {
        std::cout << strArray[i] << std::endl;
    }
    return 0;
}

在这段代码中,strArray 是一个指针数组,每个元素指向一个字符串常量。bubbleSort 函数使用冒泡排序算法对指针数组进行排序,swap 函数用于交换两个指针。注意,由于 strArray 初始化为指向 const char 类型的指针,在传递给 bubbleSort 函数时需要使用 const_cast 去掉 const 限定,这只是为了简化示例,在实际应用中应尽量避免这种操作,如果可能,应使用更合适的方式来处理字符串排序。

  1. 使用指向指针的指针优化字符串排序 我们可以将指针数组的地址作为参数传递给函数,即使用指向指针的指针来优化上述代码。这样可以减少指针数组的复制开销,特别是在处理大型字符串数组时:
#include <iostream>
#include <cstring>
void swap(char **a, char **b) {
    char *temp = *a;
    *a = *b;
    *b = temp;
}
void bubbleSort(char ***strArrayPtr, int size) {
    char **strArray = *strArrayPtr;
    for (int i = 0; i < size - 1; i++) {
        for (int j = 0; j < size - i - 1; j++) {
            if (std::strcmp(strArray[j], strArray[j + 1]) > 0) {
                swap(&strArray[j], &strArray[j + 1]);
            }
        }
    }
}
int main() {
    const char *strArray[4] = {"banana", "apple", "cherry", "date"};
    bubbleSort(&strArray, 4);
    for (int i = 0; i < 4; i++) {
        std::cout << strArray[i] << std::endl;
    }
    return 0;
}

在这个优化版本中,bubbleSort 函数的参数变为 char ***,即指向指针数组的指针。在函数内部,通过 *strArrayPtr 获取实际的指针数组。这样在传递参数时,只传递了指针数组的地址,而不是整个指针数组,提高了效率。

案例二:矩阵运算

  1. 使用指针数组实现矩阵乘法 矩阵可以用二维数组表示,而指针数组可以模拟二维数组来实现矩阵乘法。以下是一个简单的矩阵乘法实现:
#include <iostream>
void multiplyMatrices(int **matrixA, int **matrixB, int **result, int rowsA, int colsA, int colsB) {
    for (int i = 0; i < rowsA; i++) {
        for (int j = 0; j < colsB; j++) {
            result[i][j] = 0;
            for (int k = 0; k < colsA; k++) {
                result[i][j] += matrixA[i][k] * matrixB[k][j];
            }
        }
    }
}
int main() {
    int rowsA = 2, colsA = 3, colsB = 2;
    int **matrixA = new int*[rowsA];
    int **matrixB = new int*[colsA];
    int **result = new int*[rowsA];
    for (int i = 0; i < rowsA; i++) {
        matrixA[i] = new int[colsA];
        result[i] = new int[colsB];
    }
    for (int i = 0; i < colsA; i++) {
        matrixB[i] = new int[colsB];
    }
    int value = 1;
    for (int i = 0; i < rowsA; i++) {
        for (int j = 0; j < colsA; j++) {
            matrixA[i][j] = value++;
        }
    }
    value = 1;
    for (int i = 0; i < colsA; i++) {
        for (int j = 0; j < colsB; j++) {
            matrixB[i][j] = value++;
        }
    }
    multiplyMatrices(matrixA, matrixB, result, rowsA, colsA, colsB);
    std::cout << "矩阵A: " << std::endl;
    for (int i = 0; i < rowsA; i++) {
        for (int j = 0; j < colsA; j++) {
            std::cout << matrixA[i][j] << " ";
        }
        std::cout << std::endl;
    }
    std::cout << "矩阵B: " << std::endl;
    for (int i = 0; i < colsA; i++) {
        for (int j = 0; j < colsB; j++) {
            std::cout << matrixB[i][j] << " ";
        }
        std::cout << std::endl;
    }
    std::cout << "结果矩阵: " << std::endl;
    for (int i = 0; i < rowsA; i++) {
        for (int j = 0; j < colsB; j++) {
            std::cout << result[i][j] << " ";
        }
        std::cout << std::endl;
    }
    for (int i = 0; i < rowsA; i++) {
        delete[] matrixA[i];
        delete[] result[i];
    }
    for (int i = 0; i < colsA; i++) {
        delete[] matrixB[i];
    }
    delete[] matrixA;
    delete[] matrixB;
    delete[] result;
    return 0;
}

在上述代码中,matrixAmatrixBresult 都是指针数组模拟的二维数组。multiplyMatrices 函数实现了矩阵乘法的核心逻辑。最后,记得释放动态分配的内存。

  1. 使用指向指针的指针传递矩阵 我们可以使用指向指针的指针来传递矩阵,这样在函数调用时可以更方便地管理矩阵的内存。例如,我们可以修改上述代码中的函数参数,使其接收指向指针数组的指针:
#include <iostream>
void multiplyMatrices(int ***matrixAPtr, int ***matrixBPtr, int ***resultPtr, int rowsA, int colsA, int colsB) {
    int **matrixA = *matrixAPtr;
    int **matrixB = *matrixBPtr;
    int **result = *resultPtr;
    for (int i = 0; i < rowsA; i++) {
        for (int j = 0; j < colsB; j++) {
            result[i][j] = 0;
            for (int k = 0; k < colsA; k++) {
                result[i][j] += matrixA[i][k] * matrixB[k][j];
            }
        }
    }
}
int main() {
    int rowsA = 2, colsA = 3, colsB = 2;
    int **matrixA = new int*[rowsA];
    int **matrixB = new int*[colsA];
    int **result = new int*[rowsA];
    for (int i = 0; i < rowsA; i++) {
        matrixA[i] = new int[colsA];
        result[i] = new int[colsB];
    }
    for (int i = 0; i < colsA; i++) {
        matrixB[i] = new int[colsB];
    }
    int value = 1;
    for (int i = 0; i < rowsA; i++) {
        for (int j = 0; j < colsA; j++) {
            matrixA[i][j] = value++;
        }
    }
    value = 1;
    for (int i = 0; i < colsA; i++) {
        for (int j = 0; j < colsB; j++) {
            matrixB[i][j] = value++;
        }
    }
    multiplyMatrices(&matrixA, &matrixB, &result, rowsA, colsA, colsB);
    std::cout << "矩阵A: " << std::endl;
    for (int i = 0; i < rowsA; i++) {
        for (int j = 0; j < colsA; j++) {
            std::cout << matrixA[i][j] << " ";
        }
        std::cout << std::endl;
    }
    std::cout << "矩阵B: " << std::endl;
    for (int i = 0; i < colsA; i++) {
        for (int j = 0; j < colsB; j++) {
            std::cout << matrixB[i][j] << " ";
        }
        std::cout << std::endl;
    }
    std::cout << "结果矩阵: " << std::endl;
    for (int i = 0; i < rowsA; i++) {
        for (int j = 0; j < colsB; j++) {
            std::cout << result[i][j] << " ";
        }
        std::cout << std::endl;
    }
    for (int i = 0; i < rowsA; i++) {
        delete[] matrixA[i];
        delete[] result[i];
    }
    for (int i = 0; i < colsA; i++) {
        delete[] matrixB[i];
    }
    delete[] matrixA;
    delete[] matrixB;
    delete[] result;
    return 0;
}

在这个版本中,multiplyMatrices 函数的参数变为指向指针数组的指针。在函数内部,通过解引用获取实际的指针数组进行矩阵乘法运算。这种方式在处理大型矩阵时,对于内存管理和函数调用的效率都有一定的提升。

通过以上对 C++ 指针数组和指向指针的指针的详细介绍、对比以及实际案例分析,希望读者能够对这两个重要的概念有更深入的理解和掌握,从而在实际编程中能够灵活运用它们来解决各种问题。