第6单元指针 -110 第6单元指针 本单元教学目标 介绍C++中指针的基本概念 学习要求 指针是C++中最重要的基本概念之一。要求同学们充分理解和掌握: 1什么是地址?什么是指针? 2指针类型变量的声明方法和怎样将一个变量或数组的地址赋给指针类型的变量 3.怎样通过指针类型的变量去访问某个变量或数组元素的值。 授课内容 61地址与指针 我们知道,从拓扑结构上来说,计算机的内存储器就象一个巨大的一维数组,每个数组 元素就是一个存储单元(在微型计算机中其大小通常为一个字节)。就象数组中的每个元素 都有一个下标一样,每个内存单元都有一个编号,又称地址 存储器是程序活动的基本舞台。在运行一个程序时,程序本身及其所用到的数据都要 放在内存储器中:程序、函数、变量、常数、数组和对象等,无不在内存储器中占有一席之 凡是存放在内存储器中的程序和数据都有一个地址,用它们占用的那片存储单元中的 第一个存储单元的地址表示。在C++中,为某个变量或者函数分配存储器的工作由编译程 序完成l。在编写程序时,通常是通过名字来使用一个变量或者调用某个函数。这样做既直 观,又方便。而变量和函数的名字与其实际存储地址之间的变换由编译程序自动完成。但是 C+-也允许直接通过地址处理数据,在很多情况下这样做可以提高程序的运行效率 那么怎样才能知道某个变量、数组或者函数的地址呢? C++规定: (1)变量的地址可以使用地址运算符&求得。例如,&x表示变量x的地址 1实际上,为变量和函数分配存储的工作是分几步完成的。在编译和连接的过程中,只给程序中的各个变量 分配了相对地址。每个变量的实际存储位置要到程序运行时才能确定。如果是局部变量,其存储分配更晚, 要到该局部变量所属的函数被调用时才进行。因此,同一个变量,在程序的各次运行中可能被分配在不同的 存储地址上。这也是为什么通常只需要知道某变量确有一个地址,而不必关心该地址值具体是多少的原因
第 6 单元 指针 - 110 - 第 6 单元 指针 本单元教学目标 介绍C++中指针的基本概念。 学习要求 指针是C++中最重要的基本概念之一。要求同学们充分理解和掌握: 1.什么是地址? 什么是指针? 2.指针类型变量的声明方法和怎样将一个变量或数组的地址赋给指针类型的变量; 3.怎样通过指针类型的变量去访问某个变量或数组元素的值。 授课内容 6.1 地址与指针 我们知道, 从拓扑结构上来说, 计算机的内存储器就象一个巨大的一维数组, 每个数组 元素就是一个存储单元(在微型计算机中其大小通常为一个字节)。就象数组中的每个元素 都有一个下标一样, 每个内存单元都有一个编号, 又称地址。 内存储器是程序活动的基本舞台。在运行一个程序时, 程序本身及其所用到的数据都要 放在内存储器中:程序、函数、变量、常数、数组和对象等, 无不在内存储器中占有一席之 地。 凡是存放在内存储器中的程序和数据都有一个地址, 用它们占用的那片存储单元中的 第一个存储单元的地址表示。在C++中,为某个变量或者函数分配存储器的工作由编译程 序完成1。在编写程序时, 通常是通过名字来使用一个变量或者调用某个函数。这样做既直 观, 又方便。而变量和函数的名字与其实际存储地址之间的变换由编译程序自动完成。但是, C++也允许直接通过地址处理数据, 在很多情况下这样做可以提高程序的运行效率。 那么怎样才能知道某个变量、数组或者函数的地址呢? C++规定: (1) 变量的地址可以使用地址运算符&求得。例如, &x 表示变量 x 的地址; 1 实际上, 为变量和函数分配存储的工作是分几步完成的。在编译和连接的过程中, 只给程序中的各个变量 分配了相对地址。每个变量的实际存储位置要到程序运行时才能确定。如果是局部变量, 其存储分配更晚, 要到该局部变量所属的函数被调用时才进行。因此, 同一个变量, 在程序的各次运行中可能被分配在不同的 存储地址上。这也是为什么通常只需要知道某变量确有一个地址, 而不必关心该地址值具体是多少的原因
第6单元指针 (2)数组的地址,即数组第一个元素的地址,可以直接用数组名表示 (3)函数的地址用函数名表示。 前面已经介绍过,所谓地址就是存储单元在整个内存构成的数组中的下标,实际上可用 一个无符号整数(无符号长整数)表示2。因此,可以用一个无符号整型变量将地址存放起 来。这种用来存放地址的变量就叫作指针型变量,简称指针。假定pt为一指针,则语句 ptr=&x 就将变量x的地址存入了指针ptr中。当然,也可以通过指针得到变量x的值,或者改变变 量x的值: y= ptr 其中的运算符“*”用于一个地址(例如指针变量值)之前,表示该地址上的变量、数 组或者函数本身。因此,语句*p=2,的作用相当于x=2,因为指针p中存放者变量x 的地址。由于通过指针可以对一个其他类型的变量进行操作,所以有时也把“变量x的地址 存放在指针ptr中”简称为“指针ptr指向变量x”。 62指针型变量的声明 如上所述,指针也是一个变量,因此也必须遵循先声明,后使用的原则。在C+中,并 没有一个专用的指针类型说明符,声明指针类型的变量是通过声明该指针指向的变量类型 进行的。如果要声明一个用于存储int类型变量地址的指针ptr,则可以使用如下语句 注意这时运算符“*”的含义是说明指针变量pr用于指向一个int类型的变量 例6-编写用于交换两个整型变量的值的函数。 算法:交换两个变量的值必须使用第3个变量。例如,要交换变量x和y的内容 可以使用临时变量tmp int x, y, tmp 但在例5-6中直接利用上法将函数swap()编写为 //函数swap0:交换两个整形变量的值(不成功) void swap( int x. int int tmp; 2这只是一个简化了的说法,在不同的计算机中可能有更复杂的情况
第 6 单元 指针 - 111 - (2) 数组的地址, 即数组第一个元素的地址, 可以直接用数组名表示; (3) 函数的地址用函数名表示。 前面已经介绍过,所谓地址就是存储单元在整个内存构成的数组中的下标,实际上可用 一个无符号整数(无符号长整数)表示2。因此,可以用一个无符号整型变量将地址存放起 来。这种用来存放地址的变量就叫作指针型变量, 简称指针。假定 ptr 为一指针, 则语句 ptr = &x; 就将变量 x 的地址存入了指针 ptr 中。当然, 也可以通过指针得到变量 x 的值,或者改变变 量 x 的值: *ptr = 2; y = *ptr; 其中的运算符“*”用于一个地址(例如指针变量值)之前,表示该地址上的变量、数 组或者函数本身。因此, 语句 *ptr = 2; 的作用相当于 x = 2; ,因为指针 ptr 中存放者变量 x 的地址。由于通过指针可以对一个其他类型的变量进行操作, 所以有时也把“变量 x 的地址 存放在指针 ptr 中”简称为“指针 ptr 指向变量 x”。 6.2 指针型变量的声明 如上所述, 指针也是一个变量, 因此也必须遵循先声明, 后使用的原则。在C++中, 并 没有一个专用的指针类型说明符, 声明指针类型的变量是通过声明该指针指向的变量类型 进行的。如果要声明一个用于存储 int 类型变量地址的指针 ptr, 则可以使用如下语句: int *ptr; 注意这时运算符“*”的含义是说明指针变量 ptr 用于指向一个 int 类型的变量。 [例 6-1] 编写用于交换两个整型变量的值的函数。 算 法: 交换两个变量的值必须使用第 3 个变量。例如,要交换变量 x 和 y 的内容, 可以使用临时变量 tmp: int x, y, tmp; tmp = x; x = y; y = tmp; 但在例 5-6 中直接利用上法将函数 swap()编写为 // 函数 swap(): 交换两个整形变量的值(不成功) void swap(int x,int y) { int tmp; 2 这只是一个简化了的说法, 在不同的计算机中可能有更复杂的情况
第6单元指针 tmp =X 却是有问题的。试用以下主函数验证 //试验函数 swapo用的主函数 void maino int x =2,y=3 cout<"x=“<x<<“,y=“<y<<endl (x,y) cout < After exchange x& y: "< endl cout(“x=“<x<“,y=“<y<<endl 其输出为 After exchange x& y 2,y=3 从结果可以看出,上面的swap()函数根本没有完成交换变量x和y的任务。为什么?请 看图6-1中的内存分配情况 DS:1001 主函数中定义的变量x DS:1002 主函数中定义的变量y DS:1003 2 函数 swap o的参数变量x DS:1005 DS:100 函数 swapO的参数变量y DS:1007 DS:1008 函数 swapo中定义的局部变量tm DS:1009 图6-1验证交换变量值的函数swap()时的内存分配示意图3 图6-1描述的是程序运行到swap()函数中时的内存分配。由图中可以看出,主函数 中声明的变量x和y与函数swap()的参数x和y在内存中分别占有各自的存储区,它们之 间唯一的联系只是在主函数中调用函数swap()时将主函数中变量x和y的值分别传送给 了函数swap()的两个参数x和y。参数x和y在函数swap()运行期间相当于两个局部 3由于地址分配问题相当复杂,超出本课程的范围。因此图中的地址值只是示意。以下各图同此
第 6 单元 指针 - 112 - tmp = x; x = y; y = tmp; } 却是有问题的。试用以下主函数验证: // 试验函数 swap()用的主函数 void main() { int x = 2, y = 3; cout << "x = “ << x << “, y = “ << y << endl; swap(x,y); cout << "After exchange x & y:” << endl; cout << “x = “ << x << “, y = “ << y << endl; } 其输出为 x = 2, y = 3 After exchange x & y: x = 2, y = 3 从结果可以看出, 上面的 swap()函数根本没有完成交换变量 x 和 y 的任务。为什么? 请 看图 6-1 中的内存分配情况: 图 6-1 验证交换变量值的函数 swap()时的内存分配示意图3 图 6-1 描述的是程序运行到 swap()函数中时的内存分配。由图中可以看出,主函数 中声明的变量 x 和 y 与函数 swap()的参数 x 和 y 在内存中分别占有各自的存储区, 它们之 间唯一的联系只是在主函数中调用函数 swap()时将主函数中变量 x 和 y 的值分别传送给 了函数 swap()的两个参数 x 和 y。参数 x 和 y 在函数 swap()运行期间相当于两个局部 3 由于地址分配问题相当复杂,超出本课程的范围。因此图中的地址值只是示意。以下各图同此。 DS:1000 DS:1001 DS:1002 DS:1003 DS:1004 DS:1005 DS:1006 DS:1007 DS:1008 DS:1009 主函数中定义的变量x 主函数中定义的变量y 函数swap()的参数变量x 函数swap()的参数变量y 函数swap()中定义的局部变量tmp 2 3 2 3
第6单元指针 113 变量。因此,事情很明显,在函数swap()执行完毕以后,其参变量ⅹ和y的值确实已被交 换,如图6-2所示 然而,事情到此为止D:1002 了。从图62中可以看出,DS:1001 主函数中定义的变量x 虽然swap()函数的两个 DS:1002 主函数中定义的变量y 参数变量x和y的值已被交 函数swap0的参数变量x 换,但原来主函数的变量xDS:1005 和y的值却没有发生变化 DS:1002 函数 swapo的参数变量y 而且,随着函数swap()运DS:1008 行结束返回主函数后, swap DS:1092 函数 swapo中定义的局部变量tmp ()中为局部工作变量申请 的内存空间,包括其参数图6-2函数sWap()中的运算结束时的内存分配示意图 x、y和局部变量tmp占用 的内存单元,都将被释放。函数swap()所做的一切工作都付之东流了 那么,怎样才能编写一个真正可以交换参数值的swap()函数呢?这就要用到指针了 下面将函数swap()的参数声明为指向int类型的指针,重新编写该函数 程序 ∥/ Example6-1:函数 swapo:交换两个整形变量的值 void swap (int *xp int *yp) int tmp *xp * yp *kyp tmp: DS:1000 注意到这次函数DS:1001 主函数中定义的变量x DS:1002 swap()的参数变量xpDs:1003 主函数中定义的变量y 和yp是两个指向整型变m:041100 函数swap0的参数变量xp 量的指针,因此在调用该DS:1005 函数时只能使用变量的地s:10071002函数swap0的参数变量yp DS:1006 址作为实参。使用下列语DS:1008 函数 swap o中定义的局部变量tmp 句取代测试主函数中的函DS:1009 数调用语句 图6-3函数 swapo中的运算结束时的内存分配示意图 swap(&x, &y) 此时的内存分配如图6-3所示 于是,在新的swap()函数中,语句
第 6 单元 指针 - 113 - 变量。因此, 事情很明显, 在函数 swap()执行完毕以后, 其参变量 x 和 y 的值确实已被交 换, 如图 6-2 所示。 然而, 事情到此为止 了。从图 6-2 中可以看出, 虽然 swap()函数的两个 参数变量x和y的值已被交 换, 但原来主函数的变量 x 和 y 的值却没有发生变化。 而且, 随着函数 swap()运 行结束返回主函数后, swap ()中为局部工作变量申请 的内存空间, 包括其参数 x、y 和局部变量 tmp 占用 的内存单元,都将被释放。函数 swap()所做的一切工作都付之东流了。 那么, 怎样才能编写一个真正可以交换参数值的 swap()函数呢? 这就要用到指针了。 下面将函数 swap()的参数声明为指向 int 类型的指针, 重新编写该函数: 程 序: // Example 6-1:函数 swap(): 交换两个整形变量的值 void swap(int *xp,int *yp) { int tmp; tmp = *xp; *xp = *yp; *yp = tmp; } 注意到这次函数 swap()的参数变量 xp 和 yp 是两个指向整型变 量的指针, 因此在调用该 函数时只能使用变量的地 址作为实参。使用下列语 句取代测试主函数中的函 数调用语句: swap(&x,&y); 此时的内存分配如图 6-3 所示。 于是, 在新的 swap()函数中, 语句 tmp = *xp; DS:1000 DS:1001 DS:1002 DS:1003 DS:1004 DS:1005 DS:1006 DS:1007 DS:1008 DS:1009 主函数中定义的变量x 主函数中定义的变量y 函数swap()的参数变量x 函数swap()的参数变量y 函数swap()中定义的局部变量tmp 2 3 3 2 2 图6-2 函数swap()中的运算结束时的内存分配示意图 DS:1000 DS:1001 DS:1002 DS:1003 DS:1004 DS:1005 DS:1006 DS:1007 DS:1008 DS:1009 主函数中定义的变量x 主函数中定义的变量y 函数swap()的参数变量xp 函数swap()的参数变量yp 函数swap()中定义的局部变量tmp 2 3 1000 1002 2 图6-3 函数swap()中的运算结束时的内存分配示意图
第6单元指针 114 kyp tmp 通过地址直接对主函数中原来的变量x和y进行操作,完成了交换这两个变量的值的任务 在函数swap()执行完毕后,即使释放其局部变量tmp和指针参数xp、y占用的存储也不 会影响到主函数中变量x和y的新内容 般来说,函数可以用返回值的形式为调用程序提供一个计算结果。在前面的各单元中 出现的函数返回值类型大都是int、 double之类的简单类型。其实,也可以将一个地址数据 (如变量、数组和函数的地址,指针变量的值等)作为函数的返回值。在声明返回值为地址的 函数时,要使用指针类型说明符,例如 char *strchr(char *string, int c) char *strstr(char *stringl, char *string2) 这是两个用于字符串处理的库函数,其返回值均为地址。前者的功能为在字符串 string 中查找字符c,如果字符串 string中有字符c出现,则返回字符c的地址,否则返回NUI 后者的功能为在字符串 stringl中查找子字符串 string2,如果字符串 stringl中包含有子字符串 string2,则返回sing2在 string中的地址(即 string2中第一个字符的地址)否则返回空指针 值N 例6-2将表示月份的数值(1-12)转换成对应的英文月份名称。 算法:首先声明一个字符串数组 month,用来存放月份的英文名称。在转换时只须 按下标值返回一个字符串的地址即可。 程序 / Example6-2:将月份数值转换为相应的英文名称 char month static char *month[ Illegal month",∥/月份值错 January /一月 February /二月 March 三月 /四月 /五月 /六月 July /七月 /八月 /九月 /十月 /十一月
第 6 单元 指针 - 114 - *xp = *yp; *yp = tmp; 通过地址直接对主函数中原来的变量 x 和 y 进行操作, 完成了交换这两个变量的值的任务。 在函数 swap()执行完毕后,即使释放其局部变量 tmp 和指针参数 xp、yp 占用的存储也不 会影响到主函数中变量 x 和 y 的新内容。 一般来说, 函数可以用返回值的形式为调用程序提供一个计算结果。在前面的各单元中 出现的函数返回值类型大都是 int、double 之类的简单类型。其实,也可以将一个地址数据 (如变量、数组和函数的地址, 指针变量的值等)作为函数的返回值。在声明返回值为地址的 函数时, 要使用指针类型说明符, 例如 char *strchr(char *string, int c); char *strstr(char *string1, char *string2); 这是两个用于字符串处理的库函数,其返回值均为地址。前者的功能为在字符串 string 中查找字符 c,如果字符串 string 中有字符 c 出现, 则返回字符 c 的地址, 否则返回 NULL。 后者的功能为在字符串 string1 中查找子字符串 string2,如果字符串 string1 中包含有子字符串 string2, 则返回 string2 在 string1 中的地址(即 string2 中第一个字符的地址), 否则返回空指针 值 NULL。 [例 6-2] 将表示月份的数值(1−12)转换成对应的英文月份名称。 算 法:首先声明一个字符串数组 month,用来存放月份的英文名称。在转换时只须 按下标值返回一个字符串的地址即可。 程 序: // Example 6-2:将月份数值转换为相应的英文名称 char *month_name(int n) { static char *month[]= { "Illegal month", // 月份值错 "January", // 一月 "February", // 二月 "March", // 三月 "April", // 四月 "May", // 五月 "June", // 六月 "July", // 七月 "August", // 八月 "September", // 九月 "October", // 十月 "November", // 十一月
第6单元指针 December /十二月 return (n>=1 & n<=12)?month[n]: month[o] 分析:我们知道,只有全局数组和静态局部数组可以赋初值,而随意使用全局数组 和全局变量又会破坏程序的模块化结构,降低程序的可读性。因此,在上述函数中选用静态 局部指针数组存放表示月份的英文名称的各字符串的地址。该数组中的第一个字符串表示输 入错误的月份值时的提示 63指针与数组 指针是一个变量,因此也应该可以参加运算。那么,对指针进行运算的意义是什么? 设有指针p,qtr以及字符型数组 string char *ptr, *qtr char string[6]: DS: 1000B DS:1001 其关系如图6-4所示。 DS:1002 字符型数组 string 从图64中可以看出,DS:1003 DS:1004 指针p指向数组 string的第ps:1005 一个元素,其内容就是该元DS:1006 素的地址1000现在,如果DS:100771000 指针ptr:指向数组 string DS:1008 执行运算 1003 指针qtr:指向字符串之尾 DS:1009 DS: 100A ptr++ DS:100+0003整数变量len 即在指针变量ptr原来 的值上再加1,使其变为 图6-4指针的运算 1001。可以看出,这正是数组中第二个元素的地址,也就是说,指针现在改为指向数组中的 第二个元素了。如果执行运算 ptr + 3 则p的值由1000变为1003,即指向数组 string的第四个元素。由此可以看出在指针变量 上加上一个常数,相当于改变了其中存储的地址值,即改变了指针指向的数组元素。同样, 也可以从指针变量存储的地址值上减去一个常数,此时指针向前移动若干个元素 在图6-4中还有一个指针变量q,指向字符串的结束标志0(即数组 string的第四个元 素)。如果求出这两个指针的差 len qtr-ptr: 可以看出,这正是字符串 string的长度(不含字符串结束符) 由于在C艹+中每个变量、数组和函数的具体地址和相对顺序是由连接程序确定的(局 部变量是动态分配的),在编写程序时无法知道其确切地址和相对顺序,所以对于指向单个
第 6 单元 指针 - 115 - "December" // 十二月 }; return (n>=1 && n<=12)?month[n]:month[0]; } 分 析:我们知道, 只有全局数组和静态局部数组可以赋初值, 而随意使用全局数组 和全局变量又会破坏程序的模块化结构, 降低程序的可读性。因此, 在上述函数中选用静态 局部指针数组存放表示月份的英文名称的各字符串的地址。该数组中的第一个字符串表示输 入错误的月份值时的提示。 6.3 指针与数组 指针是一个变量, 因此也应该可以参加运算。那么, 对指针进行运算的意义是什么? 设有指针 ptr, qtr 以及字符型数组 string: char *ptr,*qtr; char string[6]; 其关系如图 6-4 所示。 从图 6-4 中可以看出, 指针 ptr 指向数组 string 的第 一个元素, 其内容就是该元 素的地址 1000。现在, 如果 执行运算 ptr++; 即在指针变量 ptr 原来 的值上再加 1, 使其变为 1001。可以看出, 这正是数组中第二个元素的地址, 也就是说, 指针现在改为指向数组中的 第二个元素了。如果执行运算 ptr += 3; 则 ptr 的值由 1000 变为 1003,即指向数组 string 的第四个元素。由此可以看出在指针变量 上加上一个常数, 相当于改变了其中存储的地址值, 即改变了指针指向的数组元素。同样, 也可以从指针变量存储的地址值上减去一个常数, 此时指针向前移动若干个元素。 在图 6-4 中还有一个指针变量 qtr, 指向字符串的结束标志 0(即数组 string 的第四个元 素)。如果求出这两个指针的差: len = qtr-ptr; 可以看出, 这正是字符串 string 的长度(不含字符串结束符)。 由于在C++中每个变量、数组和函数的具体地址和相对顺序是由连接程序确定的(局 部变量是动态分配的), 在编写程序时无法知道其确切地址和相对顺序, 所以对于指向单个 DS:1000 DS:1001 DS:1002 DS:1003 DS:1004 DS:1005 DS:1006 DS:1007 DS:1008 DS:1009 DS:100A DS:100B 指针ptr:指向数组string 1003 指针qtr:指向字符串之尾 图6-4 指针的运算 B i g \0 1000 0003 整数变量len 字符型数组string
第6单元指针 变量和函数的指针进行这样的运算是没有意义的。但是无论怎样分配,一个数组内的各元素 的相对位置总是固定的,所以对数组元素的引用除了使用下标以外,还可以通过使用指针运 算来实现,这是C++程序设计的一大特点 例6-3编写一个字符串复制函数 mystrcpy( 算法:我们知道,字符串的定义是以0为结束符的字符序列。因此复制字符串也只 需复制到0为止。设指针 source中存放着原来的字符串的地址,指针 destin中存放着新的字 符型数组的地址,则伪代码算法为 while*source! =0) /如果* source==0则表示原字符串结束 * destin=* source;//复制字符 // source移向原字符串中的下一个字符 destin +t / destin移向新字符数组的下一位置 destin =0 /在新字符串尾部添写一个结束符0 由于这个算法非常直观,所以上面的伪代码其实已经是最终的程序段了。由于C++的表 达式应用非常灵活,所以这段程序也可以写成: while((*destin++= *source++)!=0) 改写后的程序段只使用了半个语句( while语句的后半部分被省略掉了)!但是功能依 然不变,甚至新字符串尾部的0也已经被复制。C++的这种特点,是其程序比较精练的原因, 受到程序员们的偏爱。但是过分的精练也会使程序难以理解,有悖于结构化程序设计的基本 原则4。 程序: / Example6-3:复制字符串 nystrcpy(char *destin, char *source) ile(*source!=0) 4可能有人会说精练的程序效率高,其实这是一种误解。影响程序效率的因素很多,但源代码长度对程序效 率几乎没有直接影响。事实上,程序的效率表现在两个方面,一是程序运行速度,一是程序使用的数据占用 的存储空间大小。通常用相对于程序处理的数据量所执行的机器指令条数的数量级来表示程序的运行效率 (时间)。例如O(n)表示程序运行时间与数据量之间成比例,O(n2)表示程序运行时间与数据量的平方 成比例,运行时间的增长速度大于数据量的增长速度。显然,对于完成同样功能的两个程序来说,如果一个 程序的效率为O(n),另一个程序的效率为O(n2),则前者的效率高于后者。对程序的空间效率也可以 做如此分析。 对于上面的两个程序段来说,可以使用字符串长度表示数据量,通过简单的分析就可以得出结论,它 们的运行速度均为O(n),使用的数据占用的存储空间长度均为2n,所以效率基本相同。至于程序代码本 身的大小,应以编译后生成的目标代码长度为准,实践表明这两段程序所生成的目标代码长度亦相差无几
第 6 单元 指针 - 116 - 变量和函数的指针进行这样的运算是没有意义的。但是无论怎样分配, 一个数组内的各元素 的相对位置总是固定的, 所以对数组元素的引用除了使用下标以外, 还可以通过使用指针运 算来实现, 这是C++程序设计的一大特点。 [例 6-3] 编写一个字符串复制函数 mystrcpy()。 算 法: 我们知道,字符串的定义是以 0 为结束符的字符序列。因此复制字符串也只 需复制到 0 为止。设指针 source 中存放着原来的字符串的地址,指针 destin 中存放着新的字 符型数组的地址, 则伪代码算法为 while(*source!=0) // 如果*source==0 则表示原字符串结束 { *destin = *source; // 复制字符 source ++; // source 移向原字符串中的下一个字符 destin ++; // destin 移向新字符数组的下一位置 } *destin = 0; // 在新字符串尾部添写一个结束符 0 由于这个算法非常直观, 所以上面的伪代码其实已经是最终的程序段了。由于C++的表 达式应用非常灵活, 所以这段程序也可以写成: while((*destin++ = *source++) != 0); 改写后的程序段只使用了半个语句(while 语句的后半部分被省略掉了)!但是功能依 然不变,甚至新字符串尾部的 0 也已经被复制。C++的这种特点, 是其程序比较精练的原因, 受到程序员们的偏爱。但是过分的精练也会使程序难以理解, 有悖于结构化程序设计的基本 原则4。 程 序: // Example 6-3:复制字符串 mystrcpy(char *destin, char *source) { while(*source!=0) 4 可能有人会说精练的程序效率高, 其实这是一种误解。影响程序效率的因素很多, 但源代码长度对程序效 率几乎没有直接影响。事实上, 程序的效率表现在两个方面, 一是程序运行速度, 一是程序使用的数据占用 的存储空间大小。通常用相对于程序处理的数据量所执行的机器指令条数的数量级来表示程序的运行效率 (时间) 。例如 O(n)表示程序运行时间与数据量之间成比例; O(n 2)表示程序运行时间与数据量的平方 成比例, 运行时间的增长速度大于数据量的增长速度。显然, 对于完成同样功能的两个程序来说, 如果一个 程序的效率为 O(n), 另一个程序的效率为 O(n 2),则前者的效率高于后者。对程序的空间效率也可以 做如此分析。 对于上面的两个程序段来说, 可以使用字符串长度表示数据量, 通过简单的分析就可以得出结论, 它 们的运行速度均为 O(n), 使用的数据占用的存储空间长度均为 2n, 所以效率基本相同。至于程序代码本 身的大小, 应以编译后生成的目标代码长度为准, 实践表明这两段程序所生成的目标代码长度亦相差无几
第6单元指针 水 source source +t destin +t tin =0: 分析:函数 mystrcpy()的功能为将一个字符串的内容复制到另一个字符型数组中 去。在复制字符串时要注意,一定要保证目标数组确实可以放得下整个字符串。初学者最易 犯的一个错误是混淆指针与数组的概念,写出如下的语句 char *stringl =This is a sample. char *string2 strcpy(string2, stringl) 这时确实可以将字符串 stringl中的内容复制到从存放于指针 string2中的地址开始的 段内存中去。但问题是指针 string2中存放的究竟是谁的地址?由于没有对 string2赋值,所 以它可能指向任何地方,包括已经分配给其他变量、数组甚至函数的区域。向 stringz复制字 符串会覆盖这些地方原来的内容,造成各种运行错误,包括突然死机;即使幸而指针 string2 指向一片未被使用的存储区,成功地复制了字符串,但由于没有合法的授权,也不能保证其 后程序不再将这片存储区域分配给其他的变量或数组,从而造成刚刚复制的内容又被其他 数据所覆盖。 上面的例子使用了字符类05:1000 型的数组其特点是每个数组0:0F2 整型数组 array 元素的大小正好是一个字节。DS:1003 DS:1004 如果使用其他类型的数组,其 指针的运算规则要不要进行修:100100指针pr:指向数组aray 改呢? DS:1008 指针qtr:指向数组 array之后 假设有指针ptr,qtr以及整s:1009 型数组 array: DS: 100A 整数变量len DS: 100B int *ptr, * qtr int array [3] 图6-5指向整型数组的指针的运算 其关系如图6-5所示。 从图6-5中容易看出,如果 ptr++ 的结果仍是在指针变量pr原来的值上加1,即p的值由1000变为1001,则其作为地址来 说已经没有意义,因为地址为1001的存储单元并不是某个数组元素或变量的第一个存储单
第 6 单元 指针 - 117 - { *destin = *source; source ++; destin ++; } *destin = 0; } 分 析: 函数 mystrcpy()的功能为将一个字符串的内容复制到另一个字符型数组中 去。在复制字符串时要注意, 一定要保证目标数组确实可以放得下整个字符串。初学者最易 犯的一个错误是混淆指针与数组的概念, 写出如下的语句: char *string1 = "This is a sample."; char *string2; ... ... strcpy(string2, string1); 这时确实可以将字符串 string1 中的内容复制到从存放于指针 string2 中的地址开始的一 段内存中去。但问题是指针 string2 中存放的究竟是谁的地址? 由于没有对 string2 赋值,所 以它可能指向任何地方, 包括已经分配给其他变量、数组甚至函数的区域。向 string2 复制字 符串会覆盖这些地方原来的内容,造成各种运行错误, 包括突然死机;即使幸而指针 string2 指向一片未被使用的存储区,成功地复制了字符串, 但由于没有合法的授权, 也不能保证其 后程序不再将这片存储区域分配给其他的变量或数组, 从而造成刚刚复制的内容又被其他 数据所覆盖。 上面的例子使用了字符类 型的数组, 其特点是每个数组 元素的大小正好是一个字节。 如果使用其他类型的数组, 其 指针的运算规则要不要进行修 改呢? 假设有指针 ptr, qtr 以及整 型数组 array: int *ptr,*qtr; int array[3]; 其关系如图 6-5 所示。 从图 6-5 中容易看出, 如果 ptr++; 的结果仍是在指针变量 ptr 原来的值上加 1, 即 ptr 的值由 1000 变为 1001,则其作为地址来 说已经没有意义, 因为地址为 1001 的存储单元并不是某个数组元素或变量的第一个存储单 DS:1000 DS:1001 DS:1002 DS:1003 DS:1004 DS:1005 DS:1006 DS:1007 DS:1008 DS:1009 DS:100A DS:100B 指针ptr:指向数组array 1006 指针qtr:指向数组array之后 图6-5 指向整型数组的指针的运算 1 2 1000 0003 整数变量len 整型数组array 3
第6单元指针 元,而是存放着数组aray的第一个元素的后半部分。同样,如果表达式 len gtr-ptr 的结果为将1006-1000=6送入变量len则len也没有正确地反映出数组ara中数组元素 的数目。为了解决这一问题,C++规定,对于指针变量来说,其运算的基本单位为其指向的 数据类型的变量占用的字节数。因此,如果某指针是指向int类型变量的,由于int类型的变 量占用两个字节,所以该指针运算时的基本单位为2;如果某指针是指向foat类型的,由于 foat类型变量的长度为4个字节则该指针运算时的基本单位为4。这样可以保证对指针的 操作能正确地反映地址的变化。因此,图6-5中的 ptrtt 实际上是在ptr原来的值上加2,正好使pr指向aray的第二个数组元素;同样,语句 qtr-ptr 的结果是变量len的值为3,正好为该数组中的元素个数 例6-4编写一个函数用于将一个foa类型的数组清零(即将其所有元素全部置为0)。 算法:通过引用下标变量很容易实现数组清零的功能。但在本例中,我们采用指 编写该函数。因为数组元素的类型为 float,所以必须使用指向 float类型的指针。 程序 / Example6-4:数组清零 void clear array (float *ptr, int len) float *qtr ptr+le *ptr=0.0; ptr+t 分析:由程序中可以看出,由于C++规定指针运算的基本单位为其指向的类型变 量的长度,所以使程序设计变得相当简单。在编写程序时,如果要使指针指向下一个数组元 素,不必知道一个数组元素实际占用几个存储单元只要简单地在指针指上加1即可。而且 如果要将该函数改为对 double类型的数组清零,只要将指针类型由foat*改为 double*即可, 其他不必改动。 有时也用下标表示法引用指针指向的数组元素。例如在声明了指针变量 double x, a[100], *ptr =a 之后,也可以使用 x ptr[1o]
第 6 单元 指针 - 118 - 元, 而是存放着数组 array 的第一个元素的后半部分。同样,如果表达式 len = qtr−ptr; 的结果为将 1006−1000 = 6 送入变量 len, 则 len 也没有正确地反映出数组 array 中数组元素 的数目。为了解决这一问题, C++规定, 对于指针变量来说, 其运算的基本单位为其指向的 数据类型的变量占用的字节数。因此,如果某指针是指向 int 类型变量的, 由于 int 类型的变 量占用两个字节, 所以该指针运算时的基本单位为 2; 如果某指针是指向 float 类型的, 由于 float 类型变量的长度为 4 个字节,则该指针运算时的基本单位为 4。这样可以保证对指针的 操作能正确地反映地址的变化。因此, 图 6-5 中的 ptr++; 实际上是在 ptr 原来的值上加 2, 正好使 ptr 指向 array 的第二个数组元素; 同样, 语句 len = qtr−ptr; 的结果是变量 len 的值为 3, 正好为该数组中的元素个数。 [例 6-4] 编写一个函数用于将一个 float 类型的数组清零(即将其所有元素全部置为 0)。 算 法: 通过引用下标变量很容易实现数组清零的功能。但在本例中, 我们采用指针 编写该函数。因为数组元素的类型为 float, 所以必须使用指向 float 类型的指针。 程 序: // Example 6-4:数组清零 void clear_array(float *ptr, int len) { float *qtr = ptr+len; while(ptr<qtr) { *ptr = 0.0; ptr++; } } 分 析: 由程序中可以看出, 由于C++规定指针运算的基本单位为其指向的类型变 量的长度, 所以使程序设计变得相当简单。在编写程序时, 如果要使指针指向下一个数组元 素, 不必知道一个数组元素实际占用几个存储单元, 只要简单地在指针指上加 1 即可。而且 如果要将该函数改为对 double 类型的数组清零,只要将指针类型由 float *改为 double *即可, 其他不必改动。 有时也用下标表示法引用指针指向的数组元素。例如在声明了指针变量 double x, a[100], *ptr = a; 之后, 也可以使用 x = ptr[10];
第6单元指针 这样的用法,并不表示pt是一个数组,而只是 x=*(ptr+10) 的另一种写法 64动态存储分配 一般来说,程序中使用的变量和数组的类型、数目和大小是在编写程序时由程序员确定 下来的,因此在程序运行时这些数据占用的存储空间数也是一定的。这种存储分配方法被称 为静态存储分配。静态存储分配的缺点是程序无法在运行时根据具体情况(如用户的输入) 灵活调整存储分配情况。例如,无法根据用户的输入决定程序能够处理的矩阵的规模 C+的动态存储分配机制为克服这种不便提供了手段。动态存储分配要使用指针、运算 符new和 delete 运算符new用来申请所需的内存。其用法为 指针〉=new相适应的存储,如果 分配成功,将其首地址存入,否则置的值为NULL(空指针值,即0)。=new[ 其中中应为先前分配的存储块的地址 动态存储分配的变量和数组通过指针来访问。例如: nt x, *ptr new int (5) 使用动态存储分配时要注意几个问题。一是要确认分配成功后才能使用,否则可能造成 严重后果:二是在分配成功后不宜变动的值,否则在释放这片存储时会引起系统内存 管理混乱;三是动态分配的存储不会自动释放,只能通过 delete释放。因此要注意适时释 放动态分配的存储。例如 void funco int *ptr new int(5)
第 6 单元 指针 - 119 - 这样的用法, 并不表示 ptr 是一个数组, 而只是 x = *(ptr+10); 的另一种写法。 6.4 动态存储分配 一般来说, 程序中使用的变量和数组的类型、数目和大小是在编写程序时由程序员确定 下来的, 因此在程序运行时这些数据占用的存储空间数也是一定的。这种存储分配方法被称 为静态存储分配。静态存储分配的缺点是程序无法在运行时根据具体情况(如用户的输入) 灵活调整存储分配情况。例如, 无法根据用户的输入决定程序能够处理的矩阵的规模。 C++的动态存储分配机制为克服这种不便提供了手段。动态存储分配要使用指针、运算 符 new 和 delete。 运算符 new 用来申请所需的内存。其用法为 = new ; 或者 = new (); new 运算符从堆(管理内存中的空闲存储块)中分配一块与相适应的存储,如果 分配成功,将其首地址存入,否则置的值为 NULL(空指针值,即 0)。 用于为分配好的变量置初值。 new 运算符也可以为数组分配内存。用法为: = new []; 运算符 delete 用于释放先前申请到的存储块,并将其归还到堆中。其用法为 delete ; 其中中应为先前分配的存储块的地址。 动态存储分配的变量和数组通过指针来访问。例如: int x, *ptr = new int(5); x = *ptr; 使用动态存储分配时要注意几个问题。一是要确认分配成功后才能使用,否则可能造成 严重后果;二是在分配成功后不宜变动的值,否则在释放这片存储时会引起系统内存 管理混乱;三是动态分配的存储不会自动释放,只能通过 delete 释放。因此要注意适时释 放动态分配的存储。例如: void func() { int *ptr = new int(5);