周立功教授数年之心血之作《程序设计与数据结构》,电子版已无偿性分享到电子工程师与高校群体,在公众号回复【程序设计】即可在线阅读。书本内容公开后,在电子行业掀起一片学习热潮。经周立功教授授权,本公众号特对本书内容进行连载,愿共勉之。

第一章为程序设计基础,本文为1.8.3 指针数组中的第二要点:字符串与指针的指针、第三要点:字符串与二维数组。

>>>>1.字符串与指针的指针

除了作为或&的操作数外,指针数组的数组名在表达式中等价一个双重指针常量,其右值为数组变量的首地址。比如:

int main(int argc, char *argv[])

其完全等价于

int main(int argc, char **argv)

显然,如果要访问一个指针数组,使用指向指针的指针最为方便,但稍不注意偶尔也会写出错误的程序,详见程序清单 1.45。

程序清单1.45一个错误的范例程序

1 #

2 int main(void)

3 {

4 char **;

5 char * [5] = {"eagle", "cat", "and", "dog", "ball"};

7 = ;

8 while(** != NULL)

9 ("%sn", *(++));

10 0;

11 }

由于字符串末尾的空字符或''与空指针或NULL的值0恰好相等,因此上述代码的编译和执行结果都是正确的,但确实是一个错误的示例。因为大多数编译器在编译程序清单 1.45(9)时进行了类型转换,将**由char类型转换成了void *,或将NULL由void *转换成了char类型,但在编译时一般会给出一条警告信息,因为空字符是整数类型,而空指针是指针类型。

之所以写出这样的代码,说明程序员完全没有理解“""”和NULL的区别。如果编译器完全禁止char与指针之间的相互转换,则上述代码可能编译失败。由此可见,需要认真对待编译器给出的每一条警告信息,并分析出现警告信息的原因,而不是仅仅编译通过、程序执行结果正确就万事大吉了。

程序清单 1.46针对程序清单 1.45的一种解决方案。其首先判断*是否为空指针,如果为空指针,则退出循环;如果不是,则输出显示该字符串,然后将加1指向下一个字符串。

程序清单 1.46 用指针数组变量与双重指针变量处理多个字符串

1 #

2 int main(void)

3 {

4 char **;

5 char * [6] = {"eagle", "cat", "and", "dog", "ball", "NULL"};

7 = ;

8 while(* != NULL)

9 ("%sn", *(++));

10 0;

11 }

由于指针类型的数组也是一维数组,因此双重指针的算术运算与普通指针的算术运算十分相似。当指向时,[i]、[i]、*(+i)与*(+i)是等效的访问指针数组变量元素的4种表现形式。[i]指向了第i个字符串的首地址,即第i个字符串第1个字符的地址。若访问指针[0]所指向的目标变量,则*[0]的值是字符串""的第1个字符M。当然,*[0]也可以写成*[0]、**、**等表现形式。

程序清单 1.47是针对程序清单 1.45的另一种解决方案。其首先判断*所指向的是否为空字符串(即只包含''的字符串,也就是字符串第0个元素为''的字符串),如果为空字符串,则退出循环;如果不是,则输出显示该字符串常量指针和指针常量,然后将加1指向下一个字符串。

程序清单1.47用指针数组变量与双重指针变量处理多个字符串(2)

1 #

2 int main(void)

3 {

4 char **;

5 char * [6] = {"eagle", "cat", "and", "dog", "ball", "NULL"};

7 = ;

8 while(** != ""[0]) // ""[0]等价于''

9 ("%sn", *(++));

10 0;

11 }

在实际的应用中,程序清单 1.47(9)中的“""[0]”是一种非常少见的用法。如果用''代替它则功能一样,执行效率还稍微高一点。由于字符串常量是只读字符数组,因此字符串常量“""”就是只有字符串结束字符''的字符串常量,即数组变量的第0个元素的值为''。由于“""”是一个数组变量,因此可以使用下标运算符对“""”进行求值运算,获得指定的数组元素,从而得到“""[0]”的值''。一般来说,在大多数程序中都直接使用''不使用""[0],而程序清单 1.47(8)之所以使用“""[0]”有两重意义:

●与程序清单 1.47(5)对应,使程序的含义更清晰。当*指向最后一个字符串的第0个元素时,则结束循环;注意:这个字符串的第0个元素与其他任何一个字符串的第0个元素都不相同。

●可移植性更好。如果将来C语言的字符修改了结束字符的定义,则程序也不必修改。比如,为了支持中文,将一个中文字作为一个字符,则字符类型必须修正,因为它不再是8位,所以其结束字符也可能修改。

如果要存储静态的表格式数据,当然应该用数组。搜索程序必须知道数组中有多少个元素,对这个问题的处理方法是传递一个数组长度参数。这里采用的另一种方法是选择指针数组方式。即:

const char * [6] = {"eagle", "cat", "and", "dog", "ball", NULL};

即在表尾增加了一个NULL指针,这个NULL指针使函数在搜索这个表时能够检测到表的结束,而无需预先知道表的长度,其相应的搜索范例程序详见程序清单 1.48。

程序清单1.48搜索范例程序

1 int (char *word, char *[])

2 {

4 for(int i = 0; [i] != NULL; i++)

5 if((word, [i] == 0))

6 i;

7 -1;

8 }

在C语言中,字符串数组参数可以是char *[]或char **,虽然它们都是等价的,但前一种形式能将参数的使用方式表达得更清楚。

这里采用的搜索算法称为顺序搜索,它逐个查看每个数据是不是要找的那一个。如果数据的个数不多,顺序搜索也很快。标准库中提供了一些函数,它们可以处理某些特定类型的顺序搜索问题,比如,和能搜索给定的字符串中的字符或子串,如果对某个数据类型有这种函数就应该直接使用它。

虽然搜索看起来非常简单,但它的工作量与被搜索数据的个数成正比。如果要找的的数据并不存在,而数据量加倍也会使搜索的工作量加倍。这是一种线性关系,其运行时间是数据规模的线性函数,因此这种搜索也被称为线性搜索。

>>>>2.字符串与二维数组

有两种风格描述C风格的字符串数组,即二维数组和指针数组,比如:

char [][6] = {"eagle", "cat", "and", "dog", "ball"};

char * [5] = {"eagle", "cat", "and", "dog", "ball"};

其中,第1个声明创建了一个二维数组,详见图 1.16(a)。第2个声明创建了一个指针数组,每个指针元素都初始化为指向各个不同的字符串常量,相加图 1.16(b)。

图 1.16 矩形数组和不规则数组

如果改用二维数组代替指针数组修改程序清单 1.44,这两种方法使用了相同的初始化列表,显示字符串的for循环代码也相同,因此只要修改形参和局部变量的声明即可。由于数组变量名的值是指针,因此无论传递给函数的是指针还是数组名,函数都能运行。尽管它们的声明不同,但从某些方面看起来,它们非常相似,两者都代表5个字符串。当使用一个下标时都分别表示一个字符串,但两者的类型并不相同。当使用2个下标时都分别表示一个字符,比如,[1][2]表示数组中第2个指针指向的字符串的第3个字母't'。初看上去二维数组的效率似乎低一些常量指针和指针常量,因为它每一行的长度都被固定为刚好能容纳最长的关键字,但它不需要任何指针。另一方面,指针数组也要占用内存,但是每个字符串常量占用的内存空间只是它本身的长度。

如果它们的长度差不多,那么二维数组形式更紧凑一些。如果各个字符串的长度差别很大,绝大多是字符串都很短,只有少数几个很长,那么使用指针数组形式会更紧凑一些,取决于指针所占用的空间是否小于每个字符串都存储于固定长度的行所浪费的空间。实际上,除了非常巨大的表,它们之间的差别是非常小的,所以根本不重要。除非要改变其中的任何一个字符串,二维数组是更好的选择。

在公众号后台回复关键字【程序设计】,可在线阅读全书。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注