20 ms·
The Descent to C (2013)
- nihilist_t21 5y agoHey! The author wrote PuTTY! That was a very informative read. I feel like I am the exact target audience: I'm coming from a programing background steeped in C# and I'm learning C from the K&R book.
- besnn00 5y agoNever thought pointers could expire, but it makes sense.
- wizzwizz4 5y agoThis is (sort of) the idea behind Rust's lifetimes. (Rust focuses more on scope, whereas pointer expiry is more about “object lifetimes”, but it's basically the same thing.)
- tialaramex 5y agoI don't think that's true, it's just that the lifetime of a stack object is limited by its scope. Clearly when it goes out of scope its lifetime needs to end. You can end that lifetime prematurely, for example if you drop(x) then the lifetime of x ends immediately [ in fact the implementation of drop is entirely empty, but it takes the actual x itself as a parameter, not a reference to it, and so when the empty function exits the parameter's lifetime ends and x goes away ] even though it hasn't gone out of scope in your function where you created x. I believe once upon a time Rust needed a lot more hand-holding rather than just inferring lifetimes from scope, but in modern Rust the behaviour feels pretty natural for scopes.
- steveklabnik 5y agoRust used to use lexical scope, but now uses scope based on the control flow graph.
- _kst_ 5y agoScope and lifetime are two different things, but they're related in the case of automatic (local) variables. Scope is the region of program text in which an identifier is visible. Lifetime is the duration during program execution in which an object (stored in memory) exists. If you define a local variable, the scope of its identifier extends from its declaration to the end of the enclosing block; its lifetime is the execution of the enclosing block. If you allocate an object on the heap by calling `malloc()`, its lifetime ends when its deallocated by calling `free()`. An object defined at file scope or with the `static` keyword has a lifetime that's the entire execution of the program. In either case, if you have a pointer to the object, that pointer becomes invalid when the object it points to reaches the end of its lifetime. (And C doesn't make it particularly difficult to cause problems by trying to access an object that no longer exists.)
- jandrese 5y agoThat's a fairly common trap for new C coders. Setting a pointer to something on the stack and then returning that pointer when you exit the function. It's extra insidious because it will appear to be working fine until you call another function and then try to dereference the pointer. Then your data suddenly becomes corrupt even though your program was nowhere near it.
- pjmlp 5y agoUnless it points to a static local variable.
- tempodox 5y agoThere is also the excellent “Modern C” book: https://modernc.gforge.inria.fr https://modernc.gforge.inria.fr The page has a link to a free PDF version.
- smcameron 5y ago> (In fact, that's what array[i] means – the language defines it to be a synonym for *(array+i).) To really drive home the primitiveness of C arrays, should probably also mention that, because addition is commutative, you could also write i[array] and somewhat surprisingly, it will compile, and work, and it means "*(i + array)" which is equivalent to "*(array + i)" But nobody really does that, because that would be kind of insane.
- unwind 5y agoOnce the point is home, you can drive it a little bit more by exploiting the fact that string literals can be converted to pointers to the first characters, and do putc(2["ABCDEF"], stdout); This prints 'C'.
- deleted 5y ago[deleted]
- deleted 5y ago[deleted]
- danielozcpp 5y agoint const* const x; // C int const& x; // C++ A reference is functionally equivalent to a const pointer. (Reference reassignment is disallowed. Likewise, you cannot reassign a const pointer. A const pointer is meant to keep its pointee [address].) The difference between them is that C++ const references also allow non-lvalue arguments (temporaries). It is much easier to read from right to left when decoding types. Look for yourself: - double (* const convert_to_deg)(double const x) // const pointer to function taking a const double and returning double - int const (* ptr_to_arr)[42]; // pointer to array of 42 const ints - int const * arr_of_ptrs[42]; // array of 42 pointers to const ints - int fun_returning_array_of_ints()[42]; Try it out yourself: https://cdecl.org/ https://cdecl.org/ Hence, I am an "East conster". (Many people are "West consters" though.) You can return function pointers: typedef struct player_t player_t; // let it be opaque ;) int game_strategy1(player_t const * const p) { /* Eliminate player */ return 666; } int game_strategy2(player_t const * const p) { /* Follow player */ return 007; } int (* const game_strategy(int const strategy_to_use))(player_t const * const p) { if (strategy_to_use == 0) return &game_strategy1; return &game_strategy2; } Functional programming = immutable (const) values + pure functions (no side effects). Consting for me is also a form of documentation/specification. "East const" for life! :)
- globular-toast 5y ago> No object orientation I feel like C exists at a level below such concepts. Simply being able to define a function `void do_stuff(struct mystruct *obj)` opens the door to object-oriented style programming. A lot of people seem to define OOP by the presence of superficial stuff like inheritance, polymorphism etc, but really those are additional concepts that aren't useful for every program. The real difference is mutating state on the heap. So you could say C is an object-oriented language, by default, because it doesn't stop you doing this stuff, unlike a higher-level language like Clojure which simply doesn't have mutation (for the most part). Or you could say C is a functional language because if you don't explicitly pass pointers then you get copies. Really it's both and it's neither. It's whatever you want it to be.
- PEJOE 5y agoThat you can do functional or OOP in C does not make C either kind of language, it just means that C is flexible enough that you can make the computer do things the way you want it to, no matter what that means, and other languages purposefully prevent you from doing what you might want to do. C++ is object oriented not because it has compile time support for polymorphism or any of that other bad programming practice, but because classes have code sections that live with them, whether on the stack or in the heap, that can operate only on memory belonging to that instance of the class. Object oriented programming is a coding style and choice. Some languages make it a first class part of the language design. It is purposefully not part of C. However you can do OOP like things in C: a popular paradigm is to pass around pointers to structs that (should) live in the heap, and to have a number of functions which work on these structs. This is very similar in practice and mental modelling to OOP as users of C++ might know it, but is distinct in that no code ever lives in the stack or heap, and no code is restricted from operating on any of the program memory.
- AnimalMuppet 5y agoI mostly agree with you. But "or any of that other bad programming practice"? Polymorphism is not a bad programming practice. Yes, it can be misused. No, that doesn't make it bad in and of itself.
- aidenn0 5y agoSome nitpicks with the article: 1. Much of section 2 is wrong except the part about arrays representing contiguous objects. The rest is largely an implementation detail. Zeta C and Vacietis work considerably differently, as allowed by the standard In addition there are many (mostly obsolete now) architectures in which, when you convert a pointer to an integer, you can't perform arithmetic and convert back because a pointer isn't just an integer address; it could represent segments or support hardware tags. > C will typically let you just construct any pointer value you like by casting an integer to a pointer type, or by taking an existing pointer to one type and casting it so that it becomes a pointer to an entirely different type. To be fair, they do say "typically" in here, but these behaviors are (depending on the case) all either implementation defined or undefined; the C standard specifies a union as the only well-defined way to type-pun to non character types. > The undefined-behaviour problem with integer overflow wouldn't happen in machine code; that's a consequence of C needing to run fast on lots of very different kinds of computer, which is a problem machine code doesn't even try to solve Some architectures trap on integer overflow, which I suspect is the reason why integer overflow is undefined rather than implementation defined. Certainly compilers today take advantage of the fact that it is undefined to make certain useful optimizations, but from what I can tell of the history that's not why it was undefined in the first place.
- junon 5y agoTo be clear, signed integer overflow is undefined. Unsigned integer overflow is well defined. This is why some C programmers dictate that all code must use signed integers to avoid unexpected bugs, but many others (including myself) disagree that's a good way of going about it since, as you said, it's not guaranteed to trap or do anything to help the programmer.
- aidenn0 5y agoI've never seen signed-integers being required. They tend to introduce unexpected bugs rather than prevent it. Of course you can accidentally end up with signed integers when you didn't intend it. Here's my favorite accidental undefined signed integer overflow, assuming 32-bit integer size (the 64-bit version is similar) uint32_t foo(uint8_t x) { return x << 24; } Yes, this is signed integer overflow, since x gets upgraded to a signed integer before the shift, if an integer is 32-bits in size, this can result in undefined behavior if the top bit of X is set. Fortunately I've never seen a compiler optimize this to stupidity.
- adamrezich 5y agogreat article, very informative and easy to read. I wish I would've known about this when I was learning C/C++ (around the time it was published no less), coming from higher-level languages like C# and PHP.
- jimbob45 5y agoHow is C# higher-level than C? I'm not aware of anything you can do in C that you can't do in C# as a first-class feature. Edit: I define the level of a language as the lowest-possible feature. I guess others define level as the highest-possible feature? I don't really know who's right here.
- dahfizz 5y agoC# has exceptions, OO classes / interfaces, `unsafe`, etc. Lots of features that make it a higher level language than pure C.
- Koshkin 5y agoIn the same way as C is a higher-level language compared to assembler.
- jonsen 5y ago“...a high-level programming language is a programming language with strong abstraction from the details of the computer.”: https://en.m.wikipedia.org/wiki/High-level_programming_language https://en.m.wikipedia.org/wiki/High-level_programming_langu...
- DannyB2 5y agoThe following is NOT a criticism of C. Just pointing out different problem domains. > Modern high-level languages generally try to arrange that you don't need to think > or even know – about how the memory in a computer is actually organised Modern high-level languages try to arrange that you don't need to focus on the irrelevant. If you're working on, say, an accounting system, memory layout is not part of the problem you are trying to solve. For certain applications, C is simply too low level. A language is too low level when it forces you to focus on the irrelevant. For low level operations you probably cannot beat C.
- kaba0 5y agoAs per the somewhat famous blog post: C is not a low-level language. Especially on todays CPU’s I fail to see why would we consider C anything close to truly low level. It has no real way of managing cache, has absolutely zero support for vector instructions, etc. * With these in mind, Rust is lower and higher level than C at the same time. * other than some compiler specific pragmas, but I would be hesitant to call that natively supported
- munificent 5y ago> Especially on todays CPU’s I fail to see why would we consider C anything close to truly low level. It's effectively the lowest you can go without throwing portability out the window. It doesn't let you manage caches directly, but it gives you good control over memory layout in general, and that's often enough to give you good cache usage across a variety of chips. If you want to go lower than that, you're probably looking for assembly.
- tialaramex 5y agoIt's a poor fit for this role, even if it is in practice what we have available today. One of my favourite examples is volatile. Volatile is more crazy in C++ but it's pretty crazy even in C. In both cases the standard basically shrugs, "Hope you know what you're doing because we sure don't" and offers no real insight into what this feature promises to do for you. But there is no other mechanism provided for MMIO. Think of Rust's std::ptr::read_volatile (and write_volatile). These intrinsics do the thing you actually wanted (reading, or writing, a fixed size blob of "memory" that presumably wasn't really just RAM) and thus are important for writing device drivers and so on with MMIO. [ You may be thinking, "But I need the correct size of blob read or written or my driver won't work", Rust has generics, so these functions are generic over the integer type you're reading/ writing, if you read a u64 that's a 64-bit read, if you write four u8s that 4 x 8-bit writes and so on ] But C's volatile is a type qualifier instead. Why? Would it mean anything to, for example, integer divide an MMIO fetch by fifteen and write it back? a/= 15; No. So then why make it a type qualifier? When volatile was added to C they'd only just invented simple optimisations like re-ordering so they had no idea this was a bad idea, and it seemed simpler than adding an intrinsic (though not by much) but today we know better.
- dang 5y agoSome past threads: The Descent to C - https://news.ycombinator.com/item?id=15445059 https://news.ycombinator.com/item?id=15445059 - Oct 2017 (2 comments) The Descent to C - https://news.ycombinator.com/item?id=8127499 https://news.ycombinator.com/item?id=8127499 - Aug 2014 (15 comments) The descent to C - https://news.ycombinator.com/item?id=7134798 https://news.ycombinator.com/item?id=7134798 - Jan 2014 (230 comments)
- AceJohnny2 5y ago> So why is C like this, anyway? Worth mentioning that C is over 40 years old, and was designed to be easily portable across a range of machines that had less compute power and memory than today's smaller microcontrollers. As a result, a lot of things were left undefined, or were designed in a way to be easy to implement rather than easy to program for. There existed other programming languages that were better, but their compilers weren't as broadly available, and their better features came at the cost of speed, which at the time was a premium.
- fsckboy 5y ago> left undefined, or were designed in a way to be easy to implement rather than easy to program for. I'd tweak your statement a little, or even a lot: "left undefined" most often meant "left to be defined by the compiler writers to fit the architecture of the underlying hardware, in a way that would make it easy to program to beneficially exploit features of the architecture"; and (yes) in a way "that would not be very portable, and might even be subject to change between compilers". did the underlying machine use 2's complement? did the underlying machine have addressable bytes? big endian? 8, 16, 32 or 36 bits? These are all things you need to know to write tight efficient code in the days of slow clockspeeds and limited RAM. C let you do that without using assembly, but by using the "undefined" features of the language, because they were clearly defined locally and were features that were very important to be easy to write code for. consider how you would implement setjump and longjump, or even printf, or efficiently unpack or serialize bits for a communications protocol, without these supposedly "undefined features", or how you would write those if those features were actually undefined. People who put strlen(str) or a divide and a mod in the control expression for a loop would know better if they understood a bit more about the undefined features. this is in contrast btw with some other things that actually are undefined, such as what the order of evaluation would be for complex expressions making up argument lists, etc. I'm writing this explanation not so much to explain these technical details to noobs, but rather to get the people who understand this stuff to stop throwing around the term "undefined" with regard to C because they are cooperating in the evisceration of some ideas that are really worth exploring or understanding more deeply.
- 5y ago
- Animats 5y agoIt's not inherent in C being a low-level language that it has such a painful memory model. It's a consequence of having to fit the compiler into a really tiny machine by modern standards. Originally, there were no function prototypes. I once proposed a backwards-compatible way out for C.[1] Basically, you get to talk about arrays as objects with a length, even if that length is in some other variable. And you get slices and references. It was discussed enough to establish that it could work, but the effort to push it forward wasn't worth it. Slices are important. They let you do most of the things people do with pointer arithmetic. Once you have size and slices, you can still operate near the memory address level. [1] http://www.animats.com/papers/languages/safearraysforc43.pdf http://www.animats.com/papers/languages/safearraysforc43.pdf
- WalterBright 5y agoIt's similar to what I proposed in 2009: https://www.digitalmars.com/articles/C-biggest-mistake.html https://www.digitalmars.com/articles/C-biggest-mistake.html except mine is much simpler :-) The proven utility and effectiveness of this is apparent in D.
- pjmlp 5y agoBy now it should be clear that WG14 doesn't care about improving C's safety. A type like yours could be added, just like a string library like SDS, but it will never happen.
- markhahn 5y agoSurprise: there's a whole bunch of different reality under your the convenient illusions of your "high level" programming language. Hardware is the reality. It's not very much like the Java or Python programming model. We shouldn't hide this from programmers.
- doctor_eval 5y agoI disagree. C is also an abstraction of reality over assembly, assembly is an abstraction over machine code, which abstracts microcode, which abstracts over logic gates, and there are both deeper and adjacent abstractions as well. Underlying it all are different mathematical abstractions from karnaugh maps to quantum physics. A good developer will IMO select an abstraction that best matches their goals. If you’re writing a device driver then the abstraction might be assembly language. If you’re writing business logic it might be PL/pgSQL. But regardless of which abstraction you choose, you’re hiding something from someone.
- arunc 5y agoIn other words, hiding the complexity beneath
- 1vuio0pswjnm7 5y ago"The Python interpreter is written in C, for example." https://github.com/RustPython/RustPython https://github.com/RustPython/RustPython
- turminal 5y agoTHE Python interpreter is written in C. People usually refer to CPython when phrasing it like. That does not mean there are no python implementations in other languages, but they all have minuscule market share compared to CPython.
- deleted 5y ago[deleted]
- zzo38computer 5y agoI use C because I think that it is good (some improvements could be made, but the other programming languages which try to make C better tend to make many things worse in my opinion; I like many (but not all) of the features of (Digital Mars) D, though). I think that the worst feature of C is the confusing syntax for types. (Maybe the way to make a better one might be like "LLVM with macros", although there are a few problems with LLVM too.) Some compilers and libraries, such as GNU, do have some improvements. For example, in GNU you can make zero-length arrays (which I use sometimes), ?: without anything in between (which I use often), and some other things. They say there is no object-oriented programming in C. Well, C doesn't have object-oriented features, although you can still do some limited object-oriented stuff in cases where it is useful. For example, there is the stream object (called FILE); GNU has a fopencookie function to write your own implementation of the stream interface too, even though standard C doesn't have that. Object-oriented programming is good for some things but too often is overused in modern programming, I think. You shouldn't need object-oriented programming for everything. It is true that some of the undefined behaviour stuff is too confusing and perhaps should be changed; in some cases the compiler has options to control these things, such as -fwrapv (which I often use). I like the string handling of C; you can easily skip some from the beginning, and can sometimes use the string functions with non-text data too, and it doesn't use Unicode. It says "C lets you take the address of any variable you like", but this is not quite true. There is a "register" command which means that you cannot take the address of something. I think that many things in C (both things that they mention and some that they don't) (including pointer arithmetic, no bound checking, untagged unions, string handling, not using Unicode, setjmp/longjmp, etc) are often advantages of C.