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

时间:2022-08-31 19:05:51

68章  关于函数高级话题

C语言是面向函数的语言。C++面向过程的程序设计沿用了C语言使用函数的方法,主函数以外的函数大多是被封装在类中的,主函数或其它函数可以通过类对象调用类中的函数,这样大大降低软件开发的难度。本章演示的实例包括常见的内存使用中的错误,加深对内存使用的认识;再次重点介绍函数的参数与返回,读者须灵活掌握;演示函数指针定义及使用的案例,加深函数与指针认识;介绍函数与函数之间的调用关系;还演示了函数与结构体、共用体及类对象如何调用与返回,怎样编写一个较高质量的函数等。通过这些案例,对函数使用的一些细节深入的讨论,使读者达到更深一步理解和使用好函数的目的。

8.1  内存使用错误剖析模块

案例8-1  释放字符串常量内存错误实例

【案例描述】

所有的字符串常量都被放在静态内存区中。因为字符型常量很少需要修改,放在静态内存区中会提高效率。例:char str1[] = "abc";const char *str2 = "abc";str1是数组变量,它有各自的内存空间;而str2是指针,指向相同的常量区域。但在定义字符型数组时,一份存储在动态分配的栈中,另一份在静态存储区;栈要清空时,局部变量的内存被清空了,因释放的内存地址变量值引起错误。本例效果如图8-1所示。

【实现过程】

分两个演示,两个演示都定义字符串str,demo1()调用returnStr1()demo2()调用returnStr2()returnStr1()返回字符串的值,returnStr2()返回字符数组的值。代码实现如下:

#include<iostream>

#include<iostream.h>

char *returnStr1()

{

 char *p="hello world!"; //定义字符串

 return p;

}

int demo1()

{

 char *str=NULL; //一定要初始化,养成好的习惯

 str=returnStr1(); //调用函数

 cout<<"str="<<str<<endl; //输出字符串

 return 0;

}

char *returnStr2()

{

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

 //static char p[]="hello world!";

 return p;

}

int demo2()

{

 char *str=NULL; //一定要初始化,养成好的习惯

 str=returnStr2(); //调用函数

 cout<<"str="<<str<<endl; //输出字符串

 return 0;

}

int main()

{

    demo1();

    demo2();

    system("pause");

    return 0;

}

【案例分析】

1demo1()代码是正确的。因为字符型常量"hello world!"存放在静态数据区,把该字符型常量存放的静态数据区的首地址赋值给了指针,所以returnStr1函数退出时,该字符型常量所在内存不会被回收,故能够通过指针顺利无误地访问。

2demo2()中,把一个字符型常量"hello world!"存放在静态数据区没有错误。但是把一个字符串常量赋值给了一个局部变量(char []型数组),该局部变量存放在栈中。这样就有两块内容一样的内存,也就是说“char p[]="hello world!";”,这条语句让“hello world!”这个字符串在内存中有两份拷贝:一份在动态分配的栈中,另一份在静态存储区。这是与demo1最本质的区别,当returnStr()函数退出时,栈要清空局部变量的内存也被清空了,所以这时的函数返回的是一个已被释放的内存地址,所以打印出来的是乱码。代码前面加static就正确了。

 

注意:字符串常量又可以称之为一个无名的静态变量。demo2()中如果函数的返回值非要是一个局部变量的地址,那么该局部变量一定要申明为static类型。

8.2  重申:函数参数传递和返回机制

案例8-2  宝宝改名(函数参数直接引用变量
(形参引用))

【案例描述】

函数的参数传递中,可用引用作为形参;引用&是标识符的别名。如输入int& a, int& b,进行函数处理会不会交换里面的值,本实例讨论这方面问题。本例效果如图8-2所示。

【实现过程】

函数Swap()compare()把引用作为形参,Swap成功交换了值,compare没有交换成功;swapOne()函数使用引用形参成功交换了ab变量,swapTwo()使用的是普通的非引用类型的参数,没有交换成功。代码实现如下:

 

8-2  宝宝改名(函数参数直接引用变量(形参引用))

#include <iostream>

#include <string>

using namespace std;

void Swap(int& a, int& b) //引用可以作为形参

{    int t;

      t=a;

      a=b;

      b=t;

}

//比较两个数,如果相同返回0;如果v1大,返回1;如果v2大,返回-1   

int compare(const int &v1, const int &v2)    //引用可以作为形参

{   

    if(v1 < v2) return -1;   

    if(v2 < v1) return 1;   

    return 0;   

}  

//把形参定义为引用类型

void swapOne(int& a, int& b)

{

    int tmp = b; //数据进行交换

    b = a;

    a = tmp;

}

//使用复制的实参,只是改变局部副本,而传递函数的实参没有修改

void swapTwo(int a,int b)

{

    int tmp = b; //数据进行交换

    b = a;

    a = tmp;

}  

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

{

int x(15), y(22);

int a = 1, b = 2;

    cout<<"x="<<x<<" y="<<y<<endl; //打印输入数据

    Swap(x,y);

    cout<<"after Swap(x,y) x="<<x<<" y="<<y<<endl;

    x=15; y=22;

    compare(x,y);

    cout<<"after compare(x,y) x="<<x<<" y="<<y<<endl;

  cout<<"a="<<a<< " b="<<b<<endl; //打印输入数据

a = 1, b = 2;

    swapOne(a,b);

//打印swapOne处理数据

cout<<"after swapOne,a="<<a<<" b="<<b<<endl;

a = 1, b = 2;

     swapTwo(a,b);

//打印swapTwo处理数据

cout<<"after swapTwo,a="<<a<<" b="<<b<<endl;

system("pause");

     return 0; }

【案例分析】

1)假设代码int a = 1;int &b = a;,这里b是引用类型引用了a,也就是说ba是同一个内存地址,b也就相当于a的别名;这时如果b = 2改变b的内存地址的内容,也就改变a的内容,所以a也是2int &b = a; 功能的效果等同于 int *b = &a; ,但这样的话b是指针类型,指向a的地址,要改变a就需要改变*b,即改变*b=2,那么a也会变为2,这里注意b本身是个地址,所以*b才是内容,这是指针和引用类型的区别。

2)函数void Swap(int& a, int& b),说明引用也可以作为形参,情况和一般的形参有所不同。因为形参的初始化不是在类型说明时进行,而是在执行主调函数的调用表达式时,才为形参分配内存空间,同时用实参来初始化形参。这样引用类型的形参就通过形实结合,成为实参的一个别名,对形参的任何操作也就会直接作用于实参。swapTwo()使用的是普通的非引用类型的参数,是通过复制对应的实参实现初始化,传递函数的实参没有改变。而swapOne()中,引用形参直接关联到其绑定的对象,而并非这些对象的副本。所以交换会成功。

 

注意:在代码compare(const int &v1, const int &v2)中,const有什么作用呢。原来使用const可以避免引用复制提高性能,swapOne()中当然不能使用const了,因为它要改变值;而compare()函数只是比较两个数的大小,没有交换,所以加上const可以提高性能,因为复制对象需要付出时间和存储空间代价。

8.3  函数与指针

案例8-3  函数映射表(函数指针)

【案例描述】

设计一个芯片的模拟器,要实现一个指令到函数的映射可按本实例方法。指令是一个16位整数(二进制),然后有相当多个函数,指令和函数有对应关系。现在根据给出的指令,调用相应的函数。本例效果如图8-3所示。

【实现过程】

程序用hash表,指令作为Key,函数指针作为值,定义ptr_func类型变量arr_ptr_func,在主函数定义map,进行赋值,遍历函数指针,调用指针函数。其代码如下:

typedef void (*ptr_func)(); //hash,指令作为Key,函数指针作为值

typedef ptr_func *arr_ptr_func; //定义ptr_func类型

void test_func1()

{         

printf("函数1\n");

}

void test_func2()

{         

printf("函数2\n");

}   

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

{         

arr_ptr_func map[65536]; //16位,就是65536大小的数组,直接下标11的映射         

map[5] = (arr_ptr_func)malloc(sizeof(ptr_func) * 3); //分配内存      

map[5][0] = test_func1; //赋值      

map[5][1] = test_func2; //赋值       

map[5][2] = NULL; //赋值      

int ndx = 5;         

arr_ptr_func ptr = map[5];           

while (*ptr != NULL) //遍历函数指针

{                 

(*ptr)(); //调用指针函数      

++ ptr; //指针指向下一个

}        

getchar(); //按任意键

return 0;  }

【案例分析】

hash表,指令作为Key,函数指针作为值16位,就是65536大小的数组,直接下标11的映射得到ptr_func map_func[65536]。typedef void (*ptr_func)()定义ptr_func类型。

 

注意:除上面方法外,最直接的方法当然是用switchcase,但显然不是一个高效的方法。网上有提到“函数映射表“概念,但是并没有找到具体的原理和实现的方法。或者换句话说,有没有别的高效率的方法来执行一个相当多个caseswitch语句。

8.4  函数与数组

案例8-4  学生成绩统计

【案例描述】

这也是个综合的实例目的加深读者对函数理解和综合应用。编写软件目的经常是实行信息管理功能,把日常生活中的事务管理用程序来实现。本实例是个简单学生成绩统计方面的小软件,效果如图8-4所示。

【实现过程】

定义个函数level(),输入参数有课程成绩、课程门数和优秀、及格、不及格的人数,主函数中用一个循环实现每一个学生调用level函数一次。其代码如下:

#include <iostream>

using namespace std;

int& level(int grade[],int size,int& tA,int& tB,int& tC)

{

int temp=0;

for(int i=0;i<size;i++)              //计算总成绩

temp+=grade[i];                 

temp/=size; //计算平均成绩

if (temp>=85)  return tA; //优秀

else  if (temp>=60) return tB; //及格

else return tC; //不及格

}

void main()

{

int a[4][3]={{76,82,90},{79,92,88},{78,65,82},{66,58,58}};//3门课成绩

int typeA=0,typeB=0,typeC=0; //定义优秀、及格、不及格人数

int stdSize=4;                      //学生数

int gradeSize=3; //课程门数

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

level(a[i],gradeSize,typeA,typeB,typeC)++;

//函数调用作为左值(即可以被赋值)or &a[i][0]

//返回的引用作为左值,直接增量

cout<<"优秀人数为:"<<typeA<<endl;

cout<<"及格人数为:"<<typeB<<endl;

cout<<"不及格人数为:"<<typeC<<endl;

system("pause");

}

【案例分析】

1)这是一个简单的学生成绩统计,int grade[]定义为成绩数组,int& tA是优秀人数,然后根据输入的信息(如姓名等)进行快速查询。

2)代码int grade[],int size,int& tA,int& tB,int& tC,表示要输入的字符串。

8.5  函数与结构体、共用体及类对象

案例8-5  设计一个数据查询系统

【案例描述】

这是个综合的实例,目的加深对函数理解和综合应用。编写软件目的大部分是实现信息处理,把现实生活中的事务用程序实现。本实例是个电话号码管理方面的小软件,效果如图8-5所示。

 

8-5  设计一个数据查询系统

【实现过程】

1)定义个数据类结构TeleNumber,定义功能类TeleMessage,成员函数Insert()是添加信息,Search()查找号码,Show()显示信息。其代码如下:

#include<iostream>

#include<fstream>

#include<string>

using namespace std;

int x=0;char a;int j=1;

struct TeleNumber     //数据类

{

char name[20]; //姓名    

int phoneNumber; //固定电话号码   

int mobileNumber; //移动电话号码  

char email[30]; //电子邮箱

int s;  

TeleNumber * Next;

void ReadFile(istream & in);

void input();

void display();

};void TeleNumber::ReadFile(istream & in)    //从文件把数据读入到程序

{

in>>name>>phoneNumber>>mobileNumber>>email;

}

void TeleNumber::input() //信息输入

{

cout<<"请输入姓名"<<endl;

cin>>name;

cout<<"请输入固定电话号码"<<endl;

cin>>phoneNumber;

cout<<"请输入移动电话号码"<<endl;

cin>>mobileNumber;

cout<<"请输入电子邮箱"<<endl;

cin>>email;

s=j++;

}

void TeleNumber::display() //信息输出

{

cout<<"姓名:"<<name<<'\t'<<"固定号码:"<<phoneNumber<<'\t'

<<"移动电话号码:"<<mobileNumber<<'\t'<<"电子邮箱:"<<email<<endl;

}

class TeleMessage                           //功能类

{

public: TeleMessage();

//构造数据结构

~TeleMessage();                          

//释放单链表

TeleNumber * Search(char *);

//信息查找

void Insert();                       //插入

void Change();                         //更改

void Show();                           //显示

TeleNumber * End,* Head;

ifstream in;                           //定义读、写文件对象

ofstream out;

};

TeleMessage::TeleMessage()

{

Head=new TeleNumber;              //头插法建立单链表

     Head->Next=new TeleNumber;

End=Head->Next;

in.open("TeleNumber.text");         //打开外存文件,看是否有数据存在

if(!in)

cout<<"电话系统中没有任何号码,请输入号码"<<endl;

else

{

in.close(); cout<<"读取电话号码系统成功!"<<endl;

}

cout<<"输入任意字母继续"<<endl;        

cin>>a;

}

TeleMessage::~TeleMessage() //释放单链表     

   {

TeleNumber * temp;

while(Head->Next!=End)

{

temp=Head->Next;

Head=Head->Next;

delete temp;

}  

delete Head,End; //删除头尾指针

}

void TeleMessage::Insert()             //插入

{

End->input(); //从单链表尾部插入

End->Next=new TeleNumber;

End=End->Next;

cout<<endl<<"插入成功"<<endl;}

TeleNumber * TeleMessage::Search(char * name)

{

bool find=false;

cout<<"查找姓名:="<<name<<endl;

for(TeleNumber *p=Head->Next;p!=End;p=p->Next)

if(!strcmp(p->name,name)) //查找姓名成功

{

     p->display(); //显示信息

 find=true;

}

if(!find)

cout<<"查无此人"<<endl;

return 0;

}

void TeleMessage::Show() //显示信息

{

for(TeleNumber * p=Head->Next;p!=End;p=p->Next)

       p->display(); //显示信息

}

2)主函数显示选择帮助信息,通过判断函数switch(x),提供输入选择。代码如下:

int main()

{

bool flag=true;

TeleMessage tele;

char name[20]; //定义字符串

while(flag) //0.退出系统

{

system("cls");

cout<<"个人电话号码查询系统"<<endl<<endl;

cout<<"1.增加信息"<<endl;

cout<<"2.显示信息"<<endl;

cout<<"3.查找号码"<<endl;

cout<<"0.退出系统"<<endl<<endl;

cout<<"请选择:";

cin>>x;

switch(x)

{

case 0:flag=false;

break;

    case 1:tele.Insert();break;

case 2:tele.Show();

break;

case 3:

cout<<"请输入姓名"<<endl;

cin>>name;

tele.Search(name);

break;

}

cout<<"输入任意字母返回"<<endl;

cin>>a;

}

return 0;

}

【案例分析】

1)程序实现了一个简单的个人电话号码查询的功能,根据用户输入的信息,如姓名等进行快速查询。实现在外存上用文件保存电话号码信息,在内存中设计数据结构存储电话号码信息,根据姓名实现快速查询。

2)因电话号码信息较多,在程序运行结束后需保存电话号码信息,常采用文件的形式存放到外存中。当程序运行时,再把电话号码信息从文件调入内存来进行查找等操作。为接收文件中的内容,数据结构truct TeleNumber要与之对应,实现对电话号码的快速查询,也可将上述结构数组排序,以便运用折半查找。

 

注意:在此代码的基础上可以增加其他维护功能:例如插入、删除、修改等,按电话号码进行排序。

案例8-6  一个简单加密算法的实现

【案例描述】

本实例代码见案例9-14。下面两个实例是简单加密、解密方面的,现在是互联网时代,安全相当重要,加密、解密方法很多,如:DES3DESMD5RSA等。在网上经常用MD5码验证是不是原版。本实例讨论MD5加密算法,效果如图8-6所示。

 

8-6  一个简单加密算法的实现

【实现过程】

函数MD5Final()写入信息-摘要,MD5Encode()初始化MD5,更新MD5块操作,MD5Result()返回加密结果。其部分代码如下:

//假设长度是4的倍数,解密输入(unsigned char) 输出为 (UINT4)

static void Encode (UINT1 * output, UINT4 * input, UINT4 len)

{

 UINT4 i, j;

 for (i = 0, j = 0; j < len; i++, j += 4) {

  output[j] = (unsigned char)(input[i] & 0xff);

  output[j+1] = (unsigned char)((input[i] >> 8) & 0xff); //位运行

  output[j+2] = (unsigned char)((input[i] >> 16) & 0xff);

  output[j+3] = (unsigned char)((input[i] >> 24) & 0xff);

 }

}

//假设长度是4的倍数,解密输入(unsigned char) 输出为 (UINT4)

static void Decode (UINT4 * output, UINT1 * input, UINT4 len) {   //代码略   }

//"for loop" 替换库函数memcpy

static void MD5_memcpy (UINT1 * output, UINT1 * input, UINT4 len)

{

 UINT4 i;

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

 {

  output[i] = input[i];

 }

}

//"for loop" 替换库函数memset

static void MD5_memset(UINT1 * output, UINT1 value, UINT4 len)

{

 UINT4 i;

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

  ((UINT1 *)output)[i] = value;

}

std::string MD5Result(UINT1 digest[16])

{

 char temp[40] = {0}; //定义字符数组并清零

 UINT4 i;

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

 {

  sprintf (&(temp[i * 2]), "%02x", digest[i]); //按格式写入字符数组temp

 }

 return temp;

}std::string MD5Encode(char* src) //MD5加密

{

 MD5_CTX context;

 UINT1 digest[16];

 UINT4 len = strlen(src);

 MD5Init(&context); //初始化MD5

 MD5Update(&context, (UINT1 *)src, len); //更新MD5

 MD5Final(digest, &context); //结束MD5操作

 return MD5Result(digest); //返回加密结果

}std::string MD5Encode(char* src)

{

 MD5_CTX context;

 UINT1 digest[16];

 UINT4 len = strlen(src);

 MD5Init(&context); //初始化MD5

 MD5Update(&context, (UINT1 *)src, len); //更新MD5

 MD5Final(digest, &context); //结束MD5操作

 return MD5Result(digest); //返回加密结果

}

//传入一个密码和一个随机码,返回加密口令

std::string BBSmd5(std::string pwd,std::string sl)

{

 const char* md5s1=pwd.c_str();

 std::string md5s2=MD5Encode(md5s1)+sl; //MD5加密

 const char* md5s3=md5s2.c_str(); //字符转换

 std::string md5=MD5Encode(md5s3);

 return md5;

}

【案例分析】

1MD5的全称是Message-Digest Algorithm 5(信息-摘要算法),是把任意长度的“字节串”变换成一个128bit的大整数,它是一个不可逆的字符串变换算法。

2)主循环有四轮,每轮很相拟,见函数MD5Transform()。第一轮进行16次操作。每次操作对abcd中的其中三个进行一次非线性函数运算,然后将所得结果加上第四个变量,文本的一个子分组和一个常数。再将所得结果向右环移一个不定的数,并加上abcd中之一,最后用该结果取代abcd中之一。(&)是按位与,(|)是按位或,(~)是按位非,(^)是按位异或。

 

注意:MD5的典型应用是对一段message(字节串)产生fingerprint(指纹),以防止被“篡改”。MD5还广泛用于加密和解密技术上,在很多操作系统中,用户的密码是以MD5值或类似的其他算法的方式保存的。

案例8-7  解密算法的实现

【案例描述】

本实例代码见案例9-14。本实例继续上面的演示,讨论MD5解密的编程实现技术,解密是加密的反过程,应按相反的思路编写程序。本例效果如图8-7所示。

 

8-7  解密算法的实现

【实现过程】

函数Decode(),假定长度是4的倍数,解密输入的是字符串,字符串类型为unsigned char,解密后的类型UINT4。其部分代码如下:

//解密输入unsigned char,输出UINT4

//假定长度是4的倍数

static void Decode (UINT4 * output, UINT1 * input, UINT4 len)

{

 UINT4 i, j;

 for (i = 0, j = 0; j < len; i++, j += 4)

 {

   output[i] = (input[j]) |                 

   ((input[j+1]) << 8) |

   ((input[j+2]) << 16) |

   ((input[j+3]) << 24);                 //左移

 }

   cout<<"解密后的口令:"<<(char *)output<<endl;

}

【案例分析】

1)经过初始化处理后,MD5512位分组来处理输入的文本,每一分组又划分为1632位子分组。算法的输出由四个32位分组,再将它们级联形成一个128位散列值。

2)解密是加密的反过程,上述代码主循环有四轮,步长为4,通过左移操作实现解密。

案例8-8  使用随机函数生成密钥

【案例描述】

本实例代码见案例9-14。在信息加密过程中,密钥通常是用随机函数生成的,然后形成密文,这样就不会轻易破解。本例效果如图8-8所示。

【实现过程】

1)定义函数头文件共3个,两个同名的salt(),是重载函数。其代码如下:

 

8-8  使用随机函数生成密钥

//SALT部分函数

char saltzm();

char saltsz();

std::string salt();

void salt(char* sc1);

2)下面是3个函数实现过程。代码如下:

//SALT部分函数

char saltzm()  {

 char zimu(rand()%26+97);

 return zimu;

}

char saltsz()

{

 char shuzi(rand()%10+48);        //产生随机数

 return shuzi;  }

std::string salt()  {

 std::string sc="123456"; //标准模板库(STL)提供了一个std::string

 for (int si1=0;si1!=6;si1++)   {

  if (rand()%2)    {               //产生随机数

   sc[si1]=saltzm();    }

  else    {

   sc[si1]=saltsz();    }  }

 sc[6]='/0';

 return sc;  }

void salt(char* sc1)  {

 for (int si1=0;si1!=6;si1++)

 {

  if (rand()%2)  //产生随机数

  {

   sc1[si1]=saltzm();

  }

  else

  {

   sc1[si1]=saltsz();

  }

 }

 sc1[6]='/0';

}

【案例分析】

1)上述代码rand()%6,取值固定为06rand()%10,取值固定为010。上述代码函数名salt()是重载函数,区别在于一个给出了输入参数,另一个没有给出相应的输入参数。

2std::string是标准模板库(STL)提供了一个类,它是std::basic_string的一个特化,它是一个容器类,可把字符串当作普通类型来使用,并支持比较、连接、遍历、STL算法、复制、赋值等操作,这个类定义在<string>头文件中。

案例8-9  设计一个完整的加密解密系统

【案例描述】

这是个综合的实例,目的加深对加密解密系统理解和综合应用,加密解密技术在现在软件开发中很重要,特别是现在是互联网时代,如加密邮箱技术等。本实例涉及RC6加解密文件方面的,效果如图8-9所示。

 

8-9  设计一个完整的加密解密系统

【实现过程】

定义个需要加密的字符串m_pFileData,密钥定义为整型变量m_nKey,调用函数KeyGen()形成注册码,EncodeFile()加密一个数据包,DecodeFile()解密数据。其代码如下:

#include <math.h>

#define W 32 //单词bit大小

#define R 16 //安全评估  

char THIS_FILE[] = __FILE__;

#include<windows.h>

#include <iostream>

using namespace std;

// The one and only application object

BYTE* m_pFileData; //没有签名8位整数

UINT m_nDocLength; //没有签名16位无符号整数

DWORD m_dwS[2 * R + 4]; //一个32位无符号整数或一段和其相关的偏移地址

int m_nKey; //密钥

//左轮换,32 位整数

DWORD LeftRotate(DWORD dwVar, DWORD dwOffset)

{

DWORD temp1, temp2; //32 位整数

temp1 = dwVar >> (W - dwOffset); //右移W - dwOffset

temp2 = dwVar << dwOffset; //左移dwOffset

temp2 = temp2 | temp1;      //按位或

return temp2;

}

DWORD OffsetAmount(DWORD dwVar) //和补偿

{

int nLgw = (int)(log((double)W)/log(2.0));

dwVar = dwVar << (W - nLgw); //左移

dwVar = dwVar >> (W - nLgw); //右移

return dwVar;

}

void KeyGen(DWORD dwKey) //注册码

{

DWORD P32 = 0xB7E15163; //变化常数 P大小  

DWORD Q32 = 0x9E3779B9; //变化常数Q大小 

DWORD i, A, B;

DWORD dwByteOne, dwByteTwo, dwByteThree, dwByteFour;

dwByteOne = dwKey >> 24; //右移

dwByteTwo = dwKey >> 8; //右移

dwByteTwo = dwByteTwo & 0x0010;

dwByteThree = dwKey << 8; //左移

dwByteThree = dwByteThree & 0x0100;

dwByteFour = dwKey << 24; //左移

//按位或

dwKey = dwByteOne | dwByteTwo | dwByteThree | dwByteFour;

m_dwS[0] = P32;

for(i = 1; i < 2 * R + 4; i++)

m_dwS[i] = m_dwS[i - 1] + Q32;

i = A = B = 0;

int v = 3 * max(1, 2 * R + 4); //max取最大数

for(int s = 1; s <= v; s++)

{

A = m_dwS[i] = LeftRotate(m_dwS[i] + A + B, OffsetAmount(3));  //左轮换

B = dwKey = LeftRotate(dwKey + A + B, OffsetAmount(A + B));   //左轮换

i = (i + 1) % (2 * R + 4);

}

}

DWORD RightRotate(DWORD dwVar, DWORD dwOffset) //右轮换

{

DWORD temp1, temp2;

temp1 = dwVar << (W - dwOffset); //左移

temp2 = dwVar >> dwOffset; //右移

temp2 = temp2 | temp1; //按位或

return temp2;

}

// encrypt the file

void EncodeFile()

{  //代码见实例234  }

//解密文件

void DecodeFile()

{  //代码见实例234  }

int main()

{

    int nRetCode = 0;

    m_nDocLength=14;

    m_pFileData = new BYTE[m_nDocLength];

    memcpy(m_pFileData,(char*)"t1234567890est",m_nDocLength);//拷贝2字节到m_pFileData

cout<<"需要加密的字符串:"<<(char*)m_pFileData<<endl;

        m_nKey=1234567890;

KeyGen((DWORD)m_nKey); //注册码

EncodeFile();       //加密文件

KeyGen((DWORD)m_nKey);   //注册码

DecodeFile(); //解密文件

delete[] m_pFileData;

//delete pFile;

system("pause");

return nRetCode;

}

【案例分析】

1RC6是作为AES的候选算法,提交给美国国家标准局来作为一种新的分组密码。根据AES的要求,一个分组密码必须处理128位输入/输出数据。RC6需用432位寄存器实现,RC6把明文分别存在4个区块ABCD,刚开始分别包含明文的初始值,加密运算后则为4个密文的输出值。

2C/C++提供位逻辑运算符和移位运算符,但只能用于整型和字符型。位运算符是对每位进行操作而不影响左右两位,这有别于常规运算符(&&||!)这个运算符是将整个数进行操作的。(~)按位取反,(&)按位取与,(|)按位取或,(^)按位异或,(<<)左移,(>>)右移。

 

注意:C6是参数变量的分组算法,实际上是由3个参数确定的一个加密算法族。由三部分组成,分别为混合密钥生成过程、加密过程和解密过程。

案例8-10  数据包简单加密(加密技术)

【案例描述】

代码见案例8-9继续案例8-9的话题,讨论代码中加密和解密函数的细节的实现技术,从而加深对代码理解。本例演示RC6加密和解密算法实现,效果如图8-10所示。

 

8-10  数据包简单加密(加密技术)

【实现过程】

定义函数EncodeFile()DecodeFile(),实现数据包加密和解密。利用for循环从0到待加密文档的长度,进行加密和解密处理。其代码如下:

void EncodeFile()

{

DWORD* pdwTemp;

for(UINT i = 0; i < m_nDocLength; i += 16) //for循环

{

pdwTemp = (DWORD*)&m_pFileData[i];  //赋值

pdwTemp[0] = (pdwTemp[0] - m_dwS[2 * R + 2]);

pdwTemp[2] = (pdwTemp[2] - m_dwS[2 * R + 3]);

for(int j = R; j >= 1; j--) //for循环

{

DWORD temp = pdwTemp[3];  //赋值

pdwTemp[3] = pdwTemp[2];

pdwTemp[2] = pdwTemp[1];

pdwTemp[1] = pdwTemp[0];

pdwTemp[0] = temp;

//左轮换

DWORD t = LeftRotate((pdwTemp[1] * (2 * pdwTemp[1] + 1)),

OffsetAmount((DWORD)(log((double)W)/ log(2.0))));

//左轮换

DWORD u = LeftRotate((pdwTemp[3] * (2 * pdwTemp[3] + 1)),

OffsetAmount((DWORD)(log((double)W)/ log(2.0))));

//右轮换

pdwTemp[0] = (RightRotate((pdwTemp[0] - m_dwS[2 * j]), OffsetAmount(u))) ^ t;

//右轮换

pdwTemp[2] = (RightRotate((pdwTemp[2] - m_dwS[2 * j + 1]), OffsetAmount(t))) ^ u;

}

pdwTemp[1] = (pdwTemp[1] - m_dwS[0]);

pdwTemp[3] = (pdwTemp[3] - m_dwS[1]);

}

cout<<"加密后的字符串:"<<(unsigned char*)pdwTemp<<endl;

pdwTemp = NULL;

}

//解密文件

void DecodeFile()

{

DWORD* pdwTemp;

for(UINT i = 0; i < m_nDocLength; i += 16)

{

pdwTemp = (DWORD*)&m_pFileData[i];

pdwTemp[1] = (pdwTemp[1] + m_dwS[0]); //赋值

pdwTemp[3] = (pdwTemp[3] + m_dwS[1]);

for(int j = 1; j <= R; j++)

{

//左轮换

DWORD t = LeftRotate((pdwTemp[1] * (2 * pdwTemp[1] + 1)),

OffsetAmount((DWORD)(log((double)W)/ log(2.0))));

//左轮换

DWORD u = LeftRotate((pdwTemp[3] * (2 * pdwTemp[3] + 1)),

OffsetAmount((DWORD)(log((double)W)/ log(2.0))));

//左轮换

pdwTemp[0] = (LeftRotate(pdwTemp[0] ^ t, OffsetAmount(u)) + m_dwS[2 * j]);

//^异或运算

pdwTemp[2] = (LeftRotate(pdwTemp[2] ^ u, OffsetAmount(t)) + m_dwS[2 * j + 1]);

DWORD temp = pdwTemp[0]; //赋值

pdwTemp[0] = pdwTemp[1];

pdwTemp[1] = pdwTemp[2];

pdwTemp[2] = pdwTemp[3];

pdwTemp[3] = temp;

}

pdwTemp[0] = (pdwTemp[0] + m_dwS[2 * R + 2]); //赋值

pdwTemp[2] = (pdwTemp[2] + m_dwS[2 * R + 3]);

}

cout<<"解密后的字符串:"<<(unsigned char*)pdwTemp<<endl;

pdwTemp = NULL;

}

【案例分析】

1)函数EncodeFile()调用LeftRotate左轮换和RightRotate右轮换;函数DecodeFile()是反过程。m_nDocLength待加密文档长度,R安全评估强度,m_pFileData待加密文档内容。

2RC6是参数变量的分组算法,实际上是由3个参数确定的一个加密算法族。一个特定的RC6可以表示为RC6wrb3个参数wrb分别为字长、循环次数和密钥长度。AES中,w=32r=20。本实例中,密钥长度b128(16字节)RC64w位的寄存器ABCD来存放输入的明文和输出的密文。明文和密文的第一个字节存放在A的最低字节,经过加解密后,得到的明文和密文的最后一个字节存放在D的最高字节中。

8.6  函数编写的建议

案例8-11  补充代码使输出结果成立

【案例描述】

在某些应用情况定义函数时,不能确定函数的参数个数。如定义个函数需要打印输入多个字符串变量的内容,可用省略号指定参数表,参数的个数在调用时才能确定。本例效果如图8-11所示。

 

8-11  补充代码使输出结果成立

【实现过程】

2个演示,demo2()调用fun(),输入5个整数,用指针指向5个整数,打印输出。demo1()调用函数simple_va_fun()。代码实现如下:

#include <iostream>

using std::cout;

using std::endl;

 

#include <string>

using std::string;

 

int main()

{

   string string1( "cat" ); //定义string

   string string2;

   string string3;

 

   string2 = string1;

   string3.assign( string1 );

   

   //输出字符串

   cout << "string1: " << string1 << "\nstring2: " << string2

      << "\nstring3: " << string3 << "\n\n";

 

   //重载‘+=

   char *str10=" pet";

   char *str11=" acomb";

   string3 += str10; //加个字符串

   string1.append( str11 ); //加个字符串

  

      //输出字符串

cout << "string1: " << string1 << "\nstring2: " << string2

      << "\nstring3: " << string3 << "\n\n";

 

   //重载‘+=

   char str12='2';

   char str13='3';

   string3 += str12; //加个字符

   string1.append(3,str13); //加个字符

 

      //输出字符串

cout << "string1: " << string1 << "\nstring2: " << string2

      << "\nstring3: " << string3 << "\n\n";

system("pause");

 

   return 0;

}

【案例分析】

1)函数fun(),参数是以数据结构栈的形式存取,可实现从右至左入栈操作。包含在头文件“stdarg.h”中,用到的3个库函数va_start( )va_arg( )va_end( )。要说明一个va_list类型的变量,va_listintfloat类相同,它是C++系统预定义的一个数据类型(float),只有通过这种类型的变量才能从实际参数表中取出可变有参数;如va_list ap;va_start():有两个参数va_start(ap,b); b即为可变参数前的最后一个确定的参数。va_arg((ap,int)有两个参数int即为可变参数的数据类型名。

2)使用参数数目可变的函数时要注意以下两点:

1、在定义函数时,固定参数部分必须放在参数表的前面,可变参数放在后面,并用省略号“...”表示可变参数。在函数调用时,可以没有可变的参数,如代码simple_va_fun(int start, ...)

2、必须使用函数va_start()来初始化可变参数,为取第一个可变的参数作好准备工作;使用函数va_arg()依次取各个可变的参数值;最后用函数va_end()做好结束工作,以便能正确地返回。

 

注意:在调用参数个数可变的函数时,必定有一个参数指明可变参数的个数或总的实参个数。

案例8-12  清空屏幕——清屏的实现

【案例描述】

C语言中清屏函数是clrscr(),头文件是conio.h。那么在C++中清屏函数是什么呢?在Windows下参考微软VC++开发平台帮助MSDN后,调用API函数,自己写了个清屏函数。本例效果如图8-12所示。

【实现过程】

保存缓冲区信息,用空格填充缓冲区FillConsoleOutputCharacter,光标返回屏幕左上角坐标。其代码如下:

#include <iostream>

#include <windows.h>

using namespace std;

//API 报错宏定义 

#define PERR(bSuccess, api){if(!(bSuccess)) printf("%s:Error %d from %s on line %d\n", __FILE__, GetLastError(), api, __LINE__);}

void MyCls(HANDLE) ;

inline void clrscr(void)

{

HANDLE hStdOut=GetStdHandle(STD_OUTPUT_HANDLE);

MyCls(hStdOut);

return;

}

void MyCls(HANDLE hConsole)

{

COORD coordScreen={0,0};    //设置清屏后光标返回屏幕左上角的坐标

BOOL bSuccess;

DWORD cCharsWritten;

CONSOLE_SCREEN_BUFFER_INFO csbi;  //保存缓冲区信息

DWORD dwConSize;  //当前缓冲区可容纳的字符数

//获得缓冲区信息

bSuccess=GetConsoleScreenBufferInfo(hConsole,&csbi);

PERR(bSuccess,"GetConsoleScreenBufferInfo");

dwConSize=csbi.dwSize.X * csbi.dwSize.Y; //缓冲区容纳字符数目

//用空格填充缓冲区

bSuccess=FillConsoleOutputCharacter(hConsole,(TCHAR)' ,dwConSize,coordScreen,&cCharsWritten);

PERR(bSuccess,"FillConsoleOutputCharacter");

bSuccess=GetConsoleScreenBufferInfo(hConsole,&csbi);  //获得缓冲区信息

PERR(bSuccess,"ConsoleScreenBufferInfo");

//填充缓冲区属性

bSuccess=FillConsoleOutputAttribute(hConsole,csbi.wAttributes,dwConSize,coordScreen,&cCharsWritten);

PERR(bSuccess,"FillConsoleOutputAttribute");

//光标返回屏幕左上角坐标

bSuccess=SetConsoleCursorPosition(hConsole,coordScreen);

PERR(bSuccess,"SetConsoleCursorPosition");

return;

}

void main()

{

cout<<"cls demo"<<endl;

system("pause");

clrscr() ;

system("pause");

}

【案例分析】

1)这个程序可以实现清屏,可以让coutprintf等标准输出函数输出带颜色、光标定位的功能。

2)获取控制台输出句柄的方法:

hStdOut = GetStdHandle(STD_OUTPUT_HANDLE)

3FillConsoleOutputAttribute()函数用来为控制台的部分字体着色。

4)参数的意义:

— HANDLE hOutputHandle,控制台输出句柄

— WORD wAttribute,颜色属性

— DWORD nLength,上色的字符串长度

— COORD dwWriteCoord,上色坐标

— LPDWORD lpNumberOfAttrsWritten,返回填充的个数

— DWORD型的指针,输出成功上色的长度

SetConsoleTextAttributeAPI设置字体颜色和背景色的函数。

 

注意:通常system("cls"),这种办法的缺点是程序额外运行系统程序执行清屏操作,延长了程序执行时间。

案例8-13  七彩文字——改文字色

【案例描述】

WindowsAPI函数GetStdHandle()SetConsoleTextAttribute()来实现彩色背景及彩色文本,编写个一个新颖的输出信息和提示内容的程序。下面来实现一个彩色的Hello World!,效果如图8-13示。

 

8-13  七彩文字——改文字色

【实现过程】

定义函数SetColor(),调用API函数SetConsoleTextAttribute()设置文本及背景颜色。其代码如下:

#include<windows.h>              //GetStdHandleSetConsoleTextAttribute函数头文件

#include<iostream>

using namespace std;

//给参数默认值,使它可以接受0/1/2个参数

void SetColor(unsigned short ForeColor=4,unsigned short BackGroundColor=0)                                                                      

{

//本例以输出为例

HANDLE hCon = GetStdHandle(STD_OUTPUT_HANDLE);    

SetConsoleTextAttribute(hCon,ForeColor|BackGroundColor);

};

int main()

{   

SetColor(); //调用设置颜色函数

cout<<"Hello world!"<<endl;

SetColor(40,30);

cout<<"Hello world!"<<endl;

SetColor(0xF0,0); //恢复亮白色背景黑色字

system("pause");

return 0;}

【案例分析】

1)函数HANDLE GetStdHandle(DWORD nStdHandle);,返回标准的输入、输出或错误的设备的句柄。

2)函数SetConsoleTextAttribute()的作用是在console程序设置输入或输出文本的文本颜色和背景颜色。只有在此函数设置后才能显示彩色的文本。

案例8-14  设计一个黑客界面动画

【案例描述】

本实例演示的是一个简单的黑客帝国字幕雨,一个较酷屏幕保护程序。模仿《黑客帝国》电影的开场画面中,富含高科技韵味的数码坠落效果画面,本例效果如图8-14所示。

 

8-14  设计一个黑客界面动画

【实现过程】

窗口类WNDCLASS,包含窗口类的全部信息。RegisterClass()注册某一类型的窗口。CreateWindow()该函数创建一个重叠式窗口、弹出式窗口或子窗口。init()初始化。代码实现如下:

#include <windows.h>

#define ID_TIMER    1

#define STRMAXLEN   30 //一个显示列的最大长度

#define STRMINLEN   12 //一个显示列的最小长度

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

typedef struct tagCharChain //把整个屏幕当作一个显示列,这是个双向列表

{

struct tagCharChain *prev;   //链表的前一个元素

TCHAR  ch; //显示列中的一个字符

struct tagCharChain *next;   //链表的后一个元素

}CharChain, *pCharChain;

typedef struct tagCharColumn

{

CharChain *head, *current, *point;

int x, y, iStrLen; //显示列的开始显示的x,y坐标,iStrLen是这个列的长度

//已经停滞的次数和必须停滞的次数,必须停滞的次数是随机的

int iStopTimes, iMustStopTimes;

}CharColumn, *pCharColumn;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,

    PSTR szCmdLine, int iCmdShow) //32位应用程序的入口点

{

static TCHAR szAppName[] = TEXT ("matrix") ;

HWND      hwnd ;

MSG      msg ;

WNDCLASS     wndclass ;  

     //代码略

if(!RegisterClass (&wndclass))

{

     MessageBox (NULL, TEXT ("此程序必须运行在NT!"), szAppName, MB_ICONERROR) ;

     return 0;

}

hwnd = CreateWindow (szAppName, NULL,

                             WS_DLGFRAME | WS_THICKFRAME | WS_POPUP,

                             0, 0,

                             GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN),

                             NULL, NULL, hInstance,

                             NULL) ;

 

ShowWindow (hwnd, SW_SHOWMAXIMIZED) ; //最大化显示

UpdateWindow (hwnd) ;

ShowCursor(FALSE); //隐藏鼠标光标

srand ((int) GetCurrentTime ()); //初始化随机数发生器

while (GetMessage (&msg, NULL, 0, 0))

{

     TranslateMessage (&msg) ;

     DispatchMessage (&msg) ;

}

ShowCursor(TRUE); //显示鼠标光标

return msg.wParam ;

}

TCHAR randomChar() //随机字符产生函数

{

return (TCHAR)(rand()%(126-33)+33); //33126之间

}

int init(CharColumn *cc, int cyScreen, int x) //初始化

 { //代码略  }

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 

 {//代码见:实例209 }

【案例分析】

1)操作系统调用WinMain()函数,可作为一个32位应用程序的入口点。WinMain()函数初始化应用程序,显示主窗口,进入一个消息接收一个发送循环,这个循环是应用程序执行的其余部分的*控制结构。RegisterClass()函数注册,在随后调用CreateWindow()函数和CreateWindowEx()函数中使用的窗口类。

2tagCharChain为双向列表,也就是把整个屏幕当作一个显示列,这是个双向列表,列表存储的值是一个显示列中的一个字符。

 

注意:初始化函数init(),显示列的长度,显示列的x坐标和y坐标等。

案例8-15  文字闪动效果的实现

【案例描述】

本实例代码见实例8-14,继续讨论文字显示。在日常电脑中经常用到文字闪动效果,如互联网上文字广告等,文字的颜色是要随时间而改变。本例利用Windows API函数实现文字闪动效果,效果如图8-15所示。

 

8-15  文字闪动效果的实现

【实现过程】

实现一个窗口消息处理函数WndProc()功能。代码实现如下:

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

{

      //ctn用来确定一个显示链是否向下前进,如果等待次数超过必须等待的次数,ctn就代表要向下前进

      HDC    hdc ;

      //j为一个显示链中除链表头外的在屏幕上显示的y坐标,temp绿色过度到黑色之用

      int i, j, temp, ctn;

      static   HDC hdcMem;

      HFONT    hFont;

      static   HBITMAP hBitmap;

      static   int cxScreen, cyScreen; //屏幕的宽度、高度.

      static   int iFontWidth=10, iFontHeight=15, iColumnCount;//字体的宽度、高度,列数

      static   CharColumn *ccChain;

      switch (message)

      {

case WM_CREATE:

     cxScreen = GetSystemMetrics(SM_CXSCREEN) ; //屏幕宽度

     cyScreen = GetSystemMetrics(SM_CYSCREEN) ;

     SetTimer (hwnd, ID_TIMER, 10, NULL) ;            //设置定时器

     hdc = GetDC(hwnd);

     hdcMem = CreateCompatibleDC(hdc); //创建一个与指定设备兼容的内存设备上下文环境

          //创建与指定的设备环境相关的设备兼容的位图

     hBitmap = CreateCompatibleBitmap(hdc, cxScreen, cyScreen);

     SelectObject(hdcMem, hBitmap);

     ReleaseDC(hwnd, hdc);

     //创建字体

     hFont = CreateFont(iFontHeight, iFontWidth-5, 0, 0, FW_BOLD, 0, 0, 0,

DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,

DRAFT_QUALITY, FIXED_PITCH | FF_SWISS, TEXT("Fixedsys"));

     SelectObject(hdcMem, hFont);

     DeleteObject (hFont) ;

     SetBkMode(hdcMem, TRANSPARENT); //设置背景模式为透明

     iColumnCount = cxScreen/(iFontWidth*3/2); //屏幕所显示字母雨的列数

     ccChain = (pCharColumn)calloc(iColumnCount, sizeof(CharColumn));

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

     {

init(ccChain+i, cyScreen, (iFontWidth*3/2)*i);

     }

     return 0 ;

case WM_TIMER:  //代码略  

case WM_RBUTTONDOWN:

     KillTimer (hwnd, ID_TIMER) ;

     return 0;

case WM_RBUTTONUP:

     SetTimer (hwnd, ID_TIMER, 10, NULL) ;

     return 0;

//处理善后工作

case WM_KEYDOWN:

case WM_LBUTTONDOWN:

case WM_DESTROY:

     KillTimer (hwnd, ID_TIMER) ;            //取消定时器

     DeleteObject(hBitmap);                      //释放资源

     DeleteDC(hdcMem);

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

     {

free( (ccChain+i)->current );

     }

     free(ccChain);

     PostQuitMessage (0) ;                     //向系统表明有个线程提出终止请求

     return 0 ;

}

return DefWindowProc (hwnd, message, wParam, lParam) ;

}

【案例分析】

1CreateFont()创建字体函数,WM_TIMER实现定时器功能。

(2WndProc()窗口消息处理,窗口消息有WM_CREATE、WM_TIMER、WM_RBUTTONDOWN和WM_RBUTTONUP。

8.7  本章练习