C语言经典学习参考手册笔记.pdf

上传人:君**** 文档编号:34294683 上传时间:2022-08-15 格式:PDF 页数:31 大小:527.79KB
返回 下载 相关 举报
C语言经典学习参考手册笔记.pdf_第1页
第1页 / 共31页
C语言经典学习参考手册笔记.pdf_第2页
第2页 / 共31页
点击查看更多>>
资源描述

《C语言经典学习参考手册笔记.pdf》由会员分享,可在线阅读,更多相关《C语言经典学习参考手册笔记.pdf(31页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。

1、C语言指针导学 前言前言有人说,C语言成也指针败也指针,我觉得不无道理。指针确实是 C语言的精髓,它快捷,高效,被广泛的应用着,而正是它的灵活,也导致它变得相对复杂,它曾一度被指像 goto 语句一样难用,但指针有时是 C语言中表达计算的唯一方法,而且相较其他方法指针通常可以产生更高效、更紧凑的代码,所以正确地,规范地使用指针,是每个 C语言使用者必修的功课。想要做到这一点,首先在概念上要清晰,我会在后面的文章中把每个概念详细地介绍给大家。 现在来说一下写此文的目的:最主要的还是与朋友及前辈们相互学习、讨论。如果您是有着多年开发经验的达人,那么您无需驻足,如果您能赏光看看我的文章,很希望您能指

2、出我的错误观点以及纰漏之处,以免我误人子弟,并且促使我更加深入地研究和实践,便于我更快地进步。当然如果您是刚刚接触程序设计的朋友,对于标识符、变量、循环、数组都还不曾熟悉,那么我写的内容可能会让你感到迷茫,当然我会尽力阐述得易于理解。能从我写的东西中有些收获的应该是那些已经有一些 C语言基础但尚未涉及指针或者涉及不深以及对指针有兴趣并想要深入研究的朋友(或同学)。对于前一种朋友希望你们看过之后能对指针有一个比较全面的认识从而为以后的深入学习铺好路,而后一种朋友(其中有不少我的同学),希望我写的文章能使你们对概念更加清晰,从而更准确、更安全地去使用指针。 另外说明一下,我本人也还有很多知识或者一

3、些领域的技术不了解,不甚解,我很希望能和朋友们,同学们以及 CSDN中的前辈达人们好好探讨,学习。 还有一点,由于对外国著作的翻译,有些术语在国内的各种教材书籍中称呼不统一,这就会使有些朋友在参考资料时产生疑惑。我会在后面的文章中做出解释,并给出标准的英文原词和一些国内常用的翻译法,从而使大家不至于因为一个不同的翻译方式浪费时间,最重要的是,大家要弄懂一个概念是什么,而不是叫什么。对于同学们来说,记住英文术语十分必要,因为我们最终都要离开学校走进职场,关注一下这些业界常用的英文术语(仅是一个个零散的单词)花费不了你许多时间,但这会为你在今后带来或多或少显式隐式的各种好处! 一指针到底是什么一指

4、针到底是什么指针(pointer)到底是什么,弄清楚这个问题很重要,这是我们所讨论的话题的源头,而在阐述指针是什么之前,我们需要先来看一下变量的概念。 我们知道,计算机的内存(primary storage)被划分为多个存储单元,这些存储单元可以以单个或者顺序相连组成一个更大单元的方式被使用。每一个单独的存储单元都是一个字节(byte),它通常由8个位(bit)组成,每一个位可以表示的值只有 0或 1。每一个存储单元都被一个及其分配的标识唯一地表示,而这个标识就是地址。 下图表示了存储单元单独被操作时的情形,矩形表示存储单元,矩形内的内容是存储在这个内存单元的具体的值。矩形上方的数就是每个内存

5、单元的地址。因为每个单元为一个字节,而每个字符型常量(character constant)所占据的正是一个字节,如下所示: 再来看下面的图: 这次的情况是顺序连成组进行操作,对于整型常量(integer constant),在 32位计算机中需要四个字节来存储(有一点要声明,208位置的那个矩形里的 1078345超出了 int类型的范围,是 long int类型,但 ANSI C只规定了 long型数据长度不小于 int型,int型数据长度不小于 short型,并规定int型为 16位,long型为 32位,然而很多编译器采取的策略是使 long和 int型数据占据相同的内存字节数,即全为

6、 32位),所以地址以 4个单位增长(也就是说现在的一个矩形表示 4个内存单元),这次矩形下面多了几个小写字母,存储在矩形里面的值不是固定唯一的,而是可变的。我们可以把矩形认为是一个变量(variable),每次我们要引用矩形里的值时,机器都是通过地址来定位(那个矩形)并取得其中的值的,而对于我们来说要记住这些地址几乎是不可能的,所以高级语言提供了用名字来访问内存位置的特性,它们就是变量名,即上图的 a,b,c,d。 现在用变量名替换掉上图中的地址: 大家要注意,变量名与地址的关联是由编译器为我们实现的,具体的实现方式我们无需关心,但要清楚硬件仍然是通过地址访问内存位置的。 ( (变量名是给编

7、译器看的,编译器根据变量是局部还是全局分配内存地址或栈空间,所谓的变量名在内存变量名是给编译器看的,编译器根据变量是局部还是全局分配内存地址或栈空间,所谓的变量名在内存中不存在,操作时转换成地址数存放在寄存器中了) 中不存在,操作时转换成地址数存放在寄存器中了) 接下来,继续看图: 来看新增的 Ptr,同样是个变量,它也有地址,它的值是变量 a的地址。至此可以给出指针的定义了:指针是一种用于存放另一个变量的地址的变量。指针是一种用于存放另一个变量的地址的变量。上图中的 Ptr就是一个指针,并且我们说它指向了变量 a(因为 Ptr的值是变量 a的地址),要注意指针中只能存放地址,不能将一个整型量

8、或者其他非地址类型(整型数 0及具有 0值的整形常量表达式除外,后面的文章会细致讲解)的数据赋给一个指针! 还有,指针这个词,由于是对 pointer这个词翻译得来,完整的叫法应该是指针变量,由于指针变量中存的地址,而在大多数外国资料中,指针(pointer)的涵义不完全都是指针变量,有时也指地址,请大家在阅读或参考资料时注意区分! (注:本将及后续章节中用以表示内存的矩形中的值在实际中是以二进制数表示的,地址是以十六进制数(注:本将及后续章节中用以表示内存的矩形中的值在实际中是以二进制数表示的,地址是以十六进制数表示的,文章中只是为了简洁易读从而采用十进制数表示。)表示的,文章中只是为了简洁

9、易读从而采用十进制数表示。) 二指针的定义及运算二指针的定义及运算上一讲已经说过,指针是一种变量,它也有自己的地址,但由于它是专门用来存放地址的变量, 所以把它认为是种特殊的变量, 既然有着特殊的身份, 那么也理应受到特殊的待遇,下面来看看它享受了那些优待。 1指针的定义指针的定义 在 C 语言中,定义一个普通的变量(如整型数),我们这样做:int i; 而定义一个指针变量(指针)我们需要这样做:int *p ; 还记得吗,一个矩形中的值是有类型的,可能是整型,可能是字符型,它们原本是“清白”的,无类型的,是我们通过一些手段使它们有了类型。当我们做出 int i; 这样一个定义时,编译器就会分

10、配一个地址(例如 200)并和 i 关联起来,而 int 将限定编译器把这个区域中的内容作为整型数看待。 矩形内的值被视为矩形内的值被视为 int 型型 现在我们又有了 int *p; 这个定义,假设 p 是指向变量 i 的(见下图),p 中存的是变量 i 的地址。 * 表示 p 是一个指针, 而 int 表示 p 中所存的地址对应的变量 (即变量 i) 的类型是 int。 p 指向指向 i , int *p; 中的; 中的 int 是指针是指针 p 所指向的变量的类型所指向的变量的类型 我们将 int 称为指针 p 的基类型,或指针 p 所指向的变量的类型。 类似地,我们可以有: char

11、*s ; ( s 是指向 char 型变量的指针 ) float *f ; ( f 是指向 float 型变量的指针 ) double *d ; ( d 是指向 double 型变量的指针 ) 由此得到声明一个指针变量(指针)的一般形式 : 基类型基类型 * 指针名指针名; 有一点要注意,在定义指针时,以下两种方式都是允许的,例如: int *ptr1; int* ptr2; 但一般比较倾向用第一种,因为可以避免以下的误解: int* prt1, ptr2; 这样的定义方式,容易使人误以为 ptr2 也是一个指针,事实上并不是,prt2 是一个 int 型变量,以下的定义方式中 ptr1 与

12、ptr2 才都是指针: int* ptr1, *ptr2; 2.指针的运算指针的运算 &(address-of operator)取地址操作符:)取地址操作符: 究竟如何使一个指针指向一个变量呢?后面的语句给出了解答:int *p = &i;& 用于取一个对象的地址(本文说的对象是泛指的某一事物,如变量,数组等,和(本文说的对象是泛指的某一事物,如变量,数组等,和 C+中的对象概中的对象概念不同)念不同),这里用于将 i 的地址赋给 p , 那么指针 p 就指向了变量 i 。上述的语句也可以分开写,如:int *p; p = &i; 小扩展:小扩展:(下面大括号中的内容,出涉指针的朋友可以跳过

13、,当然也可以作为扩展知识) &的实质:当对一个 T 类型对象进行 & 操作时,返回的是一个“指向 T 的指针”类型的常量,即指针常量(pointer constant),在我们使用&运算符时我们并不关心它是如何实现的,因为有编译器帮我们隐藏了这些细节。 可当我们想要对一个指针赋一个绝对地址的时候, 这个问题就体现出来了, 而且我们不得不去关注,在 C 语言中没有一种内建(built-in)的方法去表示指针常量,所以当我们使用它的时候通常先写成整型常量的形式,然后再通过强制类型转换把它转换成相应的类型,如:int * , double * , char *等。 所以后面所示的做法是不行的: in

14、t *p = 0 x12345678 ; 正确的方式应为:int *p = (int *) 0 x12345678; 也许大家还记得我在第一讲中说的要注意指针中只能存放地址, 不能将一个非 0 值整型常量表达式或者其他非地址类型的数据赋给一个指针, 原因就在此。在大多数计算机中,内存地址确实是以无符号整型数来表示的,而且多以 16 进制表示,但我们在 C 语言中不能用整型数去表示地址,只能用指针常量来表示,因为它是被用来赋给一个指针的。 int *p = &a; int *p = &a; int *p = (int *) 0 x12345678; int *p = (int *) 0 x123

15、45678; 对于这个赋值问题还可以换一个角度去理解,在 C 语言中,使用赋值操作符时,赋值操作符左边和右边的表达式类型应该是相同的, 如果不是, 赋值操作符将试图把右边表达式的值转换为左边的类型。所以如果写出 int *p = 0 x12345678 ; 这条语句编译器会报错:= : cannot convert from const int to int * , 因为赋值操作符左边和右边的表达式的类型应该相同,而 0 x12345678 是 int 型常量,p 是一个指向 int 型的指针,两者类型不同,所以正确的方式是:int *p = (int *) 0 x12345678 ; * (

16、Dereference operator) 解引用操作符解引用操作符 * 在定义时用来说明一个变量是指针,而在定义了一个指针之后,我们使用(引用)指针时,*p 表示的是 p 所指向的对象(即 i)。也就是说,对于一个已定义的指针使用 * 操作符,将访问这个指针所指向的对象,我们来看下面的程序: #include int main( ) int i; /* 定义一个 int 型变量 i */ int *p; /* 定义一个指向 int 类型的指针 p */ i = 2 ; /* 初始化 i 为 2 */ p = &i ; /* 将 i 的地址赋给 p ,即使 p 指向 i */ printf(%

17、d/n, i ) ; /* 输出 i 的值 */ printf(%d/n, *p ) ; /* 输出 p 所指向的存储单元的值,即 i 的值*/ return 0 ; /* 标准 C 语言主函数应返回一个值,用以通知操作系统程序执行成功与否,通常 0 表示成功*/ 程序输出结果为: 2 2 对于 * 操作符, 由于它有两个等价的术语 dereference 和 indirection , 所以在国内的书籍中你会看到各种翻译方法, 如: 解引用、 解除引用、 反引用、 反向引用、 间接引用、 间接访问 只要你知道它是用来访问一个指针所指向的对象的, 那么不管它叫什么都不重要了。 还是那句话, 弄

18、懂是什么, 不要在乎叫什么, 如果你理解了它的真正含义, 大可以简洁地称它为 “星号”操作符! 3.指针的初始化指针的初始化 ANSI C 定义了零指针常量的概念:一个具有 0 值的整形常量表达式,或者此类表达式被强制转换为 void *类型,则称为空指针常量,它可以用来初始化或赋给任何类型的指针。也就是说,我们可以将 0、0L、/0、22、0*5 以及(void *)0 赋给一个任何类型的指针,此后这个指针就成为一个空指针,由系统保证空指针不指向任何对象或函数。 ANSI C 还定义了一个宏 NULL,用来表示空指针常量。大多数 C 语言的实现中 NULL是采用后面这种方式定义的:#defi

19、ne NULL (void *)0)。 对指针进行初始化时常用的有以下几种方式: 1.采用 NULL 或空指针常量,如:int *p = NULL;或 char *p = 2-2; 或 float *p = 0; 2.取一个对象的地址然后赋给一个指针,如:int i = 3; int *ip = &i; 3.将一个指针常量赋给一个指针,如:long *p = (long *)0 xfffffff0; 4.将一个 T 类型数组的名字赋给一个相同类型的指针, 如: char ary100; char *cp = ary; 5.将一个指针的地址赋给一个指针,如:int i = 3; int *ip

20、= &i;int *pp = &ip; 6.将一个字符串常量赋给一个字符指针,如:char *cp = “abcdefg”; 对指针进行初始化或赋值的实质是将一一个地址或同类型地址或同类型(或相兼容的类型或相兼容的类型)的指针的指针赋给它,而不管这个地址是怎么取得的。要注意的是:对于一个不确定要指向何种类型的指针,对于一个不确定要指向何种类型的指针,在定义它之后最好把它初始化为在定义它之后最好把它初始化为 NULL,并在解引用这个指针时对它进行检验,防止解引,并在解引用这个指针时对它进行检验,防止解引用空指针。另外,为程序中任何新创建的变用空指针。另外,为程序中任何新创建的变量提供一个合法的初

21、始值是一个好习惯,它可量提供一个合法的初始值是一个好习惯,它可以帮你避免一些不必要的麻烦。以帮你避免一些不必要的麻烦。 4void *型指针型指针 ANSI C 定义了一种 void *型指针,表示定义一个指针,但不指定它指向何种类型的数据。void *型指针作为一种通用的指针,可以和其它任何类型的指针(函数指针除外)相互转化而不需要类型强制转换,但不能对它进行解引用及下标操作。C 语言中的 malloc 函数的返回值就是一个 void *型指针,我们可以把它直接赋给一个其他类型的指针,但从安全的编程风格角度以及兼容性上讲,最好还是将返回的指针强制转换为所需的类型,另外,malloc在无法满足

22、请求时会通过返回一个空指针来作为“内存分配失败”的信号, 所以要注意返回值指针的判空。 5指向指针的指针指向指针的指针 在指针初始化的第 5 种方式中提到了用一个指针的地址来初始化一个指针。回忆一下上一讲的内容: 指针是一种变量, 它也有自己的地址, 所以它本身也是可用指针指向的对象。我们可以将指针的地址存放在另一个指针中,如: int i = 5000; int *pi = &i; int *ppi = π 变量名变量名 指针类型指针类型 指针所指向的类型指针所指向的类型 指针的值指针的值 指针本身所占内存区指针本身所占内存区 如何得到如何得到 i i 的值的值 intint i =

23、5000;i = 5000; i i i i int *pi = &i;int *pi = &i; pipi int *int * intint &i&i &pi&pi *pi*pi int *ppi = πint *ppi = π ppippi int *int * i intnt* * &pi&pi &ppi&ppi *ppi*ppi 此时的 ppi 即是一个指向指针的指针,下图表示了这些对象: i 的地址为 108,pi 的内容就是 i 的地址,而 pi 的地址为 104,ppi 的内容即是 pi 的地址。对ppi 解引用照常会得到 ppi 所指的对象,所获得的对象是指向 i

24、nt 型变量的指针 pi。想要真正地访问到 i.,必须对 ppi 进行两次解引用,如下面代码所示: printf(%d, i ); printf(%d, *pi ); printf(%d, *ppi ); 以上三条语句的输出均为 5000。 三指针与数组的三指针与数组的“ “爱恨情仇爱恨情仇” ” 本将中指针的算术运算本应放在第二讲中,但考虑到它与数组关系密切故将其纳入本将。 1.1.指针的算术运算指针的算术运算 在上一讲指针初始化的第 4种方式中提到了可以将一个 T类型数组的名字赋给一个相同类型的指针,这说明指针可以和数组发生联系,在后面我们会看到这种联系是十分密切的。当有语句 char a

25、ry100 = a, b, c, d, e, f; char *cp = ary; 后, cp就指向了数组 ary中的第一个元素。 我们可以通过指针来访问数组的元素: printf(%d, *cp);此语句的作用是打印出 cp所指向的元素的值,也就是数组的第一个元素。现在通过 cp = &ary3; 使cp指向数组中的第 4个元素,然后我们就可以对它进行各种操作了。 实际中经常会用指针来访问数组元素,当两个指针指向同一个数组时,会用到指针的算术运算: 指针指针 + + 整数整数 或或 指针指针 - - 整数整数 指针与一个整数相加的结果是一个另一个指针。例如将上面的 cp加 1,运算后产生的指

26、针将指向数组中的下一个字符。事实上当指针和一个整数相加减时,所做的就是指针加上或减去步长乘以那个整数的积。所谓步长就是指针所指向的类型的大小(即指针移动一个位置时要跳过几个字节)。下面的例子会让大家更加明了: int ia100 = 0, 1, 2, 3, 4, 5; double da100 = 0.0, 1.0, 2.0, 3.0, 4.0, 5.0; int *ip = ia; double *dp = da; ip += 3; dp += 3; ip加上 3实际进行的操作是 ip + 4 * 3,因为 ip指向的元素的类型为 int;而 dp加 3实际进行的操作是 dp + 8 * 3

27、,因为 dp指向的元素的类型为 double;这正是指针需要定义基类型的原因,因为编这正是指针需要定义基类型的原因,因为编译器要知道一个指针移动时的步长。译器要知道一个指针移动时的步长。 要注意的是指针的算术运算只有在原始指针和计算出来的新指针都指向同一个数组的元素或指向数组范围的下一位置时才是合法的;另外,这种形式也适用于使用 malloc动态分配获得的内存。 下面这段代码在大多数编译器上都是可以运行的,但它却是不安全的,因为 b元素后面的内存区域所存储的内容是不确定的,有可能是受系统保护的,如果又编写了对 p解引用的语句,那么很可能会造成运行时错误: int b; int *p = &b;

28、 p += 2; printf(%p/n, p); 。指针间的减法。指针间的减法 当两个指针指向同一数组或有一个指针指向该数组末端的下一位置时,两个指针还可以做减法运算。 int ia100 = 0, 1, 2, 3, 4, 5; int *ip = ia; int *ig = ia +3; ptrdiff_t n = ig ip; n应该为 3,表示这两个指针所指向的元素的距离为 3,ptrdiff_t是一个标准库类型,它是一个无符号整数,可以为负数。注意指针进行减法得到的结果指示出两指针所指向元素间的距离,即它们之间相隔几注意指针进行减法得到的结果指示出两指针所指向元素间的距离,即它们之间

29、相隔几个数组元素,与步长的概念无关。个数组元素,与步长的概念无关。 另外,一个指针可以加减 0,指针保持不变;如果一个指针具有 0值(空指针),则在该指针上加 0也是合法的,结果得到另一个值为 0的指针;对两个空指针做减法运算,得到的结果也是 0。注意:注意:ANSI CANSI C标准没有定义两个指针相加的运算,如果两个指针相加,绝标准没有定义两个指针相加的运算,如果两个指针相加,绝大多数编译器会在编译期报错。大多数编译器会在编译期报错。 2 2指针与数组的爱恨情仇指针与数组的爱恨情仇 数组和指针有着千丝万缕的联系,它们之间的问题困惑着不少朋友,有的朋友对它们的概念不是很清楚,所以可能会导致

30、误用,从而出错。下面就对数组名是什么,数组什么时候和指针相同等相关问题做出解释。 数组名数组名 声明中:声明中:当我们声明一个数组时,编译器将根据声明所指定的元素数量及类型为数组保留内存空间,然后再创建数组名,编译器会产生一个符号表,用来记录数组名和它的相关信息,这些信息中包含一个与数组名相关联的值, 这个值是刚刚分配的数组的第一个元素的首地址(一个元素可能会占据几个地址, 如整型占 4个,此处是取起始地址)。现在声明一个数组:int ia100; 编译器此时为它分配空间,假设第一个数组元素的地址为 0 x22ff00;那么编译器会进行类似#define ia 0 x22ff00的操作,这里只

31、是模拟,真实情况并非完全一样,我们在编程时无需关注编译器所做的事情,但要知道此时(声明时)数组名只是一个符号,它与数组第一个元素的首地址相关联。注意:数组的属性和指针的属性不相同,在声明数组注意:数组的属性和指针的属性不相同,在声明数组时,同时分配了用于容纳数组元素的空间;而声明一个指针时,只分配了用于容纳指针本身的空时,同时分配了用于容纳数组元素的空间;而声明一个指针时,只分配了用于容纳指针本身的空间。间。 表达式中:表达式中:当我们在表达式中使用数组名,如:ia10 = 25;时,这个名字会被编译器转换为指向数组第一个元素的常量指针(指针本身的值不可变),它的值还是数组的第一个元素的首地址

32、(一个指针常量),编译器的动作类似于 int *const ia = (void *)0 x22ff00; 这里我们应重点关注的是:数这里我们应重点关注的是:数组名是一个常量指针组名是一个常量指针( (常指针常指针) ),即指针自身的值不能被改变。,即指针自身的值不能被改变。如果有类似 ia+或 ia+=3这类的语句是绝对不对的,会产生编译错误。注意:当数组名作为注意:当数组名作为 sizeofsizeof操作符的操作数时,返回的是整个数组的长操作符的操作数时,返回的是整个数组的长度,度,也就是数组元素的个数乘以数组元素类型的大小;另外,在对数组名实施也就是数组元素的个数乘以数组元素类型的大小

33、;另外,在对数组名实施 & & 操作时,返回的是一个指向操作时,返回的是一个指向数组的指针,而非具有某个指针常量值的指针数组的指针,而非具有某个指针常量值的指针(这个问题在后面会详细论述)。 通过数组名引用数组元素时:通过数组名引用数组元素时:在前面讲过的指针算术运算中指针加上一个整型数,结果仍然是指针,并且可以对这个指针直接解引用,不用先把它赋给一个新指针。如 int last = *(ia + 99); 此时的ia已经是一个常指针了,这个表达式计算出 ia所指向元素后面的第 99个元素的地址,然后对它解引用得到相应的值。这个表达式等价于 int last = ia99;事实上每当我们采用

34、的方式引用数组元素时,如:ia99,在编译器中都会转换成指针形式,也就是*(ia + 99) (这里 ia + 99 和 &ia99 的值都为数组最后一个元素的首地址,所以*(ia + 99)和*&ia99得到的结果是一样的,较难理解的是*&ia99,按照优先级和结合性规则,先对 ia99 取地址再解引用,有些编译器见到这种表达式会直接优化成 ia99。)现在可以看出来在表达式中,指针和数组名的使用可以互换,但唯一要注意的就是:数组名是常指针,不能对数组名是常指针,不能对它的值进行修改。它的值进行修改。ia + 99ia + 99是可以的,但是可以的,但 ia+ia+是不行的,它的意思是是不行

35、的,它的意思是 ia = ia ia = ia +1;+1; 修改了修改了 iaia的值。的值。 作为函数参数:作为函数参数: 先来了解一下函数的实参与形参。 实参(argument)是在实际调用时传递给函数的值;形参(parameter)是一个变量,在函数定义或者原型中声明。C语言标准规定作为形参的数组声明转换为作为形参的数组声明转换为指针指针。在声明函数形参的特定情况下,编译器会把数组形式改写成指向数组第一个元素的指针。在声明函数形参的特定情况下,编译器会把数组形式改写成指向数组第一个元素的指针。所以不管下面哪种声明方式,都会被转换成指针: void array_to_pointer(in

36、t *ia) /无需转换 void array_to_pointer(int ia ) /被转换成*ia void array_to_pointer(int ia100 ) /被转换成*ia 那么如果有下面的操作 void array_test(int ia100) double da10; printf(%d, sizeof( ia ); ia+; /da+; /编译错误,数组名是常指针 输出的结果为 4,此时的 ia是作为函数形参而声明的数组,已经被转换为了一个不折不扣的指针(不再是常指针了),因此 ia+;是合法的,不会引发编译错误。为什么 C语言要把数组形参当作指针呢?因为 C语言中所

37、有非数组形式的数据实参(包括指针)均以值传递形式调用(所谓值传递就是拷贝出一个实参的副本并把这个副本赋值给形参,从此实参与形参是各不相干的,形参值的变化不会影响实参)。如果要拷贝整个数组,在时间和空间上的开销都很大,所以把作为形参的数组和指针等同起来是出于效率原因的考虑。我们可以把形参声明为数组(我们打算传递给函数的东西)或者指针(函数实际接收到的东西), 但在函数内部,编译器始终把它当作一个指向数组第一个元素(数组长度未知)的指针。在函数内部,对数组参数的任何引用都将产生一个对指针的引用。我们没有办法传递一个数组本身,因为它总是被自动转换为指向数组首元素的指针,而在函数内部使用指针时,能对数

38、组进行的操作几乎和传递数组没有区别,唯一不同的是:使用 sizeof(形参数组名)来获得数组的长度时,得到的只是一个指针的大小,正如上面所述的 ia。但要但要注意:以上讨论的都是数组名作为函数形参的特殊注意:以上讨论的都是数组名作为函数形参的特殊情况,当我们在函数体内声明一个数组时,它就是一个情况,当我们在函数体内声明一个数组时,它就是一个普通的数组,它的数组名仍是一个常指针,所以上面的普通的数组,它的数组名仍是一个常指针,所以上面的 da+;da+;仍会引起编译错误,请大家不要混淆。仍会引起编译错误,请大家不要混淆。 还有一点,既然是值传递,那么理所当然地,在用数组名作为实参调用函数时,实参

39、数组名同样会被转换为指向数组第一个元素的指针。 指向数组的指针指向数组的指针 好了,关于数组名的讨论可以告一段落了,现在来看指针与数组的另一种联系。在前面说过,当对一个一维数组的数组名进行 & 操作时,返回的是一个指向数组的指针。现在我们就来看看什么是指向数组的指针。在 C语言中,所谓的多维数组实际上只是数组的数组,也就是说一个数组中的每个元素还是数组,由于二维数组较为常用, 所以本文着重讨论二维数组, 更多维数组的原理与二维数组相同。 所谓二维数组(数组的数组),就是每个元素都是一个一维数组的一维数组。另外,请大家先有一个感性的认识:指向数组的指针主要用来对二维数组进行操作,大家不理解没有关

40、系,我会在后面详细说明。 通常我们声明一个指向一维数组中的元素的指针是这样做的:int ia100, *ip = ia; ip指向这个数组的第一个元素,通过指针的算术运算,可以让 ip指向数组中的任一元素。对于二维数组,我们的目的同样是让一个指针指向它的每一个元素,只不过这次的元素类型是一个数组,所以在声明这个指针时稍有不同,假设有二维数组 int matrix50100, C语言采用如下的方式来声明一个指向数组的指针。int (*p) 100; 比普通声明稍复杂一些,但并不难理解。由于括号的优先级是最高的,所以首先执行解引用,表明了 p是一个指针,接下来是数组下标的引用,说明 p指向的是某种

41、类型的数组,前面的 int表明 p指向的这个数组的每个元素都是整数。对于这个声明还可以换一个角度来理解:现在要声明的是一个指针,因此在标识符 p前面加上*。如果从内向外读 p的声明,可以理解为*p是 int100 类型,即 p是一个指向含有 100个元素的数组的指针。 有些朋友可能对于一个用来操纵二维数组的指针只使用一个下标表示困惑,为什么声明不是 int (*p) 50100呢?现在来回顾一下操纵一维数组的指针声明 int *ip = ia;它表示 ip指向了一个数组的第一个元素, 通过对指针的算术运算可以使它指向数组中的任何一个元素, 编译器不需要知道指针 ip指向的是一个多长的数组。对于

42、二维数组道理相同,int (*p) 100 = matrix; matrix 可以看成是一个长度为 50的一维数组,每个元素都是一个 int100型的数组,p同样指向了 matrix数组的第一个元素(第一个 int100型的数组),通过对 p的算术运算也可以使它指向 matrix数组中的任意一个元素而不需要知道 matrix是一个多长的数组, 但一定需要知道 matrix中每个数组元素的长度, 所以就有了 int (*p) 100这种形式的声明。由此可知,如果进行 p + n (n为整数)这样的运算,每次的步长就是 n * 100 * sizof (int),相当于跳过了矩阵中的 n行,因为每

43、行都有 100个元素并且元素为整型,所以跳过了 n * 100 * sizof (int)个字节,指向这些字节之后的位置。现在,对指向数组指针的声明方式的疑惑我认为已经讲清楚了。下面来看一个关于数组长度的问题。 在 C语言中没有一种内建的机制去检查一个数组的边界范围,完全是由程序员自己去控制,这是 C语言设计的一种哲学或者说一种理念:给程序员最大的自由度,程序员应该知道自己在做什么。凡事有利有弊,自由度大了,出错的几率就高了。很有朋友(包括我自己)在初用数组时应该会或多或少地遇到过数组越界的问题。在前面的论述中提到了通过对指针的算术运算可以使它指向数组中的任何一个元素包括超出数组范围的第一个元

44、素,这个超出范围的第一个元素实际上是不存在的,这个“元素”的地址在数组所占的内存之后,它是数组的第一个出界点,这个地址可以赋给指向数组元素的指针,但 ANSI C仅允许它进行赋值赋值或比较比较运算,不能对保存这个地址的指针进行解引用或下标运算。 再回首再回首数组名数组名 现在又要开始数组名的讨论了,之所以再回首而没有一气呵成,是因为在一维数组名和二维数组名之间需要一个过渡知识,就是指向数组的指针。在表达式中一维数组名会转换为指向数组第一个元素的指针,二维数组也是一样的,请大家牢记在 C语言中二维数组就是数组的数组,所以也会被转换为指向第一个元素的指针,它的第一个元素是一个数组,所以最终的结果就

45、是二维数组名被转换成指向数组的指针。 来看int (*p) 100 = matrix; 此时的matrix被转换为一个指向数组的指针, 对于matrixn,是 matrix数组的第 n+1个元素的名字,也就是 matrix数组中 50个有着 100个整型元素的数组之一,所以可以有 p = &matrixn; 即 p指向了一个数组元素,也就是矩阵中的某一行,matrixn本身是一个一维数组的数组名,它会被转换为指向数组第一个元素的指针,因此可以有 int *column_p = matrixn;这个表达式是最常见的也最容易理解。如果对 matrixn进行 sizeof 操作结果是100*size

46、of (int); 而 sizeof(matrix)结果是 50*100*sizeof (int)。 总结一下,p和 matrix都是指向矩阵的一行(一个整型数组),p + m 或者 matrix + m都将使指针跳跃 m 行(m 个整型数组),column_p 和 matrixn都指向某行(一个整型数组)的第一个元素,column_p + m和 matrixn + m都将使指针跳跃 m个整型元素。假若要访问二维数组 matrix中第 1行第 1列(注意数组下标从 0开始)的元素可以有以下的几种方式(i为 int型变量): 通过数组名引用 通过指针 p的引用 通过指针 column_p的引用

47、i = matrix 00; i = *(*(p +0)+0); column_p = matrix0; i = *(matrix 0+0); i = *(p0 + 0); i = *(column_p+0); i = *(*(matrix+0)+0); i = (*(p + 0)0; i = column_p0; 上面的各种表达式中的“+0”均可以省略掉,但如果数字不是 0就不能省略了,由此在引用第 1行第 1列的元素时会产生一些简化的表达式,如下: 通过数组名引用 通过指针 p的引用 通过指针 column_p的引用 i = matrix 00; i = *p; column_p = ma

48、trix0; i = *matrix 0; i = *p0; i = *column_p; i = *matrix; i = (*p )0; i = column_p0; 现在来看下面语句的输出,它们可能会让你感到困惑: printf(%p/n, &matrix); 对应的指针操作:无 printf(%p/n, matrix); 对应的指针操作:printf(%p/n, p); printf(%p/n, &matrix0); 对应的指针操作:printf(%p/n, p); printf(%p/n, matrix0); 对应的指针操作:printf(%p/n, column_p); print

49、f(%p/n, &matrix00); 对应的指针操作:printf(%p/n, column_p); 在我机器上的输出是: 0022B140 0022B140 0022B140 0022B140 0022B140 输出的值虽然一样,但这些参数的类型却不完全相同。下面一一做出解释: &matrix: 对二维数组名取地址,返回一个指向二维数组的指针; matrix: 二维数组名会被转换为指向第一行(第一个数组)的指针,与&matrix0等价; &matrix0: 对第一个一维数组的数组名取地址,返回一个指向一维数组的指针; matrix0: 二维数组中第一个一维数组的数组名,与&matrix00

50、是等价的; &matrix00:对第一行第一列元素取地址,返回一个指向整型元素的指针。 在 ANSI C标准中没有说明对一个数组名进行&操作是否合法,但现在的编译器大都认为是合法的,并且会返回一个指向数组的指针。简单地说就是:对一个 n维数组的数组名取地址得到的是一个指向 n维数组的指针。 另外,上例中相对应的指针表示方式我也写了出来。对于&matrix 没有相对应的指针表示方式,因为我们没有定义那种类型的指针, 用 p是表示不出来的, 如果对 p进行&的话, 得到的是 p这个指针的地址,而不是 matrix的地址,两者完全不同,值也不会相同的。 再次提醒大家:无论是再次提醒大家:无论是 ma

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 教育专区 > 高考资料

本站为文档C TO C交易模式,本站只提供存储空间、用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。本站仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知淘文阁网,我们立即给予删除!客服QQ:136780468 微信:18945177775 电话:18904686070

工信部备案号:黑ICP备15003705号© 2020-2023 www.taowenge.com 淘文阁