60分钟

第2章 软件设计基础

【学习目标】

1.知识目标

了解软件设计基本原理。

掌握软件设计的阶段分类。

掌握软件设计的方法。

了解Gprof的基本工作原理。

了解Valgrind的基本工作原理。

了解SVN和Git的概念。

2.技能目标

会用Gprof,Valgrind工具进行基本的软件调试。

会安装与配置SVN,Git代码管理软件。

能简单实用SVN和Git代码管理软件。

【认证考点】

能掌握的软件优化调测工具Gprof,Valgrind的基本应用。

能安装配置SVN和Git代码管理软件。

能简单实用SVN和Git代码管理软件。

项目引导:C语言代码应用调试测试

【项目描述】

本章涉及的软件较多,主要测试软件的基本使用方法,因此选择C语言简单代码段作为测试内容,引导学生了解学习软件调试和代码管理的方法。

知识储备

2.1软件设计原理

2.1.1软件设计阶段

从软件工程的角度,一般把软件设计分为概要设计和详细设计两个子阶段,具体包括体系结构设计、界面设计、数据设计和过程设计,这也是软件设计的基本要素。

1.概要设计

概要设计也称总体设计,主要任务是基于数据流图和数据字典,确定系统的整体软件结构,划分软件体系结构的各子系统或模块,确定它们之间的关系。确切地说,概要设计是要完成体系结构设计、界面设计和数据设计。

(1)体系结构设计:确定各子系统模块间的数据传递与调用关系。在结构化设计中,体现为模块划分,并通过数据流图和数据字典进行转换。在面向对象设计中,体现为主题划分,主要确定类及类间关系。

(2)界面设计:包括与系统交互的人机界面设计,以及模块间、系统与外部系统的接口关系。在结构化设计中,根据数据流条目,定义模块接口与全局的数据结构。在面向对象设计中,定义关联类、接口类、边界类等,既满足人机交互界面数据的统一,又完成类间数据的传递。

(3)数据设计:包括数据库、数据文件和全局数据结构的定义。在结构化设计中,通过需求阶段的实体关系图与数据字典建立数据模型。在面向对象设计中,通过类的抽象与实例化以及类的永久存储设计,完成数据设计过程。

2.详细设计

详细设计的任务是在概要设计的基础上,具体实现各部分的细节,直至系统的所有内容都有足够详细的过程描述,使得编码的任务就是将详细设计的内容“翻译”成程序设计语言。确切地说,详细设计的任务是完成过程设计。

过程设计包括确定软件各模块内部的具体实现过程与局部数据结构。在结构化设计中,模块独立性约束了数据结构与算法相分离的情况,使得两者在设计时务必有局部性减少外部对两者的影响。在面向对象设计中,类的封装性较好地体现了算法和数据结构的内部性。类的继承性提供了多个类(类家族)共同实现过程设计的机制。根据软件项目的规模和复杂度,概要设计和详细设计既可以合并为软件设计阶段,又可以反复迭代,直至完全实现软件需求内容。

2.1.2软件设计原则

随着软件技术的不断进步,一些良好的设计原则不断地被提出,并指导着软件设计过程,提高软件质量。

1.分而治之

分而治之是用于解决大型、复杂程度高的问题时所采用的策略。把大问题划分成若干个小问题,把对一个大问题的求解转换为对若干个小问题的解答,这样就极大地降低了问题的复杂度。模块化是在软件设计上实现分而治之思想的技术手段。在结构化设计中,模块可以是函数、过程甚至是代码片段。在面向对象设计中,类是模块的主要形式。

2.重用设计模式

重用是指同一事物不做修改或稍作改动就能多次使用的机制。由于概要设计完成的是系统软件结构,因而重用的内容是软件设计模式。软件设计模式针对的是一类软件设计的过程和模型,而不是某一次具体的软件设计。通过重用设计模式,不仅使得软件设计质量得到保证,而且把资源集中于软件设计的新流程、新方法中,并在设计时更进一步考虑新流程,新方法在将来的重用。

3.可跟踪性

软件设计的任务之一就是确定软件各部分间的关系。设计系统结构,就是要确定系统各部分、各模块间的相互调用或控制关系,以便在需要修改模块时,能掌握与修改模块有关的其他部分,并正确追溯问题根源。

4.灵活性

设计的灵活性是指设计具有易修改性。修改包括对已有设计的增加、删除、改动等活动。会发生修改原因有,一是用户需求发生变更;二是设计存在缺陷;三是设计需要进行优化;四是设计利用重用。软件设计灵活性主要通过系统描述问题的抽象来体现。

5.一致性

一致性在软件设计方法和过程中都得到体现。在软件设计中,界面视图的一致性保证了用户体验和对系统的忠诚度。用统一的规则和约束来规范模块接口定义,确保编码阶段对接口和数据结构的统一操作,减少数据理解上的歧义,使软件质量得到保证。在软件设计过程中,团队已成为软件开发的基本组织形式。不同人员集体完成同一软件项目,保持开发进度的一致性是项目成败的关键之一。

2.1.3软件设计方法

软件设计的方法可以分为以下几种:结构化设计、面向对象设计、数据结构为中心设计、基于构件的设计、形式化方法设计。

1.结构化设计

结构化设计方法是一个经典的软件设计方法,采取自下向上和逐步求精的思路,按照功能对系统进行分解。通过结构化分析,得到系统的数据流图和相关的过程描述;然后利用各种策略(如转换分析、事务分析)和经验(如扇入/扇出、影响范围与控制范围)将数据流图转换为结构图,再利用结构图指导设计之后的开发活动。

2.面向对象设计

面向对象设计的思想源于数据抽象和职责驱动,利用封装、继承、多态等方法,提高软件的可扩展性和可复用性。依据抽象和模块化的思想,利用面向对象设计模型表现出对象的职责分配和协作。

3.数据结构为中心设计

数据结构为中心设计方法,开始于系统操纵的数据结构而不是它所表现的功能。软件工程师首先描述的是输入/输出的数据结构和基于这些数据结构的控制逻辑。

4.基于构件的设计

软件构件是一个具有良好定义的结构和依赖的独立单元,能够独立组装和部署。基于构件的设计重点在于构件的提供、开发和集成,以提高系统的可复用性。

5.形式化方法设计

形式化方法设计通过数学方法来对复杂系统进行建模。通过严格的数学模型来验证系统的相关属性。

2.1.4软件设计优化准则

软件设计优化准则主要有以下七个方面:

1.改进软件结构提高模块独立性

概要设计过程中,划分模块时,争取做到低耦合高内聚,就是增加模块内聚,减少模块耦合,保持模块的相对独立性。

(1)如果若干模块之间耦合强度过高,每个模块内功能不复杂,可将它们合并,以减少信息的传递和公共区的引用。

(2)若有多个相关模块,应对它们的功能进行分析,消去重复功能。

2.模块规模适中

如果模块过大,不容易被理解,则要适当地对模块进行分解,这时要注意分解后不应降低模块的独立性。如果模块太小,则会导致模块接口开销过大。

3.适当控制深度、宽度、扇出、扇入

(1)深度:结构图中模块分层的层数。

(2)宽度:同一层上模块数的最大值。

(3)扇出:一个模块扇出指这个模块能够直接调用的模块个数,一般设计时,扇出应当大于3而小于9。

(4)扇入:一个模块的扇入指直接调用这个模块的模块个数。

在设计过程中需要注意,软件结构的深度、宽度、扇出、扇入应当适当。深度和宽度能粗略地反映系统的规模和复杂程度。设计时,深度不宜太大,太大说明分工过细;宽度与扇出有关,一个模块的扇出太多,说明这个模块过于复杂,缺少中间层;单一功能模块的扇入数大比较好,说明这个模块为上层几个模块共享的公用模块,重用率高。但不能把彼此无关的功能凑在一起形成一个通用的超级模块,虽然它的扇入高,但内聚低,因此非单一功能的模块扇入高时应重新分解,以消除控制耦合的情况。软件结构从形态上看,应是顶层扇出数较高一些,中间层扇出数较低一些,底层扇入数较高一些。

4.模块的作用域应该在控制域之内

软件设计时,由于存在不同事务处理的需要,某一层上的模块会存在着判定处理,这样可能影响其他层的模块处理。模块的作用域指受该模块内一个判定影响的所有模块的集合,一个模块的控制范围指模块本身及其所有能够直接或间接调用的模块的集合。一个模块的作用范围应在其控制范围之内,且条件判定所在的模块应与受其影响的模块在层次上尽量靠近。

5.力争降低模块接口的复杂程度

模块的接口要简单、清晰、含义明确、便于理解、易于实现、易于测试和维护。接口复杂可能表明模块的独立性差。如果接口复杂性不一致(即看起来传递的数据之间没有联系)是紧耦合、低内聚的征兆,应该重新分析这个模块的独立性。

6.设计单入口单出口的模块

尽量设计单入口单出口的模块,以避免内容耦合,易于理解和维护。也就是说,模块只有一个入口和一个出口,信息从模块顶端进,从模块底端出,可以有效地避免内容耦合的发生,以这样的思想设计的软件比较易于理解和维护。

7.模块功能可预测

可以把一个模块当作一个黑盒,相同输入必产生相同输出,这样模块功能就可以预测。有一些带有内部“存储器”模块的功能是不可预测的,因为输出可能取决于内部存储器的状态。例如,模块中使用全局变量或静态变量,则由于全局变量和静态变量可能存储不同的结果,所以可能导致模块功能不可预测。

项目实施

以C语言代码段编写与分析为例,学习性能测试工具Gprof和Valgrind的基本应用以及SVN和Git代码管理工具的基本应用。

需要完成的任务:

会使用Gprof软件分析函数调用关系以及运行时间。

会使用Valgrind软件查找内存使用错误。

会安装配置SVN和Git软件。

会使用SVN和Git软件进行代码的基本管理。

2.2 任务1:软件优化调测工具的使用

2.2.1性能测试工具Gprof

1.Gprof介绍

Gprof是GNU编译器工具包所提供了一种剖析工具GNU profiler。可以运行于Linux、AIX、Sun等操作系统进行C、C++、Pascal、Fortran程序的性能分析,用于程序的性能优化以及程序瓶颈问题的查找和解决。默认情况下Linux系统当中都带有这个工具。通过分析应用程序运行时产生的“flat profile”,可以得到每个函数的调用次数,每个函数消耗的处理器时间,也可以得到函数的“调用关系图”,包括函数调用的层次关系,每个函数调用花费了多少时间。

应用Gprof时,通过在编译和链接程序的时候(使用-pg编译和链接选项),Gcc在应用程序的每个函数中都加入了一个名为mcount(or “_mcount”,依赖于编译器或操作系统)的函数,也就是说你的应用程序里的每一个函数都会调用mcount,而mcount会在内存中保存一张函数调用图,并通过函数调用堆栈的形式查找子函数和父函数的地址。这张调用图也保存了所有与函数相关的调用时间,调用次数等等的所有信息。程序运行结束后,会在程序退出的路径下生成一个gmon.out文件。这个文件就是记录并保存下来的监控数据。可以通过命令行方式的Gprof来解读这些数据并对程序的性能进行分析。

2.使用方法

(1)基本用法步骤:

  • 使用-pg选项编译和链接你的应用程序。在编译和链接时加上-pg选项,一般可以加在makefile中。
  • 执行编译的二进制程序。
  • 在程序运行目录下生成gmon.out文件。如果原来有gmon.out文件,将会被重写。运行你的应用程序,使之运行完成后生成供Gprof分析的数据文件(默认是gmon.out)。
  • 使用Gprof程序分析你的应用程序生成的数据。

(2)常用的Gprof命令选项

使用Gprof时,希望观察不同的显示结果,可以使用如表2-2-1所示不同命令选项。

表2-2-1 常用的gprof命令选项

3.案例分析

这里本文编写一个测试案例代码程序,随后应用Gprof工具对其进行性能测试。代码如下所示:

//test_gprof.c
#include<stdio.h> 
static void func2(void)
{
    printf("\n Inside func2( )\n");
    int i = 0; 
    for(; i<0xffffffaa; i++);
    return;
}
void func1(void)
{
    printf("\n Inside func1( ) \n");
    int i = 0; 
    for(; i<0xffffffff; i++);
    func2(); 
    return;
}
void func3(void)
{
    printf("\n Inside func3( )\n");
    int i = 0; 
    for(; i<0xffffffee; i++); 
     return;
}
int main(void)
{
    printf("\n Inside main()\n");
    int i = 0; 
    for(; i<0xffffff; i++);
    func1();
    func2(); 
    return 0;
}

在命令窗口输入如下指令,编辑test_gprof.c代码。

vim test_gprof.c

通过下面指令,编译test_gprof程序。

gcc -pg test_gprof.c -o test_gprof

可以看到执行程序后会新生成一个gmon.out文件,指令与结果显示如图2-2-1所示。

图2-2-1 gprof执行与文件查看

运行性能测试工具Gprof,输入如下指令。

gprof test_gprof gmon.out

可以看到各种参数跃然于屏。

(1)flat profile包括每个函数的调用次数,以及每个函数消耗的处理器时间,如图2-2-2所示。

图2-2-2 flat profile 结果显示

图示结果参数说明:

  • % time,函数使用时间占所有时间的百分比;
  • cumulative seconds,函数和上列函数累计执行的时间;
  • self seconds,函数本身所执行的时间;
  • calls,函数被调用的次数;
  • self s/call,每一次调用花费在函数的时间;
  • total s/call,每一次调用,花费在函数及其衍生函数的平均时间;
  • name,函数名。

(2)call graph包括函数的调用关系,每个函数调用花费的时间,如图2-2-3所示。

图2-2-3 call graph

上图描述了程序的函数调用关系,根据调用时间的大小对函数以及它的子函数们进行排序。

图示结果参数说明:

  • index,代表表中每一条记录的唯一的数字(键),标在该记录当前的函数所在行,根据数字大小顺序依次排列;另外,每一个name列处的函数名字后面也紧跟着一个index号,代表该name的函数是属于哪个index值记录的当前函数。
  • %time,代表记录中当前行函数及其所有子孙函数所消耗的时间占程序总时间的百分比,由于有些函数被排除或者其他原因可能总和不是100%。
  • self,对于记录中当前函数行来说,代表执行本函数所消耗的时间(不包含它的所有子函数)。对于记录中当前行上面行(父函数)来说,代表本函数返回到父函数花费了(父函数)多久的时间(不包括本函数的子函数所消耗的时间)。对于记录中当前行下面行(子函数)来说,代表子函数返回到本函数花费了(本函数)多久的时间(不包括子函数的子函数所消耗的时间)。
  • children,对于记录中当前函数行来说,代表本函数的所有子孙函数返回到本函数所消耗(本函数)的时间。对于记录中当前行上面行(父函数)来说,代表本函数所有子孙返回到父函数,子孙们消耗(父函数)的时间。对于记录中当前行下面行(子函数)来说,代表子函数所有子孙返回到本函数,那些子孙消耗(本函数)的时间。
  • called,对于记录中当前函数行来说,代表本函数被调用的次数,如果是递归函数则是非递归部分次数跟一个‘+’号接递归调用次数。对于记录中当前行上面行(父函数)来说,‘/’左面代表该父函数调用该函数次数,‘/’右面代表该函数总共被调用次数。对于记录中当前行下面行(子函数)来说,‘/’左面代表该函数调用该子函数次数,‘/’右面代表该子函数总共被调用次数。
  • name:对于记录中当前函数行来说,代表当前函数名加上其所属记录的index号,若该函数是某调用环一员,则环号放在函数名和index号之间。对于记录中当前行上面行(父函数)来说,代表父函数名加其记录的index号,若父函数是某调用环一员,则环号放在父函数和index号之间。如果函数的父函数无法确定,那么在name字段打印一个“<spontaneous>”字符,并且所有其它字段为空(例如第一行)。对于记录中当前行下面行(子函数)来说,代表子函数名加其记录的index号,若子函数是某调用环一员,则环号放在子函数和index号之间。

2.2.2内存调试工具Valgrind

1.Valgrind简介

Valgrind是一套Linux下开放源代码的仿真调试工具的集合。Valgrind由内核(core)以及基于内核的其他调试工具组成。内核类似于一个框架(framework),它模拟了一个CPU环境,并提供服务给其他工具;而其他工具则类似于插件(plug-in),利用内核提供的服务完成各种特定的内存调试任务。Valgrind的体系结构如图2-2-4所示。

图2-2-4 Valgrind的体系结构

2.Valgrind包括的工具

(1)Memcheck(主要用的就是这个、默认的也是这个)

最常用的工具,用来检测程序中出现的内存问题。所有对内存的读写都会被检测到,一切对malloc/free/new/delete的调用都会被捕获,所以它能检测以下问题。

  • 对未初始化内存的使用;
  • 读/写释放后的内存块;
  • 读/写超出malloc分配的内存块;
  • 读/写不适当的栈中内存块;
  • 内存泄漏,指向一块内存的指针永远丢失;
  • 不正确的malloc/free或new/delete匹配;
  • memcpy()相关函数中的dst和src指针重叠。

这些问题往往是C/C++程序员最头疼的,利用Memcheck可以解决此类问题。

(2)Callgrind

和Gprof类似的分析工具,但它对程序的运行观察更是入微,能给我们提供更多的信息。和Gprof不同,它不需要在编译源代码时附加特殊选项,但推荐加上调试选项。Callgrind收集程序运行时的一些数据,建立函数调用关系图,还可以有选择地进行cache模拟。在运行结束时,它会把分析数据写入一个文件。callgrind_annotate可以把这个文件的内容转化成可读的形式。

(3)Cachegrind

Cache分析器,它模拟CPU中的一级缓存L1、D1和二级缓存,能够精确地指出程序中Cache的丢失和命中。如果需要,它还能够为我们提供Cache丢失次数,内存引用次数,以及每行代码,每个函数,每个模块,整个程序产生的指令数。这对优化程序有很大的帮助。

(4)Helgrind

它主要用来检查多线程程序中出现的竞争问题。Helgrind寻找内存中被多个线程访问,而又没有一贯加锁的区域,这些区域往往是线程之间失去同步的地方,而且会导致难以发掘的错误。Helgrind实现了名为“Eraser”的竞争检测算法,并做了进一步改进,减少了报告错误的次数。不过,Helgrind仍然处于实验阶段。

(5)Massif

堆栈分析器,它能测量程序在堆栈中使用了多少内存,告诉我们堆块、堆管理块和栈的大小。Massif能帮助我们减少内存的使用,在带有虚拟内存的现代系统中,它还能够加速我们程序的运行,减少程序停留在交换区中的几率。

此外,Lackey和Nulgrind也会提供。Lackey是小型工具,很少用到,Nulgrind只是为开发者展示如何创建一个工具。本教材就不做介绍了。

3.Valgrind的安装步骤

(1)获取源码

通过wget命令如下操作下载安装源码包,官网下载时间可能较长。

wget http://www.valgrind.org/downloads/valgrind-3.14.0.tar.bz2

(2)解压缩

下载完成后,执行下述命令进行解压操作,为安装做准备。

tar -jxvf valgrind-3.14.0.tar.bz2 

(3)安装

进入软件存放目录,执行安装操作指令,具体代码如下所示。

cd valgrind-3.14.0
./configure 
make install

(4)配置环境变量

首先打开~/.bashrc,通过以下操作配置环境变量。

vim ~/.bashrc
export PATH=$PATH:/usr/local/bin
source ~/.bashrc

4.Valgrind的简单使用方法

形式:valgrind --tool=toolname ./test。

其中的toolname指的是上述五个工具中的一个,test表示调试的程序对象。

例如:valgrind --tool=memcheck ./test,或者valgrind ./test5。使用memcheck的时候tool的选择是可以默认省略的。本文后面的案例仅讨论memcheck的应用,tool选择默认方式。

5.案例应用分析

(1)访问非法内存

测试程序test1_valgrind.c的代码如下:

#include <stdio.h>
#include <stdlib.h>
void func()
{
	int *x=(int*)malloc(10*sizeof(int));
	x[10]=0;
}
int main(){
	func();
	printf("\n done!\n");
	return 0;
}

test1_valgrind.c程序存在两个问题,一是“x10=0”中地址没有分配,不能读写;二是malloc申请的空间最后没有用free释放。在图2-3-5中的检查结果中也发现了这两个问题。

图2-2-5 test1_valgrind测试结果

(2)使用未初始化内存

测试程序test2_valgrind.c的代码如下:

#include<stdio.h>
#include<stdlib.h>
int main(){
	int a[5];
	int i,s=0;
	a[0]=a[1]=a[3]=a[4]=0;
	for(i=0;i<5;++i)
		s+=a[i];
	if(s==33)
		printf(“\n sum is 33 \n”);
	else
		printf(“\n sum is 33 \n”);
	return 0;
}

该程序问题在没有给“x2”初始化(没有赋值)就进行调用。图2-2-6的检查结果同样说明了该问题。

图2-2-6 test2_valgrind测试结果

(3)内存读写越界

测试程序test3_valgrind.c的代码如下:

#include<stdio.h>
#include <stdlib.h>
int main(){
	int len=4;
	int *pt=(int*)malloc(len*sizeof(int));
	int *p=pt;
	for(int i=0;i<len;++i)
		p++;
	*p=5;
	printf("\n The value of p is %d \n",*p);
	return 0;
}

程序test3_valgrind.c代码存在以下二个问题:一是语句“*p=5;”有问题,当前p所指地址空间超出分配范围,不能读写,二是malloc申请的空间没有释放。图2-2-7的结果也反映了这些问题。

图2-2-7 test3_valgrind测试结果

(4)内容覆盖

测试程序test4_valgrind.c的代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
	char x[50];
	int i;
	for(i=0;i<50;++i)
		x[i]=i+1;
	strncpy(x+20,x,20);
	strncpy(x+20,x,21);
	strncpy(x+20,x,20);
	strncpy(x+20,x,21);
	x[39]='\0';
	strcpy(x,x+20);
	x[39]=39;
	x[40]='\0';
	strcpy(x,x+20);
	return 0;
}

程序test4_valgrind.c代码存在问题是strncpy()函数使用问题。两处代码“strncpy(x+20,x,21);”读取长度不合理,存在内容覆盖错误。第二句“strcpy(x,x+20);”读取长度实际为21个字符,存在内容覆盖问题。实验结果如图2-2-8所示也反映了这三个问题。

图2-2-8 test4_valgrind测试结果

(5)动态内存管理

测试程序test5_valgrind.c的代码如下:

#include<stdio.h>
#include <stdlib.h>
int main(){
	int i;
	char *p=(char*)malloc(10);
	char *pt=p;
	for(i=0;i<10;++i){
		p[i]='z';
	}
	free(p);
	pt[1]='x';
	free(pt);
	return 0;
}

程序test5_valgrind.c代码存在两个问题,一是语句“pt1='x';”有问题,pt空间已经释放了;二是语句“free(pt);”出现重复释放问题。图2-2-9的测试结果同样反映了该问题。

图2-2-9 test5_valgrind测试结果

2.3 任务2:软件开发代码管理工具的使用

2.3.1认识SVN和Git

SVN的全称是Subversion,即版本控制系统。它是最流行的一个开放源代码的版本控制系统。作为一个开源的版本控制系统,Subversion管理着随时间改变的数据。这些数据放置在一个中央资料档案库(Repository),这里称为版本库。这个版本库很像一个普通的文件服务器,不过它会记住每一次文件的变动。这样就可以把档案恢复到旧的版本,或是浏览文件的变动历史。Subversion是一个通用的系统,可用来管理任何类型的文件,其中包括程序源码。SVN采用客户端/服务器体系,项目的各种版本都存储在服务器上,程序开发人员首先将从服务器上获得一份项目的最新版本,并将其复制到本机,然后在此基础上,每个开发人员可以在自己的客户端进行独立的开发工作,并且可以随时将新代码提交给服务器。当然也可以通过更新操作获取服务器上的最新代码,从而保持与其他开发者所使用版本的一致性。

Git是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。Git是Linus Torvalds为了帮助管理Linux内核开发而开发的一个开放源码的版本控制软件。与常用的版本控制工具CVS、SVN等不同,它采用了分布式版本库的方式,不需要服务器端软件支持。

SVN是集中式版本控制系统,版本库是集中放在中央服务器的,而用户工作的时候,用的都是自己的电脑,所以首先要从中央服务器那里得到最新的版本,然后工作编辑文件,需要把自己编辑完毕的文件推送到中央服务器。集中式版本控制系统是必须联网才能工作,因此效率受网速的限制。

Git是分布式版本控制系统,它没有中央服务器,每个人的电脑就是一个完整的版本库。这样,工作的时候就不需要联网了,因为版本都是在自己的电脑上。既然每个人的电脑都有一个完整的版本库,那多个人如何协作呢?比如说自己在电脑上改了文件A,其他人也在电脑上改了文件A,这时,你们两之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。

2.3.2 SVN安装与配置

Subversion是优秀的版本控制工具,需要安装服务端和客户端。下面以Windows环境为例在本地来搭建SVN服务器和客户端的安装,当然Subversion也可以安装在Linux、MacOS系统中。

1.下载安装软件

软件安装包可以在Subversion官网上根据系统平台选择下载。在官方网站网页中,找到Windows版本,如图2-3-1所示,下载SVN客户端(TortoiseSVN)和SVN服务端(VisualSVN)(注意:官网下载速度比较慢,可以选择在第三方平台下载)。

图2-3-1 Windows环境的SVN安装包地址

2.服务器安装

安装程序下载完毕后,点击服务器端运行安装程序,如图2-3-2所示。

图2-3-2 运行安装程序界面

根据提示依次进入如图2-3-3所示的初始安装配界面。Location是指VisualSVN Server的安装目录,Repositorys是指定版本库存放目录,Server Port指定一个端口,Use secure connection勾上表示使用安全连接。

图2-3-3 初始安装配置界面

点击Next,进入如图2-3-4所示用户认证模式选项,Use Subversion authentication表示使用Subversion自己的用户认证(本文的选择);Use Windows authentication表示使用Windows的认证。点击Next,可以一直按照默认选项进入安装。

图2-3-4 认证模式选项

安装完成后,启动VisualSVN Server Manager,进入如图2-3-5所示界面,说明服务器端安装成功。

图2-3-5 VisualSVN Server 初始界面

3.客户端安装

接下来安装TortoiseSVN客户端,双击安装包程序,进入下一步,如图2-3-6所示。

图2-3-6 TortoiseSVN安装初始向导界面

根据向导提示,安装完毕后,出现的界面如图2-3-7所示,点击Finish完成安装。

图2-3-7 TortoiseSVN安装结束向导界面

客户端安装完成后,并没有具体的客户端软件,而是在鼠标右键菜单出现SVN Checkout和TortoiseSVN选项,如图2-3-8所示,说明客户端安装成功。

图2-3-8 TortoiseSVN客户端应用菜单

2.3.3 SVN的简单使用

1.新建版本库

版本库是服务端存放文件的,在使用SVN时首先要创建新的版本库,存放客户端用户的文件。启动了SVN服务端,首先打开VisualSVN Server Manager,进入图2-3-5所示界面。

要建立版本库,需要右键单击左边窗口的Repositores新建,然后出现如图2-3-9所示的版本库类型选择,选择标准的FSFS版本库(注意,根据个人需要选择),点击下一步。

图2-3-9 Repository Type 选择向导界面

如图2-3-10所示输入自定义的版本库名称点击下一步。

图2-3-10 创建版本库名称窗口

如图2-3-11所示,选择版本库结构类型,“Empty repository”表示创建空的版本库,“Single-project repository”选项会默认建立trunk、branches、tags三个文件夹。本文选择“Empty repository”,点击下一步,进入如图2-3-12所示界面。在在如图2-3-12所示界面选择用户访问版本库的权限,本文选择默认第二种选项,即所有SVN用户都有读/写权限。点击创建按钮完成版本库的创建。

图2-3-11 版本库结构选项向导
图2-3-12版本库访问权限选项向导

版本库创建成功后,弹出如图2-3-13所示的内容,描述新建的版本库的基本信息,拷贝保存版本库的URL,作为客户端访问服务端的地址。

图2-3-13 创建版本库名称窗口

2.创建用户

版本库创建好后,开始创建访问版本库的用户。左侧窗口选择Users,右键选择创建新用户,如图2-3-14所示,输入用户名和密码,完成新用户的创建。

图2-3-14 创建用户窗口

3.客户端应用

服务端设置完成后,可以在客户端完成应用操作。用户根据需要在个人电脑上创建SVN工作文件夹,进入该文件夹,点击鼠标右键,弹出如图2-3-8所示界面。选择SVN Checkout,弹出内容如图2-3-15所示,创建的版本库的URL拷贝到对应文本框。当前文件夹作为Checkout的文件目录(这里可以自由选择),其他选项默认即可。点击OK,进入图2-3-16所示的用户登录认证。根据服务端创建的用户,输入用户名和密码点击OK。这时客户端和服务端连接成功,如果版本库有文件也会更新到该文件夹下。

图2-3-15 Checkout基本设置
图2-3-16 SVN用户登录认证

此时,可以在文件下创建一个测试文档,例如“test_file.txt”,编辑测试内容,并保存。选择该文件,右键进行添加并提交,出现如图2-3-17所示信息,点击Recent message可以查看前期提交操作的注释信息,文本框内可以输入当前提交的注释信息。下方窗口选择提交文件“test_file.txt”。然后点击OK进行提交。

图2-3-17 提交选项界面

提交完成后弹出如图2-3-18所示窗口,显示提交信息。

图2-3-18 提交完成提示信息窗口

提交完毕后,在服务端的版本库中可以看到刚才提交的信息,如图2-3-19所示。

图2-3-19 提交后版本库内容信息

上述介绍了SVN的基本使用方法。用户后面再次访问SVN,可以通过Update,从版本库更新文件内容进行新的编辑操作,工作完毕再次提交。

2.3.4 Git的安装与配置

Git是分布式版本控制系统,因此与SVN不同,没有服务器端。本文以Windows系统为例进行安装讲解。

1.下载

通过如图2-3-20所示官网,下载用户电脑系统匹配的Git安装包。官网下载速度可能较慢,也可以选择第三方平台下载。

图2-3-20 Git官网下载区

2.安装与设置

下载完毕后,点击安装程序,大部分按照默认项选择安装即可,针对几个关键步骤环节进行说明。

如图2-3-21所示步骤表示选择Git命令行方式。Use Git from Git Bash only:只能用Git提供的Git Bash才能使用Git,安全性最高(本文选择该方式安装)。Git from the command line and also from 3rd-party software:可以使用Git的命令行和第三方工具操作Git。因为一般会安装第三方的图形化操作软件(命令行不好记),所以一般选这个。Use Git and optional Unix tools from the Command Prompt:可以使用Git命令行和Unix工具。

图2-3-21 Git命令行方式选择

如图2-3-22所示表示选择Git使用的HTTPS通道方式。第一个是使用Open SSL库(本文选择的),第二个是使用Windows安全通道。

图2-3-22 的HTTPS通道方式

3.成功安装检查

安装完毕后,右键菜单会出现如图2-3-23所示的Git GUI Here和Git Bash Here选项。点击Git Bash Here,弹出Git操作命令窗口,输入如下命令,如果输出Git版本号如图2-3-24所示,说明安装成功。

git –version
图2-3-23 Git右键菜单显示
图2-3-24 版本测试结果

4.配置用户与注册

因为Git是分布式版本管理工具,所以每一个使用者需要提供个人信息,留下个人联系方式便于别的开发者联系。如下指令设置用户名和邮箱,并查看用户名和邮箱信息。

git config –global user.name “用户名”
git config –global user.email “电子邮箱”
git config –global --list

如图2-3-25所示显示用户设置过程。

图2-3-25 用户设置

该用户信息还需要到Github官网进行注册。

5.生成SSH密钥

SSH是Secure Shell安全外壳协议的缩写。它为网络服务提供目前较为可靠的安全协议,有效防止信息泄露问题。通过公钥和私钥的验证,建立安全连接。使用SSH方式和远程版本库通信的时候可以无需输入用户名密码进行验证。在任意空白位置右击选择“Git Bash Here”,启动Git的终端模拟器。输入如下指令:

 ssh -keygen -t rsa 

然后回车,会依次要求输入密钥保存位置、密码和确认密码的信息,这里可以直接敲3次回车,默认密钥保存位置是/c/Users/用户名/.ssh/目录下,无密码状态。操作完毕后如图2-3-26所示。这是在指定/默认的位置(/c/Users/用户名/.ssh/)就有两个文件,如图2-3-27所示。

图2-3-26 ssh配置
图2-3-27 rsa密钥文件

id_rsa文件是私钥,一定保存好不能泄露;id_rsa.pub文件是公钥,内容放在云端提供验证。登录Github网站,在前面注册的用户账号设置SSH。如图2-3-28所示。

图2-3-28 SSH配置

点击New SSH key,把id_rsa.pub文件内容拷贝到如图2-3-29所示的key下面的文本框中。Title下的文本框输入自定的标题。点击Add SSH key,完成SSH key的配置。

图2-3-29 添加SSH key

启动Git终端模拟器,输入如下指令:

ssh -T git@github.com

出现如图2-3-30所示的提示,说明与远端连接正常,可以传输文件。

图2-3-30连接测试

2.3.5 Git的简单使用

根据上述方法配置后,就可以使用Git进行代码管理。

1.建立本地仓库

在本地需要存放文档的位置新建一个文件夹作为本地仓库。比如在电脑D盘创建Git文件夹作为本地仓库。在该文件夹内右键启动Git Bash here,输入如下指令:

git init

结果如图2-3-31所示,并在Git文件夹下产生一个名为.git的隐藏文件夹。

图2-3-31创建本地仓库

在Git文件夹下创建测试文件,以test.txt为例,对该文件进行添加和提交操作。操作指令如下:

git add test.txt
git commit -m “注释说明内容”

结果如图2-3-32所示,test.txt被成功提交。

图2-3-32 文件添加与提交

2.创建远程仓库

在Github官网中,当前注册账号下,创建新的仓库,输入自定义的仓库名“testgit”(自己定义),并选择相应的配置选项,创建远程仓库。如图2-3-33所示。创建远程仓库方便多人协同开发,共享开发文档。

图2-3-33 创建远程仓库

通过以下指令可以把本地仓库内容上传到远程仓库。

git remote add origin git@github.com:/cyqtest1/testgit.git
git push -u origin master

结果如图2-3-34所示。同时在远程仓库可以看到文件信息以及修改提交记录等,如图2-3-35所示。

图2-3-34 本地仓库添加到远程仓库
图2-3-35 远程仓库内容

Git的功能较多,操作使用比较复杂。本文仅给出一些的基本操作,如需要更深入的了解功能内容可参考相关文献。

本章小结

本章主要介绍了软件设计中涉及的基本原理知识,包括软件设计阶段的要素、设计原则、设计方法和优化准则。通过简单的C语言代码案例介绍软件测试工具Gprof和Valgrind的基本应用,以及代码开发工具SVN和Git的安装配置和简单应用。

本章习题

项目练习题

1.三人构建开发小组,组长在本地使用Eclipse新建Java项目,项目名称为XXX。完成后,使用TortoiseSVN的import操作将NumberGame项目导入到SVN服务器,小组其他成员下载项目内容进行联合开发测试。

2.三人构建开发小组,组长在本地使用Eclipse新建Java项目,项目名称为XXX。完成后,使用Git把项目内容push到远程仓库,小组其他成员下载项目内容进行联合开发测试。

参考文献

1胡思康编著. 软件工程基础M, 北京:清华大学出版社,2018.1

2骆斌编著. 软件开发的技术基础M,北京:机械工业出版社,2012.12

3张剑飞主编. 软件工程基础与实例分析M,北京:机械工业出版社,2019.1