zoukankan      html  css  js  c++  java
  • C和C指针小记(十三)-数组

    1.1 一维数组

    一维数组的声明: int a[10]; 这里a就是一个数组. 数组a的类型就是一个指向整型的常量指针.
    但是数组和指针是**不相同**的.
    **数组具有特定数量的元素,而指针只是一个标量值.**
    只有但数组名在表达式中使用时,编译器才会为它产生一个指针常量.(注意是指针常量,不是指针变量)
    

    1.2 数组下标

    如有 int b[10];
    则 *(b + 3) 代表的就是b[3]
    

    除了优先级之外,下标引用和间接访问完全相同
    所以 array[subscript] 和 (array + (subscript) ) 完全相同.在使用下标引用的地方,你可以使用对等的指针表达式来代替.
    ** 下标可以是负数 **
    如:
    int array[10];
    int ap = array + 2;
    由于ap 指向array 的第三个元素, ap[-1] 指向 array的第二个元素.
    下标越界是非法的
    ap[9] 是非法的.
    一个奇葩 2[array] **
    它是合法的,把它转换成对等的间接访问表达式,为 (2 + (array) ) ,去掉内括号,交换 array 和 2 的位置, 得 (array+ 2). 也就是
    array[2]

    1.3 指针与下标

    假定可以互换使用指针和下标.下标绝对不会比指针更有效率,但指针有时会比下标更有效率.
    比较编译器对指针和下标的处理过程.:
    
        //下标循环
        int array[10], a;
        for(a = 0; a < 10; a +=1)
            array[a] = 0;
    
    为了对下标表达式求值,编译器在程序中插入指令,取得 a 的值,并把它与整型的长度(4)相乘.这个乘法需要话费一定的时间和空间.
    
        //指针
        int array[10], *ap;
        for( ap = array; ap < array + 10; ap++)
            *ap = 0;
    
    尽管这里不存在下标计算,但是还是存在乘法运算.乘法运算出现在for语句的调整部分, ap++ 中这个值必须与整型的长度相乘.然后再与指针相加.
    

    但这里在循环每次执行的时,执行乘法运算的都是两个相同的数(1和4, 步长和整型元素长度). 结果这个乘法只是在编译时执行一次,即程序中现在包含来一条指令,把4与指针相加,程序在运行时并不执行乘法运算.

    1.4 数组与指针

    指针和数组是不相等的.
    例子:
    int a[5];
    int *b;

    • 他们都具有指针指,他们都可以进行间接下标引用操作.
    • 区别:1)声明一个数组,编译器将根据声明所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置.声明一个指针变量时,编译器只为指针本身保留内存空间,它并不为任何整型值分配内存空间.2)指针变量并未初始化为指向任何现有的内存空间,如果它是一个自动变量,它甚至根本不会被初始化.
      所以 int *b;是非法的 *b 将访问内存中某个不确定的位置,或者导致程序终止. 但是b++ 可以编译通过,但是a++却不行.因为a 的值是一个常量.

    1.5 作为函数参数的数组名

    当一个数组名作为参数传递给一个函数时.
    数组名的值就是一个指向数组第一个元素的指针.所以此时传递给函数的是一份该指针的拷贝.
    函数如果执行来下标引用,实际上是对这个指针执行间接访问操作,并且通过这种间接访问,函数可以访问和修改调用程序的数组元素.
    表面上看,C语言中函数的参数都是值传递,但是参数是指针的时候为什么有变成地址传递了?这是不是矛盾?
    先说结论,实际上,C中所有的函数参数都是值传递.数组名也不例外.传递的参数是一份拷贝,(指向数组起始位置的指针拷贝),所以函数可以自由地操作它的指针型参,而不必担心会修改对应的作为实参的指针.
    如果传递了一个指向某个变量的指针,而函数对该指针执行了间接访问操作,那么函数就可以修改那个变量.
    例子:

    //把第二个参数中的字符串复制到第一个参数指定的缓冲区
    void strcpy(char *buffer, char const *string) {
        //重复复制字符,知道遇见NULL字节
        while( (*buffer++ = *string++) != '')
        ;
    }
    

    1.6 声明数组参数

    如果你想把一个数组名参数传递给函数,准确的函数形参应该是怎样的?它是因该声明为一个指针还是一个数组?
    实际上,调用函数时实际上传递的是一个指针,所以函数的形参实际上是一个指针.编译器实际上也接受数组形式的参数.
    int strlen( char *string);
    int strlen( char string);
    在这里,strlen的两种传参方式是一样的.

    两种声明都可以,但是哪个“更加准确呢”,答案是指针. 因为实参实际上是一个指针,而不是数组. 同样,表达式 sizeof sting的值是指向字符的指针长度,而不是数组的长度.
    为什么函数原型中的一维数组形参无需写明它的元素数目,因为函数并不为数组分配内存空间.形参只是一个指针,它指向的是已经在其他地方分配好内存的空间,这个事实解释了为什么数组形参可以与任何长度的数组匹配-- 它实际传递的只是数组第一个元素的指针.另一方面,这种实现方案使函数无法知道数组的长度.如果函数需要知道数组的长度,它必须作为一个显式的参数传递给函数.

    比如这样,
    char *find_char(char *source, int length, char *given);

    1.7 数组的初始化

    int vector[5] = {10, 20 ,30, 40, 50};
    

    静态和自动初始化
    数组的初始化方式类似于标量变量的初始化方式--也就是取决于他们的存储类型.存储于静态内存的数组只初始化一次,也就是在程序开始执行之前.程序并不要执行指令把这些值放到何时的位置,他们一开始就在哪里了.这个魔术是由连接器完成的,它用包含可执行程序的文件中何时的值对数组元素进行初始化.如果数组未被初始化,数组元素的初始值将会自动设置为0. 当这个文件载入到内存中准备执行时,初始化后的数组值和程序指令一样也被载入到内存中.因此,当程序执行时,静态数组已经初始化完毕.

    对于自动变量而言,初始化过程就没有那么浪漫了.因为自动变量位于运行时堆栈中,执行每次进入他们所在的代码块时,这类变量每次所处的内存位置可能并不相同.在程序开始前,编译器没有办法对这些位置进行初始化.所以自动变量在缺省情况下是未初始化的.如果自动变量的声明中给出了初始值,每次当执行流进入自动变量声明所在的作用域时,变量就被一条隐式的赋值语句初始化.这条隐式的复制语句和普通的赋值语句一样需要时间和空间来执行.数组的问题在于初始化列表中可能有很多值,这就可能产生许多条复制语句.对于那些非常盘大的数组,它的初始化时间可能非常可观.
    如果数组初始化位于局部于一个函数时,你因该仔细考虑一下,在程序的执行流每次进入函数时,每次都对数组进行初始化是不是值得,如果不值得,就把数组声明为static.这样改变数组的存储区为静态区,只需在程序开始前执行一次.

    1.8 自动计算数组长度

    int vector[] = {1, 2, 3, 4, 5};
    如果声明中并未给出数组的长度,编译器就把数组的长度设置为刚好能够容纳所有的初始值的长度. 如果初始值列表经常修改,这个技巧尤其有用.

    1.9 字符数组的初始化

    如果根据对整型数组的初始化,我们可以这样初始化字符数组
    int a[] = {1, 2, 3, 4, 5};
    char message[] = {'H', 'e', 'l', 'l', 'o', 0};
    但这也太笨了.
    C支持我们这样做:
    char message[] = “Hello";
    它看上去像是一个字符串常量,实际上并不是. 它只是前例的初始化列表的另一种写法.
    如何区分字符串常量和这种初始化列表呢?
    当用于初始化一个字符数组时它就是一个初始化列表,在其他任何地方,它都表示一个字符常量.
    char message1[] = "Hello";//字符数组
    char *message2 = "Hello";//字符常量
    二者的内存分配区别图示

    2、多维数组

    数组的维数不只一个,就是多维数组.
    int matrix[2][3];

    2.1 存储顺序

    int matrix[6][10];
    int *mp;
    mp = &matrix[3][8];
    ? matrix 是6行10列还是 10行6列?
    答案是两种解释都可以.
    因为在内存中,数组元素的实际存储方式是确定的.

    2.2 数组名

    一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第一个元素.
    多维数组名也类似, 它指向第一维的元素实际上是另一个数组.
    int matrix[3][10];
    可以看作是一个一维数组,包含3个元素,只是每个元素恰好是包含10个整型元素的数组.
    matrix 这个名字的值是一个指向它第一个元素的指针,matrix是一个指向一个包含10个整型元素的数组的指针.

    2.3 下标

    下标引用实际上只是间接表达式的一种伪装形式,在多维数组中也是如此.
    matrix 指向第一个包含10个整型元素的数组.
    matrix + 1 指向第二个包含10个整型元素的数组.

    *(matrix + 1)
    事实上标示了一个包含10个整型元素的子数组.数组名的值是一个常量指针.它指向数组的第一个元素,在这个表达式中也是如此.
    它的类型是指向整型的指针.

    *(matrix + 1) + 5
    由于 *(matrix + 1) 是个指向整型的指针,所以5个这个值根据整型的长度进行调整.整个表达式的结果是一个指针.它指向的位置比原先那个表达式所指向的位置向后移动了5个整型元素.

    对这个指针所指向的值进行间接访问操作:
    *( *(matrix + 1) + 5) 它所指向的正是图中箭头标示的元素. 如果它作为右值,就取得存储于那个位置的值;如果它作为左值使用,这个位置将存储一个新值.

    这个表达式实际上就是下标
    *( matrix + 1) 即 matrix[1]
    ( (matrix + 1) + 5) 即 *( matrix[1] + 5)
    还可以再次用下标代替间接访问符
    即 matrix[1][5]

    **注意: 不能用都好分割来表示多维数组 例如 matrix[4, 3]; 它不是 4X3的矩阵,它只是一个含有3个元素的 一维数组,因为编译器会忽略逗号前的4;

    2.4 指向数组的指针

    下面的声明合法嘛?
    int vector[10], vp = vector;
    int matrix[3][10], mp = matrix;
    结论:第一个合法,第二个非法.
    第一个,它为一个整型数组分配内存,并把vp声明为一个指向整型的指针,并把它初始化为指向vector数组的第一个元素.vector和vp具有相同的类型:指向整型的指针.
    第二个,它正确的创建来matrix二维数组,并把mp声明为一个指向整型数组的指针.我们因该这样声明一个指向整型数组的指针,
    int (
    p)[10];
    这个声明看起来有点复杂,但是它事实上并不是很难.你只要假定它是一个表达式并对他求值.下标优先级高于间接访问,但由于括号的存在,首先执行的还是间接访问,所以
    p是个指针,但它指向什么呢?接下来是下标引用,所以p指向某种类型的数组.这个声明表达式中没有更多的操作符,所以每个数组的每个元素都是整数.
    所以第二个声明加初始化后是下面这个样子:
    int (p)[10] = matrix;
    它使p指向matrix的第1行.
    **但是不能这样声明: Int (
    p)[] = matrix; 这样数组的长度不见了. **
    有的编译器能捕捉到这样的错误,有的捕捉不到.

    2.5 作为函数参数的多维数组

    作为函数参数的多维数组名的传递方式和一维数组名相同--实际传递的是个指向数组第一个元素的指针.但是,两者之间的区别在于,多维数组的每个元素本身是另外要给数组,编译器需要知道它的维数,以便为函数形参的下标表达式进行求值.这里有两个理智,说明了他们之间的区别:
    int vertor[10];
    func1(vector):
    参数vector的类型是指向整型的指针,所以 func1的原型可以是下面两种中的任何一种:
    void func1(int *vec);
    void func1(int vec[]);
    作用于vec上面的指针运算把整型的长度作为它的调整因子.

    对于矩阵
    int matrix[3][10];
    func2(matrix);
    这里的参数matrix的类型是指向包含10个整型元素的数组的指针.func2的原型可以是下面两种形式中的任何一种
    void func2( int (*mat)[10] );
    void func2 (int mat[][10] );
    mat的第一个下标根据包含10个元素的整型数组的长度进行调整,接着第二个下标根据整型的长度进行调整,这个原先的matrix数组一样.
    这里的关键在于,编译器必须知道第2个以及以后各维的长度才能对各下标进行求值,因此在原型中必须声明这些维的长度.第1维的长度并不需要,因为计算下标值时用不到它.
    注意:
    把func2 写成下面是不正确的:
    void func2(int **mat);
    把mat声明为一个指向整型指针的指针,它和指向整型数组的指针并不是一回事.

    2.6 初始化

    对于多维数组,数组元素的存储顺序就变得非常重要.
    一种是只给出一个常常的初始序列的值
    int matrix[2][3] = {10, 11, 12, 13, 14, 15 };
    第二种,基于多维数组实际上是福大元素的一维数组这个概念.
    例如 声明

    int tow_dim[3][5];
    int tow_dim[3][5] = {*, *, *} ;//*代表一个复杂元素
    int tow_dim[3][5] = {
        {0, 1, 2, 3, 4,},
        {10, 11, 12, 13, 14,},
        {20, 21, 22, 23, 24}
    };
    

    使用缩进是非必须的,但是这样做更加有利于阅读.

    3、指针数组

    除了类型之外,指针变量和其他变量很相似.正如你可以创建整型数组一样,你也可以声明指针数组.
    例如:
    int api[10];
    为了弄清楚这个复杂的声明,我们假定它是一个表达式,并对它进行求值.
    下标优先级高于间接访问,所以在这个表达式中,首先执行下标引用.因此,api是某种类型的数组(它包含10个元素).在取得一个数组元素之后,随机执行的是间接访问操作.这个表达式不再有其他操作符,所以它的结果是一个整型.
    对数组的某个元素执行间接访问操作后,我们得到一个整型值,所以api肯定是个数组,它的
    元素类型是指向整型的指针.
    关于运算符优先级:
    运算符优先级.

    什么地方会用到指针数组呢?
    例子:

    //数组
    char const keyword[] = {
        "do",
        "for",
        "if",
        "register",
        "return",
        "switch",
        "while"
    };
    #define N_KEYWORD
    ( sizeof(keyword)/sizeof(keyword[0]) )
    

    sizeof(keyword)是整个数组所占用的字节数.而sizeof(keyword[0])的结果则是每个元素所占的字节数.这两个值相除就是数组元素的个数.
    这个数组可以用于计算一个C源文件中关键字个数的程序中.

    思考
    如果这样声明呢?

    char const keyword[][9] = {
        "do",
        "for",
        "if",
        "register",
        "return",
        "switch",
        "while"
    };
    

    这个声明创建了一个矩阵:它每行的长度刚好容纳最长的关键字
    char const keyword[][9]的内存布局:

    char const keyword[]的内存布局

    矩阵的存储方式看上去效率低一些,因为它的每一行的长度都被固定为刚好能容纳最长的关键字.但是它不需要任何指针.
    另外,指针数组本身也要占用空间,但是每个字符串常量占据的内存空间只能是它本身的长度.

    如果字字符集中字符串的长度都差不多,那么用矩阵的方式紧凑些,
    如果字符集中字符串的长度千差万别,甚至有超长字符串,可以用指针数组.

    人们时常选择指针数组方案,但是略微改动

    char const *keyword[] = {
          "do",
        "for",
        "if",
        "register",
        "return",
        "switch",
        "while",
        NULL
    };
    

    在表的末尾增加了一个NULL指针.这个NULL指针使函数在搜索这个表时能够检测到表的结束,而无需预先知道表的长度.
    for( kwp = keyword_table; *kwp != NULL; kwp++)

  • 相关阅读:
    Java 基础(六):循环
    Java基础(五):数组
    Java 基础(四):从面试题看String
    Java Grammar(二):运算符
    「每日五分钟,玩转JVM」:指针压缩
    一份让你效率翻倍的年终总结
    「每日五分钟,玩转 JVM」:GC 概览
    「每日五分钟,玩转JVM」:对象内存布局
    「每日五分钟,玩转JVM」:对象从哪来
    Azure Web App (二)使用部署槽切换部署环境代码
  • 原文地址:https://www.cnblogs.com/wjw-blog/p/10414225.html
Copyright © 2011-2022 走看看