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~

相关文章
|
8月前
|
NoSQL 程序员 Redis
C语言字符串的设计缺陷
C语言字符串的设计缺陷
78 1
|
存储 编译器 C语言
C语言编程陷阱:语义陷阱
C语言中只有一维数组,数组大小必须在编译器就作为一个常数确定下来。 C语言中数组的元素可以是任何类型的对象。
64 1
|
C语言
让你提前认识软件开发(3):学校C语言教材的缺陷
第1部分 重新认识C语言 学校C语言教材的缺陷           我在走出校门的时候非常的“轻狂”,认为自己在学校里面已经学得够多了,工作就只算是小菜一碟。
1158 0
|
1月前
|
存储 编译器 C语言
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
36 3
|
1月前
|
存储 算法 安全
【C语言程序设计——函数】分数数列求和1(头歌实践教学平台习题)【合集】
if 语句是最基础的形式,当条件为真时执行其内部的语句块;switch 语句则适用于针对一个表达式的多个固定值进行判断,根据表达式的值与各个 case 后的常量值匹配情况,执行相应 case 分支下的语句,直到遇到 break 语句跳出 switch 结构,若没有匹配值则执行 default 分支(可选)。例如,在判断一个数是否大于 10 的场景中,条件表达式为 “num> 10”,这里的 “num” 是程序中的变量,通过比较其值与 10 的大小关系来确定条件的真假。常量的值必须是唯一的,且在同一个。
20 2
|
1月前
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
65 16
|
1月前
|
存储 编译器 C语言
【C语言程序设计——函数】回文数判定(头歌实践教学平台习题)【合集】
算术运算于 C 语言仿若精密 “齿轮组”,驱动着数值处理流程。编写函数求区间[100,500]中所有的回文数,要求每行打印10个数。根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码。如果操作数是浮点数,在 C 语言中是不允许直接进行。的结果是 -1,因为 -7 除以 3 商为 -2,余数为 -1;注意:每一个数据输出格式为 printf("%4d", i);的结果是 1,因为 7 除以 -3 商为 -2,余数为 1。取余运算要求两个操作数必须是整数类型,包括。开始你的任务吧,祝你成功!
52 1
|
1月前
|
C语言
【C语言程序设计——函数】亲密数判定(头歌实践教学平台习题)【合集】
本文介绍了通过编程实现打印3000以内的全部亲密数的任务。主要内容包括: 1. **任务描述**:实现函数打印3000以内的全部亲密数。 2. **相关知识**: - 循环控制和跳转语句(for、while循环,break、continue语句)的使用。 - 亲密数的概念及历史背景。 - 判断亲密数的方法:计算数A的因子和存于B,再计算B的因子和存于sum,最后比较sum与A是否相等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台对代码进行测试,预期输出如220和284是一组亲密数。 5. **通关代码**:提供了完整的C语言代码实现
61 24
|
1月前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
63 23
|
1月前
|
算法 C语言
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
本文档介绍了如何编写两个子函数,分别求任意两个整数的最大公约数和最小公倍数。内容涵盖循环控制与跳转语句的使用、最大公约数的求法(包括辗转相除法和更相减损术),以及基于最大公约数求最小公倍数的方法。通过示例代码和测试说明,帮助读者理解和实现相关算法。最终提供了完整的通关代码及测试结果,确保编程任务的成功完成。
68 15

热门文章

最新文章