引言
这几乎是任何一门语言的经典案例。
管理信息系统。
MIS(管理信息系统——Management Information System)系统 ,是一个由人、计算机及其他外围设备等组成的能进行信息的收集、传递、存贮、加工、维护和使用的系统。
为什么一般情况下,看别人的代码很困难?
因为很多时候写代码就不是从头到尾,线性地在写……
为了让我这篇文章成为一篇所谓的简单的教学版,我就把代码挑着贴了,以便理清思路。最后,再想办法传一个完整的项目上去。
需求如下
这是功能清单。后文会多次使用。
意思是,这个软件需要有哪些功能。(换种说法就是,这个软件如果做出来了,我就可以用它来干以下这些事情)
* 1. 学生信息录入 (姓名、年龄、id)
* 2. 学生信息修改
* 3. 学生删除
* 4. 显示
* 5. 根据id查找
* 6. 保存至文件
* 7. 从文件恢复
尽量用OOP的方式实现。分层实现。
注意事项
因为C++本身OOP还不纯粹,很有可能写着写着就是函数+函数+函数……
而且还是全局函数!简直就是面向过程的写法。
记得我的第一个完整意义上的软件,就是做的这个。
当时是用C语言写的。还照着书上的代码写。
一个个的overload的function,各种标志位。
e.g. void updateList (Stu * list, Stu stu, int flag); // flag是操作描述标志位。意味着这个函数是修改、或是删除、或是增加…
从早上写到下午,磕磕绊绊终于写完了。
2012年2月吧,大一寒假,好像是,春节期间。在巡司镇的天河温泉旁边。
现在又重来一遍。这次用C++。加上用了STL。开发速度快了不少。
所有变量都是在stack上,如果数据太大了,stack根本就装不下,所以以后可以考虑在heap上开辟空间,同时用智能指针shared_ptr。
软件设计
这么小的软件,是直接开始写代码,还是设计一下再写呢?
考虑了一下,还是先设计一下吧。说一些设计原则。
一、一些设计原则
1. 整个软件是控制台程序,所以一个黑窗口就可以搞定,不需要做别的界面。
2. 把学生信息保存到文件,用一个TXT。
3. 为了让用户和软件之间有个简单的交互,提供一个处理界面的类。(尽管main函数里面在接收用户的输入,这样设计不是很好吧…)
二、关键的class
下面,开始设计类。
1. main。不算类了。要接收用户输入,调用处理界面更新功能的CMainView类。
2. CMainView类。处理界面更新功能的类,相当于界面层(视图层,View层)。不仅是更新界面,还有处理用户的输入,调用下面CStuMg类中对应的函数实现对应功能。
3. CStuMg类。处理信息,提供业务功能的类。e.g. 添加一个学生,删除一个学生…。这个类是实现业务功能的类,执行业务功能,把执行结果交给上层(View层)。
4. CStu类。实体对象类。为什么要有这个类,我就不说了。简单起见,成员属性只有3个,学生id,学生name,学生age。
三、数据存储
1. 在软件执行过程中,数据存在内存中。为了在内存中保存学生,用了一个map<int, CStu> 的stu_map。用学生id作为map的key,用CStu作为map的value。选择map,主要是为了避免学号重复。
2. 需要保存学生信息至硬盘时,数据写入硬盘上的TXT中。保存方式是:一行对应一个学生。每一行都有三列,分别是id,name和age。这三个“字段”分别对应CStu的三个成员属性。同时,各个字段之间,用一个空格隔开。为了方便地把map中的所有学生信息全部写入到文件中,我重载了CStu的<<运算符,实现把一个CStu对象的3个成员变量的值,<<到输出流ostream中。写文件的时候,调用stringstream,<<stu,再把stringstream的str()字符串内容,<<到ofstream(文件的输出流)中。
3. 为了从文件读入学生信息,采用一行一行读取的方式。通过重载CStu的>>运算符,实现把字符串直接载入一个CStu对象的功能。
编程一:搭框架
本项目很简单,很简单,很简单。
尽管项目再小,请大家都养成好习惯:先搭框架,再填里面的内容。
如果不这样做,后果就是,按照老板的话来说,要写崩溃的。我第一次写的时候,也是这种感觉。上来就开始写,没写到一会儿,就写崩盘了。
开发Domain类CStu
我们的实体类(也叫Domain类吧),就是CStu学生类。根据上述三个成员变量,写出如下代码。(很简单,……)。
另外,重载了<<和>>运算符。(原因请见“软件设计”的第三点“数据存储”)
先写CStu的头文件。
#ifndef C_STUDENT_H
#define C_STUDENT_H
#include <string>
#include <iostream>
class CStudent
{
/* friend function */
friend std::ostream & operator<<(std::ostream & os, CStudent & stu);
friend std::istream & operator>>(std::istream & is, CStudent & stu);
public:
CStudent();
~CStudent();
int getId() const;
void setId(int val);
std::string getName() const;
void setName(std::string val);
int getAge() const;
void setAge(int val);
private:
int id;
std::string name;
int age;
};
#endif
再到CStu的CPP文件里,实现这些简单的函数。在CStu的构造函数中,先把stu的id设为-1,做一个无效标记。下面会提到为什么要这样做。
#include <iostream>
#include <string>
#include "CStudent.h"
using namespace std;
// 学生默认id是-1,说明这暂时是一个无效的学生。
CStudent::CStudent(){this->id = -1;}
CStudent::~CStudent(){}
int CStudent::getId() const { return id; }
void CStudent:: setId(int val) { id = val; }
std::string CStudent:: getName() const { return name; }
void CStudent:: setName(std::string val) { name = val; }
int CStudent:: getAge() const { return age; }
void CStudent:: setAge(int val) { age = val; }
std::ostream & operator<<(std::ostream & os, CStudent & stu) {
// 保存的时候,千万不要把,也保存进去!不然输入的时候,非常不好解决!-_-!
// ss >> x; 会失败!!有逗号!!
// os<<"id: "<<stu.getId()<<", name: "<<stu.getName()<<", age: "
// <<stu.getAge();
os<<stu.getId()<<" "<<stu.getName()<<" "<<stu.getAge();
return os;
}
std::istream & operator>>(std::istream & is, CStudent & stu) {
is>>stu.id>>stu.name>>stu.age;
return is;
}
唯一需要注意的就是在重载<<的时候。问题见代码中的注释。
开发CStuMg业务类
这里为了开发业务类,为了让软件的功能不重不漏,我就对照着最开始的需求里的功能列表来写。见上文,一共7个功能。
这里就只有一个业务类,叫CStuMg。(Mg就是management的意思……)
和开发CStu类一样,先写头文件。
#ifndef C_STUDENT_MG_H
#define C_STUDENT_MG_H
#include "CStudent.h"
#include <map>
#include <string>
class CStudentMg
{
public:
CStudentMg();
~CStudentMg();
// 增
CStudent addAStu(std::map<int,CStudent> & m1,CStudent & stu);
// 删
bool deleteStuById(std::map<int, CStudent> & m1,const int & id);
// 改
CStudent updateStu(std::map<int, CStudent> & m1, const CStudent & stu);
// 查 by id
CStudent findById(const std::map<int, CStudent> & m1, const int & id) const;
// showAll
void showAll(const std::map<int, CStudent> & m1 ) const;
// save to file
bool saveToFile(const std::map <int,CStudent> & m1,const std::string & pathName) const;
// read from file
bool readFromFile(std::map<int, CStudent> & m1, std::string path);
private:
};
#endif
这样一来,就看得见我这个软件最后一共有哪些功能了。
其实更好的做法是,在代码里多写注释!
e.g. 输入变量是什么,返回值是什么,什么样的情况下返回什么样的结果……
开发完head文件以后,再到它对应的CPP文件中,完成函数体。
可以现在就把具体的函数体写全了,也可以先就把函数体{...}空起来,意思一下即可。
我更推荐第二种做法。让程序在语法上能通,具体的业务等我们把软件的框架搭起来以后再实现。
e.g. 如果要求返回int,在{}里直接写个return 0; 就好。
因为现在要做的是,而且也只需要,把框架搭起来。
#include <iostream>
#include <fstream>
#include <string>
#include <map>
#include <sstream>
#include "CStudent.h"
#include "CStudentMg.h"
using namespace std;
CStudentMg::CStudentMg()
{
}
CStudentMg::~CStudentMg()
{
}
// 增
CStudent CStudentMg:: addAStu(map<int,CStudent> & m1,CStudent & stu) {
return stu;
}
// 删
bool CStudentMg:: deleteStuById(map<int, CStudent> & m1, const int & id) {
bool b = false;
return b;
}
// 改
CStudent CStudentMg:: updateStu(map<int,CStudent> & m1,const CStudent & cStu) {
CStudent stu;
return stu;
}
// 查 by id
CStudent CStudentMg:: findById(const map <int, CStudent> & m1, const int & id) const{
CStudent stu ;
return stu;
}
// showAll
void CStudentMg:: showAll(const map<int,CStudent> & m1) const{
}
// save to file
bool CStudentMg::saveToFile(const map <int,CStudent> & m1,const string & pathName) const{
bool b = true;
return b;
}
// read from file
bool CStudentMg:: readFromFile(std::map<int,CStudent> & m1, std::string path) {
bool b = true;
return b;
}
发现了咩???其实到现在为止,就根本还没有写什么具体的业务!!!但是,软件在语法上是可以满足的。这些代码是可以编译成功的!
开发界面层CMainView类
这个类写了,我们的软件就有界面了。(尽管这个界面还只是黑窗口的形式)
依然是先写头文件。依然是对照着需求的功能清单写。
#ifndef C_MAIN_VIEW_H
#define C_MAIN_VIEW_H
#include <iostream>
#include <map>
#include <string>
#include "CStudent.h"
#include "CStudentMg.h"
class CMainView
{
public:
CMainView();
~CMainView();
/* 欢迎 */
void welcome();
/* 显示菜单 */
void showMenu();
/* view 显示所有学生 */
void showAllStuAtView(const std::map<int, CStudent> & stu_m1);
/* view层 添加一个学生 */
void addStuAtView( std::map<int, CStudent> & stu_m1 );
/* view 查找一个学生 */
void findStuAtView(const std::map<int, CStudent> & m1) ;
/* view层删除一个学生 */
void deleteByIdAtView(std::map<int, CStudent> & v1);
/* view层 更新一个学生 */
void updateByIdAtView(std::map<int, CStudent> & m1);
/* view层 把map保存进文件 */
void saveToFileAtView(const std::map<int, CStudent> & m1, std::string pathName);
/* view层 把文件中的东西导入 map */
void readFromFileAtView(std::map<int, CStudent> & m, std::string pathName);
private:
};
#endif
然后,再到对应的CPP文件里,把函数都抄一遍……
#include "CMainView.h"
CMainView::CMainView()
{
}
CMainView::~CMainView()
{
}
/* 欢迎 */
void CMainView:: welcome() {
}
/* 显示菜单 */
void CMainView:: showMenu() {
}
/* view 显示所有学生 */
void CMainView:: showAllStuAtView(const std::map<int, CStudent> & stu_m1 ){
}
/* view层 添加一个学生 */
void CMainView:: addStuAtView( std::map<int, CStudent> & stu_m1 ){
}
/* view 查找一个学生 */
void CMainView:: findStuAtView(const std::map<int, CStudent> & m1) {
}
/* view层删除一个学生 */
void CMainView:: deleteByIdAtView(std::map<int, CStudent> & v1) {
}
/* view层 更新一个学生 */
void CMainView:: updateByIdAtView(std::map<int, CStudent> & m1) {
}
/* view层 把vec保存进文件 */
void CMainView:: saveToFileAtView(const std::map<int, CStudent> & m1, std::string pathName) {
}
/* view层 把文件中的东西导入 vec */
void CMainView:: readFromFileAtView(std::map<int, CStudent> & m, std::string pathName) {
}
View框架也写好了。
开发Main函数
main函数的开发,也就对照着需求的功能清单,把软件需要提供的功能,都列出来。
我这里的设计思路是,用户输入0,软件退出,输入别的列出来的operateType,就执行相应功能。否则,就清屏,忽略用户的错误输入。
在main函数里,针对用户不同操作类型的输入,调用业务类CStuMg中的成员函数,执行相应的操作。
在main和mainView在开发过程中,可能会互相交叉着写。这个就看个人了。
#include <iostream>
#include <string>
#include <map>
#include "CStudent.h"
#include "CStudentMg.h"
#include "CMainView.h"
using namespace std;
int main() {
string pathName = "d:/student_manegement.txt";
map<int, CStudent> stu_v1;
CMainView cView;
cView.welcome();
cView.showMenu();
string operateType;
cin>>operateType;
while (operateType!="0")
{
if (operateType=="1") {// 录入
cView.addStuAtView(stu_v1);
}
else if(operateType=="2") { // 修改
cView.updateByIdAtView(stu_v1);
}
else if(operateType=="3") { // 查找
cView.findStuAtView(stu_v1);
}
else if (operateType=="4") {
cView.deleteByIdAtView(stu_v1);
}
else if( operateType == "5") { // 显示所有
cView.showAllStuAtView(stu_v1);
}
else if( operateType=="6") { // 保存至文件
cView.saveToFileAtView(stu_v1,pathName);
}
else if(operateType=="7") {// 从文件读取
cView.readFromFileAtView(stu_v1,pathName);
}
else {
cView.welcome();
cView.showMenu();
}
cin>>operateType;
}
return 0;
}
至此,软件的框架就开发好了。剩下的内容,就是去把没有填上的函数体都填上。
编程二:实现步骤
View层的CMainView
把前面空着的一堆函数的函数体都填上。
#include "CMainView.h"
CMainView::CMainView()
{
}
CMainView::~CMainView()
{
}
/* 欢迎 */
void CMainView:: welcome() {
system("cls");
std::cout<<"欢迎来到xxx大系统"<<std::endl;
}
/* 显示菜单 */
void CMainView:: showMenu() {
std::cout<<"\n";
std::cout<<"操作步骤"<<std::endl;
std::cout<<"1. 录入"<<std::endl;
std::cout<<"2. 修改"<<std::endl;
std::cout<<"3. 查找"<<std::endl;
std::cout<<"4. 删除"<<std::endl;
std::cout<<"5. 显示所有"<<std::endl;
std::cout<<"6. 保存至文件"<<std::endl;
std::cout<<"7. 从文件导入"<<std::endl;
std::cout<<"0. 退出"<<std::endl;
std::cout<<"\n";
std::cout<<"Author:qcy"<<std::endl;
std::cout<<"2016/11/28"<<std::endl;
std::cout<<"\n";
std::cout<<"请选择操作:";
}
/* view 显示所有学生 */
void CMainView:: showAllStuAtView(const std::map<int, CStudent> & stu_m1 )
{
system("cls");
std::cout<<"id |"<<"name |"<<"age"<<std::endl;
CStudentMg cStuMg;
cStuMg.showAll(stu_m1);
system("pause");
system("cls");
welcome();
showMenu();
}
/* view层 添加一个学生 */
void CMainView:: addStuAtView( std::map<int, CStudent> & stu_m1 )
{
CStudentMg cStuMg;
int id;
std::string name;
int age;
CStudent cStu;
system("cls");
std::cout<<"录入\n";
std::cout<<"id:";
std::cin>>id;
std::cout<<"name:";
std::cin>>name;
std::cout<<"age:";
std::cin>>age;
cStu.setId(id);
cStu.setName(name);
cStu.setAge(age);
// 保存
cStuMg.addAStu(stu_m1,cStu);
system("cls");
welcome();
showMenu();
}
/* view 查找一个学生 */
void CMainView:: findStuAtView(const std::map<int, CStudent> & m1) {
system("cls");
std::cout<<"请输入要查找学生的id"<<std::endl;
int id;
std::cin>>id;
CStudentMg cStuMg;
CStudent cStu;
cStu = cStuMg.findById(m1,id);
if (cStu.getId()!=-1)
{
std::cout<<cStu<<std::endl;
}
else
{
std::cout<<"查无此人"<<std::endl;
}
system("pause");
system("cls");
welcome();
showMenu();
}
/* view层删除一个学生 */
void CMainView:: deleteByIdAtView(std::map<int, CStudent> & v1) {
system("cls");
std::cout<<"请输入要删除的学生的id"<<std::endl;
int id;
std::cin>>id;
CStudentMg cStuMg;
bool b = cStuMg.deleteStuById(v1,id);
if (b)
{
std::cout<<"删除成功"<<std::endl;
}
else {
std::cout<<"查无此人"<<std::endl;
}
system("pause");
system("cls");
welcome();
showMenu();
}
/* view层 更新一个学生 */
void CMainView:: updateByIdAtView(std::map<int, CStudent> & m1) {
system("cls");
std::cout<<"请输入要修改的学生的id"<<std::endl;
int id;
std::cin>>id;
std::string name;
std::cout<<"name:";
std::cin>>name;
int age;
std::cout<<"age:";
std::cin>>age;
CStudent cStu;
cStu.setId(id);
cStu.setName(name);
cStu.setAge(age);
CStudentMg cStuMg;
CStudent cStu2 = cStuMg.updateStu(m1,cStu);
if (cStu2.getId()!=-1)
{
std::cout<<cStu2<<std::endl;
std::cout<<"修改成功"<<std::endl;
}
else
{
std::cout<<"查无此人"<<std::endl;
}
system("pause");
system("cls");
welcome();
showMenu();
}
/* view层 把vec保存进文件 */
void CMainView:: saveToFileAtView(const std::map<int, CStudent> & m1, std::string pathName) {
if (m1.begin()==m1.end()) {
system("cls");
std::cout<<"还没有任何学生信息,无法保存"<<std::endl;
}
else {
// 保存
CStudentMg cStuMg;
bool b = cStuMg.saveToFile(m1,pathName);
if (b) {
system("cls");
std::cout<<"保存成功"<<std::endl;
}
else {
std::cout<<"保存失败"<<std::endl;
}
}
system("pause");
system("cls");
welcome();
showMenu();
}
/* view层 把文件中的东西导入 vec */
void CMainView:: readFromFileAtView(std::map<int, CStudent> & m, std::string pathName) {
system("cls");
CStudentMg cStuMg;
bool b = cStuMg.readFromFile(m,pathName);
if (b){
std::cout<<"读取成功"<<std::endl;
}
else {
std::cout<<"读取失败"<<std::endl;
}
system("pause");
system("cls");
welcome();
showMenu();
}
注意,View层是在:
1. 在屏幕上显示内容,提示用户。
2. 接收用户的输入,把用户输入的数据封装。
3. 调用CStuMg(业务层)的函数,以实现某种功能。
e.g. 用户要查找,则输入一个待找学生的id,View层的CMainView接收到这个id以后,
调用业务层CStuMg的findStuById的函数,查找一个学生。
如果找到了,就把这个学生的信息打印在屏幕上。如果没有找到,就提示没有找到这个人。
为了区分是否找到某个指定的学生,我在CStu类的构造函数中,先把学生的id设置为-1,认为是无效学生。
如果查询回来的学生的id是-1,其实意味着没有找到这个学生。
Service层的CStuMg
在View层里,程序已经在调用Service层CStuMg这个类里的成员函数了。实际上,这些成员函数还没有写出来。现在继续填空。
#include <iostream>
#include <fstream>
#include <string>
#include <map>
#include <sstream>
#include "CStudent.h"
#include "CStudentMg.h"
using namespace std;
CStudentMg::CStudentMg()
{
}
CStudentMg::~CStudentMg()
{
}
// 增
CStudent CStudentMg:: addAStu(map<int,CStudent> & m1,CStudent & stu) {
// 先假设id可以重复了
m1.insert(make_pair(stu.getId(),stu));
return stu;
}
// 删
bool CStudentMg:: deleteStuById(map<int, CStudent> & m1, const int & id) {
bool b = false;
map<int,CStudent> ::iterator iter;
iter = m1.find(id);
if (iter!=m1.end())
{
m1.erase(iter);
b = true; // 删除成功
}
return b;
}
// 改
CStudent CStudentMg:: updateStu(map<int,CStudent> & m1,const CStudent & cStu) {
// 迭代器是一个smart point!
// 是可以通过迭代器去访问到 m1里的东西,并且做出修改的!
// 除非迭代器是const迭代器
CStudent stu;
int id = cStu.getId();
map<int,CStudent> :: iterator iter;
iter = m1.find(id);
if (iter!=m1.end())
{
// 修改
iter->second = cStu;
stu = cStu; // 把修改后的对象,赋值,再返回上层
}
return stu;
}
// 查 by id
CStudent CStudentMg:: findById(const map <int, CStudent> & m1, const int & id) const{
CStudent stu ;
map<int,CStudent> ::const_iterator iter;
iter = m1.find(id);
if (iter!=m1.end())
{
stu = iter->second;
}
return stu;
}
// showAll
void CStudentMg:: showAll(const map<int,CStudent> & m1) const{
for (auto p : m1)
{
cout<<p.second<<endl;
}
}
// save to file
bool CStudentMg::saveToFile(const map <int,CStudent> & m1,const string & pathName) const{
bool b = true;
//fstream ofs(pathName,ios::out+ios::binary); // 为什么不是以binary保存?
fstream ofs(pathName,ios::out);
if (ofs) {
stringstream ss;
cout<<"文件打开"<<endl;
CStudent stu;
for (auto p = m1.begin();p!=m1.end();p++)
{
stu = p->second;
ss<<stu<<endl;
}
ofs<<ss.str(); // 注意,输出一定是 ss.str();
ofs.close();
}
else
{
cout<<"文件打开失败"<<endl;
b = false;
}
return b;
}
// read from file
bool CStudentMg:: readFromFile(std::map<int,CStudent> & m1, std::string path) {
bool b = true;
m1.clear(); // 清掉原来的
fstream ifs(path,ios::in);
if (ifs) {
cout<<"文件打开"<<endl;
string s;
stringstream ss;
while (getline(ifs,s)) // 怎么一行行地读取?
{
CStudent cStu;
ss<<s;
// cout<<ss.str();
ss>>cStu;
ss.clear();
m1.insert(make_pair(cStu.getId(),cStu));
}
ifs.close();
}
else {
cout<<"文件打开失败"<<endl;
b = false;
}
return b;
}
效果展示
效果展示,也没什么好展示的。基本上都一个样。上两张图吧。依旧是粉色的桌面。
总结
本文对一个较为完整的C++项目(“学生信息管理系统”这个小项目)进行了剖析,比较详细地介绍了我当时写代码时,大脑的运转过程。
尽管项目比较小,本文还是用了分层设计的思想(比较传统的、经典的MVC框架模式)。
百度对于MVC,有以下说法。我摘一些过来。
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。
MVC开始是存在于桌面程序中的,M是指业务模型,V是指用户界面,C则是控制器,使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。比如一批统计数据可以分别用柱状图、饼图来表示。C存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新。
框架和设计模式的区别
有很多程序员往往把框架模式和设计模式混淆,认为MVC是一种设计模式。实际上它们完全是不同的概念。
框架、设计模式这两个概念总容易被混淆,其实它们之间还是有区别的。框架通常是代码重用,而设计模式是设计重用,架构则介于两者之间,部分代码重用,部分设计重用,有时分析也可重用。在软件生产中有三种级别的重用:内部重用,即在同一应用中能公共使用的抽象块;代码重用,即将通用模块组合成库或工具集,以便在多个应用和领域都能使用;应用框架的重用,即为专用领域提供通用的或现成的基础结构,以获得最高级别的重用性。
框架与设计模式虽然相似,但却有着根本的不同。设计模式是对在某种环境中反复出现的问题以及解决该问题的方案的描述,它比框架更抽象;框架可以用代码表示,也能直接执行或复用,而对模式而言只有实例才能用代码表示;设计模式是比框架更小的元素,一个框架中往往含有一个或多个设计模式,框架总是针对某一特定应用领域,但同一模式却可适用于各种应用。可以说,框架是软件,而设计模式是软件的知识。
框架模式有哪些?
MVC、MTV、MVP、CBD、ORM等等;
框架有哪些?
C++语言的QT、MFC、gtk,Java语言的SSH 、SSI,php语言的smarty(MVC模式),python语言的django(MTV模式)等等
设计模式有哪些?
工厂模式、适配器模式、策略模式等等
简而言之:框架是大智慧,用来对软件设计进行分工;设计模式是小技巧,对具体问题提出解决方案,以提高代码复用率,降低耦合度。
后记
随着我代码能力的逐步提升,我希望以后再写这个老掉牙的经典学生信息管理系统,可以写得更漂亮。
代码下载
完整代码下载链接(在VS2012上编译通过):https://download.csdn.net/download/qcyfred/9737745
请至少保证计算机有D盘目录。
扫码关注本人微信公众号,有惊喜奥!公众号每天定时发送精致文章!回复关键词可获得海量各类编程开发学习资料!
例如:想获得Python入门至精通学习资料,请回复关键词Python即可。