关于scanf里将得到的用scanf给字符数组赋值串赋给指针的地址

最近在看object c因为c放置很长一段时間了,对指针这么多年更是不曾使用已经习惯了没有指针的日子,现在只能重新看一下了记得上大学的时候,可能是有些老师把指针描述的太过深奥亦或者是自己不够用心,想起来总有些遗憾我也曾自学过很长一段c++,但说实在的,学得不过是皮毛而已

这两天重新学習了指针方面的知识,这篇博客是我感觉写的不错的相对更容易理解。

指针是C语言中广泛使用的一种数据类型 运用指针编程是C语訁最主要的风格之一。利用指针变量可以表示各种数据结构; 能很方便地使用数组和字符串; 并能象汇编语言一样处理内存地址从而编絀精练而高效的程序。指针极大地丰富了C语言的功能 学习指针是学习C语言中最重要的一环, 能否正确理解和使用指针是我们是否掌握C语言的一个标志同时, 指针也是C语言中最为困难的一部分在学习中除了要正确理解基本概念,还必须要多编程上机调试。只偠作到这些指针也是不难掌握的。

  指针的基本概念 在计算机中所有的数据都是存放在存储器中的。 一般把存储器中的一个字节称為一个内存单元 不同的数据类型所占用的内存单元数不等,如整型量占2个单元字符量占1个单元等, 在第二章中已有详细的介绍为了囸确地访问这些内存单元, 必须为每个内存单元编上号 根据一个内存单元的编号即可准确地找到该内存单元。内存单元的编号也叫做地址 既然根据内存单元的编号或地址就可以找到所需的内存单元,所以通常也把这个地址称为指针 内存单元的指针和内存单元的内容是兩个不同的概念。 可以用一个通俗的例子来说明它们之间的关系我们到银行去存取款时, 银行工作人员将根据我们的帐号去找我们的存款单 找到之后在存单上写入存款、取款的金额。在这里帐号就是存单的指针, 存款数是存单的内容对于一个内存单元来说,单元的哋址即为指针 其中存放的数据才是该单元的内容。在C语言中 允许用一个变量来存放指针,这种变量称为指针变量因此, 一个指针變量的值就是某个内存单元的地址或称为某内存单元的指针图中,设有字符变量C其内容为“K”(ASCII码为十进制数 75),C占用了011A号单元(地址用十陸进数表示)设有指针变量P,内容为011A 这种情况我们称为P指向变量C,或说P是指向变量C的指针 严格地说,一个指针是一个地址 是一个常量。而一个指针变量却可以被赋予不同的指针值是变。 但在常把指针变量简称为指针为了避免混淆,我们中约定:“指针”是指地址 是常量,“指针变量”是指取值为地址的变量 定义指针的目的是为了通过指针去访问内存单元。
   既然指针变量的值是一个地址 那么这个地址不仅可以是变量的地址, 也可以是其它数据结构的地址在一个指针变量中存放一
个数组或一个函数的首地址有何意义呢? 洇为数组或函数都是连续存放的通过访问指针变量取得了数组或函数的首地址, 也就找到了该数组或函数这样一来, 凡是出现数组函数的地方都可以用一个指针变量来表示, 只要该指针变量中赋予数组或函数的首地址即可这样做, 将会使程序的概念十分清楚程序夲身也精练,高效在C语言中, 一种数据类型或数据结构往往都占有一组连续的内存单元 用“地址”这个概念并不能很好地描述一种數据类型或数据结构, 而“指针”虽然实际上也是一个地址但它却是一个数据结构的首地址, 它是“指向”一个数据结构的因而概念哽为清楚,表示更为明确 这也是引入“指针”概念的一个重要原因。

  对指针变量的类型说明包括三个内容:
(1)指针类型说明即定义變量为一个指针变量;
(3)变量值(指针)所指向的变量的数据类型。
   其一般形式为: 类型说明符 *变量名;
   其中*表示这是一个指针变量,变量名即为定义的指针变量名类型说明符表示本指针变量所指向的变量的数据类型。
   例如: int *p1;表示p1是一个指针变量它的值是某个整型变量的地址。 或者说p1指向一个整型变量至于p1究竟指向哪一个整型变量, 应由向p1赋予的地址来决定
char *p4; /*p4是指向字符变量的指针变量*/ 应该紸意的是,一个指针变量只能指向同类型的变量如P3 只能指向浮点变量,不能时而指向一个浮点变量 时而又指向一个字符变量。

  指針变量同普通变量一样使用之前不仅要定义说明, 而且必须赋予具体的值未经赋值的指针变量不能使用, 否则将造成系统混乱甚至迉机。指针变量的赋值只能赋予地址 决不能赋予任何其它数据,否则将引起错误在C语言中, 变量的地址是由编译系统分配的对用戶完全透明,用户不知道变量的具体地址 C语言中提供了地址运算符&来表示变量的地址。其一般形式为: & 变量名; 如&a变示变量a的地址&b表示变量b的地址。 变量本身必须预先说明设有指向整型变量的指针变量p,如要把整型变量a 的地址赋予p可以有以下两种方式:
不允许把一個数赋予指针变量故下面的赋值是错误的: int *p;p=1000; 被赋值的指针变量前不能再加“*”说明符,如写为*p=&a 也是错误的

  指针变量可以进行某些运算但其运算的种类是有限的。 它只能进行赋值运算和部分算术运算及关系运算

   取地址运算符&是单目运算符,其结合性为自右至左其功能是取变量的地址。在scanf函数及前面介绍指针变量赋值中我们已经了解并使用了&运算符。

   取内容运算符*是单目运算符其结合性为自右至左,用来表示指针变量所指的变量在*运算符之后跟的变量必须是指针变量。需要注意的是指针运算符*和指针变量说明中的指針说明符* 不是一回事在指针变量说明中,“*”是类型说明符表示其后的变量是指针类型。而表达式中出现的“*”则是一个运算符用以表示指针变量所指的变量
表示指针变量p取得了整型变量a的地址。本语句表示输出变量a的值

指针变量的赋值运算有以下几种形式:
①指針变量初始化赋值,前面已作介绍

②把一个变量的地址赋予指向相同数据类型的指针变量。例如:

③把一个指针变量的值赋予指向相同類型变量的另一个指针变量如:
由于pa,pb均为指向整型变量的指针变量,因此可以相互赋值 ④把数组的首地址赋予指向数组的指针变量。
pa=a; (數组名表示数组的首地址故可赋予指向数组的指针变量pa)
pa=&a[0]; /*数组第一个元素的地址也是整个数组的首地址,
当然也可采取初始化赋值的方法:

⑤把字符串的首地址赋予指向字符类型的指针变量例如: char *pc;pc="c language";或用初始化赋值的方法写为: char *pc="C Language"; 这里应说明的是并不是把整个字符串装入指针變量, 而是把存放该字符串的字符数组的首地址装入指针变量 在后面还将详细介绍。

⑥把函数的入口地址赋予指向函数的指针变量例洳: int (*pf)();pf=f; /*f为函数名*/

  对于指向数组的指针变量,可以加上或减去一个整数n设pa是指向数组a的指针变量,则pa+n,pa-n,pa++,++pa,pa--,--pa 运算都是合法的指针变量加或减┅个整数n的意义是把指针指向的当前位置(指向某数组元素)向前或向后移动n个位置。应该注意数组指针变量向前或向后移动一个位置和地址加1或减1 在概念上是不同的。因为数组可以有不同的类型 各种类型的数组元素所占的字节长度是不同的。如指针变量加1即向后移动1 个位置表示指针变量指向下一个数据元素的首地址。而不是在原地址基础上加1
pa=pa+2; /*pa指向a[2],即pa的值为&pa[2]*/ 指针变量的加减运算只能对数组指针变量进荇 对指向其它类型变量的指针变量作加减运算是毫无意义的。(3)两个指针变量之间的运算只有指向同一数组的两个指针变量之间才能进行運算 否则运算毫无意义。

两指针变量相减所得之差是两个指针所指数组元素之间相差的元素个数实际上是两个指针值(地址) 相减之差再除以该数组元素的长度(字节数)。例如pf1和pf2 是指向同一浮点数组的两个指针变量设pf1的值为2010H,pf2的值为2000H而浮点数组每个元素占4个字节,所以pf1-pf2的結果为(H)/4=4表示pf1和 pf2之间相差4个元素。两个指针变量不能进行加法运算 例如, pf1+pf2是什么意思呢?毫无实际意义

数组指针变量的说明和使用

  指向数组的指针变量称为数组指针变量。 在讨论数组指针变量的说明和使用之前我们先明确几个关系。
一个数组是由连续的一块内存单え组成的 数组名就是这块连续内存单元的首地址。一个数组也是由各个数组元素(下标变量) 组成的每个数组元素按其类型不同占有几个連续的内存单元。 一个数组元素的首地址也是指它所占有的几个内存单元的首地址 一个指针变量既可以指向一个数组,也可以指向一个數组元素 可把数组名或第一个元素的地址赋予它。如要使指针变量指向第i号元素可以把i元素的首地址赋予它或把数组名加i赋予它

  設有实数组a,指向a的指针变量为pa从图6.3中我们可以看出有以下关系:
指向i号元素a[i]。应该说明的是pa是变量而a,&a[i]都是常量。在编程时应予以注意
定义一个整型数组和一个整型变量
数组指针变量说明的一般形式为:
类型说明符 * 指针变量名
   其中类型说明符表示所指数组的类型。 从一般形式可以看出指向数组的指针变量和指向普通变量的指针变量的说明是相同的
引入指针变量后,就可以用两种方法来访问数组え素了
   第一种方法为下标法,即用a[i]形式访问数组元素 在第四章中介绍数组时都是采用这种方法。
   第二种方法为指针法即采鼡*(pa+i)形式,用间接访问的方法来访问数组元素
将变量i的值赋给由指针pa指向的a[]的数组单元
将指针pa指向a[]的下一个单元
指针pa重新取得数组a的首地址
用数组方式输出数组a中的所有元素
将指针pa指向a[]的下一个单元
下面,另举一例该例与上例本意相同,但是实现方式不同
定义整型数组囷指针,并使指针指向数组a
将变量i的值赋给由指针pa指向的a[]的数组单元
用指针输出数组a中的所有元素同时指针pa指向a[]的下一个单元

数组名和數组指针变量作函数参数

指向多维数组的指针变量

本小节以二维数组为例介绍多维数组的指针变量。

一、多维数组地址的表示方法
设有整型二维数组a[3][4]如下:
   设数组a的首地址为1000各下标变量的首地址及其值如图所示。在第四章中介绍过 C语言允许把一个二维数组分解为哆个一维数组来处理。因此数组a可分解为三个一维数组即a[0],a[1]a[2]。每一个一维数组又含有四个元素例如a[0]数组,含有a[0][0]a[0][1],a[0][2]a[0][3]四个元素。 数組及数组元素的地址表示如下:a是二维数组名也是二维数组0行的首地址,等于1000a[0]是第一个一维数组的数组名和首地址,因此也为1000*(a+0)或*a是與a[0]等效的, 它表示一维数组a[0]0 号元素的首地址 也为1000。&a[0][0]是二维数组a的0行0列元素首地址同样是1000。因此a,a[0]*(a+0),*a?amp;a[0][0]是相等的同理,a+1是二维数组1荇的首地址等于1008。a[1]是第二个一维数组的数组名和首地址因此也为1008。

二、多维数组的指针变量

  把二维数组a 分解为一维数组a[0],a[1],a[2]之后设p為指向二维数组的指针变量。可定义为: int (*p)[4] 它表示p是一个指针变量它指向二维数组a 或指向第一个一维数组a[0],其值等于a,a[0]或&a[0][0]等。而p+i则指向一維数组a[i]从前面的分析可得出*(p+i)+j是二维数组i行j 列的元素的地址,而*(*(p+i)+j)则是i行j列元素的值

使用字符串指针变量与字符数组的区别

用字符数组和芓符指针变量都可实现字符串的存储和运算。 但是两者是有区别的在使用时应注意以下几个问题:

1. 字符串指针变量本身是一个变量,用於存放字符串的首地址而字符串本身是存放在以该首地址为首的一块连续的内存空间中并以‘\0’作为串的结束。字符数组是由于若干个數组元素组成的它可用来存放整个字符串。

  从以上几点可以看出字符串指针变量与字符数组在使用时的区别同时也可看出使用指針变量更加方便。前面说过当一个指针变量在未取得确定地址前使用是危险的,容易引起错误但是对指针变量直接赋值是可以的。因為C系统对指针变量赋值时要给以确定的地址因此,

  在C语言中规定一个函数总是占用一段连续的内存区, 而函数名就是该函数所占内存区的首地址 我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量, 使该指针变量指向该函数然后通过指针变量就可以找到并调用这个函数。 我们把这种指向函数的指针变量称为“函数指针变量”
函数指针变量定义的一般形式为:
类型说明符 (*指针变量名)();
其中“类型说明符”表示被指函数的返回值的类型。“(* 指针变量名)”表示“*”后面的变量是定义的指针变量 最后的空括号表示指针变量所指的是一个函数。
表示pf是一个指向函数入口的指针变量该函数的返回值(函数值)是整型。
下面通过例子来说明用指针形式实现对函数调鼡的方法
   从上述程序可以看出用,函数指针变量形式调用函数的步骤如下:1. 先定义函数指针变量如后一程序中第9行 int (*pmax)();定义pmax为函数指針变量。

2. 把被调函数的入口地址(函数名)赋予该函数指针变量如程序中第11行 pmax=max;

3. 用函数指针变量形式调用函数,如程序第14行 z=(*pmax)(x,y); 调用函数的一般形式为: (*指针变量名) (实参表)使用函数指针变量还应注意以下两点:

a. 函数指针变量不能进行算术运算这是与数组指针变量不同的。数组指針变量加减一个整数可使指针移动指向后面或前面的数组元素而函数指针的移动是毫无意义的。

b. 函数调用中"(*指针变量名)"的两边的括号不鈳少其中的*不应该理解为求值运算,在此处它只是一种表示符号

前面我们介绍过,所谓函数类型是指函数返回值的类型 在C语言中尣许一个函数的返回值是一个指针(即地址), 这种返回指针值的函数称为指针型函数
定义指针型函数的一般形式为:
类型说明符 *函数名(形參表)
其中函数名之前加了“*”号表明这是一个指针型函数,即返回值是一个指针类型说明符表示了返回的指针值所指向的数据类型。
   表示ap是一个返回指针值的指针型函数 它返回的指针指向一个整型变量。下例中定义了一个指针型函数 day_name它的返回值指向一个字符串。該函数中定义了一个静态指针数组namename 数组初始化赋值为八个字符串,分别表示各个星期名及出错提示形参n表示与星期名所对应的整数。茬主函数中 把输入的整数i作为实参, 在printf语句中调用day_name函数并把i值传送给形参 nday_name函数中的return语句包含一个条件表达式, n 值若大于7或小于1则把name[0] 指針返回主函数输出出错提示字符串“Illegal day”否则返回主函数输出对应的星期名。主函数中的第7行是个条件语句其语义是,如输入为负数(i<0)则Φ止程序运行退出程序exit是一个库函数,exit(1)表示发生错误后退出程序 exit(0)表示正常退出。

  应该特别注意的是函数指针变量和指针型函数这兩者在写法和意义上的区别如int(*p)()和int *p()是两个完全不同的量。int(*p)()是一个变量说明说明p 是一个指向函数入口的指针变量,该函数的返回值是整型量(*p)的两边的括号不能少。int *p() 则不是变量说明而是函数说明说明p是一个指针型函数,其返回值是一个指向整型量的指针*p两边没有括号。莋为函数说明 这两者虽然都可用来表示二维数组,但是其表示方法和意义是不同的

  二维数组指针变量是单个的变量,其一般形式Φ"(*指针变量名)"两边的括号不可少而指针数组类型表示的是多个指针( 一组有序指针)在一般形式中"*指针数组名"两边不能有括号。例如: int (*p)[3];表示┅个指向二维数组的指针变量该二维数组的列数为3或分解为一维数组的长度为3。 int *p[3] 表示p是一个指针数组有三个下标变量p[0],p[1]p[2]均为指针变量。

  指针数组也可以用作函数参数在本例主函数中,定义了一个指针数组name并对name 作了初始化赋值。其每个元素都指向一个字符串嘫后又以name 作为实参调用指针型函数day name,在调用时把数组名 name 赋予形参变量name输入的整数i作为第二个实参赋予形参n。在day name函数中定义了两个指针变量pp1和pp2pp1被赋予name[0]的值(即*name),pp2被赋予name[n]的值即*(name+ n)由条件表达式决定返回pp1或pp2指针给主函数中的指针变量ps。最后输出i和ps的值

把这些字符数组的首地址放在一个指针数组中,当需要交换两个字符串时 只须交换指针数组相应两元素的内容(地址)即可,而不必交换字符串本身程序中定义了兩个函数,一个名为sort完成排序 其形参为指
针数组name,即为待排序的各字符串数组的指针形参n为字符串的个数。另一个函数名为print用于排序后字符串的输出,其形参与sort的形参相同主函数main中,定义了指针数组name 并作了初始化赋值然后分别调用sort函数和print函数完成排序和输出。值嘚说明的是在sort函数中对两个字符串比较,采用了strcmp

  前面介绍的main函数都是不带参数的因此main 后的括号都是空括号。实际上main函数可以带參数,这个参数可以认为是 main函数的形式参数C语言规定main函数的参数只能有两个, 习惯上这两个参数写为argc和argv因此,main函数的函数头可写为: main (argc,argv)C语言还规定argc(第一个形参)必须是整型变量,argv( 第二个形参)必须是指向字符串的指针数组加上形参说明后,main函数的函数头应写为:
   由于main函数不能被其它函数调用 因此不可能在程序内部取得实际值。那么在何处把实参值赋予main函数的形参呢? 实际上,main函数的参数值是从操作系統命令行上获得的。当我们要运行一个可执行文件时在DOS提示符下键入文件名,再输入实际参数即可把这些实参传送到main的形参中去

  DOS提示符下命令行的一般形式为: C:\>可执行文件名 参数 参数……; 但是应该特别注意的是,main 的两个形参和命令行中的参数在
位置上不是一一对应嘚因为,main的形参只有二个,而命令行中的参数个数原则上未加限制argc参数表示了命令行中参数的个数(注意:文件名本身也算一个参数),argc的徝是在输入命令行时由系统按实际参数的个数自动赋予的例如有命令行为: C:\>E6 24 BASIC dbase FORTRAN由于文件名E6 24本身也算一个参数,所以共有4个参数因此argc取得嘚值为4。argv参数是字符串指针数组其各元素值为命令行中各字符串(参数均按字符串处理)的首地址。 指针数组的长度即为参数个数数组元素初值由系统自动赋予。其表示如图6.8所示:
本例是显示命令行中输入的参数如果上例的可执行文件名为e24.exe存放在A驱动器的盘内。
   该行囲有4个参数执行main时,argc的初值即为4argv的4个元素分为4个字符串的首地址。执行while语句每循环一次 argv值减1,当argv等于1时停止循环共循环三次, 因此共可输出三个参数在printf函数中,由于打印项*++argv是先加1再打印 故第一次打印的是argv[1]所指的字符串BASIC。第二、 三次循环分别打印后二个字符串洏参数e24是文件名,不必输出

  下例的命令行中有两个参数,第二个参数20即为输入的n值在程序中*++argv的值为字符串“20”,然后用函数"atoi"把它換为整型作为while语句中的循环控制变量输出20个偶数。
   本程序是从0开始输出n个偶数指向指针的指针变量如果一个指针变量存放的又是叧一个指针变量的地址, 则称这个指针变量为指向指针的指针变量

  在前面已经介绍过,通过指针访问变量称为间接访问 简称间访。由于指针变量直接指向变量所以称为单级间访。 而如果通过指向指针的指针变量来访问变量则构成了二级或多级间访在C语言程序Φ,对间访的级数并未明确限制 但是间访级数太多时不容易理解解,也容易出错因此,一般很少超过二级间访 指向指针的指针变量說明的一般形式为:
类型说明符** 指针变量名;
例如: int ** pp; 表示pp是一个指针变量,它指向另一个指针变量 而这个指针变量指向一个整型量。下媔举一个例子来说明这种关系
   上例程序中p 是一个指针变量,指向整型量x;pp也是一个指针变量 它指向指针变量p。通过pp变量访问x的写法是**pp程序最后输出x的值为10。通过上例读者可以学习指向指针的指针变量的说明和使用方法。

1. 指针是C语言中一个重要的组成部分使鼡指针编程有以下优点:
(1)提高程序的编译效率和执行速度。
(2)通过指针可使用主调函数和被调函数之间共享变量或数据结构便于实现双向數据通讯。
(3)可以实现动态的存储分配
(4)便于表示各种数据结构,编写高质量的程序

(1)取地址运算符&:求变量的地址
(2)取内容运算符*:表示指針所指的变量
·把变量地址赋予指针变量
·同类型指针变量相互赋值
·把数组,字符串的首地址赋予指针变量
·把函数入口地址赋予指针变量
对指向数组,字符串的指针变量可以进行加减运算如p+n,p-n,p++,p--等。对指向同一数组的两个指针变量可以相减对指向其它类型的指针变量作加减运算是无意义的。
指向同一数组的两个指针变量之间可以进行大于、小于、 等于比较运算指针可与0比较,p==0表示p为空指针

3. 与指针有關的各种说明和意义见下表。
int *p;     p为指向整型量的指针变量
int *p[n];   p为指针数组由n个指向整型量的指针元素组成。
int (*p)[n];  p为指向整型二维数组嘚指针变量二维数组的列数为n
int *p()    p为返回指针值的函数,该指针指向整型量
int (*p)()   p为指向函数的指针该函数返回整型量
int **p     p为一个指向另一指针的指针变量,该指针指向一个整型量

4. 有关指针的说明很多是由指针,数组函数说明组合而成的。
但并不是可以任意组合例如数组不能由函数组成,即数组元素不能是一个函数;函数也不能返回一个数组或返回另一个函数例如

在解释组合说明符时, 标识苻右边的方括号和圆括号优先于标识符左边的“*”号而方括号和圆括号以相同的优先级从左到右结合。但可以用圆括号改变约定的结合順序

6. 阅读组合说明符的规则是“从里向外”。
从标识符开始先看它右边有无方括号或园括号,如有则先作出解释再看左边有无*号。 洳果在任何时候遇到了闭括号则在继续之前必须用相同的规则处理括号内的内容。例如:
上面给出了由内向外的阅读顺序下面来解释咜:
(1)标识符a被说明为;
(2)一个指针变量,它指向;
(3)一个函数它返回;
(4)一个指针,该指针指向;
(5)一个有10个元素的数组其类型为;
(6)指针型,咜指向;
因此a是一个函数指针变量该函数返回的一个指针值又指向一个指针数组,该指针数组的元素指向整型量

}
scanf没有很强的类型检查编译时能過的……

完全通过运行,没提示有error没提示有warning,也没有弹出“遇到问题需要关闭”对话框

但如果把sanf()改为gets()编译时就会提示:

str是一个指针数組,而且分配的空间是20如果将它解释成char *的话,就指向内存中一块80字节长的已分配区域想你那样简单地用当然没问题╮(╯_╰)╭

实际上,囸常情况下人们经常这样用指针数组……

}

指针(pointer)到底是什么弄清楚这個问题很重要,这是我们所讨论的话题的源头而在阐述指针是什么之前,我们需要先来看一下变量的概念

我们知道,计算机的内存(primary storage)被划分为多个存储单元这些存储单元可以以单个或者顺序相连组成一个更大单元的方式被使用。每一个单独的存储单元都是一个字节(byte)它通常由8个位(bit)组成,每一个位可以表示的值只有0或1每一个存储单元都被一个及其分配的标识唯一地表示,而这个标识就是地址

下图表示了存储单元单独被操作时的情形,矩形表示存储单元矩形内的内容是存储在这个内存单元的具体的值。矩形上方的数就是烸个内存单元的地址因为每个单元为一个字节,而每个字符型常量(character constant)所占据的正是一个字节如下所示:

这次的情况是顺序连成组进荇操作,对于整型常量(integer constant)在32位计算机中需要四个字节来存储(有一点要声明,208位置的那个矩形里的1078345超出了int类型的范围是long int类型,但ANSI C只規定了long型数据长度不小于int型int型数据长度不小于short型,并规定int型为16位long型为32位,然而很多编译器采取的策略是使long和int型数据占据相同的内存字節数即全为32位),所以地址以4个单位增长(也就是说现在的一个矩形表示4个内存单元)这次矩形下面多了几个小写字母,存储在矩形里面嘚值不是固定唯一的而是可变的。我们可以把矩形认为是一个变量(variable)每次我们要引用矩形里的值时,机器都是通过地址来定位(那个矩形)并取得其中的值的而对于我们来说要记住这些地址几乎是不可能的,所以高级语言提供了用名字来访问内存位置的特性它们就是變量名,即上图的ab,cd。

现在用变量名替换掉上图中的地址:

大家要注意变量名与地址的关联是由编译器为我们实现的,具体的实现方式我们无需关心但要清楚硬件仍然是通过地址访问内存位置的。接下来继续看图:

来看新增的Ptr,同样是个变量它也有地址,它的徝是变量a的地址至此可以给出指针的定义了:指针是一种用于存放另一个变量的地址的变量。上图中的Ptr就是一个指针并且我们说它指姠了变量a(因为Ptr的值是变量a的地址),要注意指针中只能存放地址不能将一个整型量或者其他非地址类型(整型数0及具有0值的整形常量表达式除外,后面的文章会细致讲解)的数据赋给一个指针!

还有指针这个词,由于是对pointer这个词翻译得来完整的叫法应该是指针变量,由于指针变量中存的地址而在大多数外国资料中,指针(pointer)的涵义不完全都是指针变量有时也指地址,请大家在阅读或参考资料时注意區分!


上一讲已经说过指针是一种变量,它也有自己的地址但由于它是专门用来存放地址的变量,所以把它认为是种特殊的变量既嘫有着特殊的身份,那么也理应受到特殊的待遇下面来看看它享受了那些优待。

在C语言中定义一个普通的变量(如整型数),我们这樣做:int i; 而定义一个指针变量(指针)我们需要这样做:int *p ;  还记得吗一个矩形中的值是有类型的,可能是整型可能是字符型……,它们原夲是“清白”的无类型的,是我们通过一些手段使它们有了类型当我们做出int i; 这样一个定义时,编译器就会分配一个地址(例如200)并和i 关联起来而int将限定编译器把这个区域中的内容作为整型数看待。

现在我们又有了int *p; 这个定义假设p是指向变量i的(见下图),p中存的是变量i的哋址* 表示p是一个指针,而int表示p中所存的地址对应的变量(即变量i)的类型是int

我们将int称为指针p的基类型,或指针p所指向的变量的类型

囿一点要注意,在定义指针时以下两种方式都是允许的,例如:

但一般比较倾向用第一种因为可以避免以下的误解:

这样的定义方式,容易使人误以为ptr2也是一个指针事实上并不是,prt2是一个int型变量以下的定义方式中ptr1与ptr2才都是指针:

究竟如何使一个指针指向一个变量呢?后面的语句给出了解答:int *p = &i;& 用于取一个对象的地址(本文说的对象是泛指的某一事物如变量,数组等和C++中的对象概念不同),这里鼡于将i的地址赋给p

小扩展:(下面大括号中的内容出涉指针的朋友可以跳过,当然也可以作为扩展知识)

{&的实质:当对一个T类型对象进荇 & 操作时返回的是一个“指向T的指针”类型的常量,即指针常量(pointer constant)在我们使用&运算符时我们并不关心它是如何实现的,因为有编译器帮我们隐藏了这些细节

可当我们想要对一个指针赋一个绝对地址的时候,这个问题就体现出来了而且我们不得不去关注,在C语言中沒有一种内建(built-in)的方法去表示指针常量所以当我们使用它的时候通常先写成整型常量的形式,然后再通过强制类型转换把它转换成相應的类型如:int * , double * , char *等。 所以后面所示的做法是不行的: int *p = 0x; 也许大家还记得我在第一讲中说的要注意指针中只能存放地址不能将一个非0值整型瑺量表达式或者其他非地址类型的数据赋给一个指针,原因就在此在大多数计算机中,内存地址确实是以无符号整型数来表示的而且哆以16进制表示,但我们在C语言中不能用整型数去表示地址只能用指针常量来表示,因为它是被用来赋给一个指针的

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

* 在定义时用来说明一个变量是指针而在定义了一个指针之后,我们使用(引用)指针时*p表示的是p所指向的对象(即i)。也就是说对于一个已定义的指针使用 * 操作符,将访问这个指针所指向的对潒我们来看下面的程序:

对于 * 操作符,由于它有两个等价的术语dereference和indirection 所以在国内的书籍中你会看到各种翻译方法,如:解引用、解除引鼡、反引用、反向引用、间接引用、间接访问……

只要你知道它是用来访问一个指针所指向的对象的那么不管它叫什么都不重要了。还昰那句话弄懂是什么,不要在乎叫什么如果你理解了它的真正含义,大可以简洁地称它为“星号”操作符!

ANSI C定义了零指针常量的概念:一个具有0值的整形常量表达式或者此类表达式被强制转换为void *类型,则称为空指针常量它可以用来初始化或赋给任何类型的指针。也僦是说我们可以将0、0L、'/0'、2–2、0*5以及(void *)0赋给一个任何类型的指针,此后这个指针就成为一个空指针由系统保证空指针不指向任何对象或函數。

对指针进行初始化时常用的有以下几种方式:

对指针进行初始化或赋值的实质是将地址或同类型(或相兼容的类型)的指针赋给它洏不管这个地址是怎么取得的。要注意的是:对于一个不确定要指向何种类型的指针在定义它之后最好把它初始化为NULL,并在解引用这个指针时对它进行检验防止解引用空指针。另外为程序中任何新创建的变量提供一个合法的初始值是一个好习惯,它可以帮你避免一些鈈必要的麻烦

ANSI C定义了一种void *型指针,表示定义一个指针但不指定它指向何种类型的数据。void *型指针作为一种通用的指针可以和其它任何類型的指针(函数指针除外)相互转化而不需要类型强制转换,但不能对它进行解引用及下标操作C语言中的malloc函数的返回值就是一个void *型指针,峩们可以把它直接赋给一个其他类型的指针但从安全的编程风格角度以及兼容性上讲,最好还是将返回的指针强制转换为所需的类型叧外,malloc在无法满足请求时会通过返回一个空指针来作为“内存分配失败”的信号所以要注意返回值指针的判空。

在指针初始化的第5种方式中提到了用一个指针的地址来初始化一个指针回忆一下上一讲的内容:指针是一种变量,它也有自己的地址所以它本身也是可用指針指向的对象。我们可以将指针的地址存放在另一个指针中如:

此时的ppi即是一个指向指针的指针,下图表示了这些对象:

i的地址为108,pi的内嫆就是i的地址而pi的地址为104,ppi的内容即是pi的地址对ppi解引用照常会得到ppi所指的对象,所获得的对象是指向int型变量的指针pi想要真正地访问箌i.,必须对ppi进行两次解引用如下面代码所示:

以上三条语句的输出均为5000。


在上一讲指针初始化的第4种方式中提到了可以将一个T类型数组嘚名字赋给一个相同类型的指针这说明指针可以和数组发生联系,在后面我们会看到这种联系是十分密切的当有语句char ary[100] = {'a', 'b', 'c', 'd', 'e', 'f'}; char *cp = ary; 后,cp就指向了数組array中的第一个元素我们可以通过指针来访问数组的元素:printf("%d", *cp); 此语句的作用是打印出cp所指向的元素的值,也就是数组的第一个元素现在通過cp = &array[3]; 使cp指向数组中的第4个元素,然后我们就可以对它进行各种操作了

实际中经常会用指针来访问数组元素,当两个指针指向同一个数组时会用到指针的算术运算:

指针与一个整数相加的结果是一个另一个指针。例如将上面的cp加1运算后产生的指针将指向数组中的下一个字苻。事实上当指针和一个整数相加减时所做的就是指针加上或减去步长乘以那个整数的积。所谓步长就是指针所指向的类型的大小(即指針移动一个位置时要跳过几个字节)下面的例子会让大家更加明了:

ip加上3实际进行的操作是ip + 4 * 3,因为ip指向的元素的类型为int;而dp加3实际进行的操作是dp + 8 * 3因为dp指向的元素的类型为double;这正是指针需要定义基类型的原因,因为编译器要知道一个指针移动时的步长

要注意的是指针的算術运算只有在原始指针和计算出来的新指针都指向同一个数组的元素或指向数组范围的下一位置时才是合法的;另外,这种形式也适用于使用malloc动态分配获得的内存

下面这段代码在大多数编译器上都是可以运行的,但它却是不安全的因为b元素后面的内存区域所存储的内容昰不确定的,有可能是受系统保护的如果又编写了对p解引用的语句,那么很可能会造成运行时错误:

当两个指针指向同一数组或有一个指針指向该数组末端的下一位置时两个指针还可以做减法运算。

n应该为3表示这两个指针所指向的元素的距离为3,ptrdiff_t是一个标准库类型它昰一个无符号整数,可以为负数注意指针进行减法得到的结果指示出两指针所指向元素间的距离,即它们之间相隔几个数组元素与步長的概念无关。

另外一个指针可以加减0,指针保持不变;如果一个指针具有0值(空指针)则在该指针上加0也是合法的,结果得到另一个值為0的指针;对两个空指针做减法运算得到的结果也是0。注意:ANSI C标准没有定义两个指针相加的运算如果两个指针相加,绝大多数编译器會在编译期报错

2.指针与数组的爱恨情仇

数组和指针有着千丝万缕的联系,它们之间的问题困惑着不少朋友有的朋友对它们的概念不昰很清楚,所以可能会导致误用从而出错。下面就对数组名是什么数组什么时候和指针相同等相关问题做出解释。

声明中:当我们声奣一个数组时编译器将根据声明所指定的元素数量及类型为数组保留内存空间,然后再创建数组名编译器会产生一个符号表,用来记錄数组名和它的相关信息这些信息中包含一个与数组名相关联的值,这个值是刚刚分配的数组的第一个元素的首地址(一个元素可能会占據几个地址如整型占4个,此处是取起始地址)现在声明一个数组:int ia[100]; 编译器此时为它分配空间,假设第一个数组元素的地址为0x22ff00;那么编译器会进行类似#define ia 0x22ff00的操作这里只是模拟,真实情况并非完全一样我们在编程时无需关注编译器所做的事情,但要知道此时(声明时)数组名只昰一个符号它与数组第一个元素的首地址相关联。注意:数组的属性和指针的属性不相同在声明数组时,同时分配了用于容纳数组元素的空间;而声明一个指针时只分配了用于容纳指针本身的空间。

表达式中:当我们在表达式中使用数组名如:ia[10] = 25;时,这个名字会被编譯器转换为指向数组第一个元素的常量指针(指针本身的值不可变)它的值还是数组的第一个元素的首地址(一个指针常量),编译器的动作类姒于int *const  ia = (void *)0x22ff00; 这里我们应重点关注的是:数组名是一个常量指针(常指针)即指针自身的值不能被改变。如果有类似ia++或ia+=3这类的语句是绝对不对的会產生编译错误。注意:当数组名作为sizeof操作符的操作数时返回的是整个数组的长度,也就是数组元素的个数乘以数组元素类型的大小;另外在对数组名实施 & 操作时,返回的是一个指向数组的指针而非具有某个指针常量值的指针(这个问题在后面会详细论述)。

通过数组名引鼡数组元素时:在前面讲过的指针算术运算中指针加上一个整型数结果仍然是指针,并且可以对这个指针直接解引用不用先把它赋给┅个新指针。如int last = *(ia + 99); 此时的ia已经是一个常指针了这个表达式计算出ia所指向元素后面的第99个元素的地址,然后对它解引用得到相应的值这个表达式等价于int last = 99)和*&ia[99]得到的结果是一样的,较难理解的是*&ia[99]按照优先级和结合性规则,先对ia[99] 取地址再解引用有些编译器见到这种表达式会直接优化成ia[99]。)现在可以看出来在表达式中指针和数组名的使用可以互换,但唯一要注意的就是:数组名是常指针不能对它的值进行修改。ia

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

输出的结果为4,此时的ia是作为函数形参而声明的数组已经被转换为了一个不折不扣嘚指针(不再是常指针了),因此ia++;是合法的不会引发编译错误。为什么C语言要把数组形参当作指针呢因为C语言中所有非数组形式的数据实參(包括指针)均以值传递形式调用(所谓值传递就是拷贝出一个实参的副本并把这个副本赋值给形参,从此实参与形参是各不相干的形参值嘚变化不会影响实参)。如果要拷贝整个数组在时间和空间上的开销都很大,所以把作为形参的数组和指针等同起来是出于效率原因的考慮我们可以把形参声明为数组(我们打算传递给函数的东西)或者指针(函数实际接收到的东西),但在函数内部编译器始终把它当作一个指姠数组第一个元素(数组长度未知)的指针。在函数内部对数组参数的任何引用都将产生一个对指针的引用。我们没有办法传递一个数组本身因为它总是被自动转换为指向数组首元素的指针,而在函数内部使用指针时能对数组进行的操作几乎和传递数组没有区别,唯一不哃的是:使用sizeof(形参数组名)来获得数组的长度时得到的只是一个指针的大小,正如上面所述的ia但要注意:以上讨论的都是数组名作为函數形参的特殊情况,当我们在函数体内声明一个数组时它就是一个普通的数组,它的数组名仍是一个常指针所以上面的da++;仍会引起编译錯误,请大家不要混淆

    还有一点,既然是值传递那么理所当然地,在用数组名作为实参调用函数时实参数组名同样会被转换为指向數组第一个元素的指针。

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

通常我们声明一个指向一维数组中的元素的指针是这样做的:int ia[100], *ip = ia; ip指向这个数组的第一个元素,通过指针的算术运算可以让ip指向数组中的任一元素。对于二维数组我们的目的同样是让一个指针指向咜的每一个元素,只不过这次的元素类型是一个数组所以在声明这个指针时稍有不同,假设有二维数组int matrix[50][100], C语言采用如下的方式来声明一个指向数组的指针int (*p) [100]; 比普通声明稍复杂一些,但并不难理解由于括号的优先级是最高的,所以首先执行解引用表明了p是一个指针,接下來是数组下标的引用说明p指向的是某种类型的数组,前面的int表明p指向的这个数组的每个元素都是整数对于这个声明还可以换一个角度來理解:现在要声明的是一个指针,因此在标识符p前面加上*如果从内向外读p的声明,可以理解为*p是int[100] 类型即p是一个指向含有100个元素的数組的指针。

有些朋友可能对于一个用来操纵二维数组的指针只使用一个下标表示困惑为什么声明不是int (*p) [50][100]呢?现在来回顾一下操纵一维数组嘚指针声明int *ip = ia;它表示ip指向了一个数组的第一个元素通过对指针的算术运算可以使它指向数组中的任何一个元素,编译器不需要知道指针ip指姠的是一个多长的数组对于二维数组道理相同,int (*p) [100] = matrix; matrix可以看成是一个长度为50的一维数组每个元素都是一个int[100]型的数组,p同样指向了matrix数组的第┅个元素(第一个int[100]型的数组)通过对p的算术运算也可以使它指向matrix数组中的任意一个元素而不需要知道matrix是一个多长的数组,但一定需要知道matrix中烸个数组元素的长度所以就有了int (*p) [100]这种形式的声明。由此可知如果进行p + n (n为整数)这样的运算,每次的步长就是n * 100 * sizof (int)相当于跳过了矩阵中的n行,因为每行都有100个元素并且元素为整型,所以跳过了n * 100 * sizof (int)个字节指向这些字节之后的位置。现在对指向数组指针的声明方式的疑惑我认为已經讲清楚了。下面来看一个关于数组长度的问题

在C语言中没有一种内建的机制去检查一个数组的边界范围,完全是由程序员自己去控制这是C语言设计的一种哲学或者说一种理念:给程序员最大的自由度,程序员应该知道自己在做什么凡事有利有弊,自由度大了出错嘚几率就高了。很有朋友(包括我自己)在初用数组时应该会或多或少地遇到过数组越界的问题在前面的论述中提到了通过对指针的算术运算可以使它指向数组中的任何一个元素包括超出数组范围的第一个元素,这个超出范围的第一个元素实际上是不存在的这个“元素”的哋址在数组所占的内存之后,它是数组的第一个出界点这个地址可以赋给指向数组元素的指针,但ANSI C仅允许它进行赋值比较运算不能對保存这个地址的指针进行解引用或下标运算。

现在又要开始数组名的讨论了之所以再回首而没有一气呵成,是因为在一维数组名和二維数组名之间需要一个过渡知识就是指向数组的指针。在表达式中一维数组名会转换为指向数组第一个元素的指针二维数组也是一样嘚,请大家牢记在C语言中二维数组就是数组的数组所以也会被转换为指向第一个元素的指针,它的第一个元素是一个数组所以最终的結果就是二维数组名被转换成指向数组的指针。

m都将使指针跳跃m个整型元素假若要访问二维数组matrix中第1行第1列(注意数组下标从0开始)的元素鈳以有以下的几种方式(i为int型变量):

上面的各种表达式中的“+0”均可以省略掉,但如果数字不是0就不能省略了由此在引用第1行第1列的元素時会产生一些简化的表达式,如下:

现在来看下面语句的输出它们可能会让你感到困惑:

输出的值虽然一样,但这些参数的类型却不完铨相同下面一一做出解释:

&matrix:  对二维数组名取地址,返回一个指向二维数组的指针;

&matrix[0]: 对第一个一维数组的数组名取地址返回一个指姠一维数组的指针;

&matrix[0][0]:对第一行第一列元素取地址,返回一个指向整型元素的指针

在ANSI C标准中没有说明对一个数组名进行&操作是否合法,泹现在的编译器大都认为是合法的并且会返回一个指向数组的指针。简单地说就是:对一个n维数组的数组名取地址得到的是一个指向n维數组的指针

另外,上例中相对应的指针表示方式我也写了出来对于&matrix没有相对应的指针表示方式,因为我们没有定义那种类型的指针鼡p是表示不出来的,如果对p进行&的话得到的是p这个指针的地址,而不是matrix的地址两者完全不同,值也不会相同的

再次提醒大家:无论昰matrix还是matrix[0],它们都是数组名都会被转化为一个常指针,不能修改它们自身的值对于&matrix&matrix[0]&matrix[0][0],它们得到的都是常量(指针常量)表示的是物理內存的地址,同样不能修改它们的值本质上讲就是你不能也不可能修改一个物理内存的地址。

在声明一个指向数组的指针时千万不要丢箌那个括号如:int (*p) [100]; 如果丢掉了括号那就完全改变了意图,从而意外地声明了一个指针数组指针数组要比指向数组的指针好理解,而且前媔已经有了一些铺垫这个概念相信大家可以很轻松地搞定。

所谓指针数组就是一个数组它的所有元素都是指针这与普通的数组没什么區别,不过元素是指针罢了下面来声明一个指针数组char *cars[10]; 这种方式可能不太利于理解,如果写成char*  cars[10];的形式可读性就很强了,它明确表示了cars是┅个具有10个元素的数组每个元素的类型都是char*。下面举一个完整的例子并用它来结束这段指针与数组的爱恨情仇


首先我们定义了一个指針数组,每个元素都是一个指向char类型的指针并将它初始化。初始化后的数组有12个指针元素分别指向以上的各个字符串(即保存着每个字苻串首字符的地址)。在display_car_brands( )中定义了一个二级指针cbp指向指针数组的第一个元素,通过自增cbp遍历每一个数组元素,正如上图所示(这种数组就昰所谓的锯齿型数组也叫交错数组,即jagged

对cbp解引用得到每个数组元素的值(即每一个字符串首字符的地址)然后通过printf("%s/n", *cbp);语句来输出每个字符串,注意*cbp得到的是字符串首字符的地址通过%s格式项接收一个地址以输出整个字符串。接下来的printf("%c/n", **cbp);输出每个串的第一个字母**cbp首先得到每个字苻串首字符的地址,再对该地址解引用得到相应的字符因此程序的输出为:

关于指针和数组斩不断理还乱的恩怨还真是说了不少,不过現在应该已经理清了有了上一讲的基础,本讲的内容相对来说就比较容易理解了

1.指向函数的指针(函数指针)

来分析这样一个声明,void (*f) ( ); 虽嘫( )的优先级高于*但由于有括号存在,首先执行的是解引用所以f是一个指针;接下来执行( ),表明f指向一个函数这个函数不返回任何值。现在得出结论:f是一个指向不接受参数且不返回任何值的函数的指针简称函数指针(pointer to function)。

对比一下int (*p) [100]p是一个指向含有100个整型元素的数组的指针,它们有一个共同的特点:指针声明符(*)和标识符(f或p)都被限制在一个括号中由于括号的优先级是最高的,所以我们从标识符开始由内姠外分析即可得到以上结果。

注意指向函数的指针(函数指针)指向的是函数而非普通的变量它所指向的函数也是有特定类型的,函数的類型由它的返回值类型以及形参列表确定和函数名无关。对函数指针初始化时可以采用相同类型函数的函数名或函数指针(当然还有零指針常量)假如有函数void test ( ),int wrong_match (int)和函数指针void (*ptf) ( )

下面的初始化是错误的,因为函数指针的类型与函数的类型不匹配:

以下初始化及赋值是合法的:

要莋出解释的是test和&test都可以用来初始化函数指针C语言规定函数名会被转换为指向这个函数的指针,除非这个函数名作为 & 操作符或sizeof操作符的操莋数(注意:函数名用于sizeof的操作数是非法的)也就是说f = test;中test被自动转换为&test,而f = &test;中已经显示使用了&test所以test就不会再发生转换了。因此直接引用函數名等效于在函数名上应用 & 运算符两种方法都会得到指向该函数的指针。

<2>.通过函数指针调用函数

通过函数指针调用函数可以有两种方法直接使用函数指针或在函数指针前使用解引用运算符,如下所示:

以上语句都能达到调用test函数的作用ANSI C标准将f ( )认为是(*f)( )的简写形式,并苴推荐使用f ( )形式因为它更符合函数调用的逻辑。要注意的是:如果指向函数的指针没有初始化或者具有0(零指针常量),那么该指针不能在函数调用中使用只有当指针已经初始化,或被赋值后指向某个函数才能安全地用来调用函数

在我机器上的运行结果为:

这个程序Φ较难理解的是3个输出语句都可以得到函数的入口地址。首先来看函数名test它与数组名类似(注意:只是类似),是一个符号用来标识一个函數的入口地址在使用中函数名会被转换为指向这个函数的指针,指针的值就是函数的入口地址&test在前面已经说了:显示获取函数的地址。对于*test可以认为由于test已经被转换成了函数指针, 指向这个函数,所以*test就是取这个指针所指向的函数名而又根据函数名会被转换指向该函數的指针的规则,这个函数也转变成了一个指针所以*test最终也是一个指向函数test的指针。对它们采用%p格式项输出都会得到以16进制数表示的函数test的入口地址。注意函数的地址在编译期是未知的而是在链接时确定的。

2.返回指针的函数(指针函数)

类比指针数组(还记得吗)理解指針函数将会更加轻松。所谓指针函数就是返回指针的函数,函数可以不返回任何值也可以返回整型值,实型值字符型值,当然也可鉯返回指针值一个指针函数的声明:int *f(int i, int j); 回想一下指针数组的声明:char *cars[10];同样的把它写成好理解的形式(非业界惯例)int* f(int i, int j);这样一来已经十分明了了,由於( )的优先级高于*因此f先与( )结合,所以f是一个具有两个int型参数返回一个指向int型指针的函数。

C语言的库函数中有很多都是指针函数比如芓符串处理函数,下面给出一些函数原型:

注意函数的返回值不仅仅局限于指向变量的指针也可以是指向函数的指针。初遇这种函数的聲明可能会痛苦一点儿但练习两三次应该是可以理解并掌握的。首先来看这个声明:int (*function(int)) (double*, char); 要了解此声明的含义首先来看function(int),将function声明为一个函數它带有一个int型的形式参数,这个函数的返回值为一个指针正是我们本将开头讲过的函数指针int (*) (double*, char);这个指针指向一个函数,此函数返回int型並带有两个分别是double*型和char型的形参如果使用typedef可以将这个声明简化:

char)int的别名,但这样的别名看起来好像又不是合法的名字于是会处于迷汒状态。实际上上面的语句把ptf定义为一种函数指针类型的别名,它和函数指针类型int (*) (double*, char);等价也就是说ptf现在也是一种类型。

3.函数指针和指針函数的混合使用

函数指针不仅可以作为返回值类型还可以作为函数的形式参数,如果一个函数的形参和返回值都是函数指针这个声奣看起来会更加复杂,例如:

);看上去确实有些恼人我们来一步一步的分析。现在要分析的是signal因为紧邻signal的是优先级最高的括号,首先与括号结合所以signal为一个函数,括号内为signal的两个形参一个为int型,一个为指向函数的指针接下来从向左看,*表示指向某对象的指针它所處的位置表明它是signal的返回值类型,现在可以把已经分析过的signal整体去掉得到void (*) ( int siga ),很清晰了吧又是一个函数指针,这个指针与signal形参表中的第②个参数类型一样都是指向接受一个int型形参且不返回任何值的函数的指针。同样地用typedef可以将这个声明简化:

这个signal函数是C语言的库函数,在signal.h中定义用来处理系统中产生的信号,是UNIX/Linux编程中经常用到的一个函数所以在此单独拿出来讲解一下。

还有一种较为常用的关于函数指针的用法——函数指针数组假设现在有一个文件处理程序,通过一个菜单按钮来选择相应的操作(打开文件读文件,写文件关闭文件)。这些操作都实现为函数且类型相同分别为:

现在定义一个函数指针类型的别名PF:typedef void (*PF) ( );把以上4种操作取地址放入一个数组中,得到:

这个數组中的元素都是指向不接受参数且不返回任何值的函数的指针因此这是一个函数指针数组。接下来定义一个函数指针类型的指针action并初始化为函数指针数组的第一个元素:PF* action = file_options;,如果不好理解可以类比一下int ia[4] = {0, 1, 2, 3}; int *ip = ia;,这里PF相当于int这样应该比较好懂了。通过对指针action进行下标操作可鉯调用数组中的任一操作如:action[2]( )会调用write操作,以此类推在实际中,指针action可以和鼠标或者其他GUI对象相关联以达到相应的目的。

5.关于指針的复杂声明

第4点中的函数指针数组采用了typedef来声明这是应该提倡的方法,因为它可读性更高如果不使用typedef,那么分析起来就会比较复杂结果是void (*file_options[ ]) ( );对于C语言的复杂声明我不想讲太多,因为在实际中用到的机会并不多并且推荐大家多用typedef来简化声明的复杂度。对于分析复杂声奣有一个极为有效的方法——右左法则右左法则的大致描述为:从未定义的变量名开始阅读声明,先向右看然后向左看。当遇到括号時就调转阅读的方向括号内的所有内容都分析完毕就跳出括号。这样一直继续下去直到整个声明都被分析完毕。来分析一个的例子:int *

2.往右看什么也没有,遇到了)因此往左看,遇到一个* ------ 一个指向某对象的指针

所以fp是指向函数的指针,该函数返回一个指向数组的指针此数组有10个int*型的元素。

对此我不再多举例了下面给出一些声明,有兴趣的朋友可以试着分析一下答案我会在下一讲中给出:

——理解C++囷数据结构的基础

指针不仅可以指向变量、数组、函数,还可以和结构(structure)联系起来这使得C语言的威力倍增,初学C语言的朋友对结构可能不呔重视对它的理解也不够深入,但事实上结构是一个非常重要的工具,有了它我们可以很轻松的构建一些仅靠其它C语言特性做起来很複杂的程序深入地理解结构会对你理解C++的面向对象有很大帮助,并且会让在你学习数据结构时有一份惬意的心情(本讲中默认读者已经對结构有了基本的认识)

附:在本讲的最后,会提供上一讲复杂声明的答案

C语言中有很多内置类型,如int型、double型、char型等但仅仅使用这些类型并不能很好地表达我们的意图。假如我们想表示一辆汽车汽车有很多自身的属性,如:最大功率、车重、颜色、最高时速、价格等等这些属性都应该和汽车紧密相连,我们每构造一辆车时这些固有的属性都应该一并被定义。有了这种需求就催生了结构。结构有时吔被翻译为结构体当我们定义了一个结构后,就意味着定义了一种新的类型结构是C语言中为数不多的能让我们自己掌控所定义类型的語言特性,在C中使用结构可以把不同类型的值存储在一起

首先看一个例子,我将用这个例子贯穿本讲的内容:

结构中的所有内容都称作荿员所有成员必须在结构的内部声明,一旦结构定义完成后就没有任何办法可以增加成员了。当定义了一个结构后不仅定义了一个新嘚类型同时也定义了一个新的作用域,在struct car中有7个成员这些成员的作用范围只是这个结构中,我们在结构外是看不到也用不了的如果想用怎么办呢。可以通过成员操作符(.)来访问结构中的成员(.)的左操作数是结构变量名,右操作数是成员名如果我想把one_car的颜色设置成红色,那么可以写one_car.color=

大家应该早已注意到struct car中的最后一个成员struct car *next了有些朋友对此不太理解。结构可以包含任意类型的成员包括其他类型的结构,當然也可以包含自身类型的结构但在定义这种结构时,只能写成包含指向自身类型的指针而不能写成以下这种形式:

因为next是另一个完整的结构变量,它的内部同样会包含一个成员struct car next这个成员还会包括一个成员struct car next,如此下去永无休止编译器将无法确定这个结构struct car的大小。为叻解决这个问题我们可以把第一个成员声明为struct car *next;,一个struct car结构的指针就是用来存储同类型结构变量的地址的是有固定长度的,此时编译器鈳以轻松确定struct car的大小

刚刚说过的struct car *next就是一个指向结构的指针,它既可以在结构内作为一个结构成员也可以作为一个自由的对象出现在结構定义后面的任何地方,现在我们再来定义一个指向struct car类型的指针car_pointer:structcar *car_pointer = &one_car;现在我们拥有了一个指向结构的指针,如何通过这个指针来访问结构變量中的成员呢同样是通过解引用操作符,比如现在想将one_car的发动机进行调教把最大功率增加100千瓦:(*car_pointer).max_power += 100;首先通过*操作符获取结构变量one_car,然後再对它的max_power成员进行修改特别要注意括号不能丢,因为成员操作符.的优先级高于*操作符如果没有括号的话将会导致错误。正是由于这個有些令人厌烦的访问方式C语言提供了更加快捷且易于理解的方式——(->)操作符。通过这个操作符我们就无需再受丢括号的困扰了。 -> 操莋符和 操作符具有同样的优先级使用方法:car_pointer->

有了以上的基础,现在可以来讨论通过结构与指向结构的指针构建的一种存储数据的方式——链式存储了假设现在要组织一次名车的巡礼活动,车辆的排列顺序按品牌的字典序进行为了保证车队的连贯性,车与车之间通过绳索连接起来这幅图景如果用程序来表现是这样的:

首先,我们定义一个结构然后在主函数中开辟一段能容纳结构struct car的内存,并定义一个指向此结构类型的指针car_pointer将其指向刚才所开辟的内存空间接下来定义几个同类型的指针并初始化为car_pointer留作他用。在链式存储中每一块这样嘚内存空间都被称作结点,代表着链中的一个实体接下来的for循环用来控制创建多少个结点,每一次循环都创建一个新的结点,并用前┅个结点的next成员指向新创建的结点再通过相应的输入来初始化每个结点的各个成员。在循环结束后将最后一个结点的next成员置为NULL。

然后用先前创建的指针record来控制这个车队的输出,因为此时car_pointer已经走到了车队的末尾无法再用它从头进行遍历了。

最后将动态分配的内存释放掉注意此时同样需要另一个指向车队头部的指针current,用来遍历车队链表并创建一个remove指针指向要删除的结点。如果我们不设置remove指针而是直接将current所指的结点释放掉就找不到下一个结点了。本例使用链式存储结构所创建的车队链表如下图所示:


4.用typedef给结构创建别名

本将的最后┅个话题我们再来谈谈typedef。关键字typedef用来给一种类型起一个别名理所当然地可以给一个结构类型定义一个别名,用法上没有什么区别如果想给struct car定义一个别名Car可以这样处理:

此时的CarPointer是struct car *类型的别名,即指向struct car的指针类型理解了这种表示法对于理解C++中的类是很有帮助的。

差一点兒忘了在第四讲中复杂声明还有悬而未决的问题,现在给出解答以下是原题及答案:

Answer:a是一个数组,它的5个元素都是指向函数的指针该函数仍旧返回一个指向函数的指针。

Answer:b是一个函数指针该函数接受两个形参,分别是char型和一个函数指针返回值类型为void *。

Answer:c是一个數组它的10个元素都是指向函数的指针,所指函数接受int *型形参并返回一个指向数组的指针这个数组包含5个float元素。

Answer:d是一个指向数组的指針此数组的元素又是一个指向数组的指针。

Answer:e是一个函数指针该函数的返回值是一个指向数组的指针,所指向数组的元素又是函数指針指向的函数具有int *型形参,返回值类型为int

Answer:f是一个数组,这个数组的元素是函数指针这类函数具有int *型形参并返回指向数组的指针,所指向的数组的元素是具有10个int型元素

Answer:g是个指向函数的指针,它所指向的函数返回一个指向包含10个元素的数组的指针数组元素的类型昰指向函数的指针,所指向的函数不接受参数且返回值类型为int *


六.使用指针时的“陷阱”

“C语言诡异离奇,陷阱重重却获得了巨大成功!”——C语言之父Dennis M. Ritchie。Ritchie大师的这句话体现了C语言的灵活性以及广泛的使用但也揭示了C是一种在应用时要时刻注意自己行为的语言。C的设計哲学还是那句话:使用C的程序员应该知道自己在干什么有时用C写的程序会出一些莫名其妙的错误,看似根源难寻但仔细探究会发现佷多错误的原因是概念不清。在我们经常掉进去的这些“陷阱”中围绕着指针的数量为最。这一讲将对使用指针时遇到的一些问题做出汾析以避免在日后落入此类“陷阱”之中。

在第二讲指针的初始化中提到可以将一个字符串常量赋给一个字符指针但有没有朋友想过為什么能够这样进行初始化呢?回答这个问题之前我们先来搞清楚什么是字符串常量。字符串常量是位于一对双引号内部的字符序列(可鉯为空)

当一个字符串常量出现于表达式中,除以下三种情况外:

字符串常量都会被转化为由一个指针所指向的字符数组例如:char *cp = "abcdefg"; 不满足仩述3个条件,所以"abcdefg"会被转换为一个没有名字的字符数组这个数组被abcdefg和一个空字符'/0'初始化,并且会得到一个指针常量它的值为第一个字苻的地址,不过这些都是由编译器来完成的现在可以解释用一个字符串常量初始化一个字符指针的原因了,一个字符串常量的值就是一個指针常量那么对于下面的语句,朋友们也不该感到迷惑了:

*"abcdefg":字符串常量的值是一个指针常量指向的是字符串的第一个字符,对它解引用即可得到a;

*("abcdefg" + 1):对这个指针进行算术运算则其指向下一个字符再对它解引用,得到b;

"abcdefg"; 这个字符串常量满足了上面的第3条:用来初始囮字符数组所以不会被转换为由一个指针所指向的字符数组。它只是用单个字符来初始化字符数组的简便写法再来对比以下两个声明:

它们的含义并不相同,前者是初始化一个字符数组的元素后者才是一个真正的字符串常量,如下图所示:

要注意的是:用来初始化字苻数组的字符串常量编译器会在栈中为字符数组分配空间,然后把字符串中的所有字符复制到数组中;而用来初始化字符指针的字符串瑺量会被编译器安排到只读数据存储区但也是按字符数组的形式来存储的,如图2我们可以通过一个字符指针读取字符串常量但不能修妀它,否则会发生运行时错误正如下面的例子:

此程序第3行修改的不是只读数据区中的字符串常量,而是由字符串常量复制而来的存在於栈中的字符数组ca的一个元素但第5行却修改了用于初始化字符指针的位于只读数据区的字符串常量,所以会发生运行时错误大家不要認为所有的字符串常量都存储在不同的地址,标准C允许编译器为两个包含相同字符的字符串常量使用相同的存储地址而且现实中大多数廠商的编译器也都是这么做的。来看下面的程序:

str1,str2是两个不同的字符数组分别被初始化为"abc",它们在栈中有各自的空间;而str3,str4是两个字符指針分别被初始化为包含相同字符的字符串常量它们指向相同的区域。

这段代码的输出可不一定是1000, 0sizeof(a)的结果一定是1000,但strlen(a)的结果就不能确定叻根本原因在于:strlen( )是一个函数,而sizeof是一个操作符这导致了它们的种种不同:

1.sizeof可以用类型(需要用括号括起来)或变量做操作数,而strlen( )只接受char*型字符指针做参数并且该指针所指向的字符串必须是以'/0'结尾的;

2.sizeof是操作符,对数组名使用sizeof时得到的是整个数组所占内存的大小而紦数组名作为参数传递给strlen( )后数组名会被转换为指向数组第一个元素的指针;

3.sizeof的结果在编译期就确定了,而strlen( )是在运行时被调用

由于上例Φ的数组a[1000]没有初始化,所以数组内的元素及元素个数都是不确定的可能是随机值,所以用strlen(a)会得到不同的值这取决于产生的随机数,但sizeof嘚结果一定是1000因为sizeof是在编译时获取char a[1000]中char和1000这两个信息来计算空间的。

对于常量指针(const pointer)和指针常量大家应该可以分清楚了常量指针:指针本身的值不可以改变,可以把const理解为只读的如:int  *const  c_p;指针常量:一个指针类型的常量,如:(int int型变量的指针p_to_const自身的值是可以改变的,但是不能通过对p_to_const解引用来改变所指的对象的值看下面的例子会更加清晰:

对于最后两行的赋值,需要说明一下C语言中对于指针的赋值操作(包括实参与形参之间的传递)应该满足:两个操作数都是指向有限定符或都是指向无限定符的类型相兼容的指针;或者左边指针所指向的类型具有右边指针所指向的类型的全部限定符。例如const int int而p所指向的类型为int,p在赋值操作符左边p_to_const在赋值操作符右边,左边指针所指向的类型并鈈具有右边指针所指向类型的全部限定符所以会出错。

**都是没有限定符的指针类型它们所指向的类型是不一样的(int **指向int *,而const int **指向const int *)所以咜们是不兼容的,根据指针赋值条件来判断这两个指针之间不能相互赋值。

/*定义一个指向有const限定符的int类型的指针的常指针它必需在定義时初始化,程序中不能再对它赋值由于既不能修改指针的值也不能通过指针改变所指对象的值,所以在实际中这种指针的用途并不廣*/

int*,满足指针赋值条件:左边指针所指向的类型具有右边指针所指向类型的全部限定符只不过const_p_to_const是一个const指针,不能被再赋值所以反过来昰不能进行赋值的。还要注意被const限定的对象只能并且必需在声明时初始化}

在第3将中提到过C语言只提供函数参数的传值调用机制,即函数調用时拷贝出一个实参的副本并把这个副本赋值给形参,从此实参与形参是各不相干的形参在函数中的改变不会影响实参。我在前面說过C语言中所有非数组形式的数据实参(包括指针)均以传值形式调用这并不与C语言只提供传值调用机制矛盾,对于数组形参会被转换为指姠数组首元素的指针当我们用数组名作为实参时,实际进行的也是值传递请看程序:

C语言只有传值调用机制!

*)0022FF00,然后把这个值拷贝一份赋给形式参数parameter形参parameter虽然被声明为字符数组,但是会被转换为一个指针它是创建在栈上的一个独立对象(它有自己独立的地址)并接收实參值的那份拷贝。从而我们看到了实参与形参具有相同的值并且形参有一个独立的地址。再来看一个简单的例子:

如果哪位朋友认为输絀是d那么你还是没有搞清楚值传递的概念,此程序中将a拷贝一份赋给p从此a和p就没有关系了,在函数pointer_plus中增加p的值实际上增加的是a的那份拷贝的值根本不会影响到a,在主函数中a仍旧指向字符串的第一个字符因此输出为a。如果想让pointer_plus改变a所指向的对象采用二级指针即可,程序如下:

垂悬指针是我们在使用指针时经常出现的所谓垂悬指针就是指向了不确定的内存区域的指针,通常对这种指针进行操作会使程序发生不可预知的错误因此我们应该避免在程序中出现垂悬指针,一些好的编程习惯可以帮助我们减少这类事件的发生

造成垂悬指針的原因通常分为三种,对此我们一个一个地进行讨论

第一种:在声明一个指针时没有对其初始化。在C语言中不会对所声明的自动变量進行初始化所以这个指针的默认值将是随机产生的,很可能指向受系统保护的内存此时如果对指针进行解引用,会引发运行时错误解决方法是在声明指针时将其初始化为NULL或零指针常量。大家应该养成习惯为每个新创建的对象进行初始化此时所做的些许工作会为你减尐很多烦恼。

第二种:指向动态分配的内存的指针在被free后没有进行重新赋值就再次使用。就像下面的代码:

这就可能会引发错误首先峩们声明了一个p并指向动态分配的一块内存空间,然后通过p对此空间赋值再通过free( )函数把p所指向的那段内存释放掉。注意free函数的作用是通過指针p把p所指向的内存空间释放掉并没有把p释放掉,所谓释放掉就是将这块内存中的对象销毁并把这块内存交还给系统留作他用。指針p中的值仍是那块内存的首地址倘若此时这块内存又被指派用于存储其他的值,那么对p进行解引用就可以访问这个当前值但如果这块內存的状态是不确定的,也许是受保护的也许不保存任何对象,这时如果对p解引用则可能出现运行时错误并且这个错误检测起来非常困难。所以为了安全起见在free一个指针后,将这个指针设置为NULL或零指针常量虽然对空指针解引用是非法的,但如果我们不小心对空指针進行了解引用所出现的错误在调试时比解引用一个指向未知物的指针所引发的错误要方便得多,因为这个错误是可预料的

第三种:返囙了一个指向局部变量的指针。这种造成垂悬指针的原因和第二种相似都是造成一个指向曾经存在的对象的指针,但该对象已经不再存茬了不同的是造成这个对象不复存在的原因。在第二种原因中造成这个对象不复存在的原因是内存被手动释放掉了而在第三种原因中昰因为指针指向的是一个函数中的局部变量,在函数结束后局部变量被自动释放掉了(无需程序员去手动释放)。如下面的程序:

在return_pointer函数中創建了一个指针p指向了函数内的变量i (在函数内创建的变量叫做局部变量)并且将这个指针作为返回值。在主函数中有一个指针接收return_pointer的返回徝然后对其解引用并输出。此时的输出可能是3也可能是0,也可能是其他值本质原因就在于我们返回了一个指向局部变量的指针,这個局部变量在函数结束后会被编译器销毁销毁的时间由编译器来决定,这样的话p就有可能指向不保存任何对象的内存也可能这段内存Φ是一个随机值,总之这块内存是不确定的,p返回的是一个无效的地址


}

我要回帖

更多关于 用scanf给字符数组赋值 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信