6 ms·
What's difficult to understand about pointers isn't the concept of a pointer itself, or even * and &, it's the fact that working with pointers requires you to s
by qsort 6y ago
What's difficult to understand about pointers isn't the concept of a pointer itself, or even * and &, it's the fact that working with pointers requires you to simultaneously understand different abstraction levels. While it's not unique to pointers, and it's in fact the case for most nontrivial programming tasks, what's unique about C is that pointers are so pervasive you can't really do anything if you don't understand how to work with them.
IME languages like Python aren't any easier than C to work with (ignoring UB issues of course), but it's certainly the case that you can probably kinda sorta get your job done with Python even without understanding the first thing of what you're doing, and that's not happening if you write in C.
- bluetomcat 6y agoIn C, pointers require you to think deeply about the ownership and lifetime of any "allocated object" at runtime. How long does it live, who is responsible for the deallocation, how many pointers does your program hold to that object (dangling issues). Ultimately, it can lead to a cleaner design if these issues are taken seriously up-front.
- qsort 6y agoI don't disagree with that, but most cases fall within a pretty clear pattern: - typedef struct { ... } foo - foo *foo_create() - void foo_destroy(foo *) - a bunch of functions that take foo* as their first arg which is kind of the same as a class and only more error-prone. I say this as someone who actually _likes_ C, but the manual memory management model is very often unnecessary, confusing, repetitive. There was an idea some time ago of a language extension that would extend the concept of automatic storage duration to allow an explicit destructor to be called when the variable goes out of scope, like <close> variables in some languages. I genuinely think things like that would make the language a bit more ergonomic without fundamentally changing its nature.
- bluetomcat 6y agoThat's why I tend to always prefer automatic and static storage to dynamic allocation wherever possible, especially in cases where you don't have "N" items or "N" cannot possibly exceed a certain small value. Also, allocation/deallocation of the certain object need not be defined within its module. It should be up to the caller to decide whether to allocate the object on the stack, statically or dynamically depending on the caller's situation: foo f; foo_init(&f); foo_destroy(&f); ... foo *g = malloc(sizeof(foo)); foo_init(g); foo_destroy(g); free(g);
- unwind 6y agoThat is of course fine, with the drawback that it requires a public definition of the foo type. I would write the allocation as foo * const g = malloc(sizeof *g); to avoid repeating the type name and "lock" the allocation to the variable. If the type on the left hand side ever would change, this still does the right thing.
- quelsolaar 6y agoInteresting, I tend to prefer a create and destroy function that allocates and frees the structure. That way you can have foo without it being initialized, and you cant have foo been freed without being de-initialized. Where do see the value in being able to move it between different memory types?
- TorKlingberg 6y agoComing from the embedded world, I am far more used to the style bluetomcat showed where the caller handles allocation. It takes more work to use, and you have to make sure to not use it before initializing or after destroying, as you said. The advantage is that the caller has full control of memory allocation. The object can be a local variable, a static, malloc:ed, come from a custom allocator (say a slab allocator), or be a part of a larger struct.
- quelsolaar 6y agoThat makes a lot of sense. I do some embedded development, but when I dont, I tend to prefer heap just because my memory debuggers are much more capable working with heap memory. In the case you show, foo would have to be a struct that doesn't contain pointers to additional allocated memory, but its a entirely valid use case and pattern. Calling malloc and free, has a cost associated with it, that the stack doesn't. But stack can in some cases, like with recursion be scary to use, because you dont know where it ends. If malloc returns NULL you know you have found the end and can do something reasonable. Thanks for you insight!
- AlbertoGP 6y agoI’m actually implementing that right now for my own use, as a pre-processor. There is a much more advanced design and implementation at “A defer mechanism for C” (December 2020): https://gustedt.wordpress.com/2020/12/14/a-defer-mechanism-for-c/ https://gustedt.wordpress.com/2020/12/14/a-defer-mechanism-f... For my own purposes, I think I can live without handling stack unwinding so I continue working on my pre-processor. Since the pre-processor is not yet finished, there I use a vector¹ of {.pointer, .destructor} where I put objects after initialization, with one macro at the end of each managed scope that calls the destructors for that scope in reverse order, then another macro meant for the function exit points that calls all the destructors in that vector. This has been built many times before by other people of course, it’s just an exercise to see which difficulties arise. ¹ Vector, growable array: I did my own trivial type-generic growable array, with the classic {.capacity, .len, .items}, but again there are many previous implementations. The two I’ve found more interesting are: - “C Template Library (CTL)”: https://github.com/glouw/ctl https://github.com/glouw/ctl - “Klib: a Generic Library in C”: https://github.com/attractivechaos/klib/ https://github.com/attractivechaos/klib/
- glouwbug 6y agoWith a bit of trickery CTL works much like the STL, in that containers can be built of containers: #define P #define T int #include <vec.h> #define T vec_int #include <deq.h> A deq_vec_int - analogous to std::deque<std::vector<int>> - is a neat example.
- cygx 6y agoThere's also a CTL fork that might be of interest: https://github.com/rurban/ctl https://github.com/rurban/ctl
- tirrex 6y agoFor a vector alternative, I find this one easy to read and use : https://github.com/tezc/sc/tree/master/array https://github.com/tezc/sc/tree/master/array It is just an array of your type, e.g int *numbers, so you have type info in debugger as well.
- 6y ago
- Joker_vD 6y agoYeah, except when it doesn't follow the pattern: for example, there is no foo_destroy(), you're supposed to "just" call free() when you're done with it. Used to be very common (not sure how it is now), and very frustrating when you link against non-standard allocators.
- alexhutcheson 6y agoIt’s not standardized, but if you’re using gcc, you can use the cleanup attribute for this: https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attribute... IIRC clang implements it as well, but I wasn’t able to find a reference to it in their docs.
- oscargrouch 6y agoI also like the Ken Thompson extensions from which part of Go was inspired by struct X { int a; } struct Y { *X; int b; int c;} void add(Y* self, int number) { self->a += number; } Y y; y.a = 10; // composition y.add(1) // y.a = 11 now This alone would simplify C coding so much without taking any power out of it. The other extension i would add is some sort of interface or protocol. As soon as you can do something like "y.add(1)", having a generic contract to refer to things without having to know its concrete type is some of the good things from the OOP world. With this you would also be able to call some cleanup code and even a initializer. This is still C and its still much simpler than C++, and yet almost as powerful. C should propose these kind of things even if it was not that conservative and it would retain a lot of coders that migrate instead giving C barely evolved from its 70's roots.
- jhgb 6y ago> In C, pointers require you to think deeply about the ownership and lifetime of any "allocated object" at runtime. Unless you decide you use libgc, presumably?
- unwind 6y agoThese are good points. I can sometimes feel that Python is more pointer-y (?) than people expect, with stuff like: a = {"one": 1, "two": 2 } b = a b["two"] = 99 print(a["two"]) The above prints 99, since "b = a" does not copy the value (the dictionary) but just the reference to the value ("the pointer", kind of). This is surprising to some people.
- qsort 6y agoI agree, which is why I'm glad the first language I learned was C. I don't get to write a lot of C at work, but the concepts the language teaches you are the very fundamentals of programming. I know it's probably baseless, but I can't shake the feeling that people who learn modern languages before learning C are just making their own lives harder.
- bregma 6y agoI think the same goes for learning assembly before learning C. A simple assembly-based computer [1] should be the first programming target in every "introduction to programming" class. After that, C becomes obvious and those who struggle with the fundamntals can be guided to more appropriate careers instead of dumbing down the tools everyone uses. [0] https://www.instructables.com/CARDIAC-CARDboard-Illustrative-Aid-to-Computation-/ https://www.instructables.com/CARDIAC-CARDboard-Illustrative...
- serial_dev 6y agoIt's also similar in Java's pass-by-value vs pass-by-reference (pass as well as copy), if I recall correctly.
- kaba0 6y agoActually, most languages only have pass-by-value, being “pedantic” (but it is actually a useful distinction). I’m sure there are other’s but out of the majorly used languages, only C++ has actual references. Pointers are passed by value, that is the pointer value gets copied. References are a language level construct. Here is a great “rant” at it in case of Java: http://www.javadude.com/articles/passbyvalue.htm http://www.javadude.com/articles/passbyvalue.htm
- bluedino 6y agoPeople that can’t comprehend arrays/lists in Python, especially multi-dimensional ones, would have had no chance in C