相信很多人在初学某门计算机语言的时候都会做过类似的题目:在控制台上输出用特定字符'拼'出来的某种图形,比如下面的这种三角形:
*
***
*****
*******
*********
这样的问题应该算是入门级的了,大多人都是看之,做之,忘之,而今天我就拿这种入门级的题目说事,小问题里也许内含有大道理。
昨晚无意中在编程爱好者论坛看到这样一道三角形输出的C语言例题,内容大致如下:
用*号组合成一个三角形!行数由键盘输入(范围为:1~20,输入超过范围,则提示出错)。如:
输入一个数4,则输出以下图形:
*
***
*****
*******
共四行;如输入的是6,则输出以下图形:
*
***
*****
*******
*********
***********
共六行,依次类推。
对于这类题目,大多数人见到后都会马上敲键盘,也许十分钟或更少时间就能拿出了一个解决方案,比如论坛上发表的一段代码:
int main()
{
int i, j, n;
printf("Please enter the number of col(1~20):\n");
while(1) {
scanf("%d",&n);
if(1<=n && n<=20) {
break;
} else {
printf("out of range(1~20),please retype:\n");
}
}
for(i=0; i<n; i++) {
for(j=i; j<n-1; j++)
printf(" ");
for(j=0; j<2*i+1; j++)
printf("*");
printf("\n");
}
return 0;
}
这段代码的实现初步看起来应该是没有问题的,而且很多人的答案与之都大同小异,但是从另外一个角度来看似乎这里存在些小问题。从什么角度呢?从设计角度。我们来看,上面的代码只是从功能的角度去考虑了,导致代码很'死',输出逻辑与图形生成逻辑交叠在一起了,或者说根本谈不上有什么设计的思维在里面。
这时有人会问:这么小的问题还需要设计吗?如果你是在参加ACM竞赛,这么实现一点问题没有,又快又正确。但是如果从一个工程的角度来看,无论问题大小与否,都需要有设计。
类似这种输出三角形或者输出实心(或空心)菱形的问题实际上都可以看成是将一个含有特殊字符的二维数组(或矩阵)输出的一个过程。我们完全可以将输出和形成图形这两种逻辑正交分开。我们的大致思路就是:校验输入 -> 根据输入的参数,确认画布(二维数组)的尺寸 -> 在画布上描点 -> 将画布整体输出到控制台上,这样的逻辑有些类似于Windows GUI图形的输出。
typedef struct {
int row;
int col;
int *p;
} canvas; //画布结构
int prepare_canvas(canvas *p_cns, /* in/out */
int row, /* in */
int col); /* in */
void get_option(int *row, /* out */
int *col); /* out */
void draw(canvas *p_cns); /*in/out */
void show(const canvas *p_cns);
void release_canvas(canvas *p_cns); /*in/out*/
/* 提供一个宏,方便访问画布上的像素点 */
#define CANVAS_PIXEL(p_cns, i, j) \
*((p_cns)->p + (i) * (p_cns)->col + (j))
int main(int argc, char *argv[]) {
int rv, row, col;
canvas cns;
get_option(&row, &col);
rv = prepare_canvas(&cns, row, col);
if (rv != 0) {
printf("fail to prepare_canvas!\n");
return;
}
draw(&cns); //描点逻辑
show(&cns); //输出逻辑
release_canvas(&cns);
return 0;
}
void get_option(int *row, int *col) {
int n;
do {
printf("enter the number of lines:\n");
scanf("%d", &n);
if (n 20) {
printf("out of range(3~20), please re-enter.\n");
} else {
*row = n;
*col = 2 * n -1; //确定画布大小
break;
}
} while (1);
}
int prepare_canvas(canvas *p_cns, int row, int col) {
p_cns->row = row;
p_cns->col = col;
p_cns->p = (int*)malloc(row * col * sizeof(int));
if (p_cns->p == NULL) {
printf("fail to malloc the canvas!\n");
return -1;
}
memset(p_cns->p, ' ', row * col * sizeof(int)); //将画布上的'像素点'都置为空白
return 0;
}
void draw(canvas *p_cns) { //生成图形逻辑,我们的focus点
int row, col;
for (row = 0; row
row; row++) {
for(col =p_cns->row-1-row ; col row-1+row; col++) {
CANVAS_PIXEL(p_cns, row, col) = '*';
}
}
}
void show(const canvas *p_cns) { //图形的通用输出逻辑,此后无须关注
int row, col;
for (row = 0; row
row; row++) {
for (col = 0; col
col; col++) {
printf("%c", CANVAS_PIXEL(p_cns, row, col));
}
printf("\n");
}
}
void release_canvas(canvas *p_cns) {
if (p_cns->p != NULL) {
free(p_cns->p);
}
}
也许单单从上面的例子来看,你会觉得这样做的代码量会多出几倍。这里我们不妨再考虑另一个道问题:输入一个奇数n,输出对角线长为n的实心菱形。比如:
*
***
*****
***
*
使用我们上面的设计,我们只需改造几个地方就可以完成这个问题。main函数是不需要改动的。我们需要关注的只是输入的校验逻辑和draw的逻辑。
void get_option(int *row, int *col) {
int n;
do {
printf("enter the number of lines:\n");
scanf("%d", &n);
if (n 20) {
printf("out of range(1~20), please re-enter.\n");
} else if (n % 2 == 0) { //增加是否为奇数的判断
printf("the number is not even, please re-enter.\n");
} else {
*row = n;
*col = n; //画布规模需根据问题的不同而定
break;
}
} while (1);
}
void draw(canvas *p_cns) {
int row, col;
int temp_row = (p_cns->row+1)/2;
for (row = 0; row < temp_row; row++) {
for(col = temp_row -1-row ; col <= temp_row-1+row; col++) {
CANVAS_PIXEL(p_cns, row, col) = '*'; //画菱形的上半部分
CANVAS_PIXEL(p_cns, p_cns->row-1-row, col) = '*'; //画镜像
}
}
}
有了三角形输出设计的基础,我们完成菱形输出已经很easy了,关键是我们可以focus到图形生成逻辑,也就是更关注问题域了。'画布'这种概念为图形生成逻辑提供了一个很好的复用平台,而且也符合我们头脑中的平面思维逻辑。
以上问题如果用面向对象的语言来实现,用面向对象的思维(我们的那个main是否类似template method)和特性(override)就更容易实现我们的这个设计了。
评论