C++程序设计案例实训教程第5章

5章  指针与引用

C++语言提供了两种低级的复合类型——数组和指针。数组和指针的区别在于数组的长度是固定的,数组一经创建,就不允许超出设定的长度来添加新的元素;而指针则可以像迭代器一样用于遍历和检查数组中的元素。不过编写C++程序应尽量使用vector迭代器类型,避免使用低级的数组和指针。在强调速度时,才在类实现的内部使用数值和指针。指针的概念对于初学者来说,一时难以理解,实际上指针可以这样理解是用于指向对象的类型,是指向内存的地址。

本章重点介绍指针的特性,如指向数组的指针、指向指针的指针、指向结构的指针;指针的应用,如指向内存数据之间的相互复制;同时还演示了错误地释放指针、野指针在使用上需注意的问题,通过这些实例,充分理解C++指针的特性。

5.1  指针的定义与使用

案例5-1  病毒特征码定位器(文件划分)

【案例描述】

传统的杀毒软件依靠特征码来识别病毒。比如有3个文件,内容分别是“我是动物”、“我是蔬菜”、“我是病毒”。用“病毒”作为特征码来扫描这3个文件,得到的结论自然是:第三个文件是病毒。本例通过特征码来识别病毒,效果如图5-1所示。

 

5-1  病毒特征码定位器(文件划分)

【实现过程】

每次recurse()函数被调用时,当前处理的区间划分为[low,high]。如果这一区间足够大,就将它分成若干个小区间,每个小区间对应一个临时文件,文件中相应区间的部分被擦除。生成这些临时文件后,需要运行一次杀毒软件,对它们进行处理;接着脚本会在删除的同时判断这些文件在杀毒之后是否仍然存在。如果存在,说明该文件被“免疫”,将对应的区间加入到待处理队列中。删除完所有的临时文件后再处理队列中的区间。代码如下:

int _tmain(int argc, _TCHAR* argv[])

{

int iReNum = 0; //分段数目

int iFileSize = 0; //文件大小

DWORD dbFirstSecBase; //第一个区块的地址

CHAR* pPath = "239\\239.exe"; //被定位的文件路径

int High; //高地址

int Low; //低地址

int NumOfFeat; //特征码数

CHAR* pBaseFile; //文件内存临时文件

DWORD NumOfRWD;

CHAR cShowFea;

CHAR* pBasePoint;

CHAR* szFileName = "239\\239.exe"; //要定位的文件

CHAR* pPathName = "239\\";  //定位文件的根目录

//打开文件

HANDLE hFile = CreateFile(szFileName,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,

NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

pBasePoint = GetBasePoint(hFile);

//不是PE文件,则提示错处并退出

if (!IsPEFile(pBasePoint))

{

cout<<"不是PE文件!";

CloseHandle(hFile);

return FALSE;

}

cout<<"输入分段数目:";

cin>>iReNum;

iFileSize = GetFileSize(hFile,NULL);   //获取文件大小

if (iFileSize == 0)

{

cout<<"获取文件大小失败!"<

CloseHandle(hFile);

return FALSE;

}

//生成内存临时文件,起始地址为pBaseFile

pBaseFile = new CHAR[iFileSize+1]; //最后结束位为‘/0

ReadFile(hFile,pBaseFile,iFileSize,&NumOfRWD,NULL);

High = iFileSize;

Low = GetFirstSection(pBasePoint);

//创建目录文件夹OUT_FILE

//调用定位函数Location()

if (!Location(pBaseFile,pPathName,iFileSize,Low,High,iReNum))

{

CloseHandle(hFile);

return FALSE;

}

//没有特征码,退出

if (stNumberOfFeat <= 0)

{

cout<<"没有特征码!"<

CloseHandle(hFile);

return 0;

}

//有特征码,显示特征码

cout<<"定位到的特征码:"<

for (int i=0;i<=stNumberOfFeat;i++)

{ wsprintf((TCHAR*)(&cShowFea),_T("%d_%d"),g_pFeature[i][LOW],g_pFeature[i][HIGH]);

cout<

}

//进行精确定位模块

cout<<"是否进行精确定位特征码?(Y/N)"<

if(char ch=getchar()!='Y')  {  return 0;  }

getchar();

NumOfFeat = stNumberOfFeat;

stNumberOfFeat = -1; //特征码数目清空

    for ( i=0;i<=NumOfFeat;i++) {

//从第一个区块开始填充0到文件结尾

memset(pBaseFile,0,iFileSize);

//读入从起始地址到第一个区块数据

ReadFile(hFile,pBaseFile,High-Low,&NumOfRWD,NULL);  

//从特征码数组中,读入特征码处数据

SetFilePointer(hFile,Low,NULL,FILE_BEGIN);

int iBlockSize = ceil((DOUBLE)(High-Low)/iReNum);

CHAR* cBuffer = new CHAR[iBlockSize];

ReadFile(hFile,cBuffer,iBlockSize,&NumOfRWD,NULL); //读取特征码到缓冲区

Low = g_pFeature[i][LOW]; //确定新缓冲区的定位区间的LowHigh

High = g_pFeature[i][HIGH];

strncpy(&pBaseFile[Low],cBuffer,iBlockSize); //得到区块只有一个特征码的文件内存缓冲区

cout<<"输入要细分的区段数目:";

cin>>iReNum;

if (!Location(pBaseFile,pPathName,iFileSize,Low,High,iReNum))

{

cout<<"精确定位出错!"<

return FALSE;  }  }

//显示特征码

for (i=0;i<=stNumberOfFeat;i++)

{ wsprintf((TCHAR*)(&cShowFea),_T("%d_%d"),g_pFeature[i][LOW], g_pFeature[i][HIGH]);

cout<<"定位到的特征码:"<

cout<

system("pause");

return 0;  }

【案例分析】

1)特征码储存在病毒库里。随着病毒库的体积越来越大,杀毒软件的“误报”现象也越来越严重。杀毒软件就像一个黑盒,输入一个个文件,输出它们“是否为病毒”。病毒库通常是加密的,从而避免自己公司的特征码被别的公司获得。许多人为了避免自己的软件被误报成病毒,常常采取加壳,甚至混淆整个文件等暴力的做法。一般定位特征码是二分搜索。

2PE文件称为可移植的执行体,全称为Portable Execute。常见的EXEDLLOCXSYSCOM文件都是PE文件,PE文件是微软Windows操作系统上的程序文件,可能是间接被执行,如DLL。程序中有几个函数是操作文件的如:CreateFile()打开文件;WriteFile()写入文件数据;ReadFile()读入从起始地址到第一个区块数据;GetFileSize()获取文件大小。

 

注意:在实践中,特征码的选取往往非常讲究。选取的恰当,可以用最短的片段有效地识别病毒;选取的不好,则容易造成“误报”或“漏杀”。

5.2  指针运算

案例5-2  A段内存复制到B段内存(指针内存
复制)

【案例描述】

写一个函数实现内存中的内容从一个存储区域移动到另一个存储区域,如直接写成*dst++ = *srC++虽能编译和运行,常需要把程序的健壮性和兼容性考虑进去,就要加入各种判断和转换,这样编写的代码才能安全并正确地运行。本例效果如图5-2所示。

 

5-2  A段内存复制到B段内存(指针内存复制)

【实现过程】

下面为指针变量赋值一个字符串,然后输出一个字符串。字符串的长度定义为count。代码如下:

#include

#include

#include

void * MyMemMove(void *dst,const void *src,int count)

//源地址和目的地址都用void*,源地址长度

{

assert(dst); //诊断

assert(src);

void * ret = dst; //定义指针指向目的地址

if (dst <= src || (char *)dst >= ((char *)src + count)) {

//判断源地址和目的地址的大小,低地址开始复制

while (count--) { //源地址长度减1

*(char *)dst = *(char *)src; //源地址字符赋给目的地址

dst = (char *)dst + 1;

src = (char *)src + 1;

}

}

else { //从高地址开始复制

dst = (char *)dst + count - 1;

src = (char *)src + count - 1;

while (count--) {

*(char *)dst = *(char *)src;

dst = (char *)dst - 1;

src = (char *)src - 1;

}

}

return(ret);

}

int main()

{

char p1[256] = "hello,world!"; //定义字符数组

char p2[256] = {0}; //定义字符数组

MyMemMove(p2,p1,strlen(p1)+1); //调用指针内存复制函数

cout<<"移动后的值为:"< // 0

system("pause");

return 0;

}

【案例分析】

1void *为指针强制转换,返回值为void *assert用于诊断;语句if (dst <= src…),判断源地址和目的地址的大小,决定是从高地址开始复制还是从低地址开始复制,两种复制指针分别为加1和减1

2)主函数定义两个数组p1p2,然后调用函数MyMemMove()一字节一字节地复制,最后输出结果

 

注意:指针是指向内存地址的变量,知道地址后,就可以一字节一字节地复制,编程中,void *可灵活指向各种数据类型。

案例5-3  将内存的数据倒转过来(指针内存复制+算法)

【案例描述】

实例081介绍的是把内存中的数据直接复制过来,如果输出的内存数据要倒转过来输出,即输入“My name is Jike”,则输出为“ekiJ si eman yM”,这就需稍微改变一下代码,同时要用到算法。本例效果如图5-3所示。

 

5-3  将内存的数据倒转过来(指针内存复制+算法)

【实现过程】

1)下列代码中第二个函数reverse()实现字符串倒转的功能,但单词保持正序,函数中调用逆序输出函数,也就是第一个函数reverse()。其代码如下:

void reverse(char *beg, char *end){//输入开始字符串和结束字符串

// cout<<"beg="<

// cout<<"end="<

    char c;

    while(beg < end){

        c = *beg;     //交换字符串

        *beg = *end;

        *end = c;

        beg++; //输入开始字符串指针加1

        end--; //输入结束字符串指针减1

    }

}

void reverse(char * src){ //输入的为一字符串

    if(!src) return; //空串返回

    int len = strlen(src);

    reverse(src, src + len - 1);

 

    char *p, *q;

    p = src; //指向源字符串

    while(*p){

        if(isspace(*p)){ //为空格字符

            p++;

            continue;

        }

        q = p + 1;

        while(*q && *q !=' ') //得到一个单词

            q++;

        reverse(p, q - 1);//

        p = q;

    }

}

2演示2实现字符串的倒转,单词保持正序,演示1就是输入字符串后逆序输出,代码如下:

 i int main()

{

    //指向常量的字符串

    char src[] = {"Hello World! My name is Jike!"};

    int len = strlen(src);

/*                               //下列代码为演示1

    reverse(src, src + len - 1);

    cout<<"数据倒转演示1"<

    system("pause");

*/

    reverse(src)                 ;//下列代码为演示2

    cout<<"数据倒转演示2"<

    system("pause");

    return 0;

}

【案例分析】

1src + len – 1表示存储最后一个字符,第一个函数reverse()取最后一个字符,然后指针减1,再取字符,这与栈的操作类似。

2)第二个函数reverse()调用第一个函数reverse()逆序处理后,取得最后一个单词,如“ekiJ”,对这个单词再逆序,也就是第一个函数reverse()逆序处理后,得到“Jike”,这样反复处理得到演示2的结果。

 

注意:指针也就是存储在内存中的地址,它在处理方法上是很灵活的,通过适当的算法,可模拟链表、栈操作。

案例5-4  将数据隐藏于内存(自定义数据访问规则)

【案例描述】

本例定义一个指针变量临时分配元素所需的内存空间,也就是堆内存,然后对堆内存中的数据进行访问或其他操作。本例通过递归算法取得最大子段,效果如图5-4所示。

 

5-4  将数据隐藏于内存(自定义数据访问规则)

【实现过程】

1)通过递归算法,从中间分成两段,取左右段相邻的子段之和,其代码如下:

int MaxSubSum(int a[],int left,int right)//输入元素,左边个数,元素的个数

{

int sum=0;

if(left==right) sum=a[left]>0?a[left]:0;

else

{

int center=(left+right)/2;

int leftsum=MaxSubSum(a,left,center);//递归,取左边进行MaxSubSum

int rightsum=MaxSubSum(a,center+1,right);//递归,取右边进行MaxSubSum

int s1=0; //初始化子段和

int lefts=0; //初始化左边子段和

for(int i=center;i>=left;i--)  //循环看左边子段和是否大于子段和

{

lefts+=a[i];

if(left>s1) s1=lefts;

}

int s2=0;

int rights=0;

for(int j=center+1;j<=right;j++)

{

rights+=a[j];

if(rights>s2) s2=rights;

}

sum=s1+s2; //把左右两边字段和相加

if(sum

if(sum

}

return sum;

}

2)主程序分配元素堆内存中,然后求最大子段和。其代码如下:

void main()

{

int arrSize; //元素的个数

int * arr,i,ss;

cout<<"Please enter the number of array elements:";

cin>>arrSize; //临时分配元素的个数

arr=new int[arrSize]; //临时分配这些元素所需的内存空间(堆内存中)

//判断,堆空间不够分配时,系统会返回一个空指针值NULL

if(arr!=NULL) {

cout<<"\nPlease enter the array elements:\n";

for (i=0;i逐个输入数组元素

cin>>arr[i];

cout<

for (i=0;i显示数组元素

cout<可替换为*(arr+i)

cout<

ss=MaxSum(arrSize,arr);

        cout<<"最大子段和为:"<

delete[]arr; //释放堆内存

}

else

cout<<"Can't allocate more memory.\n";

           system("pause");

}

【案例分析】

1C++包含了堆与栈内存区,arr=new int[arrSize];,这里的new分配了一块堆内存,在栈内存中存放了一个指向堆内存的指针。arr会先确定在堆中分配内存的大小,然后调用new分配内存,最后返回这块内存的首地址,放入栈中。

2int MaxSum(int n,int a[]),,int a[],就是指向堆内存的数据,连续存放数据转存在数组中。

 

注意:在函数体中定义的变量通常是在栈上,但C语言中,malloccallocrealloc等分配内存的函数分配得到的就是在堆上。在所有的函数体外定义的是全局量,加了static修饰符后不管在那里都存放在全局区(静态区)。

案例5-5  输出本机内存数据并排序(高端先存还是低端先存)

【案例描述】

数据类型在内存中是怎样存储的,大小是多少字节?如unsigned char1字节,而unsigned int却是4字节,把它们组成一个联合体共享内存,那么输出会怎样呢?下面代码数据类型输出0xf1f2f3f4会看到如图5-5所示的结果。

 

5-5  输出本机内存数据并排序(高端先存还是低端先存)

【实现过程】

定义一个联合数据类型uai共享同一内存,然后u.i = 0xf1f2f3f4;,输出u.i和int(u.a)。其代码如下:

union //定义联合体,共享一块内存地址

{

 unsigned char a;

 unsigned int  i;

}u;

 

int main()

{

 u.i = 0xf1f2f3f4; //给联合体的整数赋值

 cout<<"输出4字节:"<

 cout<<"输出1字节:"<

 system("pause");

 return 0;

}

【案例分析】

1union联合体是指其中定义的各种数据共享一块内存地址,而结构体的实际长度以其中占据内存空间最大的变量来计算。代码中u在内存中实际就是0xf1f2f3f4,表示为f4 f3 f2 f1,是由低地址到高地址按字节排列的,u.i占据了整个数据大小,u.a仅仅占用前面一字节f4,所以u.a强制转换后输出十六进制数f4

2)代码中u.a是一个char型数据,只占8(bit),将它强制转换成int,这时就不是从内存中扩充,而是从数据类型扩充,将8位数据扩充成32位,用0填充。

 

注意:在编程中可以定义一个联合体,读者可用不同的数据类型调试看看结果。

案例5-6  寻找地址(指针加减法)

【案例描述】

本例介绍指针自动转换机制,也就是定义一个指向char和float的指针,那么指针加1后,地址是多少?两种类型的结果是不同的。这与C++编译器定义的数据类型大小有关,本例效果如图5-6所示。

 

5-6  寻找地址(指针加减法)

【实现过程】

演示1输入地址p和f,然后对该地址加1,演示2输入地址ab,然后对该地址加4,分别输出结果。其代码如下:

void demo1()

{

    char c='a';

    float fl=4.02;

    char *p=&c;

    float *f=&fl;

    printf("%x  %x \n",p,f); //输出十六进制地址

    printf("%x  %x \n",++p,++f);

}

void demo2()

{

      int* a = reinterpret_cast(0x100);    //强制类型转换

       a+=4;

   cout<<"地址a+=4结果为:"<

       double* b = reinterpret_cast(0x200);//

       b+=4;

       cout<<"地址b+=4结果为:"<

}

【案例分析】

1demo1中p++p高出1字节,而f++f高出4字节这是由C++语言的自动转换机制决定的。char1字节,float4字节空间;编译器会自动根据指针类型对运算进行调整,实际结果是数字乘以类型大小的偏移量。所以对于char类型来说,p++指向下一个char;而对float来说,并不会指向float内部的第2个字节。

2)在演示2中,a+=4被解释为a+= 4*sizeof(int);同样,b+=4被解释为b+=4*sizeof(double)int4字节,double8字节。

案例5-7  利用指针删除数组中的指定元素
(指针移动)

【案例描述】

本实例演示指针指向一个字符数组,并且可以移动位置,实现删除功能。通过演示加深了解指针是怎样移动的,认识指针指向内存地址的特征。本例运行结果如图5-7所示。

 

5-7  利用指针删除数组中的指定元素(指针移动)

【实现过程】

主函数中取得输入的整数并且显示,然后取得欲删除的整数,调用删除函数fun(),并输出结果。其代码如下:

#include

using namespace std;

void fun(int *a,int *n,int y);

void main()

{

int n;

cout<<"please enter a number :"<

cin>>n; //取得输入整数

int *a=new int [n]; //为指针分配内存

cout<<"please enter :  "<

for(int i=0;i

cin>>a[i]; //输入各整数

int y;

cout<<"pleasse enter the number you want to delete:"<

cin>>y;//

fun(a,&n,y); //调用删除函数

cout<<"the NEW array have :"<剩余元素的数量

cout<<"the NEW array is:"<

for(int j=0;j输出各元素

cout<

cout<

delete a;                  //释放内存

system("pause");

}

void fun(int *a,int *n,int y) //删除函数

{

int k=0;

int *p=a;                   //赋值字符串

for(int i=0;i<*n;i++)      

if(*(a+i)!=y)

{

*(a+i)=*(p+k); //指针移动

k++;

}

*n=k;

}

【案例分析】

1*(p+k)表示指针p向前移动k个位置。

2)删除函数void fun(int *a,int *n,int y);表示第一、二个输入参数是一个指针。

 

注意:实际编写的程序程中,使用C++指针位置移动能代码,可增加程序的灵活性。

5.3  动态内存分配

案例5-8  简单地获取变量的字节大小(sizeof

【案例描述】

sizeof运算符以字节形式计算出一个变量或者是一种类型(包括集合类型)存储的字节数。编程中经常要知道一个变量占用的字节数。下列代码演示几个变量占用的字节数,本实例效果如图5-8所示。

 

图5-8  简单地获取变量的字节大小(sizeof

【实现过程】

程序显示数据类型int占用字节数;定义一个结构align_depends,结构中包括char和int两种数据类型,输出结构占用字节数;最后分别输出字符串常量、指针字符串和数组占用字节数。代码如下:

#include

#include

using namespace std;

int main()

{

  int size_t = sizeof( int );

  cout <<"int 占用字节:"<< size_t << endl;

  struct align_depends {

      char c;

      int i;

  };

   size_t = sizeof(align_depends);      //这个字节的值取决于

                                     //编译值设置 /Zp

                                     //字节对齐设置#pragma pack          

  cout <<"结构align_depends占用字节:"<< size_t << endl;

  cout << "hello there占用字节:"<< sizeof( "hello there" ) << endl;       

  char* s1 = "hello";    //定义一个指针字符串

  char* s2 = "hello there";

  cout << "s1占用字节:"<< sizeof( s1 ) << endl;   

  cout << "s2占用字节:"<

  char c_arr[] = "how are you?";       //定义一个数组

  cout << "c_arr[]占用字节:"<

  system("pause");

  return 0;

}

【案例分析】

1)运算符sizeof,其格式为sizeof (expression),该运算符接收一个输入参数,该参数可以是一个变量类型或一个变量本身,返回该变量类型或对象所占的字节数,如a = sizeof( int );,这将会返回4a,因为int是一个通常为4字节的变量类型。sizeof返回的值是一个常数,因此,它总是在程序执行前就被固定了。

2)定义结构类型或变量时,sizeof返回的实际大小可能包含填充空间的字节。如代码中的结构align_depends。当应用于静态维度的数组时,sizeof返回整个数组的大小,如代码中的c_arr[]。

 

注意:sizeof操作符不能返回动态创建数组的大小或外部数组的大小。

案例5-9  获取数组大小(sizeof

【案例描述】

在实例022中,讨论了sizeof运算符的用法,它用来计算一个变量或者一种类型存储的字节数。在数组中经常要知道数组的长度及占用的字节数。本实例演示了计算整数、双精度浮点数的一维和多维数组占用的字节数和数组长度,效果如图5-9所示。

 

图5-9  获取数组大小(sizeof

【实现过程】

程序分4个函数演示,demo1()函数定义一个一维整数数组,求占用字节数和数组长度;demo2()定义一个一维整数数组,求占用字节数、数组长度及输出每个数组元素;demo3()定义一个一维双精度浮点数组,求占用字节数、数组长度及输出每个数组元素;demo4()定义一个三维双精度浮点数组,求占用字节数、数组长度及输出每个数组元素。代码如下:

#include

#include

int demo1(){

   //定义一个常量整数数组并初始化

   const int a[] = { 98, 7, 54, 69, 87, 88, 56, 92, 77,39, };

   const int len = sizeof( a ) / sizeof( a[0] );

   cout<<"demo1占用字节数="<数组长度= "<

   return 0;

}

int demo2()

{

//定义一个整型数组并初始化

    int Number[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

    int *pNumbers = Number;             //定义一个整型指针指向整型数组

    int NumberOfMembers = sizeof(Number) / sizeof(int);

    cout<<"demo2占用字节数="<数组长度= "<

    cout << "显示数组的值:";

    for(int i = 0; i < NumberOfMembers; i++)

        cout << "\nNumber " << i + 1 << ": " << *(pNumbers+i);

    cout<

    return 0;

}

int demo3()

{

    //初始化浮点数组,存储平方根

    double SquareRoot[] = { 6.480, 8.306, 2.645, 20.149, 25.729 };

    int ArraySize = sizeof(SquareRoot)/sizeof(double);

    cout<<"demo3占用字节数="<数组长度= "<

    //显示数组的值

    cout << "平方根:";

    for(int n = 0; n < ArraySize; n++)

        cout << "\nRoot " << n + 1 << " = " << SquareRoot[n];

    cout<

    return 0;

}

int demo4()

{

    double EmployeeHours[2][4][7] ={    //雇员工作时间:小时

 31, 28, 31, 60, 31, 30, 61, 31, 30, 31, 30, 31,

         31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,

         31, 28, 51, 30, 31, 30, 31, 31, 70, 31, 30, 31,

         31, 28, 31, 50, 31, 30, 31, 31, 30, 31, 30, 31,

         31, 28, 71, 30, 31, 30, 31, 31

       

    };

    int Size = sizeof(EmployeeHours) / sizeof(double);

    cout<<"demo4占用字节数="<数组长度= "<

/*

    cout << "显示数组的值:";

    for(int i = 0; i < 2; i++)

        for(int j = 0; j < 4; j++)

            for(int k = 0; k < 7; k++)

                cout << "\nHours[" << i << "][" << j << "][" << k << "]: "

                  << EmployeeHours[i][j][k];

*/   

    return 0;

}

void main()

{

demo1();

demo2();

demo3();

demo4();

system("pause");

}

【案例分析】

1)运算符sizeof的格式为sizeof (expression)使用时要注意int通常是一个4字节的数据类型,double通常为8字节的数据类型。上述代码分别计算数组占用字节数、数组长度,如demo1()代码中,占用字节数sizeof( a ),数组长度为sizeof( a ) / sizeof( a[0] )。

2demo2()用指针变量*pNumbers指向数组的地址,用for循环输出每个数组元素*(pNumbers+i);demo4()3for循环直接用数组下标EmployeeHours[i][j][k]输出每个数组元素。

案例5-10  错误释放指针导致程序崩溃

【案例描述】

本案例演示指针使用不当产生错误的过程。如果程序定义了一个指针,就必须让它指向一个设定的空间或者把它设为NULL,如果释放了内存,再对这个内存变量操作,如欲写数据到这个内存,就会出现错误。本例效果如图5-10所示。

 

图5-10  错误释放指针导致程序崩溃

【实现过程】

下面代码为指针指向一个字符串,判断字符串是否为NULL,然后输出该字符串。编译时虽不会出错,但运行时会造成程序崩溃。代码如下:

#include

#include

#include

int main(void)

{

   char *str=new char[100]; //分配固定大小的指针字符串

   strcpy(str,"hello");              //复制字符串

   cout<打印字符串

   delete []str; //释放内存

   if(str!= NULL){ //判断字符串是否为NULL

       strcpy(str,"world"); //复制字符串

   cout<打印字符串

   }

return 0;

}

【案例分析】

程序崩溃原因是因为delete函数释放了str所指的内存,但指针值此时不为NULL,故if块中的语句会执行,导致试图将对一块不合法的内存写入数据。所以一般调用完free函数、释放完空间后,应将指针赋值为NULL

 

注意:要正确使用指针,指针变量使用时要初始化;指针pfree或者delete之后,要置为NULL;指针操作不能超越了变量的作用范围,往往这种情况会让人防不胜防出现错误。

案例5-11  经典栈溢出实例

【案例描述】

每个操作系统进程的栈空间是限定的,如果新开辟内存空间大于这个栈空间,就会造成栈溢出,编译虽能通过但运行的时候出现错误。本例效果如图5-11所示。

图5-11  经典栈溢出实例

【实现过程】

给数组新开辟空间,new[1000*1000]空间整数arr,堆空间输入5个数组元素,显示数组元素,释放堆内存。代码如下:

# include

using namespace std;

int main()

{

// int arr1[1000][1000]; //错误,造成栈溢出

int *arr=new int[1000*1000]; //临时分配这些元素所需的内存空间(堆内存中)

if(arr!=NULL) //判断,堆空间不够分配时,系统会返回一个空指针值NULL

{

cout<<"\nPlease enter the array elements:\n";

for (int i=0;i<5;i++) //逐个输入数组元素

cin>>arr[i];

cout<

        cout<<"the numbers you inputted are:"<

for (i=0;i<5;i++) //显示数组元素

cout<可替换为*(arr+i)

cout<

        delete[]arr; //释放堆内存

}

else

cout<<"Can't allocate more memory.\n";

system("pause");

return 0;

}

【案例分析】

屏蔽代码arr1[1000][1000]。运行程序这时分配1000*1000*4字节的内存,系统无法分配,会出现上面图示错误。可以试一试,减少一个数组的维数,比如a[1000][500]。这时就会发现,小于1M字节可以分配。所以,大数组只能用mallocnew等来分配内存,而且必须用freedelete等释放。

 

注意:编程时候尽量用mallocnew来分配内存。

5.4  指针和const

案例5-12  求最长字符

【案例描述】

函数的参数传递,可以用引用做形参,实现数据的传递。本实例演示的是一个指针变量,它指向包含20个元素的一维数组,求出数组中最长的字符。本例效果如图5-12所示。

【实现过程】

定义个函数char* fun(),在主程序中,初始化指针,并且调用这个函数。代码实现如下:

# include

# include

# include

char* fun(char(*p)[20],int n) //p是一个指针变量,它指向包含20个元素的一维数组

{

int i;

char* max;

max=p[0];

for (i=1;i

if(strlen(p[i])>strlen(max))

max=p[i]; //字符串交换

return max;

}

void main()

{

int wsize;

cout<<"输入字符串数量 : ";

    cin>>wsize; //临时分配元素的个数

    char(* a)[20];

a=new char[wsize][20]; //临时分配这些元素所需的内存空间(堆内存中)

 

if(a!=NULL) //判断,堆空间不够分配时,系统会返回一个空指针值NULL

{

   for(int i=0;i

   {

  cin>>a[i]; //取得输入字符

   }

   cout<<"输入字符串为: "<

   for( i=0;i

   {

cout<

    cout<<"  ";

   }

   cout<

   char*(*s)(char(*)[20],int); //临时分配这些元素所需的内存空间(堆内存中)

   s=fun;

   char* max; //定义字符串

   max=(*s)(a,wsize); //临时分配这些元素所需的内存空间(堆内存中)

   cout<<"最长字符串为: "<

   delete[] a; //释放堆内存

}

    else

cout<<"不能分配内存."<

    system("pause");

}

【案例分析】

1char*(*s)(char(*)[20],int);,临时分配这些元素所需的内存空间(堆内存中)。delete[] a;,释放堆内存。

2)函数char* fun(char(*p)[20],int n),第一个输入p是一个指针变量,它指向包含20个元素的一维数组。

 

注意:指针是C++语言的一大特色,指针实际上是指向一个存储地址空间,实践中多体会其用法。

案例5-13  DOS命令解释器(使main函数接收参数)

【案例描述】

由于main()函数不能被其他函数调用,因此不可能在程序内部取得实际值,那么怎样把实参赋予main()函数呢。如在DOS命令行运行一个程序,需要一运行就读取输入文件名。这就是该实例演示的main()函数的2个参数argcargv。本例效果如图5-13所示。

 

图5-13  DOS命令解释器(使main函数接收参数)

【实现过程】

1)定义帮助结构OptionDef,利用该结构初始化options[],赋值具体帮助内容。其代码如下:

#include

using namespace std;

typedef struct { //定义个帮助结构

    const char *name;  //关键词

    const char *help;  //具体帮助信息

} OptionDef;

//帮助文档

static const OptionDef options[] = {

   { "cd",  "改变当前目录" },

{ "sys",  "制作DOS系统盘" },

{ "copy",  "拷贝文件" },

{ "del",  "删除目录树" },

{ "deltree",  "删除整个目录" },

{ "dir",  "列文件名" },

};

2)函数find_option()查找输入参数,该函数第一个输入参数指针指向帮助结构,第二个参数输入要比较命令;函数show_help_options()通过和输入命令帮助字符串比较显示帮助;函数parse_options()为解析主函数输入参数;int argc, char **argvmain函数接收参数,主函数调用这个函数进行解析帮助。代码如下:

//查找输入参数

static const OptionDef* find_option(const OptionDef *po, const char *name){

    while (po->name != NULL) { //输入不为空

        if (!strcmp(name, po->name)) //字符串比较

            break;

        po++;

    }

    return po;

}

//显示帮助

void show_help_options(const OptionDef *options, const char *msg)

{

    const OptionDef *po; //定义结构

    int first;

 

    first = 1;

    for(po = options; po->name != NULL; po++) {

        char buf[64];

             if (first) {

                printf("%s", msg); //打印帮助信息

                first = 0;

        }

    }

}

//解析主函数输入参数

void parse_options(int argc, char **argv, const OptionDef *options)

{

    const char *opt, *arg; //定义字符串

    int optindex, handleoptions=1;

    const OptionDef *po; //定义结构

 

    /* parse options */

    optindex = 1;

    while (optindex < argc) { //对输入参数逐个解析

opt = argv[optindex++];

 //查找输入参数

            po= find_option(options, opt);//po= find_option(options, opt + 1)

            if (!po->name)

                po= find_option(options, "default");

            if (!po->name) {

unknown_opt:

                fprintf(stderr, "%s: 没有找到帮助信息 '%s'\n", argv[0], opt);

                exit(1);

            }

if(po->name){

show_help_options(options, po->help) ;//显示帮助

}

            arg = NULL;

}

 

}

int main( int argc, char *argv[ ] )

{

     if( argc != 2 )

    {

        cerr << "Usage: " << argv[ 0 ] << " DOS命令" << endl;

        return 1;

    }

    // parse options

    parse_options(argc, argv, options); //解析输入参数

cout<

system("pause");

    return 0;

}

【案例分析】

1)程序是由main( )函数处向下执行的,main()函数也可以带参数。除main()外其他函数由main()函数调用,即在程序中调用。main()函数是由DOS系统调用的,所以main()函数实参的值是在DOS命令行中给出的,是随着文件的运行命令一起给出的。main()函数形参的形式:main( int argc, char * argv[ ])。

2)代码main( int argc, char *argv[ ] )argc:整数为传给main()的命令行参数个数,文件名也算一个参数。*argv字符串数组,argv[0]为命令行中执行程序名,argv[1]命令行中执行程序名的第一个字符串,argv[2]执行程序名的第二个字符串,argv[argc]NULL

 

注意:main还有个形式:int main(int argc[,char *argv[ ] [, char *envp[ ] ] ]);,输入参数envp为环境变量数组指针。

案例5-14  动态输入字符串作函数参数的实现

【案例描述】

C++标准规定主函数main()可以是有参的函数,也就是在main()中使用一个或更多的参数。如DOS命令行运行,需读取输入各种运行环境参数。该实例所说的是读取main()函数的两个参数argcargv,然后显示出输入参数。本例效果如图5-14所示。

 

图5-14  动态输入字符串作函数参数的实现

【实现过程】

定义函数GetCmdLine(),用这个函数可以实现返回去掉多余空格输入的参数行;包括输入参数命令行参数的个数和命令行参数的字符数组指针,函数返回输入命令行字符串。代码如下:

#include

using namespace std;

#include

//用这个函数可以返回去掉了多余空格的cmdline

LPTSTR GetCmdLine(int argc,char *argv[])

{    

int i=0;    

int length=0; //命令行参数的个数

char * cmdline;    

if(argc<2) //命令行参数小于2    

return TEXT("");    

for(i=1; i

{        

length=length + strlen(argv[i]); //命令行参数的个数  

}    

cmdline = (char *)malloc(sizeof(char)*(length + argc -1)); //分配内存

strcpy(cmdline,argv[1]);    

if(argc>2) //命令行参数小于2

{        

for(i=2;i循环       

{            

strcat(cmdline," "); //连接两个字符串         

strcat(cmdline,argv[i]);        

}    

}    

return TEXT(cmdline); //字符串变成Unicode

}

int main(int argc,char *argv)

{    

LPTSTR cmdline; //要用到这个参数,就用变量代替原来的参数    

cmdline=GetCommandLine(); //获取命令行字符串,包括程序名本身   

cout <显示输入命令参数

system("pause");

return 0;

}

【案例分析】

1)代码int main(int argc , char *argv[]),其中argc是命令行参数的个数,argv[]是命令行参数的字符数组指针。

2)上述代码如无TEXT关键字,则在编译时编译成ASCII码;如加上_T()TEXT就变成Unicode

案例5-15  强制修改常量的值

【案例描述】

const定义的常量值一般情况是不可以修改,但有时也可修改。若在编程中获取变量的地址,也就是将地址赋给变量,再做修改,这样也可改变const常量值。本例效果如图5-15所示。

【实现过程】

定义一个常量指针ip,运行时赋予不同的值,得到不同的结果,同样定义了一个指针常量ip1,可改变ip1中的内存值char,代码如下:

void main()

{

    const int a=11;

    const int b=12;

    int c=13;

    const int * ip=&a; //定义一个常量指针ip,指向的地址是a

 // *ip=20;//错误,常量指针ip指向的内存不能被修改

    cout<<"常量*ip的值初始化为:"<<*ip<输出11

    ip=&b;     //指针值可以修改

    cout<<"常量*ip的值改变为:"<<*ip< //输出12

    ip=&c;   

    cout<<"常量*ip的值改变为:"<<*ip< //输出13

    c=3;       

    cout<<"常量*ip的值改变为:"<<*ip< //输出3

    cout<

 

    char *bb="abcd";

    char * const ip1=bb; //定义一个指针常量,指向的是bb的地址

    int i;

    cout<<"常量*ip1的值初始化为:";

//ip1=&bb; //错误,指针常量不能再用ip1指向其他变量

    for(i=0;i<4;i++)

    cout<<*(ip1+i);    //输出abcd

    cout<

    *ip1='1';        //ip1的内容变为"1bcd"

    *(ip1+1)='2';     //ip1的内容变为"12cd"

cout<<"常量*ip1的值改变为:";

    for (i=0;i<4;i++)

    cout<<*(ip1+i);              //输出12cd

    cout<

    system("pause");

}

【案例分析】

1)常量指针是指指向常量的指针,也就是指针指向的常量,指向的内容不能改变,但自身的地址可以改变。而指针常量是指指针本身的常量,指向的地址是不可改变的,但地址中的内容可以通过指针改变。

2)常量指针ip可改变ip的地址(指向),不可改变*ip的值;指针常量ip1可改变*ip1的值,不可改变ip1的地址(指向)。如*ip=20;和ip1=&bb;,都是错误的。

 

注意:强制修改常量的值,可以重新认识内存和指针等概念,实际编程中可得到内存地址并修改其中的内容。

案例5-16  防止野指针的代码

【案例描述】

程序定义了一个指针后,就必须让指针指向一个设定的空间或者把它设为NULL,如果没有这么做,那么这个指针里的内容是不可预知的,即不知道它指向内存中的哪个空间,也就是所谓的野指针。效果如图5-16所示。

 

5-16  防止野指针的代码

【实现过程】

下列代码中的指针指向一个字符串,然后输出该字符串。函数demo1()到函数demo4()都是错误的,只有函数demo()是正确的。前4个函数编译时虽不会出错,但运行时会造成程序崩溃。代码如下:

void demo1()

{

   char *p,*p1="hello world!!";      //定义指针*p,定义指针*p1并初始化

   while((*(p++) = *(p1++)) != 0);   //循环直到指针*p1为空,并把*p1复制给指针*p

      cout<

   system("pause");

  }

void demo2()

{

   char *p=NULL,*p1="hello first!";   //定义指针*pNULL,定义指针*p1并初始化

    while((*(p++) = *(p1++)) != 0);  

        cout<

   system("pause");

  }

void demo3()

{

    char *p=" ",*p1="hello first!";   //定义指针*p值为" ",定义指针*p1并初始化

    while((*(p++) = *(p1++)) != 0);  

        cout<

    system("pause");

  }

void demo4()

{

    char c[]="";  

   char *p=c,*p1="hello first!";        //定义指针*p值为"",定义指针*p1并初始化

   char *p_start=p;  

   while((*(p++) = *(p1++)) != 0);  

       cout<

   system("pause");

  }

void demo()

{

   char *p=NULL,*p1="hello world!!";    //定义指针*pNULL,定义指针*p1并初始化

   p=new char[strlen(p1)+1];           //为指针*p为分配空间

    char *p_start=p;  

   while((*(p++) = *(p1++)) != 0);  

      cout<

    system("pause");

}

void main()  

{  

   demo1();

   demo2();

   demo3();

   demo4();

   demo();

}  

【案例分析】

1)函数demo1()错误的原因是:p定义时没有初始化,且指向不确定,是一个野指针;p++可能引用的空间为非法。函数demo2()中不能对NULL的指针进行直接操作,NULL表示没有任何指向,p++没有任何意义,运行时会造成程序崩溃。demo3()p指向的是一个const常量的内容,可以通过*(p++)形式引用该值,但不能改变它指向const的内容。函数demo4()中的c太小,造成了数组越界。

2)函数demo()是正确的,new的作用是指定它的大小,对于p=new char[strlen(p1)+1];,如果写成p=new char;就是错误的。

 

注意:使用指针变量时要初始化,如定义一个指针p,那么进行free或者delete操作之后,就要置为NULL;指针操作不能超越变量的作用范围。

5.5  指针数组和数组指针

案例5-17  验证码(函数实现)

【案例描述】

编程中经常要进行字符串比较,如密码验证、读取文本文件是否包含指定内容等,可以写成个函数形式,输入指向字符串指针进行判断,本例演示的是输入验证码是否正确,效果如图5-17所示。

 

图5-17  验证码(函数实现)

【实现过程】

定义2个函数;random()为随机产生一个字符,check()字符串进行比较;主函数为计算机随机产生一个4位验证码,和输入的验证码比较,返回比较结果。代码实现如下:

#include

#include

#include

using namespace std;

char random( ) //随机产生一个字符

{

int a=0, b=0;

char c; a=rand()%3; //随机产生一个数和3取模运算

if( a==0 )

{

b=rand()%10+48; c=b; //随机产生一个数和10取模运算

} else if( a==1 )

{

b=(rand()%26) + 97; //随机产生一个数和26取模运算

c=(char)b;

}

else

{

b=rand()%26 + 65;

c=(char)b; }

return c;

}

int check(char *p, char *q) //字符串进行比较

{ int k=0,i;

for(i=0; p[i]!='\0'; i++)

{

if(p[i]!=q[i]) //字符不同

{

k=1;

break;

}

}

if(q[i]!='\0') //需要额外判定

k=1;

return k;

}

int main()

{

int k=0, num=0, i=0;

cout<<"本程序用于生成数字加字母验证码"<

while(num<3) //3次机会

{

char a[100]={0}, b[100], c[100], d[100]; //定义字符串

for(i=0; i<4; i++) //随机产生4个验证码

{

srand(time(0)+i);

a[i]=random( );

} a[4]=0; //请输入终结符

cout<<"验证码为:"<

cout<<"请输入验证码(不区分大小写)";

cin>>b;

for(i=0; a[i]!='\0'; i++)

{

if(a[i]>='A' && a[i]<='Z') //AZ大写字母

{

c[i]=tolower(a[i]); //大写字母转小写

cout<

}

else

{

c[i]=a[i];

cout<打印字符

}

}

c[i]=0; //请输入终结符

for(i=0; b[i]!='\0'; i++)

{

if(b[i]>='A' && b[i]<='Z') //AZ大写字母

{

d[i]=tolower(b[i]); //大写字母转小写

cout<

}

else

{

d[i]=b[i];

cout<

}

}

d[i]=0; //请输入终结符

k=check(c,d); //字符串比较

if( k==0 ) { break; }

else

{

cout<<"输入有误,请重新输入。"<

num++;

continue;

}

}

if(num<3) cout<<"输入正确。"<

else cout<<"抱歉,输入次数已尽。"<

system("pause");

}

【案例分析】

1srand()函数是随机数发生器的初始化函数。原型:void srand(unsigned seed); int tolower( int c );,是C语言中的函数,功能是大写字母转小写。

2)实例通过两个函数实现,random()为无参数输入,check(char *p, char *q)为字符串比较函数,输入的是2个参数为指向字符串的指针。

 

注意:这个程序是数字验证,如果是图片验证就比较复杂。

案例5-18  坐标指针(数组+指针)

【案例描述】

数组和指针在用C++语言编程经常会用。例如,怎样把类似的一组数据存储在指针变量中。在本实例中,将讲解指向数组存储的指针,然后进行数据交换处理,完成数据排序后输出。本例效果如图5-18所示。

【实现过程】

1)首先创建三个用指针和数组存储字符串的变量,接着输入5个字符串给str[i]pstr[i]=str[i];,该语句将第i个字符串的首地址赋给指针数组的第i个元素,然后p=pstr,最后进行冒泡排序sort(p)。其代码如下:

void main()

{

int i;

char **p,*pstr[5],str[5][max]; //创建指向指针的指针、指向数值的指针和二维数组

cout<<"请依次输入5个字符串(空格隔开):"<

for(i=0;i<5;i++)

cin>>str[i];

for (i=0;i<5;i++)

pstr[i]=str[i];   //将第i个字符串的首地址赋值给指针数组的第i个元素

 

p=pstr;

sort(p);

cout<<"排序后的字符串为:"<

for (i=0;i<5;i++)

cout<输出排序后的字符串

cout<

system("pause");

}

2)程序采用的是冒泡排序,该算法是依次比较相邻的两个数,将小数放在前面,大数放在后面。程序主要是指针累加并且交换数值。代码如下:

void sort(char **p)   //冒泡法对n个字符串排序

{

int i,j;

char *pc;

for (i=0;i<5;i++)         //第一趟扫描,实现n-1次排序

{

for(j=i+1;j<5;j++)    //第二趟扫描

{

if(strcmp(*(p+i),*(p+j))>0)){ //交换记录

{

pc=*(p+i);                //pc仅作为暂存单元

*(p+i)=*(p+j);

*(p+j)=pc;

}

}

}

}

【案例分析】

1)在C++语言中,指针和数组的数据可以进行交换。上述代码pstr[i]=str[i];就是将第i个字符串的首地址赋给指针数组的第i个元素。char **p中的**p是指向指针的指针,也就是*p的地址,p指向的对象就是*p。

2)指针指向内存的位置可以移动,如上面的*(p+i);还可以为*p++*p--,分别表示指针加1和减1。代码中,冒泡排序采用的交换数值pc=*(p+i);*(p+i)=*(p+j); *(p+j)=pc;,这3条语句的作用是实现移动指针,把小数放到前面的功能。

 

注意:数组的长度是固定的,指针可以根据实际的长度改变存储空间的大小,可分配和回收内存。

案例5-19  打印内存数据(char 打印1字节)

【案例描述】

当一组数据存储在内存中时,如何一个字节一个字节地取出并显示?如定义一个指向数组的指针,怎样对数组中的数据进行处理,并输出数组中每一字节的数据?本例效果如图5-19所示。

 

5-19  打印内存数据(char 打印1字节)

【实现过程】

1)输入为字符型指针数值,返回的变量是指向字符地址的指针。代码如下:

char* fun(char(*p)[20],int n)   //p是一个指针变量,它指向包含20个元素的一维数组

{

int i;

char* max;

max=p[0];

for (i=1;i

if(strlen(p[i])>strlen(max))  //与前一个得到的字符串的长度进行比较

max=p[i]  //交换字符串

return max;

}

2)首先输入字符串的数量,然后输入字符串,程序显示出输入字符串和最大长度的字符串。代码如下:

  void main()

{

int wsize;

cout<<"Please enter the number of words :";

     cin>>wsize; //临时分配元素的个数

     char(* a)[20];

a=new char[wsize][20]; //临时分配这些元素所需的内存空间(堆内存中)

 

if(a!=NULL) //判断堆空间不够分配时,系统会返回一个空指针值NULL

{

   for(int i=0;i

   {

  cin>>a[i]; //输入wsize个字符串

   }

   cout<<"the words you inputted are:"<

   for( i=0;i

   {

cout<显示刚输入的wsize个字符串

     cout<<"  ";

   }

   cout<

   char*(*s)(char(*)[20],int);

   s=fun;

   char* max;

   max=(*s)(a,wsize);

//显示输出最长的字符串

   cout<<"The longest word is:"<

   delete[] a; //释放堆内存

}

    else

cout<<"Can't allocate more memory."<

    system("pause");

}

【案例分析】

1)代码a=new char[wsize][20];char*(*s)(char(*)[20],int);,其作用是临时分配这些元素所需的内存空间(在堆内存中)。Wsize表示临时分配元素的个数;if(a!=NULL)用于判断堆空间不够分配时,系统返回一个空指针值NULL。循环输入wsize个字符串,再循环输出这些字符串。

2fun()的功能是循环比较每次取得最长的字符串,交换最长字符串后返回最长的字符串。

 

注意:指针是指向内存的地址,数组是以固定的长度存储在连续的空间中,在实际中应多实践。

5.6  引用

案例5-20  万能箱子(void*

【案例描述】

void*又称为泛型指针,也就是可以指向任何类型的数据的指针。在没有任何类型的信息时,就可以考虑把数据类型定义为void*。编译器对它的处理就是一个字节一个字节地处理,类似于unsigned  char*的处理方式,当然,char类型必须是1字节才行。本例的void*指向不同的类型,效果如图5-20所示。

 

5-20  万能箱子void*

【实现过程】

void*是泛型指针,可以为数据类型char*、int和double,还可以是一个结构,代码中打印出void*存储的地址和内存的值。代码如下:

void   main(void)

{

char*   str= "ABCD ";

void *pv=(void*)str;    //定义*pv,指向字符串地址   

cout <<"The   Address   is:\t " <输出指向地址

cout <<"The   Val   is:\t " <<(char*)pv <输出指向地址存储数据

system("pause");

int i=100;

pv = &i;

cout <<"The   Address   is:\t " <定义*pv,指向整数的地址  

cout <<"The   Val   is:\t " <<*(int*)pv <

system("pause");

double obj = 3.14;

pv = &obj; //定义*pv,指向double的地址

cout <<"The   Address   is:\t " <

cout <<"The   Val   is:\t " <<*(double*)pv <

system("pause");

struct date

{

int year; int month; int day;

};

struct date test_data;

test_data.year=2010;

test_data.month=12;

test_data.month=10;

void *__pv = &test_data ; //定义*pv,指向struct date的地址

#define pv ((struct date*)__pv)

cout <<"The   Address   is:\t " <

//输出指向地址,存储结构内的数据

printf("pv->year = %d,pv->month = %d \n",pv->year,pv->month);

system("pause");

}

【案例分析】

1void *pv可以指向任何类型,如程序中的

void *pv=(void*)str;  pv = &i; int i;double obj

甚至是一个结构数据,如

void *__pv = &test_data ; #define pv ((struct date*)__pv)

赋值时指向数值的地址,实际编程中,使用void*可以增加程序的灵活性。

2)在C的标准库中,关于memory的操作头文件 ,几乎都使用void*类型,将类型的转换交给程序员处理,否则相同的功能仅仅是因为数据类型不一样,就要提供一个函数,而偏偏C又不支持函数重载。这有助于加强C++语言中对void*概念的理解。

 

注意:void*具体指向某种类型的时候,读出里面存储的值,但类型要匹配,否则读出的值是错误的,这方面要多看、多实践,以加深理解。

案例5-21  图书名整理系统(按开头字母重新排列)

【案例描述】

void*有时候又称为泛型指针,也就是可以指向任何类型的数据的指针。指向的是一段内存地址,当然也可以指向一个存储的字符串,这样在编程中就更灵活。如在函数输入参数中指向一个读出的文本文件内容字符串。本例通过函数qsort()实现输入字符串按开头字母排序,效果如图5-21所示。

 

图5-21  图书名整理系统(按开头字母重新排列)

【实现过程】

定义函数cmp(),实现由小到大排序;函数init()赋值字符数组buffer;主函数main()接收输入的字符串,把字符串的字母捡出来排序,再扫描每个单词,根据单词第1个字母跟捡出字母进行排序输出。其代码如下:

#include

using std::cout;

using std::endl;

#include

using namespace std;

int word_num[300];

char str[1000],word[300][30]; //定义字符串

char buffer[100],sign[100];

int cmp(const void* a,const void* b)

{

return *(char*)a-*(char*)b; //由小到大排序

}

void init()

{

int i,j=0;

for(i=0;str[i]!='\0';i++) //0到字符串结束

{  

//判断是否英文字母

if((str[i]>='a'&&str[i]<='z')||(str[i]>='A'&&str[i]<='Z')) buffer[j++]=str[i];

}

buffer[j]='\0'; //赋值结尾字符

return ;

}

int main()

{

int i=0,j=0;

int i1=0,i2=0;

int k=0;

char c;

cout<<"请输入字符串:";

    cin.getline(str,100); //接收一个字符串

init();

//先把字母捡出来排序,然后再把排好序的字母还回去

qsort(buffer,strlen(buffer),sizeof(char),cmp);  

puts(buffer);

    str[strlen(str)]=' '; //字符串最后一个字母后为'\0'

    cout<<"输入的所有输入字符串:"<

cout<

    for(i=0;c=str[i]!='\0';i++)

    {

        word[i1][i2++]   =str[i];

        if(str[i]==' '&&str[i+1]!=' ')

{

i1++; //移到下个单词数组

                i2=0; //单词存储从0开始

}

    }

/*

    for(i=0;i打印每个单词

  cout<

*/

word[i][i2+1]='\0';

    str[k]='\0';

int k1=0;

string string1;

for(i=0;buffer[i]!='\0';i++) //0到字符串结束

{  

//判断是否英文字母

if((buffer[i]>='a'&&buffer[i]<='z')||(buffer[i]>='A'&&buffer[i]<='Z'))

{

  for(j=0;j扫描每个单词

//单词第一个字母,并且不重复出现该字符且不是第一个字符

     if(word[j][0]==buffer[i])//if((word[j][0]==buffer[i])&& ((buffer[i-1]!=buffer[i])&&(i>0)))

 {

                     //单词没有加入重新排列输出

if(string1.find(word[j])==string1.npos)

{

   //加入重新排列输出

                   string1=string1+word[j]+' ';

// cout<

}

  

 }

}

 }

 cout<<"开头字母重新排列结果为:"<

 cout <

system("pause");

 return 0;

}

【案例分析】

1)使用快速排序例程进行排序,用法:

void qsort(void *base,int nelem,int width,int (*fcmp)(const void *,const void *));

参数:

— 待排序数组首地址。

— 数组中待排序元素数量。

— 各元素的占用空间大小。

— 指向函数的指针,用于确定排序的顺序。

return *(char*)a-*(char*)b;是由小到大排序,return *(int *)b-*(int *)a; 为由大到小排序。

2)函数cmp(const void* a,const void* b),其参数ab是常量指针,不能修改的。

 

注意:上面代码void*有时候又称为泛型指针,也就是可以指向任何类型的数据的指针。

 

案例5-22  指针引用使用问题

【案例描述】

在调用函数,对指针输入需要了解清楚,函数调用输入的是地址还是值,否则会引起错误。本例效果如图5-22所示。

【实现过程】

主函数中定义字符ab,把值赋给xy地址,调用函数swapxy(),把值传入函数输入参数。代码如下:

#include

using namespace std;

void swapxy1(char* a, char*  b){ //错误

int x=*a,y=*b;//

x=x+y; //进行计算

y=x-y;

x=x-y;

*a=x,*b=y;

return;

}

void swapxy(char& a, char & b){ //正确

int x=1,y=b; //把传送的值赋予整型变量

x=x+y; //进行计算

y=x-y;

x=x-y;

a=x,b=y;

return;

}

int main(){

   char a='a',b='B';

char &x=a,&y=b;

    cout<<"a="<

swapxy(x,y); //swapxy1(x,y);

    cout<<"a="<

return 0;

}

【案例分析】

上面代码存在引用的使用问题,在函数swapxy1()中,要求输入的是指针,如在主函数swapxy1(x,y);中,输入字符值会显示错误。

案例5-23  斗牛网络游戏

【案例描述】

C++面向过程的程序设计中,函数的定义和调用很重要,可以通过函数输入指针来传递值。本实例举定义函数方法,举的是斗牛游戏简单实现的DOS版本的程序,效果如图5-23所示。

 

图5-23  斗牛网络游戏

【实现过程】

定义几个函数输入指针,如初始化的工作Input。代码如下:

using namespace std;

#define MAX_LEN 54

#define SEND_END 0

#define SEND_NOT_END 1

#define CARD_NUM 20

//斗牛程序的一些说明:

//n/4 表示这个牌的数值+1  

//n%4==0:黑桃 1:红桃 2:梅花 3:方块

//n==52 小王  

//n==53 大王

//当数组中的数值存储为1表示此牌已经发出;如果为0表示没有发出

//因为斗牛不需要大王和小王所以在初始化的时候将5253位设置为1

//默认A组为庄家  

//设置参与人数为4

//数组初始化为0

void Input(int* arr, int nLen);

void GetNum(int* arr, int nLen, int* a); //得到一张牌

int SendCard(int* arr, int nLen, int* pn, int nLen2); //n 张牌

void PrintCard(int a);   //打印一张牌

void Output(int* pn, int nLen);   //打印 n 张牌, 并比较牛数大小

int GetNiu(int* pn); //nLen == 5  //获取一组数据的牛数,通过返回值返回

int CompareNiu(int *pn1, int* pn2);   //比较两组数的牛数大小

void ReDirectNum(int* pn);   //从新定向数值为对应的牛数计算数值

void Sort(int* pn, int nLen);    //将数组排成有序函数

int GetMax(int* pn, int nLen);    //获取数组中最大的数值

int main()

{

 int nArr[MAX_LEN] = {0};

 int narrCard[CARD_NUM] = {0};

 int  nLen  = 0;

 int  nRet  = 0;

 char ch   = '0';

 srand(time(NULL));    //获取真的随机数

 Input(nArr, MAX_LEN);  //初始化的工作

while (true)   //开始发牌,并比较与庄家的牛数大小

 {

  cout << "按任意键开始游戏..." << endl;

  getch();

  nRet = SendCard(nArr, MAX_LEN, narrCard, CARD_NUM);

  if (SEND_END == nRet)

  {

   cout << endl << endl;

   cout << "----------------------------------------" << endl;

   cout << "牌已经发完了,是否重新开始游戏?" << endl;

   cout << "----------------------------------------" << endl;

   cin >> ch;

   if ( ('n' == ch) || ('N' == ch) )

   {    

    break;

   }

   else

   {

    //初始化的工作

    Input(nArr, MAX_LEN);

   }

  }

  else

  {

   Output(narrCard, CARD_NUM);  //打印 n 张牌, 并比较牛数大小

  }

 }

 return 0;

}

【案例分析】

斗牛游戏,先去掉鬼,每方发5张牌,大小是1~10JQK就是牛,这几张牌都算10;再把其他的牌加起来,10的倍数就去掉。如果没牛就比单牌的大小,比如按黑红梅方的顺序比大小,黑桃K在单牌里最大。

本程序主要利用函数Output()int* pn函数输入参数为指针。srand()功能是获取真的随机数。

 

注意:这是很简单的DOS版本上运行的游戏,如果网络版需要用到上个实例中讨论的网络通讯,用到VC++MFC控件。

5.7  本章练习

你可能感兴趣的:(C++程序设计案例实训教程第5章)