📜 ⬆️ ⬇️

Undefined behavior with outdated ANSI C function declarations


The ANSI C standard defines the concept of a function prototype , which is a subset of a function declaration that indicates the types of input parameters. Prototypes were introduced to eliminate the flaws that ordinary function declarations have.


Thus, specifying a list of parameter types in parentheses of a function prototype is necessary, otherwise the expression will be recognized by the compiler as an outdated function declaration, which can lead to ambiguous situations described in this article.


Outdated Prototypes


A function declaration enters the return type of the function and its identifier into the specified scope . Please note that not all function declarations can be considered prototypes, but only those that have a list of types of input parameters.


Thus, the first expression of the code below is a declaration, but not a function prototype. The following expression can rightfully be considered a prototype, since it specifies the types of its parameters:


/* #1 (   "foo") */ void foo(); /* #2 (  "bar") */ void bar(int count, const char *word); 

Outdated definitions


Let's go straight to 1972 (the year when the C language was released) and recall how the programmers of that time defined their functions. Let me remind you that the definition of a function associates its signature with the corresponding executable block (body). This code demonstrates the definition of the add function in the K & R style:


 void add(right, left, result) int right; int left; int *result; { *result = right + left; } 

As you may have noticed, in this record, parentheses identify the function, but do not contain any types of input parameters. The same property has the "classic" function declarations, described in the previous section.


Ambiguous situations


It is possible that if the new syntax of prototypes and definitions of functions introduced by the ANSI C standard is not followed, it is possible that difficult ambiguous situations can arise. Consider an example:


 #include <stdio.h> #include <stdint.h> #include <inttypes.h> #include <limits.h> /*    "print_number" */ void print_number(); int main(void) { /*  */ print_number((double)13.359); print_number((double)9238.46436); print_number((double)18437); /*    */ print_number(UINT64_MAX); print_number("First", "Second", "Third"); print_number(NULL, "Breakfast", &print_number); } void print_number(double number) { printf(" : [%f]\n", number); } 

Let's analyze this program. The correct print_number function print_number declared without specifying a list of parameter types, as a result of which you are able to call this function with any arguments. The program compiled without errors and printed the following result:


 $ gcc illegal.c -o illegal -Wall $ ./illegal  : [13.359000]  : [9238.464360]  : [18437.000000]  : [0.000000]  : [0.000000]  : [0.000000] 

Also note that even with the -Wall flag, the -Wall compiler gcc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0 did not generate any warnings (but it would be highly desirable).


It will not be difficult to print_number this program; all you have to do is to add a double number in parentheses to the print_number function print_number on the seventh line, after which any compiler following the standard will indicate errors in the main() function:


 $ gcc -Wall illegal.c -o illegal illegal.c: In function 'main': illegal.c:17:22: error: incompatible type for argument 1 of 'print_number' print_number("First", "Second", "Third"); ^~~~~~~ illegal.c:7:6: note: expected 'double' but argument is of type 'char *' void print_number(double number); ^~~~~~~~~~~~ illegal.c:17:9: error: too many arguments to function 'print_number' print_number("First", "Second", "Third"); ^~~~~~~~~~~~ illegal.c:7:6: note: declared here void print_number(double number); ^~~~~~~~~~~~ illegal.c:18:22: error: incompatible type for argument 1 of 'print_number' print_number(NULL, "Breakfast", &print_number); ^~~~ illegal.c:7:6: note: expected 'double' but argument is of type 'void *' void print_number(double number); ^~~~~~~~~~~~ illegal.c:18:9: error: too many arguments to function 'print_number' print_number(NULL, "Breakfast", &print_number); ^~~~~~~~~~~~ illegal.c:7:6: note: declared here void print_number(double number); ^~~~~~~~~~~~ 

Functions without parameters


Also note that the indication of the keyword void in parentheses prototypes and definitions of functions that do not take parameters is highly desirable (but not necessarily). Failure to comply with this advice can lead to equally sad consequences.


 #include <stdio.h> /*    "do_something" */ void do_something(); int main(void) { /*  "do_something"       */ do_something(NULL, "Papa Johns", 2842, 1484.3355); } void do_something() { puts("I am doing something interesting right now!"); } 

To correct the code above, it is necessary to insert the void keyword in the definition and declaration of the do_something() function; otherwise, this program will compile without errors. In this example, the main() function is also defined with the void token in the parameters, although this is not necessary.


Conclusion


I was inspired to write this article by Stephen Prath’s book “The C programming language. Lectures and exercises. The sixth edition”, and specifically the section “Functions with arguments” of the fifth chapter.


')

Source: https://habr.com/ru/post/456682/


All Articles