0%

Book Reading: C++ Primer Plus, Chapter 7

Funtion Definition

The return value cannot be an array. Everything else is possible. (Interestingly, even though a C++ function can’t return an array directly, it can return an array that is part of a structure or object.)

Why need a function declaration prototype? For example, when the function finishes calculation, it places its return value at some specific location, perhaps in a CPU register or memory. Because the prototype states the return type, the compiler knows how many bytes to retrieve and how to interpret then.

Can’t the compiler search for how the function is defined? One problem with that approach is that it is not very efficient. The compiler would have to put compiling main() on hold while search the rest of file. The only way to avoid using a function prototype is to place the function definition before its first use. But that is not always possible.

The function prototype does not require that you provide name for the variable; a list of type is enough:

1
void cheers(int); // okay to drop variable names in prototype

The variable names in the prototype just act as placeholders, so if you do use names, they don’t have to match the names in the function definition.

Ellipsis in C++ allows the function to accept an indeterminate number of arguments. It is also known as the variable argument list. Ellipsis tells the compiler to not check the ype and number of the parameters the function should accept which allows the user to pass the variable argument list. Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
double average(int count, ...) {
// used to iterate on ellipsis.
va_list list;

// initialize position of va_list
va_start(list, count);

double avg = 0.0;
for (int i = 0; i < count; ++i) {
avg += static_cast<double>(va_arg(list, int)) / count;
}

// end use of va_list
va_end(list);

return avg;
}

Here is explanation:

  • va_list type is used to access teh values in the ellipsis. It will be conceptually easy for you if you think of ellipsis as an array. In that case, va_list will act as the iterator type. The va_list is not a special type. It is a macro definition.
  • va_start points to the va_list at the starting point of the ellipsis. It takes two arguments: va_list itself and the last normal parameter (non-ellipsis).
  • va_arg returns the value whih va_list is currently referring to and also moeves va_list to the next parameter. It also takes two arguments: va_list itself and the type of the parameter we are trying to access.
  • va_end takes only one argument: va_list itself. It is used to clean up the va_list macro.

In general, prototyping produces automatic type casts to the expected types. (Funciton overloading can create ambiguous situation, however, that prevents some automatic type casts, see Chapter 8.) Automatic type coversion doesn’t head off all possible errors. For example, the conversion result type cannot hold the data, which results in data loss. Also prototype results in type conversion only when it makes sense. It won’t, for example, convert aninteger to a structure or pointer.

Prototyping takes place during compiler time and is termed static type checking. Static type checking, as you’ve just seen, catches many errors that are much more difficult to catch during runtime.

Functions and Arrays

You can pass either the array type or pointer to function to process the array:

1
2
int sum_arr(int arr[], int n);
int sum_arr(int *arr, int n);

It turns out that both headers are correct because in C++ the notation int *arr and int arr[] have the identical meaning when (and only when) used in a function header or function prototype.

C++ by default pass argument value, and this is still true when you pass array or pointer. The function still passes a value that’s assigned to a new variable. But that value is a single address, not the content of the array.

You can declare pointer points to constant data. This means that you can’t use it to change the data:

1
void show_array(const double arr[], int n);

In this case show_array treats the array as read-only data.

But things will be tricky when we combine pointers and const. There are two ways to use const with pointer:

  1. Make a pointer point to a constant object, and that prevents you from using the pointer to change the pointed-to value.
  2. Make the pointer itself constant, and that prevents you from changing where the pointer points.

To declare a pointer that points to a constant:

1
2
3
4
5
6
7
int age = 39;
const int *pt = &age;

*pt += 1; // invalid
cin >> *pt; // invalid

int *pm = pt; // invalid

This declartion doesn’t necessarily mean that the value it points to is really a constant; it just means the value is a constant insofar as pt is concerned. C++ prohibits you from assigning the address of a const to a non-const pointer. (unless use type cast to override it, e,g. const-cast)

You can assign the address of either const data or non-const data to a pointer-to-const, provided that the data type is not itself a pointer, but you can assign the address of non-const data only to a non-const pointer.

The above declaration doesn’t prevent you from chaning the value of pt itself. That is, you can assign a new address to pt:

1
2
int sage = 80;
pt = &sage; // okay to point to another location

To make it impossible to change the value of the pointer itself:

1
2
3
int sloth = 3;
const int *ps = &sloth; // a pointer to const int
int* const pt = &sloth; // a const pointer to int

Also if you like, you can declare a const pointer to a const object:

1
2
double trouble = 2.0E30;
const double* const stick = &trouble;

Functions and Two-Dimensional Arrays

Suppose, for example, that you start with the code to sum a 2D array:

1
2
int data[3][4] = {{1, 2, 3, 4}, {9, 8, 7, 6}, {2, 4, 6, 8}};
int total = sum(data, 3);

What should the prototype for sum() look like? Well, data is the name of an array with three elements. The first element is, itself, an array of four int values. Thus the type of data is pointer-to-array-of-four-int, so prototype would be this:

1
int sum(int (*ar2)[4], int size);

Or an alternative format:

1
int sum(int ar2[][4], int size);

Pointer to Functions

Funcitons, like data items, have addresses. A function’s address is the memory address at which the stored machine language code for the function begins.

Obtaining the address of a function is simple. You just use the function name without trailing parenthesis. That is, if think() is a function, then think is the address of the function. To pass a function as an argument, you pass the function name.

A pointer to a function has to specify to what type of function the pointer points. This means the declaration should identify the function’s return type and the function’s signature, for example:

1
double (*pf)(int); // points to function takes int and returns double

Note that parentheses have a higher precedence than * operator, so *pf(int) means pf() is a function that returns a pointer, whereas (*pf)(int) means pf is a pointer to a function:

1
2
double (*pf)(int); // function pointer
double *pf(int); // function returns a pointer

To invode a function by using function pointer, all you have to do is use (*pf) as if it were a function name:

1
double y = (*pf)(5);

Actually, C++ allows you to use pf as if it were a function name:

1
double y = pf(5);

Things could become complicated when putting them together, for example, to define an array of function pointers:

1
const double* (*pa[3])(const double*, int);

This defines an array of 3 pointers which point to function taking const double* and int as input and returns const double*.

But why put [3] there? Well, pa is an array of three things, and the starting point for declaring an array of three things is this:pa[3]. The rest of declaration is about what kind of thing is to be placed in the array. Remember operator precedence ranks [] higher than *, so *pa[3] says pa is an array of three pointers.

Can we use auto here like this?

1
auto pa[3] = {f1, f2, f3};

No, automatic type deduction works with a single initializer value, not an initialization list.

To get the address of array of function pointers, can simply use auto:

1
auto pc = &pa;

Or if you prefer to you by yourself:

1
const double* (*(*pd)[3])(const double*, int) = &pa

Remember the inner (*pd)[3] represent the pointer to an array of 3 elements, the rest of the declaration is just aboue what is stored in the array.

To call funciton with pd, you can do either way of these:

1
2
(*pd)[i](av, 3);
*(*pd)[i](av, 3);

Well, this complicated definition make it quite intimidating, but we get typedef to simplify it:

1
2
3
4
5
typedef const double* (*p_fun)(const double*, int); // p_fun now a type name
p_fun p1 = f1; // p1 points to f1 function

p_fun pa[3] = {f1, f2, f3} // pa an array of 3 function pointers
p_fun (pd*)[3] = &pa; // pd points to an array of 3 function pointers