Live data from Hacker News

Everything I wish I knew when learning C

tmewett.com

311–320 of 401 posts

Re: Everything I wish I knew when learning C

#311

I'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…

> 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.

Re: Everything I wish I knew when learning C

#312

Earlier quoted context omitted.

memcpy is not O(1)

It's O(1) relative to any size computed at runtime: that is, running the same program (with the same array size) on different inputs will always take the same of work for a given assignment.

We're in the context of the assignment operation in the language here. Yes, in C you can only assign statically-known types but that does not mean you can just ignore that a = f(); may take a very different time depending on the types of a and f

Re: Everything I wish I knew when learning C

#313
post #293

Earlier quoted context omitted.

The C and C++ (and D) compilers I wrote do not attempt to take advantage of UB. What you got with UB is what you expected to get - a seg fault with a null dereference, and wraparound 2's complement arithmetic on overflow. I suppose I think in terms of "what would a reasonable person expect to happen with this use of UB" and do that. This probably derives, again, from my experience designing flight critical aircraft p…

> What you got with UB is what you expected to get - a seg fault with a null dereference, and wraparound 2's complement arithmetic on overflow. This is how it worked in the "old days" when I learned C. You accessed a null pointer, you got a SIGSEGV. You wrote a "+", then you got a machine add.

In the really old DOS days, when you wrote to a null pointer, you overwrote the DOS vector table. If you were lucky, fixing it was just a reboot. If you were unlucky, it scrambled your disk drive.

It was awful.

The 8086 should have been set up so the ROM was at address 0.

Re: Everything I wish I knew when learning C

#314
post #308

Earlier quoted context omitted.

I think the meaning here is that assignment is never O( N ) for any variable N computed at runtime. Of course, you can create arbitrarily large assignments at compile time, but this always has an upper bound for a given program.

Then you are wrong, since we're already talking about arrays of sizes known at compile time. Indeed, otherwise we would also need to remember the size in the runtime.

IIRC this is valid in C99:

    void foo(size_t n) {
        int arr[n];
        …
    }

Re: Everything I wish I knew when learning C

#315
post #74

> 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 do…

I recently dealt with a bit of undefined behavior (in unsafe Rust code, although the behavior here could similarly happen in C/C++) where attempting to print a value caused it to change. It's hard to overstate how jarring it is to see an code that says "assert that this value isn't an error, print it, and then try to use it", and have the assertion pass but then have it be printed out as an error and then panic when trying to use it There's absolutely no reason why this can't happen since "flipping bits of the value you tried to print" doesn't count as potential UB any less than a segfault, but it can be hard to turn off the part of your brain that is used to assuming that values can't just arbitrarily change at any point in time. "Ignore the rest of the program and do whatever you want after a single mistake" is not a good failure mode, and it's kind of astonishing to me that people are mostly just fine with it because they think they'll be careful enough not to make a mistake ever or that enough of the time it happened they were lucky that it didn't completely screw them over.

The only reason we use unsafe code on my team's project is because we're interfacing with C code, so it was hard not to come away from that experience thinking that it would be incredibly valuable to shrink the amount of interfacing with C as small as possible, and ideally to the point where we don't need to at all.

Re: Everything I wish I knew when learning C

#316
post #98

When 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 w…

One thing that helped me understand pointers was understanding that a pointer is just a memory address . When I was still a noob programmer, my instructor merely stuck to words like "indirection" and "dereferencing" which are all fine and dandy, but learning that a pointer is just a memory address instantly made it click. Pointers are a $1000 topic for a $5 concept.

When I’m teaching (a very high-level language), I make a point of saying that a variable is a named memory location. Where is that location? We don’t know. Now, I am absolutely aware that the address isn’t the “real” location, but I have this idea that talking about variables in this way might help them grok the lower-level concept later on.

Re: Everything I wish I knew when learning C

#317

‘ 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 const…

The Amiga used this technique extensively throughout its OS.

Re: Everything I wish I knew when learning C

#318
I 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.

Re: Everything I wish I knew when learning C

#319
post #308

Earlier quoted context omitted.

Then you are wrong, since we're already talking about arrays of sizes known at compile time. Indeed, otherwise we would also need to remember the size in the runtime.

IIRC this is valid in C99: void foo(size_t n) { int arr[n]; … }

VLAs can be declared in a single statement, but they cannot be initialized in C17 (6.7.9):

> The type of the entity to be initialized shall be an array of unknown size or a complete object type that is not a variable length array type.

Curiously, C23 actually seems to break the O(1) rule, by allowing VLAs to be initialized with an empty initializer:

  int arr[n] = {};
GCC generates a memset call (https://godbolt.org/z/5v31bKs5a) to fill the array with zeros.

Re: Everything I wish I knew when learning C

#320
post #308

Earlier quoted context omitted.

I think the meaning here is that assignment is never O( N ) for any variable N computed at runtime. Of course, you can create arbitrarily large assignments at compile time, but this always has an upper bound for a given program.

Then you are wrong, since we're already talking about arrays of sizes known at compile time. Indeed, otherwise we would also need to remember the size in the runtime.

I don't think we're actually in disagreement here. It looks like I misread the parent comment to be claiming that fixed-size array assignment ought to be considered O(N), when no such claim is made.
Post reply on HN