C语言的语义缺陷(一)

简介: C语言的语义缺陷(一)



前言

       在一个句子,哪怕其中的每个单词都拼写正确,而且语法也无懈可击,仍然可能有歧义或者并非书写者希望表达的意思。程序也有可能表面上是一个意思,而实际上的意思却相差甚远。本篇讲述了几种可能引起上述歧义的程序书写方式

1、指针与数组

C语言中,指针与数组这两个概念之间的联系是十分密切的

C语言中的数组有两个需要注意的方面:

1、C语言中只有一维数组,而且数组大小在声明数组时就需要确定下来,不过数组中的元素可以是任何类型的对象,当然也可以是另外一个数组,这样要”仿真“出一个多维数组就不是一件难事

C99标准允许变长数组(VLA),GCC编译器中实现了变长数组,但细节与C99标准不完全一致

2、对于一个数组,我们只能做两件事:确定该数组的大小以及获得指向该数组首元素(下标为零)的指针其它有关数组的操作,哪怕它们乍看上去是以数组下标进行运算的,实际上都是通过指针进行的。换句话说,任何一个数组下标运算都等同于一个对应的指针运算,因此我们完全可以依据指针行为定义数组下标的行为。

指针与数组的关系

       1、如果一个指针指向的是数组中的第一个元素,那么我们只要给这个指针加/减1,就能够得到指向该数组中下/上一个元素的指针,其它数字同理类推

       这句话暗示了这样一个事实:给一个指针加上一个整数,与给该指针的二进制表示加上同样的整数,二者的含义截然不同。如果指针ip指向一个整数,那么ip+1指向的是该计算机内存中的下一个整数,在大多数现代计算机中,它都不同于ip所指向地址的下一个内存位置

2、如果两个指针指向同一数组中的元素,这两个指针相减的结果是有意义的:

int *q = p + i;

p指针与q指针的差值就是两指针指向的元素之间的位置的差值(意思是这个意思) ,但是如果它们指向的不是同一个数组,那么即使它们所指向的地址在内存中的位置正好间隔一个数组元素的整数倍,所得的结果仍然无法保证其正确性

3、如果我们在该出现指针的地方,采用了数组名代替,数组名就是该数组首元素的地址:

//正确,将数组a中下标为0的元素的地址赋值给了指针p
p = a
//错误
p = &a

p=&a这种写法在ANSI C标准中是非法的,因为&a是一个指向数组的指针此时取出的地址是整个数组的地址而非数组首元素的地址,而p是一个指向整型变量(数组中某个整型元素)的指针,它们的类型不匹配。

4、p是一个指向数组的指针,p = p + 1 == p++

5、*数组名,此时的数组名表示数组首元素地址,该语句可以将数组首元素的值替换

*a = 84;

它等价于*(a+0),同理,*(a+1)是数组a中下标为1的元素的引用,依次类推。概而言之,*(a+i)即数组a中下标为i的元素的引用,由于这种写法十分常用,所以C语言将它变为了另一种形式a[i],

a[i] == *(a+i)

二维数组与指针

      二维数组实际上是以数组为元素的数组,尽管我们也可以完全依据指针编写操作一维数组的程序,而且这样做在一维数组的情形下并不困难,但是对于二维数组,从记法的便利性来说,采用下标的形式就几乎是不可代替的了。

       定义一个二维数组calendar,它是一个有着12个数组类型元素的数组,每个数组类型元素又是一个有着31个整型元素的数组:

int calendar[12][31];

定义一个指向整型变量的指针p:

int* p;

       calendar[4]就表示calendar数组的第五个数组类型元素,是calendar数组中12个有着31个整型元素的数组之一。因此,calendar[4]的行为也就表示为一个有着31个整型元素的数组的行为,例如:sizeof(calendar[4]) == sizeof(int) * 32

       p = calendar[4]就表示指针指向了该数组首元素的位置,所以除了用下标表示该数组中某个元素的位置“i = calendar[4][7]”,我们还可以用上面说到的“a[i] == *(a+i)”的方式来表示“i = *(calendar[4] + 7)”同样的我们还可以进一步化简“i = *((calendar + 4) + 7)”即:

calendar[4][7] == *(calendar[4] + 7)== *((calendar + 4) + 7)

明显的,对于二维数组而言,使用下标的形式比使用指针的形式更加的简洁

       如果是p = calendar呢?它是非法的,因为calendar是一个二维数组,即”数组的数组“,如果是合法的p应该是一个指向数组的指针,而p是一个指向整型变量的指针,这句话试图将一种类型的指针赋值给另一种类型的指针,故非法。

数组指针

很显然,我们需要一种方法来声明指向数组的指针:

int (*ap)[31];

它的实际效果是:声明*ap是一个拥有31个整型元素的数组,因此ap就是一个指向这样的数组的指针,因此我们可以这样写:

int calendar[12][31];
int (*monthp)[31];
monthp = calendar;

monthp将指向数组calendar的首元素,即数组calendar的12个有着31个元素的数组类型元素之一

2、非数组的指针

       在C语言中,字符串常量代表了一块包括字符串中所有字符以及一个空字符('\0')的内存区域的地址,假设有这样两个字符串s和t,我们希望将这两个字符串连接成单个字符串r,要做到这一点我们可以利用库函数strcpy和strcat:

char* r;
strcpy(r,s);
strcat(r,t);

       但是这样的写法是非法的,因为r所指向的位置不确定,此外r不仅仅需要指向一个地址,还要让该地址处有足够多的空间用来容纳字符串,这个空间应该是以某种方式已经被分配了的:

char* r[100];
strcpy(r,s);
strcat(r,t);

       同时为了防止指定的数组大小依然不能放下要拼接的字符串,我们可以使用malloc函数来自主申请合适的内存空间来存放字符串:

char* r, *malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r,s);
strcat(r,t);

但是这个例子仍有三个错误:

  1. malloc可能申请内存空间失败,此时它会返回一个空指针来作为“内存分配失败”事件的信号
  2. 为r申请的内存空间在使用过后需要显式的释放
  3. 调用malloc时并未分配足够的内存,这是因为strlen返回参数中并不包含字符串结束标志("\0"),而r和t两个字符串都是有结束标志("\0")做结尾的,因此如果strlen(s)的值为n,那么实际上字符串需要n+1个字符的空间

我们在总结了这三个问题后,终于写出了下面正确的例子:

char* r, *malloc();
r = malloc(strlen(s) + strlen(t) + 1);//这里就是+1没有写错
//开辟失败检验
    if(!r)
    {
        //解释函数
        complain();
        exit(1);
    }
strcpy(r,s);
strcat(r,t);
//使用完后记得释放内存空间
free(r);

关于strcpy与strcat的使用我们放在了:c语言字符函数和字符串函数的模拟实现------附带习题

关于动态内存函数malloc的使用我们放在了:C语言动态内存管理函数

3、作为参数的数组声明

       在C语言中,将一个数组作为参数传递给函数的行为是无意义的(只是看起来更好理解一点),因为如果我们将数组名作为参数,那么数组名会被立刻默认地转换为指向该数组第一个元素的指针:

char hello[] = "hello";

声明了hello是一个字符数组,如果将该数组作为参数传递给一个函数:

printf("%s\n",hello);

实际上就是将该数组首元素的地址作为参数传递给该函数:

printf("%s\n",&hello[0]);

同样的,写法一与写法二、写法三与写法四所表达的效果是一样的:

//写法一
int strlen(char s[])
{
}
//写法二
int strlen(char* s)
{
}
//写法三
main(int argc,char* argv[])  //argv是一个指向指针数组首元素的指针,该首元素也为指针
{
}
//写法四
main(int argc,char** argv) //argv是一个二级指针,它可以用来指向另一个一级指针,如果是上面的写法那                
                           //就是指针数组的首元素
{
}

但是,并不是所有情形下都会有这样的自动转换:

extern char *hello 与 extern char hello[] 所代表的意思是不同的

解释:

  1. extern char *hello:表示 hello 是一个指向字符(char)的指针。它并没有为该指针分配内存或定义具体内容,只是告诉编译器这个变量在其他地方已经定义了
  2. extern char hello[]:表示 hello 是一个字符数组(C 字符串)。只是告诉编译器这个变量在其他地方已经定义了,并且可以被当作字符串来访问和操作

4、避免“举隅法”

“举隅法”是一种文学修辞上的手段,类似于以微笑表示喜悦、赞许之情,或以隐喻表示指代物与被指物的相互关系。在C语言中的“举隅法”就是混淆指针与指针所指向的数据,对于字符串的情形,编程人员经常犯这样的错误:

char *p,*q;
p = "xyz";

我们可能会认为p的值就是字符串“xyz”,但实际上p的值是一个指向由‘x’,‘y’,‘z’,‘\0'这四个字符组成的数组的起始元素的指针,因此如果我们执行:

p = q;

p和q现在是两个指向内存中同一地址的指针,这个赋值语句并没有同时赋值内存中的字符:

复制指针并不同时复制指针所指向的数据

因此,当我们执行完:

q[1] = 'Y';

q和p所指向的内存中存储的都是字符串'xYz',而不是p中存储'xYz',q中存储'xyz'

       ANSI C标准中禁止对常量字符串(string literal)进行修改,尽管有些C编译器如LCC v3.6还允许q[1] = 'Y'这种行为,但它仍然是不被提倡的

5、空指针并非空字符串

C语言的任意编译器均规定由常数0转换而来的指针不等于任何有效的指针即0也可以作为一个指针来使用该指针为空指针,处于代码文档化的考虑,常数0经常用一个符号NULL来代替:

#define NULL 0

当常数0被转换为指针使用时,这个指针绝不可能被解除引用,即在将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容,下面的写法是合法的:

if(p == (char*) 0) ...

但是如果写成下面这样,就是非法的了:

if(strcmp(p,(char*) 0) == 0) ...

这是因为strcmp函数会查看它的指针参数所指向内存中的内容,而我们在将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。

同样的,下面的两种写法也是非法的:

printf(p); 与 printf("%s",p);

关于数组与指针的详细内容我们放在了:C语言指针精简版(二)中进行讲解

~over~

相关文章
|
6月前
|
NoSQL 程序员 Redis
C语言字符串的设计缺陷
C语言字符串的设计缺陷
59 1
|
存储 编译器 C语言
C语言编程陷阱:语义陷阱
C语言中只有一维数组,数组大小必须在编译器就作为一个常数确定下来。 C语言中数组的元素可以是任何类型的对象。
49 1
|
C语言
让你提前认识软件开发(3):学校C语言教材的缺陷
第1部分 重新认识C语言 学校C语言教材的缺陷           我在走出校门的时候非常的“轻狂”,认为自己在学校里面已经学得够多了,工作就只算是小菜一碟。
1146 0
|
14天前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
34 10
|
14天前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
34 9
|
14天前
|
存储 Unix Serverless
【C语言】常用函数汇总表
本文总结了C语言中常用的函数,涵盖输入/输出、字符串操作、内存管理、数学运算、时间处理、文件操作及布尔类型等多个方面。每类函数均以表格形式列出其功能和使用示例,便于快速查阅和学习。通过综合示例代码,展示了这些函数的实际应用,帮助读者更好地理解和掌握C语言的基本功能和标准库函数的使用方法。感谢阅读,希望对你有所帮助!
29 8
|
14天前
|
C语言 开发者
【C语言】数学函数详解
在C语言中,数学函数是由标准库 `math.h` 提供的。使用这些函数时,需要包含 `#include <math.h>` 头文件。以下是一些常用的数学函数的详细讲解,包括函数原型、参数说明、返回值说明以及示例代码和表格汇总。
36 6
|
14天前
|
存储 C语言
【C语言】输入/输出函数详解
在C语言中,输入/输出操作是通过标准库函数来实现的。这些函数分为两类:标准输入输出函数和文件输入输出函数。
87 6
|
14天前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
45 6
|
14天前
|
C语言 开发者
【C语言】断言函数 -《深入解析C语言调试利器 !》
断言(assert)是一种调试工具,用于在程序运行时检查某些条件是否成立。如果条件不成立,断言会触发错误,并通常会终止程序的执行。断言有助于在开发和测试阶段捕捉逻辑错误。
23 5