C++学习教程及习题 第五章

更新时间:2024-04-29 12:10:02 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

教学目标

●能够使用指针

●能用指针按引用调用向函数传递参数 ●了解指针、数组与字符串之间的密切关系 ●了解指针在函数中的使用 ●能够声明和使用字符串数组

5.1 简介

本章介绍“C++编程语言一个最强大的特性——指针。指针是C++中最难掌握的问题之一。第3章介绍了引用可以用于实现按引用调用。指针使程序可模拟按引用调用,生成与操作动态数据结构,即能够伸缩的数据结构,如链表、队列、堆栈和树。本章介绍基本的指针概念,而且强调了数组、指针与字符串之间的密切关系.并包括一组很好的字符串操作练习。

第6章介绍结构中的指针使用。第9章和第10章介绍如何用指针和引用进行面向对象编程。第15章介绍动态内存管理技术以及生成和使用动态数据结构的例子。

把数组和字符串看成指针是从C语言演变而来的。本书后面会介绍把数组和字符串当作成熟的对象。

5.2 指针变量的声明与初始化

指针变量的值为内存地址。通常变量直接包含特定值,而指针则包含特定值变量的地址。因此可以说,变量名直接(directly)引用数值,而指针间接(indirectly)引用数值(如图5.1)。通过指针引用数值称为间接引用。 指针和任何其他变量一样,应先声明后使用。下列声明: int *countPtr, count;

声明变量countPtr的类型为int*(即指向整型值的指针),或者说成\是int的指针\或\指向整数类型的对象\。变量count声明为整数,而不是整型值的指针。声明中的*只适用于countPtr。

声明为指针的每个变量前面都要加上星号(*)。例如,下列声明:

float *xPtr,*yPtr;

表示xPtr和yPtr都是指向float值的指针。声明中以这种方式使用*时,它表示变量声明为指针。指针可以声明为指向任何数据类型的对象。

常见编程错误5.1

假设对指针的声明会分配到声明中逗号分隔的指针变量名列表中的所有指针变量名,从而将指针声明为非指针。声明为指针的每个变量前面都要加上星号(*)。

编程技巧5.1

尽管不是必需的,但在指针变量名中加上Ptr字样能更清楚地表示这些变量是指针,需要相应的处理。

图5.1 直接和间接引用变量

指针应在声明时或在赋值语句中初始化。指针可以初始化为0、NULL或—个地址。数值为0或NULL的指针不指任何内容。NULL是头文件(和另外几个标准库头文件)中定义的符号化常量。将指针初始化为NULL等于将指针初始化为0,但C++中优先选择0。指定0时,它变为指针的相应类型。数值0是惟一可以不将整数转换为指针类型而直接赋给指针变量的整数值。5.3节将介绍将变量地址赋给指针。

测试与调试提示5.1

初始化指针以防止其指向未知的或未初始化的内存区。

5.3 指针运算符

&(地址)运算符是个一元运算符,返回操作数的地址。例如,假设声明:

int y = 5; int *yPtr; 则下列语句: yPtr = &y;

将变量y的地址赋给指针变量yPtr。变量yPtr“指向”y。图5.2显示了执行上述语句之后的内存示

意图。图中从指针向所指对象画一个箭头.表示“指向关系”。

图5.3显示了指针在内存中的表示,假设整型变量y存放在地址600000,指针变量yPtr存放在

地址500000。地址运算符的操作数应为左值,(即要赋值的项目,如变量名).地址运算符不能用于

常量、不产生引用的表达式和用存储类regtster声明的变量。

”*”运算符通常称为间接运算符(indirection operator)或复引用运算符(dereferencing operator),

返回操作数(即指针)所指对象的同义词、别名或浑名。例如(图5.2再次引用),下列语句:

cout << * yPtr << endl; 指向变量y的值(5),如同下列语句: cout << y << endl;

图5.2 指针指向内存中整数变量的示意图

这里使用*的方法称为复引用指针(dereferencing a pointer)。注意复引用指针也可以用于赋值语句左边,例如下列语句: *yPtr = 9;

将数值9赋给图5.3中的y。复引用指针也可用于接收输入值,例如:

cin>> *yPtr; 复引用的指针是个左值。

yptr y 500000 600000 600000 5

图 5.3 指针在内存中的表示

常见编程错误5.2

如果指针没有正确地初始化或没有指定指向内存中的特定地址,则复引用指针可能造成致命的运行时错误,或者意外修改重要数据。虽然运行完程序,但得到的是错误结果。 常见编程错误5.3

复引用非指针是个语法错误。 常见编程错误5.4

复引用0指针通常是个致命的运行时错误。

图5.4的程序演示了指针运算符。本例中通过<<用十六进制整数输出内存地址(十六进制整数见附录“数值系统”)。 可移植性提示 5. 1

输出指针的格式与机器有关,有些系统用十六进制整数,而有些系统用十进制整数。

注意a的地址和aPtr的值在输出中是一致的,说明a的地址实际赋给了指针变量aptr。&和*运算符是互逆的,如果两者同时作用于aPtr,则打印相同的结果。图5.5显示了前面所介绍的运算符的优先级和结合律。 1 // Fig. 5.4: fig05_04.cpp 2 // Using the & and * operators 3 #include 4

5 int main() 6{

7 int a; // a is an integer

8 iht *aPtr; // aPtr is a pointer to an integer 9

10 a = 7;

11 aPtr = &a; // aPtr set to address of a 12

13 cout << \14 << \15

16 cout << \17 << \18

19 cout << \20 << \21 << \22 return 0; 23 }

输出结果:

The address of a is Ox0064FDF4 The value of aPtr is 0x0064FDF4 The value of a is 7 The value of *aPtr is 7

Showing that * and & are inverses of each other. &*aPtr = 0x0064FDF4 *&aPtr = 0x0064FDF4

图 5.4 &与*指针运算符

------------------------------------------------------------------------------

运算符 结合律 类型

------------------------------------------------------------------------------

() [] 右 括号

++ -- + - static_cast() 左 一元 & *

* / % 右 乘

+ - 右 加

<< >> 右 插入/读取

< <= > >= 右 关系

== != 右 相等

&& 右 逻辑AND

|| 右 逻辑或

?: 左 条件

= += -= *= /= %= 左 赋值

从左向从右向从左向从左向从左向从左向从左向从左向从左向从右向从右向 , 从左向右 逗号

------------------------------------------------------------------------------

图 5.5 运算符的优先级和结合律

5.4 按引用调用函数

C++用三种方式向函数传递数值:按值调用(call-by-value)、用引用参数按引用调用(call-by-reference reference argument)和用指针参数按引用调用(call-by-reference pointer argument)。第3章比较了按引用调用与按值调用,本章主要介绍用指针参数按引用调用。

第3章曾介绍过,return可以从被调用函数向调用者返回一个值(或不返回值而从被调用函数返回控制)。我们还介绍了用引用参数将参数传递给函数,使函数可以修改参数的原有值(这样可以从函数“返回”多个值),或将大的数据对象传递给函数而避免按值调用传递对象的开销(即复制对象所需的开销)。指针和引用一样,也可以修改调用者的一个或几个变量,或将大的数据对象指针传递给函数而避免按值调用传递对象的开销。

在C++中,程序员可以用指针和间接运算符模拟按引用调用(就像C语言程序中的按引用调用一样)。调用函数并要修改参数时,传递该参数地址,通常在要修改数值的变量名前面加上地址运算符(&)。第4章曾介绍过,数组不能用地址运算符(&)传递,因为数组名是内存中数组的开始位置(数组名等同于&arrayName[0]),即数组名已经是个指针。向函数传递参数地址时,可以在函数中使用间接运算符形成变量名的同义词、别名或浑名,并可用其修改调用者内存中该地址的

值(如果变量不用const声明)。

图5.6和5.7的程序是计算整数立方函数的两个版本cubeByValue和cubeByReference。图5.6按值调用将变量number传递给函数cubeByValue。函数cubeByValue求出参数的立方,并将新值用return语句返回main,井在main中将新值赋给number。可以先检查函数调用的结果再修改变量值。例如,在这

个程序中,可以将cubeByValue的结果存放在另一变量中,检查其数值,然后再将新值赋给number。

1 // Fig. 5,6: fig0506,cpp

2 // Cube a variable using call-by-value 3 #include 4

5 int cubeByValue( int ); // prototype 6

7 int main() 8 {

9 int number = 5; 10

11 cout << \12 number = cubeByValue( number );

13 cout << \14 return 0; 15 } 16

17 int cubeByValue( int n ) { 18

19 return n * n * n; // cube local variable n 2O }

输出结果:

The original value of number is 5 The new value of number is 125

图5.6 按值调用求出参数的立方

图5.7的程序按引用调用传递变量nunber(传递number的地址)到函数cubeByReference。函

数cubeByReference取nPtr(int的指针)作为参数。函数复引用指针并求出nPtr所指值的立方,从

而改变main中的number值。图5.8和5.9分别分析了图5.6和1.7所示程序。

1 // Fig. 5.7: fig05_07.cpp

2 // Cube a variable using call-by-reference 3 // with a pointer argument 4 #include 5

6 void cubeByReference( int* ); // prototype 7

8 int main() 9 {

10 int number = 5; 11

12 cout << \13 cubeByReference( &number );

14 cout << \15 return O; 16 } 17

18 void cubeByReference( int *nPtr ) 19 {

20 *nPtr = *nPtr = *nptr * *nptr; // cube number in main 21 }

输出结果:

The original value of number is 5 The new value of number is 125

图 5.7 用指针参数按引用调用求出参数的立方

常见编程错误5.5

要复引用指针以取得指针所指的值时不复引用指针是个错误。

接收地址参数的函数要定义接收地址的指针参数。例如,cubeByReference的函数首部如下所示:

void cubeByReference(int *nPtr)

这个函数首部指定函数cubeByReferenee接收整型变量的地址(即整型指针)作为参数,在nPtr中局部存放地址,不返回值。

cubeByReference的函数原型包含括号中的int*。和其他变量类型一样,不需要在函数原型中包括指针名。参数名仅用于程序中的说明,编译器将其忽略。 在需要单下标数组参数的函数首部和函数原型中,可以用cubeByReference参数表中的指针符号。编译器并不区分接收指针的函数和接收单下标数组的函数。当然,函数必须“知道”何时接收数组或要进行按引用调用的单个变量。编译器遇到形如int b[]的单下标数组函数参数时,编译器将参数变为指针符号int* const b(b是指向整数的常量指针),const见第51节介绍。声明函数参数为单下标数组的两种形式可以互换。 编程技巧5.2

除非调用者显式要求被调用函数修改调用者环境中参数变量的值,否则按值调用将参数传递给函数。这是最低权限原则的另一个例子。

图 5.8典型的按值调用分析

图 5.9典型的用指针参数按引用调用分析

5.5 指针与常量限定符

const限定符可以使程序员告诉编译器特定变量的值不能修改。 软件工程视点5. 1

const限定符可以执行最低权限原则。利用最低权限原则正确设计软件可以大大减少调试时间和不正确的副作用,使程序更容易修改与维护。 可移植性提示5.2

尽管ANSI C和C++中定义了const定符,但有些编译器无法正确实现。 几年来,大量C语言遗留代码都是在没有const限定符的情况下编写的。因此,使用旧版C语言代码的软件工程有很大的改进空间。许多目前使用ANSI C和C++的程序员也没有在程序中使用const限定符,因为他们是从C语言的早期版本开始编程的,这些程序员错过了许多改进软件工程的好机会。

函数参数使用或不用const限定符的可能性有六种,两种用按值调用传递参数,四种按引用调用传递参数,根据最低权限原则来进行选择。在参数中向函数提供完成指定任务所需的数据访问,但不要提供更多权限。

第3章曾经介绍,按值调用传递参数时,函数调用中要生成参数副本并将其传递给函数。如果函数中修改副本,则调用者的原值保持不变。许多情况下,需要修改传入函数的值以使函数能够完成任务。但有时即使被调用函数只是操作原值的副本,也不能在被调用函数中修改这个值。

假设函数取一个单下标数组及其长度为参数,并打印数值。这种函数应对数组进行循环并分别输出每个数组元素。函数体中用数组长度确定数组的最高下标,以便在打印完成后结束循环。在函数体中不能改变这个数组长度。 软件工程视点5.2

如果函数体中不能修改传递的值,则这个参数应声明为const以避免被意外修改。

如果试图修改const类型的值,则编译器会捕获这个错误并发出一个警告或错误消息(取决于特定的编译器)。 轶件工程视点5.3

按值调用时,只舱在调用函数中改变一个值。这个值通过函数返回值进行赋值。要在调用函数中改变多个值,就要按引用传递多个参数。 编程技巧5.3

使用函数之前,检查函数原型以确定可以修改的参数。

将指针传递给函数有四种方法:非常量数据的非常量指针、常量数据的非常量指针、非常量数据的常量指针和常量数据的常量指针。每种组合提供不同的访问权限。

最高访问权限是非常量数据的非常量指针,可以通过复引用指针而修改,指针可以修改成指向其他数据。声明非常量数据的非常量指针时不用const。这种指针可以接收函数中的字符串,用指针算法处理或修改字符串中的每个字符。图5.10中的函数convertToUppercase声明参数sPtr(char*sPtr)为非常量数据的非常量指针。函数用指针算法一次一个字符地处理字符串string。字符串中,st到,x,的字符用函数toupper变为相应的大写字母,其余字符不变。函数toupper取一个字符作为参数。

如果是小写字母,则返回相应的大写字母,否则返回原字符。函数toupper是字符处理库ctype.h中(见第16章)的一部分。

常量数据的非常量指针,指针可以修改成指向其他数据,但数据不能通过指针修改。这种指针可以接收函数的数组参数,函数处理数组每个元素而不修改数据。例如,图5.1l的函数printCharacters将参数sPtr声明为const char*类型.表示“sPtr是字符常量的指针”。函数体用for循环输出字符串中的每个字符,直到遇到null终止符。打印每个字符之后,指针sPtr递增,指向字符串中下一个宇符。

1 // Fig. 5.10: fig0510.cpp

2 // Converting lowercase letters to uppercase letters 3 // using a non-constant pointer to non-constant data 4 #include 5 #include 6

7 void convertToUppercase( char * ); 8

9 int main() 10 {

11 char string[] = \12

13 cout << \14 convertToUppercase( string );

15 cout << \16 cout << string << endl; 17 return 0; 18 } 19

20 void convertToUppercase{ char *sPtr ) 21 {

22 while ( *sPtr != '\\0' ) { 23

24 if (*sPtr >= 'a' && *sPtr <= 'z' )

25 *sPtr = toupper( *sPtr ); // convert to uppercase 26

27 ++sPtr; // move sPtr to the next character 28 } 29 }

输出结果:

The string before conversion is: characters and $32.98 The string after conversion is: CHARACTERS AND $32.98

图5.10将字符串变成大写

1 // Fig. 5.11:fig05 ll.cpp

2 // Printing a string one character at a time using 3 // a non-constant pointer to constant data 4 #include 5

6 void printCharacters( const char * ); 7

8 int main() 9 {

10 char string[] = \11

12 cout << \13 printCharacters( string ); 14 cout << endl; 15 return 0; 16 } 17

18 // In printCharacters, sPtr is a pointer to a character 19 // constant. Characters cannot be modified through sPtr 20 // (i.e., sPtr is a \21 void printCharacters( const char *sPtr ) 22 {

23 for ( ; *sPtr != '\\0'; sPtr++ ) // no initialization 24 cout << *sPtr; 25 }

输出结果: The string is:

print characters of a string

图5.11 用常量数据的非常量指针打印字符串(一次打印一个字符)

图5.12演示了函数接收常量数据的非常量指针,并试图通过指针修改数据在编译时产生的语法错误消息。

1 // Fig. 5.12: fig05_12.cpp

2 // Attempting to modify data through a 3 // non-constant pointer to constant data. 4 #include 5

6 void f( const int* ); 7

8 int main() 9 {

10 int y; 11

12 f( &y ); // f attempts illegak modification 13

14 return 0; 15 }

16

17 // In f, xPtr is a poin er to an integer constant 18 void f( const int *xPtr ) 19 {

20 *xPtr = 100; // cannot modify a const object 21 }

输出结果:

Compiling FIG05 12.CPP:

Error FIG05 12.CPP 20: Cannot modify a const object Warning FIGOS_12.CPP 21: Parameter 'xPtr' is never used

图5.12 试图通过常量数据的非常量指针修改数据

众所周知,数组是累计数据类型,用同一名称存放相同类型的相关数据项。第6章将介绍另一种形式的累计数据类型——结构(structure),也称为记录(record)。结构可以用同一名称存放不同类型的相关数据项(例如,存放公司每个员工的信息)。调用带数组参数的函数时,数组模拟按引用调用自动传递给函数。但结构则总是按值调用,传递整个结构的副本。这就需要复制结构中每个数据项目并将其存放在计算机函数调用堆栈中的执行时开销(函数执行时用函数调用堆栈存放函数调用中使用的局部变量)。结构数据要传递绐函数时,可以用常量数据的指针(或常量数据的引用)得到按引用调用的性能和按值调用对数据的保护。传递结构的指针时,只要复制存放结构的地址。在4字节地址的机器上,只要复制4字节内存而不是复制结构的几百或几千字节。 性能提示5.7

要传递结构之类的大对象时,可以用常量数据的指什(或常量数据的引用)得到按引用调用的性能和按值调用对数据的保护。

非常量数据的常量指针总是指向相同的内存地址,该地址中的数据可以通过

指针修改。这里的数组名是默认的。数组名是数组开头的常量指针,数组中的所有数据可以用数组名和数组下标访问和修改。非常量数据的常量指针可以接收数组为函数参数,该函数只用数组下标符号访问数组元素。声明为const的指针应在声明时初始化(如果是函数参数,则用传入函数的指针初始化)。图5.13的程序想修改常量指针,指针ptr的类型声明为int *const,图中的声明表示“ptr是整数的常量指针”,指针用整型变量x的地址初始化。程序要将y的地址赋给ptr,但产生一个错误消息。注意数值7赋

给*ptr时不产生错误,说明ptr所指的值是可修改的。 常见编程错误5.6

声明为const的指针不在声明时初始化是个语法错误。

常量数据的常量指针的访问权限最低。这种指针总是指向相同的内存地址,该内存地址的数据不能修改。数组传递到函数中,该函数只用数组下标符号读取,而不能修改数组。图5.14的程序演示声明指针变量ptr为const int* const,表示“ptr是常量整数的常量指针”。图中显示了修改ptr所指数据和修改存放指针变量的地址时产生的错误消息。注意输出ptr所指的值时不产生错误,因为输出语句中没有进行修改。

1 // Fig. 5.13:fig05 13.cpp

2 // Attempting to modify a constant pointer to 3 // non-constant data 4 #include 5

6 int main() 7 {

8 int x, y; 9

10 int * const ptr = &x; // ptr is a constant pointer to an 11 // integer. An integer can be modified

12 // through ptr, but ptr always points 13 // to the same memory location. 14 *ptr = 7; 15 ptr = &y; 16

17 return 0; 18 }

输出结果:

Compiling FIG05 13.CPP:

Error FIG05 13.CPP 15: Cannot modify a const object Warning FIGOS_13.CPP 18: 'y' is declared but never used

图5.13修改非常量数据的常量指针

1 // Fig. 5.141 fig05 14.cpp

2 // Attempting to modify a constant pointer to 3 // constant data. 4 #include 5

6 int main() 7 {

8 int x = 5, y; 9

10 const iht *const ptr = &x; // ptr is a constant pointer to a 11 // constant integer, ptr always 12 // points to the same location 13 // and the integer at that 14 // location cannot be modified.

15 eout << *ptr << endl; 16 *ptr = 7; 17 ptr = &y; 18

19 return 0; 20 }

输出结果:

Compiling FIG05 14.CPP:

Error FIG05_14.CPP 16: Cannot modify a const object Error FIG05_14.CPP 17: Cannot modify a const object Warning FIG05_14.CPP 20: 'y' is declared but never used

图5.14 修改常量数据的常量指针

5.6 按引用调用的冒泡排序

下面将图4.16的冒泡排序程序修改成用两个函数bubbleSort和swap(如图5.15)。函数bubbleSort进行数组排序,它调用函数swap,变换数组元素array[j)和array[j+1]记住,C++强制函数之间的信息隐藏,因此swap并不能访问bubbleSort中的各个元素。由于bubbleSort要求swap访问交换的数组元素,因此bubbleSort要将这些元素按引用调用传递给swap.每个数组元素的地址显式传递。

尽管整个数组自动按引用调用传递,但各个数组元素是标量,通常按值调用传递。因此,bubbleSort对swap调用中的每个数组元素使用地址运算符(&),如下所示的语句:

swap( &array[ j ], array[j+ 1]);

实现按引用调用。函数swap用指针变量element1Ptr接收&array[j]。由于信息隐藏,swap并不知道名称&array[j],但swap可以用*element1Ptr作为&array[j]的同义词。这样,swap引用*element1Ptr时,实际上是引用bubbleSort中的

&array[j]。同样,swap引用*element2Ptr时,实际上是引用bubbleSort中的array[j+1]。虽然swap不能用: hold = array [ j ];

array[ j ] = array[ j + 1 ]; array[ j + 1 ] = hold;

但图5.15中的swaP函数用 hold = * element1Ptr; *element1Ptr = *element2Ptr; *element2Ptr = hold; 达到相同的效果。

1 // Fig. 5.15: fig05_15.cpp

2 // This program puts values into an array, sorts the values into 3 // ascending order, and prints the resulting array. 4 #include 5 #include 6

7 void bubbleSort{ int *, const int ); 8

D int main(} l0 {

11 const int arraySize = 10;

12 int a[ arraySize ) = { 2, 6, 4, 8, 10, 12, 89, 68, 45, 37 }; 13 int i; 14

15 cout << \16

17 for ( i = 0; t < arraySize; i++ )

18 cout << setw( 4 ) << a[ i ]; 19

20 bubbleSort( a, arraySize ); // sort the array 21 cout << \22

23 for ( i = 0; i < arraySize; i++ ) 24 cout << setw( 4 ) << a[ i ]; 25

26 cout << endl; 27 return 0; 28 } 29

30 void bubbleSort( int *array, const int size ) 31 {

32 void swap( int *, iht * ); 33

34 for (int pass = 0; pass < size - 1; pass++ ) 35

36 for (int j = 0; j < size - 1; j++ ) 37

38 if ( array[ j ] > array[ j + 1 ] ) 39 swap( &array[ j ], &arra[ j + 1 ] ); 4O } 41

42 void swap( int *element1Ptr, int *element2Ptr ) 43 {

44 int hold = *elementlPtr; 45 *element1Ptr = *element2Ptr; 46 *element2Ptr = hold;

47 }

输出结果:

Data item: in Original Order

2 6 4 8 10 12 89 68 45 37 Data items in ascendinq Order

2 4 6 8 lO 12 37 45 68 89

图5.15 按引用调用的冒泡排序

注意函数bubbleSort中的几个特性。函数首部中将array声明为int* array而不是int array[],表示bubbleSort接收单下标数组作为参数(这些符号是可以互换的)。参数size声明为const以保证最低权限原则。尽管参数size接收main中数值的副本,且修改该副本并不改变main中的值,但是bubbleSort不必改变size即可完成任务。bubbleSort执行期间数组的长度保持不变,因此,size声明为const以保证不被修改。如果排序过程中修改数组长度,则排序算法无法正确执行。

bubbleSort函数体中包括了函数swap的原型,因为它是调用swap的惟一函数。将原型放在bubbleSort中,使得只能从bubbleSort正确地调用swap。其他函数要调用swap时无法访问正确的函数原型,这通常会造成语法错误,因为C++需要函数原型。

软件工程视点5.4

将函数原型放在其他函数中能保证最低权限原则,只能从该原型所在函数中正确地调用。

注意函数bubbleSort接收数组长度参数。函数必须知道数组长度才能排序数组。数组传递到函数时,函数接收数组第一个元素的内存地址。数组长度要单独传递给函数。

通过将函数bubbleSort定义成接收数组长度作为参数,可以让函数在排序

任何长度单下标整型数组的程序中使用。

软件工程视点5.5

向函数传递数组时,同时传递数组长度(而不是在函数中建立数组长度信息),这样能使函数更加一般化,以便在许多程序中复用。

数组长度可以直接编程到函数内,这样会把函数的使用限制在特定长度的数组并减少其复用性。程序中只有处理特定长度的单下标整型数组时才能使用这个函数。

C++提供一元运算符sizeof,确定程序执行期间的数组长度或其他数据类型长度(字节数)。采用数组名时(如图5.16所示),sizeof算符返回数组中的总字节数为size_t类型的值,通常是unsigned int类型。这里使用的计算机将float类型的变量存放在4字节内存中,array声明为20个元素,因此array使用80字节内存空间。在接收数组参数的函数中采用指针参数时,sizeof运算符返回指针长度的字节数(4)而不是数组长度。

常见编程错误5. 7

在函数中用sizeof运算符寻找数组参数长度的字节数时返回指针长度的字节数而不是数组长度的字节数。

1 // Fig. 5.16: fig05_16.cpp

2 // Sizeof operator when used on an array name 3 // returns the number of bytes in the array. 4 #include 5

6 size_t getSize( float * ); 7

8 int main() 9 {

10 float array[ 20 ]; 11

12 cout << \13 << sizeof( array )

14 << \15 << getSize( array ) << endl; 16

17 return 0; 18 } 19

20 size_t getSize( float *ptr ) 21 {

22 return sizeof( ptr ); 23 }

输出结果:

The number of bytes in the array is 80 The number of bytes returned by getSize is 4

图5.16 采用数组名时,sizeof运算符返回数组中的总字节数

数组中的元素个数也可以用两个sizeof操作的结果来确定。例如,考虑下列数组声明:

double realArray[ 22 ];

如果double数据类型的变量存放在8个字节的内存中,则数组realArray总共包含176个字节。要确定数组中的元素个数,可以用下列表达式: sizeof realArray/sizeof(double)

这个表达式确定realArray数组中的字节数.并将这个值除以内存中存放—个

double值的字节数,图5.17的程序用size运算符计算我们所用的个人计算机上存放每种标准数据类型时使用的字节数。 可移植性提示 5.3

存放特定数据类型时使用的字节数随系统的不同而不同。编写的程序依赖于数据类型长度而且要在几个计算机系统上运行时,用sizeof确定存放这种数椐类型时使用的宇节数。

1 // Fig. 5.17:fig05 17.cpp

2 // Demonstrating the sizeof operator 3 ~include 4 ~include 5

76 ~nt main(} 8 char c; 9 short s; 10 iht i; 11 long 1; 12 float f; 13 double d; 14 long double ld;

15 int arras 20 ], *ptr = array; 16

17 cout << \

18 << \19 << \

20 << \2I \22 << \23 \

24 << \25 << \27 \

28 << \29 << \

30 << \31 << \32 << \33 << endl; 34 return 0; 35 }

输出结果:

sizeof c = 1 sizeof(char) = 1 sizeof s = 2 sizeof(short) = 2 sizeof i = 4 sizeof(int) = 4 sizeof l = 4 sizeof(long) = 4 sizeof f = 4 sizeof(float) = 4 sizeof d = 8 sizeof(double) = 8 sizeof ld = 8 sizeof(long double) = 8 sizeof array = sizeof ptr = 4

图 5.17 用sizeof运算符计算存放每种标准数据类型时使用的字节数

sizeof运算符可以用于任何变量名、类型名或常量值。用于变量名(不是数组名)和常量值时,返回存放特定变量或常量类型所用的字节数。注意,如果提供类型名操作数,则sizeof使用的括号是必需的;如果提供变量名操作数,则

sizeof使用的括号不是必需的。记住,sizeof是个运算符而不是个函数。 常见编程错误5.8

如果提供类型名操作数,而不在sizeof操作中使用括号则是个语法错误。 性能提示5.2

sizeof属于编译时的一元运算符,而不是个执行时函数。这样,使用sizeof并不对执行性能遣成不良影响。

5.7 指针表达式与指针算法

指针是算术表达式、赋值表达式和比较表达式中的有效操作数。但是,通常并不是这些表达式中使用的所有运算符都在指针变量中有效。本节介绍可以用指针操作数的运算符及这些运算符的用法。

只有少量操作可以对指针进行。指针可以自增(++)或自减(--),整数可以加进指针中(+或+=),也可以从指针中减去整数(-或-=),指针可以减去另一指针。 假设声明了数组int v[5],其第一个元素位于内存地址3000。假设指针vPtr已经初始化为指向数组v[0],即vPtr的值为3000。图5.18演示了在32位的机器中的这种情况。注意vPtr可以初始化为数组v的指针,如下所示: vPtr = v; vPtr = &v[ 0 ]

图5.18 数组v和指向v的指针变量vPtr 可移植性提示5.4

如今的大多数计算机都是16位或32位,有些较新的计算机用8字节整数。由于指针算法的结果取决于指针所指对象的长度,因此指针算法与机器有关。 按照传统算法,3000+2得到3002。而指针算法通常不是这样。将指针增加或减去一个整数时,指针并不是直接增加或减去这个整数,而是加上指针所指对象长度的这个倍数。这些字节数取决于对象的数据类型。例如,下列语句:

vPtr += 2;

在用4字节内存空间存储整数时得到的值为3008(3000+2*4)。对数组v,这时vPtr指向v[2]如图5.19。如果用2字节内存空间,则上述结果得到

3004(3000+2*2)。如果数组为不同数据类型,则上述语句将指针递增指针所指对象长度的2倍。对字符数组进行指针算法时,结果与普通算法相同,因为每个字符的长度为一个字节。

图 5.19经过指针运算之后的vPtr 如果vPtr递增到3016,指向v[4],则下列语句: vptr -= 4;

将vPtr复位为3000,即数组开头。如果指针加1或减1,则可以用自增(++)和自减(--)运算符。 下列语句: ++vptr; vPtr++;

将指针移到数组中的下一个位置。下列语句: --vPtr; vPtr --;

将指针移到数组中的前一个位置。

指针变量还可以相减。例如,如果vPtr包含地址3000,v2Ptr包含地址3008,则下列浯句:

x = v2Ptr - vPtr;

将x指定为vPtr到v2Ptr的元素个数,这里为2。指针算法只在对数组进行时才有意义。我们不能假设两个相同类型的变量在内存中相邻的地址存放,除非它们是数组的相邻元素。

常见编程错误 5.9

对于不引用数组值的指针采用指针算法通常是个逻辑错误。 常见编程错误5.10

将两个不引用同一数组的指针相加或相减通常是个逻辑错误。 常见编程错误5.11

使用指针算法时超过数组边界通常是个逻辑错误。

如果两个指针的类型相同,则可以将一个指针赋给另一个指针。否则要用强制类型转换运算符将赋值语句右边的指针值转换为赋值语句左边的指针值。这个规则的例外是void的指针(即void),该指针是个一般性指针,可以表示任何指针类型。所有指针类型都可以赋给void指针而不需要类型转换。但是,void指针不能直接赋给另一类型的指针,而要先将void指针转换为正确的指针类型。 void*指针不能复引用。例如,编译器知道int指针指向32位机器中的4字节内存,但void指针只是包含未知数据类型的内存地址,指针所指的字节数是编译器所不知道的。编泽器要知道数据类型才能确定该指针复引用时的字节数。对于void指针,无法从类型确定字节数。

常见编程错误5.12

除了void*类型外,将一种类型的指针赋给另一种类型的指针而不先将一种类型的指针转换为另一种类型的指针是个语法错误。 常见编程错误5.13

复引用void*指针是个语法错误。

指针可以用相等和关系运算符比较,但这种比较只在对相同数组成员进行时才有意义。指针比较是对指针存放的地址进行比较。例如,比较指向同一数组的两个指针可以表示一个指针所指的元素号比另一个指针所指的元素号更高。指针比较常用于确定指针是否为0。 5.8 指针与数组的关系

C++中指针与数组关系密切,几乎可以互换使用。数组名可以看成常量指针,指针可以进行任何有关数组下标的操作。

编程技巧5. 4

操作数组时用数组符号而用指针符号。尽管程序编译时间可能稍长一些.但程序更加清晰。

假设声明了整数数组b[ 5 ]和整数指针变量bPtr。由于数组名(不带下标)是数组第一个元素的指针.因此可以用下列语句将bPtr设置为b数组第一个元素的地址: bPtr = b;

这等于取数组第一个元素的地址,如下所示: bPtr=&b[ O ];

数组元素b[3]也可以用指针表达式引用: *( bPtr + 3 )

上述表达式中的3是指针的偏移量(offset)。指针指向数组开头时,偏移量表示要引用的数组元素,偏移量值等于数组下标。上述符号称为指针/偏移量符号(pointer/offset notation)。括号是必需的,因为*的优先顺序高于+的优先顺序。如果没有括号,则上述表达式将表达式*bPtr的值加上3(即3加到b[0]中,假设bPtr指向数组开头)。就像数组元素可以用指针表达式引用一样,下列地址: &b[ 3 ]

可以写成指针表达式: bPtr + 3

数组本身可以当作指针并在指针算法中使用。例如,下列表达式: *( b + 3)

同样引用数组元素b[3]。一般来说,所有带下标的数组表达式都可以写成指针加偏移量,这时使 用指针/偏移量符号,用数组名作为指针。注意,上述语句不修改数组名,b还是指向数组中第一个元素指针和数组一样可以加下标。例如,下列表达式: bPtr[ 1 ]

指数组元素b[1].这个表达式称为指针/下标符号(pointer/subscript notation)。

记住,数组名实际上是个常量指针,总是指向数组开头。因此下列表达式: b += 3

是无效的,因为该表达式试图用指针算法修改数组名的值。 常见编程错误5.14

尽管数组是指向数组开头的S针,而指针可以在算术表达式中修改,但数组名不可以在算术表达式中修改,囚为数组名实际上是个常量指针。

图5. 20的程序用我们介绍的四种方法引用数组元素(数组下标、用数组名作为指针的指针/偏移量符号、指针下标和指针的指针/偏移量符号,打印数组的的4个元素)。

要演示数组和指针的互换性,还可以看看程序5.21中的两个字符串复制函数copy1和copy2。

这两个函数都是将字符串复制到字符数组中。比较copy1和copy2的函数原型可以发现,函数基本相同(由于数组和指针具有互换性)。这些函数完成相同的任务,但用不同方法实现。

1 // Fig. 5.20: f~g05_20.cpp

2 // Using subscripting and pointer notations with arrays 3

4 #include 5

6 int main() 7{

8 int b[] = { 10, 20, 30, 40 } ;

9 int *bPtr = b; // set bPtr to point to array b 10

11 cout << \12 << \13

14 for(int i = 0; i < 4; i++ ),

15 cou << \16 17

18 cout << \19 << \2O

21 for (int offset = 0; offset < 4; offset++ ) 22 cout << \23 << *( b + offset ) << '\\n'; 24 25

26 cout << \28 for ( i = 0; i < 4; i++ )

29 cout << \31 cout << \32

33 for ( offset = 0; offset < 4; offset++ ) 34 cout << \35 << * ( bPtr + offset ) << '\\ n'; 36

37 return 0; 38 }

输出结果:

Array b Printed with: Array subscript notation Pointer/offset notation where the pointer is the array name * (b + 0) = 10

* (b + 1) = 20 * (b + 2) = 30 * (b + 3) = 40

Pointer subscript notation bPtr[ 0 ] = 10 bPtr[ 1 ] = 20 bPtr[ 2 ] = 30 bPtr{ 3 ] = 40

Pointer/offset notation *(bPtr + 0) = 10 *(bPtr + 1) = 20 *(bPtr + 2) = 30 *(bPtr + 2) = 40

图5.20 用我们介绍的四种方法引用数组元素

1 // Fig. 5.21: figOS_21.cpp

2 // Copying a string using array notation 3 // and pointer notation. 4 #include 5

6 void copy1( char *, const char * ); 7 void copy2( char *, const char * ); 8

9 int main() 10 { 11

12 string3[ 10 ], string4[] = \13

14 copy1( string1, string2 );

15 cout << \16

17 copy2( string3, string4 );

18 cout << \19

20 return 0; 21 } 22

23 // copy s2 to sl using array notation 24 void copy1( char *s1, const char *s2 ) 25 {

26 for ( int i = 0; ( s1[ i ] = s2[ i ] ) != '\\0'; i++ ) 27 ; // do nothing in body 28 } 29

30 // copy s2 to sl using pointer notation 31 void copy2( char *s1, const char *s2 ) 32 {

33 for ( ; ( *s1 = *s2 ) != '\\0'; s1++, s2++ ) 34 ; // do nothing in body 35 }

输出结果: string1 = Hello string3 = Good Bye

图5.21 使用数组和指针符号复制字符串

函数copy1用数组下标符号将s2中的字符串复制到字符数组s1中。函数声明一个作为数组下标的整型计数器变量i。for结构的首部进行整个复制操作,而for结构体本身是个空结构。首部中指定i初始化为0,并在每次循环时加1。for的条件“(s1[i]=s2[i])!='\\0',从s2向s1一次一个字符地进行复制操作。遇到s2中的null终止符时,将其赋给s1,循环终止,因为null终止符等于'\\0'。 记住.赋值语句的值是赋给左边参数的值。

函数copy2用指针和指针算法将s2中的字符串复制到s1字符数组。同样是在for结构的首部进行整个复制操作.首部没有任何变量初始化。和copy1中一样,条件(*s1=*s1)!='\\0'进行复制操作。

复引用指针s2,产生的字符赋给复引用的指针s1。进行条件中的赋值之后,指针分别移到指向s1数组的下一个元素和字符串s2的下一个字符。遇到s2中的null终止符时,将其赋给s1,循环终止。

注意copy1和copy2的第一个参数应当是足够大的数组,应能放下第二个参数中的字符串,否则可能会在写人数组边界以外的内存地址时发生错误。另外,注意每个函数中的第二个参数声明为const char*(常量字符串)。在两个函数中,第二个参数都复制到第一个参数,一次一个地从第二个参数复制字符,但不对字符做任何修改。因此,第二个参数声明为常量值的指针,实施最低权限原则。两个函数都不需要修改第二个参数,因此不向这两个函数提供修改第二个参数的功能。

5.9 指针数组

数组可以包含指针,这种数据结构的常见用法是构成字符串数组,通常称为字符串数组(stringarray)。字符串数组中的每项都是字符串,但在C++中,字符串实际上是第一个字符的指针。因此,字符串数组中的每项实际上是字符串中第一个字符的指针。下列字符串数组suit的声明可以表示一副牌:

char‘*suit[ 4 ] = { \,\,\,\; 声明的suit[4]部分表示4个元素的数组。声明的char*部分表示数组suit的每个元素是char类型的指针。数组中的4个值

为”Hearts'’、”Diamonds”、”Clubs”和”Spades”。每个值在内存中存放

成比引号中的字符数多一个字符的null终上字符串。4个字符串长度分别为7、9、6、7。尽管这些字符串好像是放在suil数组中,其实数组中只存放指针(如图5.22)。每个指针指向对应字符串中的第一个字符。这样,尽管:suit数组是定长的,但可以访问任意长度的字符串,这是C++强大的数据结构功能所带来的灵活性。

图5. 22 suit数组的图形表示

suit字符串可以放在双下标数组中,每一行表示一个suit,每一列表示suit名的第一个字符、这种数据结构每一行应有固定列数,能够放下最长的字符串。因此,存放大量字符串而大部分字符串长度均比最长字符串短许多时,可能浪费很多内存空间。我们将在下一节用字符串数组帮助整理一副牌。

5.10 实例研究:洗牌与发牌

本节用随机数产生器开发一个洗牌与发牌程序。这个程序可以用于实现玩某种牌的游戏程序。

为了解决一些微妙的性能问题,我们故意用次优洗牌与发牌算法。练习中要开发更有效的算法。

利用自上而下逐步完善的方法,我们开发一个程序,洗52张牌并发52张牌。自上而下逐步完善的方法在解决大而复杂的问题时特别有用。

我们用4 x 13的双下标数组deck表示要玩的牌(如图5.23)。行表示花色,0表示红心,1表示方块,2表示梅花,3表示黑桃。列表示牌的面值,0到9对应A到10,10到12对应J、Q、K。我们要装入字符串数组suit,用字符串表示4个花色,用字符串数组face的字符串表示13张牌的面值。

这堆牌可以进行如下的洗牌:首先将数组deck清空,然后随机选择row(0--3)和column(0—12)。将数字插入数组元素deck[row][column](表示这个牌是洗出的牌中要发的第一张牌)。继续这个过程,在deck数组中随机插入数字2、3、?52,

表示洗出的牌中要发的第二、三、?、五十二张牌。在deck数组填上牌号时,一张牌可能选择两次,即选择的时候deck[row][column]为非0值。 忽略这个选择,随机重复选择其他row和colunm,直到找出未选择的牌。最后,在52个deck元素中插入1到52的值。这时,就完全洗好了牌。

图5.23 双下标数组deck表示要玩的牌

这个洗牌算法在随机重复选择已经洗过的牌时可能需要无限长的时间。这种现象称为无穷延迟(indefinite postponement)。练习中将介绍更好的洗牌算法,消陈无穷延迟。

性能提示5.3

有时自然方式的算法可能包含无穷延迟等微妙的性能问题,应寻找能避免无穷延迟的算法。

要发第一张牌,我们要寻找匹配1的deck[row][column]元素,这是用嵌套for结构进行的,n,w取。到3t column取。到12。这个数组元素对应哪种牌呢?suit数组预先装入了四种花色,因此要取花色,只要打印字符串suit[row];同样,要取牌值,只要打印字符串face[column]还要打印字符串”of\的顺序打印,即可得到每张牌如”King of Clubs\of Diamonds',等等。 下面用自上而下逐步完善的方法进行。顶层为: Shuffle and deal 52 cards

第一步完善结果为:

Initialize the suit array Initialize the face array Initialize the deck array

Shuffle the deck Deal 52 cards

”Shumelhedeck”可以展开成: For each of the 52 cards

Place card number in randomly selected unoccupied slot of deck \可以展开成: For each of the 52 cards

Find card number in deck array and print face and suit of card

合在一起得到第二步完善结果为: Initialize the suit array Initialize the face array Initialize the deck array For each of the 52 cards

Place card number in randomly selected unoccupied slot of deck For each of the 52 cards

Find card number in deck array and print face and suit of card \可以展开成:

Choose slot of deck randomly

While chosen slot of deck has been previously chosen Choose slot of deck randomly

Place card number in chosen slot of deck

\可以展开成:

For each slot of the deck array If slot contains card number

Print the face and suit of the card

合在一起得到第三步完善结果为: Initialize the suit array Initialize the face array Initialize the deck array For each of the 52 cards

Choose slot of deck randomly

While slot of deck has been previously chosen Choose slot of deck randomly

Place card number in chosen slot of deck For each of the 52 cards

For each slot of deck array

If slot contains desired card number Print the face and suit of the card

这样就完成了完善过程。注意,如果将洗牌与发牌算法组合成每张牌在放到牌堆上时进行发牌,则这个程序能更加有效。我们选择分别编程这些操作,因为通常是先洗后发,而不是边洗边发。

图5.24显示了洗牌与发牌程序,图5.25显示了示例执行结果。注意函数deal中使用的输出格式:

cout << setw( 5 ) << setiosflags( ios::right ) << wFace[ column ] << \

<< setw( 8 ) << setiosflags( ios::left ) << wSuit[ row ]

<< (card % 2 ==0? '\\n': '\\t');

上述输出语句使牌的面值在5个字符的域中右对齐输出,而花色在8个字符的域中左对齐输出。输出打印成两列格式。如果输出的牌在第一列,则在后面输出一个制表符,移到第二列,否则输出换行符。

1 // Fig. 5.24: fig05_24.cpp

2 // Card shuffling and dealing program 3 #include 4 #include 5 #include 6 #include 7

8 void shuffle( iht [][ 13 ] );

9 void deal( const int [][ 13 ], const char *[], const char *[] ); 10

11 int main() 12 {

13 const char * suit[ 4 ] =

14 { \15 const char * face[ 13 ] =

16 { \17 \

18 \19 int deck[ 4 ][ 13 ] = { 0 } ; 20

21 srand( time( 0 ) ); 22

23 shuffle( deck );

24 deal( deck, face, suit ): 25

26 return 0; 27 } 28

29 void shuffle( int wDeck[ ][ 13 ] )

本文来源:https://www.bwwdw.com/article/wjsg.html

Top