Returning Data from Functions in C
This guide explores data return methods in C by analyzing return-by-value, output parameters, heap management, and const parameter qualification.
Contents
- Values in Function Return Values
- Pointers in Function Return Values
- Manipulating Data Passed in Pointer Parameters
- Using Pointers for Output Parameters
- Allocating Memory and Returning Pointers
- Returning Data Using a Structure
constin Function Return Types- Recommendations
Passing data from functions back to the calling code is essential for procedural programming. While C provides multiple methods to return data from functions, each approach offers distinct trade-offs regarding memory efficiency, stack utilization, and safety. Selecting the appropriate mechanism—whether returning by value, leveraging output parameters, or dynamic heap allocation—depends entirely on the structure of the data and the specific requirements of the application scenario.
Values in Function Return Values
Simple values—such as numbers, characters, enumerations, and structures—can be returned directly by value:
#include <stdio.h>
#include <stdlib.h>
int add(const int a, const int b)
{
int sum = a + b; // Local variable stored on the stack.
return sum; // Return copy of the variable's value.
}
int main(void)
{
int sum = add(10, 20);
printf("sum = %d\n", sum);
return EXIT_SUCCESS;
}Pointers in Function Return Values
Local variables are stored on the stack (unless qualified as static), and their contents are destroyed when the function returns. Therefore, local variables—including arrays and pointers to local memory—must never be returned, as the resulting pointer becomes dangling and invalid:
#include <stdio.h>
#include <stdlib.h>
// Do not use!
char *get_string(void)
{
char result[] = "Hello World!"; // Local variable stored on the stack.
// GCC warning: function returns address of local variable
// [-Wreturn-local-addr]
return result;
}
int main(void)
{
char *result = get_string();
printf("result = \"%s\".\n", result);
return EXIT_SUCCESS;
}Manipulating Data Passed in Pointer Parameters
To return a string from a function, a pre-allocated buffer is typically passed to the function, which then populates or modifies its contents:
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
bool get_string(char *const buf, const size_t len)
{
const char src[] = "Hello World!"; // 13 chars including null terminator.
if (len <= strlen(src))
return false;
strcpy(buf, src);
return true;
}
int main(void)
{
#define BUF_LEN 40
char buf[BUF_LEN];
if (get_string(buf, BUF_LEN))
printf("buf = \"%s\" (strlen+1: %ld)\n", buf, strlen(buf) + 1);
else
{
printf("Buffer is too small.");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
#undef BUF_LEN
}Using Pointers for Output Parameters
Output parameters can be used to return data to the caller. These parameters hold a pointer to a destination variable that receives the result, effectively passing the argument by reference:
#include <stdio.h>
#include <stdlib.h>
void add(const int a, const int b, int *const sum)
{
*sum = a + b; // Write result to address in memory pointed to by `sum`.
}
int main(void)
{
int sum;
add(3, 5, &sum); // Pass memory address of variable `sum` to the function.
printf("3 + 5 = %d.\n", sum);
return EXIT_SUCCESS;
}Allocating Memory and Returning Pointers
As an alternative to passing a pre-allocated buffer, the function can allocate memory for the string on the heap rather than the stack. It then returns the pointer to this new string via an output parameter, result. The caller passes a pointer variable, which the function populates with the address of the newly allocated heap memory:
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Equivalent to `bool get_string(const char *result[const])`.
bool get_string(const char **const result)
{
const char src[] = "Hello World!"; // 13 chars including null terminator.
// Allocate string on the heap, the one additonal character is used for the
// null terminator.
char *const dst = malloc(sizeof(char) * strlen(src) + 1);
if (dst == NULL) // Allocation failed.
return false;
strcpy(dst, src);
*result = dst; // Make result parameter point to the string on the heap.
return true;
}
int main(void)
{
const char *result = NULL;
if (!get_string(&result)) // `result` points to the string on the heap.
{
printf("String allocation failed.\n");
return EXIT_FAILURE;
}
printf("result = \"%s\"\n", result);
// Free the string on the heap to prevent a memory leak.
free((void *)result);
result = NULL;
return EXIT_SUCCESS;
}Alternatively, the function can return the pointer to the result directly, or NULL to indicate failure:
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *get_string(void)
{
const char src[] = "Hello World!"; // 13 chars including null terminator.
char *const dst = malloc(sizeof(char) * strlen(src) + 1);
if (dst == NULL) // Allocation failed.
return NULL;
return strcpy(dst, src);
}
int main(void)
{
const char *result = get_string();
if (result == NULL) // `NULL` signifies failure.
{
printf("String allocation failed.\n");
return EXIT_FAILURE;
}
printf("result = \"%s\"\n", result);
free((void *)result);
result = NULL;
return EXIT_SUCCESS;
}Returning Data Using a Structure
To return multiple values, they can be encapsulated within a struct. Since structures are passed by value, they can be returned directly or passed via an output parameter pointer. For larger structures (typically exceeding the size of four integers), using an output parameter or heap allocation should be preferred to avoid stack overhead:
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct
{
double circumference;
double area;
}
circle_data;
circle_data calc_circle(const double r)
{
circle_data retval;
retval.circumference = 2 * r * M_PI;
retval.area = pow(r, 2) * M_PI;
return retval;
}
bool calc_circle2(const double r, circle_data *const result)
{
if (r < 0)
return false;
result->circumference = 2 * r * M_PI;
result->area = pow(r, 2) * M_PI;
return true;
}
int main(void)
{
const double r = 10;
// Returning result in the function's return value.
circle_data result = calc_circle(r);
printf(
"Circle with radius %f cm:\n"
"Circumference: %f cm\n"
"Area: %f cm²\n",
r, result.circumference, result.area
);
// Returning result in an output parameter.
circle_data result2;
if (calc_circle2(r, &result2))
printf(
"Circle with radius %f cm:\n"
"Circumference: %f cm\n"
"Area: %f cm²\n",
r, result.circumference, result.area
);
else
{
printf("Radius must be >= 0.\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}const in Function Return Types
While C lacks the comprehensive const correctness system of C++, it does allow const as a function return type qualifier. Declaring a return type as const serves multiple purposes: It enforces compile-time checks, documents how the caller should treat the return value, and enables potential compiler optimizations.
Returning const to Preserve the Qualifier
If a value passed to a function as const is subsequently returned, it should be qualified as const to preserve its immutability:
#include <stdio.h>
#include <stdlib.h>
// Return `s` preserving the `const` qualifier.
const char *returns(const char *s)
{
return s;
}
int main(void)
{
const char *s = "Hello World!";
s = returns(s);
return EXIT_SUCCESS;
}Returning const to Indicate a Value Is Read-Only
The get_string function below returns a read-only string literal as a const char *. The pointer itself is not qualified as const, indicating that it can be reassigned but must never be freed by the caller:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char *get_string(void)
{
return "Hello World!";
}
int main(void)
{
const char *s = get_string();
printf("%s\n", s);
// Fails because `s` is `const char *`.
//strcpy(s, "Goodbye World!");
return EXIT_SUCCESS;
}Returning const Pointers to Indicate the Pointer Has to Be Freed
The get_string function below allocates a string on the heap and returns it as a const pointer (char *const). This prevents the caller from modifying the pointer itself, signaling that it must be retained in its original state so it can be properly freed later:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *const get_string(void)
{
char *const s = malloc(sizeof(char) * 20);
strcpy(s, "Hello World!");
return s;
}
int main(void)
{
char *const s = get_string();
printf("%s\n", s);
// Fails because `s` is a read-only variable.
//s = "Goodbye World!";
free((void *)s);
return EXIT_SUCCESS;
}As with function parameters, the const qualifier can be applied to both the data type and the pointer itself, e. g., const char *const get_string(void). Consequently, calling strcpy(s, "Goodbye World!") inside main would trigger a compiler warning or error because the operation attempts to discard the const qualifier from the destination pointer type.
Recommendations
Simple values, such as integers, can be returned directly. Multiple values can be encapsulated within a
structor passed back via output parameters.String manipulation is typically handled by passing a pre-allocated buffer to the function. The contents of strings passed via parameters (e.g.,
char *const s) can be modified within the function using standard functions fromstring.h.Dynamic allocation inside a function—returning the pointer either directly or via an output parameter—is less common. This pattern shifts the responsibility of deallocation to the caller, which requires explicit documentation to avoid memory leaks. Note that string literals stored in the read-only data section must never be passed to
free.The principles demonstrated for strings apply equally to arrays and pointers of any other data type.