第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;
}
【案例分析】
(1)demo1()代码是正确的。因为字符型常量"hello world!"存放在静态数据区,把该字符型常量存放的静态数据区的首地址赋值给了指针,所以returnStr1函数退出时,该字符型常量所在内存不会被回收,故能够通过指针顺利无误地访问。
(2)demo2()中,把一个字符型常量"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()函数使用引用形参成功交换了a,b变量,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,也就是说b和a是同一个内存地址,b也就相当于a的别名;这时如果b = 2改变b的内存地址的内容,也就改变a的内容,所以a也是2。int &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大小的数组,直接下标1:1的映射
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大小的数组,直接下标1:1的映射得到ptr_func map_func[65536]。typedef void (*ptr_func)()定义ptr_func类型。
注意:除上面方法外,最直接的方法当然是用switch和case,但显然不是一个高效的方法。网上有提到“函数映射表“概念,但是并没有找到具体的原理和实现的方法。或者换句话说,有没有别的高效率的方法来执行一个相当多个case的switch语句。
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。下面两个实例是简单加密、解密方面的,现在是互联网时代,安全相当重要,加密、解密方法很多,如:DES、3DES、MD5、RSA等。在网上经常用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;
}
【案例分析】
(1)MD5的全称是Message-Digest Algorithm 5(信息-摘要算法),是把任意长度的“字节串”变换成一个128bit的大整数,它是一个不可逆的字符串变换算法。
(2)主循环有四轮,每轮很相拟,见函数MD5Transform()。第一轮进行16次操作。每次操作对a、b、c和d中的其中三个进行一次非线性函数运算,然后将所得结果加上第四个变量,文本的一个子分组和一个常数。再将所得结果向右环移一个不定的数,并加上a、b、c或d中之一,最后用该结果取代a、b、c或d中之一。(&)是按位与,(|)是按位或,(~)是按位非,(^)是按位异或。
注意: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)经过初始化处理后,MD5以512位分组来处理输入的文本,每一分组又划分为16个32位子分组。算法的输出由四个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,取值固定为0到6;rand()%10,取值固定为0到10。上述代码函数名salt()是重载函数,区别在于一个给出了输入参数,另一个没有给出相应的输入参数。
(2)std::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;
}
【案例分析】
(1)RC6是作为AES的候选算法,提交给美国国家标准局来作为一种新的分组密码。根据AES的要求,一个分组密码必须处理128位输入/输出数据。RC6需用4个32位寄存器实现,RC6把明文分别存在4个区块A、B、C、D,刚开始分别包含明文的初始值,加密运算后则为4个密文的输出值。
(2)C/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待加密文档内容。
(2)RC6是参数变量的分组算法,实际上是由3个参数确定的一个加密算法族。一个特定的RC6可以表示为RC6一w/r/b,3个参数w、r和b分别为字长、循环次数和密钥长度。AES中,w=32、r=20。本实例中,密钥长度b为128位(16字节)。RC6用4个w位的寄存器A、B、C、D来存放输入的明文和输出的密文。明文和密文的第一个字节存放在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_list与int、float类相同,它是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)这个程序可以实现清屏,可以让cout和printf等标准输出函数输出带颜色、光标定位的功能。
(2)获取控制台输出句柄的方法:
hStdOut = GetStdHandle(STD_OUTPUT_HANDLE)
(3)FillConsoleOutputAttribute()函数用来为控制台的部分字体着色。
(4)参数的意义:
— HANDLE hOutputHandle,控制台输出句柄
— WORD wAttribute,颜色属性
— DWORD nLength,上色的字符串长度
— COORD dwWriteCoord,上色坐标
— LPDWORD lpNumberOfAttrsWritten,返回填充的个数
— DWORD型的指针,输出成功上色的长度
SetConsoleTextAttribute是API设置字体颜色和背景色的函数。
注意:通常system("cls"),这种办法的缺点是程序额外运行系统程序执行清屏操作,延长了程序执行时间。
案例8-13 七彩文字——改文字色
【案例描述】
用Windows的API函数GetStdHandle()和SetConsoleTextAttribute()来实现彩色背景及彩色文本,编写个一个新颖的输出信息和提示内容的程序。下面来实现一个彩色的Hello World!,效果如图8-13所示。
图8-13 七彩文字——改文字色
【实现过程】
定义函数SetColor(),调用API函数SetConsoleTextAttribute()设置文本及背景颜色。其代码如下:
#include<windows.h> //GetStdHandle和SetConsoleTextAttribute函数头文件
#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); //33到126之间
}
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()函数中使用的窗口类。
(2)tagCharChain为双向列表,也就是把整个屏幕当作一个显示列,这是个双向列表,列表存储的值是一个显示列中的一个字符。
注意:初始化函数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) ;
}
【案例分析】
(1)CreateFont()创建字体函数,WM_TIMER实现定时器功能。
(2)WndProc()窗口消息处理,窗口消息有WM_CREATE、WM_TIMER、WM_RBUTTONDOWN和WM_RBUTTONUP。
8.7 本章练习