背景介绍
文本选取
文本编辑器以及字处理软件是日常常用的应用软件。从正文中选取一段文字进行拷贝粘贴也是频繁进行的操作,其操作过程通常是利用鼠标等设备定位光标到被选择文字起始处,然后将光标拖动到目标文字末尾。软件在此时通常对选中文字进行高亮反色显示,如下图所示:
在文本编辑器中选择文字从程序设计的角度思考下面的问题:
- 程序采取何种数据结构来存储数据并进行各类操作?
- 这种拖选操作的背后的原理是怎样的?需要记录哪些数据?
一种合理程序实现方案是整篇文字全部以字符串的形式放置在一个一维数组中。一维数组是最简洁的数据结构。
而在拖选某一段文字时执行以下步骤:
- 当鼠标按下时,记录当前位置字符在整个数组中的下标(index)
- 当拖动鼠标到最后一个字符时,再记录其在整个数组中的下标
- 对选中文字执行操作(例如Ctrl+C拷贝到剪贴板),则通过上面起止位置的下标来读取或者修改整段文字
按列选取
目前,不少文本编辑程序提供按列拖选文字的便利功能,即选取鼠标拖出的矩形区域的文字,这与普通的文字选取是不同的:
按列拖选文字从以上动图可以看到,有了按列选取文字的功能,我们在对相邻且内容相似的行做批量修改时效率提高n倍(n等于行数)。因此尤其在编程领域这是非常实用的一项功能。
思考:是否需要对数据结构进行更换以适应按列选取功能?
如果沿用一维数组,我们会发现被选择的文字在数组中的位置,相同行的被选中文字是连续的,而不同行的文字之间则并不连续。如此一来,我们的选择跨越了多少行,就需要记录多少组起止位置的下标。
虽然这样做并不影响功能的实现,但是毫无疑问使程序变得复杂低效,增加了维护和运行成本。那么有没有更优越的方案?
使用二维数组
可以将二维数组理解为数组的数组,即一个数组的每一个元素都是一个数组,并且这每个数组的长度完全相同。如果将整篇文本看做以行为单元组成,每一行又包含若干列的文字,则可以用二维数组来表示文本。比如我们将唐诗《登鹳雀楼》放入一个4*6的二维数组a,其示意图如下:
用二维数组存储多行文本假设我们要按列选中如下蓝色区域的文本:
屏幕快照 2018-08-25 下午11.36.07.png在二维数组的基础上,我们可以大大减少需要记录的信息量。此时只需要记录蓝色矩形区域左上角‘日’字的位置(a[0][1])和右下角‘层’字的位置(a[3][3])即可。
--
实验:编写程序实现按列读取文字
我们根据前面对按列选取文字原理的分析,动手实现一个简化版的程序。该程序从键盘读取若干行文字(最多输入100行,每行最多含100个字符),同时读取用户给出的两个列号(整数),最后依次读取每一行中介于这两个列号之间的文字并打印到屏幕。这基本上涵盖了按列选取文字最核心的思想。
步骤1 创建项目
运行Visual Studio 2008,选择菜单项“文件 -> 新建 -> 项目”或按组合件Ctrl+Shift+N,弹出新建项目对话框,在左侧“项目类型”中选择“Visual C++”,在“模板”中选择“Win32控制台应用程序”,最后在“名称”后面输入你的项目名。我为项目取名叫“ColumnSelect”:
点击“确定”后进入“Win32应用程序向导”,直接点击“下一步”。注意在紧接着出现的对话框中勾选“附加选项”下的“空项目”:
最后点击“完成”。
步骤2 添加源文件
此时项目已经完成创建。但是目前项目是空的,我们至少得添加一个C++源文件。注意左侧的解决方案视图中列出了三个文件夹如下:
在“源文件”一项上点击右键,在弹出菜单中选择“添加 -> 新建项”,这时弹出“添加新项”对话框。在模板中选择“C++文件”,并在“名称”后面输入文件名“column_sel.cpp”:
点击“添加”按钮完成操作。此时会看到在开发环境主要区域已经将新添加的源文件打开,当然,还是一片空白。
步骤3 包含必要的头文件
C/C++语言以及操作系统提供了众多的库,使得我们在程序开发中可以站在巨人的肩膀上,而不必万事从头做起。这些库涵盖广泛,包括时间日期、图形图像、声音视频、网络通信、字符串处理、输入输出(IO)等等领域的功能。那么如何调用我们需要的功能呢?对于C/C++语言来说,需要在代码中对相应模块的头文件(.h)预先进行包含。头文件中含有我们需要的函数、类或对象等组件的声明。
对于我们的程序,至少需要包含下列3个头文件:
- stdio.h:提供从标准输入设备(这里是键盘)读取数据、向标准输出设备(这里是显示器)输出信息的相关函数
- stdlib.h:提供一些工具函数
- string.h:提供字符串相关操作函数,如字符串的复制、计算长度、分割等等
在column_sel.cpp源文件最上方添加以下预处理指令代码以包含头文件:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
步骤4 定义常量
前文提到本程序最多输入100行文字,每行最多含100个字符。我们分别为这两个数字定义常量。使用常量的好处是在代码中用常量名代替数值。如此一来,将来一旦要修改数值,则只用修改一处。否则数值用了多少次,就要改多少地方,又累又容易出错。
C/C++中定义常量的方式也有多种。我们这里选择通过预编译指令define来完成常量定义。在column_sel.cpp文件中接着敲入下列代码以定义两个常量(常量本身的意义通过注释说明):
#define MAX_ROW 100 // 最大行数
#define MAX_COL 100 // 每行最大列数
步骤5 声明一组函数
在C语言中,通常将完成单一功能的代码组织成一个独立的函数(function,实际上还有“功能”的意思)。对于稍微复杂一些的程序,通过函数可以实现基本的模块化,使得代码结构清晰又便于分工协作。
考虑我们的程序,其处理过程可以划分为以下相对独立的步骤:
- 从键盘读取若干文本行,直到用户输入空行
- 分别从键盘读取选择区域左上角和右下角字符在二维数组中的行、列值
- 根据选择区域设定从输入的文本内容中读取对应的内容并存储到另一个二维数组中
- 最后打印输出读取到的内容
基于上面的分析,分别在column_sel.cpp文件中添加下面的函数声明:
read_lines()函数
/**
* 从键盘循环读取文本行,直到输入空行,并将读到的文本行存入二维数组
* input[][]: 存储输入文本行的二维数组
* max_row: 最大输入行数
* 返回值:用户实际上输入的行数,不含最后的空行
*/
int read_lines(char input[][MAX_COL], int max_row);
set_select_range()函数
/**
* 设置选择区域起止位置的行列编号(从0开始计数)
* start_row: 返回输入的左上角字符行号
* start_col: 返回输入的左上角字符列号
* end_row: 返回输入的右下角字符行号
* end_col: 返回输入的右下角字符列号
*/
void set_select_range(int &start_row, int &start_col, int &end_row, int &end_col);
思考:回忆学过的C++知识,为什么set_select_range()函数的4个参数要声明成引用的形式?
copy_selected_range()函数
/**
* 将选择区域中的文本拷贝到二维数组并返回
* input:存储用户输入的文本行的二维数组
* output: 存储读取到的选择区域文本的二维数组
* start_row: 选择区域左上角字符行号
* start_col: 选择区域的左上角字符列号
* end_row: 选择区域右下角字符行号
* end_col: 选择区域右下角字符列号
* 返回值:拷贝的文本行数
*/
int copy_selected_range(char output[][MAX_COL],
char input[][MAX_COL],
int start_row, int start_col,
int end_row, int end_col);
步骤6 完成主函数编写
main()函数是C/C++语言程序的入口函数。在这个函数里面,我们完成程序主要逻辑的编写。
首先在column_sel.cpp文件中定义主函数:
int main() {
// 在此处添加程序代码
return 1;
}
这个main()函数目前什么都没有做。现在我们在第一行注释下面添加相关代码。
首先定义所需要的变量:
char input[MAX_ROW][MAX_COL]; //存储输入文本的二维数组
char output[MAX_ROW][MAX_COL]; //存储拷贝出来的被选择区域中的文本的二维数组
int num_lines = 0; //用户实际输入的文本行数
int start_row, start_col, end_row, end_col; //选择区域的起止位置的行号和列号
在屏幕上打印一行文字提示用户输入,并以空行结束。紧接着调用read_lines()函数以接收用户输入:
// 读取用户输入的文本行,并记录行数
printf("请输入文本行,以回车键换行。结束输入请单独按回车键:\n");
num_lines = read_lines(input, MAX_ROW);
// 如果用户没有输入任何文字则中止程序
if (num_lines <= 0) {
printf("你没有输入任何文字,程序结束。\n");
exit(0);
}
然后再打印一行文字提示用户输入起止位置字符的行号和列号,然后调用set_select_range()函数接收用户输入,将值分别赋予对应的变量,并对参数进行检查:
// 读取用户输入的选择区域参数
printf("请按顺序输入选择区域起始和结束位置字符的行号和列号(2,3,5,10):\n");
set_select_range(start_row, start_col, end_row, end_col);
// 处理不合理的选择区域参数
if (start_row < 0 || end_row < 0
|| start_row > num_lines - 1 || end_row > num_lines - 1
|| start_row > end_row) {
printf("你输入了错误的选择区域参数,程序结束。\n");
exit(0);
}
int num_copied_lines = copy_selected_range(output, input, start_row, start_col, end_row, end_col);
现在,我们已经获取到了全部所需的输入数据,下面正式进行读取选择区域内容的处理,并将读取到的数据存储到另一个二维数组output。在最后,我们通过循环语句将读取到内容打印到屏幕上:
// 打印输出拷贝到的被选择文本
for (int i = 0; i < num_copied_lines; i++) {
printf("%s\n", output[i]);
}
步骤7 完成功能模块编写
目前,我们在main()函数中编写完成了程序的主干逻辑。但是具体到步骤5中的三个函数模块,我们仅仅做了声明,还没有编写代码实现。现在在column_sel.cpp源代码文件最末尾逐一完成它们。
read_lines()
- 在这里,我们每输入一行并回车后则进行一次计数,最终将得到输入的总行数,也就是函数的返回值。
- 我们调用gets()函数来完成从键盘读取一个字符串的操作。
- 我们每输入一个串则调用strlen()函数来获得输入串的长度,如果为0表示是空行,此时我们终止读取用户输入。
int read_lines(char input[][MAX_COL], int max_row) {
int n_rows = 0; //已经输入的行数
while (n_rows <= MAX_ROW) {
if (gets(input[n_rows]) != NULL) {
//输入空行则结束输入
if (strlen(input[n_rows]) <= 0) {
break;
}
n_rows++;
}
}
return n_rows;
}
set_select_range()
void set_select_range(int &start_row, int &start_col, int &end_row, int &end_col) {
// 从键盘读取4个整数,以逗号分隔
scanf("%d,%d,%d,%d", &start_row, &start_col, &end_row, &end_col);
}
- 调用scanf()函数从键盘读取4个整数并分别按顺序赋值给后面4个int型参数
- 格式串"%d,%d,%d,%d"表明接受以逗号分隔的4个整数
copy_selected_range()
copy_selected_range()的处理相对复杂一些。我们循环读取原文的每一行,对它进行分析:
- 如果这一行太短,根本没有进入选择区域,则向输出二维数组对应行写入一个空字符串
- 如果这一行在达到结束列之前就结束了,那么将拷贝结束位置设置为其最后一个字符位置
我们通过strncpy()函数来将字符串中的一部分拷贝出来
int copy_selected_range(char output[][MAX_COL],
char input[][MAX_COL],
int start_row, int start_col, int end_row, int end_col) {
int n_out_row = -1; // 当前输出二维数组行号
for (int i = start_row; i <= end_row; i++) { // 循环拷贝每一行
n_out_row++;
if (start_col > strlen(input[i]) - 1) { // 如果当前行文本太短,向输出对应行写入空串
strcpy(output[n_out_row], "");
} else {
char *p_start = input[i] + start_col; // 找到起始位置
// 确定实际结束位置,因为指定结束列有可能已经超过了当前行文字长度
int end = (end_col > strlen(input[i]) - 1) ? strlen(input[i]) - 1 : end_col;
strncpy(output[n_out_row], p_start, end - start_col + 1);
// 追加字符串结束标记
output[n_out_row][end - start_col + 1] = '\0';
}
}
return n_out_row + 1; // 返回实际拷贝的行数
}
步骤8 运行程序
选择菜单项“调试 -> 开始执行”命令,或者按组合键Ctrl+F5执行程序。
例如输入以下的文本行:
1234567
qwertyu
asdfghj
zxcvbnm
1234567
然后输入选择区域参数。设我们要选择左上角为字符'w'而右下角为字符'n'的区域,则‘w’的行、列分别为1和1,'n'的行、列分别为3和5,输入参数格式则为:
1,1,3,5
回车后,打印出拷贝出来的内容如下:
werty
sdfgh
xcvbn
运行效果如图:
总结
在本次实验项目中,我们通过实现一个简单的按列选取文本的程序,复习了C/C++语言程序设计相关的主要环节和基本技巧:
- 创建项目
- 建立源文件
- 包含头文件
- 定义常熟
- 声明和实现函数
- 编译运行程序
提交作业
将CPP文件通过交作业系统上交.
文件名格式如下:
colunmn_sel_姓名_学号.cpp
网友评论