Home > Articles > Programming > C/C++

Understanding C++ Program Structure

  • Print
  • + Share This
Learn how C++ program structure works, from the perspective of functions and data. This sample chapter covers function definitions, inline functions, recursion, pointers to functions, and functions with default arguments. It also describes structures and unions, member functions, and passing structures and unions as arguments. Additionally, you'll learn about C++ storage classes, exceptions, namespaces, and free store memory.
This chapter is from the book

This chapter examines C++ program structure from the perspective of functions and data. We begin with function definitions, inline functions, recursion, pointers to functions, and functions with default arguments. To organize data for functions to access, we present structures and unions, member functions, and how to pass structures and unions as functions arguments efficiently with references. We also look at C++ storage classes, exceptions, and namespaces. Lastly, we discuss the run-time allocation and release of free store memory, using the C++ operators new and delete.

3.1 Functions

Functions group program statements into logical units. We do this to encapsulate actions that functions perform. Encapsulation is important because it separates and hides a function's programming details from its caller. With libraries of functions that provide separate compilation and linking of modules, we can call functions from anywhere in a program.

As with variables, you must define or declare functions before you call them. C++ functions have two formats. The first format is a function declaration, often called a function prototype.

 Type function_name(Type arg1, Type arg2, Type argN); 

Function prototypes give the compiler all the information it needs to verify the correctness of a function call: the function's name, its return type, the number of arguments, and each argument's type. A comma-separated list of types and argument names is a function's signature. In a signature, each Type may be distinct or the same as another one, and a function's return Type may be void or any built-in or user-defined type. A prototype whose signature is either empty or void designates a function with no arguments. Function prototypes must always precede function calls.

Function signatures specify unique functions, much like handwriting signatures identify people. Here are several examples of function prototypes.

 char *memcat(char *p, const char *q);   // legal
 void init(void);             // legal
 double getvalue();            // legal, (void) optional
 long fellow(int n, m);          // illegal, no type for m
 double area(double);           // legal, arg name optional
 input(char *);              // illegal, no return type

If a function's return type is void, the compiler reports compilation errors when you try to use its return value in a program. The argument names following their data types (p and q, for example) are optional in function prototypes, but a type must appear for each argument (as in long fellow(int n, int m)). Function prototypes with no arguments either use empty parentheses (as in getvalue()) or use the keyword void (as in init(void)).

NOTE

Earlier versions of C++ allowed an "implicit int" for function return types. Implicit int is no longer allowed (for function return types or variable declarations). Use void for functions that do not return a value.

The second format is a function definition.

 Type function_name(Type arg1, Type arg2, Type argN)
 {
  function body
 }

The first line has the same format as a function prototype but without the terminating semicolon. The rest of the definition (function body) is a block enclosed with { and }. Signatures that are not void must specify a type and a name for each argument. The scope of an argument (where it's valid) is inside the body of a function. Any C++ statement or construct is legal within the body of a function except another function definition.

NOTE

In files where you place function definitions before function calls, function prototypes are unnecessary because the first line of the function definition serves as the prototype. Be aware of forward referencing problems, however, as the following shows.

 void g()
 {
  f();        // call function f()- error
  . . .
 }

 void f()
 {
  g();        // call function g()
  . . .
 }

A compilation error occurs where g() calls f() because the compiler has not seen a prototype for f(). Moving the function definition for f() before g() doesn't solve the problem, since f() now calls g() without a prototype. In this situation, a function prototype for f() or g() is necessary (void f(); for example) and must appear before its call.

Here's the function definition for area(), whose prototype appears earlier.

 double area(double radius) {
  const double pi = 3.14159;
  return pi * radius * radius;
 }

Note that area() uses a return statement to report a double result back to the caller. The formats for return are

 return;
 return (expression);
 return expression;

A return statement by itself appears only in functions whose return type is void. The parentheses are optional in return statements with expression. (We prefer to omit parentheses to improve readability and to cut down on typing.)

Our first example of a function is main(), where C++ programs begin execution. The formats are

 int main() { statements; }
 int main(int argc, char *argv[]) { statements; }

The first format works with programs that you execute with only a program name. The second format handles programs with character string command-line arguments. The second format makes each argument and the total number of command-line words available to your program. The variable argc is an integer equal to the number of words on the command line, including the program name (argc is always at least one). The variable argv is a pointer to an array of pointers to characters. Each pointer in the array points to the words on the command line. The first pointer, which is argv[0], points to the program name.

NOTE

The following format for main() is safer with programs that have command-line arguments.

 int main(int argc, const char *argv[]) { statements; }

With const, argv now points to an array of pointers to constant characters. This arrangement prevents accidental modifications to command-line arguments inside main().

Suppose, for example, we execute a C++ program called com, as follows.

 $ com -r file 

The first command-line word is com, the name of the program (pointed to by argv[0]). The second word is -r (presumably a program option). The third command-line word is a file name called file. Figure 3.1 shows the memory layout.

Figure 3.1Figure 3.1. Command-line arguments memory layout

Each element of the pointer array is a pointer to a NULL terminated (\0) character string from the command line. The last element of this pointer array is 0.

We use argv to access both characters and words from the command line, as Listing 3.1 shows.

Listing 3.1 Command-line arguments

 // com.C - command-line arguments
 #include <iostream.h>

 int main(int argc, const char *argv[])
 {
  if (argc != 3) {
    cerr << "Usage: " << argv[0] << " option filename" << endl;
    return 1;
  }
  cout << argv[1] << '\t' << argv[2] << endl;
  cout << argv[1][0] << '\t' << argv[1][1] << endl;
  return 0;
 }

 $ com
 Usage: com option filename

 $ com -r file
 -r  file
 -   r

The if statement verifies that com has two arguments (three words on the command line). If not, we use cerr to display a usage message on standard error and return a nonzero status (1) to the operating system.

NOTE

Always use argv[0] and not your program name (for example, com) in usage messages with cerr statements. This ensures that error messages display the correct program name, even if you rename your file.

The notation argv[i] is a pointer to the i'th word (starting at 0) on the command line. Likewise, argv[i][j] is the j'th character of the i'th command-line word (both indices start at 0). We return 0 (normal status) at the end of the program.

NOTE

The Basic Rule (page 40) helps explain the two-dimensional array notation with argv.

 argv[i][j] = *(argv[i] + j)  // equivalent expressions 

The first index is a command -line word, and the second index is a character in the word. Referencing a character from a command-line word is a character pointer offset from the beginning of the word followed by a character pointer dereference. This two-dimensional array notation for argv is useful because it does not modify argv and it's easy to remember (compare argv[1][0] to its equivalent notation **++argv, for example).

Our next example is a C++ program with function prototypes and definitions for our own functions.

Listing 3.2 Function prototypes and definitions

 // func.C - function prototypes and definitions
 #include <iostream.h>
 #include <stdlib.h>

 float f(float);           // outside function prototype

 int main(int argc, const char *argv[])
 {
  void g(float *);         // inside function prototype

  if (argc != 2) {
    cerr << "Usage: " << argv[0] << " number" << endl;
    return 1;
  }
  float m = atof(argv[1]);     // convert command-line number

  m = f(m);             // call by value
  cout << m << endl;

  g(&m);              // call by address
  cout << m << endl;
  return 0;
 }

 float f(float val)          // pass value
 {
  return val + 2;          // return value
 }
 void g(float *pf)           // pass address
 {
  *pf += 2;             // modify argument
 }

 $ func 6.5
 8.5
 10.5

The program displays a usage message on standard error if you execute it without a command-line argument. The prototype for atof(), an ASCII-to-float library routine, appears in the standard header file <stdlib.h>. Calls to function f() pass a floating point value, and calls to function g() pass a pointer to a float. Inside f(), we return a float that's two greater than the argument's value. We then use an assignment statement in the main program to modify m. Function g(), on the other hand, modifies argument m directly (updating it by 2) through the pointer argument. The output verifies that m changes with either approach (call by value followed by assignment or call by address with no assignment).

The function prototypes for f() and g() are forward references that direct the compiler's actions and provide type checking for the function calls. Note that a function prototype may appear inside or outside a function's definition. Now consider what happens when we add the following statements to this program.

 int n = atoi(argv[1]);         // convert command line number
 m = f(n);               // legal - convert int to float
 g(&n);                 // illegal - int *, not float *

The function atoi(), an ASCII-to-integer library routine, converts the command-line number to an integer. We pass this integer value to f() and assign the function's return value to m because C++ conversion rules allow int-to-float conversions, and vice versa. Conversion rules, however, do not apply to pointers. Hence, the compiler reports a compilation error when we call g() with the address of an int instead of the address of a float.

NOTE

Place your function prototypes in header files (.h files) and use #include statements in files where you call the functions. This centralizes the interface for all modules that call your functions and makes it easier to maintain and modify programs.

Inline Functions

C++ functions typically use a run-time stack to push arguments and return values. Programs that call functions repeatedly may therefore perform slowly. In these situations, the keyword inline can make function calls execute faster. The formats are

 inline Type function_name(Type arg1, Type arg2, Type argN);

 inline Type function_name(Type arg1, Type arg2, Type argN)
 {
  function body
 }

The compiler inserts function body code where you call an inline function. Inline functions, therefore, eliminate the code to push arguments and return values from the run-time stack. A program's performance improves (no function call overhead), but its size increases (each call inserts code from the body of an inline function).

Chapter 2 introduces macros that eliminate function call overhead with textual substitution (see "Macros" on page 51). Macros, however, are not type safe. Inline functions, on the other hand, give you the type safety of functions with the performance of macros. The compiler performs type checking with inline function arguments, and debuggers can trace program execution and set breakpoints. Furthermore, pointers to inline functions are legal. None of these features apply to macros.

Macros also have problems that do not apply to inline functions. To illustrate, let's contrast an inline function with a macro. The following MAX macro, for instance, determines a maximum from its two arguments. What happens when we use a ++ operator with one of its arguments?

 #define MAX(A, B) ((A) > (B) ? (A) : (B))

 int i = 8, j = 5;
 cout << MAX(i++, j) << endl;    // displays 9 (incorrect)

With a post increment (i++), the cout statement should display 8, but instead displays 9. The preprocessor output after textual substitution reveals the problem.

 cout << ((i++) > (j) ? (i++) : (j)) << endl; 

This expression evaluates incorrectly because variable i increments twice. In general, we can't use operators ++ or -- with macro arguments because textual substitution generates side effects.

Now let's convert MAX to an inline function for integers called max() and compare it to our macro version.

 inline int max(int a, int b) { return a > b ? a : b; }

 int i = 8, j = 5;
 cout << max(i++, j) << endl;    // displays 8 (correct)

The compiler inserts the code from the return statement in max() directly where we call the function. We pass the value of i before we increment it, resulting in 8, the correct answer.

Side effects are not the only problem with macros. Pointer arguments, for instance, compile with macro MAX, and the result may not be what you want. Inline function max(), on the other hand, generates compilation errors with pointer arguments.

NOTE

Specifying inline is only a request to make a function inline. If the body of an inline function is large or contains loops, your compiler may ignore an inline request and use the run-time stack anyway. For this reason, you should investigate how your compiler handles inline functions before you use them extensively. Profiling your application should identify which functions are good candidates for inlining.

Although we were successful in rewriting macro MAX as an inline function, it's not always possible to convert macros to inline functions. Consider what happens with the NELEMS macro (Listing 2.2 on page 76), which computes the number of elements in any array at compile time.

 #define NELEMS(A) (sizeof(A) / sizeof A[0])

 int buf[] = { 1, 3, 5, 7, 9 };
 cout << NELEMS(buf) << endl;         // displays 5

The NELEMS macro uses the sizeof operator to divide the size of an array by the size of its first element, resulting in the number of items in the array. The cout statement correctly displays 5, the number of elements in array buf. Now let's convert the NELEMS macro to an inline function called nelems() for integers and examine the result.

 inline int nelems(int a[]) { return sizeof(a) / sizeof a[0]; }

 int buf[] = { 1, 3, 5, 7, 9 };
 cout << nelems(buf) << endl;          // displays 1

The cout statement displays 1, which is incorrect. Why? When you pass the name of an array as an argument to a function, the compiler uses a pointer (int *a, or int a[], in this case). Therefore, nelems() divides the size of a pointer by the size of the first element of the array. (This is the same size on most machines, resulting in 1.) Not only does nelems() fail to work, it's not even as general as the macro version (NELEMS works for any array data type, whereas nelems() works only for integer arrays).

Despite isolated cases like NELEMS, we still recommend inline functions over macros. Judicious use of inline functions can make C++ programs perform well in most cases.

NOTE

Place inline function definitions (with function code statements) in header (.h) files and #include them where you call the functions. This allows the compiler to insert the code at the function call, since the compiler sees the function definition (as well as the function code statements) before the function call. Inline function definitions that appear in separate files for compilation (not .h files), may produce link errors because the compiler cannot insert inline code at a function call from a compiled linkable module.

Recursive Functions

C++ functions may call themselves. Depending on the algorithm, recursive functions can simplify designs. Recursive functions, on the other hand, use the run-time stack extensively, making some programs run slowly.

Suppose, for example, we need to write a function to convert integers to hexadecimal character strings for display. Listing 3.3 is a program that calls a recursive function to perform the conversion.

Listing 3.3 Recursive integer-to-hexadecimal conversion

 // itoh.C - recursive integer-to-hex conversion
 #include <iostream.h>
 #include <stdlib.h>

 int main(int argc, const char *argv[])
 {
  unsigned int num;
  void itoh(unsigned int);

  if (argc == 1) {
    cerr << "Usage: " << argv[0] << " number" << endl;
    return 1;
  }
  num = atoi(argv[1]);
  itoh(num);           // conversion routine
  cout << endl;
  return 0;
 }

 void itoh(unsigned int n) {
  int digit;
  const char *hex = "0123456789abcdef";

  if ((digit = n / 16) != 0)
    itoh(digit);        // recursive call
  cout << hex[n % 16];
 }

 $ itoh 23456
 5ba0

The program expects one number on the command line, which it converts to an unsigned integer with atoi(). Otherwise, a cerr statement displays a usage message on standard error. The algorithm for the recursive function itoh() uses a "divide down" approach, converting the number (n) to a digit by dividing the number by 16. Each time itoh() calls itself, the run-time stack allocates new memory locations for digit and hex. Recursion continues until the division yields 0. At this point, the run-time stack "unwinds," and successive cout statements display the characters one at a time. The algorithm uses the remainder from a modulus operation (%) as an offset into a character string (hex) to convert an integer number between 0 and 15 to the ASCII characters '0' to '9' and 'a' to 'f'.

The itoh() function is short and simple because the recursive design easily implements the algorithm. Nonrecursive solutions are possible but not always as straightforward (see Exercise 1 on page 165).

NOTE

Can recursive functions be inline? Consider inlining the following fact() function that calls itself recursively to calculate a factorial result.

 inline unsigned int fact(unsigned int n) {
  return (n <= 1) ? 1 : n * fact(n-1);
 }

Since the number of recursions is unknown until run time, the compiler cannot generate the inline code at compile time. Most compilers inline only the first call, if any. Avoid inlining recursive functions and watch out for infinite recursion (where a function calls itself forever, analogous to infinite loops with constructs).

Pointers to Functions

C++ pointers may point to your own functions or library functions. Pointers to functions are different than other pointer types because the addresses they contain typically reside in the code segment (often called the text area). The format for a pointer to a function is

 Type (*pfname)(Type arg1, Type arg2, Type argN); 

where pfname is a pointer to a function whose signature is (Type arg1, Type arg2, Type argN) and whose return type is Type. We need the parentheses surrounding the indirection operator and pointer (*pfname); otherwise, pfname is a function (not a pointer) that returns a pointer to Type. Figure 3.2 shows an example of a pointer to a function.

Figure 3.2Figure 3.2. Pointers to functions

Once you define a function pointer, you assign it to the name of a function with the same signature you specify in your function pointer prototype. Here are several examples.

 int help(short, const char *);   // function prototype
 double game(int);         // function prototype
 int (*p)(short, const char *);   // pointer to function, returns int
 double (*q)(int) = game;      // initialize pointer to game()
 p = help;             // assign address to pointer p
 q = help;             // illegal - signatures don't match

Assigning help()'s address to p is legal because their signatures match. We cannot, however, assign help()'s address to q because q points to a function with a different signature.

NOTE

When you assign a function's address to a pointer, don't use parentheses; use only the function's name. Parentheses following function names result in function calls and typically generate compilation errors.

You may initialize a function pointer on the same line as the function prototype declaration, as in

 double game(int), (*q)(int) = game;      // legal 

There are two ways to call functions with initialized function pointers.

 (*pfname)(arg1, arg2, argN);       // with dereference
 pfname(arg1, arg2, argN);        // without dereference

The indirection operator in the first format dereferences the pointer (parentheses are necessary). In the second format, the compiler calls the function with a pointer name (instead of a function name). To illustrate, suppose you want to call game() with a function pointer q. Either one of the following statements does the job.

 cout << (*q)(30);     // call game(30), return double
 cout << q(30);      // call game(30), return double

A common application of pointers to functions is dispatch tables, or arrays of pointers to functions. With a dispatch table, an index or simple lookup scheme calls a function from a table entry. The following program uses a dispatch table of mathematical functions from the math library. For simplicity, we restrict each table entry to a pointer to a math function that takes one double argument.

Listing 3.4 Dispatch table

 // calc.C - dispatch table for math functions
 #include <iostream.h>
 #include <stdlib.h>
 #include <string.h>
 #include <math.h>

 #define NELEMS(A) (sizeof(A) / sizeof A[0])

 struct math {
  const char *name;       // name of math function
  double (*pmf)(double);     // ptr to math function
 };

 math mtab[] = {          // dispatch table
  "sin", sin,
  "cos", cos,
  "exp", exp,
  "tan", tan,
  "sqrt", sqrt
 };

We build the dispatch table with two pointers, one that points to the name of a math function and one that points to the address of a math function with one double argument. Here is the rest of the program, which accesses the dispatch table and calls the appropriate math function.

Listing 3.5 Main program with dispatch table

 int main(int argc, const char *argv[])
 {
  if (argc != 3) {
    cerr << "Usage: " << argv[0] << " math_func double" << endl;
    return 1;
  }

  int mf;
  for (mf = 0; mf < NELEMS(mtab); mf++)       // lookup table
    if (strcmp(argv[1], mtab[mf].name) == 0)
      break;

  if (mf == NELEMS(mtab)) {         // found it?
    cerr << "math_func " << argv[1] << " not found" << endl;
    return 1;
  }

  double arg = atof(argv[2]);        // convert the argument
  cout << mtab[mf].pmf(arg) << endl;     // call function, display
  return 0;
 }

The NELEMS macro determines the number of elements in dispatch table mtab. If we do not find a function name from the command line that matches a table entry, we display an error message. Otherwise, we break from the for loop when we find a matching name. The statement

 cout << mtab[mf].pmf(arg) << endl;    // call function, display 

calls the appropriate math function from the dispatch table. Here are some sample runs.

 $ calc sqrt 81
 9
 $ calc exp 3
 20.0855

NOTE

Typedefs (page 43) make working with function pointers easier. The following typedef definition, for example, eliminates the signature and pointer notation from a function pointer definition.

 typedef void (*Pfunc)(void *, int);   // ptr to void function

 void gather(void *, int);        // void function
 Pfunc pf = gather;           // initialize pointer
 

The Right-Left Rule

It's not always easy to decipher declarations that have pointers to functions and arrays in C++. Consider the following.

 block *(*g())[10];     // what does this mean? 

A technique called the Right-Left Rule1<span class="docEmphasis"></span> helps you interpret advanced declarations like this. In C++, a declaration may include various combinations of pointers, arrays, and functions. To parse such declarations, the compiler has to sort out the attributes listed in Table 3.1.

Table 3.1. Attributes

Attribute

Meaning

()

function

[]

array

*

pointer


For simplicity, we assume void signatures for function attributes. Recall that () and [] evaluate left-to-right and have a higher precedence than * (see "C++ operator precedence" on page 776). This means the declarations in Table 3.2 are legal.

Table 3.2. Legal attribute combinations

Attributes

Meaning

*()

function returns pointer

(*)()

pointer to function

*[]

array of pointers

(*)[]

pointer to an array

[][]

array of array

**

pointer to pointer


Of course, we can extend these concepts to even greater complexity. For example, *[][] is a two-dimensional array of pointers. The fundamental attributes for pointers (*), arrays ([]), and functions (()), however, are all we need to derive more complex combinations.

NOTE

Make sure you place parentheses around the indirection operator (*) to declare pointers to functions or pointers to arrays because * has a lower precedence than () and [].

Not all combinations of these attributes are legal. Table 3.3 lists some illegal ones.

Table 3.3. Illegal attribute combinations

Attributes

Meaning

()[]

function returns array

[]()

array of functions

()()

function returns function


You may, however, have functions that return pointers to arrays, arrays of function pointers, and functions that return pointers to functions.

NOTE

When combining attributes, you must use an intervening parenthesis between function () and array [] attributes. Otherwise, you're building an illegal combination in Table 3.3, such as an array of functions or a function that returns an array.

Table 3.4 lists the English keywords for the attributes, followed by the Right-Left Rule.

Table 3.4. Attributes and Keywords

Attribute

English Keyword

()

function returns

[n]

array of n

*

pointer to


Right-Left Rule:

  1. Start with the identifier.

  2. Look to the right for an attribute.

  3. If none is found, look to the left.

  4. If found, substitute English keyword.

  5. Continue right-left substitutions as you work your way out.

  6. Stop when you reach the data type in the declaration.

This rule applies to any C++ declaration or definition, simple or complicated. Let's try it with some simple definitions first.

 unsigned long buf[10];   // buf is an array of 10 unsigned longs
 unsigned long *p[2];    // p is an array of 2 pointers to
               // unsigned longs

In the first example, buf is the identifier. Look to the right for the [] attribute. We stop at the data type (unsigned long). Therefore, buf is an array of 10 unsigned longs. In the second example, p is the identifier. Look to the right for the [] attribute, then to the left for the *. This means p is an array of 2 pointers to unsigned longs.

Remember that multidimensional arrays use the [] attribute more than once.

 data *ps[3][4];      // ps is a 3 by 4 array of pointers to data 

In this case, continue to the right until you've run out of []'s before you look left. Here, ps is an array of 3 arrays of 4 pointers to type data. This, of course, is better understood as a 3 by 4 array of pointers to type data.

Now let's try a more complicated declaration.

 int (*pa[5])();       // pa is an array of 5 pointers to functions
               // that return integers

pa is the identifier. Looking right, we find that pa is an array. Looking left, we see that it's an array of pointers. Looking right, we discover each array element is the address of a function that returns an integer. This means pa is an array of 5 pointers to functions that return integers.

NOTE

The parenthesis between the [] and () attributes is necessary.

 int *pa[5]();     // illegal 

Without it, pa is an array of 5 functions that return pointers to integers, which is not a legal declaration (see Table 3.3).

A small change to the original declaration creates a slightly different one.

 int *(*pa[5])();      // pa is an array of 5 pointers to functions
              // that return pointers to integers
 

Now let's apply the Right-Left Rule to decipher the declaration from the start of this section.

 block *(*g())[10];     // g is a function that returns a pointer
              // to an array of pointers to block
 

g is the identifier. Looking right, we know that g is a function. Looking left, we see that this function returns a pointer. Looking right, we find that the function returns a pointer to an array. Looking left once more, we discover that each array element is a pointer to type block. Therefore, g is a function that returns a pointer to an array of 10 pointers to type block. Again, note that the parentheses between the () and [] attributes are necessary.

The Right-Left Rule also helps you construct C++ declarations from English sentences. See if you agree with the following steps for an array of 10 pointers to functions that return pointers to integers. Start with the name of the array (buf) and place the first attribute on the right.

 buf[10]        // buf is an array of 10
 *buf[10]        // buf is an array of 10 pointers
 (*buf[10])()      // buf is an array of 10 pointers to functions
 int *(*buf[10])();   // buf is an array of 10 pointers to functions
            // that return pointers to integers

Arrays as Function Arguments

The Basic Rule (page 40) explains the relationship between arrays and pointers and how array subscripts and pointer expressions are equivalent. Another part of C++ where arrays and pointers play a major role is with function arguments. Instead of passing entire arrays to functions (which is inefficient), the compiler passes only their addresses. You cannot, therefore, pass arrays by value in C++.

When you pass the name of an array as an argument to a function, the compiler actually passes a pointer. To demonstrate, let's investigate function prototypes for one-dimensional arrays.

 void f(int *);             // 1D function prototype
 void f(int []);             // 1D equivalent prototype

 int a[10];               // 1D array of 10 integers
 f(a);                  // call function with 1D array

 void f(int *p)  { ... p[i] ... }    // 1D function definition
 void f(int p[]) { ... p[i] ... }    // 1D equivalent definition

In prototypes with one-dimensional arrays, [] and * are synonymous. You may use either [] (array notation) or * (pointer notation) in the corresponding function definition. The length of a one-dimensional array ([10], in this example) is not necessary in either a function prototype or a definition. The Basic Rule explains why.

 p[i] = *(p + i)          // 1D pointer expression 

Inside function definitions with one-dimensional array arguments, the compiler resolves array subscripts with an offset (i) from the array's starting address (p). Even if you include an array's length inside square brackets (int p[10], for instance), the compiler ignores it.

When you pass the name of a multidimensional array as an argument to a function, the compiler still passes a pointer, but the notation changes. Let's start with two-dimensional arrays.

 void f(int (*)[10]);       // 2D function prototype
 void f(int [][10]);       // 2D equivalent prototype

 int a[5][10];          // 2D array of 5 x 10 integers
 f(a);              // call function with 2D array

 void f(int (*p)[10]) {     // 2D function definition
  ... p[i][j] ...        // 2D array subscript
 }

 void f(int p[][10]) {      // 2D equivalent definition
  ... p[i][j] ...        // 2D array subscript
 }

With two-dimensional array arguments in function prototypes and definitions, the first size (5) is optional but the second size (10) is not. You must include parentheses (*) with pointer notation, and the second size must be a constant integer expression. To help you understand this notation and why you must follow these rules, let's discuss how the compiler builds two-dimensional arrays and resolves p[i][j] array subscripts.

For two-dimensional arrays, the first size is the number of rows and the second size is the number of columns.

 int a[5][10];           // 5 rows by 10 columns 

The compiler allocates a block of memory that holds 50 (5 times 10) integers. When you use two-dimensional array subscripts (a[i][j]), the compiler generates the following pointer expression to locate array elements.

 *(&a[0][0] + 10 * i + j)       // 2D storage map equation 

We call this pointer expression a storage map equation. Every multidimensional array in a program has one. Storage map equations are machine and compiler dependent, but most compilers locate array elements by multiplying the first subscript (i) by the number of columns, then add this value to the second subscript (j) to calculate an offset from the base of the array (&a[0][0]). Dereferencing this pointer offset (*) evaluates to an array element. Note that the number of rows does not appear in a storage map equation for two-dimensional arrays.

When you pass the name of a two-dimensional array to a function, the compiler uses the array's starting address (p) and a storage map equation to resolve array subscripts.

 p[i][j] = *(p + 10 * i + j)   // 2D pointer expression 

Since the number of rows (5) does not appear in the storage map equation, it is not necessary to include it in either a function prototype or a function definition. You must, however, include the number of columns (10); otherwise, the compiler generates compilation errors.

NOTE

Be aware of the difference between a two-dimensional array and a one-dimensional array of pointers when you pass them as arguments to functions.

 void f(int *[10]);         // 1D function prototype

 int a[5][10];            // 2D array of 5 x 10 integers
 f(a);                // illegal - doesn't match

 int *b[10];             // 1D array of 10 ptrs to ints
 f(b);                // legal - matches

The function prototype specifies a one-dimensional array of pointers to 10 integers. When you pass a two-dimensional array (a) to this function, their signatures do not match and the compiler reports compilation errors. The second call is correct because b is an array of 10 pointers to integers. If you use pointer notation (*) with two-dimensional array arguments, don't forget the parentheses.

Three-dimensional arrays as function arguments extend the same concepts.

 void f(int (*)[10][20]);        // 3D function prototype
 void f(int [][10][20]);         // 3D equivalent prototype

 int a[5][10][20];            // 3D array of 5 x 10 x 20 ints
 f(a);                  // call function with 3D array

 void f(int (*p)[10][20]) {       // 3D function definition
  ... p[i][j][k] ...          // 3D array subscript
 }

 void f(int p[][10][20]) {       // 3D equivalent definition
  ... p[i][j][k] ...          // 3D array subscript
 }

With three-dimensional array arguments in function prototypes and definitions, the first size (5) is optional and the second size (10) and third size (20) are necessary. You must include parentheses (*) with pointer notation, and the second and third sizes must be constant integer expressions. Let's see how the compiler builds three-dimensional arrays and resolves p[i][j][k] subscripts with this notation.

For three-dimensional arrays, the first size is the number of grids, the second size is the number of rows, and the third size is the number of columns.

 int a[5][10][20];    // 5 grids by 10 rows by 20 columns 

The compiler allocates a block of memory that holds 1000 (5 times 10 times 20) integers. When you use three-dimensional array subscripts (a[i][j][k]), the compiler generates the following pointer expression to locate array elements.

 *(&a[0][0][0] + 10 * i + 20 * j + k)   // 3D storage map equation 

The compiler uses the number of rows and the number of columns to calculate the offset from the beginning of the array. The number of grids does not appear in a three-dimensional storage map equation. When you pass the name of a three-dimensional array to a function, the compiler uses the array's starting address (p) and a storage map equation to resolve array subscripts.

 p[i][j][k] = *(p + 10 * i + 20 * j + k)   // 3D pointer expression 

Since the number of grids (5) does not appear in the storage map equation, it is not necessary to include it in either a function prototype or a function definition. You must, however, include the number of rows (10) and the number of columns (20); otherwise, the compiler generates compilation errors.

NOTE

We have shown you how to pass two-dimensional and three-dimensional arrays to functions as arguments. In fact, you may pass multidimensional arrays with any number of sizes to functions as long as they have the correct signature. With function prototypes and definitions, all sizes of a multidimensional array argument are necessary except the first one. All specified sizes must be constant integer expressions, and the first size is optional. Don't forget to include (*) when you use pointer notation.

Default Function Arguments

C++ function signatures may include one or more default arguments. The format is

 Type function_name(Type arg1 = value1, Type argN = valueN); 

An initialized argument receives a default value if a program calls the function without supplying a corresponding argument. Otherwise, function calls pass values to arguments instead of defaults. For default argument values, you may use constants, enumerations, local statics or external statics (see "static" on page 125), or function calls with these types, as follows.

 static double dval = 3.4;         // external static
 double val = 5.6;             // global

 void sub() {
  enum { max = 100 };           // enumeration
  int m1 = 10;              // local integer
  static int m2 = 6;           // local (static) integer
  void f(double s = sin(.5));       // legal - function call
  void g(int i = m1);           // illegal - local
  void h(int m = max);          // legal - enumeration
  void w(int j = val);          // legal - global
  void x(double d = dval);        // legal - external static
  void y(int k = m2);           // legal - local static
  void s(char *p, int len = strlen(p));  // illegal - local
 }

Inside sub(), the default argument for g() is illegal because m1 is a local variable, declared on the stack. Likewise, s() is illegal because initializing default argument len is illegal with local argument p. Default arguments are legal in the remaining signatures because they use function calls (with constants), enumerations, globals, and statics.

Not all signature arguments require defaults, as in

 Type function_name(Type arg1, Type argN = valueN); 

Here, a function call must supply a value for arg1 because it does not have a default.

NOTE

The compiler handles default arguments with the Positional Rule: arguments without default values must appear to the left of all default arguments. Here are several examples.

 void u(int i=5, float m=3, char c='x');    // legal
 void w(int i, float m=3, char c='x');     // legal
 void x(int i, float m, char c ='x');     // legal
 void y(int i=5, float m, char c='x');     // illegal
 void z(int i=5, float m=3, char c);      // illegal

To demonstrate default arguments, let's design an error reporting function. A header file called error.h contains the following function prototype.

 // error.h - default arguments
 void error(const char *msg = "fatal error", int dlevel = 0);

The first argument is an error message, and the second argument is a debugging level number. Both arguments have default values. You may call error() with an error message and debugging level, only an error message, or neither. Here are several examples.

 error("start process");  // legal - msg = "start process", dlevel = 0
 error("bad token",1);   // legal - msg = "bad token", dlevel = 1
 error("no value", 2);   // legal - msg = "no value", dlevel = 2
 error();          // legal - msg = "fatal error", dlevel = 0
 error(1);         // illegal - no msg specified

For the same reason that arguments without default values must appear to the left of all default arguments in function prototypes, function calls must resolve all default arguments to the left of the argument you are passing. The first call to error(), for example, is legal, because there are no arguments to the left of msg (the first argument). The last call to error() is illegal because we did not supply an argument for msg (which is to the left of dlevel, the second argument).

Here is the definition for function error().

 void error(const char *msg, int dlevel) {
  extern int Debug;
  if (dlevel == 0 || Debug >= dlevel)
    cerr << msg << endl;
 }

Note that default arguments appear only in function declarations and not in function definitions. All calls to error() without a dlevel value display the error message passed in argument msg (dlevel defaults to zero). Otherwise, error() displays the error message only if the current debugging level (called Debug and defined elsewhere) is greater than or equal to the dlevel you supply when you call error(). This approach lets you control the display of your error messages during debugging, all in one convenient function.

Default arguments are beneficial because you can include additional default arguments in function definitions and not "break" existing code. Suppose, for example, we add a third default argument to error(), one that controls program termination. Here's the approach.

 // error.h - default arguments (extended)
 void error(const char *msg = "fatal error", int dlevel = 0,
      bool kill = false);

We extend error() to handle values for argument kill as follows.

 void error(const char *msg, int dlevel, bool kill) {
  extern int Debug;

  if (dlevel == 0 || Debug >= dlevel)
    cerr << msg << endl;
  if (kill)
    exit(1);
 }

Our program terminates only if calls to error() specify a nonzero value for kill. Otherwise, error() behaves as before. Here are several examples of calls to the revised error() function.

 // old calls - same as before
 error("start process"); // legal - msg = "start process", dlevel = 0
 error("bad token",1);  // legal - msg = "bad token", dlevel = 1
 error("no value", 2);  // legal - msg = "no value", dlevel = 2
 error();         // legal - msg = "fatal error", dlevel = 0
 error(1);        // illegal - no msg specified

 // new calls
 error("no memory", 2, true);   // legal - msg = no memory",
                 // dlevel = 2, kill = true
 error(2,1);       // illegal - no msg specified

The old calls to error() default to kill = 0 and make the program behave as before. The new call error("no memory",2,1) terminates the program (kill = 1). The last call to error() is illegal because we did not supply a value for the first argument (msg).

NOTE

Adding default arguments to the end of a function's argument list lets you add new capabilities without changing existing code. Make sure, however, the default value and behavior of the new argument match the behavior of the original function.

Functions with a Variable Number of Arguments

Default arguments let you call functions with a different number of arguments. The Positional Rule, however, still requires function calls to match their data types with a prototype. Suppose you want to write a function that takes any number of arguments with, possibly, different data types. To do this, you must use a special technique that gives you (and not the compiler) control of how function arguments "pop" from the stack. This approach bypasses type checking, but it's portable and useful with debugging routines, library functions, and specialized interfaces.

The technique (called Standard Args), requires the following system header file.

 #include <stdarg.h> 

When you design a function that takes a variable number of arguments, the number of arguments and their data types are unknown inside the function. A normal function prototype is therefore unsuitable here. There is, however, a special format for these types of functions.

 Type function_name(Type arg, ...); 

The ...(ellipsis) is part of the function's signature and tells the compiler a variable number of arguments are legal with a function call. You must specify at least one argument before the ellipses in a prototype. How do you determine the number of arguments and their data types within the function? You may design any method you want, but here are two popular formats.

 Type function_name(const char *fmt, ...);
 Type function_name(int num, ...);

The library routine printf() uses the first format.

 int printf(const char *fmt, ...); 

The first argument is a format string that tells printf() how many arguments to display and what their data types are. For example,

 char buf[80]; int m;
 printf("%s %d\n", buf, m);

Here, printf() pops the stack for a format string first, which specifies two remaining arguments to display: the address (%s) of a character buffer (buf) and the value (%d) of an integer (m). This makes printf() pop the stack for these data types and perform the appropriate actions.

The second format

 Type function_name(int num, ...); 

uses its first argument (an integer) to determine the number of arguments that follow. The following statements, for example, describe a function merge(), along with calls that combine several character buffers.

 char *merge(int num, ...);
 char buf1[max1], buf2[max2], buf3[max3];

 cout << merge(2, buf1, buf2);
 cout << merge(3, buf1, buf2, buf3);

Table 3.5 lists the variables and function names you need to use the Standard Args technique. Don't forget to include <stdarg.h>, the system header file that defines these special names.

Table 3.5. Standard Args interface

Interface Name

Purpose

va_list name;

declare argument list

va_start(args, var);

initialize argument list, pop first argument

va_arg(args, Type);

pop next argument

va_end(args);

clean up argument list


Before we demonstrate the Standard Args technique, let's examine the function prototypes for the library functions strcat() and strcmp().

 char *strcat(char *, const char *);
 int strcmp(const char *, const char *);

Because strcat() returns a pointer to its first argument, we may cascade (or embed) calls, as follows.

 char buf[80];
 strcat(strcat(buf, "alpha"), "bet");

These statements place the string "alphabet" in buf. To concatenate a third character string called "alphabet soup", we embed another call to strcat(), as follows.

 strcat(strcat(strcat(buf, "alpha"), "bet"), " soup") ; 

Cascading, however, doesn't work with function strcmp() because the function returns an int and not a pointer to its first argument. This behavior means we must make separate strcmp() calls to compare two or more character strings. We would rather make a single function call with two or more arguments. Let's use the second format of the Standard Args technique to write a function scmp() that returns bool. We return true if all its character string arguments are the same, and false if they are not. Here's the code for scmp() and a program that calls it.

Listing 3.6 Comparing multiple strings with Standard Args

 // scmp.C - string comparisons for three or more character strings
 #include <iostream.h>
 #include <stdlib.h>
 #include <string.h>
 #include <stdarg.h>

 int main()
 {
  bool scmp(int, ...);   // variable arg list prototype
  char *s1 = "one";
  char *s2 = "two";
  char *s3 = "three";
  char *s4 = "four";

  cout << (scmp(3, s1, s2, s3)? "same" : "different")
     << " strings" << endl;
  cout << (scmp(4, s1, s2, s3, s4)? "same" : "different")
     << " strings" << endl;
  cout << (scmp(4, s1, s1, s1, s1)? "same" : "different")
     << " strings" << endl;
  return 0;
 }

 bool scmp(int num, ...) {
  va_list args;       // argument list
  va_start(args, num);   // init argument list, get number
  const char *s, *t;
  bool rval = true;         // return value
  s = va_arg(args, const char *);    // get first string

  while(--num) {
    t = va_arg(args, const char *);  // get next string
    if (strcmp(s, t)) {        // are they the same?
     rval = false;          // strings are different
     break;
    }
  }
  va_end(args);             // cleanup argument list
  return rval;             // return result
 }

 $ scmp
 different strings
 different strings
 same strings

The main program uses the conditional operator (?:) with scmp() to compare strings. The first integer argument is the number of character strings we pass to scmp(). We need the parentheses in the cout statements because C++ precedence rules require them (?: evaluates after <<).

Inside scmp(), we use va_list to declare args. Calling va_start() initializes args, pops the stack for the number of arguments, and stores it into num (an integer). The first call to va_arg() pops a pointer (const char *) from the stack and stores it for later use. The while loop pops the remaining pointers and compares each one to the first pointer using strcmp(). After the loop exits, scmp() cleans up the argument list and returns false to the caller if the strings differ, or true if they are all the same.

NOTE

Examine the include file <stdarg.h> on your system. You'll discover most of the calls are macros, and there's some clever code that handles popping the stack to access different data types. Note that the Standard Args technique is portable, even though popping the stack at a low level is machine dependent.

  • + Share This
  • 🔖 Save To Your Account