One of the differences that struck me the most when I started learning C, is the relative difficulty of returning strings from a function. Particularly when we compare it with higher-level languages like Python. In this post, I will show two ways of returning strings in C (and one wrong way of doing it).
Strings in C
First of all, there isn’t really a string
type in C. What we do have are arrays of char
s.
char
type
char
is the type used to represent a single character. Usually (it depends on implementation details), it’s encoded as 8 bits. With 8 bits we can represent 256 different values, which corresponds to ASCII encoding. In this encoding, we have all of the letters of the english alphabet as well as the most usual symbols.
As a side note, to use characters of other alphabets and different symbols there are different types such as wchar_t
in C.
Here’s an example of some char
values in C:
char c; c = 'a'; c = 'b'; c = 93;
See that a single char is enclosed in single quotes '
. Unlike other programming languages, single quotes can’t be used for strings and are specifically for characters.
Another interesting aspect is that we can also represent a character with any number between 0 – 255. The character corresponding to each value is that of the ASCII encoding.
Strings: arrays of chars
An array in C is a collection of elements allocated in adjacent slots of memory. This data structure is used to represent strings in C as arrays of char
:
char string[10];
We specify it’s an array by using brackets []
. Additionally, we’re allocating enough memory for 10
characters.
As an example, let’s see how the string "hello"
would be represented as an array of characters:
See that we would need an array of at least six elements. Five for each one of the characters of “hello” and an extra one for a special termination character, the null character \0
. The null character tells C that we’ve reached the end of the array. Otherwise, it would just continue reading the next slots in memory and try to decode them as char
.
Since an array is referenced by the address of its first element, we can also represent strings as char pointers: char *
.
We’ll see now that when we want to deal with strings in C, what we’re actually doing is working with arrays and pointers. Since arrays have to do with memory allocation, special care will be needed.
Returning strings: how NOT to do it
For all of the cases that will be shown, we’ll work with a simple function sayHello()
which we’d like to return the string "hello"
.
The most evident, but sadly wrong, way to do it is as follows:
char *sayHello() { char salutation[] = "hello"; return salutation; }
The reason why this is wrong is because we’re trying to return an automatic variable (also called local variable). These are the variables that are defined inside functions. C will create them and automatically discard them when the function has finished its execution.
In more detail, what our function sayHello
does is allocate memory to store the string "hello"
(as well as its termination character \0
). When it has finished its execution, these slots of memory will be “freed”. This means that any other process can use those slots and erase/corrupt the contents of the original string. At any rate, trying to access unallocated slots of memory is a recipe for disaster.
Let’s write a small program to test it:
#include <stdio.h> char *sayHello(); int main() { printf("%s\n", sayHello()); return 0; } char *sayHello() { char salutation[] = "hello"; return salutation; }
This is what I get when I try to compile and run it:
$ gcc main.c -o main main.c: In function ‘sayHello’: main.c:14:10: warning: function returns address of local variable [-Wreturn-local-addr] 14 | return salutation; | ^~~~~~~~~~ $ ./main Segmentation fault (core dumped)
As we can see, a warning was already raised during compilation. Trying to execute resulted in a Segmentation fault error.
Returning strings: dynamic memory allocation
One way of fixing the issue we had before is with dynamic memory allocation, using the malloc
function:
char *sayHello() { char *salutation; salutation = (char *) malloc(6 * sizeof (char)); strcpy(salutation, "hello"); return salutation; }
Here we are asking C to allocate enough memory to hold 6 char
(five for “hello” plus one for termination). Memory allocated with malloc
isn’t automatically freed after the function has finished execution so we can use it as a function call return.
On the other hand, for larger programs we have to be careful of free
ing memory that has been allocated with malloc
.
We can compile this program by including the <string.h>
and <stdlib.h>
libraries:
#include <stdio.h> #include <string.h> #include <stdlib.h> char *sayHello(); int main() { printf("%s\n", sayHello()); return 0; } char *sayHello() { char *salutation; salutation = (char *) malloc(6 * sizeof (char)); strcpy(salutation, "hello"); return salutation; }
Upon execution we get:
$ gcc main.c -o main $ ./main hello
Returning strings: passing the address to the function
This is how I saw strings handled in K&R. We saw that automatic variables are erased after the function has finished its execution. To handle this, we pass the string from the caller namespace:
char salutation[6]; sayHello(salutation);
sayHello
will perform whatever operations it needs on the char address passed to it. In this case:
void sayHello(char *s) { strcpy(s, "hello"); }
We had to change the definition of the function. We don’t return anything so we set the return type to void
and we make it accept a char *
parameter.
Putting it all together:
#include <stdio.h> #include <string.h> void sayHello(char *); int main() { /* Memory allocation */ char salutation[6]; sayHello(salutation); printf("%s\n", salutation); return 0; } void sayHello(char *s) { strcpy(s, "hello"); }
The allocation of memory for the string is done from the function that will call sayHello
(in this case from the main
function).
With this, we have finished with the different ways of returning strings from functions in C. For a more technical explanation, you can see this excellent resource.