深入理解C指针 指针和数组

4.指针和数组

  一种常见的错误观点是“数组和指针是完全可以互换的”。尽管数组名字有时候可以当作指针来用,但数组的名字不是指针。数组表示法也可以和指针一起使用,但两者明显不同,也不一定能互换。尽管数组使用自身的名字可以返回数组地址,但名字本身不能作为赋值操作的目标。

4.1.1 一维数组

   一维数组是线性结构,用一个索引访问成员。数组索引从0开始,到声明的长度减1结束。

 对数组做sizeof操作会得到为该数组分配的字节数,要知道元素的数量,只需将数组长度除以元素长度。

4.1.2 二维数组

  二维数组使用行和列来标识数组元素,这类数组需要映射为内存中的一维地址空间。在C中这是通过行-列顺序实现的。先将数组的第一行放进内存,接着是第二行、第三行,直到最后一行。

  可以将二维数组当作数组的数组,也就是说,可以只用一个下标访问数组,得到的是对应行的指针。下面片段代码会打印每一行的地址和长度:

    int matrix[2][3] = { {1,2,3},{4,5,6} };
    for (int i = 0; i < 2; i++) {
        printf("&matrix[%d]:%p  sizeof(matrix[%d]):%d
",
            i, &matrix[i], i, sizeof(matrix[i]));
    }
    &matrix[0]:100 sizeof(matrix[0]):12
    &matrix[1]:112 sizeof(matrix[1]):12    //假设数组位于地址100处

4.2指针表示法和数组

  单独使用数组名字时会返回数组地址。可以把地址赋给指针,如下所示:

  int vector[5] = {1,2,3,4,5};

  int *pv = vector;

pv变量是指向数组第一个元素而不是指向数组本身的指针。给pv赋值是把数组的第一个元素的地址赋给pv。数组的首地址,也就是第一个字符的地址

我们可以只用数组名字,也可以对数组的第一个元素用取地址操作符,这些写法是等价的,都会返回vector的地址。
  printf("%p ",vector);
  printf("%p ",&vector[0]);

  有时候也会使用&vector这个表达式获取数组地址,不同于其它表示法,这么做返回的是整个数组的指针,其他两种方法得到是整数指针。

我们可以把数组下标用在指针上,实际上pv[i]这种表示法等价于:   *(pv +i)

pv指针包含一个内存块的地址,方括号表示法会取出pv中包含的地址,用指针算术运算把索引i加上,然后解引新地址返回其内容。下面两个语句是等价的:

  *(pv + i)   ==   *(vector + i)

  pv + i实现“基址 + 位移”的运算,其值恰为数组 vector 第i个元素的地址,即&vector[i]。

  由于数组元素的下标在内部实现是统一按“基址 + 位移”的方式处理的,这样,一个指向数组的指针也能够以数组名的形式出现 pv[i]等价于 vecotr[i]

  vector[i]生成的代码和*(vector+i)生成的不一样,vector[i]表示法生成的机器码从位置vector开始,移动i个位置,取出内容。而*(vector+i)表示法,生成的机器码则是从vector开始,在地址上增加i,然后取出这个地址中的内容。尽管结果是一样的,生成的机器码却不一样,对于大部分人来说,这种差别几乎无足轻重。

  sizeof操作符对数组和同一个数组的指针操作也是不同的。对vector调用sizeof操作符会返回20,就是这个数组分配的字节数。对pv调用sizeof操作符会返回4,就是指针的长度。

  pv是一个坐值,左值表示赋值操作符左边的符号。左值必须能修改。像vector这样的数组名字不是左值,它不能被修改。

4.3 使用malloc创建一维数组

如果从堆上分配内存并把地址赋给一个指针,那就肯定可以对指针使用数组下标并把这块内存当成一个数组。

  int *pv = (int *)malloc(5 * sizeof(int));
      for (int i = 0; i < 5; i++) {
          pv[i] = i + 1;//*(pv + i) = i + 1 指针表示法
      }

用malloc创建的一维数组也可以使用数组表示法,但是用完之后要记得释放内存。

4.4 用realloc调整数组长度

  用realloc函数通过一个定长增量来分配额外空间。

char *getLine(void) 
{
    const size_t sizeIncrement = 10; //缓冲区的初始大小以及需要增大时的增量
    char *buffer = malloc(sizeIncrement); //指向读入字符的指针
    char *currentPosition = buffer; //指向缓冲区中下一个空白位置的指针
    size_t maximumLength = sizeIncrement; //可以安全地存入缓冲区的最大字符数
    size_t length = 0;    //读入的字符数
    int character;    //上次读入的字符数
    
    if (currentPosition == NULL) return NULL;
    while(1){
        character = fgetc(stdin); //从标准输入每次读取一个字符
        if (character == '
') break; //如果是回车符,循环退出

        if (++length >= maximumLength) //判断有没有超出缓冲区大小
        {
            char *newBuffer = realloc(buffer, maximumLength += sizeIncrement);

            if (newBuffer == NULL) { //如果无法分配内存
                free(buffer);//释放已分配内存 强制函数返回NULL
                return NULL;
            } //新分配的地址可能在原地址也有可能在其它位置
            currentPosition = newBuffer + (currentPosition - buffer);
            buffer = newBuffer;
        }
        *currentPosition++ = character; //没有超出字符添加到缓冲区中
    }
    *currentPosition = '';
    return buffer;
}

如果realloc分配成功,我们不需要释放buffer,因为realloc会把原来的缓冲区复制到新的缓冲区,再把旧的释放。如果试图释放buffer,十有八九程序会终止,因为我们试图重复释放同一块内存。

realloc函数也可以用来减少指针指向的的内存。如下所示的,trim函数会把字符串中开头的空白符和中间的空白符删掉。

#define  _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char *trim(char *phrase)
{
    char *old = phrase;
    char *new = phrase;
    while (*old == ' ') old++; //去掉开头空白符

    while (*old != '')
    {
        *(new++) = *(old++);
        while (*old == ' ') old++;//去除中间空白符
    }
    *new = '';
    return (char *)realloc(phrase, strlen(phrase) + 1);
}
void main()
{
    char *buffer = (char *)malloc(strlen("   C  at  && Tiger ") + 1);
    strcpy(buffer, "   C  at  && Tiger ");
    printf("%s
", trim(buffer));

    system("pause");
}

4.5传递一维数组

4.5.1 用数组表示法

void display(int arr[], int size) {
    for (int i = 0; i < size; i++)
        printf("%2d", arr[i]);
}

4.5.2 用指针表示法

  声明函数的数组参数不一定要用方括号表示法,也可以用指针表示法,如下所示:

void display(int *arr, int size) {
    for (int i = 0; i < size; i++)
        printf("%2d", arr[i]);
}

  在函数内部我们仍然使用数组表示法,如果有需要也可以使用指针表示法:

void display(int *arr, int size) {
    for (int i = 0; i < size; i++)
        printf("%2d", *(arr + i));
}

  如果在声明函数时用了数组表示法,在函数体内还是可以用指针表示法:

void display(int arr[], int size) {
    for (int i = 0; i < size; i++)
        printf("%2d", *(arr + i));
}

4.6 使用指针的一维数组

  下面的代码片段声明一个整数指针数组,为每个元素分配内存,然后把内存的内容初始化为元素的索引值。

    int *arr[5];
    for (int i = 0; i < 5; i++) {
        arr[i] = (int *)malloc(sizeof(int));
        *arr[i] = i;
    }

  因为arr声明为一个指针数组,arr[i]返回的是一个地址,用*arr[i]解引指针时,得到的是这个地址的内容。

也可以在循环体中使用下面这种等价的指针表示法:

   *(arr + i) = (int *)malloc(sizeof(int));
    **(arr + i) = i;

  表达式 arr[3][0]引用arr的第4个元素,然后是这个元素所指向的数组的第一个元素。表达式 arr[3][1]有错误,因为第4个元素所指向的数组只有一个元素。

4.7指针和多维数组

  可以将多维数组的一部分看作子数组,比如,二维数组的每一行都可以当作一维数组。

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

  int (*pmatrix)[5] = matrix;

 (*pmatrix) 表达式声明了一个数组指针,上面的整条声明语句将pmatrix定义为一个指向二维数组的指针,该二维数组的元素类型是整数,每列有5个元素。

  int *pmatrix[5] 如果我们把括号去掉就声明了5个元素的数组,数组元素的类型是整数指针。

  如果声明的列数不是5,用该指针访问数组的结果则是不可预期的。

  matrix + 1 返回的地址不是从数组开头偏移4,而是偏移了第一行的长度,20字节,得到的是数组的第二行地址。

  matrix[0]返回数组第一行第一个元素的地址,这个地址是一个整数数组的地址。

  于是,给它加1实际加上的是一个整数的长度,得到的是第二个元素。*(matrix[0] + 1)。

4.8传递多维数组

  要传递数组matrix,可以这么写:

    void display2DArray(int arr[][5],int rows){

  或者这么写:

    void display2DArray(int (*arr)[5],int rows){

  这两种写法都指明了数组的列数,这很有必要。在第一种写法中 arr[ ]是数组指针的一个隐式声明,第二种写法(*arr)则是指针的一种显示声明。

下面的声明是错误的:

  void display2DArray(int *arr[5], int rows){

  尽管不会产生语法错误,但是函数会认为传入的数组拥有5个整数指针。

也可能遇到下面这样的函数,接受的参数是一个指针和行列数:

void display2DArrayUnknownSize(int *arr, int rows, int cols) {
    for (int i = 0; i < rows; i++) 
        for (int j = 0; j < cols; j++) {
            printf("%d ", *(arr + (i*cols) + j));
        }       
        printf("
");
}

  printf语句通过给arr加上前面行的元素数(i * cols)以及当前列的j来计算每个元素的地址。要调用这个函数可以这么写:

  display2DArrayUnknownSize(&matrix[0][0], 2, 5);

  在函数内我们无法像下面这样使用数组下标:

    printf("%d ", arr[i][j]); 
原因是没有将指针声明为二维数组。不过倒是可以像下面这样使用数组表示法。
我们可以用一个下标,这样写只是解释为数组内部的偏移量,不能用两个下标是因为编译器不知道一维的长度:
  printf("%d ", (arr+i)[j]);
这里传递的是&matrix[0][0]而不是matrix,尽管matrix也能运行,但是会产生编译警告,原因是指针类型不兼容。
&matrix[0][0]表达式是一个整数指针,而matrix则是一个整数数组的指针。

在传递二维以上的数组时,除了第一维以外,需要指定其他维度的长度。下面这个函数打印一个三位数组,声明中指定了数组的后二维。
#include<stdio.h>
#include<stdlib.h>
void display3DArray(int(*arr)[2][4], int rows){
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 2; j++) {
            printf("{");
            for (int k = 0; k < 4; k++) {
                printf("%d ", arr[i][j][k]);
            }
            printf("}");
        }
        printf("
");
    }
}
void main()
{
    int arr3d[3][2][4] = {
        {{1,2,3,4},{5,6,7,8}},
        { { 9,10,11,12 },{ 13,14,15,16 } },
        { { 17,18,19,20 },{ 21,22,23,24 } },
    };
    display3DArray(arr3d, 3);

    system("pause");
}
arr3d[1] 表达式引数组的第二行,是一个2行4列的二维数组的指针。
arr3d[1][0]引用数组的第二行第一列,是一个长度为4的一维数组的指针。


4.9 动态分配二维数组

为二维数组动态分配内存涉及几个问题:  数组元素是否需要连续;  数组是否需要规则。

一个声明如下的二维数组所分配的内存是连续的:
  int matrix[2][5] = {{1,2,3,4,5},{6,7,8,9,0}};

  当我们用malloc这样的函数创建二维数组时,在内存分配上会有几种选择。

由于我们可以将二维数组当作数组的数组,因而“内层”的数组没有理由一定要是连续的。如果对这种数组使用下标,数组的不连续对程序员是透明的。
内存的连续性还会影响复制内存等其他操作,内存不连续就可能需要多次复制。

4.9.1 分配可能不连续的内存

下面代码片段创建一个内存可能不连续的二维数组。首先分配 外层 数组,然后分别用malloc语句为每一行分配。

    int rows = 2;
    int columns = 5;
    int **matrix = (int **)malloc(rows * sizeof(int *));

    for (int i = 0; i < rows; i++) {
        matrix[i] = (int *)malloc(columns * sizeof(int));
    }

因为分别用了malloc,所以内存不一定是连续的。实际分配情况取决于堆管理器和堆的状态,也有可能是连续的。

4.9.2 分配连续内存

第一种:首先分配“外层”数组,然后是各行所需的所有内存。

int rows = 2;
    int columns = 5;
    int **matrix = (int **)malloc(rows * sizeof(int *));

    matrix[0] = (int *)malloc(rows*columns * sizeof(int)); //按图示来看,这里是否该去掉rows?????
    for (int i = 1; i < rows; i++)
        matrix[i] = matrix[0] + i*columns;

第二种:数组所需内存一次性分配。

    int *matrix = (int *)malloc(rows * columns * sizeof(int));

后面的代码用到这个数组时不能使用下标,必须手动计算索引。如下代码片段所示,每个元素被初始化为其索引的积:

for (int i = 0; i < rows; i++) {
        for (int j = 0; j < columns; j++) {
            *(matrix + (i*columns) + j) = i*j;
        }
    }

不能使用数组下标是因为丢失了允许编译器使用下标所需的"形态"信息。4.8节讲过了。

实际项目中很少使用这种方法,但它确实说明了二维数组概念和内存的一维本质的关系。

 4.10 不规则数组和指针

  不规则数组是每一行的列数不一样的二维数组。

原文地址:https://www.cnblogs.com/Yang-Xin-Yi/p/13538911.html