C 语言复习与提高--- IV. 数组与指针

  IV. 数组与指针

C 语言提供访问数组的两种方法:指针算术和数组下标。

指针算术的速度可以高于数组下标。考虑到速度因素,程序员一般都使用指针来访问数组元素。

一、数组(Array):具有相同类型的数据的有序集合,并用唯一的名字来标识。

1、数组必须直接声明,编译器在编译阶段为其分配内存空间。

2、在 C89 中,数组必须是定长的,数组的大小在编译时是固定的;C99 允许使用变长数组(VLA),数组的大小在运行时确定。 但是只有本地数组才可以申请为变长的。增加变长数组是为了支持数值处理。

[例]void f(int longeur, int wide) { int matrix[longeur][wide]; /* 定义一个矩阵 */ /* 数组的长度由两个参数决定 */ }

3、数组的所有元素占据连续的内存空间,在内存中是线性存放的,保存数组所需的内存空间直接与基类型和数组长度有关。 数组占用内存空间 = sizeof(基类型) * 数组长度。

4、C 不检查数组是否越界,程序可以在两边越界。程序员应自己加入越界检查。数组可以越界使用,但是初始化时不允许!

5、向函数传递数组: 定义数组形参的方法有三种:指针,定长数组,无尺寸数组。 void func1(int *a) { }

void func2(int a[10]) { }

void func3(int a[]) { }

在函数的形参的声明中,数组的尺寸无所谓,因为C语言没有边界检查。 实际上,第二种方法在编译后,编译器产生的代码就是让函数接受指针,并不生成 10 个元素的数组。

(1、)形参中的数组不能再理解为数组,而必须理解为指针:不能用 sizeof() 求大小;但可以再赋值,这与数组名的指针常量性质不一样。传值时有内容的复制,但数组内的元素可能很多,为避免内容的大量复制而占用太多的内存,C 规定数组传参就是传指针。

(2、)int a[][] 不能做形参,因为 a 是指向 int[] 这样一种数据类型的数组指针,但下标大小没有确定。而 int a[][8] 可以,并可以直接用二维数组名(无须显示转换)做其实参。

6、在处理一个数组的元素时,使用指针自增(p++)的方式通常比直接使用数组下标更快,使用指针能够使程序得以优化。

7、C 允许定义多维数组,维数上限由编译器定义。但多于三维的数组并不常用,因为多维数组所需的内存空间对维数呈指数增长。并且,计算各维下标会占用 CPU 时间(存取多维数组元素的速度比存取一维数组元素的速度慢)。

8、对数组初始化时注意,C89 要求必须使用常量初始化字符,而 C99 允许使用非常量初始化字符来初始化本地数组。

二、串(String):数组(尤其是一维数组)最常用的地方。

1、C 没有专门的字符串变量,对于它的操作全部由一维数组实现。字符串是字符数组的一种特殊形式,唯一的区别就在于它是作为一个整体操作,而普通数组则不能。最终的差别就在末尾的 NULL(0)上。

2、注意与 C++ 中 string 类型(为字符串处理提供了 OO 的方法)的区别,C 并不支持它。

3、初始化操作:要使用字符串常量时则把它放到数据区中的 CONST 区(数据区、全局变量区和静态变量区),用字符串常量初始化字符数组时有一个复制内容的操作,而不仅仅是用一个指针指向它。实际上字符串常量是在常量区还是堆区、采用何种存储结构、以及是否连续的问题,取决于不同的编译器。

4、串的输入与输出:下面的函数均由 <stdio.h> 定义。

(1、)printf("%s", str);

(2、)puts(str);

(3、)scanf("%s", str);

(4、)gets(str);

5、串运算:下面的函数均由 <string.h> 定义。

strcpy(s1, s2),strcat(s1, s2),strlen(str),strcmp(s1, s2),strchr(s1, ch),strstr(s1, s2)。

[注意]字符数组、字符指针之间的 == 比较是地址的比较,结果不可能是 true。但字符串常量的 == 比较则不一定:VC 对 == 进行了重载,将字符串常量的地址比较变为内容比较(如同输出字符指针实际是输出字符串一样,都是重载在作怪),因而 ("abc" == "abc") 在 VC 中成立,但在 BC 中则不成立。为避免二义性,应该尽可能用 strcmp() 来比较。

三、指针(Pointer):

指针是 C 语言的精华,正确理解并灵活运用指针是衡量能否成功地编写 C 程序的标准。

1、使用指针的好处:

--> 能够为调用函数灵活地修改实参变量的值。

--> 支持动态内存分配,能够方便地实现动态的数据结构(如二*树和链表)。

--> 可以提高某些程序的效率。

--> 实现缓冲方式的文件存取。

2、指针是地址。技术上,任何类型的指针都可以指向内存的任何位置。但是指针的操作都是基于类型的。

指针操作是相对于指针的基类型而执行的,尽管在技术上指针可以指向对象的其它类型,但指针始终认为它指向的是其基类型的对象。指针操作受指针类型而不是它所指向的对象类型的支配。

3、指针表达式:原则上讲,涉及指针的表达式符合其它 C 表达式的规则。

(1、)printf("%p", …); /* 以宿主计算机使用的格式显示地址 */

(2、)指针转换:

--> void * 型:为 Generic Pointer,常用来说明基类型未知的指针。 它允许函数指定参数,此参数可以接受任何类型的指针变量而不必报告类型失配。 当内存语义不清时,也常用于指原始内存。

[例]一个函数可以返回“多个”类型(类似于 malloc()): void * f1(void *p) { return p; } void main(void) { int a=100; int *pp=&a; printf("%d/n", *((int*)f1(pp))); }

--> 其它类型的指针转换必须使用明确的强制类型转换。但要注意,一种类型的指针向另一种类型转换时可能会产生不明确的行为。

--> 允许将 int 型转换为指针或将指针转换为 int 型,但必须使用强制类型转换,且转换结果是已定义的实现,可能导致非定义的行为。(转换 0 时不需要强制转换,因为它是 NULL 指针)

--> 为了与 C++ 更好地兼容,很多 C 程序员舍弃了指针转换,因为在 C++ 中,必须使用强制类型转换,包括 void * 型。

(3、)指针算术:可以作用于指针的算术操作只有加法和减法。 --> 指针与整数的加、减法。 --> 从一个指针中减去另一个指针(主要目的是为了求指针偏移量)。

(4、)指针比较:主要用于两个或多个指针指向共同对象的情况。

4、初始化指针:

(1、)非静态局部指针已声明但未赋值前,其值不确定。

(2、)全局和静态局部指针自动初始化为 NULL。

(3、)赋值前使用指针,不仅可能导致程序瘫痪,还有可能使 OS 崩溃,错误可谓严重之至!

(4、)[习惯用法]对于当前没有指向合法的内存空间的指针,为其赋值 NULL。 因为 C 保证空地址不存在对象,所以任何空指针都意味着它不指向任何对象,不应该使用它。 用空指针来说明不用的指针基本上就是程序员遵守的协定(但并不是 C 的强制规则)。

[例]int *p=NULL; *p=1; /* ERROR!*/ /* 能通过编译,但对 0 赋值通常会使程序崩溃 */

5、函数指针:void process(char *a, char *b, int (* appel)(const char *, const char *)); /* 函数是通过指针调用的,appel 是函数指针 */

在工作中常需要向过程传入任意函数,有时需要使用函数指针构成的数组。如在解释程序运行时,常需要根据语句调用各种函数。此时,用函数指针构成的数组取代大型 switch 语句是非常方便的,由数组下标实施调用。

6、动态分配(Dynamic Allocation)内存空间:指程序在运行中取得内存空间。

全局变量在编译时分配内存空间,非静态局部变量使用栈区,两者在运行时使用固定长度的内存空间。

(1、)为了实现动态的数据结构,C 动态分配函数在堆区分配内存空间。堆是系统的自由内存区,空间一般都很大。

(2、)核心函数:malloc() 和 free()。

(3、)堆区是有限的,分配内存空间后必须检查 malloc() 的返回值,确保指针使用前它是非空的。

(4、)绝对不要用无效的指针调用 free(),否则将破坏自由表。

7、杂项:主要是一些常见的与指针有关的问题。

(1、)在某些情况下,用 const 来限制指针对提高程序的安全性有重要意义。大家可以仔细看一下微软编写的系统函数便会有所体会。

(2、)指针的错误难于定位,因为指针本身并没有问题。问题在于,通过错误的指针操作时,可能引入最难排除的错误:程序对未知内存区进行读或写操作。

--> 读:最坏的情况是取得无用数据。 --> 写:可能冲掉其它代码或数据。

这种错误可能要到程序执行了相当一段时间后才出现,因此把排错工作引入歧途。

虽然使用指针可能导致奇怪的错误,但是我们不能因此而放弃使用指针。(记得我刚开始做项目的时候,由于惧怕指针,在我做的第一个项目里,我使用了 200 多个数组……)使用指针时应当小心谨慎,请记住,一定要首先确定指针指向内存的什么位置。

[例1]未初始化的指针(uninitialized pointer)。 int *p; scanf("%d", p); /* ERROR */ /* 把值写到未知的内存空间 */

运行小程序时,p 中随机地址指向安全区域(不指向程序的代码和数据,或 OS)的可能性较大。但随着程序的增大,p 指向重要区域的概率增加,最终使程序瘫痪。

[例2]对内存中数据放置的错误假定。 char a1[80], a2[80]; char *p1=a1, *p2=a2; if (p1<p2) { /* 处理 */ } /* 概念错误!*/

通常程序员不能确保数据处于内存中的同样位置,不能确保各种平台都用同样格式保存数据,也不能确保各种编译器处理数据的方法完全相同。比较指向不同对象的指针时,容易产生意外结果。

[例3]假设相邻数组顺序排列,从而简单地对指针增值,希望跨越数组边界。 int a1[10], a2[10]; int *p=a1; for (int i=0; i<20; i++) *p++=i; /* ERROR */

尽管在某些条件下可适用于某些编译器,但这是假设两个数组在内存中先存放 a1,随后存放 a2 的条件下进行的。这种情况并不常有。

[例4]时刻关注指针的当前位置。 char a[80], *p; p=a; /* ERROR */ do { /* p=a; 应该把这句放在循环体内 */ gets(a); /* 读入 */ while(*p) printf("%d", *p++); } while(strcmp(a, "DONE"));

第一次循环,p 从 a[0] 开始。第二次循环时,p 值从第一次循环的结束点开始。此时 p 可能指向另一个串,另一个变量,甚至是程序的某一段。

[例5]表达式的地址。 int *p=&(a+b); /* ERROR */

在 C 中,& 只能获取一个变量的地址。在程序中,除变量外,其它表达式的存储空间是不可访问的。

四、指针与数组:

数组和指针关系非常紧密,几乎就像 UNIX 和 C 的关系一样:二者是不分家的。实际上,一维数组名可被不严格地认为是指针常量:可执行指针的操作;可按地址单元赋值和引用;也可用来初始化同型的指针变量;但不能对一维数组名赋值。

char p[]="Hello, World!"; char *p="Hello, Wrold!"; /* 两条语句完全等价 */

1、指针数组:常用于放置指向串的指针。

在程序中,对串的操作是很常见的,但就介绍过的技术来看还是有一定的局限性的。此外,在一些特殊的项目中,可能需要把若干个串作为参数传递给函数,但串的长度不能确定,这是采用定长的数组显然不合适。

[例1]给定错误编号后,输出错误信息。 void print_error(int n) { static char *erreur[]={"Syntax Error/n", "Variable Error/n", "Disk Error/n"}; printf("%s", error[n]); }

[例2]典型应用:打印命令行参数(类似于 echo 命令)。 void main(int argc, char **argv, char **env) { while(*++argv) printf("%s ", *argv); }

[例3]访问命令行参数中的字符(对 argv 施加第二下标)。 void main(int argc, char *argv[], char **env) { int i, j; for (i=0; i<argc; ++i) { j=0; while(argv[i][j]) { putchar(argv[i][j]); j++; } printf("/n"); } }

[注意] --> 不要像数组那样按下标单独赋值,也不要使用 *(p+n) 这样的间接引用来修改某个单元的值,这样做都可能引起运行错,因为字符串常量是在常量区,是不允许被修改的。严格来说,用一个普通指针指向一个常量是不对的,会产生一个“cannot convert from 'const type *' to 'type *'”的编译错误。字符指针的初始化是一个特例,可用一个字符指针变量指向一个字符串常量,但仍然不能修改其内容。将const char * 强制转换成 char * 能获得正确的地址,可引用,但仍然不能修改其内容。(将const int * 转换成 int * 则无法获得正确的地址,这是字符串常量的特殊性)。

--> 按下标或*(p+n)这样的间接引用来读取某单元的内容有时是可行的,这取决于不同的编译器对字符串常量的存放方式:在常量区还是堆区;采用何种存储结构;是否连续(块链就不连续)。而字符数组这样引用和赋值总是可行的,因为它开辟了一块连续的空间并复制了内容。

2、对于数组 a[],输出 a、*a、&a 的值都一样,但我们并不能认为含义一样。a 有双重含义,只不过通过 printf() 表现出来的是首元素地址;*a 中 a 取了其数组指针含义,值为第一个元素的地址;&a 中 a 取了其自定义数据类型含义,值为结构中第一个元素的地址。若把 a 赋给数组指针变量 p,则 &p 是 p 的地址,因为 p 并不具备数组这一层自定义数据类型含义。用数组名初始化指针或数组指针时做了自动类型转化,取了其指针含义。

也就是说:不要把赋值就理解为一样了。这个等到将来学过 C++ 后就会有一个比较深入的认识了。C++ 的运算符重载和自动类型转化隐含了很多东西。

3、纯指针数组:即 JAVA 中定义数组的方法。这样的数组是真正的多级指针,不能用 sizeof() 求数组大小,但可以实现多维动态,而且存取效率较高(无须做乘法来寻址)。 [例]int n1,n2,n3;

int *p1=new int[n1]; //p1[n1],但与普通数组名不同,p1 不是指针常量,而是有内存的指针变量。

int **p2=new int*[n1]; for(int I=0;I<n2;I++) p2[I]=new int[n2];

int ***p3=new int**[n1]; for(int j=0;j<n1;j++) { p3[j]=new int*[n2]; for(int k=0;k<n2;k++) p2[j][k]=new int[n3]; //三维动态数组p3[n1] [n2] [n3] } p2-

4、指向指针的指针(Pointers to Pointers):指针数组名就是二级指针。 在实际工作中很少需要使用指向指针的指针,它容易引起概念错误。

5、动态分配的数组(Dynamically Allocated Array):

[例1]char *p=(char *) malloc(80); /* 必须对 p 进行下面的测试,以避免使用空指针 */ if (!p) { printf("ERROR/n"); exit(1); }

get(p); for (int i=strlen(p)-1; i>=0; i--) putchar(p[i]); free(p);

[例2]int (*p)[10] = (int (*)[10]) malloc(40*sizeof(int)); /* 为了与 C++ 兼容,必须舍弃所有的指针转换 */ if (!p) { printf("ERROR/n"); exit(1); }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值