Passing parameters to a C function seems simple enough. The details of how that process takes place have a significant impact on the behavior and performance of the code. Programmers of desktop computers do not care about this impact; embedded developers do not have that luxury. This article looks at parameter passing mechanisms and how to use them optimally.
Programming in general is all about writing some code which acts upon some data. Although, in an embedded application, the data may be obtained from an external device, it is much more likely that the code is in the form of a subroutine or function, which receives its input by means of parameters.
Different programming languages offer a selection of options with regard to parameter passing.
Arguably the most flexibility is available when programming in assembly language, so it is worth appreciating the options in this context in order to understand the impact of the possibilities in C and C++.
Parameters in assembly
It was once common to code entire embedded applications in assembly. That practice is now relatively rare and only small amounts of such low level code are used. However, considering how such code may be written clarifies what is going on “under the hood” with high level languages.
To pass data to an assembly language subroutine, there are normally four broad options:
- Global variables. It is very easy to understand the mechanism of just sharing storage between parts of the code, but use of global variables is generally frowned upon, as it can make code of non-trivial complexity more difficult to understand and maintain. However, in very simple cases, it may be an acceptable and efficient approach.
- Registers. For most CPUs, registers offer very fast access and flexible addressing modes, so they are an attractive parameter passing mechanism, if only a small amount of data needs to be transferred. It can often work out well, if the required data is already in an appropriate register. Without care, however, this mechanism can become as unmaintainable as the use of global variables. Clear and well documented programming standards need to be implemented so that the use of registers is clear and rational.
- Stack. Most CPUs use the stack to store the return address during a subroutine call, so it seems a logical place to store parameters. Indeed many processors are designed so that addressing relative to the stack pointer (SP-relative addressing) is efficient. However, some smaller devices do not have such addressing modes. This approach yields reentrant code, which can be vital.
- Dedicated memory block. This approach makes sense if the CPU’s addressing modes are limited.
There are, of course, hybrid approaches that combine elements of these four. For example, a block of memory (which may have been dynamically allocated) can be used to pass the bulk of the data, the address of which is passed in a register or on the stack.
Parameters in C
In C, some aspects of parameter passing are specified by the language definition; others are implementation dependent and may vary from one compiler to another. There are no specific constraints on the parameter passing mechanism. It is left to the compiler writer to choose between stack (which is most common, as it is usually efficient and reentrant), registers or something else.
Passing by value
Parameters in C are always passed by value. This means that each parameter’s value is evaluated and the result passed to the function. This may not be apparent if you look at a call like this:
But it is much more obvious if the call takes this kind of form:
This passing mechanism means that a function like this will not work as intended:
void swap(int a, int b)
temp = a;
a = b;
b = temp;
A call like this will have no effect:
To achieve the expected result requires the use of pointers:
void swap(int *a, int *b)
temp = *a;
*a = *b;
*b = temp;
And a call would need to look like this:
Pointers to x and y are evaluated and their values passed as parameters to the function. This achieves the effect of passing parameters by reference.
Passing by reference
Passing parameters by value is mostly useful, but there are times when a reference is better. Sometimes this is for convenience, as shown above, and pointers enable the required result. On other occasions it is more efficient to pass larger quantities of data by reference. This may also be done using pointers, thus:
ptr = (int *)malloc(1024);
A block of memory has been created, presumably populated with some data and a pointer sent to the function. The memory is recovered on return. Although it should be noted that the use of dynamic memory allocation in embedded applications may be questionable, but that is a topic outside the scope of this article. The use of an array may be better:
In this example a pointer to the first element of the array is passed to the function. Although this is valid, the C language makes it easier and the call could look like this:
This works because a “bare” array name (i.e. not followed by the [ ] operators) is a (constant) pointer to the first element of the array. So, arrays are automatically passed by reference (in effect).
Incidentally, a bare function name (i.e. not followed by the ( ) operators) behaves in the same fashion, yielding a pointer to the function.
Given knowledge of how parameters are passed, it is possible to write code like this:
void fun(int n)
int *p, x;
p = &n;
x = *++p;
This function simply takes the address of the first parameter, assumes that it is on the stack and indexes off of that address to find further parameters. This is very bad practice for two reasons. First, the code is non-portable; a different compiler may pass parameters in a different way. Second, the code is hard to understand; writing clear, maintainable code should always be the priority.
The most likely motivation for this code was to enable the implementation of a function which can accept a variable number of parameters. This is foolish, as the C language incorporates provision for this requirement.
Parameters in C++
The C++ language performs parameter passing in essentially the same way as C, however it incorporates two improvements. First, trailing parameters may be provided with default values by defining a function thus:
void dpfun(int a, int b, int c=0, int d=99)
Valid calls to this function might be:
dpfun(1, 2, 3, 4);
dpfun(1, 2, 3); // d is set to 99
dpfun(1, 2); // c is set to 0 and d to 99
However, the following calls are invalid:
dpfun(1); // b is not defined
dpfun(1, 2, , 4); // only trailing parameters may default
The other enhancement is the option to pass parameters by reference. The earlier example may be rewritten thus:
void swap(int &a, int &b)
temp = a;
a = b;
b = temp;
This code will behave as intended. A call may be made like this:
And the values of x and y will be swapped. This avoids the use of pointers and the associated danger of introducing errors. Of course, C++ is really using pointers “behind the scenes”, but this is transparent to the programmer.
The only downside of this facility is that, with the C version of swap() the call would look like this:
which clearly shows that pointers are being passed and, hence, there is a possibility that the values of x and y may be changed as a result of the call. Using reference parameters, even though it is indicated in the definition and declaration of the function, it is unclear at the call site that parameters may be affected by the call.
Parameter passing is such an everyday activity for C or C++ programmers that any explanation of the process might seem trivial and redundant. However, there is a significant amount of detail which, for an embedded developer, who is concerned with writing efficient code, must be understood.
Colin Walls has over thirty years experience in the electronics industry, largely dedicated to embedded software. A frequent presenter at conferences and seminars and author of numerous technical articles and two books on embedded software, Colin is an embedded software technologist with Mentor Embedded [the Mentor Graphics Embedded Software Division], and is based in the UK. His regular blog is located at: http://blogs.mentor.com/colinwalls. He may be reached by email at email@example.com.