c指针和数组入门

标签: 翻译 c 指针 老外的文档有很多确实不错,翻译过来跟大家分享下。

c指针和数组入门

帖子NT流人 于 2010年 6月 10日 16:17

原文名:A TUTORIAL ON POINTERS AND ARRAYS IN C
作者:Ted Jensen
发表时间:Sept. 2003
原文档下载:
pointers.zip
A TUTORIAL ON POINTERS AND ARRAYS IN C
(159.95 KB) 被下载 34 次


c指针和数组入门

前言
引言
第一章:什么是指针?
第二章:指针类型和数组
第三章:指针和字符串
第四章:字符串
第五章:指针和结构体
第六章:字符串和字符串数组
第七章:多维数组
第八章:数组指针
第九章:指针和内存的动态分配
第十章:函数指针
结语


前言

本文档旨在向C语言新手介绍指针。这几年,我在各种C讨论组(有一些在FidoNet和UseNet上)里读写文章时,发现非常多的C语言新手会在理解指针的基本原理上遇到困难。因此,我开始着手用一些浅显的例子去解释指针。

该文档的第一个版本跟这个版本一样都是不受版权限制的。Bob Stout将第一个版本以PTR-HELP.TXT的文件形式发布在了他的网站SNIPPETS上。从1995年的原始版本发布,我已经在原来的基础上增加了很多材料并且做了一些小的修改。

之后在1998年左右,我在自己的站点发布了一个HTML版本。地址是:http://pweb.metcom.com/~tjensen/ptr/cpoint.htm

在大家的多次要求下,我终于发布了和那个HTML版本内容一样的PDF版本,该版本同样可以从我的网站找到。


鸣谢

很多不知名的人为这份文档做出了贡献,他们在FidoNet C Echo,UserNet新闻组comp.lang.c,或其他网站上的讨论组上提出了很多问题,这里很难将他们一一列出。特别感谢Bob Stout好心的将这个文档的第一个版本发布在他的SNIPPTES上。


关于作者

Ted Jensen是一个退休的电子学工程师,曾经在磁记录领域做过硬件工程师和经理。 从1968年开始,编程就一直是他的业余爱好,当时他刚学会了如何用打孔机在卡片上打孔然后再提交给主机执行。(当时的主机只有64K的内存)


本文档的使用

文档中的一切都是以公众域(Public Domain)形式发布的。任何人都可以用任何他们希望的方式复制和传播这份文档。唯一我想强调的是,这份文档要是被用作某门课程的教学材料,我很希望她能被当作一个整体发布,包含所有的章节,前言和引言。我同样非常希望这门课程的讲师能够将这件事通过以下的地址告诉我。我写该文档的目的就是希望她能够对别人有用并且我是不要求任何报酬的,而那些认为该文档有用的人的反馈,正是让我知道我已经部分实现目标的唯一途径。

顺便说一句,你不一定非要是讲师或教师才能联系我。我很高兴能收到任何人的反馈,不管是觉得该文档有用,还是提出建设性的批评。而且,我也很乐于回答提交到以下邮箱的问题。

Ted Jensen
Redwood City, California
tjensen@ix.netcom.com
July 1998


引言

如果你想熟练编写C语言代码,你必须对指针有足够多的应用知识。不幸的是,C指针就好像是新手的一块绊脚石,尤其是那些来自于其它语言的人,如Fortran,Pascal,Basic。

为了帮助新手理解指针我写了以下的材料。想要从这篇文档中获取更多的东西,我觉得运行文中的代码是很重要的。这些代码都是符合ANSI标准的,所以他们可以运行在任何ANSI标准编译器上。并且,我非常小心地将代码写在文本中,所以你只要借助ASCII文本编辑器,复制一段代码到新文件中就能在你的系统中编译了。我建议读者这么做,因为这将非常有助于你理解这份文档。
在指尖流浪
1. Everything changes and ends. 所有的事情在变化,都有终结
2. Things do not always go according to plan. 事情总会出乎意料(计划)之外
3. Life is not always fair. 生活并不总是公平
4. Pain is part of life. 痛苦是生活的一部分
5. People are not loving and loyal all the time. 人们并不总是热爱和忠诚
头像
NT流人
网站管理员
 
帖子: 744
加入时间: 2008年 1月 2日 13:15

第一章:什么是指针

帖子NT流人 于 2010年 6月 10日 16:17

第一章:什么是指针?

C初学者的一个难点就是指针的概念。这章就是向初学者介绍指针的概念和使用。

我发现初学者们之所以觉得指针很困难是因为他们对C语言的变量不太理解或者理解得不深刻。接下来,我们就大概的讨论一下C的变量。

程序中的变量是指有名称并且可以变化值的量。编译器和连接器将变量的值存储在计算机内存的一个特定的块里面。这个块的大小取决于该变量允许变化的最大值。譬如说,PC上整形变量的大小是2bytes,长整形的大小是4bytes。C的变量类型如整型所需块的大小在不同机器上是不一样的。

当我们声明一个变量的时候,我们告诉了编译器两样东西,变量名和变量类型。譬如,我们声明一个名称为k的整形变量时,应该写成:
代码: 全选
int k;


当发现表达式中的“int”,编译器会为该整型变量预留2bytes的内存(PC上),用来存储这个整数的值。同时,编译器还会建立一个符号表,用来记录符号k以及预留下的2bytes大小内存的相对位置。

接下来,如果我们如果写
代码: 全选
k =2 ;

我们认为,当这个表达式运行的时候,2这个值会被存放到为变量k预留的内存单元。在C里面我们把整型变量K看做一个“对象”。

从某种意义上说,实际上有两个“值”与对象k关联着。一个是存储起来的整数(上面例子中的2),另一个则是这个内存单元的“值”,也就是k的地址。有些教科书用术语右值(rvalue,发“are value”音)和左值(lvalue,发“el value”音)来分别称呼这两个值。

在某些语言中,左值(lvalue)是指放在赋值操作符‘=’左边的值(也就是右边值的地址)。右值(rvalue)是赋值表达式的右边部分,譬如上面的2。右值不能被放到赋值表达式的左边。因此,2 = k;是非法的。

实际上,上面左值的定义在C中被略加修改了。参照K&R II(197页):[1]
“对象是一个命名了的存储区域,左值则是指向这个对象的表达式。”


就这一点而言,上面引用的定义已经足够了。当我们变得更加熟悉指针的时候,我们会接着讨论更多的细节。

好,接着考虑:

代码: 全选
int j, k;
k = 2;
j = 7; // 第一行
k = j; // 第二行


上面的示例中,编辑器会将第一行的j解释成j变量的地址(他的左值),然后生成代码将7这个值拷贝到那个地址。第二行,j又被解释成他的右值(只要他在赋值操作符‘=’的右边)。就是说,j储存在内存单元里的7这个值,最终被拷贝到了左值k指定的地址了。

所有的例子中我们都是用2byte的整型,所以所有右值的拷贝(从一个存储单元到另一个存储单元)都是在拷贝2byte。当我们使用长整型时,就会拷贝4byte。

好,这个时候,我们就有理由希望设计出一个变量来存储左值(变量的地址)。存储这个值的空间大小依赖于系统。老一些64K内存的桌面电脑,内存中任何点的地址都可以被存放在2bytes中。拥有更多内存的计算机可能需要更多的bytes来存储这个地址。有些计算机,如IBM PC,在某些情况下可能需要特殊的处理方式去存储一个内存片段和偏移量。需要的实际大小并不是太重要,只要我们能有一种方法告诉编译器我们想存储的是一个地址。

这样的变量被称作为指针变量。C里面我们同样的也给指针一个类型,这个类型就是存储在指针对应的那个地址上的值的类型。比如:看看下面的变量声明:

代码: 全选
int *ptr;


ptr是变量名(就和上面例子中的整型变量名k一样)。星号 * 告诉编译器我们需要一个指针变量,然后编译器会在内存中预留一些bytes去存地址。int则是表明我们要用指针变量存一个整型的地址。我们称这样的指针“指向”某个整型。然而,注意一下,在写int k;时,我们并没有给k赋值。如果这个定义是在函数外的,ASNI标准的编译器会将k的值初始化为0。同样地,ptr也没有值,更确切的说是在上面的声明中我们并没有存地址进去。这种情况下,如果也是在函数外声明,ptr的值会被初始化一个值以保证他不会指向其它的C对象和函数。这样初始化了的指针被称做"空"("null")指针。

实际上一个空指针的二进制值(bit pattern)有可能等于0,也有可能不等于0,主要依赖于代码开发使用的系统。为了保证代码能够兼容不同系统的不同编译器,我们用一个宏来表示空指针。这个宏被命名为NULL。用这样的表达式ptr = NULL就可以使指针成为空指针。判断一个整数是否为0,可以用表达式 if (k == 0),而判断一个指针是否为空则可以用 if (ptr == NULL)。

好,接着回到我们的新变量ptr。如果我们想将整型变量k的地址存在ptr中,可以用一元操作符&,写成:

代码: 全选
ptr = &k;


&操作符的作用就是找出k的左值(地址),并且将这个值拷贝给指针ptr,即便k是在赋值操作符‘=’的右边。这时,ptr是指向k的。到现在,我们只剩下一个操作符要讨论了。

解引用操作符(dereferencing operator)就是星号,它的用法如下:
代码: 全选
*ptr = 7;


上面的表达式将7拷贝到ptr指向的地址。所以,如果ptr指向k,上面的表达式会将k的值设置为7。也就是说,用*我们得到的值是ptr指向的值,而不是指针本身的值。

同样的,我们可以写成这样:

代码: 全选
printf("%d\n", *ptr);


将ptr指向地址中存储的整数值打印到屏幕上。想搞明白这些东东是怎样结合起来的,你可以试着运行下面的程序,并好好研究代码和输出。

代码: 全选
------------ Program 1.1 ---------------------------------
/* Program 1.1 from PTRTUT10.TXT 6/10/97 */
#include <stdio.h>
int j, k;
int *ptr;
int main(void)
{
   j = 1;
   k = 2;
   ptr = &k;
   printf("\n");
   printf("j has the value %d and is stored at %p\n", j, (void *)&j);
   printf("k has the value %d and is stored at %p\n", k, (void *)&k);
   printf("ptr has the value %p and is stored at %p\n", ptr, (void*)&ptr);
   printf("The value of the integer pointed to by ptr is %d\n", *ptr);
   return 0;
}


注意:我们还应该讨论表达式(void *)的涵义。以后我们会讲解,现在你只需将它包含进测试代码。

复习:
    . 变量声明必须指定类型和名称(如 int k;)
    . 指针变量也必须指定类型和名称(如 int *ptr;)。星号告诉编译器ptr是一个指针变量,类型告诉编译器指针指向的类型。(这个例子中是整形)
    . 一旦变量被声明,我们就可以在前面加上一元操作符&来获取它的地址,如 &k。
    . 我们可以“解引用”(dereference)一个指针。就是说,可以用一元操作符*,如 *ptr,来获取指针指向的那个值。
    . 变量的“左值”(lvalue)是它的地址,就是它存在内存里的位置。而变量的“右值”就是存储在那个地址中的值。

第一章参考书目:
The C Programming Language 第二版
B. Kernighan and D. Ritchie
Prentice Hall
ISBN 0-13-110362-8
在指尖流浪
1. Everything changes and ends. 所有的事情在变化,都有终结
2. Things do not always go according to plan. 事情总会出乎意料(计划)之外
3. Life is not always fair. 生活并不总是公平
4. Pain is part of life. 痛苦是生活的一部分
5. People are not loving and loyal all the time. 人们并不总是热爱和忠诚
头像
NT流人
网站管理员
 
帖子: 744
加入时间: 2008年 1月 2日 13:15

第二章:指针类型和数组

帖子NT流人 于 2010年 6月 12日 17:30

第二章:指针类型和数组

好,我们继续。考虑下为什么我们需要确定指针指向的变量类型,就像下面这样:
代码: 全选
int *ptr;


这样做是为了,一旦ptr“指向”某个值,如当我们写成:
代码: 全选
*ptr = 2;

编译器就会知道需要拷贝多少字节到ptr指向的那个内存单元。如果ptr声明时指向的是一个整型,拷贝2字节,如果是长整型,就会拷贝4字节。同理,浮点型和双精度型则拷贝相应的字节数。其实呢,定义指针指向的类型还有其他一些有趣并且编译器可以明白的方法。举个例子,如果要在一个内存块中存10个连续的整数,需要给这10个整数预留20个字节的空间。

好,现在我们假设有一个指针ptr指向这些整数的第一位,并且这个整数在内存中的位置是100(十进制的),当我们写下面这个表达式时会发生什么:
代码: 全选
ptr + 1;


因为编译器知道这是个指针(它的值是地址),并且知道这个指针指向一个整数(当前位于内存中的地址是100)。所以编译器给ptr加了2,而不是1,因此,指针指向下一个整数的内存地址是102。同理,如果ptr是长整型指针,就会加4。其他数据类型也是一样的,如浮点型,双精度型或者用户自定义的结构体类型。显然,这与我们平时认为的加法有点不太一样。C里面把这种加法称为“指针算法”(pointer arithmetic),我们会在后面介绍这个术语。

++ptr和ptr++都等同于ptr+1(尽管ptr增长时实际位置是不一样的)。使用一元操作符++(放在ptr之前或之后都可以)来增长地址,而地址增幅的大小由指针指向对象的类型决定。如整型需要增长2,长整型需要增长4。

之前提到的内存块中存放的连续的10个整数,被定义为整数数组。接下来咱们就说说数组和指针之间有趣的关系。

考虑下面的代码:
代码: 全选
int my_array[] = {1,23,17,4,-5,100};


这里我们有一个包含6个整数的数组。我们可以通过my_array的下标来查询每个整数,从my_array[0]到my_array[5]。同时,我们也可以借助指针来查询,代码如下:
代码: 全选
int *ptr;
ptr = &my_array[0]; /*指针指向数组的第一位整数*/


这样我们就可以使用数组下标或指针的解引用(dereferencing)来打印数组。请看下面的代码示例:
代码: 全选
----------- Program 2.1 -----------------------------------
/* Program 2.1 from PTRTUT10.HTM 6/13/97 */
#include <stdio.h>
int my_array[] = {1,23,17,4,-5,100};
int *ptr;
int main(void)
{
int i;
ptr = &my_array[0]; /* 指针指向数组的第一个元素 */
printf("\n\n");
for (i = 0; i < 6; i++)
{
printf("my_array[%d] = %d ",i,my_array[i]); /*<-- A */
printf("ptr + %d = %d\n",i, *(ptr + i)); /*<-- B */
}
return 0;
}


编译运行上面的程序,注意A行和B行,你会发现两种写法打印出来的结果是一样的。再看下B行我们是怎么解引用(dereference)的,我们先给指针加上i,然后再解引用新的指针。改下B行试试:
代码: 全选
printf("ptr + %d = %d\n",i, *ptr++);

再运行下试试...再改下试试:(有完没完,有啥话不能一气儿说完)
代码: 全选
printf("ptr + %d = %d\n",i,*(++ptr));

每次运行时,仔细看输出是否与你预测的结果一致。

C标准里规定,任何使用&var_name[0]的地方都可以替换成var_name,因此将
代码: 全选
ptr = &my_array[0];

换成:
代码: 全选
ptr = my_array;

可以得到同样的结果。

这又得用很多文字去解释为什么数组的名称是指针。关于这一点,我更愿意理解成“数组的名称就是数组首个元素的地址”。很多初学者(包括我最开始时)都会想不明白,它怎么会是一个指针呢。比如,我们可以写成:
代码: 全选
ptr = my_array;

却不可以写成:
代码: 全选
my_array = ptr;


因为当ptr是变量,而my_array是常量(constant)。因为,一旦my_array[]声明了,my_array首元素的地址就被存起来,不会发生变化。

早些时候在讨论术语“左值”是我引用了 K&R-2 的规定:
“对象是一个命名了的存储区域,左值则是指向这个对象的表达式。”


这又引起了一个有趣的问题。既然my_array是一个命名的存储区域,那为什么上面赋值表达式中的my_array不是左值呢?为了解决这个问题,有人将my_array称为“不可改变的左值”(unmodifiable lvalue)。

修改上面的例程
代码: 全选
ptr = &my_array[0];


代码: 全选
ptr = my_array;

运行并验证下结果是不是一样滴。

现在,再让我们稍稍深入研究下例子中ptr和my_array的区别。有些作者愿意把数组成为常量指针(constant pointer)。说啥呢,到底啥意思啊?好,为了搞明白术语“常量”在这里是啥意思,让我们回到术语“变量”的定义上。当我们声明了一个变量,我们会在内存中预留一个空间,用来存储相应类型的值。一旦这个过程OK了,变量名就会被两种方式解释。一种,变量名用在赋值操作符的左边,解析器把它解释成内存地址,这个地址就来存与赋值操作符右边值相等的值;另外一种,变量名用在赋值操作符的右边,解释器就认为它是存储在为这个变量预留的内存地址中的具体内容。(老外说话真啰嗦)

趁热打铁,咱们来看看最简单的常量,如下:
代码: 全选
int i, k;
i = 2;

这里的i是个变量同时占用了内存中数据片段的空间。2则是一个常量,它并没有放在内存的数据片段中,而是直接嵌入到内存的代码片段。也就是说,当写成 k = i;时,编译器会创建一些代码,这些代码在运行时,会将内存地址&i上的值拷贝给k。而i = 2;只是简单的将2放进代码片段,并没有引用数据片段。因此,k和i都是对象,但2并不是对象。

类似地,既然my_array是常量,那么一旦确定数组存储的位置,编译器就能知道my_array[0]的地址,再看:
代码: 全选
ptr = my_array;


它只是简单的将这个地址作为代码片段中的常量,并没有引用数据片段。

这里可能更适合解释第一章1.1代码中的表达式(void *)的使用。正如你所见,我们有很多类型的指针。到目前为止,我们讨论了整型指针和字符指针。在接下来的章节中我们将学习结构体指针甚至指针的指针。

我们还知道了不同系统指针的大小也不同。指针的大小还依赖于指向对象的数据类型。因此,当你尝试将某种类型指针指向另一种类型指针变量时,你会遇到麻烦,就像你尝试将一个长整型赋值给一个短整型变量一样。

为了防止这种问题,C提供了一种指针类型void。我们可以用如下方式声明一个变量:
代码: 全选
void *vptr;


空类型指针(void pointer,与空指针null pointer区分开)有点像通用的指针。举个例子,C不允许整型指针和字符型指针做比较,这两者却都可以和空类型指针做比较。当然,跟其它变量一样,在适当条件下,强制类型转化也可以转换指针的类型。第一章1.1程序中,为了适应%p格式转换,我将整型指针强制转化为空类型指针。以后的章节中还会讲到其它的强制类型转换。好了,有太多的技术细节需要消化,我并不期望新手能在第一次阅读就全部搞懂。随着时间和不断的实验,你会想回来重新阅读这前两章的。不过,暂时还是让我们接着讨论指针,字符数组和字符串之间的奇妙关系吧~~
在指尖流浪
1. Everything changes and ends. 所有的事情在变化,都有终结
2. Things do not always go according to plan. 事情总会出乎意料(计划)之外
3. Life is not always fair. 生活并不总是公平
4. Pain is part of life. 痛苦是生活的一部分
5. People are not loving and loyal all the time. 人们并不总是热爱和忠诚
头像
NT流人
网站管理员
 
帖子: 744
加入时间: 2008年 1月 2日 13:15

第三章:指针和字符串

帖子NT流人 于 2010年 7月 13日 12:56

第三章:指针和字符串

字符串的学习将进一步将指针和数组联系起来。同时本章还将阐述标准C中一些字符串函数的作用方式,以及指针必须在何时或者应该怎样传递给函数。

C中,字符串是字符数组。这句话不一定适用于其它语言。在Basic,Pascal,Fortran或者其它什么语言中,字符串有他自己的数据类型。而在C中,字符串没有自己的数据类型,他是一个以二进制0字符(写作'\0')结束的字符数组。开始讨论之前,我们将看一段代码,这段代码更适合用来做演示说明,你在实际程序中可能永远不会这么写。好,上代码:
代码: 全选
char my_string[40];
my_string[0] = 'T';
my_string[1] = 'e';
my_string[2] = 'd';
my_string[3] = '\0';


尽管没人会这样构建一个字符串,但最终结果还是个字符串,一个以空字符(nul character)结尾的字符数组。根据定义,C中的字符串是一个以空字符结尾的字符数组。空字符的“空”跟“NULL”是不一样的。空字符的空是转义字符'\0',并且他只占一字节内存。另一方面,NULL则是一个初始化空指针的宏的名称。NULL在头文件中以#defined形式被定义,而空字符根本不需要定义。

上面那种代码的写法非常浪费时间,C提供了两种替代方案。第一种,你可以写成:
代码: 全选
char my_string[40] = {'T','e','d','\0'};


但是这种写法还是不够方便。因此,C还允许另一种写法:
代码: 全选
char my_string[40] = "Ted";


当使用双引号而不是单引号时,空字符('\0')自动追加到字符串的最后。上面的例子中发生了同样的事情:编译器预留一段40字节的连续内存来保存字符,并且初始化前4个字符为Ted\0。

现在,接着看下面的程序:
代码: 全选
------------------program 3.1-------------------------------------
/* Program 3.1 from PTRTUT10.HTM 6/13/97 */
#include <stdio.h>
char strA[80] = "A string to be used for demonstration purposes";
char strB[80];
int main(void)
{
char *pA; /* 一个字符指针 */
char *pB; /* 另一个字符指针 */
puts(strA); /* 打印strA */
pA = strA; /* 将pA指向strA */
puts(pA); /* 打印pA指向的字符串 */
pB = strB; /* pB指向strB */
putchar('\n'); /* 换行 */
while(*pA != '\0') /* A行 */
{
*pB++ = *pA++; /* B行 */
}
*pB = '\0'; /* C行 */
puts(strB); /* 打印strB */
return 0;
}
--------- end program 3.1 -------------------------------------


上面的代码我们定义了2个大小为80的字符数组。由于他们是全局定义,所以他们先被初始化为'\0',接着strA被初始化为引号中47个字符组成的字符串。

我们声明了2个字符指针,并在屏幕上打印了字符串。然后,我们将指针pA指向strA。实际上,表达式的意思是,将strA[0]的地址复制给变量pA。接着,我们用puts()将pA指向的值打印到屏幕上。puts()函数的原型是:
代码: 全选
int puts(const char *s);


暂时忽略const。传递给puts()的参数是一个指针,更确切的说是这个指针的值(因为C中传递的所有参数都是值),这个指针的值就是指针指向的地址,或者简单的说puts()的参数就是一个地址。因此,当我们写成puts(strA),实际上传递的是strA[0]的地址。

同理,我们写puts(pA);时,传参是同样的地址,因为pA = strA;

根据这个,接着看A行的while()表达式。A行的意思是:

只要pA指向的字符(即*pA)不是空字符(即没有用'\0'结尾),就一直执行下面的代码:

B行的意思是:复制pA指向的字符到pB指向的空间,然后自增pA确保它指向下一个字符,同时pB也指向下一个空间。

一旦我们复制了最后一个字符,pA就指向结尾的空字符并且循环终止。然而,我们并没有复制空字符,但是C中规定字符串必须以空字符结尾。所以我们在C行加上空字符。

运行这个程序,用调试器逐行观察strA,strB,pA和pB,对你的理解非常有帮助。如果将上面的strB的定义替换为如下:
代码: 全选
strB[80] = "12345678901234567890123456789012345678901234567890";

将会对你的理解更加有帮助。这里数字串的长度大于strA的长度,然后再单步运行下看看这些变量的变化。赶快动手试试吧!

咱们暂时回到puts()的原型上来,const是变量修饰器,他告诉用户这个函数不会修改s指向的字符串,就是说他将字符串看成一个常量。

当然,上面的代码实际上是一个简单的复制字符串的方法。试过上面的代码之后,你应该很了解到底发生了什么。我们甚至可以创建我们自己的函数来替代C的标准函数strcpy()。代码如下:
代码: 全选
char *my_strcpy(char *destination, char *source)
{
char *p = destination;
while (*source != '\0')
{
*p++ = *source++;
}
*p = '\0';
return destination;
}


(译者注:下面的代码更好一些
char *my_strcpy(char *destination, const char *source) {
assert((destination != NULL) && (source != NULL)); // assert用来检验参数是否正确
char *tmpAddress = destination; // 声明一个临时变量用来存储destination的首地址,因为在下面的代码中destination的地址发生了变化
while ((*destination++ = *source++) != '\0') ; // 将source全部复制到destination,复制完\0,退出循环
return tmpAddress; // 返回临时变量存储的destination的首地址,方便链式调用
}
)


这个例子中,我按照标准惯例返回了最终值的指针。
(译者注:这样做是为了函数的链式调用,如 int length = strlen(my_strcpy(strDest,"hello world"));)


这个函数被设计成可以接受两个字符指针的值(指针),因此前面的代码可以写成:
代码: 全选
int main(void) {
my_strcpy(strB,strA);
puts(strB);
}


我稍稍偏离了标准C的原型,标准C的原型是这么写的:
代码: 全选
char *my_strcpy(char *destination, const char *source);


这里的const修饰器告诉用户这个函数不会修改源指针指向的内容。你可以试着修改上面的函数和原型,将const修饰器添加进去。然后,在函数里你可以添加一个表达式去尝试修改源指针指向的内容,如下:
代码: 全选
*source = 'X';


这个表达式正常情况下会修改字符串的首字符为X。但是const修饰器却会让编译器捕获一个错误。动手试试看。

好,我们来看看上面的例子想告诉我们些什么。首先,*ptr++先返回ptr指向的值,然后再增加指针的值。这是根据操作符的优先级来的。当我们写成(*ptr)++时,我们增加的不是指针的值,而是增长指针指向的值!也就是说,如果(*ptr)++用在上面的例子中,首字母'T'会增长为'U'。你可以写些简单的示例代码证明这一点。

再次提醒下,字符串只不过是以'\0'结尾的字符数组。上面的例子实际上就是复制了一个数组,不过恰巧拷贝了字符数组。这种技术还可以用来复制整数数组,双精度数组等。只不过,因为处理的不是字符串,所以数组的结尾不是空字符。我们可以实现一个依赖结尾特殊值来复制的版本。比如说,我们可以复制一个结尾用负数标识的正整数数组。另一个更常见的情况,写一个函数去复制不是字符串的数组的部分元素。我们给函数传递需要复制元素的数量以及数组的地址,原型大概像下面这样:
代码: 全选
void int_copy(int *ptrA, int *ptrB, int nbr);


这里的nbr是需要复制整数的数量。你可以试着写int_copy(),再创建个整数数组,看看你的程序能不能跑起来。

这种方法还可以用来操作大数组。假设我们想操作一个包含5000个整数的数组,我们只需要给这个函数传数组的地址(当然还有其它辅助的参数,像上面的nbr参数,这取决于我们想做什么)。数组本身没有被传递,只传递了他的地址,就是说,函数调用前整个数组并没有被复制进堆栈。

这跟传递一个整数给函数是不一样的。当我们传递一个整数时,会生成一个复本,即将整数的值放进堆栈。函数中对这个传递值的任何操作都不会影响到原始整数。但是,由于数组和指针传递时是变量的地址,所以操作会改变原始变量的值。
在指尖流浪
1. Everything changes and ends. 所有的事情在变化,都有终结
2. Things do not always go according to plan. 事情总会出乎意料(计划)之外
3. Life is not always fair. 生活并不总是公平
4. Pain is part of life. 痛苦是生活的一部分
5. People are not loving and loyal all the time. 人们并不总是热爱和忠诚
头像
NT流人
网站管理员
 
帖子: 744
加入时间: 2008年 1月 2日 13:15

第四章:更多字符串

帖子NT流人 于 2010年 7月 14日 16:45

第四章:更多字符串

好的,我们在短时间内进步不小呢!让我们在另一个角度简单回顾下第三章的字符串复制。考虑下面的代码:
代码: 全选
char *my_strcpy(char dest[], char source[])
{
int i = 0;
while (source[i] != '\0')
{
dest[i] = source[i];
i++;
}
dest[i] = '\0';
return dest;
}


回忆下,字符串是字符数组。这里我们用数组代替指针实现这次的复制。结果是一样的,用这种方法复制字符串和之前的一样精确。这又带来些有趣的话题让我们讨论。

不管是传递字符指针还是像上面那样传递数组的名称,参数都是以值的形式被传递的,而且传递的实际上都是这些数组首个元素的地址。所以,当我们用字符指针或数组名称作为

参数是传递的都是这个参数的数字值(the numerical vale)(译者注:传递的是地址,地址是用数字表示的)。这说明在某种程度上source[i]和*(p+i)是一样滴。

实际上,这是正确的,任何程序中a[i]都可以被替换成*(a+i)。编译器会给他们创建同样的代码。因此,我们把指针算法和数组索引看成是同样的东西,两种语法都会得到同样的结果。

这不代表指针和数组是同样的东西,他们不一样。只是说找出数组的某个元素我们有两种语法,使用数组索引或指针算法,并且两种方法结果一致。

现在,看看上个表达是的一部分 (a+i),一个使用+操作符的简单加法。C的规则里面这样的表达式是可以被替换的,(a+i)等同于(i+a)。因此我们可以用*(i+a)来替换*(a+i)。

但是*(i+a)跟i[a]一样呀!又一个奇妙的事实。
假设:
代码: 全选
char a[20];
int i;


接着写:
代码: 全选
a[3] = 'x';



代码: 全选
3[a] = 'x';

是一样一样滴~~

试试看!设置一个字符数组,整数数组或长整型数组,等等,给第三或第四个元素用常规方法赋值,再打印下确保代码正常。然后像我上面那样翻转数组标识。一个好的编译器不会报错,并且结果一致。奇技淫巧罢了,没啥~~

接着看上面的函数,我们写:
代码: 全选
dest[i] = source[i];


由于数组索引和指针算法可以得到相同的值,我们可以写成:
代码: 全选
*(dest + i) = *(source + i);


但是,后者增加了2个加法操作。一般而言,加法比增量(就是++啦)更耗时。在现代优化编译器中可能不是这样,但谁能保证永远不是呢。因此,使用指针的那个版本比这个使用数组的版本要快那么一点点。

另一个给指针版本提速的方法,将:
代码: 全选
while (*source != '\0')


简写成:
代码: 全选
while (*source)


因为两种情况下圆括号中的值会同时变成0(FALSE)。

现在你可能想试着用指针写自己的程序。操作字符串是个不错的练习方法。你可以下面的标准函数写出自己的版本:
strlen();
strcat();
strchr();

未来的章节中我们会接着讨论字符串和指针对他们的操作。现在,我们还是先接着讨论下结构体吧~~
在指尖流浪
1. Everything changes and ends. 所有的事情在变化,都有终结
2. Things do not always go according to plan. 事情总会出乎意料(计划)之外
3. Life is not always fair. 生活并不总是公平
4. Pain is part of life. 痛苦是生活的一部分
5. People are not loving and loyal all the time. 人们并不总是热爱和忠诚
头像
NT流人
网站管理员
 
帖子: 744
加入时间: 2008年 1月 2日 13:15

第五章:指针和结构体

帖子NT流人 于 2010年 7月 15日 14:10

第五章:指针和结构体

众所周知,我们可以声明结构体来存储不同的数据类型。比如,一个人事文件可以用下面的结构体表示:
strcut tag {
char lname[20];/* 姓 */
char fname[20];/* 名 */
int age;/* 年龄 */
float rate;/* 假设时薪为12.75 */
}


假设磁盘中有一堆这样的结构体,我们想读出并打印着所有人的姓名,从而获得这些用户的列表。其它信息不用打印。我们希望有一个函数可以调用,并且直接给这个函数传递结构体的指针。为了演示我这里只使用一个结构体。但要意识到,我们的目的是写这个函数,而不是如何读文件,如何读文件大家可能已经知道怎么做了。

回顾下,我们可以用点操作符访问结构体的成员,如下:
代码: 全选
--------------- program 5.1 ------------------
/* Program 5.1 from PTRTUT10.HTM 6/13/97 */
#include <stdio.h>
#include <string.h>
struct tag {
char lname[20]; /* 姓 */
char fname[20]; /* 名 */
int age; /* 年龄 */
float rate; /* 假设时薪12.75*/
};
struct tag my_struct; /* 声明一个结构体my_struct */
int main(void)
{
strcpy(my_struct.lname,"Jensen");
strcpy(my_struct.fname,"Ted");
printf("\n%s ",my_struct.fname);
printf("%s\n",my_struct.lname);
return 0;
}
-------------- end of program 5.1 --------------


这个结构体在C程序里算是比较小的。我们可能会给这个结构体添加一些属性:(没写数据类型)
date_of_hire;(录用时间)
date_of_last_raise;(上次涨工资时间)
last_percent_increase;(上次涨工资幅度)
emergency_phone;(紧急联络电话)
medical_plan;(医疗计划)
Social_S_Nbr;(社保号码)
等等。。。


如果我们有很多雇员,操作这些结构体数据我们不得不依赖于函数。譬如说,我们希望有函数能打印任何一个结构体中的雇员姓名。然而,在原始C中(K&R,第一版)不允许传递结构体,只能传递结构体的指针。虽然,在ANSI C(译者注:即美国标准C)中,完整的结构体是可以被传递滴。但为了学习更多关于指针的知识,我们这里不那么做。

不管怎样,一旦要传递整个结构体,那就意味着我们必须把结构体的内容从一个函数复制到另外一个函数。用堆栈的系统中,则会将结构体的内容放到堆栈中。这样,遇到大的结构体就一定会引起问题。然而,传递指针则只使用最小的堆栈空间。

另外,以为我们讨论的是指针,所以这里只讨论怎样传递一个结构体指针,并在函数中使用它。

考虑下面的场景:我们想要一个可以接受结构体指针参数的函数,并且通过这个函数我们可以访问结构体的成员。比如说,我们想要打印出示例结构体中雇员的姓名。

好,现在我们知道指向结构体的指针可以用struct tag来声明。首先声明一个指针:
代码: 全选
struct tag *st_ptr;


然后我们将它指向我们的示例结构体:
代码: 全选
str_ptr = &my_struct;


现在我们就可以通过解引用(dereference)指针来访问某个成员。但是怎样解引用结构体指针呢?好的,接着看。想要用这个指针来设置雇员的年龄,我们可以这么写:
代码: 全选
(*st_ptr).age = 63;
(译者:CEO?董事?这么老了还不退休。哦,有可能是作者当时的年龄)

仔细观察下代码,它实际上用(*st_ptr)替换了结构体my_struct。因此,它和my_strcut.age是一样滴。

但因为它是个经常被使用的表达式,所以C设计者们又创造了一个可替换的语法:
代码: 全选
st_ptr->age = 63;


记住这一点,咱们再来看看下面的程序:
代码: 全选
------------ program 5.2 ---------------------
/* Program 5.2 from PTRTUT10.HTM 6/13/97 */
#include <stdio.h>
#include <string.h>
struct tag{ /* 结构体类型 */
char lname[20]; /* 姓 */
char fname[20]; /* 名 */
int age; /* 年龄 */
float rate; /* 假设时薪为12.75 */
};
struct tag my_struct; /* 定义一个结构体 */
void show_name(struct tag *p); /* 函数原型 */
int main(void)
{
struct tag *st_ptr; /* 声明一个结构体指针 */
st_ptr = &my_struct; /* 将指针指向my_struct */
strcpy(my_struct.lname,"Jensen");
strcpy(my_struct.fname,"Ted");
printf("\n%s ",my_struct.fname);
printf("%s\n",my_struct.lname);
my_struct.age = 63;
show_name(st_ptr); /* 指针传参 */
return 0;
}
void show_name(struct tag *p)
{
printf("\n%s ", p->fname); /* p指向一个结构体 */
printf("%s ", p->lname);
printf("%d\n", p->age);
}
-------------------- end of program 5.2 ----------------


哎呀,一下子有太多的信息需要消化了。读者应该编译运行这些代码片段,并且应该用调制器逐步跟踪代码进函数,看看my_struct和p到底发生了什么。
在指尖流浪
1. Everything changes and ends. 所有的事情在变化,都有终结
2. Things do not always go according to plan. 事情总会出乎意料(计划)之外
3. Life is not always fair. 生活并不总是公平
4. Pain is part of life. 痛苦是生活的一部分
5. People are not loving and loyal all the time. 人们并不总是热爱和忠诚
头像
NT流人
网站管理员
 
帖子: 744
加入时间: 2008年 1月 2日 13:15


回到 文档翻译

在线用户

正在浏览此版面的用户:没有注册用户 和 1 位游客

cron