420 likes | 663 Views
第 14 章 指针. 通过本章的学习,需要掌握以下知识点: 指针的概念以及指针变量的内存访问方式; 指针变量的定义、赋值和初始化; 使用指针作为函数形参和函数值; 函数型指针的使用; void 型指针的使用。. 14.1 什么是指针. 简单地说,指针就是一种数据类型,用来表示内存地址。使用指针数据类型声明的变量就是指针变量,使用指针变量可以灵活地对内存空间进行灵活的操作。本节将讨论内存访问的两种方式、以及指针的概念和指针变量的定义等内容。. 14.1.1 访问内存的两种方式.
E N D
第14章 指针 • 通过本章的学习,需要掌握以下知识点: • 指针的概念以及指针变量的内存访问方式; • 指针变量的定义、赋值和初始化; • 使用指针作为函数形参和函数值; • 函数型指针的使用; • void型指针的使用。
14.1 什么是指针 • 简单地说,指针就是一种数据类型,用来表示内存地址。使用指针数据类型声明的变量就是指针变量,使用指针变量可以灵活地对内存空间进行灵活的操作。本节将讨论内存访问的两种方式、以及指针的概念和指针变量的定义等内容。
14.1.1 访问内存的两种方式 • 在C语言中,对内存空间的访问提供了两种访问方式,第一种是直接访问,第二种是间接访问。 • 1. 直接访问 • 在第2章关于变量的讨论中已经知道,如果程序中定义了一个变量,那么系统会为这个变量分配一块内存。每一个变量都有两个属性:变量值和变量地址。变量的地址指示了该变量在内存中存储的位置。而变量值就是该内存上的内容。要访问该空间上的内容可以直接使用变量名,例如: • int a = 241; • printf(“%d”, a);
14.1.1 访问内存的两种方式 • 在第1行语句中,系统会为变量a分配一个空间,并将变量名a与该地址对应起来;在第2行语句中,系统会根据变量名a和其地址的对应关系找到变量名a对应的内存地址,然后再根据该地址值,找到内存空间取出空间上的内容。变量a在内存中的存储形式如下图所示。 这种直接从空间的地址获取该内存内容的访问方式叫做内存的“直接访问”。
14.1.1 访问内存的两种方式 • 2. 间接访问 • 在前面的章节中已经知道,可以使用地址操作符来获得变量的地址,例如对上面的a取&a,可以得到a的地址,即0016。再使用指针操作符可以获得该地址的内容,例如: • *(&a) • 这样便可以获得a的内容。可以将&a赋值给另一个类型的变量,例如: • b = &a; • 变量b的内存空间如右图所示。
14.1.1 访问内存的两种方式 • 借助变量b,以如下方式也可以获得a的内容: • *b • 在这个取a的内容的过程中,系统要访问b,先找到与变量名b对应的内存地址,再根据该地址找到为b分配的空间,获取b的值,即&a,也就是0016;根据b的类型,系统可以知道这个值是一个地址,于是继续查找地址为0016的内存空间,最后获取这个空间上的内容,即241。 • 提示:这种先从其他内存空间获得要访问的内存空间的地址,再根据该地址访问目的空间的方法就是内存的“间接访问”。
14.1.2 指针的概念 • 在C语言中,表示内存地址的数据类型就是指针类型。所以,地址就是指针型数据,一个变量的地址就是一个指针型常量,用来保存地址的变量就是一个指针型变量。通过指针访问内存空间的方式为间接访问。在14.1.1节讨论的间接访问内存的过程中,a的地址(0016)和b的地址(1010)都是指针型数据,而用来保存要访问的目的地址(即a的地址,0016)的变量b就是指针变量。
14.1.2 指针的概念 • 普通变量有两个属性,而指针变量则关联到4个属性:指针变量的地址,即为指针变量分配的内存空间的地址,在上例中为1010。指针变量的值,即该内存空间的内容,在上例中为0016。以指针变量的值为地址的空间地址,也称为指针指向的空间地址,上例中为0016。指针指向的内存空间的内容,上例中为241。指针的所有变化都会在这4个属性上反应出来。
14.1.2 指针的概念 • 虽然指针变量的值和其指向空间地址的值是一样的,但实际上还是有本质上的不同的。指针变量的值,为一个内存空间的内容,是可以改动的。指针变量指向的空间的地址是一个内存空间的地址,这是一个常量,是本身是不可改变的。
14.1.3 指针变量的定义 • C语言中指针类型由两部分组成:数据类型名和指针操作符。定义指针变量的标准形式如下所示。 • 数据类型名 * 指针变量名; • 其中,数据类型名可以为任何数据类型,该数据类型名声明了指针变量指向的内存空间存储的数据类型。
14.1.3 指针变量的定义 • 指针操作符的位置可以和数据类型名紧挨在一起,也可以和指针变量名挨在一起,也可以放在中间,而且中间的空格可以为任意多个。例如,下列的指针变量定义方式都是合法的。 • 数据类型名* 指针变量名; • 数据类型名 *指针变量名; • 数据类型名 * 指针变量名; • 数据类型名*指针变量名; • 数据类型名 * 指针变量名; /* 中间任意多个空格 */ • 注意:定义指针变量时,一般将指针操作符放在靠近变量名的位置。
14.1.3 指针变量的定义 • 下面是几个定义指针变量的例子。 • char * cp; • int * ip; • float * fp;
14.2 指针的使用 • 由于指针的特殊性,对指针变量的赋值和初始化和一般变量有所不同。本节将先介绍指针变量的依次赋值和多次赋值,并且讨论将指针变量赋值为整数的问题,然后讨论指针变量初始化,最后介绍了如何使用const修饰符来声明指针变量。
14.2.1 指针变量的一次赋值 • 为指针变量赋值就是为指针设定它指向的内容空间的过程。指针变量只能保存内存地址,因此给指针变量赋值时必须赋给它一个地址。例如: • 01 char c = ‘a’; • 02 char * cp; • 03 cp = &c; • 第2行,字符型指针cp只是分配了内存,并没有设定指向的内存空间,指针变量值是不可知的。 • 第3行,将指针cp的值设置为字符变量c的地址,那么cp指向变量c的空间。
14.2.1 指针变量的一次赋值 • 此时,要访问c的内容,就可以使用直接访问和间接访问两种方式: • 01 printf(“%c”, c); /* 直接访问 */ • 02 printf(“%c”, *cp); /* 间接访问 */ • 第2行语句中的*cp就是取指针变量cp指向的内存空间的内容。若要对变量c赋值,也可采用以下直接和间接两种方式: • 01 c = 2; /* 直接访问 */ • 02 *cp = 2; /* 间接访问 */
14.2.2 指针变量的多次赋值 • 当然,指针变量的值在一次赋值后,可以再次被赋值为其他地址。这种情况下,指针变量的指向从一个空间转移到另一个空间。下面的范例讨论了在多次赋值过程中指针变量各个属性的变化。
14.2.3 将指针变量赋值为整数 • 原则上,不能将整数数值赋值给指针变量;否则指针变量会指向以该整数数值为地址值的内存空间,此时对该指针变量的操作会操作内存中不可知的一些内存空间,从而会导致一些不可预测的问题。例如,以下行为是危险的: • 01 int * p1 = 12; /* p1指向地址为12的内存空间 */ • 02 *p1 = 23; /* 将地址为12的内存空间上的内容改为23 */ • 但是,如果赋给指针的整数值是一个有效的空间地址,那么程序还是可以正常工作的。不过,这仍是危险的做法,因为很难保证该整数值是有效的。
14.2.4 初始化指针变量 • 使用指针变量时,也应该在其定义语句中为其需要初始化,否则指针将指向一个不可知的空间。
14.2.4 初始化指针变量 • 下面是几个指针变量初始化的例子: • /* 1 */ • int a = 0; • int * p1 = &a; /* 正确的初始化 */ • /* 2 */ • char c = ‘e’; • char *p2 = &c; /* 正确的初始化 */ • /* 3 */ • char c = ‘e’; • int *p3 = &c; /* 不提倡 */ • /* 4 */ • int * p4 = 0xffff22; /* 不提倡,危险的做法 */ • /* 5 */ • int * p5 = NULL; /* 正确的初始化 */
14.2.5 使用const声明指针变量 • 在第4章中已经学习了使用限定词const声明变量可以带来很多好处。同样地,限定词const也可以用来声明指针变量。根据const关键字在声明中出现位置的不同,可以得到多种const指针类型,下面将依次进行讲解。为方便表述,以int型代表数据类型名,以p代表变量名。
14.2.5 使用const声明指针变量 • 1.指向const的指针变量 • 声明指向const的指针变量有以下两种方式: • int const *p • const int * p • 由于变量声明中,const和数据类型名的出现次序可以随意排列,因此以上两种声明表达式是等效的。以上的两个表达式将变量p都声明为指向存储const int型数据的内存空间的指针变量,该类指针指向的内存空间的内容是不可变的。例如,以下操作是错误的: • 01 const int a = 1; • 02 const int * p1 = &a; • 03 • 04 *p1 = 2; • 第4行语句通过赋值表达式改变p1指向的内存空间的内容,而声明中p1指向的内容是不可改变的,因此该语句是错误的。
14.2.5 使用const声明指针变量 • 2.const型指针变量 • int * const p • 该表达式声明了一个int型的const指针变量,即该指针变量的值是不可以改变的,也就是说const型指针变量指向的内存空间是固定的,初始化后不能将其指向其他空间。例如,以下操作是错误的: • 01 int a = 1; • 02 int b = 2; • 03 int * const p = &a; • 04 *p = 12; /* 正确 */ • 05 p = &b; /* 错误 */ • 第4行,对int型const指针变量指向的内存空间赋值是允许的;但是第5行,试图改变const指针变量p的值,将p指向变量b的内存空间,这是错误的,因为const指针的值是不可变的。
14.2.5 使用const声明指针变量 • 3.指向const的const指针变量 • const int * const p • 该表达式声明了一个指向存放const int型空间的const指针变量,该指针变量的值和该指针指向的空间的值都是不可改变的。以下行为是错误的: • 01 int a = 1; • 02 int b = 2; • 03 int const * const p = &a; • 04 *p = 12; /* 错误 */ • 05 p = &b; /* 错误 */ • 第4行,试图改变p指向的内存空间的内容,也就是p指向的变量a的值,这是错误的;第5行,试图改变p的值,也是错误的。该指针变量的值及其指向空间的值都是不可改变的。
14.3 指针与函数 • 在函数一章中,已经知道可以使用整型变量、浮点型变量和字符型变量作为函数参数,指针变量也可以作为函数参数使用。此外,还可以使用指针型变量作为函数的函数值。本章将讨论如何使用指针型变量作为函数的形参和函数值。
14.3.1 指针形参 • 函数调用时,会实参的值赋值给形参。使用指针变量作为函数参数,可以将一个内存空间的地址传递到函数中,可以通过该地址来操作该地址上的内存空间。例如,若有以下函数声明: • void func(int * pt, int a); • 该函数含有两个形参,一个为int型指针变量,一个为int型变量。
14.3.1 指针形参 • 若有以下语句: • 01 int *p; • 02 int x, y; • 03 x = 5; • 04 y = 20; • 05 p = &x; • 06 func(p, y); • 函数调用时,形参pt和a分别被赋值为实参p和y的值。该过程可以简单地视为: • pt = p; • a = y;
14.3.1 指针形参 • 此时,pt的值为x的地址,即pt也指向x;a的值为20。赋值后,pt的值的改变不会影响p的值,a的改变也不会影响y的值。但是,此时如果改变*pt,例如: • *pt = 20; • 该语句将pt指向的空间赋值为20,即a被赋值为20。此时,函数内的操作改变了函数外的变量值,指针参数可以通过地址传递来间接改变外部的变量值,这种功能是其他类型的参数不能实现的。这种传递变量地址的方式称为地址传递。
14.3.2 指针型函数值 • 函数的函数返回值也可以是指针型的数据,即地址。返回该类型值时,执行机制与返回其他类型完全相同。含有指针型函数值的函数的声明一般为: • 数据类型 * 函数名(形参列表); • 其中,数据类型和指针操作符组成指针类型。例如: • int * max(int a, int b, int c); • 此max函数中的return语句必须返回一个变量的地址或一个指针变量的值。
14.3.3 函数型指针 • C程序中的函数也都是存放在代码区内的,它们同样也是有地址的。那么如何取得函数的地址呢?在前面也说过函数定义的时候实际上是定义了一个函数变量,那么是否可以将函数变量赋值给其他变量呢?回答这些问题需要涉及到另外一个概念:函数型指针。按照已有的指针的知识,顾名思义,函数型指针就是指向函数的指针。如果有一个函数声明为: • int func(const int a, const int b);
14.3.3 函数型指针 • 那么此时声明的函数变量add的地址即为这个函数的地址,同时add的值保存为这个函数的地址,这个特性与数组相似:数组变量与数组变量的地址均为数组的起始地址。而在这个函数声明中,函数类型为int (const int a, const int b)。使用该函数类型来定义一个函数型指针,其方式如下: • int (* fp)(const int a, const int b); /* 其中,参数列表的参数名a和b可省 */
14.3.3 函数型指针 • 上述语句将变量func定义为指向类型为int (const int a, const int b)的指针操作符和变量名两侧的小括号不可省,否则其含义大不相同。例如: • int * fp(const int a, const int b); • 此时,指针操作符与数据类型int结合为int型指针类型,该语句只是声明了一个fp函数,而非定义一个函数指针。为该函数型指针赋值的方式如下: • fp = func; • 被赋值的函数变量的类型必须与fp的类型完全一致,包括其返回类型和每一个形参的类型。否则程序将报错。
14.3.3 函数型指针 • 注意:函数型指针变量赋值时,左值与右值的类型必须完全一致。 • 使用函数型指针变量调用函数的方法与使用函数变量类似,得到函数地址后再带上参数列表即可。可以使用下面两种方式来调用函数: • fp(5, 6); • 或 • (*fp)(5, 6); • 由于fp被赋值为函数变量func的地址,而func的值又等于其地址,所以*fp可以得到func函数的地址。因此,在调用方式上,可以粗略地将两者视为一致(实际上其后台的处理略有不同)。
14.4 void型指针 • 从前面的学习中知道,可以使用关键字void作为函数的形参列表,其表示该函数不需要参数。void还可以用作函数的函数值类型,其表示该函数没有返回值。此外,void还可以用做指针类型,这种指针被成为void型指针。本节将讨论void型指针的含义和使用。
14.4.1 void型指针的含义 • void型指针就是无类型指针。一般的数据类型指针只能指向存储该数据类型的空间,而void型指针则可以指向存储任意数据类型的空间。其定义形式如下: • void * 变量名; • 可以知道,不能在不同的数据类型指针变量之间赋值。例如: • char * p1 = “Goodbye”; • int * p2 = p1; • 上述语句将char型指针变量的值赋值给int型指针变量,这是不允许的。编译器会产生如下的警告信息: • warning C4133: 'initializing' : incompatible types - from 'char *' to 'int *'
14.4.1 void型指针的含义 • 要消除警告信息,必须进行强行转换。例如: • int * p2 = (int *)p1; • 而void型指针变量可以赋值为任意类型的地址值,也可以用来作为赋值表达式的右值。例如: • void * p1 = “Goodbye”; • int * p2 = p1; /* 不推荐的做法 */
14.4.1 void型指针的含义 • 第1行中,将void型指针变量p1赋值char型地址值;第2行中,将void型指针的值赋值给int型指针变量p2。在大部分编译器中,这两个行为都是允许的。但在一些编译器中,不允许直接将void型指针变量赋值给其他类型指针,同样也需要类型转换。例如: • int * p2 = (int *)p1; /* 推荐的做法 */ • void型指针还有一个特殊的地方就是不能对void型指针做加减运算。因为,对指针的加减是基于指针指向的类型空间的字节长度进行的。例如: • int * p1; • ++p1;
14.4.1 void型指针的含义 • 第2行语句将p1加1,其实际地址值增加了sizeof(p1)。而void型指针变量指向的地址空间的类型是不可知的,因此无法对其做加减运算。由于同样的原因,也不能对void型指针取内容。以下操作是错误的: • void * p2; • ++p2; /* 错误 */ • *p2; /* 错误 */ • 提示:不能对void型指针作加减运算和取内容操作。
14.4.2 void指针型形参 • 由于void型指针具有可以赋值为所有其他类型指针的特性,因此void型指针经常用做函数的形参,以使该形参能接受指向任意类型的指针变量,使该函数能处理更大范围的数据。若有函数声明如下: • void initial(void * p) • 那么initial函数可以接受各种类型的实参,例如: • int a = 2; • char b = ‘c’; • double c = 3.2; • initial(&a); • initial(&b); • initial(&c); • 因此,当一个函数需要处理各种不同类型的形参时,可以考虑使用void型指针作为形参。
14.4.3 void指针型函数值 • void型指针变量也可以用作函数的函数值。函数声明如下: • void * 函数名(形参类型); • 具有void指针型函数值的函数经类型转换后可以赋值给任意类型的指针变量。例如,有以下声明的函数: • void * func(int a); • 可以使用该函数作为赋值表达式的右值,例如: • int * p1 = NULL; • char * p2 = NULL; • … • p1 = (int *)func(sizeof(int)); • p2 = (char *)func(sizeof(char));
14.5 综合练习 • 1.使用指针从标准输入获取三个整数,并求其中的最大值。 • 2.实现一个可以初始化各种类型数据的函数。