C数组和指针


尽管现在很多的高级语言,脚本语言提供了非常非常方便实用的数组,队列(list),字典(map)等数据结构,但是理解C语言的数组和指针是掌握这些高级数据结构的基础。即使不经常写底层的数组,也应该熟练掌握。

初始化

数组的初始化比较简单不罗嗦了,这里介绍一下数组的指定项目初始化方法,这是c99添加的特性,个人觉得很有意思。

// 普通的初始化方法
int year[6] = {31, 28, 31, 30, 31, 30};
// 可以在初始化时指定要初始化的项及其值如:
int year[6] = { [1] = 28, [3] = 30, 31, 30, [1] = 29};
// 初始化结果为: [0, 29, 0, 30, 31, 30]

看起来还算好理解,就是指定下标对其赋值。某个指定赋值后的为指定项目用来对后续的元素初始化,未初始化的项会初始化为0(只要对数组进行了初始化就会把剩余的部分初始化为0,这是一般行为)。甚至可以对一项多次指定赋值,以最后一次为准。

再提一次,花括号列表的形式只能用于初始化,不能用于对数组赋值。

对数组指针使用sizeof的时候会计算到第一个 \0 处,常用 sizeof arr / sizeof arr[0] 计算数组的大小。但是实际上这样做可能会导致问题。

多维数组

多维数组的初始化可以嵌套的字面量方法表示,也就是多层花括号的嵌套。多数数组的字两面初始化方法可以省略内部的花括号,只保留最外一层的花括号,只要保证数量的个数正确即可,如果数量不对,后面没有数值的元素会被初始化为0。这样做并没有什么好处,使用的时候建议使用嵌套的花括号方式,程序更好理解。

对于定义时省略维度的数组,遍历时使用sizeof确定数组的长度:

double arr[][2] = {1.2, 2.3, 3.4, 4.5, 5.6, 6.7, 6.8, 7.8, 8.9, 9.9};
int len = sizeof(arr) / (2 * sizeof(double)); // use len to iterate arr

指针和数组

数组名同时也是该数组首元素的地址。或者说数组名是指向数组首元素地址的常量(不可变)。

所有的数组操作的内容基本都是根据这一条扩展来的。

指针是一类数据类型,不同基本数据类型的指针类型是不同的类型。声明指针时要指明它所指向的对象的类型,这样才能正确处理存储的字节数。

  • 指针的数值就是它所指向的对象的地址。地址的内部表示方式是由硬件决定的,所以并不关心这个值具体是多少。
  • 在指针前运用运算符 * 就可以得到该指针所指向的对象的值
  • 对指针加1等价于指针的值加上它指向的对象的字节大小

*ptr++ 这是很常见的用法,这是因为一元操作符*++ 有相等的优先级,但是它们的结合时是从右向左的,这意味着 ++ 是作用于 ptr 的,即指针自增1,total += *ptr++; 表示将当前ptr的值加到 total 再使ptr指向下一个元素(a++)的动作在指令执行之后。虽然 *ptr++ 很常见,但是更推荐容易理解的 *(ptr++)

注意区分:

(*ptr)++;
*(ptr++)
*ptr++
*(++ptr);
*++ptr;

对于指针最常见的错误就是对未初始化的指针取值:

char *s;
s = "hello world";

一定要注意,指针在定义时并不会初始化,也就是说s可能指向任何地方,也就是说 “hello world”可能被存储到任何地方(编译器可能会检测出这种错误)而导致程序奔溃。记住:创建一个指针时,系统值分配了用来存储指针本身的内存空间,并没有分配用来存储指向的对象(数据)的空间。

初始化指针有两种方法,一种是对变量取地址后对指针变量赋值,一种是使用malloc函数将返回值赋值给指针变量。

保护数组内容

有时候要我们要求传递给函数作为参数的指针指向的对象(比如数组的元素)的值不被修改,在 K&R C 中,避免这种错误的唯一方法就是警惕不出错,哈哈。不过在 ANSI C中,可以在函数原型和定义中的形式参量声明中使用关键字 const。比如函数

int sum(const int *a, int len);

const告诉编译器,第一个参数指向的数组作为包含常量数据的数组对待,如果意外地想在函数内部修改内容,比如使用 a[i]++ 这样的表达式,编译器会认为是一个错误并生成一条错误信息。这一条非常有利于我们理解头文件中的函数声明。

使用const声明的指针变量pt,只是不能使用pt修改指针指向的对象的值,但是可以让这个指针指向新的对象的地址,或者通过另一个普通指针修改对象的值。比如上面的例子,只是不允许 a[i]++ 这样的表达式,但是可以是 a = b; // b是一个int *

将常量或者非常量数据的地址赋值给常量的指针是合法的,但是只有非常量数据的地址才能赋值给普通指针(非常量指针)。这样就不会用指针来修改认为是常量的数据了。这样导致使用const修饰的函数参数可以接受普通数组和常量数组的名称作为实际参数,而没有使用const修饰的函数参数则只能使用普通数组,而不能使用const修饰的数组。

可以用const声明并初始化指针,确保指针不会指向别处,关键在于 const 的位置。

double r = {88, 89, 100.1};
double const *p = r;
p[1] = 89; // 不允许
p = &r[2]; // 允许

double * const pc = r;
pc = &r[2]; // 不允许
*pc = 89; // 允许修改指向对象的值

const doule * const pp = r;
pp = &r[2]; // 不允许
pp[1] = 89; // 不允许

指针与多维数组

多维数组也都是可以使用指针来操作的,最基础的要理解下面两个定义:

int (*pz)[2];
int * pax[2];

这在之前的一篇文章中也提到过了,这里在解释一下:在表达式中 [] 的优先级比 * 要高,高优先级意味着先结合,所以第一个使用圆括号改变表达式的结合性。

第一个表达式定义pz为一个指向一个包含2个int值的数组的指针。这是比较常见的定义,常用于二维数组的外循环,而第二个表达式分析如下:首先方括号与 pax 结合,表示 pax是包含两个元素的数组,然后和 结合,表示 pax 是两个指针组成的数组,最后用int来定义,表示pax是由两个指向int值的指针构成的数组, 即等价于: `(int ) pax[2]`。

所以 pz表示一个指针,pax表示一个数组。但是,数组名实际上又就是一个指针,所以这两个很容易混淆,这两个声明都会创建两个指向单个int值的指针,但是前面的版本通过圆括号使 pz 首先和 * 结合,从而创建一个指向包含两个int值的数组的数组的指针(数组的每个元素是两个int值,其实是一个二维数组)的指针。这一段是比较难以理解的(我自己也不知道有没有理解对)。

可以有下面的用法(只是一个示例):

double daar[][2] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6}; 
double (*dpt)[2];
dpz = daar; // double **pp = daar; error

变长数组(VAL)

C99标准引入了变长数组,它允许使用变量作为数组的各维。我们已经多次使用变长的数组了,要注意变长数组的一些限制:变长数组必须是自动存储类的,这意味他们必须是在函数内部,或者作为函数参量声明的,而且这种数组声明时不可以进行初始化

int a = 5;
int b = 4;
int arr[a][b];

这样的数组声明并不难理解,主要是要考虑变长数组作为函数参数的情况。我们知道不同(类型相同各维度长度不同)的数组被认为是不同的数据类型,那么在函数声明中应该怎么写呢,下面是一个变长数组参数的函数示例:

int sum2d(int rows, int cols, int ar[rows][cols];

还记得之前讲过的变量作用域的部分么,这里也是使用变量作用域分析的,即rows, cols的声明必须在其被使用(作为数组维度长度)之前。

根绝C99的规定,可以省略函数原型中的名称,用星号(*) 代替省略的维度:

int sum2d(int, int, int ar[*][*]);

复合字面量(compound literal)

这个在之前的某一篇已经提到过了,也翻译为复合文字常量,我觉得字面量这个名词更好理解。

对于数组来说,复合字面量就是在数组的初始化列表前面加上用圆括号括起来的类型名。例如下面的一个复合字面量,创建了一个包含两个int值的无名称数组。

(int [2]){10, 20};
int *p1;
p1 = (int []){10, 20, 30, 40, 50 };

int (*p2)[4];
p2 = (int [2][4]) {1,2,3,4}, {5,6,7,8};

复合字面量可以直接作为函数的实际参数使用,这是复合字面量用的最多的地方。