同样,函数设计的实现方法也是具有多样化的,实现的方式也不尽相同。有时可以使用普通函数,有时也可以用宏定义。下面就举一个例子说明这个问题。
【例25.5】编写一个将整型数转换为字符串的程序。
设计函数尽量使用void类型,从而简化程序设计。
为了设计方便,先将整数转换为逆序的字符串,然后再将其反序,转换成正确的字符串。程序中完成将整数转换为逆序的字符串序后,用printf语句将其输出以供验证。
#include <stdio.h> void itoa (char *buf , int num ); // 转换函数 void reverse ( char buf , int i ); // 反序函数 void swap ( char *a , char *b ); // 交换函数 int main () { int num = 0 ; // 接受键盘输入应初始化为0 char buf[128] ; // 缓存 printf (\" 输入数字: \" ); scanf (\"%d\" ,&num ); itoa ( buf , num ); printf (\" 转换的字符串:%sn\" , buf ); return 0 ; } // 数字- 字符转换函数 void itoa (char *buf , int num ) { int i=0 ; // 数字转换为倒序的字符串 do { buf[i] = num % 10 + \'0\' ; // 加数字0 的值完成转换 i++ ; num /= 10 ; }while (num !=0 ); buf[i]=\'\0\' ; // 添加结束符 printf (\" 逆序:%sn\" ,buf ); // 验证信息 reverse (buf , i ); // 反序 } void reverse ( char buf , int i ) { int j=0 ; // 字符串反序 for (j=0 ; j<i/2 ; j++ ) swap ( &buf[j] , &buf[i-1-j] ); } void swap ( char *a , char *b ) { char temp ; temp = *b ; *b = *a ; *a = temp ; }
运行示范如下:
输入数字: 10250986 逆序:68905201 转换的字符串:10250986
swap函数应该使用指针传递参数,调用使用地址符&。如果设计为如下形式:
void swap ( char a , char b ) { char temp ; temp = b ; b = a ; a = temp ; }
则实现不了。可以定义为宏,则不用使用&传递参数。为了简单,直接将宏定义在reverse函数里。下面使用宏定义swap函数,程序中给出两种方式:位运算和使用临时变量进行交换。其实,也可以不使用临时变量,直接对变量进行运算实现交换。在程序中注释掉一种,使用位运算实现交换。下面是程序实现和运行示范。
#include <stdio.h> void itoa (char *buf , int num ); // 转换函数 void reverse ( char buf , int i ); // 反转函数 int main () { int num = 0 ; // 接受键盘输入应初始化为0 char buf[128] ; // 缓存 printf (\" 输入数字: \" ); scanf (\"%d\" ,&num ); itoa ( buf , num ); printf (\" 转换的字符串:%sn\" , buf ); return 0 ; } // 数制转换函数内部使用宏定义swap void itoa (char *buf , int num ) { int i=0 ; // 数字转换为倒序的字符串 do { buf[i] = num % 10 + \'0\' ; // 加数字0 的值完成转换 i++ ; num /= 10 ; }while (num !`=0 ); buf[i]=\'\0\' ; // 添加结束符 printf (\" 逆序:%sn\" ,buf ); // 验证信息 reverse (buf , i ); // 反序 } // 定义反序函数reverse void reverse ( char buf , int i ) { int j=0 ; #if 0 // 定义交换宏实现反转 #define SWAP (a ,b ) do{a= (a )+ (b ); b= (a )- (b ); a= (a )- (b ); }while (0 ) #endif // 使用异或定义交换宏,异或运行快(加法要有进位操作) #define SWAP (a ,b ) do{a= (a )^ (b ); b= (a )^ (b ); a= (a )^ (b ); }while (0 ) // 字符串反序 for (j=0 ; j<i/2 ; j++ ) SWAP ( buf[j] , buf[i-1-j] ); } 输入数字: 30257890 逆序:09875203 转换的字符串:30257890
函数设计要在能完成功能的前提下,尽量简单。尽可能设计为void类型,也是出于这个考虑。
另外,要注意传结构参数时,是要整体复制结构体的数据。如果结构很大,例如结构里有很大的数组,这种复制是很费时的。同样,如果返回值是结构,也需要将结构值整体复制到函数外。为了避免这种费时的操作,常传递结构的指针(它们的地址值),返回也是一样。
尤其要注意字符串常量与字符串数组的区别,字符串常量是分配在全局文字常量区,传递效率高。
注意使用一些算法技巧来简化函数设计,如在第17章的例17.10和例17.11中,用构造不同的数进行位运算以解决统计一个数的二进制中包含1的个数问题。其实,还可以构造出一些特殊数字来简化求解过程并提高求解的速度。另外,还可以使用一些编程技术提高程序性能,如状态机等技术。
【例25.6】编写统计一个数的二进制中包含1的个数的程序。
第17章的两种编程方法都是使用比较的方法,效率较低。现在使用加法运算。编写时一般先求正确,正确之后再求优化,不要优化过早,以免进入歧路。
现在先以0xff为例,构造一个数0x55,0x55的特点是01相隔。把两者进行与运算。在结果一行中的注释符号里把它们用序号编号并记为第1次&结果。
1111 1111 & 0101 0101 0101 0101 // (1 ) 第1 次&0x55 的结果
“0xff&0x55=0x55”。解释一下结果的含义:结果里的1的含义是0xff如果偶数位有1,则这2位里的数字就是1(01),由此可知结果代表有4个1。
将0xff右移“0xff>>1”,还是0xff,再与0x55进行&运算。
1111 1111 >>1 //0xff 右移, 0xff>>1 & 0111 1111 // 再&0x55 第1 次与 0101 0101 // (2 ) 第2 次&0x55 的结果
这即相当于把奇数位的1保留,也是4个1。
将两次二进制数相加,即
0101 0101 // (1 ) + 0101 0101 // (2 ) 1010 1010 // (3 ) 第1 轮结果
二进制两两相加时,不要把它看做一个数,而是表示两位代表1的个数相加。即原来两位表示1的个数(二进制)相加,现在的2位就是代表具有几个1(只能是0或1,2)。上面的结果表明每两位都是2,下面就是要考虑如何将4个2相加,使其结果代表总共具有的1的位数是8位。
现在的运算结果的十六进制是0xaa,下面进行第二轮操作。这次操作使用0x33,用它与(3)进行&操作。
0011 0011 // 构造一个数0x33 & 1010 1010 //& (3 ) 0010 0010 // (4 ) 第1 次&0x33 的结果
与第1轮一样,要将(3)的结果移位后再&0x33,但这次是移2位。
0010 1010 // 将(3 )的结果0xaa>>2 & 0011 0011 //0x33 0010 0010 // (5 ) 第2 次&0x33 的结果
进行(5)+(6)运算。
0010 0010 // (4 ) + 0010 0010 // (5 ) 0100 0100 // (6 )
经过两轮得到0x44,第3轮要计算4+4=8,再构造0xf(0000 1111)。与前两轮的方法一样,继续做下去。用0x44,选择0xf进行第3轮如下:
0000 1111 // 构造一个数0xf & 0100 0100 // (6 ) 0000 0100 // (7 ) 第1 次&0xf 的结果 0000 0100 // 将(6 )>>4 & 0000 1111 //0xf 0000 0100 // (8 ) 第2 次&0xf 的结果 + 0000 0100 // (7 ) 0000 1000 // ( 8 )+ (7 ) = 8
由此得出8位的编程方法。固定次数8bit用到2的3次方,先定义3个常量。
#define M1 0x55 #define M2 0x33 #define M3 0x0f
对给定的num,将上述3个步骤写出如下的公式。
(num & M1 ) + ((num >> 1 ) & M1 ) (num & M2 ) + ((num >> 2 ) & M2 ) (num & M3 ) + ((num >> 4 ) & M3 )
根据如上公式,编写如下程序。
#include <stdio.h> #define M1 0x55 #define M2 0x33 #define M3 0x0f int main () { int number , num=0 ; printf (\" 输入数字: \" ); scanf (\"%d\" ,&number ); num=number ; num = (num & M1 ) + ((num >> 1 ) & M1 ); printf (\"num=%#xn\" , num ); num = (num & M2 ) + ((num >> 2 ) & M2 ); printf (\"num=%#xn\" , num ); num = (num & M3 ) + ((num >> 4 ) & M3 ); printf (\"num=%#xn\" , num ); printf (\"%d 含有%d 个1n\" , number , num ); return 0 ; }
使用255验证上述算法,第1次是0xaa,第2次是0x44,第3次是0x8,即8个1。
输入数字: 255 num=0xaa num=0x44 num=0x8 255 含有8 个1
32位要定义M4和M5,并构造5个常量。
//32 位程序 #include <stdio.h> #define M1 0x55555555 #define M2 0x33333333 #define M3 0x0F0F0F0F #define M4 0x0FF00FF #define M5 0x0000FFFF int main () { int number , num=0 ; printf (\" 输入数字: \" ); scanf (\"%d\" ,&number ); num=number ; num = (num & M1 ) + ((num >> 1 ) & M1 ); num = (num & M2 ) + ((num >> 2 ) & M2 ); num = (num & M3 ) + ((num >> 4 ) & M3 ); num = (num & M4 ) + ((num >> 8 ) & M4 ); num = (num & M5 ) + ((num >> 16 ) & M5 ); printf (\"%d 含有%d 个1n\" , number , num ); return 0 ; }
运行示范如下:
输入数字: 65535 65535 含有16 个1 输入数字: 100 100 含有3 个1 输入数字: 0 0 含有0 个1
程序中去掉了打印每次结果的信息,这个程序很确定,不足之处是0也要5次,但也是很确定的5次,所以也是可以的。
很多程序还要受到条件的影响,约瑟夫环就是典型的例子。根据要求,可以使用一维数组、二维数组、结构、动态内存、链表、循环链表等手段编写求解程序。可以参考拙作《C程序设计课程设计(第2版)》(机械工业出版社)。