【C-Primer-Plus读书笔记】第10章:数组和指针
数组
初始化数组
只存储单个值的变量有时也称为标量变量,而存储多个值的数组,C使用新的语法来初始化数组,如下:
int main(void) { int powers[8] = {1,2,4,6,8,16,32,64}; /* 从ANSI C开始支持这种初始化 */ }
如上所示,用以逗号分隔的值列表(用花括号括起来)来初始化数组,各值之间用逗号分隔。在逗号和值之间可以使用空格。
- 注意,通常的做法是用符号常量事先声明好数组的大小,以后修改数组大小只需要修改符号常量即可
- 同普通变量一样,使用
const
声明和初始化数组可以把数组设为只读。 - 同普通变量一样,在使用数组钱,必须先给他们初始化赋值,否则编译器使用的值是内存相应位置上现有的值,是不确定的。
- 当初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为0(注意,上面提到的是完全没有初始化,这里是初始化了部分);当初始化列表中的值多于数组元素个数时,编译器会将其视为错误,其实,可以省略方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数。
- 当使用
sizeof
对数组求值时(sizeof
运算符给出它的运算对象的大小,以字节为单位),结果是整个数组的大小(以字节为单位),可以用sizeof
对数组单个元素求值(以字节为单位),整个数组大小除以单个元素大小就是数组元素的个数。
指定初始化器(C99)
- C99增加了一个新特性:指定初始化器。利用该特性可以初始化指定的数组元素。
例如,只初始化数组中的最后一个元素。对于传统的C初始化语法,必须初始化最后一个元素之前的所有元素,如下:
- C99增加了一个新特性:指定初始化器。利用该特性可以初始化指定的数组元素。
int arr[6] = {0, 0, 0, 0, 0, 212}; //传统的语法
而C99规定可以在初始化列表中使用带方括号的下标指明待初始化的元素:
int arr[6] = {[5] = 212};
对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置成0。
指定初始化的两个特性:
- 如果指定初始化器后面有更多的值,那么后面这些值将被用于初始化指定元素后面的元素
- 如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化
- 如果未指定数组大小,编译器会把数组的大小设置得足够装得下初始化的值。
按数组元素赋值
- 声明数组后,可以借助数组下标(或索引)给数组元素赋值。
- 使用循环给数组的元素依次赋值是常见的手法,C不允许把数组作为一个单元赋给另一个数组,除初始化外也不允许使用花括号列表的形式赋值。
数组边界
- 在使用数组时,要防止数组下标超出边界(越界),也就是说,必须确保下标是有效的值。
- 编译器不会检查数组下标是否使用得当。使用越界下标的结果是未定义的,赋值操作还会导致程序改变其他变量的值。
- 数组元素的编号是从0开始。最好是在声明数组时使用符号常量来表示数组的大小。
指定数组的大小
- 在C99标准之前,声明数组时只能在方括号中使用整型常量表达式。所谓整型常量表达式,是有整型常量构成的表达式。
sizeof
表达式被视为整型常量,const
值不是。表达式的值必须大于0。 - 而C99标准则允许变长数组,或简称VLA。即方括号中可以使用变量。
- 在C99标准之前,声明数组时只能在方括号中使用整型常量表达式。所谓整型常量表达式,是有整型常量构成的表达式。
多维数组
初始化二维数组
- 初始化二维数组是建立在初始化一维数组的基础上。首先考虑在初始化一维数组的情况下,每个一维数组的元素值又是另一个数组,那么如果以花括号的方式初始化,得嵌套两层花括号进行赋值。
- 初始化时也可以省略内部的花括号,只保留最外面的一对花括号。只要保证初始化的数值个数正确,初始化效果与上面相同。在计算机内部,这样的数组是按顺序存储的。
其他多维数组
- 前面讨论的二维数组相关内容适用于三维数组或更多维数组。
- 可以把一维数组想成一行数据,把二维数组想象成数据表,把三维数组想象成一叠数据表。
- 通常,处理二维数组需要使用两重循环嵌套,三维数组要使用三重循环嵌套,依次类推。
指针和数组
数组表示法其实是在变相的使用指针
- 数组名是数组首元素的地址
- 在C中,指针加 1 指的是增加一个存储单元。对数组而言,这意味着加 1 后的地址就是下一个元素的地址,而不是下一个字节的地址。
现在可以结合前面所学总结下指针:
- 指针的值是它所指对象的地址
- 在指针前面使用*运算符可以得到该指针所指向对象的值
- 指针加 1,指针的值递增它所指向类型的大小(以字节为单位)。
- 可以使用指针 标识数组的元素和获得元素的值。
dates + 2 = &dates[2]; //相同的地址
*(dates + 2) == dates[2]; //相同的值
- 指针表示法和数组表示法是两种等效的方法。编译器编译这两种写法生成的代码相同。
函数、数组和指针
当一个函数处理一个数组时,即传递数组给函数时,注意一下几点:
- 数组名是该数组首元素的地址(所以传递数组实际上是传递一个指针)。
- 知道了数组首地址还不够,我们得知道数组在哪里结尾(即数组边界),通常的做法是用第二个参数来传递数组的大小。
- 关于函数的形参有一点要注意,只有在函数原型或函数定义头中,才可以用
int ar[]
代替int * ar
。int ar[]
和int * ar
形式都表示ar
是一个指向int
的指针。
- sum(int * ar, int n); //指针形式
sum(int ar[], int n); //数组形式
使用指针形参
- 函数要处理数组必须要知道何时开始、何时结束。前面提到,使用一个指针形参标识数组的开始,用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型)。但是还有一种方法是传递两个指针,第一个指明数组的开始处,第二个指明数组的结束处。
- 使用“越界”指针的函数调用更为简洁。这里的越界指数组边界的下一个位置(即超出边界一个位置),因为数组索引从0开始,所以首元素地址加上数组大小必然是越界一个位置的地址,但这也无妨,只要保证测试条件小于该越界指针即可,不用做其他处理,程序更简洁。
指针表示法和数组表示法
- 使用数组表示法,让函数是处理数组的这一意图更加明显
- 从机器的角度看,使用指针更加自然
- 只有指针变量才能用递增递减运算符
- 指针表示法更接近机器语言,所以一些编译器在编译时能生成效率更高的代码(尤其是与递增递减运算符一起使用时)。
指针操作
下面分别描述了指针的基本操作:
- 赋值(
&
运算符) - 解引用(
*
运算符) - 取(指针本身的)地址
- 指针与整数相加 或 指针减去一个整数
- 递增与递减指针
- 指针间求差
- 指针间比较
- 赋值(
- 编译器不会检查指针是否指向数组元素,结果是未定义的,但是可以解引用指向数组任意元素的指针,也能解引用越界指针。但千万不要解引用未初始化的指针,因为未初始化的指针其值是一个随机值,指向的内存地址是未知的。
指针的基本用法:
- 在函数间传递信息
- 处理数组的函数
保护数组中的数据
- 在函数间传递信息通常都是直接传递数值,只有在程序需要在函数中改变该数值时,才会传递指针。对于数组别无选择,必须传递指针,因为这样做效率高。
然而,某些情况函数并不需要修改数据,而数组必须传递指针进去,就会有一定风险导致原始数据被修改。 对形式参数使用
const
ANSI C提供了一种预防手段。如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字const
。int sum(const int ar[], int n);
这样使用const
并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。
const
的其他内容指向
const
的指针不能用于改变值(指针所指向地址的值),无论是使用指针表示法还是数组表示法。但是,可以让该指针指向别处。const int * p = rates;
关于指针赋值和
const
需要注意一些规则:- 把
const
数据或非const
数据的地址 初始化为指向const
的指针或为其赋值是合法的。
- 把
- 然而,只能把非
const
数据的地址赋给普通指针 - 这个规则很合理。否则,通过指针就能改变
const
数组中的数据。 - C标准规定,使用非
const
标识符修改const
数据,导致的结果是未定义的。 - 可以声明并初始化一个不能指向别处的指针,关键就是
const
的位置。
int * const p = rates;
- 同理,在创建指针时还可以使用
const
两次,该指针既不能更改它所指的地址,也不能修改指向地址上的值。
const int * const p = rates;
指针和多维数组
如下二维数组声明:
zippo4;
我们展开分析: - 从数组首元素的地址上看:数组名(`zippo`)存储着首元素(`zippo[0]`)的地址,而首元素(`zippo[0]`)存储着另一个数组的首元素(`zippo[0][0]`,此时可以把`zippo[0]`当成另一个数组的数组名,然后就可以回到本段开始处重复往下推)地址。所以,`zippo[0]`是一个占用一个int大小对象(因为`zippo[0][0]`存储的是一个类型为int的具体数据)的地址,而`zippo`是一个占用两个int大小对象(因为`zippo[0]`存储着类型为int大小为2个元素的数组)的地址。由于这两个数组都开始于同一个地方,所以它们的值相同。 - 在指针或地址的加减上:上面提到两个数组名占用的大小不一样,所以在加减法上,地址的改变也不一样。 - 在解引用上:因为第一层数组(`zippo`)存储着第二层数组(`zippo[0]`)的地址,所以如果单纯的解引用数组名(`zippo`)得到的是第二层数组(`zippo[0]`)的地址,所以`*(zippo[0])`就表示第二层数组第一个元素(`zippo[0][0]`)的值,而这是一个具体的int数据(如果是更多维数组,得继续向下推),于是得到该值。而如果全部采用指针表示法,得解引用两次(用两个`*`)才能得到该值,同理全部用数组表示法,后面跟两个方括号,这在上面已经看出来这点,前面说明的是指针表示法和数组表示法混用的情况。
指向多维数组的指针
在声明一个指针变量指向一个二维数组时,单纯的把指针声明为指向指定类型还不够,因为这仅仅代表指向一个指定类型值,实际上二维数组下是一个内含n个指定类型值的数组,并不是一个具体的数据,所以必须指向的是内含n个指定类型值的数组,而不是指向一个指定类型值。(* pz)[2]; //pz指向一个内含两个int类型值的数组
当然,这样声明后,完全可以用数组表示法和指针表示法来表示一个元素,既可以把pz
当指针名,也能当数组名。
- 指针的兼容性
指针之间的赋值比数值的要严格,低精度类型值不用转换就可以赋给高精度类型变量。但指针不行,即指针对类型严格要求一致,包括二维数组的下层数组元素个数都得完全一致,否则就会不兼容。 函数和多维数组
如果要编写处理二维数组的函数,首先要能正确理解指针才能写出声明函数的形参,即前面介绍的声明指向多维数组的指针。- some( int (* pz)[2] ); //pz指向一个内含两个int类型值的数组
some_two( int pz[][2] ); //此写法只能应用在形参,空方括号表明pz是个指针
一般而言,声明一个指向n维数组的指针时,只能省略最左边方括号的值。因为第一对方括号表明这是一个指针,而其他方括号则用于描述指针所指向数据对象的类型。
变长数组(VLA)
前面提到过变长数组,现在来详细说明几点:
- 变长数组必须是自动存储类型,而且不能在声明中初始化它们。
- 变长数组不能改变大小,一旦创建了变长数组它的大小则保持不变,“变”指的是在创建数组时可以使用变量指定数组的维度。
在函数形参中,需要传入指定维度的变量,并写在变长数组前面,因为变长数组的声明要使用到它们
int sum2d(int rows, int cols, int ar[rows][cols]); //ar是一个变长数组 int sum2d_two(int, int, int ar[*][*]); //这种省略原型形参名的写法,注意用 * 来代替省略的维度
- 和传统的语法类似,变长数组名实际上是一个指针。
- 变长数组还允许动态内存分配,这说明可以在程序运行时指定数组的大小。普通C数组都是静态内存分配,即在编译时确定数组大小。由于数组大小是常量,所以编译器在编译时就知道了。
复合字面量
- C99新增了复合字面量,字面量是指除符号常量外的常量。
int diva[2] = {10, 20}; //这是一个普通的数组声明
(int [2]){10, 20}; //复合字面量
- 复合字面量是匿名的,所以不能先创建然后再使用它,必须再创建的同时使用它。使用指针记录地址就是一种用法。
int * pt1;
pt1 = (int [2]){10, 20};
- 把信息传入函数前不必先创建数组,这就是复合字面量的典型用法。
- 复合字面量是提供只临时需要的值的一种手段。复合字面量具有块作用域。