一. 再探数组:数组与指针

1.1 数组的连续性

一维数组在内存的空间地址是连续的。二维数组在内存的空间地址连续吗?

不同的编程语言的内存管理是不一样的,在 C++ 中,二维数组是连续分布的

// 打印一维数组的地址
#include <stdio.h>

int main(void)
{
    char arr[5] = {'a' , 'b' , 'c' , 'd' , 'e'};
    for(int i = 0; i < 5; ++i)
    {
        printf("%p " , &arr[i]);
    }
    printf("\n");
    return 0;
}
// 打印二维数组的地址
#include <stdio.h>

int main(void)
{
    int arr[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };
    for(int i = 0; i < 3; ++i)
    {
        for(int j = 0; j < 3; ++j)
        {
            printf("%p " , &arr[i][j]);
        }
        printf("\n");   
    }
    return 0;
}

1.2 指针常量与常量指针

指针常量,指的是指针自身的值是一个常量,不可改变,始终指向同一个地址,但这个地址里的值可以更改。在定义的时候必须初始化

// 地址不可更改
#include <stdio.h>

int main(void)
{
    int num_1 = 1;
    int num_2 = 2;
    int* const int_pointer = &num_1;
    int_pointer = &num_2;
}
// 指向的值可以更改
#include <stdio.h>

int main(void)
{
    int num_1 = 1;
    int* const int_pointer = &num_1;
    *int_pointer = 2;
    printf("%d\n" , num_1);
}

常量指针,指的是指针自身的值可以更改,可以指向不同的地址,但地址里的值是不可改变的。

// 指向的值不可以更改
#include <stdio.h>

int main(void)
{
    int num_1 = 1;
    const int* int_pointer = &num_1;
    *int_pointer = 2;
}
// 地址可以更改
#include <stdio.h>

int main(void)
{
    int num_1 = 1;
    int num_2 = 2;
    const int* int_pointer = &num_1;
    int_pointer = &num_2;
    printf("%d\n" , *int_pointer);
}

常量指针常量,也就是指针自身的值不可以更改,指针指向的值也不可以更改

#include <stdio.h>

int main(void)
{
    int num_1 = 1;
    int num_2 = 2;
    const int* const int_pointer = &num_1;
    int_pointer = &num_2;
    *int_pointer = 2;
}

1.3 指针运算

指针 + 整数,指针 - 整数

标准定义这种形式只能用于指向数组中某个元素的指针,运算后的结果也是指针;这种形式也适用于 new 函数动态分配的内存

  1. 数组中的元素存储在连续内存中,后面元素的地址大于前面元素的地址。
  2. 因此,对一个指针加 1 使它指向数组中下一个元素,加 5 使它向右移动 5 个元素的位置,依次类推。
  3. 注意,对指针执行加法或减法运算之后,如果指针所指的位置在数组第一个元素的前面或在数组最后一个元素的后面,那么其效果是未定义的。
#include <stdio.h>

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    int* int_pointer_0 = &arr[0];
    int* int_pointer_1 = int_pointer_0 + 1;
    int* int_pointer_2 = int_pointer_0 + 2;
    int* int_pointer_3 = int_pointer_0 + 3;
    int* int_pointer_4 = int_pointer_0 + 4;
    
    printf("%d\n" , *int_pointer_0);
    printf("%d\n" , *int_pointer_1);
    printf("%d\n" , *int_pointer_2);
    printf("%d\n" , *int_pointer_3);
    printf("%d\n" , *int_pointer_4);
   
    return 0;
}
#include <stdio.h>
 
int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    int* int_pointer_0 = &arr[0];
    int* int_pointer_1 = int_pointer_0 - 1;
    int* int_pointer_2 = int_pointer_0 + 5;

    printf("%d\n" , *int_pointer_1);
    printf("%d\n" , *int_pointer_2);
    return 0;
}

实际上,绝大多数编译器都不会检查指针表达式的结果是否位于合法的边界之内,因此程序员应该负起责任,确保这一点。

指针越界和指向未知值的指针是两个常见的错误根源。使用指针运算时,必须非常小心,确信运算的结果将指向有意义的东西。

1.4 数组名

数组名的值什么?

数组名的值是指向数组第一个元素的指针常量,但这个规则只有两个例外

  1. sizeof 函数返回整个数组所占用的字节,而不是一个指针所占用的字节
  2. 单目操作符 & 返回一个指向数组的指针,而不是一个指向数组第一个元素的指针的指针
#include <stdio.h>

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    printf("%p\n" , arr);
    printf("%p\n" , &arr[0]);
    printf("==============\n");
    printf("%d\n" , *arr);
    printf("%d\n" , arr[0]);
    printf("==============\n");    
    printf("%p\n" , arr+1);
    printf("%p\n" , &arr[0]+1);
    printf("%p\n" , &arr[1]);
    printf("==============\n");  
    printf("%d\n" , *(arr+1));
    printf("%d\n" , arr[1]);
    printf("==============\n");  
    printf("%ld\n" , sizeof(arr));
    printf("%ld\n" , sizeof(&arr[0]));
    printf("==============\n");  
    printf("%p\n" , &arr);
    printf("%p\n" , arr);
    printf("%p\n" , &arr + 1);
    printf("%p\n" , arr + 1);
    return 0;
}
array[i] 等价于 *(array + i)

根据数组名值的定义,一维数组名的值的类型是“指向元素类型的指针”,而二维数组名的值的类型是“指向一个数组的指针”

#include <stdio.h>

int main(void)
{

    int arr[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };

    printf("%p\n" , arr);
    printf("%p\n" , arr + 1);
    printf("%p\n" , arr + 2);
    printf("==============\n");
    printf("%p\n" , *(arr + 1));
    printf("%p\n" , *(arr + 1) + 1);
    printf("%d\n" , *(*(arr + 1) + 1));
    printf("%d\n" , arr[1][1]);
    return 0;
}
array[i][j] 等价于 *(*(array + i) + j)

1.5 使用 new 创建数组

// 创建一维数组
#include <stdio.h>

int main(void)
{
    int* arr = new int[5]{1, 2, 3, 4, 5};
    for(int i = 0; i < 5; ++i)
    {
        printf("%d " , arr[i]);
    }
    printf("\n");
    delete []arr;
    return 0;
}
// 创建二维数组,方法1
#include <stdio.h>

int main(void)
{
    int **arr = new int*[3];
    for(int i = 0; i < 3; ++i)
    {
        arr[i] = new int[3];
    }

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

    for(int i = 0; i < 3; ++i)
    {
        delete [] arr[i];
    } 
    delete [] arr;
    return 0;
}
// 创建二维数组,方法2
#include <stdio.h>

int main(void)
{
    int (*arr)[3] = new int[3][3]{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };

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

    delete [] arr;
    return 0;
}

注意 int(*array)[] 和 int *array[] 的区别,第一个是数组指针,是一个指针。第二个是指针数组,是一个数组。

二者都可以表示二维数组,是因为有数组名是指向数组第一个元素的指针这一重要性质。

作业

  1. 将讲义中的程序自己敲一遍,理解程序为何会这样运行。全都是重点。