Snowming04's Blog
一颗红❤
Toggle navigation
Snowming04's Blog
主页
Cobalt Strike
Accelerated C++
区块链安全
友链
关于我
常用工具
代码积累
归档
标签
【Accelerated C++】课时4:组织程序和数据
? C++ ?
2020-03-25 14:03:58
273
0
0
snowming
? C++ ?
# 0x01 本章要点 **<u>函数、数据结构和类</u>** 跟大多数程序设计语言一样,C++提供了两种基本的方法来让我们组织大型的程序: 1. 函数(也被称为子程序) 2. 数据结构 C++还可以让程序员把函数和数据结构结合在类这个概念中。 **<u>独立编译</u>** 把程序划分为我们可以独立编译且在编译后能组合在一起的文件。 # 0x02 组织计算 **<u>总成绩计算函数</u>** 之前计算总成绩: > 0.2*期中成绩+0.4*期末成绩+0.4*作业成绩 是直接输出表达式的值。现在我们把它换成一个函数: ``` double grade(double midterm, double final, double homework) { return 0.2*midterm + 0.4*final + 0.4*homework; } ``` 这样在输出的时候,我们就做了如下的改变: ``` cout << "Your final grade is " <<setprecision(3) <<0.2*midterm + 0.4*final + 0.4*sum/count <<setprecision(prec) <<endl; ``` 利用grade函数改写: ``` cout<<"Your final grade is " <<setprecision(3)<<grade(midterm,final,sum/count)<<setprecision(prec)<<endl; ``` - 参数不仅仅是变量,它们还可以是诸如sum/count这样的表达式。 **<u>按值调用</u>** 一般而言,每个参数都是用来对相应的参数进行初始化的,参数在初始化之后就可以像普通的局部变量一样的在函数中使用了。例如,如果我们调用 `grade(midterm,final,sum/count)`,那么grade函数的参数并不是直接指向参数本身,实际上,这些参数的初始值是相应参数值的复制,这个行为经常被称为「按值调用」(call by value),这是因为参数获得的只是参数值的一个复制。 ## 查找中值 **<u>计算中值函数</u>** ``` //计算一个vector<double>类型的变量的中值 //值得注意的是,调用函数时整个vector参数都会被复制(vector作为参数被传入) 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]; } ``` **<u>抛出异常</u>** 如果向量为空,那么就抛出一个异常。 如果一个程序抛出了一个异常,那么这个程序就会在抛出异常的地方终止执行并转移到程序的另一部分,并向这部分提供一个异常对象,异常对象中含有调用程序可以用来处理异常的信息。 信息包括: 1. 一个异常被抛出了的事实。 2. 异常对象的类型。 这两个信息相结合,就足以让调用程序知道应该采取什么措施了。 在本例中,我们抛出的异常类型是 `domain_error`。这是在头文件 `<stdexcept>` 中定义的一种类型,它会向我们报告,函数参数的<u>取值是函数所不能接受的。</u> 在我们创建一个待抛出的`domain_error`对象时,我们可以给它一个字符串来描述错误信息。捕获异常的程序可以在一个诊断信息中使用这个字符串。 **<u>函数复制参数</u>** 在调用一个函数的时候,可以把参数看作是初始值等于参数的局部变量。这样的话,在调用一个函数的时候,参数同时也会被复制到参数中。特别地,如果我们调用median函数,那么我们用做参数的那个向量就会被复制到 `vec` 中。 就median而言,把参数复制到参数中是非常有用的,但这样做会花费较多的时间——因为median函数会通过调用sort从而改变它的参数值。如果函数使用复制参数的方式,那么sort所作的改变就不会反馈到调用程序中了。这个做法是很有意义的,因为我们获取向量的中值时不应该连带改变向量的本身。 > 这就是我在[【C基础】课时13:一个玄学问题的解决】](http://blog.leanote.com/post/snowming/cde7a9ac662d)一文中提及的问题:如果把一个全局变量作为参数传入某个函数,那么该函数被调用之后,将不会改变该全局变量的值;只有不作为参数传入,但是在函数中改变该全局变量,那么该全局变量的值在该函数的作用域之外也会是被改变之后的值。 ## 重新制定计算成绩的策略 **<u>重新制定计算成绩的策略</u>** ``` //根据期中、期末考试成绩和保存家庭作业的向量来计算学生的总成绩 //这个函数不用复制它的参数,因为median已经为我们完成了这个工作 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)); } ``` **<u>引用</u>** 第三个参数指定的类型是 `const vector<double>&`,这个类型被称为「对参数类型为double的向量常量的引用」,或者是「双精度向量常量引用」。 我们说名称是一个引用是指,这个名称是一个特定对象的另一个名称。例如,如果我们编写了以下的语句: ``` vector<double> homework; vector<double>& hw = homework; //hw是homework的一个替代名 ``` > 可以理解为: 定义一个`vector<double>&`类型的变量,其值为homework。而这个变量定义赋值的实际意义就是给homework起一个别名hw。 那么`hw`就是homework的另一个名称,从现在起,我们对hw所做的任何动作都等价于对homework做同样的动作,反过来也是一样。 ---------------- 如果再加上一条语句: ``` const vector<double>& chw = homework; ``` 这条语句仍然表示`chw`是homework的另一个替代名,但是const确保了我们将不会对chw做任何可能改变它的值的动作。 因为一个引用是原先的变量的另一个名称,所以不存在诸如一个引用的引用这样的说法。定义一个引用的引用跟定义原来对象的引用的效果是一样的。例如,如果我们编写了: ``` //hw1和chw1想homework的替代名,chw1是只读的 vector<double>& hw1 = hw; const vector<double>& chw1 = chw; ``` 那么,`hw1`跟`hw`一样都是homework的另一个名称,而chw1则跟chw一样是homework的<u>不允许写访问</u>的另一个名称。 > 注意: 这一切的前提是 `vector<double> homework; `; 然后引用就是变量名层面的把戏,并不复杂。 -------------------- 如果我们定义一个非常量引用(也就是一个允许写访问的引用)我们就不能让它指向一个常量对象或常量引用,因为这样做表示:我们将要做const所不允许的动作。因此我们不能编写: ``` vector<double>& hw2 = chw; //错误,请求了对chw的写访问 ``` 这是因为我们之前已经保证了不会去改变`chw`的值: ``` const vector<double>& chw = homework; ``` > 其实很好理解: 既然 `vector<double>& hw = homework` 可以理解为,定义一个`vector<double>&`类型的变量hw,给其赋值为homework。那么在定义变量的时候,可以给其添加限定符`const`,那么就无法对此变量做任何修改。也不能再定义一个不加限制符`const`的变量给其赋值为此静态变量,否则就会陷入可以通过普通变量名改变静态变量的窘境。 这个引用也是一样的道理,只不过其实际意义是「起别名」而已。 ---------------- 引用与开销: 同样地,如果我们说一个参数的类型是`const vector<double>&`,那么,实际上我们就是在请求系统环境给予我们对关联变量的直接访问权而不用我们去复制它,同时这样做还可以确保我们将不会改变参数的值(否则参数的值也就跟着改变了)。因为这个参数是对常量的一个引用,所以我们既可以为常量也可以为非常量向量调用这个函数。又因为参数是一个引用,所以我们就可以避免复制参数的额外开销。 **<u>重载</u>** ``` //根据期中、期末考试成绩和保存家庭作业的向量来计算学生的总成绩 //这个函数不用复制它的参数,因为median已经为我们完成了这个工作 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)); } ``` > 在我的理解中,完整的程序会先定义一个类型为引用的变量比如 `const vector<double>& hw = homework`,然后把 hw 作为实参传入此 grade 函数。 此grade函数中,调用了另一个`grade`函数。我们能够让几个函数具有同样的函数名,这个概念就做「重载」。重载在许多的C++程序中都会经常出现。就算我们有两个函数的函数名是相同的,这也不会导致意义上的含糊,因为不管我们在什么时候调用grade,我们都会提供一个参数列表,系统环境能够根据第三个参数的类型来辨别我们所指的是哪个函数。 这里外层grade函数调用的里面那个grade函数指的是我们先前定义的这个grade函数(名称相同、参数类型不同): ``` double grade(double midterm, double final, double homework) { return 0.2*midterm + 0.4*final + 0.4*homework; } ``` 注:median函数的返回值类型为`double`。 **<u>抛出异常</u>** ``` //根据期中、期末考试成绩和保存家庭作业的向量来计算学生的总成绩 //这个函数不用复制它的参数,因为median已经为我们完成了这个工作 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)); } ``` 在这段程序中,我们会检查`homework.size()`是否为0——尽管`median`函数也会做这个检查。之所以再次抛出一个异常是因为:如果median函数发现我们是在请求一个空向量的中值的话,它就会抛出一个包含了信息"median of an empty vector"的异常。对那些正在计算学生成绩的人来说,这条信息并不是直接有用的。因此我们抛出了这个异常,可以给用户更直接的提示,帮助用户更好的定位错误所在。 ## 读家庭作业成绩 **<u>如何让一个函数返回多个值</u>** 现在我们需要进行这部分的程序设计:如何把家庭作业成绩读进一个向量中。在设计这个问题时候,逻辑上需要一次返回两个值:一个值就是读到的家庭作业成绩,另一个值则指示输入尝试是否成功。 但是没有一个直接的方法从函数中返回多于一个的值,一个间接的处理方法是:给函数一个参数,这个参数是对一个对象的引用,而函数的其中一个结果值会放置在这个参数里面。 ``` //读入一个输入流,把家庭作业成绩读进一个vector<double>类型的向量中 istream& read_hw(is_stream& in, vector<double>& hw) { //这一部分代码有待补充 return in; } ``` 注意这里的`vector<double>& hw`类型的参数`hw`没有被const限定,一个不含const的引用参数通常表示我们可以修改作为函数参数的对象的值。例如,如果我们执行: ``` vector<double> homework; read_hw(cin, homework); ``` 那么实际上,read_hw的第二个参数是一个引用。根就这个事实我们就可以预料到,对read_hw的调用将会改变homework的值。 **<u>通过函数修改其参数的值</u>** 如果我们希望函数能够修改它的参数的值,那么在调用这个函数的时候,就不能在参数列表中使用任何的表达式。**相反,我们必须传递一个左值参数给引用参数。**左值是一个用来指示非临时对象的值。例如,如果一个变量是左值,那么同时它也可以是一个引用或者说可以是返回一个引用的函数的调用结果。一个产生算术值的表达式,例如`sum/count`,并不是左值。 **`read_hw`函数的两个参数都是引用,这是因为,我们希望用这个函数来改变这两个参数的状态。** 我们并不了解cin的细节,但是可以假定,库是把它定义成一个数据结构的。在这种数据结构中存储了库需要用来了解我们的输入文件状态的所有东西。从标准输入文件读输入会改变文件的状态,因此,从逻辑上说,它同时也会改变cin的值。 值得注意的是,`read_hw`会返回in。此外,它是以引用的形式来完成这个动作的。实际上,这是表示我们指定了一个对象,而我们将不会在函数调用时对此对象进行复制;同时我们还会在函数返回时返回同一对象——即返回时也不复制对象。 **<u>完整的read_hw函数</u>** ``` //从输入流中将家庭作业的成绩读入到一个vector<double>中 istream read_hw(istream& in, vector<double>& hw) { if(in) { //清除homework向量中原先的内存 hw.clear(); //读家庭作业成绩,在这里不断循环读入,获取一个学生全部的homework输入 double x; while(in>>x) hw.push_back(x); //清除流以使输入动作对下一个学生有效 in.clear(); } return in; } ``` 注意:clear成员函数在为istream服务时所表现出来的行为特性跟它在为向量对象服务时是完全不同的。对于istream对象,它清除了所有的错误标记以使输入动作可以继续;而对于向量对象,它删除了向量中可能已经含有的全部内容,这样就会让我们再次拥有一个空的向量。 ## 三种函数参数 > **划重点!!!** 我们已经定义了3个对家庭作业向量进行操作的函数: 1、median函数 2、grade函数 3、read_hw函数 这3个函数中的每一个都会出于某种目的而去处理对应的参数,同时这3个函数所采取的处理方式都是互相不同的。 **<u>median函数——按值调用</u>** median 函数有一个 `vector<double>` 类型的参数。这会导致:对这个函数的调用会导致此参数被复制。这个类型确保了在不修改向量的情况下我们可以获得向量的中值。median会对它的参数进行排序(sort函数)。如果它不复制其参数的话,那么我们调用median(homework)的时候就会改变homework的值。但是就因为复制了(按值调用),所以不改变homework的值。 **<u>grade函数——const引用</u>** grade函数具有一个家庭作业向量。这个函数有一个`const vector<double>&` 类型的参数。在这个类型中,`&`让系统环境不用复制对应的参数,仅引用。同时`const`确保了程序将不会改变参数的值。使用这样的参数是提高程序效率的一种重要的手段。 无论何时,函数都不应该改变参数的值,而且对属于诸如`vector`或`string`这样类型的参数值的复制可能会消耗比较多的时间。一般而言,我们并没有必要为了诸如int或double这样简单的内部类型的参数而去使用`const引用`。因为对这类小对象的复制通常都是非常快的,因此在按值调用它们的时候,复制动作就算会有额外的开销,也只是很小的一部分。 **<u>read_hw函数——普通引用</u>** read_hw函数有一个类型为`vector<double>&`类型的参数——在这里并没有const。同样的,`&`让系统环境直接地把参数和参数连接起来,这样我们就可以避免对参数的复制了。不过在这里,**我们之所以要避免复制操作是因为函数要改变参数的值。** 思路为: - 直接传入参数类型(即按值调用)——不会改变参数的值 - const 引用——不会改变参数的值 仅仅普通引用,会改变参数的值。 与非常量引用参数**(const限定的就是常量引用参数)**对应的参数必须是左值——也就是说,它们必须是非临时对象(不然改变其值有什么意义呢?)。按值传递与一个常量引用连接在一起的参数可以取任何值。例如,假定我们有一个返回空向量的函数: ``` vector<double> emptyVec() { vector<double> v; //仅初始化,没有元素 return v; } ``` 函数声明: ``` double grade(double midterm, double final, const vector<double>& hw); istream read_hw(istream& in, vector<double>& hw); ``` 我们可以调用emptyVec函数并使用其结果作为grade函数的一个参数,如下: ``` grade(midterm,final,emptyVec()); ``` 在运行的时候,grade函数会抛出一个异常,因为它的参数为空,但是语法上这样的调用是完全合法的。因为第三个参数的位置`const vector<double>& hw`是常量引用。 但是在调用read_hw的时候,它的两个参数都必须是左值,因为它的两个参数都是非常量引用(未加const的引用)。那么如果我们给read_hw一个并不是左值的向量: ``` read_hw(cin,emptyVec()); //错误:emptyVec()不是左值 ``` 编译器就会提示出错,**因为我们在调用emptyVec时所创建的那个未命名的向量将会在read_hw返回时立即消失**。如果我们被允许进行这样的调用,那结果就是,我们把输入存进了一个我们无法访问的对象中。 **<u>什么是左值?</u>** 如果我们希望函数能够修改它的参数的值,那么在调用这个函数的时候,就不能在参数列表中使用任何的表达式。**相反,我们必须传递一个左值参数给引用参数。**左值是一个用来指示非临时对象的值。例如,如果一个变量是左值,那么同时它也可以是一个引用或者说可以是返回一个引用的函数的调用结果。一个产生算术值的表达式,例如`sum/count`,并不是左值,因为这个计算结果是临时的。 个人理解:举个例子,一个全局变量就不是一个左值。 **<u>重写计算成绩的程序</u>** ``` //include指令和对库工具的using声明 //优化后的median函数代码 //优化后的grade(double,double,double)函数代码 //优化后的grade(double,double,const vector<double>&)函数代码 //read_hw(istream&,vector<double>&)函数代码 int mian() { //请求并读入学生姓名 cout << "Please enter your first name: " ; string name; cin >> name; count << "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 = count.setprecision(); 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; } ``` **<u>使用异常的注意点</u>** 比较一下异常中这两种写法的区别:   原因:   # 0x03 组织数据 ## 把一个学生的所有数据放置在一起 我们需要找到一个方法来把所有属于一个学生的信息通通都存储在一个地方。这个所谓的地方应该是一个数据结构,它保存了学生的姓名、期中和期末考试成绩以及所有的家庭作业成绩: ``` struct Student_info { string name; double midterm; double final; vector<double> homework; }; //注意这里的分号是不可缺少的 ``` - `struct`是结构类型。 - 这个struct的定义表示,每一个`Student_info`类型的对象都是包含这四个数据成员的一个实例。因为`Student_info`是一种具有四个数据成员的类型。 ## 处理学生记录 现在使用了 struct 数据类型之后,我们需要解决的问题是: 1. 需要把数据读到一个 Student_info 类型的对象中; 2. 要为一个 Student_info 类型的对象生成总成绩; 3. 要对一个 Student_info 类型的向量进行排序。   ## 生成报表 ``` int main() { //students向量,每一个元素的类型为Student_info。相当于一些Student_info数据的集合,每个单独的数据都有那四个属性的数据 vector<Student_info> students; //Student_info 类型的数据record,记录每一次的读入拼成一个结构体 Student_info record; string::size_type maxlen = 0; //读并存储所有的记录,然后找出最长的姓名的长度 while(read(cin, record)) { //max函数属于algorithm头文件,给maxlen赋值 maxlen = max(maxlen, record.name.size()); students.push_back(record); } //按字母顺序排列记录 sort(students.begin(), students.end(), compare); //先初始化一个size_type类型的变量,便于后面使用setw函数 for(vector<Student_info>::size_type i =0;i != student.size();++i) { //输出姓名,填充姓名以达到maxlen+1的长度 cout << setw(maxlen+1) << students[i].name; //计算并输出成绩 try { double final_grade = grade(students[i]); streamsize prec = count.precision(); cout << setprecision(3) << final_grade << setprecision(prec); } catch(domain_error e) { count << e.what(); } cout << endl; } return 0; } ``` **<u>max函数</u>**  **<u>使用setw函数设置输出宽度</u>**  我们使用索引`i`来逐个处理students的元素。为了获取当前的Student_info元素,我们把索引写到students中,这样我们就取得了待输出的姓名。然后我们输出对象的name成员并使用`setw`来填充输出。 **<u>e.what()</u>**  # 0x04 独立编译多文件  **<u>组装median函数</u>**   **<u>在头文件中声明median函数</u>**   **<u>使用median函数</u>**  **<u>#ifndef指令</u>** > 注意:是 `#ifndef` 不是 `#ifdef`! 下面说的一切都是在头文件(`.h`)中定义的!   **<u>median.h及median.cpp</u>**   # 0x05 分块编译计算成绩的程序 **<u>组装Student_info结构和相关函数</u>**   对应的,我们会在一个名为 `Student_info.cpp` 的程序中给出这些函数的具体定义:  **<u>编写头文件声明不同的grade重载函数</u>**  注意:把这些重载函数的声明放在一起,会让这些可供选择的函数更易查找。因为这三个函数是密切相关的,所以我们将在同一个文件中定义它们。此外,这个具体定义函数的文件的名称将取决于系统环境,可能是`grade.cpp`、`grade.C` 或 `grade.c`。 `grade.cpp`的具体内容为:  # 0x06 优化程序设计之后的main函数 最后,我们就可以编写出完整的程序了。所以最终的程序结构为:  最终的`main`函数为:  # 0x07 本章小结 **<u>包含头文件</u>** - 标准头 - 用户定义头文件  **<u>#ifndef指令检查重复包含</u>**  > 个人理解中的所谓的「重复包含」就是:   **<u>类型</u>**  **<u>struct</u>**  **<u>函数</u>**   **<u>异常处理</u>** 第一种:try catch  如:  第二种:throw e;  如:  **<u>异常类</u>**  如:  **<u>库工具</u>** 
上一篇:
【C++进阶】01. C++基础
下一篇:
【Accelerated C++】课时3:使用批量数据
0
赞
273 人读过
新浪微博
微信
腾讯微博
QQ空间
人人网
提交评论
立即登录
, 发表评论.
没有帐号?
立即注册
0
条评论
More...
文档导航
没有帐号? 立即注册