項目 | 內容 |
---|---|
這個做業屬於那個課程 | 班級博客 |
這個做業的要求在哪裏 | 做業要求 |
我在這個課程的目標是 | 學習軟件工程相關知識,加強本身的開發能力。 |
這個做業在哪一個具體方面幫我實現目標 | 學習結對編程的技巧和方法 |
sample | Sample |
教學班級:005java
項目地址:https://github.com/harrychen23235/MultiIntersectc++
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | ||
· Estimate | · 估計這個任務須要多少時間 | 20 | 25 |
Development | 開發 | ||
· Analysis | · 需求分析 (包括學習新技術) | 120 | 300 |
· Design Spec | · 生成設計文檔 | 30 | 30 |
· Design Review | · 設計複審 (和同事審覈設計文檔) | 20 | 20 |
· Coding Standard | · 代碼規範 (爲目前的開發制定合適的規範) | 40 | 30 |
· Design | · 具體設計 | 30 | 20 |
· Coding | · 具體編碼 | 120 | 180 |
· Code Review | · 代碼複審 | 30 | 60 |
· Test | · 測試(自我測試,修改代碼,提交修改) | 120 | 120 |
Reporting | 報告 | ||
· Test Report | · 測試報告 | 30 | 30 |
· Size Measurement | · 計算工做量 | 20 | 20 |
· Postmortem & Process Improvement Plan | · 過後總結, 並提出過程改進計劃 | 240 | 240 |
合計 | 820 | 1025 |
此次的做業主要難點是新知識的學習,包括dll庫的生成以及qt的學習,其中qt對vs生成dll的不支持也讓我DEBUG了很是長的時間。在架構上此次的程序沿用了以前的架構,所以具體設計時並無花費太多的時間,可是測試時仍然發現了很多的問題。在結對編程上,最大的難點是對雙方的代碼不熟悉同時遠程合做極爲不方便,所以將任務一分爲二,我負責UI開發以及程序實現,另外一位同窗主要負責對於程序功能進行測試。git
信息隱藏、接口設計、鬆耦合都是軟件工程以及面向對象實現的重要方法。目標是使得模塊的調用者不須要關注模塊的細節部分,只須要按照接口說明進行操做就能獲得但願的結果。同時調用者也沒法修改模塊內容,或者反推出模塊源代碼。github
信息隱藏:算法
接口設計:編程
鬆耦合:架構
核心模塊的主要功能包括對於圖像的添加,計算交點個數功能。app
一、首先是對於具體節點的處理。創建了自定義的point類,並採用unordered_set結構進行儲存,內部採用相似java的hashset進行儲存。因爲採用的爲自定義類,同時須要對於浮點數double進行hash,所以重寫了equal和hash函數,具體以下所示:函數
unordered_set<Point*, Hash_Point, Equal_Point> g_allpoint; struct Hash_Point { size_t operator()(const class Point* input1)const { long temp1 = floor(input1->x); long temp2 = floor(input1->y); if (abs(input1->x - (temp1+1)) <= EPS) temp1++; if (abs(input1->y- (temp2+1)) <= EPS) temp2++; return (temp1 + temp2) * 13 + (temp1 * 1000 % 1000 + temp2 * 1000 % 1000); } }; struct Equal_Point { bool operator()(const class Point* input1, const class Point* input2)const { return abs(input1->x - input2->x) <= EPS && abs(input1->y - input2->y) <= EPS; } };
須要特別注意的是對於double的處理,在試運行時發現無論是floor仍是強制轉換爲int操做,均可能會出現BUG。例若有機率把(double)1轉換成0或者是1,出現二義性。所以在轉換完成以後必須新增一步if判斷操做。犧牲部分效率確保正確性。工具
二、其次是對於圖形類的具體實現以及圖像的添加。對於新增的射線和線段類,讓它們繼承直線類,射線類新增方向屬性以及起始節點,以下所示:
class Seg :public Line { public:double mx2, my2, mx1, my1; Seg(double input1, double input2, int ifspecial, double input3, double input4, double input5, double input6); };,而線段類則新增對於2個端點的記錄。以下所示: class Ray :public Line { public:double mx1, my1, mx2, my2; int direction;//1,2,3,4表示延長到的象限位置,-1表示朝x軸正方向延伸,-2,表示朝x軸負方向延伸,-3表示向y軸正方向延伸,-4表示向y軸負方向延伸 Ray(double input1, double input2, int ifspecial, double input3, double input4, double input5, double input6); };
圖形的產生依然使用工廠模式,在其中增長了對於線段和射線類的處理:
Shape* ShapeFactory::GetShape(string type, double temp1, double temp2, double temp3, double temp4) { if (type == "L") { if (temp1 == temp3) return new Line(temp1, 0, 1); else return new Line((temp4 - temp2) / (temp3 - temp1), temp2 - (temp4 - temp2) / (temp3 - temp1) * temp1, 0); } else if (type == "R") { if (temp1 == temp3) return new Ray(temp1, 0, 1, temp1, temp2, temp3, temp4); else return new Ray((temp4 - temp2) / (temp3 - temp1), temp2 - (temp4 - temp2) / (temp3 - temp1) * temp1, 0, temp1, temp2, temp3, temp4); } else if (type == "S") { if (temp1 == temp3) return new Seg(temp1, 0, 1, temp1, temp2, temp3, temp4); else return new Seg((temp4 - temp2) / (temp3 - temp1), temp2 - (temp4 - temp2) / (temp3 - temp1) * temp1, 0, temp1, temp2, temp3, temp4); } else if (type == "C") { return new Circle(temp1, temp2, temp3); } else { } return NULL;}
三、對於具體交點的計算,因爲新增的線段和射線計算交點的方式和直線計算交點的方式基本相同,所以在架構上徹底沿用以前對直線求交點的函數。在交點計算完成後增長一個判斷交點是否在射線或線段上的環節。須要注意的是可能會出現線段或射線首尾相連,出現1個交點的特殊狀況,所以須要對這種狀況進行特殊判斷:
void L2L(Line* input1, Line* input2, unordered_set<Point*, Hash_Point, Equal_Point>& g_allpoint) { if (input1->mspecial == 0 && input2->mspecial == 0) { if (abs(input1->ma - input2->ma) <= EPS) { if (abs(input1->mb - input2->mb) <= EPS) L2L_Special(input1, input2, 0, g_allpoint);//對於特殊狀況的判斷處理 return; } double x = (input2->mb - input1->mb) / (input1->ma - input2->ma); double y = x * input1->ma + input1->mb; if (Line_Process(input1, x, y) && Line_Process(input2, x, y))//判斷交點是否在線上 g_allpoint.insert(new Point(x, y)); return; } else if (input1->mspecial == 1 && input2->mspecial == 1) { if (input1->ma == input2->ma) L2L_Special(input1, input2, 1, g_allpoint); return; } else { if (input1->mspecial == 1) { double x = input1->ma; double y = input1->ma * input2->ma + input2->mb; if (Line_Process(input1, x, y) && Line_Process(input2, x, y)) g_allpoint.insert(new Point(x, y)); } else { double x = input2->ma; double y = input1->ma * input2->ma + input1->mb; if (Line_Process(input1, x, y) && Line_Process(input2, x, y)) g_allpoint.insert(new Point(input2->ma, input1->ma * input2->ma + input1->mb)); } } }
對於特殊狀況處理,這部分代碼邏輯較爲複雜。主要思路是判斷位於同一直線上的射線和線段,射線和射線,線段和線段之間究竟是存在一個交點,仍是存在覆蓋區域,致使無限多個交點產生。代碼以下所示:
void L2L_Special(Line* input1, Line* input2, int ifspecial, unordered_set<Point*, Hash_Point, Equal_Point>& g_allpoint) { if (input1->mtype == "L" || input2->mtype == "L") throw string("infinite point"); //若是有一個爲直線一定存在無限多個交點 else if (input1->mtype == "R" && input2->mtype == "R") { if (abs(input1->ma - input2->ma) <= EPS && abs(input1->mb - input2->mb) <= EPS) { if (((Ray*)input1)->direction == ((Ray*)input2)->direction) { throw string("infinite point"); }//起始節點相同以及方向相同射線一定有無數多個節點 else { g_allpoint.insert(new Point(input1->ma, input1->mb)); //起始節點相同以及方向相反射線一定只有一個節點 return; } } else { //起始節點不一樣節點要麼有無數多個節點,要麼沒有節點 int direction = DirectionGet(((Ray*)input1)->mx1, ((Ray*)input1)->my1, ((Ray*)input2)->mx1, ((Ray*)input2)->my1); if (direction == ((Ray*)input1)->direction)throw string("infinite point"); else return; } } else if (input1->mtype == "R" || input2->mtype == "R") { //對於射線和線段的判斷 Ray* r1; Seg* s1; if (input1->mtype == "R") { r1 = (Ray*)input1; s1 = (Seg*)input2; } else { r1 = (Ray*)input2; s1 = (Seg*)input1; } int direction1 = DirectionGet(r1->mx1, r1->my1, s1->mx1, s1->my1); int direction2 = DirectionGet(r1->mx1, r1->my1, s1->mx2, s1->my2); if (direction1 == r1->direction || direction2 == r1->direction) throw string("infinite point"); else if (direction1 == 0 || direction2 == 0) g_allpoint.insert(new Point(r1->mx1, r1->my1)); else return; } else { //對於線段和線段的判斷 Seg* s1 = (Seg*)input1; Seg* s2 = (Seg*)input2; double largex1 = s1->mx1 > s1->mx2 ? s1->mx1 : s1->mx2; double smallx1 = s1->mx2 > s1->mx1 ? s1->mx1 : s1->mx2; double largex2 = s2->mx1 > s2->mx2 ? s2->mx1 : s2->mx2; double smallx2 = s2->mx2 > s2->mx1 ? s2->mx1 : s2->mx2; if (abs(smallx1 - largex2) <= EPS) { if (abs(smallx1 - s1->mx1) <= EPS) g_allpoint.insert(new Point(s1->mx1, s1->my1)); else g_allpoint.insert(new Point(s1->mx2, s1->my2)); } else if (abs(largex1 - smallx2) <= EPS) { if (abs(largex1 - s1->mx1) <= EPS) g_allpoint.insert(new Point(s1->mx1, s1->my1)); else g_allpoint.insert(new Point(s1->mx2, s1->my2)); } else if (smallx1 > largex2 || smallx2 > largex1) return; else throw string("infinite point"); } }
對於求交點的函數不太好將其歸爲具體的一類之中,所以不將其做爲具體類的成員函數進行處理,而做爲類c函數提供給接口。
下表爲優化前的具體花費時間,可見當節點數大於1000後增加速度很是不樂觀:
N | 時間(ms) |
---|---|
200 | 45 |
400 | 215 |
600 | 605 |
800 | 920 |
1000 | 1754 |
2000 | 8410 |
3000 | 30360 |
4000 | 83956 |
可見調用時間最多的是在unordered_set中的hash和equal函數的使用,所以打算着重對這兩個函數進行優化。
對於hash函數將相同變量外提,減小具體代碼量,同時改寫對hash的生成,減小hash衝突:
size_t operator()(const class Point* input1)const { //return (int)(((int)input1.x) * 1e6 / 10 + ((int)input1.y) * 1e6 / 10); double x = input1->x; double y = input1->y; long temp1 = (long)floor(x); long temp2 =(long) floor(y); if (abs(x - ((long long)temp1+1)) <= EPS) temp1++; if (abs(y- ((long long)temp2+1)) <= EPS) temp2++; std::hash<long> long_hash; return long_hash.operator()(temp1)+ long_hash.operator()(temp2); }
因爲equal函數已經爲最簡形態,沒法進行進一步優化。
優化後的結果爲:
可見hash衝突狀況顯著降低,同時hash和equal調用次數顯著下降。對於4000個圖形時的計算時間下降到14208毫秒,效率大幅提高。
code contract與design by contract經過前置條件,不變式以及後置條件,同時運用動態檢查以及靜態檢查,使函數和接口的正確性獲得充分的保證
優勢:
相較於天然語言而言表述更加準確可靠,有無二義性。
明確接口的功能。幫助編程者以目標爲導向進行編程,同時幫助調用者可以更全面地瞭解接口的功能
方便DEBUG調試。能夠經過外置的插件快速生成大量測試數據完成DEBUG,比人工測試更加快捷和可靠。
便於程序複用。以後的編程者可以快速清楚代碼的功能,更高效的着手對改寫。
缺點:
對於JML早在面向對象的課程中就有所涉及。在此次的程序設計中我將UI所需的接口封裝爲5個函數,2個變量實體,並在底層確保以上接口的正確性。使得調用者並不須要瞭解程序運行的所有機制就可以經過調用獲得所想要的效果。
我對本程序的單元測試共分爲4個環節。包括對於程序計算函數的單元測試,對於exe文件的單元測試,對於UI接口的單元測試以及對於錯誤處理的單元測試。
對於計算函數測試部分主要目的是確保底層的準確性,進而確保接口準確性,如下爲部分代碼:
TEST_METHOD(TestMethod20) { bool b1 = RangeJudge(-100000); bool b2 = RangeJudge(100000); bool b3 = RangeJudge(-100001); bool b4 = RangeJudge(100001); bool b5 = RangeJudge(-99999); bool b6 = RangeJudge(99999); Assert::AreEqual(b1, true); Assert::AreEqual(b2, true); Assert::AreEqual(b3, true); Assert::AreEqual(b4, true); Assert::AreEqual(b5, false); Assert::AreEqual(b6, false);
對於exe文件部分測試代碼以下所示:
TEST_METHOD(TestMethod22) { FILE* stream1; FILE* stream2; freopen_s(&stream1, "G:\\360MoveData\\Users\\HP\\Desktop\\nt\\input.txt", "w", stdout); printf("%d\n", 3); printf("L 0 0 1 1\n"); printf("L 0 0 1 2\n"); printf("L 0 0 1 3\n"); fclose(stdout); PROCESS_INFORMATION ProcessInfo; STARTUPINFO StartupInfo; //入口參數 ZeroMemory(&StartupInfo, sizeof(StartupInfo)); StartupInfo.cb = sizeof StartupInfo; //分配大小 if (CreateProcess("G:\\360MoveData\\Users\\HP\\Desktop\\nt\\IntersectProject.exe", "G:\\360MoveData\\Users\\HP\\Desktop\\nt\\IntersectProject.exe -i G:\\360MoveData\\Users\\HP\\Desktop\\nt\\input.txt -o G:\\360MoveData\\Users\\HP\\Desktop\\nt\\output.txt", NULL, NULL, FALSE, HIGH_PRIORITY_CLASS, NULL, NULL, &StartupInfo, &ProcessInfo)) { WaitForSingleObject(ProcessInfo.hProcess, INFINITE); CloseHandle(ProcessInfo.hThread); CloseHandle(ProcessInfo.hProcess); } //WinExec("G:\\360MoveData\\Users\\HP\\Desktop\\nt\\IntersectProject.exe -i G:\\360MoveData\\Users\\HP\\Desktop\\nt\\input.txt -o G:\\360MoveData\\Users\\HP\\Desktop\\nt\\output.txt", SW_HIDE); int result = 0; FILE* open = fopen("G:\\360MoveData\\Users\\HP\\Desktop\\nt\\output.txt", "r"); fscanf(open, "%d", &result); fclose(open); Assert::AreEqual(1, result); };
對於錯誤處理的部分代碼以下所示:
TEST_METHOD(TestMethod46) { FILE* stream1; freopen_s(&stream1, "G:\\360MoveData\\Users\\HP\\Desktop\\nt\\input.txt", "wt", stdout); printf("%d\n", 1); printf("C 0 0 0\n"); fclose(stdout); PROCESS_INFORMATION ProcessInfo; STARTUPINFO StartupInfo; //入口參數 ZeroMemory(&StartupInfo, sizeof(StartupInfo)); StartupInfo.cb = sizeof StartupInfo; //分配大小 if (CreateProcess("G:\\360MoveData\\Users\\HP\\Desktop\\nt\\IntersectProject.exe", "G:\\360MoveData\\Users\\HP\\Desktop\\nt\\IntersectProject.exe -i G:\\360MoveData\\Users\\HP\\Desktop\\nt\\input.txt -o G:\\360MoveData\\Users\\HP\\Desktop\\nt\\output.txt", NULL, NULL, FALSE, HIGH_PRIORITY_CLASS, NULL, NULL, &StartupInfo, &ProcessInfo)) { WaitForSingleObject(ProcessInfo.hProcess, INFINITE); CloseHandle(ProcessInfo.hThread); CloseHandle(ProcessInfo.hProcess); } //WinExec("G:\\360MoveData\\Users\\HP\\Desktop\\nt\\IntersectProject.exe -i G:\\360MoveData\\Users\\HP\\Desktop\\nt\\input.txt -o G:\\360MoveData\\Users\\HP\\Desktop\\nt\\output.txt", SW_HIDE); char result[40]; FILE* open = fopen("G:\\360MoveData\\Users\\HP\\Desktop\\nt\\output.txt", "r"); fgets(result, 40, open); fclose(open); Assert::AreEqual("radius must be greater than 0\n", result); };
對於UI接口測試的代碼以下所示:
TEST_METHOD(TestMethod53) { Add_Diagram("C 1 0 2 0", 1); Add_Diagram("C 2 2 1 0", 1); Add_Diagram("C 3 -2 6 0", 1); Add_Diagram("L -1 4 4 -1", 1); Calculate(); Assert::AreEqual(6, (int)myallpoint.size()); Clear(); Calculate(); Assert::AreEqual(0, (int)myallpoint.size()); Add_Diagram("C 1 0 2 0", 1); Add_Diagram("C 2 2 1 0", 1); Add_Diagram("C 3 -2 6 0", 1); Add_Diagram("L -1 4 4 -1", 1); Calculate(); Assert::AreEqual(6, (int)myallpoint.size()); Sub_Diagram("C 1 0 2 0", 1); Sub_Diagram("C 2 2 1 0", 1); Sub_Diagram("C 3 -2 6 0", 1); Sub_Diagram("L -1 4 4 -1", 1); myallpoint.clear(); Calculate(); Assert::AreEqual(0, (int)myallpoint.size()); }
總共進行了52組不一樣的測試,且測試覆蓋率達到了90%以上,整體上知足了課程要求。
錯誤類型 | 輸入(其中一種) | 描述 | 輸出(輸出在文件中) |
---|---|---|---|
線重合 | 2 L 0 0 1 1 L 0 0 -1 -1 | 線性圖形有重疊部分,致使無限多個交點 | infinite point |
圓的重合 | 2 C 1 1 1 C 1 1 1 | 兩個圓圓心和半徑相等 | same circle error |
輸入點重合 | 1 L 1 1 1 1 | 線性圖形2個輸入點徹底相同 | same point in a line |
座標超限 | 1 L 10000 1000001 1000002 1000003 | 輸入點座標超過限制 | coordinate out of range |
半徑小於0 | 1 C 0 0 -1 | 輸入圓的半徑小於-1 | radius must be greater than 0 |
本程序使用QT完成界面模塊的開發工做。結合以前計算模塊給出的接口,同時QT模塊中使用了Qcustomplot模組幫助進行了繪畫功能的實現。整體上界面由一個主界面和子界面組成。主界面中給出了7種功能,包括從文件中導入圖形,手動輸入圖形,手動刪除圖形,繪製圖形,給出交點個數,清零,退出功能。
爲了實現各分功能,在主界面程序中定義了所須要相應的函數,以及對應的信號槽,以下圖所示:
private slots: void on_input_clicked(); void F_INPUT(); void FILE_INPUT(); void RESULT(); void CLEAR(); void SHOWDELETE(); void myPaint(); ui->setupUi(this); connect(ui->quit, SIGNAL(clicked()), this, SLOT(close())); connect(ui->input,SIGNAL(clicked()),this,SLOT(F_INPUT())); connect(ui->load,SIGNAL(clicked()),this,SLOT(FILE_INPUT())); connect(ui->show_result,SIGNAL(clicked()),this,SLOT(RESULT())); connect(ui->clear,SIGNAL(clicked()),this,SLOT(CLEAR())); connect(ui->delete_2,SIGNAL(clicked()),SLOT(SHOWDELETE())); connect(ui->draw,SIGNAL(clicked()),this,SLOT(myPaint()));
如下爲自定義輸入界面以及具體的函數,函數中使用了計算模塊給出的接口Add_Diagram(),實現了圖形的增長操做:
void Form::hand_out(){ string str=ui->first_edit->text().toStdString(); const char* ch=str.c_str(); if(temp==NULL){ ui->first_edit->setText("illegal input"); } else{ Add_Diagram((char*)ch,1); ui->first_edit->setText("successful insert");} //close();} }
圖像繪製部分的函數較爲複雜,使用了第三方庫Qcustomplot完成。因爲繪製部分偏向於UI部分單獨完成,所以在UI中定義了一系列的函數幫助進行繪圖工做:
void paint::paintEvent(QPaintEvent *){ QCustomPlot *customPlot = ui->qcustomPlot; customPlot->clearItems(); customPlot->clearGraphs(); ui->qcustomPlot->addGraph(); ui->qcustomPlot->graph(0)->setLineStyle(QCPGraph::LineStyle::lsNone); ui->qcustomPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables); ui->qcustomPlot->axisRect()->setupFullAxesBox(); ui->qcustomPlot->rescaleAxes(); ui->qcustomPlot->xAxis->setRange(-100000,100000); ui->qcustomPlot->yAxis->setRange(-100000,100000); for (int i = 0 ; i <(int) myallshape.size();i++){ double x1,y1,x2,y2; string type=myallshape.at(i)->mtype; if(type=="R"){ Ray* myray=(Ray*)myallshape.at(i); x1= myray->mx1; y1 = myray->my1; x2 = x1+myray->mx2; y2 = y1+ myray->my2; paintRay(customPlot,x1,y1,x2,y2); } else if(type=="S"){ Seg* myray=(Seg*)myallshape.at(i); x1= myray->mx1; y1 = myray->my1; x2 = x1+myray->mx2; y2 = y1+ myray->my2; paintSegment(customPlot,x1,y1,x2,y2); } else if(type=="L"){ Line* myline=(Line*)myallshape.at(i); if(myline->mspecial==0){ x1=1; y1=myline->ma+myline->mb; x2=2; y2=2*myline->ma+myline->mb; paintLine(customPlot,x1,y1,x2,y2); } if(myline->mspecial==1){ x1=myline->ma; y1=1; x2=myline->ma; y2=2; paintLine(customPlot,x1,y1,x2,y2); } } else if(type=="C"){ Circle* mycircle=(Circle*)myallshape.at(i); x1=mycircle->mx; y1=mycircle->my; x2=mycircle->mr; paintCircle(customPlot,x1-x2,y1+x2,x1+x2,y1-y2); } if (i%5 == 0) customPlot->replot(); } customPlot->replot(); }
其餘部分因爲篇幅有限,不給出具體UI圖片,只給出具體函數代碼:
刪除操做:
void mydelete::delete_this(){ string str=ui->lineEdit->text().toStdString(); const char* ch=str.c_str(); Sub_Diagram((char*)ch,1); ui->lineEdit->setText("successful delete"); }
求解交點個數並顯示:
result::result(QWidget *parent) : QMainWindow(parent), ui(new Ui::result) { ui->setupUi(this); myallpoint.clear(); Calculate(); QStandardItemModel* ItemModel = new QStandardItemModel(this); int result=myallpoint.size(); QString string = QString::number(result); QStandardItem *item = new QStandardItem(string); ItemModel->appendRow(item); ui->listView->setModel(ItemModel); //ui->listView->setFixedSize(600,600); }
經過計算模塊事先給出的接口,UI模塊只須要調用這些接口就可以實現求解交點、增長圖形、刪除圖形、清空和處理文件輸入的操做,給出的接口以下所示:
extern vector <Shape*> myallshape; extern unordered_set<Point*, Hash_Point, Equal_Point> myallpoint; void Add_Diagram(char* input1, int ifoutsource); void Sub_Diagram(char* input2, int ifoutsource); void File_InputProcess(); void Clear(); void Calculate();
UI程序經過引入dll程序以及調用.h文件就能快速實現所須要的一系列操做。
結對編程 | 我 | 結對夥伴 | |
---|---|---|---|
優勢 | 一、持續代碼複審,減小BUG產生。二、互相學習代碼風格,可以糾正本身自己的錯誤。三、可以讓雙方更高效地編程,增長工做效率。 | 代碼能力較爲熟練。 | 比較仔細,可以快速找到程序中的BUG。 |
缺點 | 一、磨合和交流都須要花費必定的時間。二、結對夥伴的能力直接影響了編程的質量。三、容易出現二人之間地位不平衡的狀況。 | 吧不太擅長測試,同時編寫程序BUG較多,而且交流不太主動。 | 編碼能力不強。 |
已完成對圓的處理(具體實現略),交換程序部分並無完成。