Postfix expressions consist of primary expressions or expressions in which postfix operators (see Table 4.1) follow a primary expression.
Table 4.1 Postfix Operators
Operator Name | Operator Notation |
Subscript operator | [ ] |
Function-call operator | ( ) |
Explicit type conversion operator | type-name( ) |
Member-selection operator | . or –> |
Postfix increment operator | ++ |
Postfix decrement operator | –– |
postfix-expression:
primary-expression
postfix-expression[expression]
postfix-expression(expression-listopt)
simple-type-name(expression-listopt)
postfix-expression.name
postfix-expression–>name
postfix-expression++
postfix-expression––
expression-list:
assignment-expression
expression-list,assignment-expression
A postfix-expression followed by the subscript operator, [ ], specifies array indexing. One of the expressions must be of pointer or array type—that is, it must have been declared as type* or type[ ]. The other expression must be of an integral type (including enumerated types). In common usage, the expression enclosed in the brackets is the one of integral type, but that is not strictly required. Consider the following example:
MyType m[10]; // Declare an array of a user-defined type.
MyType n1 = m[2]; // Select third element of array.
MyType n2 = 2[m]; // Select third element of array.
In the example above, the expression m[2] is identical to 2[m]. Although m is not of an integral type, the effect is the same. The reason that m[2] is equivalent to 2[m] is that the result of a subscript expression e1[ e2 ] is given by:
*( (e2) + (e1) )
The address yielded by the above expression is not e2 bytes from the address e1. Rather, the address is scaled to yield the next object in the array e2. For example:
#include <iostream.h>
int main()
{
double aDbl[2];
cout << “Address of first element is: ”
<< &aDbl[0] << “\n”;
cout << “Address of second element is: ”
<< &aDbl[1] << “\n”;
return 0;
}
The preceding program prints two addresses that are 8 bytes apart—the size of an object of type double. This scaling according to object type is done automatically by the C++ language, and is defined in “Additive Operations” where addition and subtraction of operands of pointer type is discussed.
Positive and Negative Subscripts
The first element of an array is element 0. Therefore, the range of a C++ array is from array[0] to array[size – 1]. However, since C++ supports both positive and negative subscripts, it can be convenient to use them in arrays. Although C++ permits negative subscripts, they must fall within the array boundaries or the results are unpredictable. The following code illustrates this concept:
#include <iostream.h>
main()
{
int iNumberArray[1024];
int *iNumberLine = &iNumberLine[512];
cout << iNumberArray[-256] << “\n”; // Run-time error
cout << iNumberLine[-256] << “\n”; // OK
return 0;
}
The negative subscript in iNumberArray can produce a run-time error because it yields an address 256 bytes lower in memory than the actual origin of the array. The object iNumberLine is initialized to the middle of iNumberArray; it is therefore possible to use both positive and negative array indices on it. Array subscript errors do not generate compile-time errors, but they yield unpredictable results.
The subscript operator is commutative. Therefore, the expressions array[index] and index[array] are guaranteed to be equivalent as long as the subscript operator is not overloaded (see “Overloaded Operators” in Chapter 12, on topic ). The first form is the most common coding practice, but either works.
A postfix-expression followed by the function-call operator, ( ), specifies a function call. The arguments to the function-call operator are zero or more expressions separated by commas—the actual arguments to the function.
The postfix-expression must be of one of these types:
Function returning type T. An example declaration is T func( int i )
Pointer to a function returning type T. An example declaration is T (*func)( int i )
Reference to a function returning type T. An example declaration is T (&func)(int i)
Pointer-to-member function dereference returning type T. Example function calls are: (pObject->*pmf)(); (Object.*pmf)();
Calling programs pass information to called functions in “actual arguments.” The called functions access the information using corresponding “formal arguments.”
When a function is called, the following tasks are performed:
All actual arguments (those supplied by the caller) are evaluated. There is no implied order in which these arguments are evaluated, but all arguments are evaluated and all side effects completed prior to entry to the function.
Each formal argument is initialized with its corresponding actual argument in the expression list. (A formal argument is an argument that is declared in the function header and used in the body of a function.) Conversions are done as if by initialization—both standard and user-defined conversions are performed in converting an actual argument to the correct type. The initialization performed is illustrated conceptually by the following code:
void Func( int i ); // Function prototype
...
Func( 7 ); // Execute function call
The conceptual initializations prior to the call are shown below:
int Temp_i = 7;
Func( Temp_i );
Note that the initialization is performed as if using the equal-sign syntax instead of the parentheses syntax. A copy of i is made prior to passing the value to the function. (For more information, see “Initializers” in Chapter 7, on topic and “Conversions,” “Initialization Using Special Member Functions,” and “Explicit Initialization” in Chapter 11, on topic , topic, and topic, respectively.
Therefore, if the function prototype (declaration) calls for an argument of type long, and the calling program supplies an actual argument of type int, the actual argument is promoted using a standard type conversion to type long (see Chapter 3, “Standard Conversions”).
It is an error to supply an actual argument for which there is no standard or user-defined conversion to the type of the formal argument.
For actual arguments of class type, the formal argument is initialized by calling the class's constructor. (See “Constructors” in Chapter 11, on topic for more about these special class member functions.)
The function call is executed.
The following program fragment demonstrates a function call:
void func( long param1, double param2 );
int main()
{
int i, j;
// Call func with actual arguments i and j.
func( i, j );
...
}
// Define func with formal parameters param1
// and param2.
void func( long param1, double param2 )
{
...
}
When func is called from main, the formal parameter param1 is initialized with the value of i (i is converted to type long to correspond to the correct type using a standard conversion), and the formal parameter param2 is initialized with the value of j (j is converted to type double using a standard conversion).
Note:
Formal arguments declared as const types cannot be changed within the body of a function. Functions can change any argument that is not of type const. However, the change is local to the function and does not affect the actual argument's value unless the actual argument was a reference to an object not of type const.
The following functions illustrate some of these concepts:
int func1( int i, int j, char *c )
{
i = 7; // Error: i is const.
j = i; // OK, but value of j is
// lost at return.
*c = 'a' + j; // OK: changes value of c
// in calling function.
return i;
}
double& func2( double& d, const char *c )
{
d = 14.387; // OK: changes value of d
// in calling function.
*c = 'a'; // Error: c is a pointer to
// a const object.
return d;
}
Ellipses and Default Arguments
Functions can be declared to accept fewer arguments than specified in the function definition, using one of two methods: ellipsis (...) or default arguments.
Ellipses denote that arguments may be required but that the number and types are not specified in the declaration. This is normally poor C++ programming practice because it defeats one of the benefits of C++: type safety. Different conversions are applied to functions declared with ellipses than to those functions for which the formal and actual argument types are known:
If the actual argument is of type float, it is promoted to type double prior to the function call.
Any signed or unsigned char, short, enumerated type, or bit field is converted to either a signed or unsigned int using integral promotion.
Any argument of class type is passed by value as a data structure; the copy is created by binary copying instead of by invoking the class's copy constructor (if one exists).
Ellipses, if used, must be declared last in the argument list. For more information about the use of ellipses to pass a variable number of arguments, see the Run-Time Library Reference manual, under the topics: va_arg, va_list, and va_start.
Default arguments allow the programmer to specify the value an argument should assume if none is supplied in the function call. The following code fragment shows how default arguments work (for more information about default arguments, see “Default Arguments” in Chapter 7, on topic ):
#include <iostream.h>
// Declare the function print that prints a string,
// then a terminator.
void print( const char *string,
const char *terminator = “\n” );
int main()
{
print( “hello,” );
print( “world!” );
print( “good morning”, “ ,” );
print( “sunshine.” );
return 0;
}
// Define print.
void print( char *string, char *terminator )
{
if( string != NULL )
cout << string;
if( terminator != NULL )
cout << terminator;
}
The above program declares a function, print, that takes two arguments. However, the second argument, terminator, has a default value, “\n”. In main, thefirst two calls to print allow the default second argument to supply a new line to terminate the printed string. The third call specifies an explicit value for the second argument. The output from the program is:
hello,
world
good morning, sunshine.
A function call evaluates to an r-value unless the function is declared as a reference type. Functions with reference return type evaluate to l-values, and it is legal to use them on the left side of an assignment statement as follows:
#include <iostream.h>
class Point
{
public:
// Define “accessor” functions as
// reference types.
unsigned& x() { return _x; }
unsigned& y() { return _y; }
private:
unsigned _x;
unsigned _y;
};
int main()
{
Point ThePoint;
ThePoint.x() = 7; // Use x() as an l-value.
unsigned y = ThePoint.y(); // Use y() as an r-value.
// Use x() and y() as rvalues.
cout << “x = ” << ThePoint.x() << “\n”
<< “y = ” << ThePoint.y() << “\n”;
return 0;
}
The above code defines a class called Point, which contains private data objects that represent x and y coordinates. These data objects must be modified and their values retrieved. The above program is only one of several designs for such a class; use of the GetX and SetX or GetY and SetY functions is another possible design.
A function returning a pointer to an object can appear on the left side of an assignment statement as follows:
struct A
{
int i;
};
A *func();
...
func()->i = 7;
The above statement is legal because func returns a pointer to an object of type A. Therefore, the member-selection operator, –>, dereferences the pointer, making it an l-value. See “L-Values and R-Values” in Chapter 2, on topic for more about expressions that are l-values.
Functions that return class types, pointers to class types, or references to class types can be used as the left operand to member-selection operators. Therefore, the following code is legal:
class A
{
public:
int SetA( int i ) { return (I = i); }
int GetA() { return I; }
private:
int I;
};
// Declare three functions:
// func1, which returns type A
// func2, which returns a pointer to type A
// func3, which returns a reference to type A
A func1();
A* func2();
A& func3();
int iResult = func1().GetA();
func2()->SetA( 3 );
func3().SetA( 7 );
Functions can be called recursively. For more information about function declarations, see “Function Specifiers” in Chapter 6, on topic and “Member Functions” in Chapter 8, on topic . Related material is in “Program and Linkage” in Chapter 2, on topic .
A postfix-expression followed by the member-selection operator (.) and a name is also a postfix-expression. The first operand of the member-selection operator must be a class object (an object declared as class, struct, or union type) or a reference to a class object, and the second operand must identify a member of that class.
The result of the expression is the value of the member, and it is an l-value if the named member is an l-value.
A postfix-expression followed by the member-selection operator (–>) and a name is a postfix expression. The first operand of the member-selection operator must be a pointer to a class object (an object declared as class, struct, or union type), and the second operand must identify a member of that class.
The result of the expression is the value of the member, and it is an l-value if the named member is an l-value. The –> operator dereferences the pointer. Therefore, the expressions e–>member and (*e).member (where e represents an expression) yield identical results (except when the operators –> or * are overloaded).
When a value is stored through one member of a union but retrieved through another member, no conversion is performed. The following program stores data into the object U as int, but retrieves the data as two separate bytes of type char:
#include <iostream.h>
int main()
{
struct ch
{
char b1;
char b2;
};
union u
{
struct ch uch;
int i;
};
u U;
U.i = 0x6361; // Bit pattern for “ac”
cout << U.uch.b1 << U.uch.b2 << “\n”;
return 0;
}
Note:
The preceding code is not portable because it assumes an int is two bytes long while a char is one byte long. In 32-bit target compilations, this assumption is false.
Postfix Increment and Decrement Operators
C++ provides prefix and postfix increment and decrement operators; this section describes only the postfix increment and decrement operators. (For more information, see “Increment and Decrement Operators”.) The difference between the two is that in the postfix notation, the operator appears after postfix-expression, whereas in the prefix notation, the operator appears before expression. The following example shows a postfix-increment operator:
i++
The effect of applying the postfix increment, or “postincrement,” operator (++) is that the operand's value is increased by one unit of the appropriate type. Similarly, the effect of applying the postfix decrement or “postdecrement” operator (––) is that the operand's value is decreased by one unit of the appropriate type.
For example, applying the postincrement operator to a pointer to an array of objects of type long actually adds four to the internal representation of the pointer. This behavior causes the pointer, which previously referred to the nth element of the array, to refer to the (n+1)th element.
The operands to postincrement and postdecrement operators must be modifiable (not const) l-values of arithmetic or pointer type. The result of the postincrement or postdecrement expression is the value of the postfix-expression prior to application of the increment operator. The type of the result is the same as that of the postfix-expression, but it is no longer an l-value.
Postincrement and postdecrement, when used on enumerated types, yield integral values. Therefore, the following code is illegal:
enum Days {
Sunday = 1,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
};
int main()
{
Days Today = Tuesday;
Days SaveToday;
SaveToday = Today++;
return 0;
}
The intent of the above code is to save today's day, then move to tomorrow. However, the result is that the expression Today++ yields an int—an error when assigned to an object of the enumerated type Days.