C/C++ 指针和数组

C/C++ 指针和数组

数组是 C 语言里一种很重要的数据结构,用来存放多个相同类型的元素。它是一种线性结构,可以按顺序访问和操作里面的元素。

1. 数组存储原理

C 数组的存储原理,可以从下面这几个角度来理解:

  1. 连续内存分配:数组的元素在内存里是挨着放的。这样用下标访问元素就很快,因为靠简单的指针算术就能算出元素地址。
  2. 内存布局:数组通常按从低地址到高地址排列,即第一个元素放在数组起始地址,最后一个元素放在结束地址。
  3. 元素大小:元素类型决定了每个元素占多大内存。比如元素类型是 int,每个元素一般占 4 个字节。
  4. 数组大小:声明数组时要指定大小,系统才好一次性给它分配够内存。
  5. 数组没有边界检查。越界访问元素可能导致内存越界,这是一种未定义行为,轻则数据错乱,重则程序崩溃。

注意: 多维数组在内存里同样是用线性连续内存来存元素的。

2. 下标与指针

在 C 语言里,数组和指针关系非常紧密。

  1. 数组名当指针用:大多数情况下,数组名会被隐式转成指向首元素的指针。比如数组 int arr[5];,arr 就可以看成指向 arr[0] 的指针。
  2. 指针加偏移量:用指针算术在数组里移动,对指针做加减就能指向不同位置。比如 arr + 2 返回的就是指向 arr[2] 的指针。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>


int main() {

    int arr[] = {10, 20, 30, 50, 50};
    // 打印 arr 存储的地址,数组首元素地址
    printf("arr 值: %ld,arr[0] 地址: %ld\n", arr, &arr[0]);
    // 下标和指针元素访问
    printf("arr[2] = %d,*(arr + 2) = %d\n", arr[2], *(arr + 2));
    

    return 0;
}

程序运行结果:

arr 值: 140732676908768,arr[0] 地址: 140732676908768
arr[2] = 30,*(arr + 2) = 30

注意: 数组名本身的指向是不允许修改的。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>


int main() {

    int arr[] = {10, 20, 30, 50, 50};
    // 下面代码报错
    // arr = NULL;

    // 可以将 arr 赋值给 p 指针,修改 p 指针指向
    int* p = arr;
    p += 1;
    *p = 200;

    printf("arr[1] = %d\n", arr[1]);


    return 0;
}

3. 数组指针类型

int arr[] = {10, 20, 30, 50, 50};

我们知道数组名 arr 表示指向首元素 arr[0] 的指针。那么对数组名再取一次地址,得到的又是什么呢?

顺着直觉想,arr 是 int* 类型,那对它取地址自然就是二级指针 int** 类型。可真的是这样吗?

int* 的步长是 4 字节,int** 的步长也是 4 字节。我们直接打印出来看看是不是这样:

#include <stdio.h>


int main() {

    int arr[] = {10, 20, 30, 50, 50};

    printf("%ld\n", &arr);
    printf("%ld\n", &arr + 1);

    return 0;
}

程序运行结果:

140732795418336
140732795418356

可以看到,步长是 20,正好等于整个数组的大小。这说明对数组名取地址并不是二级指针,而是指向整个数组的指针,也就是数组指针类型。数组指针的定义语法如下:

#include <stdio.h>


int main() {

    int arr[] = {10, 20, 30, 50, 50};

    // 1. 方式一
    int(*p1)[5] = &arr;
    printf("%ld %ld\n", (long)p1, (long)(p1+1));

    // 2. 方式二
    typedef int(ARRAY_TYPE)[5];
    ARRAY_TYPE* p2 = &arr;
    printf("%ld %ld\n", (long)p1, (long)(p2+1));

    // 3. 方式三
    typedef int(*ARRAY_POINTER)[5];
    ARRAY_POINTER p3 = &arr;
    printf("%ld %ld\n", (long)p1, (long)(p3+1));

    
    return 0;
}

程序运行结果:

140732691384032 140732691384052
140732691384032 140732691384052
140732691384032 140732691384052

4. 数组传参

在 C 语言里,数组可以作为函数参数传进去。把数组当参数传时,实际上传的是数组的指针。这种传址方式避免在函数内部再拷贝一份数组,省了内存和时间,处理大数组或频繁操作数组时尤其有用。要在函数里用数组参数,我们得指定数组的类型和大小。

示例代码:

#include <stdio.h>

// 1. 一维数组作为函数参数
void test01(int arr[5], int len)
{
    for (unsigned int i = 0; i < len; ++i)
        printf("%d ", arr[i]);
    printf("\n");
}

void test02(int arr[], int len)
{
    for (unsigned int i = 0; i < len; ++i)
        printf("%d ", arr[i]);
    printf("\n");
}

void test03(int* arr, int len)
{
    for (unsigned int i = 0; i < len; ++i)
        printf("%d ", arr[i]);
    printf("\n");
}

// 2. 二维数组作为函数参数
void test04(int arr[3][3], int len_row, int len_col)
{
    for (size_t i = 0; i < len_row; ++i)
    {
        for (size_t j = 0; j < len_col; ++j)
            printf("%d ", arr[i][j]);
    }
    printf("\n");
}

void test05(int arr[][3], int len_row, int len_col)
{
    for (size_t i = 0; i < len_row; ++i)
    {
        for (size_t j = 0; j < len_col; ++j)
            printf("%d ", arr[i][j]);
    }
    printf("\n");
}

void test06(int(*arr)[3], int len_row, int len_col)
{
    for (size_t i = 0; i < len_row; ++i)
    {
        for (size_t j = 0; j < len_col; ++j)
            printf("%d ", arr[i][j]);
    }
    printf("\n");
}


int main() {

    // 1. 一维数组作为函数参数
    int arr1[] = {10, 20, 30, 50, 50};
    test01(arr1, sizeof(arr1) / sizeof(int));
    test02(arr1, sizeof(arr1) / sizeof(int));
    test03(arr1, sizeof(arr1) / sizeof(int));

    // 2. 二维数组作为函数参数
    int arr2[][3] = {{10, 20, 30}, {40, 50, 60}, {70, 80, 90}};
    test04(arr2, sizeof(arr2)/sizeof(arr2[0]), sizeof(arr2[0])/sizeof(arr2[0][0]));
    test05(arr2, sizeof(arr2)/sizeof(arr2[0]), sizeof(arr2[0])/sizeof(arr2[0][0]));
    test06(arr2, sizeof(arr2)/sizeof(arr2[0]), sizeof(arr2[0])/sizeof(arr2[0][0]));

    return 0;
}