Worth mentioning that C is over 40 years old, and was designed to be easily portable across a range of machines that had less compute power and memory than today's smaller microcontrollers.
As a result, a lot of things were left undefined, or were designed in a way to be easy to implement rather than easy to program for.
There existed other programming languages that were better, but their compilers weren't as broadly available, and their better features came at the cost of speed, which at the time was a premium.
> left undefined, or were designed in a way to be easy to implement rather than easy to program for.
I'd tweak your statement a little, or even a lot: "left undefined" most often meant "left to be defined by the compiler writers to fit the architecture of the underlying hardware, in a way that would make it easy to program to beneficially exploit features of the architecture"; and (yes) in a way "that would not be very portable, and might even be subject to change between compilers".
did the underlying machine use 2's complement? did the underlying machine have addressable bytes? big endian? 8, 16, 32 or 36 bits?
These are all things you need to know to write tight efficient code in the days of slow clockspeeds and limited RAM. C let you do that without using assembly, but by using the "undefined" features of the language, because they were clearly defined locally and were features that were very important to be easy to write code for.
consider how you would implement setjump and longjump, or even printf, or efficiently unpack or serialize bits for a communications protocol, without these supposedly "undefined features", or how you would write those if those features were actually undefined. People who put strlen(str) or a divide and a mod in the control expression for a loop would know better if they understood a bit more about the undefined features.
this is in contrast btw with some other things that actually are undefined, such as what the order of evaluation would be for complex expressions making up argument lists, etc.
I'm writing this explanation not so much to explain these technical details to noobs, but rather to get the people who understand this stuff to stop throwing around the term "undefined" with regard to C because they are cooperating in the evisceration of some ideas that are really worth exploring or understanding more deeply.
Sorry, you're absolutely right, and I was lazy in my comment.
There is indeed a big difference between "undefined" behavior and "implementation-defined" behavior.
For example, there's a lot of spooky "undefined behavior" around dereferencing pointers. In one famous case [1], dereferencing a pointer actually led the compiler to skip a later check on whether the pointer was NULL, because if the pointer was already dereferenced then it must have been valid.
Another classic "implementation-defined" detail is what is the size of a "char"? Nowadays we can readily assume it's 8 bits, but that wasn't so guaranteed when C was written!
that's interesting, I've done DSP on paper (and microcode, i'm old school), but never on such an architecture.
and it's interesting to think, "well, that's useful, let's make it a defined behavior!" but then that defined behavior could be a nightmare of inefficiency on a machine with a different architecture. (i mean, probably not for the particular case of 16 bit chars, but in the general case quite unpredictable)
Not really related to C, but I once worked on an SoC where the DSP was mixed 16/24-bit, so they mapped each word to the ARM 32-bit bus. That is, each 4-byte address would map to one 16-byte (or 24-byte) word of the DSP's memory & register space.
But under the DSP was an I2C-based ISP that was glued in, and _its_ 8-bit words were mapped to each of the DSP's 16-bit (/24-bit) words.
So if the ARM wanted to talk to the ISP's control space, it read one 8-bit word of the ISP in each 32-bit word address.
The cherry-on-top was that the DSP was Big-Endian and the ARM was Little-Endian, and so you had to swap all the data you were exchanging with it.
Still the biggest HW WTF I've encountered in my career. This was the STMicroelectronics Nomadik 8810/8815/8820 chips. https://en.wikipedia.org/wiki/Nomadik
Worth mentioning that C is over 40 years old, and was designed to be easily portable across a range of machines that had less compute power and memory than today's smaller microcontrollers.
As a result, a lot of things were left undefined, or were designed in a way to be easy to implement rather than easy to program for.
There existed other programming languages that were better, but their compilers weren't as broadly available, and their better features came at the cost of speed, which at the time was a premium.