If you mean just because it's signed on x86, fair enough; but it sounds as if you think signed is just a more natural option. My intuition goes the other way, for what it's worth.
Anyway, here is one possible reason. It used to be that if you wanted to load a single byte from memory on ARM, the only way to do it always treated it as unsigned. So if you wanted to work with signed chars, you needed explicit extra instructions to do the sign-extension. This isn't true for more recent versions of the ARM ISA -- there's an LDRSB instruction to go with the older LDRB -- but it may be one reason why that choice was originally made.
Functions like toupper() take an int but say "the value must be representable as an unsigned char" so you technically need to cast to "unsigned char" on most platforms, but I doubt anybody uses these functions for anything but ascii. They may work for single-byte locales like iso8859-1 etc if you have the locale env vars set right, but they won't work for non-ascii chars encoded as utf-8, which is generally what you always want to use these days. (There's towupper() which works with 2-byte locales like UCS-2, which is utf-16 without surrogate pairs, and can't represent all the new unicode chars, but you probably don't want to go there, you want some modern unicode library that works properly with utf-8 or UCS-4.)
The only use for char that ever seemed intuitively reasonable to me was to hold ascii characters.
This means you could implement your own versions of memcpy, fread and fwrite by casting the void * arguments to char * , but if you cast them to uint8_t * , your code might not be correct.
I would tend to prefer explicitly using unsigned char or signed char rather than plain char though, partly to signal that I am treating the bytes as integers rather than character. (Actually I would still use uint8_t even though I just learnt it might not be unsigned char, because it looks clearer in my eyes, but I'm not sure I should admit it here...)
That's all just having fun, I like the consistency argument and the fact (is it a fact?) that char is signed on most platforms.
Not sure if that's just a typo, but you would use a signed char, which is a different type to char even on implementations where char is signed. Part of the reason for this, of course, is because char can be unsigned so if you want a signed integer you have to specify that. But more philosophically, unsigned char and signed char are numerical types that are not meant to be characters (despite their names), whereas char is a character type that just happens to be backed by an integer.
Indeed I believe that int8_t is almost always just a typedef for signed char (but I still would use int8_t where available for clarity).
> Whether plain chars are signed or unsigned is machine-dependent, but printable characters are always positive.
Kernighan, Brian W.. C Programming Language (p. 36). Pearson Education. Kindle Edition.
But if you think of char as a typedef [u]int8_t, then I do get the consistency argument.
But why would you expect char to represent an generic integer in the first place?
It's just a wrapper for bytes, which in their nature are just bits devoid of traditional mathematical, numerical value.
In C, a function argument or return value of type char gets promoted to int. So, code that uses char a lot does a lot of such promotions.
I think you're confusing standard C and machine/implementation-specific behavior. (If not for yourself, for people who read your comment.)
stdint.h is nearing 20 years old now, I don't think it counts as a "newfangled thing" anymore.
But yes, there is plenty of very obsolete software still out there.
In the world of C, it is. Custom compilers for particular embedded systems, such as MCUs, often only speak C89 and nothing else. C99 still hasn't found complete and widespread acceptance.
Because sometimes you are not the author of the software you want to port. Of course, when you start porting the software, you'd use a (predefined) typedef.
You need some low-level justification for using an uint32_t and such: like conforming to some externally imposed data format or memory-mapped register bank.
The justification for <stdint.h> is that it's better to have one way of defining these types in the language, than every program and every library in every program rolling its own configuration system for detecting types and the typedefs which name them. Let's see, for calling Glib, we use guint32, for OpenMax we use OMX_U32, ...
Funny how these situations persist almost 20 years after stdint.h was standardized (and a number of more years after being draft features).
An expression of type int doesn't undergo any default promotion to anything, period; the widening promotion is only applied to the char and short types.
An int operand may convert to the type of an opposite operand in an expression, and an int argument or return value will convert to the parameter or return value type. That applies to int32_t also.
Anyway, you really have to know the rules to be working in C. Someone who uses fixed width types for everything doesn't know what they are doing and are just taking swipes at imaginary ghosts in the dark out of fear.
I'm guessing you've never ported code that was written for 32-bit ints back to a 16-bit architecture.
Always better to make your type sizes crystal-clear to the reader, IMO, even if it risks using a suboptimal word size on some other platform down the line.
This is backwards. You should use uint32_t etc. like any higher-level language would, unless you have specific reasons to know you need a machine-sized type. Making your code behave differently in some arbitrary, untested way on machines with different default int sizes isn't going to make it "less limited", it's just going to make it broken.
To me it seems like a weird design choice that only complicates porting software from x86.