Refer this
All of this is mostly in practice only relevant to varargs but due to quirks like being able to define a function protoype with no parameters ( in C, not in C++ ), this allows for some interesting code that compiles.
When the compiler does not know the types, it does what’s called “default argument promotions”. The rules are roughly:
- Any integer type with a rank lower than int is promoted to int if int can hold all values of the type. Otherwise, it’s promoted to unsigned int. So for a system where
unsigned shortandunsigned intare same in size,unsigned shortwill be promoted tounsigned intnot toint. - Any floating-point type is promoted to double.
- Rest all stay the same, pointers basically.
#include <stdio.h>
int sum();
int main() {
// will compile with no errors
printf("Sum of 2 ints: %d\n", sum(5, 10));
printf("Sum of 3 ints: %d\n", sum(1, 2, 3)); // 3 is ignored here
printf("Sum with float: %d\n", sum(5.5, 10)); // promotion to double then read as int
printf("Sum with double: %d\n", sum(5.5l, 10)); // double that is read as int, same as above
printf("Sum with char: %d\n", sum('A', 'B')); // chars get promoted to int
printf("Sum with string: %d\n", sum("hello", 42)); // Undefined behavior as reads int no of bytes
return 0;
}
// Function definition - compiler doesn't know about it when it's at the
// above calls
int sum(int a, int b) {
return a + b;
}This compiles and runs though most compilers warn if warning are enabled + this is a compile error with C++.
How do I then signify with a protoype that I actually don’t expect any parameters? You can use void in the parameter list.
int sum(void) changing this above make this compile errors as the arguments are not compatible.
int sum(int); is also treated as a protype that has type information. It’s only when you don’t specify anything that the compiler does not know the types and does default argument promotions.
Note that this is different from implicit conversions allowing use of expressions that are smaller than the type to be promoted. Example is usage of char as integer in an expression.
For a spinoff on this, what if the type was char or float, like
int sum(char a, float b){};Compile failure as it expects one of the possible promoted types only.
Case with varargs
Since with varargs, compiler needs to read memory in offsets, you cannot know the type beforehand and need to hardcode “something” in the implementation, which is why printf uses % formatters to specify the type and then does something like va_arg(args, type) to read the next argument.
Note that specifying char as the type above is warning and incorrect behaviout since va_arg will read the next 1 byte but the type is promoted to int so this misaligns the stack and causes undefined behavior. It’s also a compiler warning ( not an error, but reading it like this fails at runtime with an illegal hardware instruction ( for my case atleast ) ) . I guess they could’ve made it not read in offsets of 4 or 8 ( since you DO know the type and number of bytes to read when you do va_arg(args, int) ) but probably this was for performance and implementation reasons, I don’t know.
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
void var_arg_test(int num, ...) {
va_list args;
va_start(args, num);
for (int i = 0; i < num; i++) {
int value = va_arg(args, int); // change to char = runtime error of misalignment, prints illegal hardware instruction for me
printf("Argument %d: %d\n", i + 1, value);
}
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <3> [3 args]\n", argv[0]);
return 1;
}
int num_args = atoi(argv[1]);
var_arg_test(num_args, argv[2], argv[3], argv[4]);
return 0;
}I could also have used some “sentinel” value like -1 or 0 to instead of taking the number of args as input.
The case with printf
Prints is implementd as a variadic function and hence the arguments undergo default argument promotions.
A practical case where this needs to be take care of is when trying to print hex values for a byte ( read as char values from a file and returned as an int ) which gets promoted to an int.
Prior to the consider this case:
#include <stdio.h>
int main() {
char byte = 0xF0; // two hex = 1 byte
printf("Integer: %d\n", byte);
printf("Hex: %02X\n", byte);
}Output being:
Integer: -16
Hex: FFFFFFF0
How is a negative number stored in memory? It’s the two’s complement of the binary of absolute. Note that two’s complement of two’s complement gives back the original absolute value. Postive is stored as it is.
0xF0 is 11110000 in binary. Since char is signed, it’s read as -(two’s complement) which is the general way for representing signed numbers.
11110000 (memory)
00001111 (inverted)
00001111 + 1 = 00010000 (two's complement) ( this is 16 )
When this get’s converted to an int, sign extention happens, which is basically a way to preserve the value as well as the sign and doing it is trivally copying the sign bit to fill the rest of the bits.
So the integer hex representation becomes FFFFFFF0 ( which is -16 ). Note I need to do two’s complement since the sign bit is set, othrwise it’s just the binary representation as it is.
This explains the output above.
#include <stdio.h>
int main() {
char byte = 0x10; // two is 1 byte
printf("Integer: %d\n", byte);
printf("Hex: %02X\n", byte);
}Sign extension on positive is simply extendin with 0. So you get
Integer: 16
Hex: 10
What if it’s an unsigned char?
#include <stdio.h>
int main() {
unsigned char byte = 0xF0; // two is 1 byte
printf("Integer: %d\n", byte);
printf("Hex: %02X\n", byte);
}Output is:
Integer: 240
Hex: F0
Since it’s unsigned, there’s no sign bit and hence no extension, it’s simply read as it is.
What if I try to assign a larger value?
#include <stdio.h>
int main() {
unsigned char byte = 0x2F0; // two is 1 byte
printf("Integer: %d\n", byte);
printf("Hex: %02X\n", byte);
}Bytes are read from right to left so only 0xF0 is read and the rest is ignored, with a compiler warning about the conversion.
Another way where I still use the char type is to some bit masking.
#include <stdio.h>
int main() {
char byte = 0xF0; // two is 1 byte
printf("Integer: %d\n", byte);
printf("Hex: %02X\n", byte & 0xFF); // mask to get last byte
}Output is:
Integer: -16
Hex: F0
OxFF is treated as an int literal, char being used in an int expression promotes it to an int and then the bitwise AND is performed. In such a case, the sign bit will never be set since has that as 0 hence the output is F0 and not FFFFFFF0.