You Can Convert Strings To Numbers Safely, Even In C!

Converting from strings to numbers has always been a psychological problem for many developers in any language. Decisions are made more from the heart than from the head, but the result is not always what one expects.

For example, among the many possibilities available, dry casting is not exactly the best solution in certain cases.

Example

Let’s write two lines of C code on the fly to try out a typical type casting:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void) {
char input = '1';
int casted = (int)input;
printf("Char '%c' casted to int %d.\n", input, casted);

return 0;
}

The output of this program is:

1
Char '1' casted to int 49.

Stop. If you thought a number within a char converted smoothly to an int, I’m sorry. The result of the conversion instead is the ASCII value relating to the character that we have given as input.

atoi, atol, atof, atolf

No, I’m not declining an archaic word, these are the standard library functions for converting a string to int, long, float and long double respectively.

🐬 FUN FACT: The name of the functions begins with “a” to indicate that an ASCII string is taken as the starting point. For example atoi means ASCII string To Integer.

Returning to the previous example, using one of the available functions, in this case, atoi, the code would be as follows:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>

int main(void) {
char *input = "1";
int converted = atoi(input);
printf("String \"%s\" converted to int %d.", input, converted);
return 0;
}

The output will be:

1
String "1" converted to int 1.

I’d say we’re there. If you pass a string like “423” the result will be the number 423, if you pass the string “-5312” the result will be the negative number 5312. Surprisingly, if you pass a string containing letters like “540euro”, the result will be the number 540, while if you pass the string “63 euros and 20 cents” the result will be the number 63. In short, the conversion from string to number stops as soon as a non-convertible character is found.

Anyway, are you happy now that you can use functions like atoi?

Well, because it is not recommended to use them.

strtol, strtof, strtod, strtold

Yes, I know, it’s getting worse. But there is a reason why these other functions were created, which are similar to the previous ones.

⚠️ INFO: Since there is no strtoi in the C99 standard, for this example we will see the difference between atol and strtol, which convert from string to long.

The atol function declaration is:

1
long int atol(const char *str)

The parameter accepted is the string to convert.

Here are the usual two lines of code to try:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <stdlib.h>

int main(void) {
char *input = "313373";
long converted = atol(input);
printf("String \"%s\" converted to long %ld.", input, converted);

return 0;
}

The output will be:

1
String "313373" converted to long 313373.

Nothing new compared to the example with atoi. Let’s see strtol instead.

The declaration of the strtol function is:

1
long int strtol(const char *str, char **endptr, int base)

Compared to the atol function, the parameters accepted are the string to convert, a pointer to the first non-convertible character, and the numerical base of conversion.

The same previous program rewritten with strtol will be:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdlib.h>

int main(void) {
char *input = "313373";
char *endPointer;
long converted = strtol(input, &endPointer, 10);
printf("String \"%s\" converted to long %ld.", input, converted);

return 0;
}

And the output will be pretty much identical:

1
String "313373" converted to long 313373.

And so what is the difference?

Well, first of all, we can decide which numerical base to convert to, while for functions ato* is always 10.

Second, we have the famous pointer to the first non-convertible character. This mainly allows us to be able to do two things: error detection and forced parsing.

If the input string contains all numbers, the pointer will refer to the null character terminating the string. Conversely, if it is not null, it means that the string is not a clean number, which could lead to problems in the program’s logic.

Let’s modify the previous code to be able to see the pointer in action:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>

int main(void) {
char *input = "313373 rules!";
char *endPointer;
long converted = strtol(input, &endPointer, 10);
printf("String \"%s\" converted to long %ld.\n", input, converted);
printf("Extra string is \"%s\".", endPointer);

return 0;
}

In this case, the output will be:

1
2
String "313373 rules!" converted to long 313373.
Extra string is " rules!".

By checking if the pointer is null or contains something, we can handle any errors, perhaps by simply adding an if like the following:

1
if (*endPointer) { /* error case */ }

I would say that is already a good reason to use strtol.

But there is also the possibility, again thanks to the pointer, to parse strings by extrapolating the numbers, for example from a comma-separated list of numbers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>

// Function prototype AKA function declaration
void error();

int main(void) {
char *input = "1,2,3";
char *endPointer;

int first = strtol(input, &endPointer, 0);
if (*endPointer != ',') error();

int second = strtol(endPointer + 1, &endPointer, 0);
if (*endPointer != ',') error();

int third = strtol(endPointer + 1, &endPointer, 0);
if (*endPointer != '\\0') error();

printf("Numbers are: %d, %d and %d.", first, second, third);

return 0;
}

void error() {
printf("Error in parsing!");
exit(1);
}

I can’t see this badly written code, of course, it could be optimized with a for loop, but for now, it was just to give you an idea.

The output of this program will be:

1
Numbers are: 1, 2 and 3.

errno

The ato* functions return the value 0 if there is nothing convertible in the string or the first character is not numeric. This could be a problem if you also expect this value among the allowed values to convert. In this case, you cannot understand if the value 0 is correct or due to a problem in the conversion.

The strto* functions also return the value 0, but having the pointer in addition, as we have seen, we can use that to carry out the checks.

In addition, the strto* functions also set errno to EINVAL if the conversion cannot be performed due to an invalid numerical base, or to ERANGE if an overflow or underflow occurs in the conversion.

An on-the-fly example with an overflow using [strerror](https://linux.die.net/man/3/strerror):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <limits.h>

extern int errno;

int main(void) {
char *input = "15546346634735834583";
char *endPointer;
long converted = strtol(input, &endPointer, 10);

printf("String \"%s\" converted to long %ld.\n", input, converted);

if (*endPointer)
printf("Extra string is \"%s\".\n", endPointer);
if (errno) {
printf("ERROR: %s (%d).\n", strerror(errno), errno);
if (errno == ERANGE)
printf("LONG_MAX: %ld", LONG_MAX);
}

return(0);
}

The output will be:

1
2
3
String "15546346634735834583" converted to long 9223372036854775807.
ERROR: Numerical result out of range (34).
LONG_MAX: 9223372036854775807

Cool, right? At least there is more control over what we do.

Conclusions

In programming, there are two thousand ways to do the same thing, but you always have to be careful when deciding how to implement even the stupidest of things, because some really nice side effects could come out.

The good rule is to always keep up-to-date or check if a function has been deprecated and why. In some cases you could even use it if the context allows it. For example, the fact that there is no strtoi could lead you to use atoi (after verifying the string to convert, of course), but no one will stop you.

Extra

🐬 FUN FACT: You can use strtol just like atol!

The following use of strtol makes it like atol:

1
strtol(str, (char **)NULL, 10);

It doesn’t make any sense, but oh well, it was just to say.