第4章 组织程序和数据
从前面的学习中,我们可以发现程序并不是我们所想的那么简短,他们都有一个共同的特性,那就是
1)都能解决某些特定类型的问题
2)与其他的大多数工具都互相独立
3)都具有一个自己的名称
C++中提供两种基本的方法来让我们组织大型的程序,函数(子程序)和数据结构。
1、组织计算
1)计算总成绩
子函数grade
<span style="font-family:KaiTi_GB2312;">//根据学生的期中考试、期末考试、家庭作业成绩来计算总成绩 double grade(double midterm, double final, double homework) { return 0.2 * midterm + 0.4 * final + 0.4 * homework; }</span>
2)查找中值
子函数median
<span style="font-family:KaiTi_GB2312;">/计算一个vector<double>类型的变量的中值 double median(vector<double> vec) { typedef vector<double>::size_type vec_sz; vec_sz size = vec.size(); if(size == 0) throw domain_error("median of an empty vector"); sort(vec.begin(), vec.end()); vec_sz mid = size/2; return size % 2 == 0 ? (vec[mid] + vec[mid - 1]) / 2 : vec[mid]; }</span>
3)重新制定计算成绩的策略
<span style="font-family:KaiTi_GB2312;">//根据期中、期末考试成绩和保存家庭作业的向量来计算学生的总成绩 double grade(double midterm, double final, const vector<double>& hw) { if (hw.size() == 0) throw domain_error("student has done no homework"); return grade(midterm, final, median(hw)); }</span>
程序说明:
1)const vector<double>& hw,这个类型被称为“对参数类型为double的向量常量的引用”,const确保了我模拟将不会对hw做任何可能改变它的值得动作。
2)这个函数名叫做grade,可以让几个函数具有同样的函数名,叫做函数的重载
3)最后有一个检查程序,这样做的目的主要就是抛出自己的异常,希望给用户更多的提示。
4)读家庭作业成绩
<span style="font-family:KaiTi_GB2312;">//从输入流中将家庭作业的成绩读入一个vector<double>中 istream read_hw(istream& in, vector<double>& hw) { if(in) { //清除原先的内容 hw.clear(); //读家庭作业成绩 double x; while (in >> x) hw.push_back(x); //清除流以使输入动作对下一个学生有效 in.clear(); } return in; }</span>
程序说明:
1)这里与上面不一样的地方,去掉了const,说明这里可以修改作为函数参数的对象的值;
2)对于istream对象,它清除了所有的错误标记以使输入动作可以继续;而对于向量对象,它删除了向量中可能已经含有的全部内容,这样就会让我们再次拥有一个空的向量。
5)使用函数来计算学生的成绩
<span style="font-family:KaiTi_GB2312;">// lesson4_1.cpp : 定义控制台应用程序的入口点。 //功能:使用函数来计算学生的成绩 //时间:2014.5.12 #include "stdafx.h" #include <algorithm> #include <iomanip> //#include <ios> #include <stdexcept> #include <iostream> #include <string> #include <vector> using std::cin; using std::cout; using std::domain_error; using std::endl; using std::istream; using std::ostream; using std::setprecision; using std::sort; using std::streamsize; using std::string; using std::vector; //根据学生的期中考试、期末考试、家庭作业成绩来计算总成绩 double grade(double midterm, double final, double homework) { return 0.2 * midterm + 0.4 * final + 0.4 * homework; } //计算一个vector<double>类型的变量的中值 double median(vector<double> vec) { typedef vector<double>::size_type vec_sz; vec_sz size = vec.size(); if(size == 0) throw domain_error("median of an empty vector"); sort(vec.begin(), vec.end()); vec_sz mid = size/2; return size % 2 == 0 ? (vec[mid] + vec[mid - 1]) / 2 : vec[mid]; } //根据期中、期末考试成绩和保存家庭作业的向量来计算学生的总成绩 double grade(double midterm, double final, const vector<double>& hw) { if (hw.size() == 0) throw domain_error("student has done no homework"); return grade(midterm, final, median(hw)); } //从输入流中将家庭作业的成绩读入一个vector<double>中 istream& read_hw(istream& in, vector<double>& hw) { if(in) { //清除原先的内容 hw.clear(); //读家庭作业成绩 double x; while (in >> x) hw.push_back(x); //清除流以使输入动作对下一个学生有效 in.clear(); } return in; } int _tmain(int argc, _TCHAR* argv[]) { //请求并读入学生姓名 cout << "Please enter your first name: "; string name; cin >> name; cout << "Hello, " << name << "!" << endl; //请求并读入期中和期末考试的成绩 cout << "Please enter your midterm and final exam grades: "; double midterm, final; cin >> midterm >> final; //请求用户输入家庭作业成绩 cout << "Enter all your homework grades, " "followed by end-of-file: "; vector<double> homework; //读入家庭作业成绩 read_hw(cin, homework); //如果可以的话,计算生成总成绩 try { double final_grade = grade(midterm, final, homework); streamsize prec = cout.precision(); cout << "Your final grade is " << setprecision(3) << final_grade << setprecision(prec) << endl; } catch (domain_error) { cout << endl << "You must enter your grades. " << "Please try again." << endl; return 1; } return 0; } </span>
运行结果:
程序说明:
这里需要讲解下的就是try语句,它尝试执行在{}中紧跟那一条trry语句,如果在这语句中的任何一个地方发生了一个domain_error异常,它就会停止这些语句。
注意:这里我在仿真的时候istream& read_hw(istream& in, vector<double>& hw)注意istream& 的这个符号,如果不加,C++会报错,error
C2248: “std::basic_ios<_Elem,_Traits>::basic_ios”: 无法访问 private 成员(在“std::。
2、组织数据
计算一个学生的成绩对这个学生来说是很有用的,不过这个计算十分简单,对于教授来说,我们更希望的是要计算出一整班学生的成绩,这样我们希望按照学生的姓名的字母顺序来组织输出报表,而且我们希望纵向排列总成绩以便于阅读,所以这就要求我们需要有一个地方来储存所有学生的记录,这样我们就可以按字母顺序来排列他们了,还需要特别注意的是为了确定在每一个姓名和对应的成绩之间要放置多少个空格,我们还要找出最长的姓名的长度。
1)把所有的学生的所有数据放置在一起
<span style="font-family:KaiTi_GB2312;">struct Student_info { string name; double midterm, final; vector<double> homework; };//这里一定要注意分号是不可缺少的</span>
程序说明:
1)Student_info是一种具有四个数据成员的类型,因为Strudent_info是一种类型,所有我们就可以定义这种类型的对象,而每一个对象都会包含这四个数据成员的一个实例。
2)每一个Student_info类型的对象都保存了一个学生的信息,因为Student_info是一种类型,我们用一个vctor<Student_info>类型来保存任意数目的学生的信息,这就好像相当于我们之前使用一个vector<double>类型的对象来保存任意数目的家庭作业成绩一样。
3)这里还需要注意结尾的那个分号是不可缺少的。
2)处理学生记录
简单思路:①我们要把数据读到一个Student_info类型的对象中;②要为一个Student_info类型的对象生成总成绩。③找到一种方法对一个Student_info类型的向量进行排序。
①读入并储存学生的姓名以及期中、期末考试成绩
<span style="font-family:KaiTi_GB2312;">//需要读学生的姓名以及考试成绩 istream& read(istream& is, Student_info& s) { is >> s.name >> s.midterm >> s.final; read_hw(is, s.homework);//读入并储存学生的所有家庭作业成绩 return is; }</span>
程序说明:
read函数具有两个引用,一个是istream,我们就是从istream中读数据的;另一个引用则是一个对象,它储存了函数所读到的数据,在函数内部使用参数s的时候,我们会影响传递给我们的那个参数的状态。
②为Student_info类型的对象计算总成绩
<span style="font-family:KaiTi_GB2312;">double grade(const Student_info& s) { return grade(s.midterm, s.fianl, s.homework); }</span>
程序说明:
注意参数类型是const Student_info&而不是简单Student_info,这样的话,在我们调用它的时候就不会有复制整个Student_info对象而带来的额外开销了。
③对Student_info类型的对象进行排序
这里要注意不可以使用
<span style="font-family:KaiTi_GB2312;">sort(student.begin(), student.end());</span>
因为sort试图去比较两个这样的对象的话,编译器会报错。
所以我们必须引入第三个参数
<span style="font-family:KaiTi_GB2312;">// 按字母顺序排列学生记录 bool compare(const Student_info& x, const Studnt_info& y) { return x.name < y.name; } sort(students.begin(), students.end(), compare);</span>
这里编写的比较函数仅仅是对名字进行比较。
3)生成报表
<span style="font-family:KaiTi_GB2312;">int _tmain(int argc, _TCHAR* argv[]) { vector<Student_info> students; Student_info record; string::size_type maxlen = 0; //最长的姓名的长度 //读入并存储所有学生的数据 //不变式 // students 包含了所有的学生的记录 // max包含了students中最长的姓名 while (read(cin, record)) //找出最长的姓名的长度 { maxlen = max(maxlen, record.name.size()); students.push_back(record); } // 按字母顺序排列学生记录 sort(students.begin(), students.end(), compare); //输出姓名和成绩 for (std::vector<Student_info>::size_type i = 0; j != students.size(); ++i) { //输出姓名,把姓名填充至maxlen + 1 个字符长度 cout << setw(maxlen+1) << students[i].name; //计算并输出成绩 try { double final_grade = grade(students[i]); streamsize prec = cout.precision(); cout << setprecision(3) << final_grade << setprecision(prec); } catch(domain_error e) { cout << e.what(); } cout << endl; } return 0; }</span>
程序说明:
1)库函数中max在头文件<algorithm>中定义了,表明上max的行为是很明显的,但是它的行为的其中一方面是不明显的,由于某些复杂的原因,它的两个参数必须具备同样的类型,所以我们把它maxlen定义为int类型是不合适的,那么我们必须把它定义为一个string::size_type类型的变量;
2)原先计算成绩中我们使用了setprecision;setw的工作方式与之相似,setw来控制输出流的格式,这里用setw来确保输出中每一个姓名都会消耗跟输入中的最长的姓名一样多的字符,同时它还会用一个额外的空格来作为姓名与成绩之间的空白。,不同的地方在于对宽度的修改是短暂的,每一个标准输出运算符在完成了它的输出动作之后就马上回重置流的宽度。代码中作用就是为name填充字符,同时输出name的运算符将把流的宽度重置为0。
3、把各部分代码连接在一起
目前为止,我们已经定义了很多抽象函数,这些抽象函数对于解决各种成绩问题都是很有用的,我们以前使用这些函数的方法就是把他们所有的定义都放在同一个文件中并编译文件,很明显这个方法很快会变得越来越复杂
1)学会组装函数
例如组装median函数
<span style="font-family:KaiTi_GB2312;">//median函数的源文件 #include "stdafx.h" #include <algorithm> //获取sort声明 #include <stdexcept> //获取domain_error的声明 #include <vector> //获取vector的声明 using namespace std; //计算一个vector<double>类型的对象的中值 { typedef vector<double>::size_type vec_sz; vec_sz size = vec.size(); if(size == 0) throw domain_error("median of an empty vector"); sort(vec.begin(), vec.end()); vec_sz mid = size/2; return size % 2 == 0 ? (vec[mid] + vec[mid - 1]) / 2 : vec[mid]; }</span>
程序说明:我们需要把median函数放如一个名为median.cpp,或者median.C或者median.c文件中。
2)让median函数对其他用户也可用
标准库把其定义的名称放进头文件中,与之类似,我们可以编写自己的头文件,这样用户就可以使用我们定义的名称了
<span style="font-family:KaiTi_GB2312;">//使用median函数的一种较好的方法 #include "median.h" #include <vector> int main {……}</span>
程序说明:如果我们在#include指令中使用了一个双引号而不是尖括号来把头文件名称括起来,那么就表示,为了替代#include指令,我们要求编译器把与此名称对应的头文件中所有内容都复制到我们的程序中。
3)写出头文件的声明
<span style="font-family:KaiTi_GB2312;">#ifndef GUARD_median_h #define GUARD_median_h #include <vector> double median(vector<double>); #endif</span>
程序说明
1)记住我们的median.h头不能单单包含这个声明,我们还需要把声明本身所用到的所有名称包含进去,所有必需加上vector
2)我们这里不适用using声明,因为我们的代码用户可能不想使用一个using std::vector的声明
2)我们需要注意一个细节,加上预处理程序,#ifndef指令检查GUARD_median_h是否被定义。
4、把计算成绩的程序分块
1)把Student_info结构与相关的函数组装起来
头文件:
<span style="font-family:KaiTi_GB2312;">#ifndef GUARD_Student_info #define GUARD_Student_info //Student_info.h 头文件 #include <iostream> #include <string> #include <vector> struct Student_info { std::string name; double midterm, final; std::vector<double> homework; }; bool compare(const Student_info&, const Student_info&); std::istream& read(std::istream&, Student_info&); std::istream& read_hw(std::istream&, std::vector<double>&); #endif </span>
程序说明:注意这里我们在限定标准库的名称的时候所使用的是std::而不是using声明。
源文件:
<span style="font-family:KaiTi_GB2312;">#include "Student_info.h" using std::istream; using std::vector; bool compare(const Student_info& x, const Student_info& y) { return x.name < y.name; } istream& read(istream& is, Student_info& s) { //读和储存学生的名字,期中和期末考试成绩 is >> s.name >> s.midterm >> s.final; read_hw(is, s.homework); //读和储存家庭作业成绩 return is; } //读一个家庭作业分数,储存在一个'vector<double>'向量中 istream& read_hw(istream& in, vector<double>& hw) { if(in) { //清除先前的内容 hw.clear(); //读家庭作业分数 double x; while (in >> x) hw.push_back(x); //清除the stream 保证可以工作到下一个学生 in.clear(); } return in; }</span>
程序说明:源文件中使用using声明并没有问题,不像头文件,源文件对使用这些函数的程序没有影响。因此在源文件中使用using声明只不过是一个局部的决策罢了,不会影响到全局。
2)编写头文件来声明不同的grade重载函数
头文件:
<span style="font-family:KaiTi_GB2312;">#ifndef GUARD_grade_h #define GUARD_grade_h #include <vector> #include "Student_info.h" double grade(double, double, double); double grade(double, double, const std::vector<double>&); double grade(const Student_info&); double grade(const Student_info&); #endif</span>
源文件:
<span style="font-family:KaiTi_GB2312;">#include <stdexcept> #include <vector> #include "grade.h" #include "median.h" #include "Student_info.h" using std::domain_error; using std::vector; //计算一个vector<double>类型的对象的中值 double median(vector<double> vec) { typedef vector<double>::size_type vec_sz; vec_sz size = vec.size(); if(size == 0) throw domain_error("median of an empty vector"); sort(vec.begin(), vec.end()); vec_sz mid = size/2; return size % 2 == 0 ? (vec[mid] + vec[mid - 1]) / 2 : vec[mid]; } //根据期中、期末考试成绩和保存家庭作业的向量来计算学生的总成绩 double grade(double midterm, double final, const vector<double>& hw) { if (hw.size() == 0) throw domain_error("student has done no homework"); return grade(midterm, final, median(hw)); } //需要读学生的姓名以及考试成绩 istream& read(istream& is, Student_info& s) { is >> s.name >> s.midterm >> s.final; read_hw(is, s.homework);//读入并储存学生的所有家庭作业成绩 return is; }</span>
5、修正后的计算成绩的程序
<span style="font-family:KaiTi_GB2312;">// lesson4.2.cpp : 定义控制台应用程序的入口点。 //功能:修正后的计算成绩的程序 //时间:2014.5.12 #include "stdafx.h" #include <algorithm> #include <iomanip> #include <stdexcept> #include <string> #include <vector> #include "grade.h" #include "Student_info.h" using namespace std; int _tmain(int argc, _TCHAR* argv[]) { vector<Student_info> students; Student_info record; string::size_type maxlen = 0; //最长的姓名的长度 //读入并存储所有学生的数据 //不变式 // students 包含了所有的学生的记录 // max包含了students中最长的姓名 while (read(cin, record)) //找出最长的姓名的长度 { maxlen = max(maxlen, record.name.size()); students.push_back(record); } // 按字母顺序排列学生记录 sort(students.begin(), students.end(), compare); //输出姓名和成绩 for (std::vector<Student_info>::size_type i = 0; i != students.size(); ++i) { //输出姓名,把姓名填充至maxlen + 1 个字符长度 cout << setw(maxlen+1) << students[i].name; //计算并输出成绩 try { double final_grade = grade(students[i]); streamsize prec = cout.precision(); cout << setprecision(3) << final_grade << setprecision(prec); } catch(domain_error e) { cout << e.what(); } cout << endl; } return 0; } </span>
6、编写一个程序来计算从1-100的整数(int)值的平方。程序的输出分为两列;第一列是整数值。第二列是整数值的平方
<span style="font-family:KaiTi_GB2312;">// lesson4_3.cpp : 定义控制台应用程序的入口点。 //功能:编写一个程序来计算从1-100的整数(int)值的平方。程序的输出分为两列;第一列是整数值。第二列是整数值的平方 //时间:2014.5.12 #include "stdafx.h" #include <cmath> #include <iomanip> //定义控制器setprecision 这个控制器可以让我们指明输出所包含的有效位数 #include <iostream> using namespace std; int _tmain(int argc, _TCHAR* argv[]) { for(int i = 1; i <= 100; ++i) { cout << setw(3) << i << setw(6) << i * i << endl; } return 0; } </span>运行结果:
程序说明:setw(n) 返回一个类型为streamsize的值,如果把这个函数用于输出流s,那么它的作用跟调用s.width(n)的一样。
7、改进程序让它更好的适应性,当i增长的时不需要修正setw的参数
<span style="font-family:KaiTi_GB2312;">// lesson4_4.cpp : 定义控制台应用程序的入口点。 //功能:让它更好的适应性,当i增长的时不需要修正setw的参数 //时间:2014.5.12 #include "stdafx.h" #include <cmath> #include <iomanip> #include <iostream> using namespace std; int get_width(double n) { return log10(n) + 1; } int _tmain(int argc, _TCHAR* argv[]) { double max = 100.0; for(int i = 1; i <= max; ++i) { cout << setw(get_width(max)) << i << setw(get_width(max * max) + 1) << i * i << endl; } return 0; } </span>
注意:VS2008中定义的那个get_width只能用float,否则报错。
8、编写一个函数来从输入流读单词,把读到的单词存储在一个向量中,利用这个函数编写一个程序来计算输入的单词的数目以及每一个单词所出现的次数。
<span style="font-family:KaiTi_GB2312;">// lesson4_5.cpp : 定义控制台应用程序的入口点。 //功能:编写一个函数来从输入流读单词,把读到的单词存储在一个向量中,利用这个函数编写一个程序来计算输入的单词的数目以及每一个单词所出现的次数。 //时间:2014.5.13 #include "stdafx.h" #include <algorithm> #include <iostream> #include <string> #include <vector> using namespace std; //从输入流中将单词读入到一个vector<string> istream& read_words(istream& in, vector<string>& words) { if(in) { //清除原先的内容 words.clear(); //读单词 string word; while (in >> word) words.push_back(word); //清除流以使输入动作对下一个单词有效 in.clear(); } return in; } int _tmain(int argc, _TCHAR* argv[]) { typedef vector<string>::size_type vec_sz; vector<string> words; read_words(cin, words); cout << "Num of words: " << words.size() << endl; sort(words.begin(), words.end()); string prev_word = ""; int count = 0; for(vec_sz i = 0;i < words.size(); ++i) { if (words[i] != prev_word) { if (prev_word != "") { cout << prev_word << " appeared " << count << "times" << endl; } prev_word = words[i]; count = 1; } else ++count; } cout << prev_word << " appeared " << count << "times" << endl; return 0; } </span>
运行结果:
注意:这里我在实现的时候,报错error c3872: “0x3000”: 此字符不允许在标识符中使用,解决方法:
这个语句的后面空白部分,都删除掉,免得有不可见的全角空格。可能是不小心按到空格键了。
9、编写一个程序来计算存储在一个vector<double>类型的向量中的数据的平均值
<span style="font-family:KaiTi_GB2312;">// lesson4_7.cpp : 定义控制台应用程序的入口点。 //功能:编写一个程序来计算存储在一个vector<double>类型的向量中的数据的平均值 #include "stdafx.h" #include <iostream> #include <numeric> #include <vector> using namespace std; int _tmain(int argc, _TCHAR* argv[]) { vector<double> nums; double num; while (cin >> num) nums.push_back(num); cout << accumulate(nums.begin(), nums.end(), 0.0) / nums.size() << endl; //如果不要求存储在一个vector<double>类型的向量 /*int count = 0; double sum = 0; double x; //把数据读入变量x中 while (cin >> x) { ++count; sum += x; } cout << "The avg: " << sum / count << endl; */ return 0; } </span>
程序说明:本程序使用了accumulate函数
10、小结
1)#include<系统头文件>;#include"用户定义的头文件名称"(这里需要注意不应该包含using声明,但是给标准库的名称应该明确加上一个前缀using)
2)通过学习本章懂得了如何使用函数和数据结构来组织运算,还是需要一个方法来把程序划分为我们可以独立编译且在编译后能组合在一起的文件。
——To_捭阖_youth