当前位置:   article > 正文

C++中常见的异常处理问题_c++中 哪些异常是程序自己无法处理的

c++中 哪些异常是程序自己无法处理的

目录

前言

一、输入纯数字的字符串

二、检验输入的字符串是否为中文

三、函数(T* arr, int size)防止溢出

总结


前言

        在实现学生(教师)信息管理系统、通讯录、医疗系统等项目的实现过程中,往往离不开信息的写入操作,无论是控制台简单读入还是后续写入文件都离不开信息的精确输入,比如年龄int型当然不能接收传入的字符串等。亦或是读取数组中数组元素可能存在的越界风险,下标超过数组长度或输入有误造成读取arr[负数]的情况都可能对程序的正常运行造成的影响是不可忽视的,所以我将详细给出应对可能出现的异常提供范例,希望帮助初学者完善课程设计或是轻量化的项目,提高代码的健壮性和可行性。


一、输入纯数字的字符串

下面以要求输入字符串代表学生编号,字符串长度为 10 示例:

  1. // 要求输入字符串代表学生编号,长度为 10 ,且为纯数字
  2. void test1()
  3. {
  4. string s;
  5. constexpr int size = 10;
  6. while (true)
  7. {
  8. bool ret = true; // 判断输入是否理想标识符
  9. try
  10. {
  11. cin >> s;
  12. if (size != s.size()) { throw string("cin is error: 数字位数不匹配"); }
  13. for (const auto& elem : s)
  14. {
  15. if (elem > 57 || elem < 48) // 判断每个字符位是否为0~9即是否为数字 0的ASCLL值为48
  16. {
  17. ret = false;
  18. }
  19. }
  20. if (ret) { break; }
  21. else { throw string("cin is error: 非纯数字"); } // 符合则退出循环
  22. }
  23. catch (const string& expection)
  24. {
  25. cout << expection << endl;
  26. }
  27. catch (...)
  28. {
  29. cout << "other default!" << endl;
  30. }
  31. }
  32. }

上面代码中,我们通过字符串s接收输入的一整串数字,while(true)循环保证了如果输入不符合要求会在进行异常处理后继续从键盘接收值赋给字符串s。可以看到try{}语句块中内置了两个throw语句分别用于抛出输入位数异常检验输入非纯数字异常,这两种异常在这里都可以通过 catch (const string& expection) 语句捕获,当然为了避免出现其他未预料到的问题建议在最后追加一个catch(...) 语句,以便提高代码的健壮性。

试着运行并输入测试值:

同时,和两个throw语句先后顺序一样,当输入值非纯数字且长度不足10时,会先抛出长度匹配的异常:

 二、检验输入的字符串是否为中文

和上面检验是否为纯数字类似,有以下实现方法:

首先我们了解到每个中文字符占用2个字节,但是string类型字符串进行遍历时是按逐个字节读取的,这就造成中文输入的字符串在程序内部逐字读取时效果并不理想,所以引出wstring宽字符串类型恰好采取宽字符存储,即每个字符占用两个字节,所以我们写出如下遍历字符串判断是否每个宽字符都是中文的函数 (这里采用Unicode范围判断):

如果对宽字符串理解不是很清楚,可以参考本人关于字符串的博客:http://t.csdn.cn/q8ceT

  1. bool isChineseCharacter(const std::wstring& wstr)
  2. {
  3. bool ret = true;
  4. for (wchar_t c : wstr)
  5. {
  6. if (c < 0x4E00 || c > 0x9FFF) // 中文汉字的Unicode范围是0x4E00至0x9FFF
  7. {
  8. ret = false;
  9. break;
  10. }
  11. }
  12. return ret;
  13. }

需要特别注意的是,由于使用wstring宽字符串类型作为遍历选择,所以必要时需要定义为宽字符串(wstring)对应的宽输入输出流(wcin wcout),当需要遍历或访问宽字符串中的单个字符时,需要将单个字符类型定义为宽字符(wchar_t)

判断的逻辑实现完毕,接下来接收输入的字符串并将其传入上面判断函数呈现理想结果却不是一件容易的事情:

  1. wcout << L"请输入一个纯中文字符串: "; // L修饰字符串表示宽字符串
  2. wstring line;
  3. getline(wcin, line);
  4. wstr = s2ws(string(line.begin(), line.end())); // 这里s2ws()函数为自定义的string类型转wstring类型函数 (后续会给出)
  5. if (wstr.empty()) // 字符串判空操作
  6. {
  7. throw std::string("cin is error: 空字符串");
  8. }
  9. ret = isChineseCharacter(line); // 判断是否为纯中文字符串函数
  10. if (!ret)
  11. {
  12. throw std::string("cin is error: 非纯中文");
  13. }
  14. break;

以上代码即为需要实际运行的部分,由于该文章以及该模块主题为异常处理,所以可以直接置入try{}语句块,同时提供对应的catch{}语句捕获各类异常,实现对中文字符串的清晰判断,避免了输入值不符合要求导致的项目崩溃或未定义错误

那么还有真正的难点在于wstring和string类型之间的相互转换,在这里只需要实现string转wstring即可,接下来深入探究 s2ws() 函数究竟怎么编程实现:(着急方法直接看方案四)

下面是现在网上绝大多数的实现方法,我对其进行适配本模块要求后给出代码:

方案一:

  1. std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter(new std::codecvt_utf8<wchar_t, 0x10FFFF, std::codecvt_mode::skip_header>());
  2. std::wstring wideStr = converter.from_bytes(str);

在上述代码中,我们在构造wstring_convert 对象时,传入了codecvt_mode::skip_header 参数,以跳过读取 BOM 的步骤。这样,就可以避免在处理纯中文字符串时抛出 range_error 异常。

然而,codecvt_mode::skip_header编译报错不存在skip_header,原因是在 C++17 中,std::wstring_convert 已被弃用,并且 std::codecvt 类也已被弃用。

所以方案一不可行。

方案二:

  1. std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
  2. std::wstring wideStr = converter.from_bytes(str);

在上述代码中,我们使用 std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> 来进行字符串转换。这个类使用 std::codecvt_utf8_utf16 类型的转换器,将 UTF-8 编码的字符串转换为 UTF-16 编码的宽字符串。

然而,在 C++17 中,不能使用 wstring_convert<std::codecvt_utf8_utf16<wchar_t>> 来直接进行字符串转换,因为它的行为是不可确定的,同时wstring_convert已在官网上被淘汰,不建议使用,重要的是wstring_convert<std::codecvt_utf8_utf16<wchar_t>> 引起的异常:bad conversion在每次正常输入纯中文字符串时同样会出现,无法实现到 isChineseCharacter() 就已经因为报出异常程序中止了。

所以方案二也不可行。

 方案三:

  1. std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
  2. const std::locale oldLocale(std::locale::classic(), new std::codecvt_utf8_utf16<wchar_t>());
  3. std::wstring wstr;
  4. std::wcout << L"请输入一个纯中文字符串: ";
  5. std::wstring line;
  6. std::getline(std::wcin, line);
  7. wstr = converter.from_bytes(string(line.begin(), line.end()));

实际调试发现问题同方案二:传入中文字符串仍然报异常 bad conversion 

方案四(可行方案):

好消息:终于来到可行方案,经过几天的卡壳和求助拍案了真正可行的字符串类型转换方法

坏消息:C++标准库没有现成函数,需要自己手写

这也就引出了上面代码中提到的 s2ws() 函数,代码如下:

  1. std::wstring s2ws(const std::string& s) // string to wstring 转换
  2. {
  3. setlocale(LC_ALL, "chs");
  4. const char* _Source = s.c_str();
  5. size_t _Dsize = s.size() + 1;
  6. wchar_t* _Dest = new wchar_t[_Dsize];
  7. wmemset(_Dest, 0, _Dsize);
  8. mbstowcs(_Dest, _Source, _Dsize);
  9. std::wstring result = _Dest;
  10. delete[]_Dest;
  11. setlocale(LC_ALL, "C");
  12. return result;
  13. }

如果需要宽字符串(wstring)转换字符串(string)方法,可以参考本人关于字符串的博客:http://t.csdn.cn/q8ceT

现在已经实现判断和转换部分都已经实现完毕,接下来只剩整合进异常处理try-catch语句块,接下来直接给出整体代码:

  1. // 检验输入的字符串是否为纯中文
  2. bool isChineseCharacter(const std::wstring& wstr)
  3. {
  4. bool ret = true;
  5. for (wchar_t c : wstr)
  6. {
  7. if (c < 0x4E00 || c > 0x9FFF)
  8. {
  9. ret = false;
  10. break;
  11. }
  12. }
  13. return ret;
  14. }
  15. std::wstring s2ws(const std::string& s) // string to wstring 转换
  16. {
  17. setlocale(LC_ALL, "chs");
  18. const char* _Source = s.c_str();
  19. size_t _Dsize = s.size() + 1;
  20. wchar_t* _Dest = new wchar_t[_Dsize];
  21. wmemset(_Dest, 0, _Dsize);
  22. mbstowcs(_Dest, _Source, _Dsize);
  23. std::wstring result = _Dest;
  24. delete[]_Dest;
  25. setlocale(LC_ALL, "C");
  26. return result;
  27. }
  28. void test2()
  29. {
  30. std::wstring wstr;
  31. while (true)
  32. {
  33. bool ret = true;
  34. try
  35. {
  36. wcout << L"请输入一个纯中文字符串: "; // L修饰字符串表示宽字符串
  37. wstring line;
  38. getline(wcin, line);
  39. wstr = s2ws(string(line.begin(), line.end())); // 自定义的string类型转wstring类型函数
  40. if (wstr.empty()) // 字符串判空操作
  41. {
  42. throw std::string("cin is error: 空字符串");
  43. }
  44. ret = isChineseCharacter(line); // 判断是否为纯中文字符串函数
  45. if (!ret)
  46. {
  47. throw std::string("cin is error: 非纯中文");
  48. }
  49. break;
  50. }
  51. catch (const std::string& exception)
  52. {
  53. std::cout << exception << std::endl;
  54. }
  55. catch (const std::range_error& ex)
  56. {
  57. std::cout << "Range error: " << ex.what() << std::endl;
  58. }
  59. catch (const std::ios_base::failure& ex)
  60. {
  61. std::cout << "cin is error: 输入无效" << std::endl;
  62. std::wcin.clear(); // 清除错误状态
  63. std::wcin.ignore(10000000,L'\n'); // 忽略剩余的输入
  64. }
  65. catch (...)
  66. {
  67. std::cout << "other default!" << std::endl;
  68. }
  69. }
  70. }

这里 catch (const std::ios_base::failure& ex) 语句块用来在发生输入操作失败的情况下,输出相应的错误信息,并进行一些恢复操作可能包括清除输入流的错误状态(wcin.clear(),以及忽略剩余的输入(wcin.ignore()。目的是为了确保程序能够继续正常执行,而不会受到输入失败的影响而导致程序崩溃或进入错误状态。

三、函数(T* arr, int size)防止溢出

我们知道当数组作为参数传入函数时,会退化为指向数组首元素的指针,无法获取数组的具体类型信息。所以当函数内部需要对作为传入参数的数组进行操作,为了操作的合理性和安全性,往往需要在传入数组同时传入数组的大小,从而函数内部即可注意避免访问越界等问题。可能你会想函数外部不也得通过数组计算数组大小吗,把计算的过程放在函数里还能减少函数参数的冗杂这样不可以吗?

回答是不可以。因为数组包括静态数组和动态数组,若是静态数组那么完全可以将size值的计算放入函数体中,对动态数组而言,_msize() 函数用于求堆区创建的动态数组大小,因为两者的数组大小size计算方式是不同的:

  • 静态数组:size = sizeof(arr)/sizeof(arr[0])
  • 动态数组:size = _msize(arr)/sizeof(arr[0])

又因为数组作参数传入函数时会退化为指向首元素的指针,即函数内部无从得知数组是静态数组亦或是动态数组,从而对于上面求size值的方式也就无法确定。

好,那把数组size值的计算就留在函数外,但是有没有可能在函数外size值计算错误,导致错误的值传给函数引发越界访问等问题,按理来说size的计算出错属实不应该,但是抱着严谨的态度,还是得想想办法避免粗心酿成程序异常。

那有没有一种办法可以监测外部size值的正确计算呢,答案是有,往下看:

  1. template<typename T>
  2. void do_arr(const vector<T>& vec, const int size)
  3. {
  4. try
  5. {
  6. if (size != vec.size()) { throw string("do_arr -> size != vec.size()"); }
  7. // 代表相关可能越界的操作示例
  8. for (int i = 0; i < size; ++i)
  9. {
  10. cout << vec.at(i * i) << endl;
  11. }
  12. }
  13. catch (const string& expection) // 接收size值不对应问题
  14. {
  15. cout << expection << endl;
  16. }
  17. catch (const out_of_range& expect) // 接收vector对象的at()读取越界时抛出的错误
  18. {
  19. cout << expect.what() << endl;
  20. }
  21. catch (...) // 接收其他错误
  22. {
  23. cout << "other default!" << endl;
  24. }
  25. }

以上函数中,我们把数组换为更加安全的vector容器,同时对容器对象调用 size() 函数和传入参数中的size值作比较,就可以有效地避免size计算出错却不自知的情况,一旦两者不相等函数立马抛出异常,阻断操作部分正常但出现越界访问等问题出现的可能

我们要求操作的对象是数组,这里要调用函数只需要利用vector的构造函数构建临时对象即可,需要注意vector的构造函数对于静态数组和动态数组存在差异性,函数调用实现代码如下:

  1. void test3()
  2. {
  3. int a[10] = { 1,2,3,4,5,6,7,5,8,3 };
  4. int* b = new int[10] {1, 2, 3, 4, 5, 6, 7, 5, 8, 3};
  5. constexpr int size_a = sizeof(a) / sizeof(a[0]);
  6. const int size_b = _msize(b) / sizeof(b[0]);
  7. shared_ptr<int> p_b(b); // 防止防止忘记delete[]
  8. // 利用vector构造函数 vec(begin(静态数组),end(静态数组))
  9. do_arr(vector<int>(begin(a), end(a)), size_a); // 将静态数组转换为vector临时对象,以便访问越界时抛出异常
  10. // 利用vector构造函数 vec(动态数组头指针,动态数组尾指针)
  11. do_arr(vector<int>(b, b + 10), size_b); // 将动态数组赋值给vector临时对象
  12. }

运行结果:

我们发现没有抛出size不相等的异常,当运行上面的越界代码:cout << vec.at(i * i) << endl; 时,vec.at() 函数抛出异常并被 catch (const out_of_range& expect) 语句块接收,异常内容为:invalid vector subscript 表明发生了越界访问,同时函数中止退出。


总结

        以上各方面的提供的范例和方法,对常见的人为可能造成的异常给出说明和解决方案,可以帮助初学者在课程设计或轻量化项目中提高代码的健壮性和可行性。通过对用户输入的字符串进行检验和对数组访问的合法性进行判断,可以防止输入错误或越界访问导致程序崩溃或进入错误状态。这样能够提升程序的稳定性和用户体验。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/714955
推荐阅读
相关标签
  

闽ICP备14008679号