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

C++中数组与指针的本质区别及应用场景

2024-09-106.3k 阅读

C++ 中数组与指针的本质区别

数组的本质

  1. 数组是一块连续的内存区域 在 C++ 中,数组是由相同类型元素组成的集合,这些元素在内存中是连续存储的。例如,定义一个整型数组 int arr[5];,系统会在内存中分配一块连续的空间,足以容纳 5 个 int 类型的数据。假设 int 类型占 4 个字节,那么这块内存空间大小就是 5 * 4 = 20 字节。
#include <iostream>
int main() {
    int arr[5];
    std::cout << "Size of arr: " << sizeof(arr) << " bytes" << std::endl;
    return 0;
}

上述代码通过 sizeof(arr) 输出数组 arr 所占用的内存大小,这里会输出 20 字节(假设 int 占 4 字节)。 2. 数组名的特殊性 数组名在大多数情况下代表数组的首地址,但它并非指针。例如,当使用 &arr 获取数组的地址时,得到的是整个数组的起始地址,它和 &arr[0] 虽然值相同,但意义不同。&arr 的类型是 int(*)[5],即指向包含 5 个 int 类型元素的数组的指针;而 &arr[0] 的类型是 int*,即指向 int 类型的指针。

#include <iostream>
int main() {
    int arr[5];
    std::cout << "Address of arr: " << &arr << std::endl;
    std::cout << "Address of arr[0]: " << &arr[0] << std::endl;
    std::cout << "Type of &arr: " << typeid(&arr).name() << std::endl;
    std::cout << "Type of &arr[0]: " << typeid(&arr[0]).name() << std::endl;
    return 0;
}

这段代码输出数组 arr 的地址和数组第一个元素 arr[0] 的地址,并通过 typeid 获取它们的类型。可以看到输出的地址值相同,但类型不同。 3. 数组的内存布局 数组的内存布局是按照元素顺序依次排列的。例如,对于 int arr[5] = {1, 2, 3, 4, 5};,在内存中 arr[0] 存储 1,arr[1] 紧挨着 arr[0] 存储 2,以此类推。这种连续的内存布局使得通过下标访问数组元素非常高效,因为计算元素地址的公式为:&arr[i] = &arr[0] + i * sizeof(arr[0])

#include <iostream>
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    for (int i = 0; i < 5; ++i) {
        std::cout << "Address of arr[" << i << "]: " << &arr[i] << std::endl;
    }
    return 0;
}

上述代码输出数组每个元素的地址,可以清晰看到地址是连续递增的。

指针的本质

  1. 指针是一个变量,存储内存地址 指针是一种特殊的变量,它存储的是另一个变量的内存地址。例如,int* ptr; 定义了一个 int 类型的指针 ptr。可以通过 ptr = &num; 将变量 num 的地址赋给指针 ptr。指针本身也占用一定的内存空间,在 32 位系统中,指针通常占 4 字节;在 64 位系统中,指针通常占 8 字节。
#include <iostream>
int main() {
    int num = 10;
    int* ptr = &num;
    std::cout << "Value of num: " << num << std::endl;
    std::cout << "Address of num: " << &num << std::endl;
    std::cout << "Value of ptr: " << ptr << std::endl;
    std::cout << "Size of ptr: " << sizeof(ptr) << " bytes" << std::endl;
    return 0;
}

上述代码定义了一个 int 变量 num 和一个指向 int 的指针 ptr,输出 num 的值、地址,以及 ptr 的值(即 num 的地址)和 ptr 本身占用的内存大小。 2. 指针的类型 指针的类型决定了它所指向的数据类型,以及解引用操作时访问的内存大小。例如,int* 类型的指针解引用时会访问 4 字节(假设 int 占 4 字节)的数据,char* 类型的指针解引用时会访问 1 字节的数据。

#include <iostream>
int main() {
    int num = 10;
    int* intPtr = &num;
    char ch = 'a';
    char* charPtr = &ch;
    std::cout << "Value of *intPtr: " << *intPtr << std::endl;
    std::cout << "Value of *charPtr: " << *charPtr << std::endl;
    return 0;
}

这段代码展示了不同类型指针的解引用操作,分别输出 int 指针和 char 指针所指向的值。 3. 指针的动态内存分配与释放 指针常用于动态内存分配,通过 new 操作符在堆上分配内存,并返回指向该内存的指针。例如,int* dynamicArr = new int[5]; 在堆上分配了一块能容纳 5 个 int 类型数据的内存,并将其起始地址赋给 dynamicArr。使用完动态分配的内存后,需要通过 delete 操作符释放内存,如 delete[] dynamicArr;,否则会导致内存泄漏。

#include <iostream>
int main() {
    int* dynamicArr = new int[5];
    for (int i = 0; i < 5; ++i) {
        dynamicArr[i] = i * 2;
    }
    for (int i = 0; i < 5; ++i) {
        std::cout << "dynamicArr[" << i << "]: " << dynamicArr[i] << std::endl;
    }
    delete[] dynamicArr;
    return 0;
}

上述代码在堆上分配了一个整型数组,对其进行赋值并输出,最后释放内存。

数组与指针在 sizeof 操作符上的区别

  1. 数组的 sizeof 对于数组,sizeof 操作符返回整个数组占用的内存大小,即数组元素个数乘以单个元素的大小。例如,对于 int arr[5];sizeof(arr) 返回 5 * sizeof(int)。这是因为数组名在 sizeof 操作符中不会被转换为指针。
#include <iostream>
int main() {
    int arr[5];
    std::cout << "Size of arr: " << sizeof(arr) << " bytes" << std::endl;
    return 0;
}
  1. 指针的 sizeof 对于指针,sizeof 操作符返回指针本身占用的内存大小。在 32 位系统中,无论指针指向何种类型的数据,sizeof 指针通常返回 4 字节;在 64 位系统中,通常返回 8 字节。例如,对于 int* ptr;sizeof(ptr) 返回指针 ptr 本身占用的字节数。
#include <iostream>
int main() {
    int* ptr;
    std::cout << "Size of ptr: " << sizeof(ptr) << " bytes" << std::endl;
    return 0;
}
  1. 示例对比
#include <iostream>
int main() {
    int arr[5];
    int* ptr = arr;
    std::cout << "Size of arr: " << sizeof(arr) << " bytes" << std::endl;
    std::cout << "Size of ptr: " << sizeof(ptr) << " bytes" << std::endl;
    return 0;
}

上述代码中,数组 arr 和指针 ptr 都与数组相关,但 sizeof(arr) 返回数组占用的内存大小,sizeof(ptr) 返回指针本身占用的内存大小,两者结果不同。

数组与指针在函数参数传递上的区别

  1. 数组作为函数参数 当数组作为函数参数传递时,数组名会退化为指针。例如,函数 void func(int arr[]) 实际上等价于 void func(int* arr)。在函数内部,无法通过 sizeof 操作符获取数组的真实大小,因为此时数组名已经是指针。
#include <iostream>
void func(int arr[]) {
    std::cout << "Size of arr in func: " << sizeof(arr) << " bytes" << std::endl;
}
int main() {
    int arr[5];
    std::cout << "Size of arr in main: " << sizeof(arr) << " bytes" << std::endl;
    func(arr);
    return 0;
}

在上述代码中,在 main 函数中 sizeof(arr) 返回数组真实大小,而在 func 函数中 sizeof(arr) 返回指针大小。 2. 指针作为函数参数 指针作为函数参数传递时,传递的是指针的值(即所指向内存的地址)。函数可以通过该指针访问和修改所指向的数据。例如:

#include <iostream>
void modify(int* ptr) {
    *ptr = 20;
}
int main() {
    int num = 10;
    std::cout << "Value of num before modify: " << num << std::endl;
    modify(&num);
    std::cout << "Value of num after modify: " << num << std::endl;
    return 0;
}

上述代码通过指针作为函数参数,在函数 modify 中修改了 main 函数中 num 的值。 3. 传递数组大小的方式 由于数组作为函数参数时无法获取其真实大小,通常需要额外传递数组的大小。例如:

#include <iostream>
void printArray(int arr[], int size) {
    for (int i = 0; i < size; ++i) {
        std::cout << "arr[" << i << "]: " << arr[i] << std::endl;
    }
}
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printArray(arr, 5);
    return 0;
}

在上述代码中,printArray 函数通过额外传递的 size 参数来遍历数组。

C++ 中数组与指针的应用场景

数组的应用场景

  1. 固定大小的数据集合 当需要存储固定数量的相同类型数据,并且这些数据的数量在编译时就确定时,数组是很好的选择。例如,一个班级学生的成绩存储,假设班级人数固定为 50 人,可以定义 int scores[50]; 来存储成绩。
#include <iostream>
int main() {
    int scores[50];
    for (int i = 0; i < 50; ++i) {
        scores[i] = i * 2;
    }
    for (int i = 0; i < 50; ++i) {
        std::cout << "Score of student " << i << ": " << scores[i] << std::endl;
    }
    return 0;
}
  1. 栈上的高效存储 数组存储在栈上,访问速度快。对于一些需要频繁访问且数据量不大的场景,数组能提供高效的存储和访问。例如,一个小型游戏中的简单地图数据,地图大小固定且不需要动态改变,可以使用数组存储地图信息。
#include <iostream>
const int MAP_SIZE = 10;
char map[MAP_SIZE][MAP_SIZE];
int main() {
    for (int i = 0; i < MAP_SIZE; ++i) {
        for (int j = 0; j < MAP_SIZE; ++j) {
            map[i][j] = '.';
        }
    }
    map[5][5] = 'X';
    for (int i = 0; i < MAP_SIZE; ++i) {
        for (int j = 0; j < MAP_SIZE; ++j) {
            std::cout << map[i][j];
        }
        std::cout << std::endl;
    }
    return 0;
}
  1. 多维数组的应用 多维数组常用于表示矩阵、表格等数据结构。例如,一个二维数组可以表示一个棋盘,三维数组可以表示一个立体空间的网格数据。
#include <iostream>
const int ROWS = 8;
const int COLS = 8;
bool chessBoard[ROWS][COLS];
int main() {
    for (int i = 0; i < ROWS; ++i) {
        for (int j = 0; j < COLS; ++j) {
            chessBoard[i][j] = (i + j) % 2 == 0;
        }
    }
    for (int i = 0; i < ROWS; ++i) {
        for (int j = 0; j < COLS; ++j) {
            std::cout << (chessBoard[i][j]? "W " : "B ");
        }
        std::cout << std::endl;
    }
    return 0;
}

上述代码使用二维数组表示棋盘,并对棋盘格子进行黑白上色。

指针的应用场景

  1. 动态内存分配 当需要在运行时根据实际需求分配内存时,指针是必不可少的。例如,在一个图像处理程序中,图像的大小可能根据用户输入而变化,此时可以使用指针动态分配内存来存储图像数据。
#include <iostream>
int main() {
    int width, height;
    std::cout << "Enter width and height of the image: ";
    std::cin >> width >> height;
    int* imageData = new int[width * height];
    for (int i = 0; i < width * height; ++i) {
        imageData[i] = i;
    }
    // 处理图像数据
    delete[] imageData;
    return 0;
}
  1. 链表、树等动态数据结构 指针常用于构建链表、树等动态数据结构。这些数据结构的节点通过指针相互连接,能够灵活地插入、删除节点。例如,一个简单的单链表:
#include <iostream>
struct Node {
    int data;
    Node* next;
    Node(int value) : data(value), next(nullptr) {}
};
int main() {
    Node* head = new Node(1);
    Node* node2 = new Node(2);
    Node* node3 = new Node(3);
    head->next = node2;
    node2->next = node3;
    Node* current = head;
    while (current) {
        std::cout << current->data << " ";
        current = current->next;
    }
    // 释放内存
    current = head;
    while (current) {
        Node* temp = current;
        current = current->next;
        delete temp;
    }
    return 0;
}

上述代码构建了一个简单的单链表,并遍历输出链表节点的值,最后释放链表占用的内存。 3. 函数指针 函数指针用于指向函数,在需要根据不同条件调用不同函数的场景中非常有用。例如,在一个计算器程序中,可以根据用户输入的操作符,通过函数指针调用相应的计算函数。

#include <iostream>
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }
int main() {
    int num1, num2;
    char op;
    std::cout << "Enter an expression (e.g., 3 + 5): ";
    std::cin >> num1 >> op >> num2;
    int (*func)(int, int);
    switch (op) {
    case '+':
        func = add;
        break;
    case '-':
        func = subtract;
        break;
    case '*':
        func = multiply;
        break;
    case '/':
        func = divide;
        break;
    default:
        std::cout << "Invalid operator" << std::endl;
        return 1;
    }
    std::cout << "Result: " << func(num1, num2) << std::endl;
    return 0;
}

上述代码通过函数指针根据用户输入的操作符调用相应的计算函数。

混合使用数组与指针的场景

  1. 动态数组的模拟 虽然 C++ 有 std::vector 等动态数组容器,但在一些情况下,也可以通过指针和动态内存分配来模拟动态数组。例如,实现一个简单的动态整型数组类:
#include <iostream>
class DynamicArray {
private:
    int* data;
    int size;
    int capacity;
public:
    DynamicArray() : size(0), capacity(10) {
        data = new int[capacity];
    }
    ~DynamicArray() {
        delete[] data;
    }
    void push_back(int value) {
        if (size == capacity) {
            capacity *= 2;
            int* newData = new int[capacity];
            for (int i = 0; i < size; ++i) {
                newData[i] = data[i];
            }
            delete[] data;
            data = newData;
        }
        data[size++] = value;
    }
    int operator[](int index) {
        if (index < 0 || index >= size) {
            std::cerr << "Index out of range" << std::endl;
            return -1;
        }
        return data[index];
    }
};
int main() {
    DynamicArray arr;
    arr.push_back(1);
    arr.push_back(2);
    arr.push_back(3);
    std::cout << "arr[1]: " << arr[1] << std::endl;
    return 0;
}

在这个类中,使用指针 data 动态分配内存来存储数组元素,通过 push_back 方法实现动态增长。 2. 数组指针与指针数组 数组指针是指向数组的指针,指针数组是数组元素为指针的数组。例如,在处理多个字符串时,可以使用指针数组。

#include <iostream>
int main() {
    char* strings[3] = {"Hello", "World", "C++"};
    for (int i = 0; i < 3; ++i) {
        std::cout << strings[i] << std::endl;
    }
    return 0;
}

上述代码定义了一个指针数组 strings,每个元素指向一个字符串。同时,数组指针可以用于更复杂的数据结构操作,例如:

#include <iostream>
int main() {
    int arr[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
    int(*ptr)[4] = arr;
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 4; ++j) {
            std::cout << ptr[i][j] << " ";
        }
        std::cout << std::endl;
    }
    return 0;
}

这里定义了一个指向包含 4 个 int 元素的数组的指针 ptr,并通过它遍历二维数组 arr

  1. 在函数模板中的应用 在函数模板中,数组和指针的特性可以灵活应用。例如,一个通用的打印数组函数模板:
#include <iostream>
template <typename T, int size>
void printArray(T(&arr)[size]) {
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}
int main() {
    int intArr[5] = {1, 2, 3, 4, 5};
    double doubleArr[3] = {1.1, 2.2, 3.3};
    printArray(intArr);
    printArray(doubleArr);
    return 0;
}

在这个函数模板中,通过引用传递数组,能够获取数组的真实大小,从而实现对不同类型、不同大小数组的通用打印。同时,如果需要处理动态大小的数组,可以使用指针作为模板参数:

#include <iostream>
template <typename T>
void printDynamicArray(T* arr, int size) {
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}
int main() {
    int* dynamicIntArr = new int[4] {1, 2, 3, 4};
    printDynamicArray(dynamicIntArr, 4);
    delete[] dynamicIntArr;
    return 0;
}

这段代码通过指针和大小参数实现对动态分配数组的打印。

通过深入理解 C++ 中数组与指针的本质区别,并明确它们各自的应用场景以及混合使用场景,开发者能够在编程过程中根据实际需求选择最合适的数据存储和操作方式,提高程序的效率和可读性。