相信很多人在初学某门计算机语言的时候都会做过类似的题目:在控制台上输出用特定字符'拼'出来的某种图形,比如下面的这种三角形:
    *
   ***
  *****
 *******
*********
这样的问题应该算是入门级的了,大多人都是看之,做之,忘之,而今天我就拿这种入门级的题目说事,小问题里也许内含有大道理。

昨晚无意中在编程爱好者论坛看到这样一道三角形输出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)就更容易实现我们的这个设计了。

© 2008, bigwhite. 版权所有.

Related posts:

  1. 第一道ACM练习题
  2. 我来'Mixing Milk'
  3. 线程函数参数引发的问题
  4. 走马观花ANSI C标准-类型表示
  5. 美妙的文件描述符传递