32 ms·
Everything I wish I knew when learning C
- felipelalli 4y agoIs there a similar article but to Rust?
- asicsp 4y agoThese might help: * https://dystroy.org/blog/how-not-to-learn-rust/ https://dystroy.org/blog/how-not-to-learn-rust/ * https://federicoterzi.com/blog/12-rust-tips-and-tricks-you-might-not-know-yet/ https://federicoterzi.com/blog/12-rust-tips-and-tricks-you-m...
- torstenvl 4y agoDecent article. A couple minor points: c89 or c99, not c98. Plain static variables aren't thread-safe by default, true, but there's also _Thread_local
- gpderetta 4y agoI haven't written actual C code in decades. Did C11 get magic statics with thread-safe initialization like C++11? Does C even have non-trivial initialization of statics local variables?
- AstralStorm 4y agoC does have initialization of local variables, including static. It does not have any thread safety in standard for these, however, so no thread safe singletons. (You can get them initialized by linker or runtime safely, of course, e.g. ELF .bss and UCRT on Windows) You can use atomics to implement such a singleton if they are available. This is one of the main reasons why C is more portable than C++.
- torstenvl 4y agoI don't know why it would be magic to have thread-local storage. Thread-local variables are either static or extern. Thread-local static variables are initialized like normal static variables (initialization on declaration line occurs on first instantiation), but with a separate copy per thread. N1570, sec. 6.2.4, para. 4: An object whose identifier is declared with the storage-class specifier _Thread_local has thread storage duration. Its lifetime is the entire execution of the thread for which it is created, and its stored value is initialized when the thread is started. There is a distinct object per thread, and use of the declared name in an expression refers to the object associated with the thread evaluating the expression. N1570, sec. 6.7.9, para. 10: If an object that has static or thread storage duration is not initialized explicitly, then [it is initialized to NULL or 0 as appropriate, including all-bits-zero padding in structs]
- gpderetta 4y agoNot thread local storage. Plain function statics are initialized on first use in c++ (if the initializer is not trivial), and this initialization is thread safe. There is a simple algorithm to implement it that has very little overhead on the already initialized case that is known as magic statics (it is basically an instance of the double checked locking pattern).
- nadavision 4y agoThanks for submitting this. I'm teaching myself C so these high level overviews are super useful for improving my intuition. In the following example, shouldn't there be an asterisk * before the data argument in the getData function call? The way I understand it the function is expecting a pointer so you would need to pass it a pointer of the data object. > "If you want to “return” memory from a function, you don’t have to use malloc/allocated storage; you can pass a pointer to a local data: void getData(int *data) { data[0] = 1; data[1] = 4; data[2] = 9; } void main() { int data[3]; getData(data); printf("%d\n", data[1]); } "
- torstenvl 4y agoNo, it's correct. The asterisk is a little inconsistent, in that it means two opposite things. In the declaration it means "this is a pointer." However, in an expression, it means "this is the underlying type" and serves to dereference the pointer. int a = 5; int *x; // this is a pointer x = &a; int c = *x; // both c and *x are ints If it were *data, it would be equivalent to *(data + 0), which is equivalent to data[0], which is an int. You don't want to pass an int, you want to pass an *int.
- karatinversion 4y agoThe way I got this to stick in my head was to always think of * as dereferencing, and tell myself that int *x; is declaring that the type of *x is int.
- unsafecast 4y agoIt's not just a memorization trick; that's exactly what the statement means. If you do int *x, y; You're saying that both *x and y are integers.
- pilif 4y ago... which is why I never understood why this is the convention rather than int* x, y. Does somebody know?
- hddqsb 4y agoSome constructive feedback: > Here are the absolute essential flags you may need. I highly recommend including `-fsanitize=address,undefined` in there (docs: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.h...). (Edit: But probably not in release builds, as @rmind points out.) > The closest thing to a convention I know of is that some people name types like my_type_t since many standard C types are like that Beware that names beginning with "int"/"uint" and ending with "_t" are reserved in <stdint.h>. [Edited; I originally missed the part about "beginning with int/uint", and wrote the following incorrectly comment: "That shouldn't be recommended, because names ending with "_t" are reserved. (As of C23 they are only "potentially reserved", which means they are only reserved if an implementation actually uses the name: https://en.cppreference.com/w/c/language/identifier https://en.cppreference.com/w/c/language/identifier. Previously, defining any typedef name ending with "_t" technically invokes undefined behaviour.)"] The post never mentions undefined behaviour, which I think is a big omission (especially for programmers coming from languages with array index checking). > void main() { As @vmilner mentioned, this is non-standard (reference: https://en.cppreference.com/w/c/language/main_function https://en.cppreference.com/w/c/language/main_function). The correct declaration is either `int main(void)` or the argc+argv version. (I must confess that I am guilty of using `int main()`, which is valid in C++ but technically not in C: https://stackoverflow.com/questions/29190986/is-int-main-without-void-valid-and-portable-in-iso-c https://stackoverflow.com/questions/29190986/is-int-main-wit...). > You can cast T to const T, but not vice versa. This is inaccurate. You can implicitly convert T* to const T*, but you need to use an explicit cast to convert from const T* to T*.
- rmind 4y agoCertainly yes, but for debug builds and tests. It can be heavyweight for production.
- deleted 4y ago[deleted]
- sedeki 4y agoHow can a (badly chosen) typedef name trigger _undefined behavior_, and not just, say, a compilation error...? I find it difficult to imagine what that would even mean.
- unwind 4y agoThis looks decent, but I'm (highly) opposed to recommending `strncpy()` as a fix for `strcpy()` lacking bounds-checking. That's not what it's for, it's weird and should be considered as obosolete as `gets()` in my opinion. If available, it's much better to do the `snprintf()` way as I mentioned in a comment last week, i.e. replace `strcpy(dest, src)` with `snprintf(dst, sizeof dst, "%s", src)` and always remember that "%s" part. Never put src there, of course. There's also `strlcpy()` on some systems, but it's not standard.
- masklinn 4y agostrncpy does have its odd and rare use-case, but 100% agree that it is not at all a “fix” for strcpy, it’s not designed for that purpose, and unsuited to it, being both unsafe (does not guarantee NUL-termination) and unnecessary costly (fills the destination with NULs). The strn* category was generally designed for fixed-size NUL-padded content (though not all of them because why be coherent?), the entire item is incorrect, and really makes the entire thing suspicious.
- deleted 4y ago[deleted]
- AstralStorm 4y agoThen there are strn*_s since C11 (and available before that on many platforms) which do exactly what you want.
- masklinn 4y agoLol no. These are the Annex K stuff which Microsoft got into the standard, which got standardised with a different behaviour than Windows’ (so even on windows following the spec doesn’t work) and which no one else wants to implement at all. And they don’t actually “do exactly what you want”, see for instance N1967 (“field experience with annex k”) which is less than glowing.
- mtlmtlmtlmtl 4y ago>sizeof dst Note that this only works if dst is a stack allocated(in the same function) array and not a char *
- jmclnx 4y agoNice, slowly "teaching" someone at work c for use on AIX. I will send him the link. I am not a very good teacher :)
- deleted 4y ago[deleted]
- donio 4y agoCurious what the backstory is for this (someone learning C for the first time on AIX in 2022)
- vmilner 4y agoIs void main() Still non-standard in C?
- zabzonk 4y agoyes
- vmilner 4y agoThanks - it certainly was undefined in the original standard.
- selimthegrim 4y agoBorland was to blame for it I thought
- deleted 4y ago[deleted]
- curling_grad 4y ago"Teaching C" seems to be a good read too: https://news.ycombinator.com/item?id=32798826 https://news.ycombinator.com/item?id=32798826
- wyldfire 4y ago> -Werror to turn warnings into errors. I recommend always turning on at least -Werror=implicit, which ensures calling undeclared functions results in an error(!) "Implicit declarations" is such a frustrating "feature" of C. Thankfully, in more recent clang builds this warning is enabled by default.
- sfusato 4y agoI wish I'd have known how bad everyone was in it, not just me...
- _ache_ 4y ago> You can cast T to const T, but not vice versa. Humm, actually, you can. But guess what ? Modification of the underlying data is an UB !
- pif 4y agoWhich is the only possible logical consequence of modifying something that others asked you not. const_cast is there for buggy (or legacy) libraries where const was not specified explicitly, but is implicit in the behavior.
- _ache_ 4y agoIt can happen easily if you don't pay attention with only the standard library. const char *hello = "Hello World !", *world = "World"; char *tmpStr = strstr(hello, world); // IMPLICITE cast from const to non const if(tmpStr) { *tmpStr = 0; // Oupss } C is hard.
- synergy20 4y agoso how to safely use strstr then? this is truly a bug hard to find, yet another caveat. not sure if fuzz testing can help here. I wonder how many implicit-const-to-non-cost API are in ANSI C and Posix C
- LegionMammal978 4y agoIt's only UB if the original variable is declared with a const type. If you convert a modifiable T * into a const T *, then cast it back into a T *, then you can modify it without any issues.
- vmilner 4y agoThis used to be my bible when doing full time C programming around 2000 (together with the standard docs) but I’m out of date with the latest standard updates (as is this) but it may still be of interest. https://c-faq.com/ https://c-faq.com/
- MisterTea 4y ago> I get an int* value which has 5 ints allocated at it. Not crazy about this array explanation. Better wording would be: "I get a memory address that points to the first byte of a chunk of memory that is large enough to hold 5 ints and tagged as int" > Essential compiler flags Nitpick, but this is only true if you are on a gcc/clang platform. > If you want to “return” memory from a function, you don’t have to use malloc/allocated storage; you can pass a pointer to a local data It should be specified the memory must be passed by the caller for this to work. You can't create a var in a called function and return that (you can but you will get undefined behavior as that memory is up for grabs after the function returns). > Integers are very cursed in C. Writing correct code takes some care. Cursed how? No explanation. Overall this article is not very good. I would add these to the lists: General resources: Read the K&R book. Read the C spec. Be familiar with the computer architectures you are working on. Good projects to learn from: Plan 9. Seriously. It's an entire OS that was designed to be as simple as possible by the same people who made Unix and the C.
- pif 4y ago> Be familiar with the computer architectures you are working on. But keep in mind that C is not a portable assembly!
- modernerd 4y agoLove the intro and overview — looking forward to more! These weren't mentioned in the post but have been very helpful in my journey as a C beginner so far: - Effective C by Robert C. Seacord. It covers a lot of the footguns and gotchas without assuming too much systems or comp-sci background knowledge. https://nostarch.com/Effective_C https://nostarch.com/Effective_C (Also, how can you not buy a book on C with Cthulhu on the cover written by a guy with _three_ “C”s in his name?) - Tiny C Projects by Dan Gookin, for a “learn by doing” approach. https://www.manning.com/books/tiny-c-projects https://www.manning.com/books/tiny-c-projects - Exercism's C language track: https://exercism.org/tracks/c https://exercism.org/tracks/c - Computer Systems, A Programmer's Perspective by Randal E. Bryant and David R. O'Hallaron for a deeper dive into memory, caches, networking, concurrency and more using C, with plenty of practice problems: https://csapp.cs.cmu.edu/ https://csapp.cs.cmu.edu/
- deleted 4y ago[deleted]
- sylware 4y agoC syntax is already too rich and complex. typedef/enum/(_Generic)/etc should go (fix/cleanup function pointer type declaration). Only sized primitive types (u32/s32,u64/s64,f32/f64 or udw/sdw,uqw/sqw,fdw/fqw...). We would have only 1 loop statement "loop{}", no switch. I am still thinking about "anonymous" code blocks for linear-code variable sub-scoping (should be small compile-unit local function I guess). No integer promotion, no implicit cast (except for void* and maybe for literals like rust) with explicit compile-time/runtime casts (not with that horrible c++ syntax). Explicit compile-time/"scoped" runtime constants: currently it is only "scoped" runtime, with on some optimization passes to detect if the constant would be compile time. extern properly enforced for function plz (aka write proper headers with proper switches), and maybe "local" instead of "static" for compile-unit-only functions since all functions in a compile-unit are "static" anyway like global variables. "Non-standard": ban attributes like binary format visibility (let the linker handle the fine details), packed (if your struct is packed and not compatible with the C struct, it should be byte defined, with proper pointer casts), etc. Fix the preprocessor variable argument macro for good (now it is a mess because of gcc way and c++ ISO way, I guess we will stick to gcc way). With the preprocessor and some rigorous coding, we should be able to approximate that with a "classic" C compiler, since we mostly remove stuff. Was told that many "classic" C compiler could output optional warnings about things like implicit casts and integer promotions. In theory, this should help writting a naive "C-" compiler much more easily than a "classic" C compiler and foster real life alternatives. I am sure stuff I said are plain broken as I did not write a C compiler. I wonder how far rust is from this "C-" as it is expected to have a much less rich and complex, but more constraint, syntax than C.
- RustLove 4y agoThere's a lot in "C", but I don't see anything in it that really isn't needed to serve its modern purpose. In 2022 "C" is used as a portable assembly language. When you really need to control where and how memory is allocated, and represent data structures used directly by the hardware in a high-level language.
- pif 4y ago> In 2022 "C" is used as a portable assembly language. No! Only poor programmers use C while reasoning assembly, and then they complain about undefined behaviour.
- quietbritishjim 4y ago> Declaring a variable or parameter of type T as const T means, roughly, that the variable cannot be modified. I would add "... cannot be modified through that pointer". (Yes, in fairness, they did say "roughly".) For example consider the following: void foo(int* x, const int* y) { printf("y before: %d\n", *y); *x = 3; printf("y after: %d\n", *y); } This will print two different values if you have `int i = 1` and you call `foo(&i, &i)`. This is the classic C aliasing rule. The C standard guarantees that this works even under aggresive optimisation (in fact certain optimisations are prevented by this rule), whereas the analogous Fortrain wouldn't be guaranteed to work.
- pm2222 4y ago*y in both printf, right?
- vardump 4y agoA small detail: you probably meant printf("y before: %d\n", *y);
- quietbritishjim 4y agoOops you're right! Fixed now thanks.
- pafje 4y agoYou already know this, but I would add that under strict aliasing rules, this is only valid because x and y point to the same type. The most common example is when y is float* and someone tries to access its bitwise representation via an int*. (Please correct me if I'm wrong) https://gist.github.com/shafik/848ae25ee209f698763cffee272a58f8 https://gist.github.com/shafik/848ae25ee209f698763cffee272a5...
- nottorp 4y agoC is easier if you first learn assembler for an architecture or two :)
- wheelerof4te 4y agoEverything is easier once you torture yourself with trying to learn Assembler :)
- nottorp 4y agoThe idea is not to write apps for a living in assembler, but to get an idea of what's going on under the hood. Just as C will help understanding what's going on under the hood of Javascript/Python etc.
- webstrand 4y agoDefinitely pick a simpler assembly than x86 assembly, and it's not so bad. I learned 68HC11 assembly which has been a boon for understanding what's happening underneath the hood.
- snvzz 4y agoEven today, I still find 68000 the most comfortable. It is almost easier than writing C.
- nottorp 4y agoHmm speaking of nostalgia, are there hobby z80 boards? Perhaps with enough peripherals to run cp/m? I do mean real z80s not emulators.
- snvzz 4y agoThe Retrobrew community has designed a few of them, even as SBCs[0]. There's definitely more elsewhere. 0. https://www.retrobrewcomputers.org/doku.php?id=boards:sbc:start https://www.retrobrewcomputers.org/doku.php?id=boards:sbc:st...
- wheelerof4te 4y agoHere is another one: A lot of things can be done without using malloc and free. Use these functions to manage memory in large clusters. Let the compiler deal with the rest.
- pjc50 4y agoThe elderly "C Traps and Pitfalls" http://www.literateprogramming.com/ctraps.pdf http://www.literateprogramming.com/ctraps.pdf was an extremely helpful book when I was learning C, although I don't know how well it's held up.
- nayuki 4y agoI skimmed it and all the points made still appear to be true today. Namely, all the C language rules and the human psychological pitfalls.
- fsloth 4y agoI would add to this: 1. Visual Studio debugger is really, really good and you should use it to step through your program (or equivalent IDE based workflow). 2. Learn to compile your code with memory safeguards and use the tooling around them. Specific depends on the platform. On POSIX address sanitizer is good I hear. On Windows you can use the debug CRT and gflags to instrument your binary really well.
- rahen 4y agoBesides Visual Studio (the Windows IDE, not VS Code), QT Creator also has an excellent debugger. Probably the best open source and cross platform C/C++ IDE around.
- Sohcahtoa82 4y agoPeople should be using IDEs with modern debuggers anyways. Being able to step through your code one line at time and instantly see the values of all your variables is going to be FAR more effective than adding a bunch of print statements, no matter what language you're using. It always blows my mind to hear about how many engineers don't know how to use their IDE's debugger and don't know what a breakpoint is.
- fsloth 4y agoPerhaps there is not enough training available on these tools. Raw terminal GDB also is not very user friendly so if someone has experience from that it might be a disencouragement.
- Sohcahtoa82 4y agoIt really should be a standard part of any school curriculum, book, website, or any other media that people consume to learn programming. Debugging is such an integral skill to software engineering that it's beyond inexcusable that it's not taught. Graphical debuggers aren't even hard to use. You can learn PyCharm's in an hour. Learn how to set a break point, examine local variables, learn the different step buttons, and view the function call stack and the local variables at each level of the stack. Heck, maybe people wouldn't struggle with recursion so much if they were taught how to examine the call stack using the debugger, showing what happens when function A calls B which calls C, examine the local variables at each level of the stack, including an example where A and B both have a local variable called "x", and then note that function A calling itself is not a special case and adds another level to the stack with a new "x". Without knowing how to use a debugger, learning to program feels like programming a black box. Sure, a bunch of "print" statements help, but nothing beats stepping through code line-by-line.
- spacedcowboy 4y ago‘ You can’t extend structs or do anything really OO-like, but it’s a useful pattern to think with’ That’s not quite true. If you define 2 structs so that they start the same (eg: both with “int x; int y” in your example), pointers can be passed to functions with either struct type. You can use this to add fields (eg: int z) to structures, and extend a 2d vector into a 3d one… With a bit of creative thought, and constructive use of pointer-to-functions, you can do quite a bit of OOP stuff in C.
- dahfizz 4y agoYou can also put function pointers in a struct, which is sometimes useful. The syntax is ugly, but you can do a lot of OO stuff that way too.
- mannykannot 4y agoIn the days when grokking OO was taken to validate your membership of the cognoscenti, this led to some pointlessly awful code.
- fwip 4y agoIn 2nd year of college, one of my friends and I figured this out on our own, which was a really rewarding experience. The code did look pretty hairy, though. Details for anyone interested: The CS course project was to write a game solver for a variety of games with perfect information (e.g: tic-tac-toe), and they highly suggested we use object-oriented design and recursion + backtracking. They also let us pick any language we wanted, and being computer engineers, between the two of us, we were most comfortable with C. So we kind of started writing our project and implementing the first game, and when we got to the second, we were scratching our heads like, "Is it possible to just... take a pointer to a function?" "Yeah, it's just somewhere in memory, right?" And then everything fell into place, and we just had to define a struct with pointers to game state and "methods", and our TA was baffled that we did the project in C but we got a great grade.
- shultays 4y agoI would be surprised if C guarantees those x and y to have same offsets Plus you cant cast and dereference one type's pointer to the other. That would be UB
- dahfizz 4y ago> The sizes of arrays are not known in any useful way to C. When I declare a variable of type int[5] in a function, I don’t get a value of type int[5]; I get an int* value which has 5 ints allocated at it. Since this is just a pointer, the programmer, not the language, has to manage copying the data behind it and keeping it valid. This is not quite correct. Assuming arrays == pointers is usually true enough, but it isn't actually true. This[1] SO thread has some useful information in it, but the TLDR is that actual arrays are treated differently than pointers. You do get an object of type "int array" rather than "int pointer" when you do int[5]. The compiler does know the size of an array allocated like T a[n]. It does not, however, do any bounds checking for you. [1] https://stackoverflow.com/questions/4607128/in-c-are-arrays-pointers-or-used-as-pointers https://stackoverflow.com/questions/4607128/in-c-are-arrays-...
- caerwy 4y agoI'd recommend also reading Rob Pike's Notes on Programming in C, http://doc.cat-v.org/bell_labs/pikestyle http://doc.cat-v.org/bell_labs/pikestyle
- emmelaich 4y agoQuote which is interesting re Go lang.. > I eschew embedded capital letters in names; to my prose-oriented eyes, they are too awkward to read comfortably. They jangle like bad typography.
- DSMan195276 4y agoI like it, but the array details are a little bit off. An actual array does have a known size, that's why when given a real array `sizeof` can give the size of the array itself rather than the size of a pointer. There's no particular reason why C doesn't allow you to assign one array to another of the same length, it's largely just an arbitrary restriction. As you noted, it already has to be able to do this when assigning `struct`s. Additionally a declared array such as `int arr[5]` does actually have the type `int [5]`, that is the array type. In most situations that decays to a pointer to the first element, but not always, such as with `sizeof`. This becomes a bit more relevant if you take the address of an array as you get a pointer to an array, Ex. `int (*ptr)[5] = &arr;`. As you can see the size is still there in the type, and if you do `sizeof *ptr` you'll get the size of the array.
- tmewett 4y agoInteresting, I didn't fully realise that. That it's arbitrary is annoying, I clearly had tried to rationalise it to myself! Thanks for the comments, will get around to amending
- emmelaich 4y agoHi, great article. Regarding char, I'd remark that getchar() etc return int so it can return -1 for EOF or error. I'm pretty sure this implies int as a declaration is always signed, but tbh I'm not completely sure!
- yakubin 4y agoint, as all other integer types except char, is indeed signed by default. Aside: signedness semantics of char is implementation-defined. However, the type char itself is always distinct from both signed char and unsigned char.
- dahfizz 4y agoAnother weird property about C arrays is that &arr == arr. The reference of an array is the pointer to the first element, which is what `arr` itself decays to. If arr was a pointer, &arr != arr.
- nayuki 4y ago> Everything I wish I knew when learning C By far my biggest regret is that the learning materials I was exposed to (web pages, textbooks, lectures, professors, etc.) did not mention or emphasize how insidious undefined behavior is. Two of the worst C and C++ debugging experiences I had followed this template: Some coworker asked me why their function was crashing, I edit their function and it sometimes crashes or doesn't depending on how I rearrange lines of code, and later I figure out that some statement near the top of the function corrupted the stack and that the crashes had nothing to do with my edits. Undefined behavior is deceptive because the point at which the program state is corrupted can be arbitrarily far away from the point at which you visibly notice a crash or wrong data. UB can also be non-deterministic depending on OS/compiler/code/moonphase. Moreover, "behaving correctly" is one legal behavior of UB, which can fool you into believing your program is correct when it has a hidden bug. A related post on the HN front page: https://predr.ag/blog/falsehoods-programmers-believe-about-undefined-behavior/ https://predr.ag/blog/falsehoods-programmers-believe-about-u... , https://news.ycombinator.com/item?id=33771922 https://news.ycombinator.com/item?id=33771922 My own write-up: https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs https://www.nayuki.io/page/undefined-behavior-in-c-and-cplus... The take-home lesson about UB is to only rely on following the language rules strictly (e.g. don't dereference null pointer, don't overflow signed integer, don't go past end of array). Don't just assume that your program is correct because there were no compiler warnings and the runtime behavior passed your tests.
- halpmeh 4y agoWas rewriting the stack due to undefined behavior or was it due to a logic error, e.g. improper bounds calculation?
- ghostpepper 4y agoIsn’t all UB a result of logic errors? Writing beyond the end of allocated memory (due to incorrect bounds calculation ) is an example of undefined behaviour
- halpmeh 4y ago
- user3939382 4y agoThe couple of times I tried to really learn C I ran into the same problems. I started by trying to answer two questions: what are the modern features of C that I need to learn to use it, and what style/rules should I follow. What I found is a body of several C spec updates that are each defined in reference to previous C spec updates. So I'm supposed to... ? Learn C from the 70s and then study each version update and mentally diff everything to figure out what C is now? Then in terms of rules and style, unlike when K&R C was published, I couldn't find any authority. Actually what I see is that even programmers who have been writing C for many years frequently debate what practices are correct vs not. You can see it in this very thread. Every language has this, but I've seen it much more with C than other languages. Actually learning the material for me is hard when I can't even get a firm handle on what it is I'm supposed to learn.
- tmewett 4y agoCompletely agree - that's exactly why I wrote this. Some of things I wrote even seem super obvious but with no real authority you wonder whether or not this is "the way" to do things. If you ever want to get some practical experience, we'll help you out if you want to contribute to Brogue :)
- philsnow 4y agoReplying here hoping that you'll have a better chance of seeing it: thank you for maintaining Brogue. I've sung praises [0][1][2] for years about the quality of the Brogue codebase (and the game itself). [0] https://news.ycombinator.com/item?id=20560132 https://news.ycombinator.com/item?id=20560132 [1] https://news.ycombinator.com/item?id=19901128 https://news.ycombinator.com/item?id=19901128 [2] https://news.ycombinator.com/item?id=13594689 https://news.ycombinator.com/item?id=13594689
- tmewett 4y agoThank you :) Though I should say, most of the code is by the original author Brian
- 4y ago
- lebuffon 4y ago" ... is often called the stack." " integers are cursed" These statements and a few others made me uncomfortable. They imply, to me, that the author has too little knowledge of computer internals to be programming in C. C does a wonderful job of looking like a high level language but it was designed to do low level stuff. There is an implicit assumption, to my mind, that the user has a deeper understanding of what is behind the "curtain" that is created by the compiler. It almost seems like the there should a pre-requisite course like "Introduction to Computer hardware" before one is allowed to write a line of C. Maybe I'm just too old...
- pclmulqdq 4y agoI think the "automatic storage" terminology is technically correct. C can be used in places where there is no actual "stack" and these still need a mechanism for local variables, so they specify a different kind of automatic storage. Everyone uses a stack now, though, even very exotic processors.
- Koshkin 4y agoYou can easily have a stack without the special machine instructions (like push and pop) or the special stack pointer register. In fact, such special register is only useful in the absence of (a sufficient number of) general-purpose registers, which is characteristic to simpler architectures that have few registers (most of which, too, are specialized); IBM mainframes, for instance, never had the notion of the stack built into the architecture: to allocate a stack frame the program would simply subtract the entire frame's size from the current value in some register and then populate whatever pieces it needs there using the register as the base pointer.
- webstrand 4y agoIn the standard it's "automatic storage duration" which is one of the three (or four in C11) classes of storage duration.
- jjav 4y ago> There is an implicit assumption, to my mind, that the user has a deeper understanding of what is behind the "curtain" that is created by the compiler. It's also useful to remember that C is from a time when approximately everyone doing programming understood these things because you had to be exposed to lower level details.
- einpoklum 4y agoHalf of the items the poster lists as "wish I'd known" are things which were literally part of the curriculum when I taught C at my alma mater (Technion IIT, Haifa). And then, some things are better not know - like cppreference, which would just confuse you with a lot of C++, which is interesting, but not relevant. Also, there are some nitpicks to make, like about array decay etc. More generally: We can't expect to know a lot of context before learning anything. You have to learn things in _some_ order. It's better to know C when you study PDP assembly, and at the same time it's better to already know PDP assembly when you study C - as you have insight into the motivation of what's possible via the language syntax itself. Same thing for Calculus and Topology: The latter course helped me understand and generalize a lot of what we had done in the former, but without the former, I wouldn't have had proper motivation for the latter, which might have seemed like useless sophistry.
- tmewett 4y agoI didn't learn computer science at college/university - only via the internet, and C is quite different to learn that way vs other languages. That was my main motivation for writing :)
- krylon 4y agoWhen I first learned C - which also was my first contact with programming at all - I did not understand how pointers work, and the book I was using was not helpful at all in this department. I only "got" pointers like three or four years later, fortunately programming was still a hobby at that point. Funnily when I felt confident enough to tell other people about this, several immediate started laughing and told me what a relief it was to hear they weren't the only ones with that experience. Ah, fun times. EDIT: One book I found invaluable when getting serious about C was "The New C Standard: A Cultural and Economic Commentary" by Derek Jones (http://knosof.co.uk/cbook http://knosof.co.uk/cbook). You can read it for free because the book ended up being to long for the publisher's printing presses or something like that. It's basically a sentence-by-sentence annotated version of the C standard (C99 only, though) that tries to explain what the respective sentence means to C programmers and compiler writers and how other languages (mostly C++) deal with the issue at hand, but also how this impacts the work of someone developing coding guidelines for large teams of programmers (which was how the author made a living at the time, possibly still is). It's more than 1500 pages and a very dense read, but it is incredibly fine-grained and in-depth. Definitely not suitable for people who are just learning C, but if you have read "Expert C Programming: Deep C Secrets" and found it too shallow and whimsical, this book was written for you.
- nayuki 4y agoFrom over a decade ago, I really enjoyed this clay animation on C pointers: https://www.youtube.com/watch?v=5VnDaHBi8dM https://www.youtube.com/watch?v=5VnDaHBi8dM , http://cslibrary.stanford.edu/104/ http://cslibrary.stanford.edu/104/
- chasil 4y agoHaving basic experience in any assembly language makes pointers far more clear. "Addressing modes," where a register and some constant are used to calculate the source or target of a memory operation, make the equivalence of a[b]==*(a+b) much more obvious. I also wonder about the author's claims that a char is almost always 8 bits. The first SMP machine that ran Research UNIX was a 36-bit UNIVAC. I think it was ASCII, but the OS2200/EXEC8 SMP matured in 1964, so this was an old architecture at the time of the port. "Any configuration supplied by Sperry, including multiprocessor ones, can run the UNIX system." https://www.bell-labs.com/usr/dmr/www/otherports/newp.pdf https://www.bell-labs.com/usr/dmr/www/otherports/newp.pdf
- nwellnhof 4y agoI wouldn't recommend fixed-size integers in general. Most of the time you want size_t or the lesser-known, signed ptrdiff_t. More often than not, integers are array sizes or indices, so size_t is the best type to use. In other cases, int is practically the same as int32_t and long long the same as int64_t except for really exotic platforms.
- throwawaaarrgh 4y agoI've been burnt enough by varying sizes that I don't care if there's a performance impact anymore. Consistency and reliability are the main things I care about.
- nwellnhof 4y agoWell, then you should always use 64-bit types. Using uint32_t instead of size_t on a 64-bit platform will bite you eventually. size_t is also used extensively and for good reason in the standard library. Code like the following is a disaster waiting to happen: uint32_t len = strlen(str);
- nayuki 4y ago> C has no environment which smooths out platform or OS differences Not true - C has little environment, not no environment. For example, fopen("/path/file.txt", "r") is the same on Linux and Windows. For example, uint32_t is guaranteed to be 32 bits wide, unlike plain int. > Each source file is compiled to a .o object file Is this a convention that compilers follow, or are intermediate object files required by the C standard? Does the standard say much at all about intermediate and final binary code? > static This keyword is funny because in a global scope, it reduces the scope of the variable. But in a function scope, it increases the scope of the variable. > Integers are very cursed in C. Writing correct code takes some care Yes they very much are. https://www.nayuki.io/page/summary-of-c-cpp-integer-rules https://www.nayuki.io/page/summary-of-c-cpp-integer-rules
- LegionMammal978 4y ago> Is this a convention that compilers follow, or are intermediate object files required by the C standard? Does the standard say much at all about intermediate and final binary code? The standard only says that the implementation must preprocess, translate, and link the several "preprocessing translation units" to create the final program. It doesn't say anything about how the translation units are stored on the system. > This keyword is funny because in a global scope, it reduces the scope of the variable. But in a function scope, it increases the scope of the variable. Not quite: in a global scope, it gives the variable internal linkage, so that other translation units can use the same name to refer to their own variables. In a block scope, it gives the variable static storage duration, but it doesn't give it any linkage. In particular, it doesn't let the program refer to the variable outside its block.
- trap_goes_hot 4y agoOn Windows you can directly access UNC paths (without mounting) with fopen. You can't do this on POSIX platforms. Also, not all API boundaries are fixed width so you're going to be exposed to the ugliness of variable width types. I think the article is correct that one must be aware of the platform and the OS when writing C code.
- manv1 4y agofopen will/should fail on windows with the unix path syntax. The reason it's indeterminate is because some stdc lib vendors will do path translation on Windows, some won't. I believe cygwin does (because it's by definition a unix-on-windows), but I'm pretty sure the normal stdclib vendors on windows do not. I'm almost positive that MacOS (before MacOS X) will fail with unix path separators, since path separators are ':' not '/'.
- alexandreyc 4y agoAfter more than a decade without writing any C code, I'm currently reading "C Programming: A modern Approach" by K. N. King (http://www.knking.com/books/c2/ http://www.knking.com/books/c2/) and I found it very good. I think it's a better modern alternative to the K&R.
- Koshkin 4y ago832 pages is not a "better alternative."
- Existenceblinks 4y agoPlus, it's 2008. 14 years is enough for lots of thought to have a revisit with revise. If there's a very good C book in 2023 I would buy it. Edit: No, change my mind. I'd go for Zig book instead!
- fastaguy88 4y agoI find this article very strange, perhaps because I started using 'c' so long ago. To the bullet points: (1) In general, 'c' is always 'c' at the command line, regardless of the platform. (2) yes, there are options and build tools, but cc my_program.c -o my_program works fine. I have a very hard time figuring out how to compile/run java. (3) hard to see how this has anything to do with 'C', vs any other compiled language. (4) so?? I would think I would be more concerned about how to use 'c' for my problem, without worrying about how to use 'c' to solve some other problem. It is hard for me to understand why a language that can do many things is more problematic than a language that only does a few things. My sense is that reading this article makes things harder, not easier. Most people do not care whether an int is 32 or 64 bits. I won't argue that copying things (that are not ints or floats) needs to be understood, but many popular languages (specifically python) have the same problem. Understanding the difference between a reference and a value is important for most languages. There are different schools of thought -- those that can imagine lots of issues after reading the documentation, vs those that simply try writing code and start exploring edge cases when something breaks. I learn faster by trying things, and rarely encounter the edge-case issues.
- msla 4y agoNow I need to make a C dialect with better string handling built-in and call it 'C'
- alana314 4y agoAgreed, I tore my hair out trying to figure out strings in C
- alcover 4y agoWorking on it so you don't have to ! You'll enjoy comfy stuff like let s = "string.." s += "handled" The runtime Buffer type is already operational (https://github.com/alcover/buffet https://github.com/alcover/buffet)
- jklehm 4y agoIt's largely the same, aside perhaps from not shipping with the java compiler by default: * install java (includes compiler/runtime): `sudo apt install default-jre` * compile: `javac MyProgram.java` * run: `java MyProgram`
- twawaaay 4y agoThe most important thing I had to learn when I started on hardcore ANSI C projects (large embedded business critical application) was to really learn to build abstractions. C has few tools to help you with this and so it is super important to get the most of what is available. Another lesson is that idioms are very useful in C and cut a ton of time. For most or all repeatable tasks there should be conventions for how to implement it and how to use it. These are useful in any programming language and environment but I think are especially useful in C where you are on your own when structuring your application.
- wiseowise 4y agoWhere is the most important part: build tools and libraries?
- andrewmcwatters 4y agoThe older I got, the more I realized you seem to be heavily disadvantaged by not having some rudimentary understanding of assembly when using C. When you do have such an understanding, some small details, like declaring stack variables at the top of functions in ANSI C, become obvious.
- programmer_dude 4y agoOne does not follow from the other. You can declare your stack variable anywhere in the function and still have the same stack layout. It is upto the compiler/language designer (I am not talking about C per se).
- 0xbadcafebee 4y agoMore Good projects to learn from: - Busybox (https://github.com/mirror/busybox) - uClibc (https://git.uclibc.org/uClibc/tree/) - musl (https://git.musl-libc.org/cgit/musl/tree/) - misc GNU tools (https://git.savannah.gnu.org/cgit/grep.git/tree/, https://git.savannah.gnu.org/cgit/findutils.git/tree/, etc) The first two are oriented towards embedded development, which I find leads to the simplest, most portable code. Those devs are absolute wizards.
- _8j50 4y agoI'll just say that if you have to write C in 2022/23 then you should learn+automate fuzz testing with confirmation of coverage.
- arendtio 4y agoAfter learning C, one of the first projects I came into contact with, was the ID Tech 3 game engine [1] On the one hand, it taught me how professional C programmers structure their code (extra functions to remove platform differences, specific code which is being shared between server and client to allow smooth predictions) and how incredible fast computers can be (thousands of operations within milliseconds), but it also showed me, how the same code can result in different executions due to compiler differences (tests pass, production crashes) and how important good debugging tools are (e.g. backtraces). To this day I am very grateful for the experience and that ID decided to release the code as open source. [1] https://github.com/id-Software/Quake-III-Arena https://github.com/id-Software/Quake-III-Arena
- limaoscarjuliet 4y agoI was born in '74 so the last generation to start with C and go to other, higher-level, languages like Python or JavaScript. Going in this direction was natural. I was amazed by all the magic the higher-level languages offered. Going the other direction is a bit more difficult apparently. "What do you mean it does not do that?". Interesting perspective indeed!
- lp251 4y agoI was born in the late 80s and C was my first language, in a community college intro to programming class.
- cbdumas 4y agoIntroductory programming courses at the University of Arizona were still taught in C when I was a freshman in 2008
- Hadarai5 4y agoI started coding with C and OCaml in 2019. Everything in between these two was so unnatural. With JavaScript as the worst of all
- sbaiddn 4y agoIm a decade younger and my university taught C for its intro to programing class. Granted it was a disaster of a programing class.
- user3939382 4y agoWhat was nice about C then was that, based on my study of CPUs at the time, you could pretty much get your head around what the CPU was doing. So you could learn the instructions (C) and the machine following them (the CPU). When I got to modern CPUs it's so complex my eyes glazed over reading the explanation and I gave up trying to understand them.
- untech 4y agoThis was my experience with learning programming as well, however, I am 2x younger :)
- BeetleB 4y agoThe one thing missing from this list: Compiler optimizations can have undesirable effects. A trivial example I ran into in an older job: I was compiling a program that relied on libm.so (the standard math library you get when including math.h). Now I wanted the code to use my own custom libm.so - not the one that was installed in /usr/lib or wherever, so I ensured it was dynamically compiled. My code had some calls like: int x = sin(3.2); During compilation, it computed sin(3.2) using the system libm. Notably, it would not use the sine function in my custom libm.so (and indeed, I needed it to!) And IIRC, even -O0 was not enough to prevent this from happening. I had to go through the man page to figure out which compiler flags would prevent it. (I don't recall if this was gcc or icc).
- kccqzy 4y agoC doesn't have namespaces so the compiler is certainly within its right to deduce that the sin() function is the one from the standard library. Actually even in C++ after the compiler performs the unqualified name lookup if the result of the lookup is the standard sin() function it will make use of its internal knowledge about the function to do optimization. Remember that the C or C++ standard doesn't deal with compilers and linkers; the standard deals with the "implementation" as a whole.
- manv1 4y agoThe reason you use specific sizes for types (int8, int16, uint16, etc) is so you know how to read/write them when you move your data between platforms. x86 is little endian. ARM apparently can be configured to be either. In real code there should be readXXX and writeXXX functions that read/write data from disk/network and do the byte swapping in there. You could also just convert everything to JSON, but you're trading space for complexity/time.
- jesse__ 4y ago> ARM apparently can be configured to be either. That's insane!!! TIL
- programmer_dude 4y agoNot really, it is very easy to do this in hardware.
- yevpats 4y agoPreviously (recently) discussed on HN on why not to use c/c++ - https://news.ycombinator.com/item?id=32905885 https://news.ycombinator.com/item?id=32905885
- glonq 4y agoMy college taught us pascal and x86 asm before teaching us C. I think that was perfect because "bookending" it with a high-level language and a low-level one helped put C in perspective nicely. Knowing asm definitely helped to demystify pointers in C, which is usually a stumbling block for novice programmers.
- hujun 4y agothis is a good a good blog, although I wonder is there any good up-to-date free online resource to learn C for experienced programmer (I learned C many years ago, but never use it seriously and forgot much of it)? I searched around, the results are either for beginner, or seems out-dated
- jhallenworld 4y agoI've been programming in C forever, one advantage is that the language has not evolved much (especially compared with C++), but it has evolved. There was the big K&R C to ANSI C function declaration transition. For portable code, you used K&R C well into the 90s (because older machines only had the K&R compiler), or used ugly macros to automatically convert from ANSI to K&R. Another was the addition of 'const' to the language. It used to be said that const was a virus: once you start using it, you need to use it universally in your entire code-base. A more recent big one is stdint.h (types like uint32_t). To correctly use these types you must also use the macros in inttypes.h for scanf and printf conversions. IMHO, they both should have been in the same header files, they go along with each other. So in the old days, you would say: unsigned int x; printf("%u\n", x); But now, you should tediously say: uint32_t x; printf("%"PRIu32"\n", x); (Because uint32_t might be defined as a long even if it's the same size as in int, so you will get compiler warnings. You could use %lu, but then you get compiler warnings the other way.) Another: On UNIX, we don't have to worry about "wide" characters (use UTF-8 instead) and wide strings, but you certainly do on Windows. "Wide" characters are obsolete but Windows is stuck with them since they are built into the OS.
- suprjami 4y ago> stdint.h / inttypes.h Including inttypes also includes stdint. You use stdint where you need the types only, like a header or non-IO module, and use inttypes where you need the print formatting. I agree it's a bit weird but that's the way I understand the intended usage.
- a1369209993 4y ago> It used to be said that const was a virus: once you start using it, you need to use it universally in your entire code-base. In order for const to actually work for what it's supposed to do, it does have to be viral in the direction of data flow. You should start by adding const to function arguments that point to data the function only reads (and doesn't pass the pointer to any subroutines) and expand from there. Eg: _Bool isurl(char /*const here*/* s) { while(isalpha(*s)) s++; return *s == ':'; } /* s is never written through */ Then anything that passes pointers (only) to functions like isurl, and so on as is convenient.
- Hadarai5 4y agoThis post is also a list of reasons why everyone thinking seriously about becoming computer scientists should start nowhere else than with C
- ben_bradley 4y agoI started learning C around when ANSI C came out, and learned much of this in self defense. I'm glad I decided to learn C++ in recent years, it has fixes for so many things (like pass by reference instead of passing pointers, const values can be used to define array sizes though it's better to use vectors anyway, etc.), but that's off topic. A few things I didn't see mentioned: Add multiple inclusion guards to every header file you write, it saves multiply-defined errors and such: file mygreatheaderfile.h: #ifndef MYGREATHEADERFILE_H #define MYGREATHEADERFILE_H /* insert usual header file content here / #endif / #ifndef MYGREATHEADERFILE_H */ Most (all?) compilers have a "don't include this file more than once" preprocessor directive, but from what I've seen they're nonstandard and they vary, but using the above method always works. If I have a "complete program" with a main function and other functions in one source file, I put main() at the end and put all functions in the order they are called, that way there's no need for function prototypes (except for recursion) as there would be if main() is the first function in the file. None of the C books I've read said you could do this, but when I figured it out I thought yeah, it's just like Pascal and assembly, you have to define something before you use it, but you can make the first occurrence be the function definition and not have to have a separate prototype. As for naming and capitalizing, as the document said, there's no standard/convention of camelCase vs. snake_case, but all macro names using #define are by convention in ALL_CAPS. That way it's easy to tell a MAX(x, y) macro from a max (x, y) function, and you can eventually learn why never to write such perverse things as MAX (x++, y++). Trace through the expansion to see why (and see why it's better to use a function instead, or in C++ a template): #define MAX(x,y) x>y?x:y Equals comparison/assignment and if statements: One of the most common and insidious errors in C is accidentally doing an assignment (=) instead of comparison (==). Modern C compilers (the ones with integrated C++ compilers, see below) will give a warning when they see this, but still, if one of these is a constant, put the constant on the left so it will give an ERROR if you accidentally try to assign something to the constant as in if (5 = n) instead of what may feel natural but be wrong (and compile fine with an old compiler!), if (n = 5). There are other gotchas like this, but I can't think of them all, and there's probably too many to post here anyway. I do see "undefined behavior" discussed. Be sure to make backups before running your code. If you need to do maintenance using some original C compiler for an embedded controller from 30 years ago (or indeed modern C as is still popular in embedded systems), you really need to know all these ins and outs, and I might be convinced to help for an appropriately high hourly amount, but virtually every C compiler nowadays is part of a C++ compiler, and you can do much of this stuff in C++ using better code practices, resulting in fewer bugs.
- pratk 4y agoOne C idiom that I found pretty useful is Xmacros [1]. For instance, it's used extensively in GCC code-base (.def files). [1] https://www.drdobbs.com/the-new-c-x-macros/184401387 https://www.drdobbs.com/the-new-c-x-macros/184401387
- rramadass 4y agoVery Neat! Didn't know of this. Have any more of "C Idioms" collection?
- ErikCorry 4y agoNever use -Og for debug builds. It doesn't work. Use -O0. If you try to debug a -Og built program you will not be able to print locals because the optimizer has removed them. People blame their debugger. It's my humble opinion that -Og should be an alias for -O0. Broken for decades it's time to stop pretending it works.
- Elizabethtrusco 4y ago[dead]
- willfiveash 4y agoI still remember when a co-worker told me that the biggest problem with C is that programmers are terrible at memory management. Given the number of memory corruption bugs I encountered in 27 years of working with C, I have to say that rings true.