First of all, it must be noted that C was invented during a very early computer era, based on B and BCPL languages from the 1960s. Lots of different experimental computers existed back then - nobody quite knew which ones would survive or become industry standard.
Because of this, the C language even supports three different forms of signed number formats: 1's complement, 2's complement and signed magnitude. Where 1's complement and signed magnitude are allowed to come with exotic behavior such as trap representations or padding bits. But some 99.999% of all modern real-world computers use 2's complement, so all of this is very unhelpful.
Why we need to define these data type so vague
We don't. Not giving the integer types a fixed size and signedness was arguably a naive design mistake. The rationale back in the days was to allow C to run on as many different computers as possible. Which is as it turns out, not at all the same thing as porting C code between different computers.
Lazy programmers might find it handy to sloppily spam int
everywhere without thinking about integer limits, then get a "suitable, large enough integer of the local signedness". But that's not in the slightest helpful when we for example need to use exactly 16 bits 2's complement. Or when we need to optimize for size. Or when we are using an 8 bit CPU and want to avoid anything larger than 8 bits whenever possible.
So int
& friends are not quite portable: the size and signedness format is unknown and inconsistent across platforms, making these so-called "primitive data types" potentially dangerous and/or inefficient.
To make things worse, the unpredictable behavior of int
collides with other language flaws like implicit int type promotion (see Implicit type promotion rules), or the fact that integer constants like 1
are always int
. These rules were meant to turn every expression into int
, to save incompetent programmers from themselves, in case they did arithmetic with overflow on small, signed integer types.
For example int8_t i8=0; ... i8 = i8 + 256;
doesn't actually cause signed overflow in C, because the operation is carried out on type int
, which is then converted back to the small integer type int8_t
(although in an implementation-defined manner).
However, the implicit promotion rules always caused more harm than good. Your unsigned short
may suddenly and silently turn into a signed int
when ported from a 16 bit system to a 32 bit system. Which in turn can create all manner of subtle bugs, particularly when using bitwise operators/writing hardware-related code. And the rules create an inconsistency between how small integer types and large integer types work inside expressions.
To solve some of these problems, stdint.h
was introduced in the language back in 1999. It contains types like uint8_t
that are guaranteed to have a fixed size no matter system. And they are guaranteed to be 2's complement. In addition, we may use types like uint_fast8_t
to let the compiler pick the fastest suitable type for a given system, portably. Most professional C software nowadays - embedded systems in particular - only ever use the stdint.h
types and never the native types.
stdint.h
makes it easier to port code, but it doesn't really solve the implicit promotion problems. To solve those, the language would have to be rewritten with a stronger type system and enforce that all integer converts have to be explicit with casts. Since there is no hope of C ever getting fixed, safe subsets of the language were developed, such as MISRA-C and CERT-C. A significant portion of these documents are dedicated to solving implicit conversion bugs.
A note about size_t
specifically, it is guaranteed to be unsigned and "large enough", but that's about it. They didn't really give enough thought about defining what it's supposed to represent. The maximum size of an object? An array? Or just the type returned by sizeof
? There's an unexpected dependency between it and ptrdiff_t
- another language flaw - see this exotic problem I ran into when using size_t
to represent the maximum allowed size of an array.