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~

相关文章
|
5月前
|
NoSQL 程序员 Redis
C语言字符串的设计缺陷
C语言字符串的设计缺陷
50 1
|
存储 编译器 C语言
C语言编程陷阱:语义陷阱
C语言中只有一维数组,数组大小必须在编译器就作为一个常数确定下来。 C语言中数组的元素可以是任何类型的对象。
45 1
|
C语言
让你提前认识软件开发(3):学校C语言教材的缺陷
第1部分 重新认识C语言 学校C语言教材的缺陷           我在走出校门的时候非常的“轻狂”,认为自己在学校里面已经学得够多了,工作就只算是小菜一碟。
1141 0
|
29天前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
31 3
|
20天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
33 10
|
13天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
19天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
47 7
|
19天前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
26 4
|
24天前
|
存储 编译器 C语言
C语言函数的定义与函数的声明的区别
C语言中,函数的定义包含函数的实现,即具体执行的代码块;而函数的声明仅描述函数的名称、返回类型和参数列表,用于告知编译器函数的存在,但不包含实现细节。声明通常放在头文件中,定义则在源文件中。
|
30天前
|
C语言
c语言回顾-函数递归(上)
c语言回顾-函数递归(上)
31 2