• 深入理解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 不规则数组和指针

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

  • 相关阅读:
    ZOJ 3891 K-hash
    ZOJ 3890 Wumpus
    ZOJ 3888 Twelves Monkeys
    ZOJ 3885 The Exchange of Items
    HDU 3849 By Recognizing These Guys, We Find Social Networks Useful
    HDU 2242 考研路茫茫——空调教室
    BZOJ 3676: [Apio2014]回文串
    [转载]CAsyncSocket及CSocket注解
    WritePrivateProfileString()
    GetSystemMetrics()
  • 原文地址:https://www.cnblogs.com/Yang-Xin-Yi/p/13538911.html
Copyright © 2020-2023  润新知