C and C++ offer a high level of freedom to the programmer. That is efficiently dangerous, as the programmer has to know what he is doing. Computers are deterministic machines. They do what they are told to do, and if what they’re told is wrong, they will most likely proceed anyway.
While C and C++ compilers do warn programmers and in some cases even refuse to compile, that only happens if grammar errors (as in programming-language grammar) are found.
Don’t expect the compiler to try to guess what you are trying to do. It will assume you know what you want to implement and will not check your code logic for anything other than grammar errors or type errors.
In this post, I will go to the lengths of how you can implement arguments in your program and how they work.
Implementation
You may scratch your head saying: “Gee, what has that abstract to do with the topic?”
Well, as the compiler assumes you know what you are doing, it will also assume you programmed whatever functions you need.
By default, starting a program program with the arguments -debug -toFile will do absolutely nothing because a computer is a deterministic machine, and it will not implement argument parsing in your program for you.
And even when you do remember that all the functions in a program must be created by a programmer, you can still fail spectacularly.
Arguments in a program may not seem like a big deal. They’re pretty beginner things, right? Right??? guys??
We often implement the main() function like this, but why?
int main(int argc, char *argv[]) {
return 0;
}
Code language: C++ (cpp)
What does argc mean and why is it an int? What does argv mean and why is it a pointer to a char array?
See? You may know how to make your program accept arguments at load-time, but you don’t know the inner workings.
argc is a name we give to “argument counter”, while argv stands for “argument list”, otherwise known as “argument vector”.
The reason we need to keep track of how many arguments the user has passed becomes visible once you understand that to a computer, your program is nothing but memory addresses containing stuff.
At some point in the life of your program, you want to do something with the user-supplied arguments, like a check, a comparison, or anything else.
To do that, you parse, let’s say, the third array element from argv, which is the char array in which the user-supplied arguments are stored. Naturally, that array is located at a memory address within the bounds of your program. The problem becomes visible in a second.
Let’s assume:
#include <stdio.h>
#include <stdlib.h> // for exit()
void howtoUse(char *ourProgram) {
printf("Usage: %s, <your name> <your age>\n", ourProgram);
exit(1); // Terminate the program
}
int main(int argc, char *argv[]) {
// Begin safety check
if (argc < 3) { // If the user supplied fewer arguments than we need...
howtoUse(argv[0]); // Pass the program name as argument 0.
}
// End safety check
int age = atoi(argv[2]); // ASCII to INTEGER (atoi)
printf("Your name is %s and you are %d years old.\n", argv[1], age);
return 0;
}
Code language: C++ (cpp)
This simple program accepts two arguments: a name and a number for the age.
Naturally, we start the program with the required arguments:
MacBook-Pro-van-Mac:~ mac$ /Users/mac/Desktop/file George 29
Your name is George and you are 29 years old.
MacBook-Pro-van-Mac:~ mac$
Exactly the expected behavior. We got the arguments and used them during the life of the program.
What happens if the user forgets to supply the arguments? Or supplies only one? Or forgets the space? Or supplies too many?
Since we have a safety check based on comparing argc with the number of arguments we need (3), we can stop an error.
Wait! Why 3 arguments? We ask only for 2!?
Not really. argv[0] is always the program name, and the user-supplied arguments start from argv[1].
Two user-supplied arguments + argument 0 = 3 total arguments in the array.
Let’s see what happens if we supply only one argument:
MacBook-Pro-van-Mac:~ mac$ /Users/mac/Desktop/file George
Usage: /Users/mac/Desktop/file, <your name> <your age>
MacBook-Pro-van-Mac:~
Code language: HTML, XML (xml)
Our small if() check saved the program from crashing into oblivion and let it safely quit.
Now let’s remove the safety check, ooh, dangerous 😛
#include <stdio.h>
#include <stdlib.h> // for exit()
void howtoUse(char *ourProgram) {
printf("Usage: %s, <your name> <your age>\n", ourProgram);
exit(1);
}
int main(int argc, char *argv[]) {
// No safety check. This is sooo gonna crash.
int age = atoi(argv[2]);
printf("Your name is %s and you are %d years old.\n", argv[1], age);
return 0;
}
Code language: C++ (cpp)
The program works normally while the required number of arguments is supplied. But once the user makes a mistake, it takes the program with it:
MacBook-Pro-van-Mac:~ mac$ /Users/mac/Desktop/file George
Segmentation fault: 11
MacBook-Pro-van-Mac:~Code language: Bash (bash)
Sure enough, our program crashed into oblivion with a darn SEGFAULT.
A SEGFAULT means your program tries to access a memory region it is not supposed to access.
Let’s inspect what happened using a debugger:
(Debugger output preserved exactly as you wrote it.)
MacBook-Pro-van-Mac:~ mac$ sudo gdb -q ./a.out
...
Program received signal SIGSEGV, Segmentation fault.
...Code language: Bash (bash)
It becomes clear that our program went overboard. Without proper checks, when it tries to work with that variable, it attempts to access a chunk of memory it never had permission to touch and crashes spectacularly.
I hope this write-up helps future programmers.
Have fun and always check user input! NEVER trust user input.
Originally written on April 7, 2018, on my old Github blog.