Expert C Programming: Deep C Secrets https://www.amazon.com/dp/0131774298/
A little dated, still lots of relevant knowledge though.
An especially memorable part for me was how it took 3 pages of language lawyering to explain why this doesn't compile:
foo(const char **p) { }
main(int argc, char **argv)
{
foo(argv);
}It takes quite a bit to explain because the "common sense" is that if one level of pointer indirection allows you to pass non-const where const is expected, then two levels shouldn't be any different. But common sense is wrong, and the compiler is right. And it doesn't require any language lawyering, either - all you need to do is slightly tweak the example to show why exactly it is unsafe.
Now, it is not undefined behavior for the function to not return anything despite having a return type. It is UB for the caller to try to use the returned value, but in this case it's not actually used.
The implicit int feature is really very much deprecated (in fact, it was already removed in C99, almost 20 years ago!). If, for some mysterious reason, you're trying to compile code like that, it's probably very old code dating to before C was an ANSI standard, and void return type was a thing. In such code, it would be pretty common for functions to not return anything, because semantically they don't - it was just a quirk of the language that there was no notion to express a non-value-returning function back then, and so returning an (undefined) int became idiomatic. In C89, this entire behavior was retained largely because backwards compatibility was necessary. C99 finally fixed it.
* Clang vs gcc: doesn't really matter. Clang has slightly better compile times. GCC has really advanced but in some cases still has inferior warning/error messages. Best practice is not to really use GNU c features unless you want to. If you do want to, decide if you want to just use the subset supported by clang or the full shabang from gcc. Beyond that: if you want to support both, test on both before release and do release builds using whichever produces faster or smaller (choose whichever metric you prefer) binaries.
* Build systems: plain old make is sufficient in most cases. I would recommend autoconf over cmake just because it's significantly simpler, although cmake has better cross-platform support (it can generate visual studio build files, for instance). I wouldn't use something outside of make, cmake, or autoconf, even if it seems like it's better in every way than those, because it probably isn't, and even if it is, you lose out on the battle-tested, available-everywhere, and widely-used nature of the above build systems. I shouldn't have to install a build system to build your program, and if I do, I should be able to use that build system to build a significant of other programs too.
* Linting: not really necessary IME. Aim for no compiler warnings, though.
* Yes, use valgrind. Anything it complains about, fix. Uninitialized values and out-of-bounds memory accesses are significantly more important and worrisome than memory leaks (because they represent potential attack vectors), but still, it won't complain about something unless it's actually something that should be complained about.
* Testing: like the other commenter said, just a little bit of macro magic and you're golden.
* Code organization: headers in include/, source files in src/. You can separate src/ into subdirectories if your project grows to sufficient complexity that the source files become difficult to wrangle. Separating the headers into separate directories is probably not necessary, however.
* Not directly c-related, but pick a SANE code style and stick to it.
* Debugging: make a separate target that compiles in debug symbols and disables optimizations, and use gdb (or lldb) on it. You don't need much to get started: 99% of the time, all I do is "break main", "run" ("r" for short), "backtrace" ("bt" for short), "frame <number>" (to switch between stack frames), and "print <variable>" ("p <variable>" for short); it's taken me quite far.
Clang seems to have better sanitizer support, which can be lighter weight initial passes before running valgrind.
I agree with your conclusion but not your reasoning. Attack vectors are way down the list of reasons why uninitialised values and out of bounds memory accesses are higher priority. They're higher priority because they will likely (maybe definitely) crash your program or (even worse) cause it to fail in a frustratingly indeterminate manner. You should be so lucky that your program becomes significant enough to be targetted by "attackers".
node.next = null
It can have a garage unless you assigned a value.This can be hard to debug in production.
gcc main.c
main.c #includes any other .c files that are needed (the term for this appears to be a 'unity build'). Compiling this way is /really/ fast, which is why it's okay to use a dumb build script that always recompiles everything.Also, I do a lot of programming on Windows, where GNU make would be another dependency to install. (Also, in my experience, make is slow on Windows, since they have to emulate fork()). I guess I could use Microsoft nmake, since I assume it's still installed along with Visual Studio, but again, batch files are simpler.
And for GNU tools on Windows, I would heavily recommend MSYS2 these days - having Pacman as the package manager is very nice, and there are already a lot of packages there.
Also, although this is merely a personal quirk that shouldn't persuade anyone else, I've seen enough horrific, unreadable make files to instinctively dislike them by now.
[0]: http://www.man7.org/tlpi/ [1]: https://www.cs.cmu.edu/~213/assignments.html
I often recommend postgres because it's highly commented, and easy to understand.
I can’t recommend it as I haven’t read it yet — it has been on my eyes-bigger-than-my-time-and-energy book pile for a couple of years.
C has lots of edges where it's easy for a beginner to make mistakes. Say for example the minimal range of a char is -127 to 127 (yes, C cares 1's complement machines) or 0 to 255. Not knowing this may result in a code working in one machine while broken on others. So learning the standard is a must, especially, the undefined behaviors.
For the Linux C interface there's https://nostarch.com/tlpi and http://www.apuebook.com/apue3e.html / http://www.unpbook.com/ , all old to different degrees. http://www.danlj.org/lad/ was great but even the second edition http://www.worldcat.org/title/linux-application-development-... is probably out of date by now. ( https://9p.io/cm/cs/upe/index.html is most certainly out of date, but more than that it's almost an anti-C-interface book by comparison to the C-API-centric view of Unix which has become prevalent since, in some part due to the popularity of K&R... https://www.cs.princeton.edu/~bwk/tpop.webpage/ and http://www.catb.org/esr/writings/taoup/ are in the same "anti-C-book" vein.)
CMake is by far the most convenient build tool I've come across for C so far, the link below will give you an idea of how it looks. I find linters mostly a waste of time , but I run all my tests through valgrind before committing; I don't miss programming in C without valgrind at all; it's also an excellent profiler in combination with KCacheGrind. For testing, simple functions and bit of macro magic on top goes pretty far; C programmers tend to value simplicity, you won't find that many epic unit testing adventure frameworks out there. clang or gcc doesn't really matter, I prefer clang's error messages; and both support GNU extensions, which solve most of the boring problems with coding in C.
https://github.com/basic-gongfu/cixl/blob/master/CMakeLists....