第8章善于利用指针
指针?什么是指针?从根本上看,指针(pointer)是一个值为内存地址 的变量(或数据对象)。正如char类型变量的值是字符,int类型变量的值是 整数,指针变量的值是地址。在C语言中,指针有许多用法。本章将介绍如 何把指针作为函数参数使用,以及为何要这样用。 假设一个指针变量名是ptr,可以编写如下语句: ptr = &pooh; // 把pooh的地址赋给ptr 对于这条语句,我们说ptr“指向”pooh。ptr和&pooh的区别是ptr是变量,而&pooh是常量。或者,ptr是可修改的左值,而&pooh是右值。还可以把ptr
指向别处:
ptr = &bah; // 把ptr指向bah,而不是pooh
现在ptr的值是bah的地址。
数组和指针
数组形式和指针形式有何不同?以上面的声明为例,数组形式(ar1[]) 在计算机的内存中分配为一个内含29个元素的数组(每个元素对应一个字 符,还加上一个末尾的空字符'\0'),每个元素被初始化为字符串字面量对 应的字符。通常,字符串都作为可执行文件的一部分储存在数据段中。当把 程序载入内存时,也载入了程序中的字符串。字符串储存在静态存储区 (static memory)中。但是,程序在开始运行时才会为该数组分配内存。此 743时,才将字符串拷贝到数组中(第 12 章将详细讲解)。注意,此时字符串 有两个副本。一个是在静态内存中的字符串字面量,另一个是储存在ar1数 组中的字符串。 此后,编译器便把数组名ar1识别为该数组首元素地址(&ar1[0])的别 名。这里关键要理解,在数组形式中,ar1是地址常量。不能更改ar1,如果改变了ar1,则意味着改变了数组的存储位置(即地址)。可以进行类似 ar1+1这样的操作,标识数组的下一个元素。但是不允许进行++ar1这样的操 作。递增运算符只能用于变量名前(或概括地说,只能用于可修改的左 值),不能用于常量。 指针形式(
*pt1)也使得编译器为字符串在静态存储区预留29个元素的 空间。另外,一旦开始执行程序,它会为指针变量pt1留出一个储存位置, 并把字符串的地址储存在指针变量中。该变量最初指向该字符串的首字符, 但是它的值可以改变。因此,可以使用递增运算符。例如,++pt1将指向第 2 个字符(
o)。 字符串字面量被视为const数据。由于pt1指向这个const数据,所以应该 把pt1声明为指向const数据的指针。这意味着不能用pt1改变它所指向的数 据,但是仍然可以改变pt1的值(即,pt1指向的位置)。如果把一个字符串 字面量拷贝给一个数组,就可以随意改变数据,除非把数组声明为const。
总之,初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针 只把字符串的地址拷贝给指针。程序清单11.3演示了这一点让你不再害怕指针——C指针详解(经典,非常详细)_唐大麦的博客-CSDN博客_指针C语言*p、p以及&p的区别,*p和**p的区别_14skyang的博客-CSDN博客_c语言*pint p; //这是一个普通的整型变量int *p; //首先从P 处开始,先与*结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针
int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组
int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组
int (*p)[3]; //首先从P 处开始,先与*结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针
int **p; //首先从P 开始,先与*结合,说是P 是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针.
int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据
Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针
int *(*p(int))[3]; //可以先跳过,不看这个类型,过于复杂从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的*结合,说明函数返回的是一个指针,,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与*结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数.
指针的四方面的内容:
指针的类型、指针所指向的类型、指针的值或者叫指针所指向的内存区、指针本身所占据的内存区。
1.指针的类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:
(1)int*ptr;//指针的类型是int*
(2)char*ptr;//指针的类型是char*
(3)int**ptr;//指针的类型是int**
(4)int(*ptr)[3];//指针的类型是int(*)[3]
(5)int*(*ptr)[4];//指针的类型是int*(*)[4]
2.指针所指向的类型
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:
(1)int*ptr; //指针所指向的类型是int
(2)char*ptr; //指针所指向的的类型是char
(3)int**ptr; //指针所指向的的类型是int*
(4)int(*ptr)[3]; //指针所指向的的类型是int()[3]
(5)int*(*ptr)[4]; //指针所指向的的类型是int*()[4]
在指针的算术运算中,指针所指向的类型有很大的作用。
指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C 越来越熟悉时,你会发现,把与指针搅和在一起的"类型"这个概念分成"指针的类型"和"指针所指向的类型"两个概念,是精通指针的关键点之一。我看了不少书,发现有些写得差的书中,就把指针的这两个概念搅在一起了,所以看起书来前后矛盾,越看越糊涂。
3.指针的值----或者叫指针所指向的内存区或地址
指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32 位程序里,所有类型的指针的值都是一个32 位整数,因为32 位程序里内存地址全都是32 位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为si zeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。
以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?(重点注意)
4 指针本身所占据的内存区
指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32 位平台里,指针本身占据了4 个字节的长度。指针本身占据的内存这个概念在判断一个指针表达式(后面会解释)是否是左值时很有用。
指针的算术运算
指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的,以单元为单位。
例二:
char a[20];
int *ptr=(int *)a; //强制类型转换并不会改变a 的类型
ptr++;
在上例中,指针ptr 的类型是int*,它指向的类型是int,它被初始化为指向整型变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr 的值加上了sizeof(int),在32 位程序中,是被加上了4,因为在32 位程序中,int 占4 个字节。由于地址是用字节做单位的,故ptr 所指向的地址由原来的变量a 的地址向高地址方向增加了4 个字节。由于char 类型的长度是一个字节,所以,原来ptr 是指向数组a 的第0 号单元开始的四个字节,此时指向了数组a 中从第4 号单元开始的四个字节。
我们可以用一个指针和一个循环来遍历一个数组,看例子:
例三:
int array[20]={0};
int *ptr=array;
for(i=0;i<20;i++)
{
(*ptr)++;
ptr++;
}
这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr加1 个单元,所以每次循环都能访问数组的下一个单元。
再看例子:
例四:
char a[20]="You_are_a_girl";
int *ptr=(int *)a;
ptr+=5;
在这个例子中,ptr 被加上了5,编译器是这样处理的:将指针ptr 的值加上5 乘sizeof(int),在32 位程序中就是加上了5 乘4=20。由于地址的单位是字节,故现在的ptr 所指向的地址比起加5 后的ptr 所指向的地址来说,向高地址方向移动了20 个字节。
在这个例子中,没加5 前的ptr 指向数组a 的第0 号单元开始的四个字节,加5 后,ptr 已经指向了数组a 的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。如果上例中,ptr 是被减去5,那么处理过程大同小异,只不过ptr 的值是被减去5 乘sizeof(int),新的ptr 指向的地址将比原来的ptr 所指向的地址向低地址方向移动了20 个字节。
下面请允许我再举一个例子:(一个误区)
例五:
#include
int main()
{
char a[20]=" You_are_a_girl";
char *p=a;
char **ptr=&p;
//printf("p=%d\n",p);
//printf("ptr=%d\n",ptr);
//printf("*ptr=%d\n",*ptr);
printf("**ptr=%c\n",**ptr);
ptr++;
//printf("ptr=%d\n",ptr);
//printf("*ptr=%d\n",*ptr);
printf("**ptr=%c\n",**ptr);
}
误区一、输出答案为Y 和o
误解:ptr 是一个char 的二级指针,当执行ptr++;时,会使指针加一个sizeof(char),所以输出如上结果,这个可能只是少部分人的结果.
误区二、输出答案为Y 和a误解:ptr 指向的是一个char *类型,当执行ptr++;时,会使指针加一个sizeof(char *)(有可能会有人认为这个值为1,那就会得到误区一的答案,这个值应该是4,参考前面内容), 即&p+4; 那进行一次取值运算不就指向数组中的第五个元素了吗?那输出的结果不就是数组中第五个元素了吗?答案是否定的.
正解: ptr 的类型是char **,指向的类型是一个char *类型,该指向的地址就是p的地址(&p),当执行ptr++;时,会使指针加一个sizeof(char*),即&p+4;那*(&p+4)指向哪呢,这个你去问上帝吧,或者他会告诉你在哪?所以最后的输出会是一个随机的值,或许是一个非法操作.
总结一下:
一个指针ptrold 加(减)一个整数n 后,结果是一个新的指针ptrnew,ptrnew 的类型和ptrold 的类型相同,ptrnew 所指向的类型和ptrold所指向的类型也相同。ptrnew 的值将比ptrold 的值增加(减少)了n 乘sizeof(ptrold 所指向的类型)个字节。就是说,ptrnew 所指向的内存区将比ptrold 所指向的内存区向高(低)地址方向移动了n 乘sizeof(ptrold 所指向的类型)个字节。指针和指针进行加减:两个指针不能进行加法运算,这是非法操作,因为进行加法后,得到的结果指向一个不知所向的地方,而且毫无意义。两个指针可以进行减法操作,但必须类型相同,一般用在数组方面,不多说了。
运算符&和*
&是取地址运算符,*是间接运算符。
&a 的运算结果是一个指针,指针的类型是a 的类型加个*,指针所指向的类型是a 的类型,指针所指向的地址嘛,那就是a 的地址。
*p 的运算结果就五花八门了。总之*p 的结果是p 所指向的东西,这个东西有这些特点:它的类型是p 指向的类型,它所占用的地址是p所指向的地址。
int a=12; int b; int *p; int **ptr;
分析:
p=&a; //&a 的结果是一个指针,类型是int*,指向的类型int,指向的地址是a 的地址。
*p=24; //*p 的结果,在这里它的类型是int,它所占用的地址是p 所指向的地址,显然,*p 就是变量a。
ptr=&p; //&p 的结果是个指针,该指针的类型是p 的类型加个*,在这里是int **。该指针所指向的类型是p 的类型,这里是int*。该指针所指向的地址就是指针p 自己的地址。
*ptr=&b; //*ptr 是个指针,&b 的结果也是个指针,且这两个指针的类型和所指向的类型是一样的,所以用&b 来给*ptr 赋值就是毫无问题的了。
**ptr=34; //*ptr 的结果是ptr 所指向的东西,在这里是一个指针,对这个指针再做一次*运算,结果是一个int 类型的变量。
指针表达式
一个表达式的结果如果是一个指针,那么这个表达式就叫指针表式。
由于指针表达式的结果是一个指针,所以指针表达式也具有指针所具有的四个要素:指针的类型,指针所指向的类型,指针指向的内存区,指针自身占据的内存。
好了,当一个指针表达式的结果指针已经明确地具有了指针自身占据的内存的话,这个指针表达式就是一个左值,否则就不是一个左值。在例七中,&a 不是一个左值,因为它还没有占据明确的内存。*ptr 是一个左值,因为*ptr 这个指针已经占据了内存,其实*ptr 就是指针pa,既然pa 已经在内存中有了自己的位置,那么*ptr 当然也有了自己的位置。
数组和指针的关系
数组的数组名其实可以看作一个指针。看下例:
例九:
int array[10]={0,1,2,3,4,5,6,7,8,9},value;
value=array[0]; //也可写成:value=*array;
value=array[3]; //也可写成:value=*(array+3);
value=array[4]; //也可写成:value=*(array+4);
上例中,一般而言数组名array 代表数组本身,类型是int[10],但如果把array 看做指针的话,它指向数组的第0 个单元,类型是int* 所指向的类型是数组单元的类型即int。因此*array 等于0 就一点也不奇怪了。同理,array+3 是一个指向数组第3 个单元的指针,所以*(array+3)等于3。其它依此类推。
例十:
char *str[3]={
"Hello,thisisasample!",
"Hi,goodmorning.",
"Helloworld"
};
char s[80];
strcpy(s,str[0]); //也可写成strcpy(s,*str);
strcpy(s,str[1]); //也可写成strcpy(s,*(str+1));
strcpy(s,str[2]); //也可写成strcpy(s,*(str+2));
上例中,str 是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名str 当作一个指针的话,它指向数组的第0 号单元,它的类型是char **,它指向的类型是char *。
*str 也是一个指针,它的类型是char *,它所指向的类型是char,它指向的地址是字符串"Hello,thisisasample!"的第一个字符的地址,即'H'的地址。注意:字符串相当于是一个数组,在内存中以数组的形式储存,只不过字符串是一个数组常量,内容不可改变,且只能是右值.如果看成指针的话,他即是常量指针,也是指针常量.
str+1 也是一个指针,它指向数组的第1 号单元,它的类型是char**,它指向的类型是char*。
*(str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向"Hi,goodmorning."的第一个字符'H'
下面总结一下数组的数组名(数组中储存的也是数组)的问题:
声明了一个数组TYPE array[n],则数组名称array 就有了两重含义:
第一,它代表整个数组,它的类型是TYPE[n];
第二,它是一个常量指针,该指针的类型是TYPE*,该指针指向的类型是TYPE,也就是数组单元的类型,该指针指向的内存区就是数组第0 号单元,该指针自己占有单独的内存区,注意它和数组第0 号单元占据的内存区是不同的。该指针的值是不能修改的,即类似array++的表达式是错误的。在不同的表达式中数组名array 可以扮演不同的角色。在表达式sizeof(array)中,数组名array 代表数组本身,故这时sizeof 函数测出的是整个数组的大小。
在表达式*array 中,array 扮演的是指针,因此这个表达式的结果就是数组第0 号单元的值。sizeof(*array)测出的是数组单元的大小。
表达式array+n(其中n=0,1,2,.....)中,array 扮演的是指针,故array+n 的结果是一个指针,它的类型是TYPE *,它指向的类型是TYPE,它指向数组第n号单元。故sizeof(array+n)测出的是指针类型的大小。在32 位程序中结果是4
例十一:
int array[10];
int (*ptr)[10];
ptr=&array;:
上例中ptr 是一个指针,它的类型是int(*)[10],他指向的类型是int[10] ,我们用整个数组的首地址来初始化它。在语句ptr=&array中,array 代表数组本身。
本节中提到了函数sizeof(),那么我来问一问,sizeof(指针名称)测出的究竟是指针自身类型的大小呢还是指针所指向的类型的大小?
答案是前者。例如:
int(*ptr)[10];
则在32 位程序中,有:
sizeof(int(*)[10])==4
sizeof(int[10])==40
sizeof(ptr)==4
实际上,sizeof(对象)测出的都是对象自身的类型的大小,而不是别的什么类型的大小。
指针和结构类型的关系
可以声明一个指向结构类型对象的指针。
例十二:
struct MyStruct
{
int a;
int b;
int c;
};
struct MyStruct ss={20,30,40};
//声明了结构对象ss,并把ss 的成员初始化为20,30 和40。
struct MyStruct *ptr=&ss;
//声明了一个指向结构对象ss 的指针。它的类型是
//MyStruct *,它指向的类型是MyStruct。
int *pstr=(int*)&ss;
//声明了一个指向结构对象ss 的指针。但是pstr 和
//它被指向的类型ptr 是不同的。
请问怎样通过指针ptr 来访问ss 的三个成员变量?
答案:
ptr->a; //指向运算符,或者可以这们(*ptr).a,建议使用前者
ptr->b;
ptr->c;
又请问怎样通过指针pstr 来访问ss 的三个成员变量?
答案:
*pstr; //访问了ss 的成员a。
*(pstr+1); //访问了ss 的成员b。
*(pstr+2) //访问了ss 的成员c。
虽然我在我的MSVC++6.0 上调式过上述代码,但是要知道,这样使用pstr 来访问结构成员是不正规的,为了说明为什么不正规,让我们看看怎样通过指针来访问数组的各个单元: (将结构体换成数组)
例十三:
int array[3]={35,56,37};
int *pa=array;
//通过指针pa 访问数组array 的三个单元的方法是:
*pa; /访问了第0 号单元
*(pa+1); //访问了第1 号单元
*(pa+2); //访问了第2 号单元
从格式上看倒是与通过指针访问结构成员的不正规方法的格式一样。
所有的C/C++编译器在排列数组的单元时,总是把各个数组单元存放在连续的存储区里,单元和单元之间没有空隙。但在存放结构对象的各个成员时,在某种编译环境下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两个成员之间加若干个"填充字节",这就导致各个成员之间可能会有若干个字节的空隙。
所以,在例十二中,即使*pstr 访问到了结构对象ss 的第一个成员变量a,也不能保证*(pstr+1)就一定能访问到结构成员b。因为成员a 和成员b 之间可能会有若干填充字节,说不定*(pstr+1)就正好访问到了这些填充字节呢。这也证明了指针的灵活性。要是你的目的就是想看看各个结构成员之间到底有没有填充字节,嘿,这倒是个不错的方法。
不过指针访问结构成员的正确方法应该是象例十二中使用指针ptr 的方法。
指针和函数的关系
可以把一个指针声明成为一个指向函数的指针。
int fun1(char *,int);
int (*pfun1)(char *,int);
pfun1=fun1;
int a=(*pfun1)("abcdefg",7); //通过函数指针调用函数。
可以把指针作为函数的形参。在函数调用语句中,可以用指针表达式来作为实参。
例十四:
int fun(char *);
inta;
char str[]="abcdefghijklmn";
a=fun(str);
int fun(char *s)
{
int num=0;
for(int i=0;;)
{
num+=*s;s++;
}
return num;
}
这个例子中的函数fun 统计一个字符串中各个字符的ASCII 码值之和。前面说了,数组的名字也是一个指针。在函数调用中,当把str作为实参传递给形参s 后,实际是把str 的值传递给了s,s 所指向的地址就和str 所指向的地址一致,但是str 和s 各自占用各自的存储空间。在函数体内对s 进行自加1 运算,并不意味着同时对str 进行了自加1 运算。
指针类型转换
当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋值号的右边是一个指针表达式。在我们前面所举的例子中,绝大多数情况下,指针的类型和指针表达式的类型是一样的,指针所指向的类型和指针表达式所指向的类型是一样的。
例十五:
float f=12.3;
float *fptr=&f;
int *p;
在上面的例子中,假如我们想让指针p 指向实数f,应该怎么办?
是用下面的语句吗?
p=&f;
不对。因为指针p 的类型是int *,它指向的类型是int。表达式&f 的结果是一个指针,指针的类型是float *,它指向的类型是float。
两者不一致,直接赋值的方法是不行的。至少在我的MSVC++6.0 上,对指针的赋值语句要求赋值号两边的类型一致,所指向的类型也一致,其它的编译器上我没试过,大家可以试试。为了实现我们的目的,需要进行"强制类型转换":
p=(int*)&f;
如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP *TYPE, 那么语法格式是: (TYPE *)p;
这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE *,它指向的类型是TYPE,它指向的地址就是原指针指向的地址。
而原来的指针p 的一切属性都没有被修改。(切记)
一个函数如果使用了指针作为形参,那么在函数调用语句的实参和形参的结合过程中,必须保证类型一致,否则需要强制转换
例十六:
void fun(char*);
int a=125,b;
fun((char*)&a);
void fun(char*s)
{
charc;
c=*(s+3);*(s+3)=*(s+0);*(s+0)=c;
c=*(s+2);*(s+2)=*(s+1);*(s+1)=c;
}
注意这是一个32 位程序,故int 类型占了四个字节,char 类型占一个字节。函数fun 的作用是把一个整数的四个字节的顺序来个颠倒。注意到了吗?在函数调用语句中,实参&a 的结果是一个指针,它的类型是int *,它指向的类型是int。形参这个指针的类型是char *,它指向的类型是char。这样,在实参和形参的结合过程中,我们必须进行一次从int *类型到char *类型的转换。
结合这个例子,我们可以这样来
想象编译器进行转换的过程:编译器先构造一个临时指针char *temp,然后执行temp=(char *)&a,最后再把temp 的值传递给s。所以最后的结果是:s 的类型是char *,它指向的类型是char,它指向的地址就是a 的首地址。
我们已经知道,指针的值就是指针指向的地址,在32 位程序中,指针的值其实是一个32 位整数。
那可不可以把一个整数当作指针的值直接赋给指针呢?就象下面的语句:
unsigned int a;
TYPE *ptr; //TYPE 是int,char 或结构类型等等类型。
a=20345686;
ptr=20345686; //我们的目的是要使指针ptr 指向地址20345686
ptr=a; //我们的目的是要使指针ptr 指向地址20345686
//编译一下吧。结果发现后面两条语句全是错的。那么我们的目的就不能达到了吗?不,还有办法:
unsigned int a;
TYPE *ptr; //TYPE 是int,char 或结构类型等等类型。
a=N //N 必须代表一个合法的地址;
ptr=(TYPE*)a; //呵呵,这就可以了。
严格说来这里的(TYPE *)和指针类型转换中的(TYPE *)还不一样。这里的(TYPE*)的意思是把无符号整数a 的值当作一个地址来看待。上面强调了a 的值必须代表一个合法的地址,否则的话,在你使用ptr 的时候,就会出现非法操作错误。想想能不能反过来,把指针指向的地址即指针的值当作一个整数取出来。完全可以。下面的例子演示了把一个指针的值当作一个整数取出来,然后再把这个整数当作一个地址赋给一个指针:
例十七:
int a=123,b;
int *ptr=&a;
char *str;
b=(int)ptr; //把指针ptr 的值当作一个整数取出来。
str=(char*)b; //把这个整数的值当作一个地址赋给指针str。
现在我们已经知道了,可以把指针的值当作一个整数取出来,也可以把一个整数值当作地址赋给一个指针。
指针的安全问题
看下面的例子:
例十八:
char s='a';
int *ptr;
ptr=(int *)&s;
*ptr=1298;
指针ptr 是一个int *类型的指针,它指向的类型是int。它指向的地址就是s 的首地址。在32 位程序中,s 占一个字节,int 类型占四个字节。最后一条语句不但改变了s 所占的一个字节,还把和s 相临的高地址方向的三个字节也改变了。这三个字节是干什么的?只有编译程序知道,而写程序的人是不太可能知道的。也许这三个字节里存储了非常重要的数据,也许这三个字节里正好是程序的一条代码,而由于你对指针的马虎应用,这三个字节的值被改变了!这会造成崩溃性的错误。
让我们再来看一例:
例十九:
char a;
int *ptr=&a;
ptr++;
*ptr=115;
该例子完全可以通过编译,并能执行。但是看到没有?第3 句对指针ptr 进行自加1 运算后,ptr 指向了和整形变量a 相邻的高地址方向的一块存储区。这块存储区里是什么?我们不知道。有可能它是一个非常重要的数据,甚至可能是一条代码。
而第4 句竟然往这片存储区里写入一个数据!这是严重的错误。所以在使用指针时,程序员心里必须非常清楚:我的指针究竟指向了哪里。在用指针访问数组的时候,也要注意不要超出数组的低端和高端界限,否则也会造成类似的错误。在指针的强制类型转换:ptr1=(TYPE *)ptr2 中,如果sizeof(ptr2的类型)大于sizeof(ptr1 的类型),那么在使用指针ptr1 来访问ptr2所指向的存储区时是安全的。如果sizeof(ptr2 的类型) 小于sizeof(ptr1 的类型),那么在使用指针ptr1 来访问ptr2 所指向的存储区时是不安全的。至于为什么,读者结合例十八来想一想,应该会明白的。
第八章典型例题:
调试示例error11_1.cpp (指针数组、内存动态分配)
程序填空,不要改变与输入输出有关的语句。
输入若干有关颜色的英文单词,以#作为输入结束标志,对这些单词升序排列后输出。其中颜色的英文单词数数小于20个,颜色的英文单词长度均不超过10个字符。
输入输出示例:括号内为说明
输入:
red
blue
yellow
green
purple
#
输出:
blue green purple red yellow
#include
#include
#include
void main()
{
int i,j, index, n = 0;
char *color[20], str[10], *temp;
scanf("%s", str);
while(str[0] != '#') {
color[n] = (char *)malloc(sizeof(char)*(strlen(str)+1));
strcpy(color[n], str);
n++;
scanf("%s", str);
}
/*---------*/
for (i =0; i
index = i;
for (j = i+1; j
if (strcmp(color[j],color[index]) < 0) index = j;
strcpy(str, color[i]);
strcpy(color[i], color[index]);
strcpy(color[index], str);
}
for(i = 0; i < n; i++)
printf("%s ", color[i]);
printf("\n");
}
10022 编程题(指针数组)
输入一个正整数repeat (0
编写程序,输入一个月份,输出对应的英文名称,要求用指针数组表示12个月的英文名称。
若输入月份错误,输出提示信息。
输入输出示例:括号内为说明
输入:
3 (repeat=3)
5
9
14
输出:
May
September
Wrong input!
#include
void main()
{
int ri,repeat;
int month;
char *month_name[]={"","January","February","March","April","May","June","July","August","September","October","November","December"};
scanf("%d",&repeat);
for(ri=1;ri<=repeat;ri++){
scanf("%d",&month);
if((month>=1)&&(month<=12))
puts(month_name[month]);
else
printf("Wrong input!");
}
}
10023 编程题 (指针数组,查找相同的字符串)
程序填空,不要改变与输入输出有关的语句。
输入一个正整数repeat (0
定义一个指针数组将下表的星期信息组织起来,输入一个字符串,在表中查找,若存在,输出该字符串在表中的序号,否则输出-1。
(表格详见实验教材P99)
输入输出示例:括号内为说明
输入:
3 (repeat=3)
Tuesday
Wednesday
year
输出:
3
4
-1
#include
#include
void main()
{
int i,ri,repeat,dateNum;
char *date[]={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"};
char str[80];
scanf("%d",&repeat);
getchar();
for(ri=1;ri<=repeat;ri ++){
scanf("%s",str);
/*---------*/
dateNum=sizeof(date)/sizeof(char *);
for (i = 0; i
if (strcmp(date[i],str) == 0) {
printf("%d\n",i+1); break;
}
}
if (i==dateNum) printf("%d\n",-1);
}
}
第八章目录回顾
8.1指针是什么217
8.2指针变量219
8.2.1使用指针变量的例子219
8.2.2怎样定义指针变量220
8.2.3怎样引用指针变量222
8.2.4指针变量作为函数参数224
8.3通过指针引用数组229
8.3.1数组元素的指针229
8.3.2在引用数组元素时指针的运算229
8.3.3通过指针引用数组元素231
8.3.4用数组名作函数参数236
*8.3.5通过指针引用多维数组243
8.4通过指针引用字符串254
8.4.1字符串的引用方式254
8.4.2字符指针作函数参数258
8.4.3使用字符指针变量和字符数组的比较262
*8.5指向函数的指针265
8.5.1什么是函数的指针265
8.5.2用函数指针变量调用函数265
*8.5.3怎样定义和使用指向函数的指针变量267
*8.5.4用指向函数的指针作函数参数269
*8.6返回指针值的函数273
*8.7指针数组和多重指针276
8.7.1什么是指针数组276
8.7.2指向指针数据的指针变量279
8.7.3指针数组作main函数的形参281
*8.8动态内存分配与指向它的指针变量284
8.8.1什么是内存的动态分配284
8.8.2怎样建立内存的动态分配284
8.8.3void指针类型286
8.9有关指针的小结288
第9章用户自己建立数据类型
书上典型例题总结:
如何去定义一个结构体数组:
#include
#include
struct Person
{
char name[20];
int count;
}leader[3]={"Li",0,"Zhang",0,"Sun",0};
int main()
{
int i,j;
char leader_name[20];
for (i=1;i<=10;i++)
{
scanf("%s",leader_name);
for(j=0;i<3;i++)
if(strcmp(leader_name,leader[j].name)==0)leader[j].count++;
}
printf("\nResult:\n");
for(i=0;i<3;i++)
printf("%5s:%d\n",leader[i].name,leader[i].count);
return 0;
}
指向结构体变量的指针:
#include
#include
int main()
{
struct Student
{
long num;
char name[20];
char sex;
float score;
};
struct Student_stu_1;
struct Student*p;
p=stu&_1; //p指向stu_1
stu_1.num=10101;
strcpy(stu_1.name,"Li Lin");
stu_1.sex='M';
stu_1.score=89.5;
printf("No.:%ld\nname:%s\nsex:%c\nscore:%5.1f\n");
stu_1.num,stu_1.name,stu_1.sex,stu_1.score);
printf("No.:%ld\nname:%s\nsex:%c\nscore:%5.1f\n");
(*p).num,(*p).name,(*p).sex,(*p).score); //(*p)表示p指向的结构体变量,(*p).num表示p所指向的结构体变量中的成员num。注意*两侧的括号不可省,因为成员运算符“.”优先于“*”运算符
return 0;
}
如果p指向一个结构体变量stu,以下三种用法等价
(1)stu.成员名(如stu.num);
(2)(*p).成员名(如(*p).num);
(3)p->成员名(如p->num)。
306页例9.7:
学生成绩管理系统
#include
#include
#include
#include
struct Student{ //每个学生对应一个结构体
char ID[20];//学号
char Name[10];//姓名
float Mark1;//语文成绩
float Mark2;//数学成绩 //四个变量
float Mark3;//英语成绩
float Mark4;//计算机成绩
float All; //总分
float Average;//平均成绩
}students[1000];
int num=0; //计数器
void Copy(struct Student *arr,int i,int j)
{
strcpy(arr[i].ID,arr[j].ID); //strcpy()函数的简介:
是将一个字符串复制到另一块空间地址中 的函数,‘\0’是停止拷贝的终止条件,
也复制到目标空间。下面是库中的strcpy()函数声明:
strcpy(arr[i].Name,arr[j].Name);
arr[i].Mark1 = arr[j].Mark1;
arr[i].Mark2 = arr[j].Mark2;
arr[i].Mark3 = arr[j].Mark3;
arr[i].Mark4 = arr[j].Mark4;
arr[i].All = arr[j].All;
arr[i].Average = arr[j].Average;
}
int Student_SearchByName(char name[])//通过姓名来检索学生
{
int i;
for (i=0;i
{
if (strcmp(students[i].Name,name)==0) //通过strcmp函数来对比学生姓名,找到返回在数组的位置
{
return i;
}
}
return -1; //未找到返回 -1
}
int Student_SearchByIndex(char id[])//通过学号来检索学生信息
{
int i;
for (i=0;i
{
if (strcmp(students[i].ID,id)==0) //通过strcmp函数来对比学生id,找到返回位置
{
return i;
}
}
return -1; //未找到返回 -1
}
void Student_DisplaySingle(int index)//输出表头
{
printf("%10s%10s%8s%8s%8s%10s\n","学号","姓名","语文","数学","英语","计算机","总成绩","平均成绩");
printf("-------------------------------------------------------------\n");
printf("%10s%10s%8.2f%8.2f%8.2f%8.2f%10.2f%10.2f\n",students[index].ID,students[index].Name,
students[index].Mark1,students[index].Mark2,students[index].Mark3,students[index].Mark4,students[index].All,students[index].Average);
}
void inputt()//利用循环录入学生信息
{
while(1)
{
printf("请输入学号:");
scanf("%s",&students[num].ID);
getchar();
printf("请输入姓名:");
scanf("%s",&students[num].Name);
getchar();
printf("请输入成绩:");
scanf("%f",&students[num].Mark1);
getchar();
printf("请输入成绩:");
scanf("%f",&students[num].Mark2);
getchar();
printf("请输入成绩:");
scanf("%f",&students[num].Mark3);
getchar();
printf("请输入成绩:");
scanf("%f",&students[num].Mark4); //依次输入各项数据
getchar();
students[num].All=students[num].Mark1+students[num].Mark2+students[num].Mark3+students[num].Mark4; //输完数据后自动计算总成绩与平均成绩
students[num].Average=(students[num].Mark1+students[num].Mark2+students[num].Mark3+students[num].Mark4)/4;
if(Student_SearchByIndex(students[num].ID) == -1)
{
num++; //移向下一个位置
}
else
{
printf("学号重复,输入数据无效 !!!\n");
}
printf("是否继续?(y/n)");
if (getchar()=='n')
{
break;
}
}
}
void modify()//修改成绩
{
while(1)
{
char id[20];
int index;
printf("请输入要修改的学生的学号:");
scanf("%s",&id);
getchar();
index=Student_SearchByIndex(id); //调用搜查id函数,根据其返回值确定位置
if (index==-1)
{
printf("学生不存在!\n");
}
else
{
printf("你要修改的学生信息为:\n");
Student_DisplaySingle(index);
printf("-- 请输入新值--\n");
printf("请输入学号:");
scanf("%s",&students[index].ID);
getchar();
printf("请输入姓名:");
scanf("%s",&students[index].Name);
getchar();
printf("请输入语文成绩:");
scanf("%f",&students[index].Mark1);
getchar();
printf("请输入数学成绩:");
scanf("%f",&students[index].Mark2);
getchar();
printf("请输入英语成绩:");
scanf("%f",&students[index].Mark3);
getchar();
printf("请输入计算机成绩:");
scanf("%f",&students[index].Mark4); //重新录入一套新的数据替代
getchar();
students[index].All=students[index].Mark1+students[index].Mark2+students[index].Mark3+students[index].Mark4;
students[index].Average=(students[index].Mark1+students[index].Mark2+students[index].Mark3+students[index].Mark4)/4;
}
printf("是否继续?(y/n)");
if (getchar()=='n')
{
break;
}
}
}
void deletee()//删除学生信息
{
int i;
while(1)
{
char id[20];
int index;
printf("请输入要删除的学生的学号:");
scanf("%s",&id);
getchar();
index=Student_SearchByIndex(id); //调用搜查id函数,根据其返回值确定位置
if (index==-1)
{
printf("学生不存在!\n");
}
else
{
printf("你要删除的学生信息为:\n");
Student_DisplaySingle(index);
printf("是否真的要删除?(y/n)");
if (getchar()=='y')
{
for (i=index;i
{
Copy(students,i,i+1);
//students[i]=students[i+1]; //把后边的对象都向前移动
}
num--;
}
getchar();
}
printf("是否继续?(y/n)");
if (getchar()=='n')
{
break;
}
}
}
void display()//打印已录入的学生信息
{
int a;
printf("%10s%10s%8s%8s%8s%8s%10s%10s\n","学号","姓名","语文","数学","英语","计算机","总成绩","平均成绩");
printf("-------------------------------------------------------------\n");
for (a=0;a
{
printf("%10s%10s%8.2f%8.2f%8.2f%8.2f%10.2f%10.2f\n",students[a].ID,students[a].Name,
students[a].Mark1,students[a].Mark2,students[a].Mark3,students[a].Mark4,students[a].All,students[a].Average);
}
}
void insert()//指定位置插入学生信息
{
int a,b,c;
printf("请输入你要插入的位置");
scanf("%d",&a);
if(a>num) {
printf("输入的位置有误,请重新输入,当前共%d条数据\n",num);
scanf("%d",&a);}
b=num-1;
for(;b>=a-1;b--)
{
//strcpy(students[b+1].ID,students[b].ID);
//strcpy(students[b+1].Name,students[b].Name);
//students[b+1].Mark1=students[b].Mark1;
//students[b+1].Mark2=students[b].Mark2;
//students[b+1].Mark3=students[b].Mark3;
//students[b+1].Mark4=students[b].Mark4;
//students[b+1].All=students[b].All;
//students[b+1].Average=students[b].Average;
Copy(students,b+1,b); //根据其输入的位置,将其及以后的数据向后移动一个位置
}
num++;
printf("请输入学号:");
scanf("%s",&students[a-1].ID);
getchar();
printf("请输入姓名:");
scanf("%s",&students[a-1].Name);
getchar();
printf("请输入语文成绩:");
scanf("%f",&students[a-1].Mark1);
getchar();
printf("请输入数学成绩:");
scanf("%f",&students[a-1].Mark2);
getchar();
printf("请输入英语成绩:");
scanf("%f",&students[a-1].Mark3);
getchar();
printf("请输入计算机成绩:");
scanf("%f",&students[a-1].Mark4); //输入新数据
getchar();
students[a-1].All=students[a-1].Mark1+students[a-1].Mark2+students[a-1].Mark3+students[a-1].Mark4;
students[a-1].Average=(students[a-1].Mark1+students[a-1].Mark2+students[a-1].Mark3+students[a-1].Mark4)/4;
}
void search()//查询学生信息
{
while(1)
{
char name[20];
int index;
printf("请输入要查询的学生的姓名:");
scanf("%s",&name);
getchar();
index=Student_SearchByName(name); //调用搜查name函数,根据其返回值确定位置
if (index==-1)
{
printf("学生不存在!\n");
}
else
{
printf("你要查询的学生信息为:\n");
Student_DisplaySingle(index);
}
printf("是否继续?(y/n)");
if (getchar()=='n')
{
break;
}
}
}
voidsort()//根据平均分排序
(此时注意按照题目要求应该排序两个)
{
int i,j;
//struct students tmp;
for (i=0;i
{
students[i].Average=(students[i].Mark1+students[i].Mark2+students[i].Mark3+students[i].Mark4)/4;
}
for (i=0;i
{
for (j=1;j
{
if (students[j-1].Average
{
Copy(students,num,j-1);
Copy(students,j-1,j);
Copy(students,j,num);
//tmp=students[j-1];
//students[j-1]=students[j];
//students[j]=tmp; //冒泡排序
}
}
}
int a;
printf("%10s%10s%8s%8s%8s%10s\n","学号","姓名","语文","数学","英语","计算机","总成绩","平均成绩");
printf("-------------------------------------------------------------\n");
for (a=0;a
{
printf("%10s%10s%8.2f%8.2f%8.2f%8.2f%10.2f%10.2f\n",students[a].ID,students[a].Name,
students[a].Mark1,students[a].Mark2,students[a].Mark3,students[a].Mark4,students[a].All,students[a].Average);
}
}
void SearchLow()//搜索不及格的并输出
{
int a;
printf(" 语文不及格的有%10s%10s%8s\n","学号","姓名","语文");
for(a=0;a
{
if(students[a].Mark1<60)
printf("%10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark1); //从头搜索到尾,若小于60就输出
}
printf(" 数学不及格的有%10s%10s%8s\n","学号","姓名","数学");
for(a=0;a
{
if(students[a].Mark2<60)
printf("%10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark2);
}
printf(" 英语不及格的有%10s%10s%8s\n","学号","姓名","英语");
for(a=0;a
{
if(students[a].Mark3<60)
printf("%10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark3);
}
printf(" 计算机不及格的有%10s%10s%8s\n","学号","姓名","计算机");
for(a=0;a
{
if(students[a].Mark4<60)
printf("%10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark4);
}
system("pause"); //这个好像没作用
}
void SearchHigh()//搜索成绩最高者输出
{
int a;
int max ;
printf(" 语文最高分为%10s%10s%8s\n","学号","姓名","语文");
max=students[0].Mark1;
for(a=1;a
{
if(students[a].Mark1>max)
max=students[a].Mark1;
}
for(a=0;a
{
if(max==students[a].Mark1)
printf("%10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark1);
}
printf(" 数学最高分为%10s%10s%8s\n","学号","姓名","数学");
max=students[0].Mark2;
for(a=1;a
{
if(students[a].Mark2>max)
max=students[a].Mark2;
}
for(a=0;a
{
if(max==students[a].Mark2)
printf("%10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark2);
}
printf(" 英语最高分为%10s%10s%8s\n","学号","姓名","英语");
max=students[0].Mark3;
for(a=1;a
{
if(students[a].Mark3>max)
max=students[a].Mark3;
}
for(a=0;a
{
if(max==students[a].Mark3)
printf(" %10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark3);
}
printf(" 计算机最高分为%10s%10s%8s\n","学号","姓名","计算机");
max=students[0].Mark4;
for(a=1;a
{
if(students[a].Mark4>max)
max=students[a].Mark4;
}
for(a=0;a
{
if(max==students[a].Mark4)
printf(" %10s%10s%8.2f\n",students[a].Name,students[a].ID,students[a].Mark4);
}
system("pause");
}
void Save()
{
FILE*fp = fopen("temp.txt","w+");
fprintf(fp,"%d\n",num);
for(int i = 0 ; i< num ;i++)
{
fprintf(fp,"%s %s %f %f %f %f %f %f\n",students[i].ID,students[i].Name,students[i].Mark1,students[i].Mark2,students[i].Mark3,students[i].Mark4,students[i].All,students[i].Average);
}
fclose(fp);
}
void Load()
{
FILE*fp = fopen("temp.txt","r");
fscanf(fp,"%d",&num);
for(int i = 0 ; i< num ;i++)
{
fscanf(fp,"%s %s %f %f %f %f %f %f\n",students[i].ID,students[i].Name,&students[i].Mark1,&students[i].Mark2,&students[i].Mark3,&students[i].Mark4,&students[i].All,&students[i].Average);
}
fclose(fp);
}
/*主程序*/
int main(){
int i;
while(1){
Load();
printf("\t\t\t\t\t-------- 学生成绩管理系统-------\n\n\n\n"); //菜单
printf("\t\t\t\t\t1. 增加学生记录\n\n");
printf("\t\t\t\t\t2. 修改学生记录\n\n");
printf("\t\t\t\t\t3. 删除学生记录\n\n");
printf("\t\t\t\t\t4. 插入学生记录\n\n");
printf("\t\t\t\t\t5. 显示所有记录\n\n");
printf("\t\t\t\t\t6. 查询学生记录\n\n");
printf("\t\t\t\t\t7. 按平均成绩排序\n\n");
printf("\t\t\t\t\t8. 输出各科目不及格学生\n\n");
printf("\t\t\t\t\t9. 输出各科目最高分\n\n");
printf("\t\t\t\t\t0. 退出\n\n\n");
printf("请选择(0-9):");
scanf("%d",&i);
switch(i){
case 1:inputt();break;
case 2:modify();break;
case 3:deletee();break;
case 4:insert();break;
case 5:display();break;
case 6:search();break;
case 7:sort();break;
case 8:SearchLow();break;
case 9:SearchHigh();break;
case 0:exit(0);
default: ;
}
Save();
}
return 0;
}
void Save() {
FILE *fp = fopen("temp.txt", "w+");
fprintf(fp, "%d\n", num);
for (int i = 0 ; i < num ; i++) {
fprintf(fp, "%s %s %f %f %f %f %f %f\n", students[i].ID, students[i].Name, students[i].Mark1, students[i].Mark2,
students[i].Mark3, students[i].Average1);
}
fclose(fp);
}
void Load() {
FILE *fp = fopen("temp.txt", "r");
fscanf(fp, "%d", &num);
for (int i = 0 ; i < num ; i++) {
fscanf(fp, "%s %s %f %f %f %f %f %f\n", students[i].ID, students[i].Name, &students[i].Mark1, &students[i].Mark2,
&students[i].Mark3, &students[i].Average1);
}
fclose(fp);
调试示例error09_1.cpp(结构)
输入一个正整数n(3≤n≤10),再输入n个雇员的信息,包括姓名、基本工资、浮动工资和支出,输出每人的姓名和实发工资,实发工资=基本工资+浮动工资-支出。
输入输出示例:括号内为说明
输入:
3 (n=3)
zhao 240 400 75
qian 360 120 50
zhou 560 0 80
输出:
zhao 实发工资: 565.00
qian 实发工资: 430.00
zhou 实发工资: 480.00
#include
int main (void)
{
int i, n;
struct emp{
char name[10];
double jbg;
double fdg;
double zc;
} s[10];
scanf("%d", &n);
for(i=0;i
scanf("%s%lf%lf%lf",s[i].name, &s[i].jbg,&s[i].fdg,&s[i].zc);
for (i = 0; i < n; i++)
printf ("%5s 实发工资:%7.2f\n", s[i].name, s[i].jbg + s[i].fdg - s[i].zc);
}
90002 时间换算(结构)
输入一个正整数 repeat (0
输入一个时间数值,再输入秒数 n,输出该时间再过 n 秒后的时间值,时间的表示形式为时:分:秒,超过 24 时从 0 时重新开始计时。
输入输出示例:括号内为说明
输入:
3 (repeat=3)
0:0:1
59 (秒数n=59)
11:59:40
30 (秒数n=30)
23:59:40
301 (秒数n=301)
输出:
time: 0:1:0 (0:0:01加上59秒的新时间)
time: 12:0:10 (11:59:40加上30秒的新时间)
time: 0:4:41 (23:59:40加上301秒的新时间)
#include
int main(void)
{
int n;
int repeat, ri;
struct time{
int hour, minute, second;
}time;
scanf("%d", &repeat);
for(ri = 1; ri <= repeat; ri++){
scanf("%d:%d:%d", &time.hour, &time.minute, &time.second);
scanf("%d",&n);
time.second=time.second+n;
if(time.second>=60){
time.minute=time.minute+time.second/60;
time.second=time.second%60;
if(time.minute>=60){
time.hour=time.hour+time.minute/60;
time.minute=time.minute%60;
if(time.hour>=24)
time.hour=time.hour-24;
}
}
printf("time: %d:%d:%d\n", time.hour, time.minute, time.second);
}
}
90003 计算平均成绩(结构)
输入整数n(n<10),再输入n个学生的基本信息,包括序号、姓名和成绩,要求计算并输出他们的平均成绩(保留2位小数)。
输入输出示例:括号内为说明
输入:
3 (n=3)
1 zhang 70
2 wang 80
3 qian 90
输出:
average: 80.00
#include
int main(void)
{
int i, n;
double average, sum;
struct student{
int num;
char name[10];
int score;
}s[10];
scanf("%d", &n);
sum=0;
for(i=0;i
scanf("%d%s%d",&s[i].num,s[i].name,&s[i].score);
sum+=s[i].score;
}
average=sum/n;
printf("average: %.2f\n", average);
}
90004 计算两个复数之积(结构)
输入4个整数a1,b1,a2,b2,分别表示两个复数的实部与虚部,求两个复数之积(a1+b1i)*(a2+b2i),乘积的实部为:a1*a2-b1*b2,虚部为:a1*b2+a2*b1。
输入输出示例:括号内为说明
输入:
3 4 5 6
输出:
(3+4i) * (5+6i) = -9 + 38i
#include
int main(void)
{
struct complex{
int real;
int imag;
}product, x, y;
scanf("%d%d%d%d", &x.real, &x.imag, &y.real, &y.imag);
product.real=x.real*y.real-x.imag*y.imag;
product.imag=x.real*y.imag+x.imag*y.real;
printf("(%d+%di) * (%d+%di) = %d + %di\n", x.real, x.imag, y.real, y.imag, product.real, product.imag);
}
示例问题:创建图书目录
Gwen Glenn要打印一份图书目录。她想打印每本书的各种信息:书名、
作者、出版社、版权日期、页数、册数和价格。其中的一些项目(如,书
名)可以储存在字符数组中,其他项目需要一个int数组或float数组。用 7 个
不同的数组分别记录每一项比较繁琐,尤其是 Gwen 还想创建多份列表:一
份按书名排序、一份按作者排序、一份按价格排序等。如果能把图书目录的
信息都包含在一个数组里更好,其中每个元素包含一本书的相关信息。
因此,Gwen需要一种即能包含字符串又能包含数字的数据形式,而且
还要保持各信息的独立。C结构就满足这种情况下的需求。我们通过一个示
例演示如何创建和使用数组。但是,示例进行了一些限制。第一,该程序示
例演示的书目只包含书名、作者和价格。第二,只有一本书的数目。当然,
别忘了这只是进行了限制,我们在后面将扩展该程序。请看程序清单14.1及
其输出,然后阅读后面的一些要点。
程序清单14.1 book.c程序
//* book.c -- 一本书的图书目录 */
#include
#include
char * s_gets(char * st, int n);
#define MAXTITL41 /* 书名的最大长度 + 1 */
#define MAXAUTL31 /* 作者姓名的最大长度 + 1*/
struct book { /* 结构模版:标记是 book */
char title[MAXTITL];
1007char author[MAXAUTL];
float value;
}; /* 结构模版结束 */
int main(void)
{
struct book library; /* 把 library 声明为一个 book 类型的变量 */
printf("Please enter the book title.\n");
s_gets(library.title, MAXTITL); /* 访问title部分*/
printf("Now enter the author.\n");
s_gets(library.author, MAXAUTL);
printf("Now enter the value.\n");
scanf("%f", &library.value);
printf("%s by %s: $%.2f\n", library.title,
library.author, library.value);
printf("%s: \"%s\" ($%.2f)\n", library.author,
library.title, library.value);
printf("Done.\n");
return 0;
}
1008char * s_gets(char * st, int n)
{
char * ret_val;
char * find;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
find = strchr(st, '\n'); // 查找换行符
if (find) // 如果地址不是 NULL,
*find = '\0'; // 在此处放置一个空字符
else
while (getchar() != '\n')
continue; //处理输入行中剩余的字符
}
return ret_val;
}
我们使用前面章节中介绍的s_gets()函数去掉fgets()储存在字符串中的换
行符。下面是该例的一个运行示例:
Please enter the book title.
1009Chicken of the Andes
Now enter the author.
Disma Lapoult
Now enter the value.
29.99
Chicken of the Andes by Disma Lapoult: $29.99
Disma Lapoult: "Chicken of the Andes" ($29.99)
Done.
程序清单14.1中创建的结构有3部分,每个部分都称为成员(member)
或字段(
field)。这3部分中,一部分储存书名,一部分储存作者名,一部
分储存价格。下面是必须掌握的3个技巧:
为结构建立一个格式或样式;
声明一个适合该样式的变量;
访问结构变量的各个部分
建立结构声明
结构声明(
structure declaration)描述了一个结构的组织布局。声明类
似下面这样:
struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
该声明描述了一个由两个字符数组和一个float类型变量组成的结构。该
声明并未创建实际的数据对象,只描述了该对象由什么组成。〔有时,我们
把结构声明称为模板,因为它勾勒出结构是如何储存数据的。如果读者知道
C++的模板,此模板非彼模板,C++中的模板更为强大。〕我们来分析一些
细节。首先是关键字 struct,它表明跟在其后的是一个结构,后面是一个可
选的标记(该例中是 book),稍后程序中可以使用该标记引用该结构。所
以,我们在后面的程序中可以这样声明:
struct book library;
这把library声明为一个使用book结构布局的结构变量。
在结构声明中,用一对花括号括起来的是结构成员列表。每个成员都用
自己的声明来描述。例如,title部分是一个内含MAXTITL个元素的char类型
数组。成员可以是任意一种C的数据类型,甚至可以是其他结构!右花括号
后面的分号是声明所必需的,表示结构布局定义结束。可以把这个声明放在
所有函数的外部(如本例所示),也可以放在一个函数定义的内部。如果把
结构声明置于一个函数的内部,它的标记就只限于该函数内部使用。如果把
结构声明置于函数的外部,那么该声明之后的所有函数都能使用它的标记。
1011例如,在程序的另一个函数中,可以这样声明:
struct book dickens;
这样,该函数便创建了一个结构变量dickens,该变量的结构布局是
book。
结构的标记名是可选的。但是以程序示例中的方式建立结构时(在一处
定义结构布局,在另一处定义实际的结构变量),必须使用标记。我们学完
如何定义结构变量后,再来看这一点。
访问结构成员
访问结构成员
结构类似于一个“超级数组”,这个超级数组中,可以是一个元素为char
类型,下一个元素为forat类型,下一个元素为int数组。可以通过数组下标单
独访问数组中的各元素,那么,如何访问结构中的成员?使用结构成员运算
符——点(
.)访问结构中的成员。例如,library.value即访问library的value
部分。可以像使用任何float类型变量那样使用library.value。与此类似,可以
像使用字符数组那样使用 library.title。因此,程序清单 14.1 中的程序中有
s_gets(library.title, MAXTITL);和scanf("%f", &library.value);这样的代码。
本质上,.title、.author和.value的作用相当于book结构的下标。
注意,虽然library是一个结构,但是library.value是一个float类型的变
量,可以像使用其他 float 类型变量那样使用它。例如,scanf("%f",...)需要一
个 float 类型变量的地址,而&library.float正好符合要求。.比&的优先级高,
因此这个表达式和&(library.float)一样。
指向结构的指针
指向结构的指针
喜欢使用指针的人一定很高兴能使用指向结构的指针。至少有 4 个理由
可以解释为何要使用指向结构的指针。第一,就像指向数组的指针比数组本
身更容易操控(如,排序问题)一样,指向结构的指针通常比结构本身更容
易操控。第二,在一些早期的C实现中,结构不能作为参数传递给函数,但
是可以传递指向结构的指针。第三,即使能传递一个结构,传递指针通常更
有效率。第四,一些用于表示数据的结构中包含指向其他结构的指针。
下面的程序(程序清单14.4)演示了如何定义指向结构的指针和如何用
这样的指针访问结构的成员。
程序清单14.4 friends.c程序
/* friends.c -- 使用指向结构的指针 */
#include
#define LEN 20
struct names {
char first[LEN];
char last[LEN];
};
struct guy {
struct names handle;
char favfood[LEN];
char job[LEN];
1031float income;
};
int main(void)
{
struct guy fellow[2] = {
{ { "Ewen", "Villard" },
"grilled salmon",
"personality coach",
68112.00
},
{ { "Rodney", "Swillbelly" },
"tripe",
"tabloid editor",
432400.00
}
};
struct guy * him; /* 这是一个指向结构的指针 */
printf("address #1: %p #2: %p\n", &fellow[0], &fellow[1]);
him = &fellow[0]; /* 告诉编译器该指针指向何处 */
1032printf("pointer #1: %p #2: %p\n", him, him + 1);
printf("him->income is $%.2f: (*him).income is $%.2f\n",
him->income, (*him).income);
him++; /* 指向下一个结构 */
printf("him->favfood is %s: him->handle.last is %s\n",
him->favfood, him->handle.last);
return 0;
}
该程序的输出如下:
address #1: 0x7fff5fbff820 #2: 0x7fff5fbff874
pointer #1: 0x7fff5fbff820 #2: 0x7fff5fbff874
him->income is $68112.00: (*him).income is $68112.00
him->favfood is tripe: him->handle.last is Swillbelly
关键概念
关键概念
我们在编程中要表示的信息通常不只是一个数字或一些列数字。程序可
能要处理具有多种属性的实体。例如,通过姓名、地址、电话号码和其他信
息表示一名客户;或者,通过电影名、发行人、播放时长、售价等表示一部
电影DVD。C结构可以把这些信息都放在一个单元内。在组织程序时这很重
要,因为这样可以把相关的信息都储存在一处,而不是分散储存在多个变量
中。
设计结构时,开发一个与之配套的函数包通常很有用。例如,写一个以
结构(或结构的地址)为参数的函数打印结构内容,比用一堆printf()语句强
得多。因为只需要一个参数就能打印结构中的所有信息。如果把信息放到零
散的变量中,每个部分都需要一个参数。另外,如果要在结构中增加一个成
员,只需重写函数,不必改写函数调用。这在修改结构时很方便。
联合声明与结构声明类似。但是,联合的成员共享相同的存储空间,而
且在联合中同一时间内只能有一个成员。实质上,可以在联合变量中储存一
个类型不唯一的值。
enum 工具提供一种定义符号常量的方法,typedef 工具提供一种为基本
或派生类型创建新标识符的方法。
指向函数的指针提供一种告诉函数应使用哪一个函数的方法。
本章小结
本章小结
C 结构提供在相同的数据对象中储存多个不同类型数据项的方法。可以
使用标记来标识一个具体的结构模板,并声明该类型的变量。通过成员点运
算符(.)可以使用结构模版中的标签来访问结构的各个成员。
如果有一个指向结构的指针,可以用该指针和间接成员运算符(->)代
替结构名和点运算符来访问结构的各成员。和数组不同,结构名不是结构的
地址,要在结构名前使用&运算符才能获得结构的地址。
一贯以来,与结构相关的函数都使用指向结构的指针作为参数。现在的
C允许把结构作为参数传递,作为返回值和同类型结构之间赋值。然而,传
递结构的地址通常更有效。
联合使用与结构相同的语法。然而,联合的成员共享一个共同的存储空
间。联合同一时间内只能储存一个单独的数据项,不像结构那样同时储存多
种数据类型。也就是说,结构可以同时储存一个int类型数据、一个double类
型数据和一个char类型数据,而相应的联合只能保存一个int类型数据,或者
一个double类型数据,或者一个char类型数据。
通过枚举可以创建一系列代表整型常量(枚举常量)的符号和定义相关
联的枚举类型。
typedef工具可用于建立C标准类型的别名或缩写。
函数名代表函数的地址,可以把函数的地址作为参数传递给其他函数,
然后这些函数就可以使用被指向的函数。如果把特定函数的地址赋给一个名
为pf的函数指针,可以通过以下两种方式调用该函数:
#include /* 提供sin()函数的原型:double sin(double) */
...
double (*pdf)(double);
1121double x;
pdf = sin;
x = (*pdf)(1.2); // 调用sin(1.2)
x = pdf(1.2); // 同样调用 sin(1.2)
第九章目录:
第九章目录:
9.1定义和使用结构体变量293
9.1.1自己建立结构体类型293
9.1.2定义结构体类型变量295
9.1.3结构体变量的初始化和引用296
9.2使用结构体数组300
9.2.1定义结构体数组300
9.2.2结构体数组的应用举例301
9.3结构体指针303
9.3.1指向结构体变量的指针303
9.3.2指向结构体数组的指针304
9.3.3用结构体变量和结构体变量的指针作函数参数306
9.6使用枚举类型322
*9.7用typedef声明新类型名326
第九章课后题答案:
C语言程序设计第五版谭浩强课后答案 第九章习题答案_月已满西楼的博客-CSDN博客_在第三题的基础上,编写一个函数input