C++学习-入门到精通-【5】类模板array和vector、异常捕获
类模板array和vector、异常捕获
一、array对象
Array是C++标准库中的类模板。array对象是一组具有相同类型的、连续的内存空间。每个array对象都知道自己的大小,可以调用类的成员函数size来确定。例如一个array对象c,使用c.size()
可以获得c的大小。
array对象的声明
使用如下的语法声明一个array对象:
array <类型, 大小> array对象名
;
// 声明一个元素类型为int的,大小为5的array对象
array <int, 5> arr;
尖括号对<>
表明了array是一个类模板。编译器将根据元素的类型和array对象的大小来分配合适的内存空间(一个分配内存的声明,它的更合适的叫法应该是定义)。array对象的大小必须是一个无符号整型。
使用array对象的例子
#include <array>
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
array<int, 5> arr1;
static array<int, 10> arr;
cout << setw(7) << "Element" << setw(13) << "Values" << endl;
for (int i = 0; i < 5; i++)
{
cout << setw(5) << "arr1[" << i << "]" << setw(13) << arr1[i] << endl;
}
cout << "\n";
for (int i = 0; i < 10; i++)
{
cout << setw(5) << "arr[" << i << "]" << setw(13) << arr[i] << endl;
}
}
运行结果:
从结果中看,自动的array对象并不会隐式的初始化为0,静态的array对象会隐式的初始化为0。静态的array对象与静态的局部变量一样,只会在函数第一次调用时进行初始化,之后直到程序结束才会销毁。
使用初始化列表初始化array对象
在array对象的声明中,也可以对其元素进行初始化。具体做法是:在array对象名称的后面加一个等号和一个用逗号分隔的初始化列表,该列表用花括号括起来。
例如:
array <int, 5> a = {};
如果初始化列表中的值的个数小于array对象中元素的个数,那么剩下的对象元素都被初始化为0。上面的例子中,初始化列表中值的个数比array对象中元素的个数少(实际上一个值也没有),所以将没有对应值的元素初始化为0。
注:此方法只能在声明时使用
如果要在非声明的地方初始化array对象的元素,可以使用一个循环语句。
提示:
如果在一个array对象的声明中指定了array对象大小和初始化列表,那么初始化值的个数必须小于或等于array对象的大小。
下面的array对象声明语句就会引起编译错误:
array <int, 5> = { 1,2,3,4,5,6 } // error
使用常量变量指定array对象的大小
在C语言中,使用下面的语句是不被允许的:
const int i = 5;
int arr[i] = { 1,2,3,4,5 }; // error
在C语言中声明一个数组,数组的元素个数必须是一个常量。使用const修饰的变量虽然使用上具有常量的性质(初始化之后无法被修改,但是其本质上仍是一个变量)。
在C++中我们就可以使用常量变量来指定一个array对象的大小,比如:
const int a = 5;
array <int, i> arr = { 1,2,3,4,5 };
但是绝对不能使用变量作为array对象的大小;
注意:使用常量变量时,必须在声明它的时候进行初始化
二、基于范围的for语句
这是一个C++11的新特性,该语句允许程序员不使用计数器就可以完成所有元素的编历,从而避免array缓冲区溢出的可能性,且减少了程序员的工作量(不需要他们自己去实现对边界的检查功能)。
提示:当处理array对象的所有元素时,如果没有访问array对象元素下标的需求,那么使用基于范围的for语句可以较大程度上预防错误的发生
基于范围的for语句的语法形式:
for(范围变量声明 : 表达式) 语句
其中范围变量声明中必须包含一个类型名称和一个标识符(例如:int item),表达式是需要迭代遍历的array对象。范围变量声明中的类型必须与array对象的元素类型相一致,而标识符代表循环的连续迭代中下一个array对象的值。
下面给出一个使用基于范围的for语句的例子。
#include <iostream>
#include <array>
using namespace std;
int main()
{
array <int, 5> items = { 1,2,3,4,5 };
cout << "items before modification: ";
for (int item : items)
{
cout << item << " ";
}
for (int item : items)
{
item *= 2;
}
cout << "\nitems after modification(non-reference): ";
for (int item : items)
{
cout << item << " ";
}
for (int& item : items)
{
item *= 2;
}
cout << "\nitems after modification(reference): ";
for (int item : items)
{
cout << item << " ";
}
}
运行结果:
可以看到在这种基于范围的for语句中,使用的标识符是array对象的一个副本,对它进行修改并不会对array对象本身的值产生影响。所以这里需要使用引用,来实现对array对象值的修改。
注意:要使用基于范围的for语句,循环对象必须要有begin函数(它是什么我们会在迭代器章节介绍)
三、利用array对象存放成绩的GradeBook类
GradeBook.h
#include <string>
#include <array>
class GradeBook
{
public:
static const size_t students = 10; // 测试数据,学生人数
GradeBook(const std::string &, const std::array<int, students> &);
void displayMessage() const;
void setCourseName(const std::string &);
std::string getCourseName() const;
//auto getCourseName() const -> std::string;
// 使用之前提到的尾随返回值类型的语法声明get成员函数的返回值类型
void processGrades() const;
int getMaximum() const;
int getMinimum() const;
double getAverage() const;
void outputBarChart() const;
void outputGrades() const;
private:
std::string CourseName;
std::array<int, students> grades; // 保存学生的成绩
};
GradeBook.cpp
#include "GradeBook.h"
#include <iostream>
#include <iomanip>
using namespace std;
// 构造函数
GradeBook::GradeBook(const string& name,
const array<int, students>& gradeArray)
// or const array<int, GradeBook::students>& gradeArray)
// 在这里不需要使用(类名::数据成员)这样的语法来使用这个数据成员
// 因为构造也是类的成员函数,成员函数的作用域中可以直接使用类中的公开数据成员
: CourseName(name), grades(gradeArray) // 使用初始化列表初始化两个数据成员
{
}
// 打印欢迎信息
void GradeBook::displayMessage() const
{
cout << "Welcome to the Grade Book for\n"
<< getCourseName() << "!" << endl;
}
// set成员函数
void GradeBook::setCourseName(const string& name)
{
CourseName = name;
}
// get成员函数
string GradeBook::getCourseName() const
{
return CourseName;
}
// 对成绩进行多种操作
void GradeBook::processGrades() const
{
// 打印成绩
outputGrades();
// 打印班级平均成绩
cout << setprecision(2) << fixed; // 打印浮点数后面两位,不足补0
cout << "\nClass Average is " << getAverage() << endl;
// 打印最低成绩和最高成绩
cout << "Lowest grade is " << getMinimum() << endl
<< "Highest grade is " << getMaximum() << endl;
// 打印班级成绩分布条形图
outputBarChart();
}
int GradeBook::getMaximum() const
{
int max = 0; // 成绩是一个非负数,将最大值初始化为0
// 不需要考虑数组的下标,只需要遍历数组的每个元素,使用基于范围的for语句
// 表达式是要循环的对象,也就是数据成员grades
for (int grade : grades)
{
if (grade > max)
{
max = grade;
}
}
return max;
}
int GradeBook::getMinimum() const
{
int min = 100; // 成绩最大值为100,将最小值初始化为100
// 不需要考虑数组的下标,只需要遍历数组的每个元素,使用基于范围的for语句
for (int grade : grades)
{
if (grade < min)
{
min = grade;
}
}
return min;
}
double GradeBook::getAverage() const
{
int total = 0;
for (int grade : grades)
{
total += grade;
}
return (static_cast<double>(total) / students);
// or return (static_cast<double>(total) / grades.size());
// 可以使用array类中提供的成员函数size()来获取array对象中元素的个数
// 该值与使用类模板创建一个array对象时的第二个参数相同
}
void GradeBook::outputBarChart() const
{
cout << "\nGrade distribution:" << endl;
// 将成绩分成11个区间
// 0-9
// 10-19
// 20-29
// 30-39
// ...
// 90-99
// 100
const int frequencySize = 11; // 用一个变量来保存区间数,方便代码的维护
array<int, frequencySize> frequency = {}; // 将一个有11个元素的array对象初始化为全0
// 用以保存每个区间中学生的人数
// 遍历成绩数组,获取每个区间的人数
for (int grade : grades)
{
frequency[grade / 10]++;
}
// 打印条形图
for (int i = 0; i < frequencySize; i++)
{
// 打印表头
if(i != 10)
cout << setw(2) << i * 10 << " - "
<< setw(3) << 9 + i * 10 << ": ";
else
cout << setw(10) << "100: ";
// 打印符号*,表示一个人
for (int stars = 0; stars < frequency[i]; stars++)
{
cout << "*";
}
cout << endl;
}
}
// 打印学生的成绩
void GradeBook::outputGrades() const
{
cout << "\nThe grades are: \n";
for (int student = 1; student <= grades.size(); student++)
{
cout << "student" << setw(2) << student << ":"
<< setw(3) << grades[student - 1] << endl;
}
}
test.cpp
#include "GradeBook.h"
using namespace std;
int main()
{
// 学生成绩
array<int, GradeBook::students> grades =
{ 87,68,94,100,88,92,78,67,83,98 };
// 课程名
string CourseName = "CS1201 C++ Programming";
// 创建一个GradeBook对象
GradeBook myGradeBook(CourseName, grades);
myGradeBook.displayMessage();
myGradeBook.processGrades();
}
运行结果:
其中使用了array类中的一个成员函数:
注意:类中定义的static的数据成员是所有成员共有的,有const的static数据成员可以在类定义时就初始化,没有const的数据成员必须在类外进行初始化。并不像普通的数据成员一样每一个对象都有一个副本
四、array对象的排序与查找
排序
查找
使用排序和查找的例子
在介绍示例之前我们先看一段代码:
#include <string>
#include <iostream>
using namespace std;
int main()
{
string s1 = "abcf";
string s2 = "abcd";
if (s1 < s2)
{
cout << "s1 < s2" << endl;
}
else
{
cout << "s1 >= s2" << endl;
}
}
这段代码在C语言中是不可行的,因为关系运算符是无法比较字符串的大小的,但是C++中引入了运算符重载的概念(后面会介绍),所以string对象的大小可以使用关系运算符进行比较。
#include <iostream>
#include <algorithm>
#include <array>
using namespace std;
int main()
{
array<int, 5> a = { 5,4,2,3,1 };
cout << "Before sort:" << endl;
for (int i : a)
{
cout << i << " ";
}
sort(a.begin(), a.end());
cout << "\n\nAfter sort:" << endl;
for (int i : a)
{
cout << i << " ";
}
}
运行结果:
上面传递给sort函数模板的参数类型如下:
关于begin和end两个函数,我们会在后续介绍迭代器的章节详细说明。
另一个例子:
#include <iostream>
#include <string>
#include <array>
#include <algorithm>
using namespace std;
int main()
{
const int arraySize = 7;
array <string, arraySize> colors = { "red", "yellow", "blue",
"green", "orange", "indigo", "violet"};
cout << "Unsorted array:" << endl;
for (string item : colors)
{
cout << item << " ";
}
sort(colors.begin(), colors.end());
cout << "\n\nSorted array:" << endl;
for (string item : colors)
{
cout << item << " ";
}
bool found = binary_search(colors.begin(), colors.end(), "indigo");
cout << "\n\n\"indigo\"" << (found? " was " : " was not ")
<< "found in colors." << endl;
found = binary_search(colors.begin(), colors.end(), "cyan");
cout << "\n\n\"cyan\"" << (found ? " was " : " was not ")
<< "found in colors." << endl;
}
运行结果:
注意:要利用binary_search函数确定一个值是否在array对象中,前提是该array对象中的值是升序排序好的。该函数并不会检查array对象是否已排序
五、多维array对象
多维的array对象是通过嵌套语句的形式进行声明的。例如:
#include <array>
using namespace std;
// 声明一个二维array对象
array<array<int, 4>, 3> arr = {}; // 所有值初始化为0
const int rows = 2;
const int columns = 3;
array<array<int, columns>, rows> arr2 = { 1,2,3,4,5 };
上面代码定义了一个3行4列的二维array对象,初始化为0。
arr是一个array对象(有3个元素),它的元素的类型也是一个array对象(有4个int类型的元素)。
二维array对象的使用——嵌套的基于范围的for语句
for (const auto& row : arr2)
{
for (auto const& column : row)
{
// operation
}
}
上面的for语句中,使用auto关键字作为变量的类型,它的含义是让编译器根据该变量初始化值来确定它的数据类型。
六、利用二维array对象的GradeBook类
在上面的GradeBook类的代码中,我们只有一个一维的array对象用来保存成绩,但是一个学期不可能只有一次考试,所以这显然是不够的。
假设一个学期进行4次考试:当前班级中有10个学生,所以我们需要一个10x4的array对象来保存这些数据。
GradeBook.h
#include <string>
#include <array>
class GradeBook
{
public:
static const size_t students = 10; // 测试数据,学生人数
static const size_t tests = 4;
GradeBook(const std::string&, const std::array<std::array<int, tests>, students>&);
void displayMessage() const;
void setCourseName(const std::string&);
std::string getCourseName() const;
//auto getCourseName() const -> std::string;
// 使用之前提到的尾随返回值类型的语法声明get成员函数的返回值类型
void processGrades() const;
int getMaximum() const;
int getMinimum() const;
double getAverage(const std::array<int, tests> &) const;
void outputBarChart() const;
void outputGrades() const;
private:
std::string CourseName;
std::array<std::array<int, tests>, students> grades; // 保存学生的成绩
};
GradeBook.cpp
#include "GradeBook.h"
#include <iostream>
#include <iomanip>
using namespace std;
GradeBook::GradeBook(const string& name,
const array<array<int, tests>, students>& gradeArray)
// const array<int, GradeBook::students>& gradeArray)
// 在这里不需要使用(类名::数据成员)这样的语法来使用这个数据成员
// 因为构造也是类的成员函数,成员函数的作用域中可以直接使用类中的公开数据成员
: CourseName(name), grades(gradeArray) // 使用初始化列表初始化两个数据成员
{
}
// 打印欢迎信息
void GradeBook::displayMessage() const
{
cout << "Welcome to the Grade Book for\n"
<< getCourseName() << "!" << endl;
}
// set成员函数
void GradeBook::setCourseName(const string& name)
{
CourseName = name;
}
// get成员函数
string GradeBook::getCourseName() const
{
return CourseName;
}
// 对成绩进行多种操作
void GradeBook::processGrades() const
{
// 打印成绩
outputGrades();
// 打印最低成绩和最高成绩
cout << "Lowest grade is " << getMinimum() << endl
<< "Highest grade is " << getMaximum() << endl;
// 打印班级成绩分布条形图
outputBarChart();
}
int GradeBook::getMaximum() const
{
int max = 0; // 成绩是一个非负数,将最大值初始化为0
// 不需要考虑数组的下标,只需要遍历数组的每个元素,使用基于范围的for语句
// 表达式是要循环的对象,也就是数据成员grades
for (auto const& student : grades) // 使用引用,减少复制副本的开销
{
for (auto const& grade : student)
{
if (grade > max)
{
max = grade;
}
}
}
return max;
}
int GradeBook::getMinimum() const
{
int min = 100; // 成绩最大值为100,将最小值初始化为100
// 不需要考虑数组的下标,只需要遍历数组的每个元素,使用基于范围的for语句
for (auto const& student : grades) // 使用引用,减少复制副本的开销
{
for (auto const& grade : student)
{
if (grade < min)
{
min = grade;
}
}
}
return min;
}
double GradeBook::getAverage(const array<int, tests>& setOfGrades) const
{
int total = 0;
// 计算一个学生一学期的平均成绩
for (auto const& grade : setOfGrades)
{
total += grade;
}
return (static_cast<double>(total) / tests);
// or return (static_cast<double>(total) / setOfGrades.size());
// 可以使用array类中提供的成员函数size()来获取array对象中元素的个数
// 该值与使用类模板创建一个array对象时的第二个参数相同
}
void GradeBook::outputBarChart() const
{
cout << "\nGrade distribution:" << endl;
// 将成绩分成11个区间
// 0-9
// 10-19
// 20-29
// 30-39
// ...
// 90-99
// 100
const int frequencySize = 11; // 用一个变量来保存区间数,方便代码的维护
array<int, frequencySize> frequency = {}; // 将一个有11个元素的array对象初始化为全0
// 用以保存每个区间中学生的人数
// 遍历成绩数组,获取每个区间的人数
for (auto const& student : grades) // 使用引用,减少复制副本的开销
{
for (auto const& grade : student)
{
frequency[grade / 10]++;
}
}
// 打印条形图
for (int i = 0; i < frequencySize; i++)
{
// 打印表头
if(i != 10)
cout << setw(2) << i * 10 << " - "
<< setw(3) << 9 + i * 10 << ": ";
else
cout << setw(10) << "100: ";
// 打印符号*,表示一个人
for (int stars = 0; stars < frequency[i]; stars++)
{
cout << "*";
}
cout << endl;
}
}
// 打印学生的成绩
void GradeBook::outputGrades() const
{
cout << "\nThe grades are: \n";
cout << setw(10) << " "; // 打印表头,对应student id
for (int i = 0; i < tests; i++)
{
cout << setw(5) << "Test" << setw(2) << i + 1;
}
cout << setw(9) << "Average" << endl;
// 输出所有学生的成绩
for (int student = 1; student <= grades.size(); student++)
{
cout << "student" << setw(2) << student << ":" ;
// 输出一个学生的所有成绩
for (int test = 0; test < tests; test++)
{
cout << setw(7) << grades[student - 1][test];
}
// 输出该学生的平均成绩
cout << setprecision(2) << fixed << setw(9) << getAverage(grades[student - 1]) << endl;
}
}
test.cpp
#include "GradeBook.h"
using namespace std;
int main()
{
// 学生成绩
array<array<int, GradeBook::tests>, GradeBook::students> grades =
{
87, 96, 70, 97,
68, 87, 90, 93,
94, 100, 90, 98,
100, 82, 81, 92,
83, 65, 83, 80,
78, 87, 65, 81,
67, 78, 85, 90,
77, 79, 63, 88,
99, 100, 95, 96,
85, 83, 79, 90
};
// 课程名
string CourseName = "CS1201 C++ Programming";
// 创建一个GradeBook对象
GradeBook myGradeBook(CourseName, grades);
myGradeBook.displayMessage();
myGradeBook.processGrades();
}
运行结果:
七、C++标准库类模板vector的介绍
vector对象的声明语法:
vertor<类型> 名称(大小);
示例代码
#include <iostream>
#include <vector>
using namespace std;
void outputVector(const vector<int> &);
void inputVector(vector<int> &);
int main()
{
// 与array类模板的声明类似,在<>中指定保存元素的类型,但是容量在变量名后指定
vector<int> integer1(7); // 创建一个有7个int类型元素的vector对象
vector<int> integer2(10); // 创建一个有7个int类型元素的vector对象
// 输出两个vector对象的大小及默认初始值
cout << "\nSize of vector integer1 is " << integer1.size() << endl
<< "vector after initialization:";
outputVector(integer1);
cout << "\nSize of vector integer2 is " << integer2.size() << endl
<< "vector after initialization:";
outputVector(integer2);
// 输出17个整数用于初始化两个vector对象
cout << "Enter 17 ingeters:" << endl;
inputVector(integer1);
inputVector(integer2);
// 输出赋值后的vector对象的值
cout << "\nAfter input, the vectors contain:\n"
<< "integer1:" << endl;
outputVector(integer1);
cout << "integer2:" << endl;
outputVector(integer2);
cout << "\nEvaluation: integer1 != integer2" << endl;
if (integer1 != integer2)
{
cout << "integer1 and integer2 are not equal" << endl;
}
// 创建一个新vector对象,并使用integer1赋值
vector<int> integer3(integer1);
cout << "\nSize of vector integer3 is " << integer3.size() << endl
<< "vector after initialization:";
outputVector(integer3);
// 对vector对象直接使用'='进行赋值
cout << "\nAssigning integer2 to integer1: " << endl;
integer1 = integer2;
cout << "integer1:" << endl;
outputVector(integer1);
cout << "integer2:" << endl;
outputVector(integer2);
cout << "\nEvaluation: integer1 == integer2" << endl;
if (integer1 == integer2)
{
cout << "integer1 and integer2 are equal" << endl;
}
cout << "\ninteger1[5] is " << integer1[5] << endl;
cout << "\n\nAssigning 1000 to integer1[5]." << endl;
integer1[5] = 1000;
cout << "integer1:" << endl;
outputVector(integer1);
// 异常处理,访问越界
try {
cout << "\n\nAttempt to display integer1.at(15)." << endl;
cout << integer1.at(15) << endl; // error,越界
}
catch (out_of_range &ex){
cerr << "An exception occured: " << ex.what() << endl;
}
cout << "\nCurrent Size of integer3 is " << integer3.size() << endl;
integer3.push_back(1000); // 在integer3的末尾添加一个元素3
cout << "New integer3 size is " << integer3.size() << endl;
cout << "integer3 now contains: " << endl;
outputVector(integer3);
}
void outputVector(const vector<int>& array)
{
// 使用基于范围的for语句遍历整个vector对象
for (int item : array)
{
cout << item << " ";
}
cout << endl;
}
void inputVector(vector<int>& array)
{
for (int item : array)
{
cin >> item;
}
}
运行结果:
代码中使用的库函数:
从上面的程序执行结果可以看出,vector对象可以使用关系运算符进行比较操作。
一个vector对象可以用另一个vector对象进行初始化。也可以使用一个vector对象为另一个vector对象赋值。
还可以像数组一样使用[]
和下标来访问指定的vector元素,但是在使用[]
进行访问时,C++是不会进行边界检查的。但是在vector类提供的at函数中,是提供了边界检验功能的。
异常处理:处理超出边界的下标
异常是一个在程序运行时出现的问题的表现。异常处理使程序员能够创建可以解决(或处理)异常的容错程序。在很多情况下,在处理异常的同时还允许程序继续运行,就像没有遇到异常一样。
try语句:
为了处理一个异常需要把可能抛出一个异常的任何代码放置在一个try语句中。
catch语句:
包含了发生异常时,负责处理异常的代码。
比如,上面例子中try语句中包含了一个调用vector类的at成员函数的语句,该函数提供了边界检查和抛出异常的功能,当这个函数的实参是一个无效下标时,就会抛出一个异常。在默认的情况下这会导致C++程序终止。在这里下标15显然不是一个有效的下标,所有异常出现,由该try语句对应的catch语句来处理这个异常。
从上面的库函数介绍图中,可以看出,at函数抛出的异常是out_of_range
类型的异常。所以在上面代码的catch语句指定了处理的异常的类型为out_of_range
,且声明了一个用于接收引用的异常形参ex
。在这个语句块中,可以使用形参的标识符来实现与捕捉到的异常对象的交互。
注意,try和catch是两个不同的语句块,它们的作用域不同,在try中声明的变量无法在catch中使用。
异常形参的what函数:
在上面的代码中调用了这个异常对象的what成员函数,来得到并显示存储在此异常对象中的错误信息。
更详细的异常处理内容会在之后的章节中说明。
vector对象和array对象的主要区别
vector对象和array对象的一个主要区别就是vector对象可以动态增长以容纳更多元素。
使用成员函数push_back
可以为vector对象增加一个元素,对应的可以使用pop_back
这个成员函数删除vector对象中的一个元素。
除了像上面一样使用for循环语句对vector对象进行初始化,还可以使用初始化列表对vector对象进行初始化。例如:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> integer4 = { 1,2,3,4,5 };
// or vector<int> integer4{1,2,3,4,5};
for (int item : integer4)
{
cout << item << " ";
}
}
上面这段代码中,使用初始化列表将vector对象integer4初始化为有5个元素vector对象。