概述
Joel Spolsky认为,对指针的理解是一种aptitude,不是通过训练就可以达到的。虽然如此,我还是想谈一谈这个C/C++语言中最强劲也是最容易出错的要素。
鉴于指针和目前计算机内存结构的关联,很多C语言比较本质的特点都孕育在其中,因此,本篇和第六、第七两篇我都将以指针为主线,结合在实际编程中遇到的问题,来详细谈谈关于指针的几个重要方面。
指针类型的本质分析
1、指针的本质
指针的本质:一种复合的数据类型。下面我将以下面几个作为例子进行展开分析:
a)、int *p;
b)、int **p;
c)、int (*parValue)[3];
d)、int (*pFun)();
分析:
所谓的数据类型就是具有某种数据特征的东东,比如数据类型char,它的数据特征就是它所占据的内存为1个字节, 指针也很类似,指针所指向的值也占据着内存中的一块地址,地址的长度与指针的类型有关,比如对于char型指针,这个指针占据的内存就是1个字节,因此指针也是一种数据类型,但我们知道指针本身也占据了一个内存空间地址,地址的长度和机器的字长有关,比如在32位机器中,这个长度就是4个字节,因此指针本身也同样是一种数据类型,因此,我们说,指针其实是一种复合的数据类型,
好了,现在我们可以分析上面的几个例子了。
假设有如下定义:
int nValue;
那么,nValue的类型就是int,也就是把nValue这个具体变量去掉后剩余的部分,因此,上面的4个声明可以类比进行分析:
a)、int *
*代表变量(指针本身)的值是一个地址,int代表这个地址里面存放的是一个整数,这两个结合起来,int *定义了一个指向整数的指针,类推如下:
b)、int **
指向一个指向整数的指针的指针。
c)、int (*)[3]
指向一个拥有三个整数的数组的指针。
d)、int (*)()
指向一个函数的指针,这个函数参数为空,返回值为整数。
分析结束,从上面可以看出,指针包括两个方面,一个是它本身的值,是一个内存中的地址;另一个是指针所指向的物,是这个地址中所存放着具有各种各样意义的数据。
2、对指针本身值的分析
下面例子考察指针本身的值(环境为32位的计算机):
void *p = malloc( 100 );
请计算sizeof ( p ) = ?
char str[] = “Hello” ;
char *p = str ;
请计算sizeof ( p ) = ?
void Func ( char str[100])
{
请计算 sizeof( str ) = ? //注意,此时,str已经退化为一个指针,详情见
//下一篇指针与数组
}
分析:上面的例子,答案都是4,因为从上面的讨论可以知道,指针本身的值对应着内存中的一个地址,它的size只与机器的字长有关(即它是由系统的内存模型决定的),在32位机器中,这个长度是4个字节。
3、对指针所指向物的分析
现在再对指针这个复合类型的第二部分,指针所指向物的意义进行分析。
上面我们已经得到了指针本身的类型,那么将指针本身的类型去掉 “*”号就可得到指针所指向物的类型,分别如下:
a)、int
所指向物是一个整数。
b)、int*
所指向物是一个指向整数的指针。
c)、int ()[3]
()为空,可以去掉,变为int [3],所指向物是一个拥有三个整数的数组。
d)、int ()()
第一个()为空,可以去掉,变为int (),所指向物是一个函数,这个函数的参数为空,返回值为整数。
4、附加分析
另外,关于指针本身大小的问题,在C++中与C有所不同,这里我也顺带谈一下。
在C++中,对于指向对象成员的指针,它的大小不一定是4个字节,这主要是因为在引入多重虚拟继承以及虚拟函数的时候,有些附加的信息也需要通过这个指针进行传递,因此指向对象成员的指针会增大,不论是指向成员数据,还是成员函数都是如此,具体与编译器的实现有关,你可以编写个很小的C++程序去验证一下。另外,对一个类的静态成员(static member,可以是静态成员变量或者静态成员函数)来说,指向它的指针只是普通的函数指针,而不是一个指向类成员的指针,所以它的大小不会增加,仍旧是4个字节。
----------------------------------------------------------------------------
指针本身的相关问题
1、问题:空指针的定义
曾经看过有的.h文件将NULL定义为0L,为什么?
答案与分析:
这是一个关于空指针宏定义的问题。指针在C语言中是经常使用的,有时需要将一个指针置为空指针,例如在指针变量初始化的时候。
C语言中的空指针和Pascal或者Lisp语言中的NIL具有相同的地位。那如何定义空指针呢?
下面的语句是正确的:
char *p1 = 0;
int *p2;
if (p != 0)
{
...
}
p2 = 0;
也就是说,在指针变量的初始化、赋值、比较操作中,0会被编译器理解为要将指针置为空指针。至于空指针的内部表示是否是0,则随不同的机器类型而定,不过通常都是0。但是在另外一些场合下,例如函数的参数原型是指针类型,函数调用时如果将0作为参数传入,编译器则不能将其理解为空指针。此时需要明确的类型转换,例如:
void func (char *p);
func ((char *)0);
一般情况下,0是可以放在代码中和指针关联使用的,但是有些程序员(数量还不少呦!也许就包括你在内)不喜欢0的直白,认为其不能表示作为指针的特殊含义,于是要定义一个宏NULL,来明确表示空指针常量。这也是对的,人家C语言标准就明确说:“ NULL应该被定义为与实现相关的空指针常量”。但是将NULL定义成什么样的值呢?我想你一定见过好几种定义NULL的方法:
#define NULL 0
#define NULL (char *)0
#define NULL (void *)0
在我们使用的绝大多数计算系统上,例如PC,上述定义是能够工作的。然而,世界上还有很多其它种类的计算机,其CPU也不是Intel的。在某些系统上,指针和整数的大小和内部表示并不一致,甚至不同类型的指针的大小都不一致。为了避免这种可移植性问题,0L是一种最为安全的、最妥帖的定义方式。0L的含义是: “值为0的整数常量表达式”。这与C语言给出的空指针定义完全一致。因此,建议采用0L作为空指针常量NULL的值。
其实 NULL定义值,和操作系统的的平台有关, 将一个指针定义为 NULL, 其用意是为了保护操作系统,因为通过指针可以访问任何一块地址, 但是,有些数据是不许一般用户访问的,比如操作系统的核心数据。 当我们通过一个空(NULL)的指针去方位数据时,系统会提示非法, 那么系统又是如何知道的呢??
以windows2000系统为例, 该系统规定系统中每个进程的起始地址(0x00000000)开始的某个地址范围内是存放系统数据的,用户进程无法访问, 所以当用户用空指针(0)访问时,其实访问的就是0x00000000地址的系统数据,由于该地址数据是受系统保护的,所以系统会提示错误(指针访问非法)。
这也就是说NULL值不一定要定义成0,起始只要定义在系统的保护范围的地址空间内,比如定义成(0x00000001, 0x00000002)都会起到相同的作用,但是为了考虑到移植性,普遍定义为0 。
2、问题:与指针相关的编程规则&规则分析
指针既然这么重要,而且容易出错,那么有没有方法可以很好地减少这些指针相关问题的出现呢?
答案与分析:
减少出错的根本是彻底理解指针。
在方法上,遵循一定的编码规则可能是最立竿见影的方法了,下面我来阐述一下与指针相关的编程规则:
1) 未使用的指针初始化为NULL 。
2) 在给指针分配空间前、分配后均应作判断。
3) 指针所指向的内容删除后也要清除指针本身。
要牢记指针是一个复合的数据结构这个本质,所以我们不论初始化和清除都要同时兼顾指针本身(上述规则1,3)和指针所指向的内容(上述规则2,3)这两个方面。
遵循这些规则可以有效地减少指针出错,我们来看下面的例子:
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, “hello”);
free(str);
if(str != NULL)
{
strcpy(str, “world”);
printf(str);
}
}
请问运行Test函数会有什么样的结果?
答:
篡改动态内存区的内容,后果难以预料,非常危险。因为free(str);之后,str成为野指针,if(str != NULL)语句不起作用。
如果我们牢记规则3,在free(str)后增加语句:
str = NULL;
那么,就可以防止这样的错误发生。
一.指针。
它的本质是地址的类型。在许多语言中根本就没有这个概念。但是它却正是C灵活,高效,在面向过程的时代所向披靡的原因所在。因为C的内存模型基本上对应了现在von Neumann(冯·诺伊曼)计算机的机器模型,很好的达到了对机器的映射。不过有些人似乎永远也不能理解指针【注1】。
注1:Joel Spolsky就是这样认为的,他认为对指针的理解是一种aptitude,不是通过训练就可以达到的
指针可以指向值、数组、函数,当然它也可以作为值使用。
看下面的几个例子:
int* p;//p是一个指针,指向一个整数
int** p;//p是一个指针,它指向第二个指针,然后指向一个整数
int (*pa)[3];//pa是一个指针,指向一个拥有3个整数的数组
int (*pf)();//pf是一个指向函数的指针,这个函数返回一个整数
后面第四节我会详细讲解标识符(identifier)类型的识别。
1.指针本身的类型是什么?
先看下面的例子:int a;//a的类型是什么?
对,把a去掉就可以了。因此上面的4个声明语句中的指针本身的类型为:
int*
int**
int (*)[3]
int (*)()
它们都是复合类型,也就是类型与类型结合而成的类型。意义分别如下:
point to int(指向一个整数的指针)
pointer to pointer to int(指向一个指向整数的指针的指针)
pointer to array of 3 ints(指向一个拥有三个整数的数组的指针)
pointer to function of parameter is void and return value is int (指向一个函数的指针,这个函数参数为空,返回值为整数)
2.指针所指物的类型是什么?
很简单,指针本身的类型去掉 “*”号就可以了,分别如下:
int
int*
int ()[3]
int ()()
3和4有点怪,不是吗?请擦亮你的眼睛,在那个用来把“*”号包住的“()”是多余的,所以:
int ()[3]就是int [3](一个拥有三个整数的数组)
int ()()就是int ()(一个函数,参数为空,返回值为整数)【注2】
注2:一个小小的提醒,第二个“()”是一个运算符,名字叫函数调用运算符(function call operator)。
3.指针的算术运算。
请再次记住:指针不是一个简单的类型,它是一个和指针所指物的类型复合的类型。因此,它的算术运算与之(指针所指物的类型)密切相关。in
t a[8];
int* p = a;
int* q = p + 3;
p++;
指针的加减并不是指针本身的二进制表示加减,要记住,指针是一个元素的地址,它每加一次,就指向下一个元素。所以:
int* q = p + 3;//q指向从p开始的第三个整数。
p++;//p指向下一个整数。
double* pd;
……//某些计算之后
double* pother = pd – 2;//pother指向从pd倒数第二个double数。
4.指针本身的大小。
在一个现代典型的32位机器上【注3】,机器的内存模型大概是这样的,想象一下,内存空间就像一个连续的房间群。每一个房间的大小是一个字节(一般是二进制8位)。有些东西大小是一个字节(比如char),一个房间就把它给安置了;但有些东西大小是几个字节(比如double就是8个字节,int就是4个字节,我说的是典型的32位),所以它就需要几个房间才能安置。
注3:什么叫32位?就是机器CPU一次处理的数据宽度是32位,机器的寄存器容量是32位,机器的数据,内存地址总线是32位。当然还有一些细节,但大致就是这样。16位,64位,128位可以以此类推。
这些房间都应该有编号(也就是地址),32位的机器内存地址空间当然也是32位,所以房间的每一个编号都用32位的二进制数来编码【注4】。请记住指针也可以作为值使用,作为值的时候,它也必须被安置在房间中(存储在内存中),那么指向一个值的指针需要一个地址大小来存储,即32位,4个字节,4个房间来存储。
注4:在我们平常用到的32位机器上,绝少有将32位真实内存地址空间全用完的(232 = 4G),即使是服务器也不例外。现代的操作系统一般会实现32位的虚拟地址空间,这样可以方便运用程序的编制。关于虚拟地址(线性地址)和真实地址的区别以及实现,可以参考《Linux源代码情景分析》的第二章存储管理,在互联网上关于这个主题的文章汗牛充栋,你也可以google一下。
但请注意,在C++中指向对象成员的指针(pointer to member data or member function)的大小不一定是4个字节。为此我专门编制了一些程序,发现在我的两个编译器(VC7.1.3088和Dev-C++4.9.7.0)上,指向对象成员的指针的大小没有定值,但都是4的倍数。不同的编译器还有不同的值。对于一般的普通类(class),指向对象成员的指针大小一般为4,但在引入多重虚拟继承以及虚拟函数的时候,指向对象成员的指针会增大,不论是指向成员数据,还是成员函数。【注5】。
注5:在Andrei Alexandrescu的《Modern C++ Design》的5.13节Page124中提到,成员函数指针实际上是带标记的(tagged)unions,它们可以对付多重虚拟继承以及虚拟函数,书上说成员函数指针大小是16,但我的实践告诉我这个结果不对,而且具体编译器实现也不同。一直很想看看GCC的源代码,但由于旁骛太多,而且心不静,本身难度也比较高(这个倒是不害怕),只有留待以后了。
还有一点,对一个类的static member来说,指向它的指针只是普通的函数指针,不是pointer to class member,所以它的大小是4。
5.指针运算符&和*
它们是一对相反的操作,&取得一个东西的地址(也就是指针),*得到一个地址里放的东西。这个东西可以是值(对象)、函数、数组、类成员(class member)。
其实很简单,房间里面居住着一个人,&操作只能针对人,取得房间号码;
*操作只能针对房间,取得房间里的人。
参照指针本身的类型以及指针所指物的类型很好理解。
小结:其实你只要真正理解了1,2,就相当于掌握了指针的牛鼻子。后面的就不难了,指针的各种变化和C语言中其它普通类型的变化都差不多(比如各种转型)。
C常见错误新手看,高手闪
1.数据溢出
int a = 32767;
a = a + 1;
此时a的值为 –32768
2.(-1)&&(-1) 值为:1
非0的数,逻辑值均为1
只有数0,逻辑值为0
3.设a=5, b="6", c="7", d="8", m="2", n="2"
执行 (m = a > b) && ( n = c > b)
结果:n 的值为 2
解析:&&是短路(shortway)运算符,当&&左边表达式的值为0时,
不再执行右边的表达式
4.1 / 2 = 0
1.0 / 2 = 0.5
5.转义字符
遇到’\’字符应注意转义字符问题,比如’\\’,’\ddd’就是转义字符
常见考法:Q:”a\045+045\b”有几个字节?
A:8个字节(分别是’a’,’\045’,’+’,’0’,’4’,’5’,’\b’,’\0’)
字符串结束符
如上题,”a\045+045\b”含有最后的字符串结束符’\0’,因而长度为8个字节
注意:计算字符串长度时,sizeof会将’\0’计算在内,strlen()则不算
Main()
{
char b[] = “hello, you”;
b[5] = 0;
printf(“%s\n”, b);
}
运行结果:hello
解析:b[5]=0 ;等价于 b[5] = ‘\0’;
6.++ i , i ++
x = 5;
y1 = x++ * x++;
y2 = ++x * ++x;
此时y1的值为25,y2的值为49
7.printf
l 考法1
x = 11, y = 10;
printf(“%d, %d”, (x++, y), y++);
输出结果:11,10
解析:
1)printf函数按参数从右到左顺序进行分析
y++ -> x++ -> y
| |
10,后y变11 11
2)分析结束,输出顺序是从左到右;即:11, 10
l 考法2
printf(“%4s”, “abc”); // 输出结果 (空格)abc
printf(“%-4s”, “abc”); // 输出结果 abc(空格)
printf(“%4s”, “abcde”); // 输出结果 abcde
printf(“%9.3f”, 12.3456); // 输出结果 1.234e+01
8.scanf
scanf(“%7.2f”, &a); // 错,不能规定精度
scanf(“%3d”, &a); // 正确
scanf(“%d:%d”, &a, &b); // 输入格式:(整数):(整数) 例 12:34
scanf(“%3c”, &ch); // 正确。但是ch只取输入字符中的第一个字符
// 例:输入abc,则ch值为’a’
9.注意switch中的break
10.while (a = 1) { … } // 注意这里是赋值号
11.for( x = 0, y = 3, i = 0; (y > 3)&&(x < 4); x++, y++ ) i++;
运行结果 i = 0
解析:for循环运行过程 表达式1->表达式2-> 循环体->表达式3
12.数组
int a[10] = {0, 1, 2, 3} // 正确。前四个元素赋值,后6个元素全为0
int a[2][2] = {1,2,3,4} // 正确
int a[ ][2] = {1,2,3,4} // 正确。第一维可以省略
int a[2][ ] = {1,2,3,4} // 错误。非第一维不能省略
13.预处理
定义宏:#define s(x) x*x
表达式 s(a+b) 宏展开为: a + b*a + b
定义宏:#define s(x) (x)*(x)
表达式 s(a+b) 宏展开为: (a + b)*(a + b)
14.指针
l 若定义: int a[10], *p; p = a;
则: a + i 是地址
*(a+i) 是地址a + i的内容
l 数组是指针
*(a+i) 和 *(p+i) 是等价的
a[i] 和 p[i]是等价的
l 字符串也是指针
若定义: char *sp = “bug”;
char s[] = {‘b’,’u’,’g’};
则: s与sp是等价的
15.函数
l 值传递:
Void swap1(int a, int b)
{
int c = a;
a = b;
b = c;
}
void main()
{
int x = 1, y = 2;
swap1(x, y);
printf(“%d, %d”, x, y);
}
程序运行输出结果:1,2
可见x与y变量的值没有交换
l 地址传递:
Void swap2(int *a, int *b)
{
int *c = a;
*a = *b;
*b = *c;
}
void main()
{
int x = 1, y = 2;
swap1(&x, &y);
printf(“%d, %d”, x, y);
}
程序运行输出结果:2,1
可见x与y变量的值实现了交换
基本解释
1、指针的本质是一个与地址相关的复合类型,它的值是数据存放的位置(地址);数组的本质则是一系列的变量。
2、数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。
3、当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
问题:指针与数组
听说char a[]与char *a是一致的,是不是这样呢?
答案与分析:
指针和数组存在着一些本质的区别。当然,在某种情况下,比如数组作为函数的参数进行传递时,由于该数组自动退化为同类型的指针,所以在函数内部,作为函数参数传递进来的指针与数组确实具有一定的一致性,但这只是一种比较特殊的情况而已,在本质上,两者是有区别的。请看以下的例子:
char a[] = "Hi, pig!";
char *p = "Hi, pig!";
上述两个变量的内存布局分别如下:
数组a需要在内存中占用8个字节的空间,这段内存区通过名字a来标志。指针p则需要4个字节的空间来存放地址,这4个字节用名字p来标志。其中存放的地址几乎可以指向任何地方,也可以哪里都不指,即空指针。目前这个p指向某地连续的8个字节,即字符串“Hi, pig!”。
另外,例如:对于a[2]和p[2],二者都返回字符‘i’,但是编译器产生的执行代码却不一样。对于a[2],执行代码是从a的位置开始,向后移动2两个字节,然后取出其中的字符。对于p[2],执行代码是从p的位置取出一个地址,在其上加2,然后取出对应内存中的字符。
问题:数组指针
为什么在有些时候我们需要定义指向数组而不是指向数组元素的指针?如何定义?
答案与分析:
使用指针,目的是用来保存某个元素的地址,从而来利用指针独有的优点,那么在元素需要是数组的情况下,就理所当然要用到指向数组的指针,比如在高维需要动态生成情况下的多维数组。
定义例子如下: int (*pElement)[2]。
下面是一个例子:
int array[2][3] = {{1,2,3},{4,5,6}};
int (*pa)[3]; //定义一个指向数组的指针
pa = &array[0]; // '&'符号能够体现pa的含义,表示是指向数组的指针
printf ("%d", (*pa)[0]); //将打印array[0][0],即1
pa++; // 猜一猜,它指向谁?array[1]?对了!
printf ("%d", (*pa)[0]); // 将打印array[1][0],即4
上述这个例子充分说明了数组指针—一种指向整个数组的指针的定义和使用。
需要说明的是,按照我们在第四篇讨论过的,指针的步进是参照其所指对象的大小的,因此,pa++将整个向后移动一个数组的尺寸,而不是仅仅向后移动一个数组元素的尺寸。
问题:指针数组
有如下定义:
struct UT_TEST_STRUCT *pTo[2][MAX_NUM];
请分析这个定义的意义,并尝试说明这样的定义可能有哪些好处?
答案与分析:
前面我们谈了数组指针,现在又提到了指针数组,两者形式很相似,那么,如何区分两者的定义呢?分析如下:
数组指针是:指向数组的指针,比如 int (*pA)[5]。
指针数组是:指针构成的数组,比如int *pA[5]。
至于上述指针数组的好处,大致有如下两个很普遍的原因:
a)、各个指针内容可以按需要动态生成,避免了空间浪费。
b)、各个指针呈数组形式排列,索引起来非常方便。
在实际编程中,选择使用指针数组大多都是想要获得如上两个好处。
问题:指向指针的指针
在做一个文本处理程序的时候,有这样一个问题:什么样的数据结构适合于按行存储文本?
答案与分析:
首先,我们来分析文本的特点,文本的主要特征是具有很强的动态性,一行文本的字符个数或多或少不确定,整个文本所拥有的文本行数也是不确定的。这样的特征决定了用固定的二维数组存放文本行必然限制多多,缺乏灵活性。这种场合,使用指向指针的指针有很大的优越性。
现实中我们尝试用动态二维数组(本质就是指向指针的指针)来解决此问题:
图示是一个指针数组。所谓动态性指横向(对应每行文本的字符个数)和纵向(对应整个文本的行数)两个方向都可以变化。
就横向而言,因为指针的灵活性,它可以指向随意大小的字符数组,实现了横向动态性。
就竖向而言,可以动态生成及扩展需要的指针数组的大小。
下面的代码演示了这种动态数组的用途:
// 用于从文件中读取以 '\0'结尾的字符串的函数
extern char *getline(FILE *pFile);
FILE *pFile;
char **ppText = NULL; // 二维动态数组指针
char *pCurrText = NULL; // 指向当前输入字符串的指针
ULONG ulCurrLines = 0;
ULONG ulAllocedLines = 0;
while (p = getline(pFile))
{
if (ulCurrLines >= ulAllocedLines)
{
// * 当前竖向空间已经不够了,通过realloc对其进行扩展。
ulAllocedLines += 50; // 每次扩展50行。
ppText = realloc (ppText, ulAllocedLines * (char *));
if (NULL == ppText)
{
return; // 内存分配失败,返回
}
}
ppText[ulCurrLines++] = p; // 横向“扩展”,指向不定长字符串
}
问题:指针数组与数组指针与指向指针的指针
指针和数组分别有如下的特征:
指针:动态分配,初始空间小
数组:索引方便,初始空间大
下面使用高维数组来说明指针数组、数组指针、指向指针的指针各自的适合场合。
多维静态数组:各维均确定,适用于整体空间需求不大的场合,此结构可方便索引,例a[10][40]。
数组指针:低维确定,高维需要动态生成的场合,例a[x][40]。
指针数组:高维确定,低维需要动态生成的场合,例a[10][y]。
指向指针的指针:高、低维均需要动态生成的场合,例a[x][y]。
问题:数组名相关问题
假设有一个整数数组a,a和&a的区别是什么?
答案与分析:
a == &a == &a[0],数组名a不占用存储空间。需要引用数组(非字符串)首地址的地方,我一般使用&a[0],使用a容易和指针混淆,使用&a容易和非指针变量混淆。
区别在于二者的类型。对数组a的直接引用将产生一个指向数组第一个元素的指针,而&a的结果则产生一个指向全部数组的指针。例如:
int a[2] = {1, 2};
int *p = 0;
p = a; /* p指向a[0]所在的地方 */
x = *p; /* x = a[0] = 1*/
p = &a; /* 编译器会提示你错误,*/
/*显示整数指针与整数数组指针不一样 */
问题:函数指针与指针函数
请问:如下定义是什么意思:
int *pF1();
int (*pF2)();
答案与分析:
首先清楚它们的定义:
指针函数,返回一个指针的函数。
函数指针,指向一个函数的指针。
可知:
pF1是一个指针函数,它返回一个指向int型数据的指针。
pF2是一个函数指针,它指向一个参数为空的函数,这个函数返回一个整数。
C源程序常见错误分析
一、C语言出错有两种情况:
1、语法错误。指编程时违背了C语法的规定,对这类错误,编译程序一般都能够给出“出错信息”,并且告诉在哪一行出错及出错的类型。只要仔细检查,是可以很快发现错误并排除的。
2、逻辑错误。程序并无违背语法规则,但程序执行结果与原意不符。这是由于程序设计人员写出的源程序与设计人员的本意不相同,即出现了逻辑上的混乱。
例如:
unsigned char i="1";
unsigned int sum="0";
while (i<=100)
sum="sum"+i;
i++;
在上例中,设计者本意是想求从1到100的整数和,但是由于循环语句中漏掉了大括号,使循环变为死循环而不是求累加。对于这种错误,C编译通常都不会有出错信息(因为符合C语法,但有部分编译系统会提示有一个死循环)。对于这类逻辑错误,比语法错误更难查找,要求程序设计者有丰富的设计经验(不会有类似的错误)和有丰富的排错经验(通过仿真能够很快发现问题)。
二、初学者在编写C源程序时常见错误及分析
1、忘记定义变量就使用
例如:
main ()
{
x=3;
y=x;
}
在上式中看似正确,实际上却没有定义变量x和y的类型。C语言规定,所有的变量必须先定义,后使用。因此在函数开头必须有定义变量x和y的语句,应改为:
main ()
{
int x,y;
x=3;
y=x;
}
2、变量没有赋值初就直接使用。(似乎编译时不会自动初始化,没有赋值时会是产生一个不确定的值,全局变量除会自动初始化)
例如:
unsigned int addition (unsigned int n)
{
unsigned int i;
unsigned int sum;
for (i=0;i<n;i++)
sum+=i;
return (sum);
}
上例中本意是计算1到n之间整数的累加和,但是由于sum没有赋初值,sum中的值是不确定的,因此得不到正确的结果。应改为如下:
unsigned int addition (unsigned int n)
{
unsigned int i;
unsigned int sum="0";
for (i=0;i<n;i++)
sum+=i;
return (sum);
}
或者将sum定义为全局变量(全局变量在初始化时自动赋值“0”)。
unsigned int sum;
unsigned int addition (unsigned int n)
{
unsigned int i;
for (i=0;i<n;i++)
sum+=i;
return (sum);
}
3、输入输出的数据类型与所用格式说明符不一致
例如:
main ( )
{
int a="3",b=4.5;
printf("%f %d\n",a,b);
}
在上例中,a与b变量错位,但编译时并不给出出错信息,输出结果为:
0.000000 16402
它们并不是按赋值的规则进行转换,如把3转换成3.0,把4.5转换成4,而是将存储单元中的数据按格式符的要求的宽度直接输出,如b占4个字节却用“%d”说明,则只有最后两个字节中的数据当成一个整数输出,a也相同,将a地址前两个字节(并不属于a)与变量a的两个字节当成一个4个字节的浮点数输出。
4、没有注意数据的数值范围
8位单片机适用的C编译器,对字符型变量分配一个字节,对整型变量分配二个字节,因此有数值范围的问题。有符号的字符变量的数值范围为-128~127,有符号的整型变量的数值范围为-32768~32767。其它类型变量的范围这里就不再一一列举,请读者参见相应编译器的使用手册。
例如:
main ()
{
char x;
x=300;
}
在上例中,有很多读者会认为x的值就是300,实际上却是错误的。
300的二进制为0b100101100,赋值给x时,将赋值最后的8位,高位截去,因此x的值实际上为0b101100(即整数44)。
如果将500赋给一个有符号的字符型变量时,变量内存储的值还会变成负数,由读者自行分析原因。
5、输入变量时忘记使用地址符号
常见是忘记使用地址符:
例如:
main ()
{
int a,b;
scanf ("%d%d",a,b);
}
应改为:
scanf ("%d%d",&a,&b);
6、输入时数组的组织方式与要求不符
scanf ("%d %d",a,b);
如果输入数据格式为:
3,4
则是错误的,两个数据之间应用空格分来分隔,应为:
3 4
7、误把“=”作为关系运算符“等于”
在数学和其它高级语言中,都是把“=”作为关系运算符“等于”,因此容易将程序误写为:
if (a=b)
c=0;
else
c="1";
在上例中,本意是如果a等于b,则c=0,否则c=1。但C编译系统却认为将b赋值给a,并且如果a不等于0,则c=0,当a等于0,则c=1,这与原设计的意图完全不同。应将条件表过式更改为:
a==b
8、语句后面漏加分号
C语言规定语句末尾必须有分号,分号是C语句不可缺少的一部分,
例如:
main ()
{
unsigned int i,sum;
sum=0;
for (i=0;i<10;i++)
{sum+=i}
}
很多初学者认为用大括号括起就不必加分号,这是错误的,即使该语句用大括号括起来,也必须加入分号。在复合语句中,初学者往往容易漏写最后一个分号。上例应改为如下形式:
main ()
{
unsigned int i,sum;
sum=0;
for (i=0;i<10;i++)
{sum+=i;}
}
当漏写分号而出错,光标将停留在漏写分号的下一行。
9、在不该加分号的地方加了分号
#include "io8515v.h";
由于伪指令不是C程序语句,因此后面不能加分号。
初学者也常在判断语句的条件表达式后面加入分号,
例如:
main ()
{
unsigned int i,sum;
sum=0;
for (i=0;i<10;i++);
sum+=i;
}
在上例中,在for的表达式后面中入分号,则C编译认为循环体是一个空操作,这与设计者的本意不符。
10、对应该有花括号的复合语句,忘记加花括号
例如:
unsigned char i="1";
unsigned int sum="0";
while (i<=100)
sum="sum"+i;
i++;
我们在前面举过这个例子,应改为:
unsigned char i="1";
unsigned int sum="0";
while (i<=100)
{
sum="sum"+i;
i++;
}
11、括号不配对
当一个复合语句中使用多层括号时,常会出现这类错误;也常出现大括号不配对的现象,都是粗心所致。
例如:
while ((c=getchar ()!='a')
putchar(c);
少了一个右括号。