结对项目作业

结对作业

项目 内容
所属课设:北航2020年春软件工程 班级博客
作业要求:实现一个能求解简单几何形状之间交点的软件。 作业要求
教学班级 006
项目地址 GitHub链接
个人课程目标 学习一个具备一定规模的软件在生命周期中需要哪些工作,锻炼自己的团队协作能力,并使自己具有开发一个“好软件”的能力
这个作业在哪个具体方面帮助我实现目标 体会结对编程,在结对编程的过程中收获编程经验,加深对结对编程的理解

一、PSP估算

在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。(0.5')

在你实现完程序之后,在下述 PSP 表格记录下你在程序的各个模块上实际花费的时间。(0.5')

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 20
Estimate 估计这个任务需要多少时间 30 20
Development 开发 1170 1240
Analysis 需求分析 (包括学习新技术) 120 150
Design Spec 生成设计文档 60 50
Design Review 设计复审 (和同事审核设计文档) 30 60
Coding Standard 代码规范 (为目前的开发制定合适的规范) 60 80
Design 具体设计 120 140
Coding 具体编码 600 480
Code Review 代码复审 60 100
Test 测试(自我测试,修改代码,提交修改) 120 180
Reporting 报告 300 360
Test Report 测试报告 240 280
Size Measurement 计算工作量 10 15
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 50 65
合计 1500 1620

从这个PSP表格中可以看出,在预估阶段,我认为花费在编码上的时间比较多,因为用C++进行错误处理和GUI的设计没有编程经验,可能需要较长的时间来适应,同时在新增的需求上也需要花费相当多的时间来实现,但在实际操作中,编码实际远远低于预估,反而在需求分析,测试等阶段花费了较多的时间,这与我和结对伙伴前期的设计和复审有关。

看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。

Information Hiding(信息隐藏)

  • 信息隐藏的意思是让模块仅仅公开必须要让外界知道的东西,而隐藏其他一切内容。在模块设计的接口设计中,就充分体现了信息隐藏这一原则——接口是模块的外部特征,应当公开;而数据结构、算法、实现体等则是模块的内部特征,应当隐藏。一个模块仅提供有限的接口,接口是模块与外界交互的唯一途径。
  • 我们的接口设计非常好的满足了这一特征,从上图可以看出,我们在用户只需要按照给定的参数类型传入相应的参数,内部的类的实现,计算过程对用户透明,完全不用关心。

Interface Design(接口/界面设计)

  • 书中出现的这个短语用于用户界面设计,鉴于我们整个问题都是在描述接口的设计,考虑这个小节下从用户界面的角度来说明我们接口设计的方法。
  • 我们为core模块设计接口时,非常重要的一点是能够获取到足够的需要的信息,同时要满足尽可能丰富且有用的功能,考虑到我们有添加、删除几何对象,绘制几何对象及其交点,以及从文件夹读入等固有需求,我们设计了用于求解交点,以及添加、删除几何对象,从文件中读取几何对象等接口,此外,还涉及了删除现有几何对象和获取现有几何对象的接口。

Loose Coupling(松耦合)

  • 耦合是模块之间依赖程度的度量。低耦合意味着模块之间的独立性更好,改变一个模块不会影响其它模块。
  • 这一点体现在我们对于不同类的处理上有所体现,将线类和圆类区别对待,两类之间仅通过交点求解函数关联,不同模块的功能相对独立,这样的好处在于可以快速定位是哪个模块的那一部分出现了问题,方便bug的查找和修复。
  • 另一方面体现在我们和其他小组的对接上,我们虽然内部的实现各不相同,但我们经过协商,确定了统一的接口,这样使得我们core模块和GUI模块可以无缝衔接,不需要更改任何一行代码,只需要简单的互换就能够实现这一点。

参考博客

计算模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。

内部设计

在这一部分我们经过讨论,沿用了上次我的设计,在我个人作业基础上进行了拓展,仍然保持了上次作业的两个类——直线类和圆类

直线类

在直线类中,我们新加了一个属性类型,用来区分直线是直线型、射线型还是线段型,因为在求解交点的过程中,三种“直线”的求解方式是完全一样的,我们只需要根据其类型判断交点在不在其对应的区域上即可,这个类中除了上次的求解直线和直线交点的函数,增加了一个判断交点是不是在“直线”上的函数;

上图是我们这次作业中直线类中的接口示意图,其方法较少,只比第一次多了一个“PointOnLine”的函数,下面展示这一函数:

bool Line::pointOnLine(pair<double, double> intersection)
{
	if (type == SEGMENT)
	{
		if (x1 == x2)
		{
			return ((x1 - eps <= intersection.first && intersection.first <= x1 + eps) &&
				((y1 - eps <= intersection.second && intersection.second <= y2 + eps) ||
				(y2 - eps <= intersection.second && intersection.second <= y1 + eps)));
		}
		return ((x1 - eps <= intersection.first && intersection.first <= x2 + eps) ||
			(x2 - eps <= intersection.first && intersection.first <= x1 + eps));
	}
	else if (type == RAY)
	{
		if (x1 == x2)
		{
			return ((x1 - eps <= intersection.first && intersection.first <= x1 + eps) &&
				((intersection.second - y1 - eps)*(y2-y1)>=0||
				(intersection.second - y1 + eps) * (y2 - y1) >= 0));
		}
		else if (x1 < x2) return x1 - eps <= intersection.first;
		else return intersection.first <= x1 + eps;
	}
	return true;
}

这一函数是在判断线线相交时交点是否在两条直线类对象上的,其方法很简单,因为所求出的交点必然在直线类对象上或其延长线上,所以我们只根据横坐标就能做出判断。

圆类

在圆类中,和上次作业几乎完全一致,仍然是求垂足、求直线和圆的距离、求圆和圆的交点、求圆和直线的交点四个方法,区别在于在求垂足时需要暂时将直线类型统一设置为直线型,来避免垂足被判为不在直线上。直线类和圆类在功能上是一种协同关系。

上图是我们这次作业中圆接口的示意图,其内部方法与属性与上次个人作业保持高度一致,这里不再赘述。

外部接口

我们将项目的主类作为对外的接口,在里面实现了solve,ioHandler等函数接口,用于求解交点,以及添加、删除几何对象,从文件中读取几何对象,在类内创建了保存现有几何对象的容器,在每个函数内调用直线类或圆类的方法来实现功能。这个类可以说是一个顶层类,是连接外部和内部的枢纽。

上图是我们计算模块中主类的接口声明:

  • slove 函数用来求解容器内现有对象的交点,并将其存储在容器内。
  • ioHandler 函数用来从文件中读入集合对象,需要注意的是其并不会清空现有的几何对象,而是直接增加新的几何对象。
  • addLine/deleteLine 用来增加新的直线类几何对象或删除现有直线类几何对象,其参数列表为直线类型,以及确定直线的两个点的坐标。
  • addCircle/deleteCircle 用来增加新的圆类几何对象或删除现有圆类几何对象,其参数列表为圆心坐标及半径。
  • deleteAll 用来删除所有现有几何对象。

上面是我们设计的所有接口函数,其设计的出发点是支持GUI界面的一系列功能。源码较长,这里不做展示。

流程图(以ioHandler和solve为例)

算法关键及独到之处

算法的关键在于各类图形间交点的求解,这是我们的任务所在,也是整个模块功能的核心。我认为算法中的独到之处如下:

  • 3类直线型对象的统一处理,在交点求解时以统一的函数来求解交点,在求解之后判断点在不在对应的几何对象上,短短数十行代码就实现了功能的拓展。
  • 交点存储方式的选取,在交点的存储上,我们选择了用vector存储后排序去重的方式,其性能上的优势非常明显,对于一个10000个几何对象2900w+交点的数据,在未去重时,我和另一组同学的运行时间分别为4.97s和5.21s,在排序去重后运行时间分别为10s和31s,可见我们这种处理方式的优势是非常明显的。

阅读有关 UML 的内容。画出 UML 图显示计算模块部分各个实体之间的关系。

UML的相关知识在面向对象的课程上已经学习过,因此选择了在线工具平台ProcessOn来绘制我们计算模块各个实体之间的关系。

计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。

性能的改进

这一部分与其说是性能的改进,不如说是bug的修复,在性能上我们已经取得了不错的效果,但有两个问题一开始处理的比较差,下面我们来分析一下这两方面的问题。

  • 判断点在线段、射线上。我们一开始采用了计算距离的方式,理论上说这种方式没有任何问题,在计算精确度足够高的条件下,这种方法自然没有问题,但在我们完成作业后和同学进行比对时,发现在一个6000+条直线类几何对象上,我们的交点数目能相差40000多个,不愿意相信的是,就是这个判断点在不在直线上的函数造成的,距离的计算引入了1e-6级别的误差,让两个原本应该重合的点不再重合,将判断条件改成之间判断横纵坐标(都是整数),问题才得以解决。
  • 判断圆相切。这个问题和上面的问题一样,同样是精度问题,但这个问题的解决过程要漫长的多,在600w+个交点上我们的结果多了两个,经过一晚上复杂的排查,锁定了两组几何对象,下面以其中一组进行说明。这是一个直线和圆相切的例子,(L, -272, 469, 673, 973)和(-401, 968, 501),我们的程序计算出两个交点,原因是直接使用==来判断相切,这带来了1e-5级别的误差,我们使用wolfram平台绘图验证了其相切,并计算出了交点(见下图)。最后改变了相切判断条件,问题才得以解决。

性能分析

下图是我们使用VS性能分析工具分析的结果。

可以看出,对vector的排序花费了较多的时间。其中,消耗最大的函数是solve,因为全部的交点求解过程都是在这个函数里面完成的,下面是这个函数的代码。

void solve(vector<pair<double, double>> &realIntersections) throw(const char*)
{
	vector<pair<double, double>> intersections;
	for (unsigned i = 0; i < lines.size(); i++)
	{
		for (unsigned j = i + 1; j < lines.size(); j++)
		{
			try
			{
				lines[i].intersect(lines[j], intersections);
			}catch (const char* msg)
			{
				throw msg;
			}
		}
	}
	for (unsigned i = 0; i < circles.size(); i++)
	{
		for (unsigned j = i + 1; j < circles.size(); j++)
		{
			if (circles[i].c1 == circles[j].c1 && circles[i].c2 == circles[j].c2 && circles[i].r == circles[j].r)
			{
				throw "Error: There are two same circles";
			}
			circles[i].intersectCircle(circles[j], intersections);
		}
		for (unsigned j = 0; j < lines.size(); j++)
		{
			circles[i].intersectLine(lines[j], intersections);
		}
	}
	sort(intersections.begin(), intersections.end(), cmp);
	if (intersections.size() == 0) 
	{ 
		return;
	}
	realIntersections.push_back(intersections[0]);
	for (unsigned i = 1; i < intersections.size(); i++)
	{
		if (!myequal(intersections[i - 1], intersections[i]))
		{
			realIntersections.push_back(intersections[i]);
		}
	}
}

看 Design by Contract,Code Contract 的内容:描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。

  • 在维基百科(Design by Contract)中,对合同设计做了详细的描述,里面我对下面两段话很有感触。第一段话是在具体设计过程中需要满足条件,这段话其实不是第一次见到,在面向对象的课程中我们已经接触过了,即要有规范化且通用的接口,并且假设只要按照规定的数据类型输入,一定能得到期望的结果。在结对编程中,我们就是这样进行设计的,每一个类就像一个器官,每一个方法就像一个组织,彼此之间为了“机体”的正常运转协同工作,却又相互独立的具备一定的功能。

"It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants."
"The central idea of DbC is a metaphor on how elements of a software system collaborate with each other on the basis of mutual obligations and benefits. "

  • Code Contract在我们完成任务的过程中并没有非常显式地使用到,但这个工具中的思想我们有所体现,和Design by Contract一样,这个也是按照一定的规范编程,我们在很多地方使用了断言,对某个方法应能执行怎样的任务也做了详细的协商。

  • 上面两种方法都是以一个“合同”的方式在进行编程,即合同上写明了这个方法能怎么用,这个方法在实现时就要按照合同上的来。这样做的好处非常明显,规范化,以这个方式完成的代码会像模板一样,无论何时何地用到其中的方法,只要按合同上的来,就能正常运行,但在两人结对这样一个工作量相对较小的任务下,合同设计如果太上纲上线会花费大量的时间,虽然在编程上会更加方便,但得不偿失,在这次的实际工作中,我更倾向于口头制定规范(口头合同),或者是像我们组一样随时处于语音连线状态,这样两个人对每个接口的功能也了如执掌,能达到接近的效果,且性价比更高,但在更加复杂的工作中,合同设计的优势就非常明显了。

计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。

在这次作业中我们切实感受到了单元测试的重要性,在每次增加新功能后进行回归测试,可以很容易的发现问题,设计上主要就是对新添功能的测试,以及一些边缘问题的测试。

部分单元测试代码展示

TEST_CLASS(testinterface_solve)
	{
		TEST_METHOD(method1) 
		{
			deleteAll();
			vector<pair<double, double>> myIntersections;
			ioHandler("../testinput2.txt");
			solve(myIntersections);
			int answer = myIntersections.size();
			Assert::AreEqual(26, answer);
		}
	};

这是在我们写好ioHandler和solve接口后进行单元测试的样例,测试了通过接口进行交点计算。

TEST_CLASS(testinterface_ad)
	{
		TEST_METHOD(method1)
		{
			vector<pair<double, double>> myIntersections;
			//ioHandler("../testinput2.txt");
			addLine(-1, 4, 5, 2, LINE);
			addLine(2, 4, 3, 2, SEGMENT);
			addLine(2, 5, -1, 2, RAY);
			addCircle(3, 3, 3);
			solve(myIntersections);
			int answer = myIntersections.size();
			Assert::AreEqual(5, answer);
		}
		TEST_METHOD(method2)
		{
			vector<pair<double, double>> myIntersections;
			//ioHandler("../testinput2.txt");
			deleteCircle(3,3,3);
			deleteLine(2, 5, -1, 2, RAY);
			solve(myIntersections);
			int answer = myIntersections.size();
			Assert::AreEqual(1, answer);
		}
	};

这是我们写好addLine(Circle)和deleteLine(Circle)接口后进行单元测试的样例,测试了通过接口进行几何对象的增添和删除。

单元测试覆盖率截图

从图中可以看出我们单元测试覆盖了93%的内容,剩下没有覆盖的部分大多为函数头和main函数中的内容。

计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。

直线型对象给定两点重复

TEST_METHOD(method1)
		{
			try
			{
				addLine(-1, 4, -1, 4, LINE);
			}
			catch (const char* msg)
			{
				Assert::AreEqual("Error: two points of a line should be different", msg);
			}
		}

这一错误是对于直线型几何对象,其输入的两点坐标重合。

坐标值越界

TEST_METHOD(method2)
		{
			try
			{
				addLine(-1000000, 4, -1, 4, LINE);
			}
			catch (const char* msg)
			{
				Assert::AreEqual("Warning: your coordinate value is out of bound", msg);
			}
		}

这一错误针对所有几何对象,正确的数据坐标值应限定在(-100000,100000)。

圆半径出现非正数

TEST_METHOD(method3)
		{
			try
			{
				addCircle(-10, 4, -1);
			}
			catch (const char* msg)
			{
				Assert::AreEqual("Error: circle's radius should be a positive integer", msg);
			}
		}

这一类错误针对圆类型几何对象,圆的半径应该为正值。

未定义类型标识

TEST_METHOD(method5)
		{
			deleteAll();
			vector<pair<double, double>> myIntersections;
			try
			{
				ioHandler("../undefined.txt");
			}
			catch (const char* msg)
			{
				Assert::AreEqual("Error: unexcepted type mark", msg);
			}
		}

这一类错误针对出现除'L','S','R','C'之外的类型标识符。

下图展示我们所有单元测试用例的运行结果,从图中可以看出我们单元测试用例全部运行正确并且消除了警告信息:

界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。

本次作业的界面模块,我们使用了Qt进行设计,并使用了Qt的开源库QCustomPlot进行图像绘制。主要包括6个函数。

QioHandler():

从文件中读取数据并进行计算。

void myGUI::QioHandler()
{
	string input = ui.fileInput->toPlainText().toStdString();
	ioHandler(input);
	fstream inputfile(input);
	int n;
	inputfile >> n;
	for (int i = 0; i < n; i++)
	{
		char type;
		inputfile >> type;
		if (type == 'L' || type == 'R' || type == 'S')
		{
			int tempType = -1;
			if (type == 'L') tempType = LINE;
			else if (type == 'R') tempType = RAY;
			else if (type == 'S') tempType = SEGMENT;
			double x1, x2, y1, y2;
			inputfile >> x1 >> y1 >> x2 >> y2;
			lines.push_back(UILine(x1, y1, x2, y2, tempType));
		}
		else if (type == 'C')
		{
			double c1, c2, r;
			inputfile >> c1 >> c2 >> r;
			circles.push_back(UICircle(c1, c2, r));
		}
	}
	Qsolve();
}

QdeleteAll():

删除所有几何对象。

void myGUI::QdeleteAll()
{
	deleteAll();
	ui.widget->clearItems();
	ui.widget->clearGraphs();
	lines.clear();
	circles.clear();
	Qsolve();
}

QaddLine():

添加直线。

void myGUI::QaddLine()
{
	string newType = ui.newType->toPlainText().toStdString();
    //通过ui读入两个点
	double x1 = ui.newX1->toPlainText().toDouble();
	double y1 = ui.newY1->toPlainText().toDouble();
	double x2 = ui.newX2->toPlainText().toDouble();
	double y2 = ui.newY2->toPlainText().toDouble();
    //判断类型
	int type = -1;
	if (newType == "L") {
		type = LINE;
	}
	else if (newType == "R") {
		type = RAY;
	}
	else if (newType == "S") {
		type = SEGMENT;
	}
    //执行接口的addLine函数
	addLine(x1, y1, x2, y2, type);
	lines.push_back(UILine(x1, y1, x2, y2, type));
    //重新计算交点
	Qsolve();
}

QdeleteLine():

删除直线。

void myGUI::QdeleteLine()
{
	string newType = ui.newType->toPlainText().toStdString();
	double x1 = ui.newX1->toPlainText().toDouble();
	double y1 = ui.newY1->toPlainText().toDouble();
	double x2 = ui.newX2->toPlainText().toDouble();
	double y2 = ui.newY2->toPlainText().toDouble();
	int type = -1;
	if (newType == "L") type = LINE;
	else if (newType == "R") type = RAY;
	else if (newType == "S") type = SEGMENT;
	deleteLine(x1, y1, x2, y2, type);
	for (auto iter = lines.begin(); iter != lines.end(); iter++)
	{
		if (iter->x1 == x1 && iter->y1 == y1 && iter->x2 == x2 && iter->y2 == y2 && iter->type == type)
		{
			lines.erase(iter);
			break;
		}
	}
    //在删除线后清屏
	ui.widget->clearItems();
	ui.widget->clearGraphs();
    //重新绘制几何对象并求解交点
	Qsolve();
}

圆的函数和直线类似,就不再赘述了。这里面的Qsolve()函数功能是使用QCustomPlot中的QCPItem模块绘制所有几何图形,并执行接口中的solve()函数,根据结果绘制交点。所以每次添加删除几何对象后都执行Qsolve()函数,可以实现图像的自动更新,Qsolve函数至关重要,但是由于长度过长,这里不进行展示。

界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。

本次作业我们采用动态链接库(dll)的方式进行模块对接。

首先在计算模块中实现这些函数:

void solve(vector<pair<double, double>> & realIntersections) throw(const char*);
void ioHandler(string input) throw(const char*);
void addLine(double x1, double y1, double x2, double y2, int type) throw(const char*);
void deleteLine(double x1, double y1, double x2, double y2, int type);
void addCircle(double c1, double c2, double r) throw(const char*);
void deleteCircle(double c1, double c2, double r);
void deleteAll();

然后再函数声明前加_declspec(dllexport),就可以在dll中实现这些函数的接口,然后界面模块导入计算模块的dll,即可使用这些函数。

如上一节所述,我们写好了界面模块的几个函数,然后在ui的按钮中添加与这些函数的链接,即可在ui中实现点击功能。下图是链接后的示意图:

在编译运行后,从testinput2.txt中导入图形,即可实现交点求解和图像绘制功能,然后再使用添加直线和圆的功能。运行结果如下图:

描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。

在结对的过程中我们经历了大致几个阶段,使用live share+腾讯会议阶段,这一阶段一开始感觉很新奇,在计算模块的设计过程中我们都采用这一模式,后来在图形界面设计时由于live share编译时非常不稳定,且双方都没有接触过QT设计图形界面,共同探索效率较低,因此我们采用了腾讯会议连线加桌面共享的方式,这样有问题可以随时讨论,也可以方便展示最新的成果,下面是我们两个阶段的截图。

此外,我们通过热身作业中学到的pull request操作,共同维护了一个GitHub仓库,实现了代码的交互管理。

看教科书和其它参考书,网站中关于结对编程的章节,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。

结对编程的优点

  • 结对编程能提供更好的设计质量和代码质量,两人合作能有更强的解决问题的能力。这一点非常明显,虽然1+1不一定大于2,但1+1往往大于1,两人思想的不断碰撞能提出更合理的想法。
  • 结对工作能带来更多的信心,高质量的产出能带来更高的满足感。举个不恰当的例子——“狐假虎威”,当两人差距很大时,相对较差的一方会因此获得信心,但当团队成员相差不大时,或许可以得到相得益彰的成效。
  • 当有另一个人在你身边和你紧密配合, 做同样一件事情的时候, 你不好意思开小差, 也不好意思糊弄。这一点在我们结对过程中体现的非常明显,因为Live Share的不稳定性,编程的主阵地在我这边,我需要经常共享屏幕给小伙伴,这样对我有一个非常好的监督作用,让我能专心在完成任务上。
  • 结对能更有效地交流,相互学习和传递经验,能更好地处理人员流动。因为一个人的知识已经被其他人共享。这一点也感受良多,两个人交互,一个人踩过的坑,另一个人可以避免,一个人解决过的问题,另一个人可以相对容易的复现,在交流中共同成长。
  • 结对编程让复审随时随地发生,能及时地发现问题和解决问题,避免把问题拖到后面的阶段。

结对编程的缺点

  • 如果团队的人员要在多个项目中工作,不能充分保证足够的结对编程时间,那么成员要经常处于等待的状态,反而影响效率。这一点也有所体现吧,因为我们不只软工一门课程,其他课程也需要投入一定时间,两个人需要协商好时间进行,尽量避免单干或者忙等的情况发生。
  • 验证测试需要运行很长时间,那么两个人在那里等待结果是有点浪费时间。在我们后期做UI界面时,编译一次需要长达近一分钟的时间,这时如果两人一起盯着屏幕看,似乎有些太过浪费时间。

结对中两人的优缺点

小组成员 优点 缺点
细心可以及时发现问题;能及时交流,发现问题随时取得联系;时间上较有保证 害怕重构,甚至连文件名都不敢轻易改
队友 有耐心,能潜心解决疑难问题;对C++的编程经验比较足,能流畅使用各种数据结构;对接口等的设计经常有改进的想法 虽然工作时很认真,但不够积极

附加题(松耦合)

合作小组两位同学:17373456,17373459

我方运行对方core.dll成功的GUI截图:

我方运行对方core.dll成功的命令行截图:

对方运行我方core.dll成功的截图:

对接过程中出现的问题:

  • GUI.exe和core.dll的编译方式不一致,导致互换core.dll之后无法运行。
    解决方法:统一使用Release x64的模式进行编译。
  • 接口函数的关键词不一致,我方采用__cdecl,对方使用的默认。
    解决方法:对方将函数声明添加__cdecl关键词,即可正常运行。
posted @ 2020-03-24 18:19  胖胖的毛毛虫  阅读(236)  评论(2编辑  收藏  举报