• No se han encontrado resultados

We shall begin by writing a function to calculate a student's final grade from the midterm and final exam grades and overall homework grade. We'll assume that we've already calculated the overall homework grade from the individual homework grades, which we have been computing as the average or median. Aside from that assumption, this function will use the same policy as the one that we've been using all along: Homework and the final exam contribute 40% each to the total, and the midterm makes up the remaining 20%.

Whenever we do—or might do—a computation in several places, we should think about putting it in a function. An obvious reason for doing so is that then we can use the function instead of redoing the computation explicitly. Not only does using functions reduce our total programming effort, but doing so also makes it easier for us to change the

computation if we wish. For example, assume we wanted to change our grading policy. If we had to hunt through every program we had ever written, looking for the parts that dealt with grading, we would probably become discouraged quickly.

There is a more subtle advantage to using functions for such computations: A function has a name. If we name a computation, we can think about it more abstractly—we can think more about what it does and less about how it works. If we can identify important parts of our problems, and create named pieces of our programs that correspond to those parts, then our programs will be easier to understand and the problems easier to solve.

Here is a function that computes grades according to our policy:

// compute a student's overall grade from midterm and final exam grades and homework

grade

double grade(double midterm, double final, double homework) {

return 0.2 * midterm + 0.4 * final + 0.4 * homework; }

Until now, all the functions that we've defined have been named main. We define most other functions similarly, by specifying the return type, followed first by the function name, next by a parameter list enclosed in ( ), and, finally, by the function body, which is enclosed in { }. The rules are more complicated for functions that return values that denote other functions; see §A.1.2/297 for the full story.

In this example, the parameters are midterm, final, and homework, each of which has type double. They behave like variables that are local to the function, which means that calling the function creates them and returning from the function destroys them.

As with any other variables, we must define the parameters before using them. Unlike other variables, defining them does not create them immediately; only calling the function creates them. Therefore, whenever we call the function, we must supply corresponding arguments, which are used to initialize the parameters when the function begins

execution. For example, in §3.1/36 we computed a grade by writing

cout << "Your final grade is " << setprecision(3) << 0.2 * midterm + 0.4 * final + 0.4 * sum / count << setprecision(prec) << endl;

If we had the grade function available, we could have written

cout << "Your final grade is " << setprecision(3) << grade(midterm, final, sum / count)

<< setprecision(prec) << endl;

Not only must we supply arguments that correspond to the parameters of the functions that we call, but we must supply them in the same order. Accordingly, when we call the grade function, the first argument must be the midterm grade, the second must be the final exam grade, and the third must be the homework grade.

Arguments can be expressions, such as sum / count, not just variables. In general, each argument is used to initialize the. corresponding parameter, after which the parameters behave like ordinary local variables inside the function. So, for example, when we call grade(midterm, final, sum / count), the grade function's parameters are initialized to copies of the arguments' values, and do not refer directly to the arguments themselves. This behavior is often called call by

value, because the parameter takes on a copy of the value of the argument.

4.1.1 Finding medians

Another problem that we solved in §3.2.2/46, and that we can imagine wanting to solve in other contexts, is finding the median of a vector. We'll see in §8.1.1/140 how to define a function that is so general that it works with a vector of any type of value. For now, we'll limit our attention to vector<double>.

To write our function, we'll start with the part of the program in §3.2.2/47 that computes medians, and make a few changes:

// compute the median of a vector<double>

// note that calling this function copies the entire argument 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]; }

One change is that we named our vector vec, rather than homework. After all, our function can compute the median of anything, not just homework grades. We also eliminated the variable median, because we no longer need it: We can return the median as soon as we've calculated it. We are still using size and mid as variables, but now they are local to the median function and, therefore, inaccessible (and irrelevant) elsewhere. Calling the median function will create these variables, and returning from the function will destroy them. We define vec_sz as a local type name, because we don't want to conflict with anyone who might want to use that name for another purpose.

The most significant change is what we do if the vector is empty. In §3.2.2/46, we knew that we would have to complain to whoever was running our program, and we also knew who that person would be and what kind of complaint would make sense. In this revised version, on the other hand, we don't know who is going to be using it, or for what purpose, so we need a more general way of complaining. That more general way is to throw an exception if the vector is empty.

When a program throws an exception, execution stops in the part of the program in which the throw appears, and passes to another part of the program, along with an exception object, which contains information that the caller can use to act on the exception.

The most important part of the information that is passed is usually the mere fact that an exception was thrown. That fact, along with the type of the exception object, is usually enough to let the caller figure out what to do. In this particular example, the exception that we throw is domain_error, which is a type that the standard library defines in header <stdexcept> for use in reporting that a function's argument is outside the set of values that the function can accept. When we create a domain_error object to throw, we can give it a string that describes what went wrong. The program that catches the exception can use this string in a diagnostic message, as we shall see in §4.2.3/65.

There is one more detail about how functions behave that is important to understand. When we call a function, we can think of the parameters as local variables whose initial values are the arguments. If we do so, we can see that calling a function involves copying the arguments into the parameters. In particular, when we call median, the vector that we use as an argument will be copied into vec.

In the case of the median function, it is useful to copy the argument to the parameter, even if doing so takes significant time, because the median function changes the value of its parameter by calling sort. Copying the argument prevents changes made by sort from propagating back to the caller. This behavior makes sense, because taking the median of a vector should not change the vector itself.