前言
时间已经过去一周了,这次的博客比之前说好的一周一发晚了一天,因为我记错时间了哈哈哈哈,我以为上周是周三发的博客来着。这篇博客内容是C++高级编程部分,主要有文件和流、异常处理、多线程、模板和STL。在我本周学完这些内容后,我做了一个涵盖这些知识的一个小程序,小程序涵盖了这些知识中比较经典的程序。因此这篇博客我主要记录开发这个小程序的历程、遇到的困难以及我的解决方法。
开始前准备
项目设计文档
在程序的设计之前,必须要完成相应的设计文档。设计文档就相当于一张蓝图,它决定了程序的大致方向。设计文档主要包括项目的设计需求、功能分析、模块图、流程图、各种API、函数的参数和返回值、数据库设计等等。我做的项目是一个涵盖C++高级编程知识的小程序,下面的图片就是我做该项目之前的设计文档截图:
我做这个程序是按模块来设计,每个模块对应部分的知识内容,分的模块和对应内容如下:
文件和流
- 流成员函数举例
- 文本文件读写
- 二进制文件读写
异常处理
- 多个catch块异常处理
- 求三角形面积的异常处理
- 定义新的异常
模板
- 在类模板外定义成员函数
- 函数模板和非函数模板的重载
多线程
- 创建线程
- 互斥量
- 生产者消费者模型
- 互斥锁处理多窗口卖票
STL
- vector的插入和删除
- 使用栈进行进制转换
- 使用deque产生随机数
- set关联式容器的使用
流程图
根据模块和设计文档来绘制相应的流程图:
思维导图
思维导图主要包括各个模块函数的详细解释说明以及各个模块的函数名。思维导图如下:
详细设计
导航栏
为了方便程序的运行和管理,我用switch–case和while()循环做了一个小菜单导航栏,用户可输入对应的编号运行相对应的功能,十分方便。在程序中我设置了二级菜单栏,在二级菜单栏返回上一级菜单时遇到了困难,尝试了很多方法,后来发现使用 goto loop来返回上一级菜单栏,也算是取了巧了。二级菜单代码奉上,简单易懂,非常傻白甜,我看了都摇摇头哈哈哈哈:
#include <stdio.h>
#include <iostream>
#include <stdlib.h>
#include <conio.h>
using namespace std;
int main()
{
loop:
while (1)
{
cout << "1.文件和流" << endl;
cout << "2.异常处理" << endl;
cout << "3.模板" << endl;
cout << "4.多线程" << endl;
cout << "5.STL " << endl;
cout << "0.退出" << endl;
int a;
cin >> a;
switch (a)
{
case 1:
system("cls");
while (1)
{
cout << "1.流成员函数举例" << endl;
cout << "2.文本文件读写" << endl;
cout << "3.二进制文件读写" << endl;
cout << "0.返回上一级" << endl;
int b1;
cin >> b1;
switch (b1)
{
case 1:
system("cls");
cout << "流成员函数举例" << endl;
break;
case 2:
system("cls");
cout << "文本文件读写" << endl;
break;
case 3:
system("cls");
cout << "二进制文件读写" << endl;
break;
case 0:
goto loop;
}
}
break;
case 2:
system("cls");
while (1)
{
cout << "1.多个catch块的异常处理" << endl;
cout << "2.求三角形面积的异常处理" << endl;
cout << "3.定义新的异常" << endl;
cout << "0.返回上一级" << endl;
int b2;
cin >> b2;
switch (b2)
{
case 1:
system("cls");
cout << "多个catch块的异常处理" << endl;
break;
case 2:
system("cls");
cout << "求三角形面积的异常处理" << endl;
break;
case 3:
system("cls");
cout << "定义新的异常" << endl;
break;
case 0:
goto loop;
}
}
break;
case 3:
system("cls");
while (1)
{
cout << "1.在类模板外定义成员函数" << endl;
cout << "2.函数模板和非函数模板的重载" << endl;
cout << "0.返回上一级" << endl;
int b3;
cin >> b3;
switch (b3)
{
case 1:
system("cls");
cout << "在类模板外定义成员函数" << endl;
break;
case 2:
system("cls");
cout << ".函数模板和非函数模板的重载" << endl;
break;
case 0:
goto loop;
}
}
break;
case 4:
system("cls");
while (1)
{
cout << "1.创建线程" << endl;
cout << "2.互斥量" << endl;
cout << "3.生产者消费者模型" << endl;
cout << "0.返回上一级" << endl;
int b4;
cin >> b4;
switch (b4)
{
case 1:
system("cls");
cout << ".创建线程" << endl;
break;
case 2:
system("cls");
cout << "互斥量" << endl;
break;
case 3:
system("cls");
cout << ".生产者消费者模型" << endl;
break;
case 0:
goto loop;
}
}
break;
case 5:
system("cls");
while (1)
{
cout << "1.vector的插入和删除" << endl;
cout << "2.使用栈进行进制转换" << endl;
cout << "3.使用deque产生随机数" << endl;
cout << "4.set关联式容器的使用" << endl;
cout << "0.返回上一级" << endl;
int b5;
cin >> b5;
switch (b5)
{
case 1:
system("cls");
cout << "vector的插入和删除" << endl;
break;
case 2:
system("cls");
cout << "使用栈进行进制转换" << endl;
break;
case 3:
system("cls");
cout << "使用deque产生随机数" << endl;
break;
case 4:
system("cls");
cout << "set关联式容器的使用" << endl;
break;
case 0:
goto loop;
}
}
break;
case 0:
exit(1);
}
}
return 0;
}
这是运行后的截图:
二级界面:
goto loop一般大家都不推荐用,goto loop 太过于强大,会破坏程序的模块化。因为现在的程序都是模块化编程,所以以后还是要慎用goto loop。当然在我这个程序里还好,我程序代码量小,使用goto loop无伤大雅。
文件和流
在写完文件和流部分模块内容后,师父感觉我写的太过于冗杂,建议我可以把对文件的各种操作封装成一个类,如文本文件的读写,二进制文件的读写,文件的删除和重命名。在经过今天一上午的奋战后,我终于把文件的相关操作封装成了一个“类”,文件的读写、删除和重命名都已经实现。用户可以自定义相关的操作,逻辑还算完整。
#include <iostream>
#include <fstream>
#include <Windows.h>
#include <io.h>
using namespace std;
class CFile
{
public:
CFile(); // 类的构造函数
~CFile(); // 类的析构函数
void tFout();//文本文件写入
void tFin();//文本文件读出
void bFout();//二进制文件写入
void bFin();//二进制文件读出
void Remove();//删除文件
void Rename();//重命名文件
};
CFile::CFile() // 类的构造函数
{
}
CFile::~CFile() {} // 类的析构函数
void CFile::tFout()
{
string s,str1;
cout << "以文本文件写入" << endl;
cout << "请输入要写入的文件名:";
cin >> s;
ofstream fout(s, ios::out);//定义输出文件流对象fout,打开输出文件f2.dat
if (!fout)
{
cout << "Cannot open output file.\n";
exit(1);
}
cout << "要写入的信息:";
cin >> str1;
fout << str1 << endl;
fout.close();
}
void CFile::tFin()
{
string s1, str2;
cout << "以文本文件读出" << endl;
cout << "请输入要读出的文件名:";
cin >> s1;
ifstream fin(s1, ios::in);
if (!fin)
{
cout << "Cannot open input file.\n";
exit(1);
}
char str[50];
while (fin)
{
fin.getline(str, 50);
cout << str << endl;
}
fin.close();
}
void CFile::bFout()
{
string s, str1;
cout << "以二进制文件写入" << endl;
cout << "请输入要写入的文件名:";
cin >> s;
ofstream outf(s, ios::binary);
if (!outf)
{
cout << "Cannot open output file\n,";
exit(1);
}
cout << "要写入的信息:";
cin >> str1;
for (int i = 0; i <str1.length(); i++)
{
outf.put(str1[i]);
}
outf.close();
}
void CFile::bFin()
{
string s1;
cout << "以二进制文件读出" << endl;
cout << "请输入要读出的文件名:";
cin >> s1;
ifstream inf(s1, ios::binary);
if (!inf)
{
cout << "Cannot open input file\n,";
exit(1);
}
char ch;
while (inf.get(ch))
cout << ch;
cout << endl;
inf.close();
}
void CFile::Remove()
{
char FileName[256] = { 0 };
cout << "请输入要删除的文件:";
cin>> FileName;
if (0 == remove(FileName))
{
cout << "删除成功" << endl;
}
/*cout << "请输入删除的文件名:";
string path;
cin >> path;
remove(path);*/
}
void CFile::Rename()
{
char source[256];//文件路径
char newname[256];
cout << "请输入要重命名的文件路径:" << endl;
cin >> source;
cout << "请输入文件的新名称:" << endl;
cin >> newname;
if (!_access(source, 0))//如果文件存在:
{
if (!rename(source, newname))//改名成功
{
cout << source << " 成功重命名为: " << newname << endl;
}
else//无法重命名:文件打开或无权限执行重命名
{
cout << "文件无法重命名(可能原因如下):" << endl;
cout << "\t" << "1. " << newname << " 已存在" << endl
<< "\t" << "2. " << newname << " 正在使用,未关闭." << endl
<< "\t" << "3. " << "你没有权限重命名此文件." << endl;
}
}
else//文件不存在
{
cout << source << " 不存在,无法重命名." << endl;
}
cin.get();
}
int main()
{
CFile file;
while (1)
{
cout << "1.写入文本文件" << endl;
cout << "2.读出文本文件" << endl;
cout << "3.写入二进制文件" << endl;
cout << "4.读出文本文件" << endl;
cout << "5.删除文件" << endl;
cout << "6.重命名文件" << endl;
cout << "0.退出" << endl;
int a;
cin >> a;
switch (a)
{
case 1:
system("cls");
file.tFout();
break;
case 2:
system("cls");
file.tFin();
break;
case 3:
system("cls");
file.bFout();
break;
case 4:
system("cls");
file.bFin();
break;
case 5:
system("cls");
file.Remove();
break;
case 6:
system("cls");
file.Rename();
break;
case 0:
exit(1);
}
}
return 0;
}
在设计这部分程序的时候,文件的读写都没问题,但是我在文件的删除和重命名这两个功能上卡了壳。我学这部分内容时重点放在文件读写,对文件的删除和重命名一头雾水,脑子里根本没有相关的函数,后来上网才了解到remove()函数和rename()函数。两个函数的函数原型如下:
- int remove(char *filename); 【参数】filename为要删除的文件名,可以为一目录。如果参数filename 为文件,则调用unlink()处理;若参数filename
为一目录,则调用rmdir()来处理。 【返回值】成功则返回0,失败则返回-1。- int rename(char * oldname, char * newname); 【参数】oldname为旧文件名,newname为新文件名。 【返回值】修改文件名成功则返回0,否则返回-1。
在知道相关函数后设计相关内容就很简单了。
异常处理
异常处理是对所能预料的运行错误进行处理的一套实现机制。其中抛出异常是使用throw语句,异常的检查和捕获使用try语句和catch语句。在异常处理这部分,最经典的就是求三角形面积的异常处理。当用户输入三角形的三条边后,程序应首先对输入的数据进行是否输入负数检查,然后再进行三角形两边之和是否大于第三边检查,当输入的数据有异常时要输出相对应的异常。注意检查的顺序不能乱,否则会出现逻辑性错误。我就是在这犯了错误,结果输入负数时老是抛出两边之和小于第三边异常。相关的代码如下:
#include <iostream>
using namespace std;
//此程序是为了处理求三角形面积时出现的异常。
//异常包括边不能为负和两边之和大于第三边。处理异常时应先处理边不能为负然后再处理两边之和小于第三边的异常。
double triarea(int a, int b, int c)
{
double s = (a + b + c) / 2;
if (a + b <= c || a + c <= b || b + c <= a)
{
throw 1;
}
return sqrt(s * (s - a) * (s - b));
}
int main()
{
int x;
int y;
int z;
double area;
try
{
cin >> x >> y >> z;
if (x <= 0 || y <= 0 || z <= 0)
{
throw 1.0;
}
if (x > 0 && y > 0 && z > 0)
{
area = triarea(x, y, z);
}
cout << "三角形的面积为: " << area << endl;
}
catch(int)
{
cout << "两边之和小于第三边,不能构成三角形" << endl;
}
catch (double)
{
cout << "边不能为负" << endl;
}
return 0;
}
模板
模板时实现代码重用机制的一种重要工具,模板主要分为函数模板和类模板。学习该部分内容只要记住函数模板、类模板的声明,以及在类模板外定义成员函数的一般形式就可以了。关于函数模板的使用,可以通过函数模板和非函数模板的重载来加深理解:
/*
开发者:Wang
功能:函数模板和非函数模板的重载
版本:V1.0.0
时间:10:12 2022/7/15
*/
#include <iostream>
using namespace std;
template <typename T>
T Min(T a, T b)
{
cout << "调用模板函数: ";
return (a < b) ? a : b;
}
int Min(int a, int b)
{
cout << "调用非模板函数:";
return (a < b) ? a : b;
}
int main()
{
int x = 1, y = 2;
cout << "最小的数为:" << Min(x, y) << endl;
double x1 = 1.0, y1 = 2.0;
cout << "最小的数为:" << Min(x1, y1) << endl;
return 0;
}
多线程
多线程算是C++高级编程中比较难学习和理解的知识。对于多线程的学习,我现在还在摸索阶段。因为多线程涉及的比较广和其他知识结合的比较紧密,如线程安全的queue、linux下的多线程。以后还会接触。在这里我就浅谈下多线程。
- 首先是在Windows下创建线程:
HANDLE WINAPI CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程安全相关的属性,常置为NULL
SIZE_T dwStackSize, //新线程的初始化栈在大小,可设置为0
LPTHREAD_START_ROUTINE lpStartAddress, //被线程执行的回调函数,也称为线程函数
LPVOID lpParameter, //传入线程函数的参数,不需传递参数时为NULL
DWORD dwCreationFlags, //控制线程创建的标志
LPDWORD lpThreadId //传出参数,用于获得线程ID,如果为NULL则不返回线程ID );
说明:
- lpThreadAttributes:指向SECURITY_ATTRIBUTES结构的指针,决定返回的句柄是否可被子进程继承,如果为NULL则表示返回的句柄不能被子进程继承。
- dwStackSize :线程栈的初始化大小,字节单位。
- lpStartAddress:指向一个函数指针,该函数将被线程调用执行。因此该函数也被称为线程函数(ThreadProc),是线程执行的起始地址,线程函数是一个回调函数,由操作系统在线程中调用。线程函数的原型如下:
- DWORD WINAPI ThreadProc(LPVOID lpParameter);
//lpParameter是传入的参数,是一个空指针
- lpParameter:传入线程函数(ThreadProc)的参数,不需传递参数时为NULL
- dwCreationFlags:控制线程创建的标志,有三个类型,0:线程创建后立即执行线程;CREATE_SUSPENDED:线程创建后进入就绪状态,直到线程被唤醒时才调用;
- 返回值:如果线程创建成功,则返回这个新线程的句柄,否则返回NULL。如果线程创建失败,可通过GetLastError函数获得错误信息。
- 互斥锁
来保证共享数据操作的完整性
HANDLE WINAPI CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, //线程安全相关的属性,常置为NULL
BOOL bInitialOwner, //创建Mutex时的当前线程是否拥有Mutex的所有权
LPCTSTR lpName //Mutex的名称 );
3.生产者消费者模型
利用互斥锁来实现生产者消费者模型,具体代码如下:
/*
生产者消费者问题
*/
#include <iostream>
#include <deque>
#include <thread>
#include <condition_variable>
#include <Windows.h>
using namespace std;
deque<int> q;
mutex mu;
condition_variable cond;
int c = 0;//缓冲区的产品个数
void producer()
{
int data1;
while (1)//通过外层循环,保证生成永不停止
{
if (c < 3)//限流
{
data1 = rand();
unique_lock<mutex>locker(mu);//锁
q.push_front(data1);
cout << "存了" << data1 << endl;
cond.notify_one();//通知取
++c;
}
Sleep(500);
}
}
void consumer()
{
int data2;//data用来覆盖存放取的数据
while (1) {
{
unique_lock<mutex> locker(mu);
while (q.empty())
cond.wait(locker);
data2 = q.back();
q.pop_back();
cout << "取了" << data2 << endl;
--c;
}
Sleep(1500);
}
}
int main()
{
thread t1(producer);
thread t2(consumer);
t1.join();
t2.join();
return 0;
}
STL
标准模板库中提供了很多使用的组件,组件分为算法、容器、函数和迭代器。常见的容器有vector、list、deque、set、map等。
vector容器提供了insert()插入函数、erase()删除函数、push_back()添加一个函数到vector尾,pop_back()删除当前vector最后一个元素。
下面是分别用insert()、erase()函数和push_back()函数对vector容器进行读取:
/*
开发者:Wang
功能:vector容器
版本:V1.0.0
时间:15:07 2022/7/17
*/
#include <iostream>
#include <vector>
using namespace std;
int main()
{
unsigned i;
vector <int> number;
number.insert(number.begin(), 90);
number.insert(number.begin(), 80);
number.insert(number.end(), 20);
number.insert(number.end(), 30);
for (i = 0; i < number.size(); i++)
{
cout << number[i] << endl;
}
number.erase(number.begin());
number.erase(number.end()-1);//不能为number.erase(number.end());会超出范围
for (i = 0; i < number.size(); i++)
{
cout << number[i] << endl;
}
return 0;
}
/*
开发者:Wang
功能:vector容器中iterator用法
版本:V1.0.0
时间:15:25 2022/7/17
*/
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
int main()
{
vector <int> intVector;
for (int i = 0; i < 10; i++)
{
intVector.push_back(i + 10);
}
vector <int>::iterator theIterator = intVector.begin();
intVector.insert(theIterator, 4, 5);
for (theIterator = intVector.begin(); theIterator != intVector.end(); ++theIterator)
cout << *theIterator ;
return 0;
}
set容器可以用来存储相同数据类型的数据,set中每个元素唯一,并且自动排序。
/*
开发者:Wang
功能:set关联式容器的使用
版本:V1.0.0
时间:11:35 2022/7/19
*/
#include <iostream>
#include <set>
#include <string>
using namespace std;
void setTest()
{
set<string> s;
s.insert("linxiaocha");
s.insert("chenweixing");
s.insert("gaoying");
s.insert("chenweixing");
set<string>::iterator myit;
for (myit = s.begin(); myit != s.end(); ++myit)
{
cout << *myit << endl;
}
cout << endl;
}
程序运行结果是:
chenweixing
gaoying
linxiaocha
总结
因为涉及的知识面比较广,所以设计该小程序还是有点难度的。但是程序完成的那一瞬间还是蛮有成就感的。相关的设计文档、代码在文末会有链接。谢谢你的阅读,下周再见!
链接:https://pan.baidu.com/s/1A8JEsc1_5zyd8KX-95ACeQ?pwd=or01
提取码:or01