The thing I love about C is that it the most obedient of all languages and never assumes you are an idiot. Never does it tell me I'm about to blow my own foot off - it just does it and I've learned from the experience. Due to this it taught me how to think about things first and know of all the consequences of doing everything.
Doing exactly what you told it is fine until you hit one of the many undefined behaviours and then it does exactly what you thought you told it only some of the time.
There's a lot of undefined behaviours to know about, and not all of them are obvious. You can be merrily compiling your programs fine on GCC and Clang and your intended behaviour seem entirely obvious, only to have it blow up on another compiler or with some different compiler flags.
That said, the undefined behaviours do allow the compilers to do some very tight optimisations, so swings and roundabouts.
Not everywhere C's undefinededness lies; e.g. shifting more sizof(int) bits - in case the shift parameter is dependent on your input.
"Decent test harness" is like "sufficiently smart compiler". Everyone assumes it will be there when planning, but in practice it is only available in very specific and not-generally-useful cases.
Here's a one-file, ISC-licensed test framework I wrote for ANSI C: "greatest" (https://github.com/silentbicycle/greatest). It doesn't depend on any dynamic allocation, or anything beyond C89, and compiles with zero warnings under -Wall -pedantic.
The following program compiles without warnings using gcc 4.7.0. The compiler uses the undefined behavior of accessing a variable that may not have been initialized to turn the conditional into one that's always taken, and then folds it away.
==46592== Conditional jump or move depends on uninitialised value(s)
==46592== at 0x100000E90: foo (in ./a.out)
==46592== by 0x100000ED3: main (in ./a.out)
The matter is, valgrind exists. Everything is a C deficiency that valgrind is able to overcome decently is no longer a C deficiency practically speaking.
clang -Weverything uninit.c
uninit.c:3:6: warning: no previous prototype for function 'foo' [-Wmissing-prototypes]
void foo(int bar) {
^
uninit.c:10:6: warning: variable 'lol' may be uninitialized when used here [-Wconditional-uninitialized]
if (lol == 7) {
^~~
uninit.c:4:9: note: initialize the variable 'lol' to silence this warning
int lol;
^
= 0
uninit.c:37:3: warning: no newline at end of file [-pedantic,-Wnewline-eof]
*/
^
3 warnings generated.
I've been working on some ideas about how different languages lead to different sorts of premature optimization problems. I see a lot of OOP problems where developers try to avoid re-initializing or re-instantiating an object, and end up with weird partial-initialization or wrong initialization edge cases. I think language structure encourages this, and have been thinking about what other sorts of premature-optimization problems languages encourage.
This is exactly why I'm learning the various ASM right now. I am not content (depending on what I'm trying to do, of course) with a program just doing "something" that works.
Usually when something doesn't go as quick as you were hoping, profiling is unhelpful and you have to take the python bytecode to bits to find out what the hell it's doing inside.
When you are attempting to do a very specific thing. Not going to go into what that might be, unless you're curious, but sometimes you do need to know exactly or as close to exactly how your code is operating.
*NIX is the same.