Testing Your C Knowledge With This One Simple Quiz

One of the most exciting aspects of the C programming language — as effectively high-level assembly — is that although it’s a bit friendlier for the developer, it also adds a lot of required know-how on account of its portability across platforms and architectures. This know-how is what [Oleksandr Kaleniuk] manages to wonderfully illustrate with a simple 5-question, multiple-choice quiz on what the return value is of the provided function snippets of C code. How well do you know C?

For those who have had their run-ins with C directly (or indirectly via the support for it in languages like C++) the words ‘undefined behavior‘ (UB) are likely to induce a nervous twitch or two, along with a suspicious glance at whichever parts of reality are about to evaporate and destabilize the Universe this time. Although it is said that a proper C program is written with zero UB cases in it, in practice this can be rather tough, even before considering the other exciting ways in which a piece of code can fail to do the expected thing.

For languages other than C this is of course also a challenge, which is the reason why certification programs for e.g. avionics go out of their way to weed out such preventable issues, and only few programming languages like Ada (anything avionics, medical, etc.) and C++ (F-35 and other US DoD projects) make it into devices where failure is literally not an option.

53 thoughts on “Testing Your C Knowledge With This One Simple Quiz

  1. “…and I’ve done some successful projects in C on my first full-time job, and even then, when I was mostly working with C++, I thought of it as over-bloated C.” lol; no, it’s not.
    I did fail the last one, though. I thought that was well-qualified, but I was wrong.

  2. I both appreciate and don’t terribly like this quiz. I got all the answers correct, but really, these are somewhat silly because in my experience, if you’re writing in an environment where you have any doubts, you’re going to be using `int#_t` and so you will “know” the answer. And if you’re being extra pedantic, some of these you would have as macros-asserts and your code wouldn’t compile!

    As someone who has written a lot of extremely pedantic C and C++ (I worked on safety-critical embedded systems where I drooled over the future where something like Ferrocene existed – https://ferrous-systems.com/ferrocene/ ), the types of “things programmers get wrong because they assume they know the answer” are just more subtle than any of these.

    Admittedly, the types of weirdness you run into with embedded systems and ostensibly “portable C” code are just terrifying, and almost all the nastiness lives on the serialization/deserialization/parsing code. Have you ever worked with a system where a char was 16 bits? Where NULL was non-zero? Have you ever had to implement causing a segmentation fault when a NULL deref happens? Embedded! Yeah, it sucks down here.

    I think all I really really want to say is – know that the demons are deeper than even this simple quiz implies. Yes – you probably don’t know the size of stuff at runtime unless you implemented compile-time checks. No, you probably shouldn’t write your own serializer. If you’re down in the weeds trying to implement something on bare-metal in C? Good luck! We all need it.

    1. Largely agree, however often you come a cropper due to difference compilers.

      I have to say though that if I saw any of my team writing code like this, I would slap their wrists. C can be written safely and process independent, but it takes a little discipline and an understanding that even though you can write code like that, it does not mean you should. The mantra should always be to write clear, understandable code even if it takes 3 lines instead of 1

    2. There are reasons coding standards like MISRA C and CERT-C exist (and it’s a big part of why I got all that right).
      IMHO a big issue is that C hasn’t really been updated. Sure there is C11 and C18 but those don’t really impact things like the type issues or order of operations issues highlighted by this quiz.
      It’s not just undefined behavior that is a problem, it’s also implementation-defined behavior. For example, the number of significant characters in an external identified in the C standard vs what compilers actually allow.

      I’ve done enough bare metal C to both appreciate it’s capabilities (at least over something like assembly), to fear how easy it is to shoot yourself in the foot, and understand the struggle of having to implement stuff that is just there in other programing environments (and often done better than I could manage on my own).

      What you’ve posted about RUST is interesting. As I learn a little more about the language here and there, I’m getting interested in it. I haven’t had a need professionally to learn the language but I am growing more curious about it, especially as I’m feeling burnt out in my current role and I’m looking for a change.

  3. While it is possible to write reliable C++, the “enough rope to shoot yourself in the foot” axiom still applies. Also, when did the US DoD move away from Ada?

    My bachelors degree was a UK MoD sponsored course aiming to try and fill a skill set shortages (make more people who can interface hardware and software. As a result we had courses on Pascal-FC, which the UK gov choose over of Ada.

    1. The US DoD didn’t so much ‘move away from Ada’ as that they saw themselves forced to tap a larger source of developers using a still pretty safe alternative. DoD C++ is pretty restrictive, with no C-isms and the like from what I understand.

      Of course they’d love to have everything in Ada, but when too few new people learn Ada, those missing developers cannot fill those positions, etc.

      I mostly write in C-like C++ (including embedded), and have become pretty proficient in dodging said foot shooting, but doing a project in Ada is always a breath of fresh air. Having the compiler yell at you for a while for every single little thing may seem annoying, but the ‘it just works’ part when you run the code always makes me appreciate the language’s design.

      1. Thanks, that makes sense.

        At the time (early/mid ’90s) I was disappointed we were not studying ADA. Pascal-FC just seemed like a bit of a hack and a would be a dead end, even in the short term.

      1. Not necessarily. The idea is to make sure you refer back to something concrete in your target environment’s headers. We’re just not making assumptions about sizes. Same goes for packed or padded structs. It can be controlled where it matters.

  4. The questions are interesting to think about, but the author seems to be trying to say “aHA, you’re not as smart as you think you are!”

    Only, the gotcha falls flat because, no, I absolutely DON’T imagine I know the specs that well, which is why I (like most people) would never write any of this code. And if I saw “i++ + ++i” in published code, I’d consider the author to be an objectively bad coder regardless of whether it’s formally correct, because they’re deliberately failing to communicate effectively, for no reason.

    Once upon a time, I would look up C operator precedence so often that I had memorised the page number from K&R (54 I think). And then it occurred to me, this is stupid, because anyone reading the code – myself included – will also to have to spend minutes looking that up, when they could read the expression instantly if I just used more parentheses. Arcane language details are like highway crash barriers – the point is to learn how to NOT run into them.

    1. amen!!! i don’t use a *lot* of parens in my code but i’ve seen enough bugs that came down to && vs || vs ? : sort of precedence that i *always* use the full complement of parens in any complicated comparison. and yeah once you ask what order side-effects are evaluated in, you’ve reached the point where you need to add some semicolons and split things up.

    2. I used to add parentheses to code to make it clearer, but these days I prefer to put sub-expressions into named variables. If I need to expend the mental energy to work out what each variable is doing and it makes semantic sense for them to be nested then there’s probably a way to express that in a variable name. If I end up with a final expression that reads like a sentence then great!

  5. In over 50 years I have only used C when I have to and I’m not very good at it. But when I have to, I grab my copy of “Expert C Programing: Deep C Secrets” By Peter Van Der Linden who when he wrote it was on the compiler team at SUN. His Magic Decoder Ring for C Declarations and several de-obfuscators and sections on all the things that “don’t work right” has been invaluable. I see it is on archive.org in the form of the digital version of the book with perfect formatting and all that.

  6. Honestly, even though I’ve fully studied, I still have a hard time wrapping my head around pointers. I feel this is only a matter of practice– Yet I don’t know how to practice (?) Does anyone have any suggestions? I mean, obviously, otherwise you are not going to just write code for ‘nothing’.

      1. C does not use * and & or think about pointers (or handles) in a way that any same assembly writer would. As in used in more than one way in a small bit of code. Points to versus address of and all that?

        1. I’ve used/learned like PIC ASM. Somehow there it ‘makes sense’; But also if you are actually using assembly in such a case, you are probably not wrtiting a huge program. I’m not sure why I’ve still found it confusing… Yet I felt this was a good forum as any to ask.

          1. It uses * to both declare and indicate a type, and as an arithmetic operator. As if you could use + to add two values and use + to define an “add type”. It stinks.

    1. The concept is pretty simple to understand. A piece of memory containing the address of another piece of memory. To the memory it’s all just bytes. To C and the developer you can see it as a variable or a pointer, it’s just a matter of definition.
      The hard part is how it’s implemented in C, with ref &, deref *, the ways you can use arrays (ie *(a+x) or a[x]) naming schemes of variables like pString or _string etc makes it really hard to understand and use. Especially when dealing with nested structs etc. Adopting a simplification of these principles is recommended and stick to that.
      Practice by reading others code a lot, trying to understand what’s happening, stepping through the code with a debugger. See how variables and memory locations change etc.

  7. The explanation for why 4 is undefined is technically correct but I can’t think of any implementation where the value wouldn’t be 1; even if int is 16 bits and there’s some particularly weird implementation detail of how overshifting is handled on the architecture, any 16-bit integer shifted right by 16 bits will be either 0 or -1, and in both cases those would make the final result 1. What am I missing?

    1. i happen to have just been bit by this, shifting a long long by (64-n)…a lot of hardware shift instructions mask the shift count to only consider the lowest bits. so like if you are shifting a 16-bit type, the instruction may only support the numbers 0-15. if you give it 16, it will only look at the bottom 4 bits and it will see 0. so it will shift by 0. because of this variability in hardware implementation, and a desire to have a moderately efficient shift idiom, the C standard leaves shift overflow undefined.

      it gets right confusing because some platforms will allow a shift count of up to 63 bits even if the logical datatype is a 32-bit int. and to add insult to injury, sometimes the optimizer might have different overflow behavior than the instruction set. that seems pathological but when you’re dealing with an instruction set itself that might have several different shift instructions, it can be hard to decide which one should be “canonical”. with this modern 64-bit world we live in, a lot of chips and ABIs have both a 32-bit “register pair” and a 64-bit “big register” way to do a shift, one instruction from 1970 and the other 1998. it can get very involved so the standard just says over-shifting is undefined.

      of course 1<>0 is still 1, so even on those platforms, it would probably get the same answer. but it’s undefined.

  8. heh the 4th one got me because i had an epiphany back in 1995 that i would never again use a platform with 16-bit ints (so i use assembly, not C, when programming PICs), and i have never looked back and at this point i can confidently say that i never will.

    int is at least 32-bit. but the other ones are truly and practically undefined. i know environments off the top of my head that will give different answers for those…i don’t know, i guess i’d be surprised if #2 wasn’t 0, but…. (i don’t have strong opinion on short, though i know it’s always 16).

    1. False. int is as least 16-bit. I know that from working with Atmel(now Microchip) AVR MCUs. I quickly learned to use stdint.h and think about the worst case value of variables so I would never get an overflow.

  9. This quiz makes a good point about why the language is dangerous, because it easily seduces one into thinking they know when in actuality, they don’t. That’s one of the worst thing a language can do. Very well done, gave me something to think about.

    1. It’s actually not such a dangerous language as you seem to think. If you know and understand the platform (cpu and C version) that you’re programming for, there is no danger other than the normal dangers of creating a bug. Because you wouldn’t make assumptions.

      C is not a high-level language. It’s a low-level language with high-level constructs. If you understand how to program your machine in machine code/assembly, you are quite safe to program it in C.

      Only developers who regard C as a high-level language that virtualises the underlying hardware (like Kotlin, C#, Swift, etc.) will experience any dangers. The developers who regard C for what it is, see only advantages.

      Of course, not saying that the last won’t make any bugs. But they would have probably made the same bugs if they would have written in assembly instead of C.

      I would personally say that > C++ < is dangerous. Because C++ does pretend to be a high-level language, trying to hide the hardware more than C. It's easy to forget that C++ is still C and not (e.g.) Swift.

      Most of my younger colleagues learned Swift as their main programming language (and why not). And all of them have traumas from having tried to program in C++. While I (being quite a bit older ;)) learned C as my main programming language, and I have a hard time understanding their problems with C++. And I regularly curse Swift for making it so hard to do bitwise operations and accessing memory buffer directly. Take for instance parsing packed data that we receive from other systems (devices running BLE, that try to pack as much data in 23 bytes as possible). It's so simple to do in C/C++, but I need so much code to parse such data when using Swift…

    2. heh i’ve been thinking about this one and i have a totally different take than RetepV… i have a lot of opinions on this :)

      i don’t think this quiz is really a good example because the undefined behavior it points out isn’t really that dangerous. the standard doesn’t define these things, but you have to know them. when you’re writing string code, you have to know if you’re targetting ASCII or EBCDIC or UTF-16. if you’re going to use ‘int’ (and i do), then you have to have an idea of how big it is because 65536 is a really tiny number. you should never use sizeof to probe int promotions. you should never chain ++ — side effects without a semicolon in between. they’re undefined behaviors but without real-life consequence.

      i’m saying, people don’t actually write deeply portable code all that often. whenever you’re coding, you have to have a vague idea in your head whether malloc(1000000) might succeed. embedded development is different from PC development. it’s neat that we can use the same language but most code is going to target one or the other. and sometimes people do aim to be really portable, and when they do that, using int32_t and so on isn’t really a big burden.

      but i want to acknowledge there are hazards, they just aren’t in this quiz. there is a harmful fad in compiler development. an example is overflow…for decades, we have known that integer overflow is twos-complement, even though the standard doesn’t specify it. this is such a useful bit of knowledge! gcc, though, wants to say that because integer overflow is undefined, they can define that it can’t happen! one of the carrots they’re chasing is that it can eliminate comparisons. if you have a loop where i starts at 0, and is only ever incremented, then i think gcc can eliminate some comparisons to zero. like if (i lessthan 0) can be translated to if (false), because i++ can never generate a negative number. that optimization is going to give an imperceptible benefit, really far along the law of diminishing returns. but it costs so much, because it means you can’t overflow your integers.

      but they’re hostile, they go a step further. if they detect that you are intentionally overflowing an integer, they *INTENTIONALLY GENERATE INCORRECT CODE*, because they feel like the standard doesn’t say they won’t. without a warning, too — they’ve detected that they aren’t honoring programmer intent and they silently generate the wrong value. really frustrating. i think their take on the standard is wrong — ‘undefined’ doesn’t mean ‘defined to not happen’. it’s simply a wrong fad.

      and fwiw this goes against what RetepV said — knowing assembly puts you in this hole, rather than liberates you from it. ugh.

      and they do the same thing about NULL dereferences. the language defines that you won’t dereference a NULL pointer. the carrot for this one is a little more substantive. once the compiler sees that a pointer is dereferenced, it can optimize away all the checks against NULL. since pro forma redundant NULL pointer checks are genuinely prevalent in a lot of coding patterns (macro expansion etc), it can actually be a win. but there are a *lot* of environments (not just embedded ones) where there is an OS/environment block at address 0. so on those platforms, if for some reason the programmer compares the environment pointer to NULL, they might get the wrong answer. not a big hazard imo but certainly a surprising result.

      but, again, gcc doesn’t stop there. if it detects that you intentionally dereference NULL, it doesn’t generate a warning, but it does generate an explicit illegal instruction. i think their logic is that you must be hoping for a memory fault, so maybe an instruction fault would be just as good for you. but it is aggressively evil to generate bad code instead of honoring programmer intent.

      but the good news is, while there is a malicious group within gcc, they are counterbalanced. I use -fno-delete-null-pointer-checks -fwrapv. and so do financially-influential corporate customers, so i think those features are safe.

  10. Follow the MISRA Consortium’s advice, and nothing will beat well-structured, functional C for bottom coding. Spend the time to get good at it (a 10-year journey), and you’ll be rewarded with elegant and mathematically beautiful source, that runs more economically and faster, than anything else.

    1. My only problem with MISRA is there hate of multiple exit points which if followed to the letter will end up with deeply nested spaghetti. MISRA is good if you use it as a guideline not a bible

  11. Terrible quiz. There is a difference between I don’t know the answer and the right answer isn’t listed or the answer is undefined. I thought at least some answers were undefined, but that option isn’t listed, so it forced me to take a guess.
    Anyway as someone with more than a decade of C experience I know enough not to write code with undefined behavior.

    1. “I know enough not to write code with undefined behavior” – this is hubris. Loads of code can be undefined if you don’t define the environment. The examples in this quiz are presented in an academic way, of course, in the real world you won’t stumble across or write code that says “what does this do? wink wink”, but they’re real edge cases that can and do catch people out. You’ll most likely only find them professionally when you’re debugging a ‘working’ system that sometimes exhibits undesired behaviour.

  12. I’m aware of the fact that C has its issues. But when going through the quiz (which made me frown and laugh at the same time) I could not help thinking that every language has it’s issues. However, no matter what language you use, there will always be people who prefer to cram a 3 liner piece of code into one single line and without the use of any comments.

    Creating readable code is key to all software in the war against bugs. No matter how smart you think you are, when reading back code a simple comment might come a long way in understanding what the intention of the code is (without executing it in your mind first). There is no shame in commenting your code.

    1. I sort of agree and also disagree.
      I prefer to see “self documenting” code. Name your functions, methods, classes, structures, variables, etc., etc. well enough, and keep your code blocks short enough, that reading it becomes like reading a book. If there’s something particularly complicated then by all means write comments that support the code, and even link to documentation, but don’t choose first to write unreadable code and then write a comment explaining it.

  13. Stupid quiz. If you are in project that allows this kind of obfuscations, then run as fast as long as possible.

    Just “fun” hocus pocus trickery but nothing to do with professionally written C.

    1. Not just obfuscations/trickery, but actualy the source of some serious bugs. The compiler makes use of UB. Especially with -O3 flags, you can really encounter the effect of optimizations that are perfectly legal, but seem like a compiler error. Even with -O2 flags, the linux kernal had some famous bugs due to UB.

  14. Yes. I got them all right BUT to me, the questions were presented so that answer ” I don’t know” would always work. Anyway, no matter what code I am writing, the devil is always hiding in the details (or not) and ready to strike. PEEK & POKE, where are you?

    1. “You only worked with C for 15 years, I read the book and made a quiz, look at me!!! I did not give you the compiler settings, /smirk. You made a bad assumption.”

      Thank you so much for your wisdom. I wonder how we possibly managed all these years…. all those random structure aliments. Oh my god, my assembler code could not possibly interface with that?!? What ever will we do?!?

  15. Now that I recovered my sanity and cooled off, I still find that quiz offensive and misleading.

    All the examples are academic and in practice will produce solid results, for the last 30 years worth of compilers. If you need to fire up a Borland compiler from the 80’s sure, you’ll run into issues. char will be unsigned. But any modern compiler based on clang or gcc will behave consistently. C has to interface with other languages, you’ll find your data aligned based on your compiler settings. I had wished many times that structures could be reshuffled and optimized by the compiler, but this does not happen because of comparability with other languages and system components.
    Integer math is well defined, there is a clear outcome based on the size of the variable. “The standard” might allow of ambiguity but your compiler will have you covered.
    Even big endian vs little endian has not been a problem for over a century. The ARM cpu on your mobile is set to use little endian, making it compatible with your AMD/Intel code.

    The only time I have ever run into a cross compatibility issues was the order in which comma separated arguments where pushed onto a stack… gcc vs msvc (even that was over 20 years ago now) :

    int rgb2int(int r, int g, int b) {return r<<16|g<<8|b;}
    int color = rgb2int(*pRGB++, *pRGB++, *pRGB++) // might give bgr vs rgb

    Anyway, the link below, offers access to an article with many compilers. Maybe run the quiz through it and see if you can manage to get an inconsistent result.

    https://hackaday.com/2019/09/13/peek-into-the-compilers-code-lots-of-compilers/

    PS: I hate that quiz, I hate "gotcha" questions. "Rosters don't lay eggs, heh".

  16. I can say what would always happen in the environments I normally have, and that when they aren’t, the parts that resemble anything that there would be a good reason to do are all things that you will know the result of when you choose to do them. If the code I’m writing is the firmware for a flashlight, it’s never going to run on a x86_64 environment. It may be that I only have a few hundred operations in between each PWM cycle in order to do everything I need to do – measure temperature, voltage, take user input, calculate new drive parameters, etc, and I need my code to compile into like 2K of space. I’m just not going to run checks and protect against the eventuality where I actually have a FPU and multiplier, or I have numbers greater than 8 or 16 bit available and my overflow-conscious decisions are wrong.

  17. I’ve taught embedded systems security, in which we use several of these ambiguous code snippets. It’s always an interesting moment when attendees argue with the compiler. A good way to test and practice is to use godbolt as it allows changing the compiler, and observing the assembly can be instrumental.

    They’ve taught me assembly, C and later C++ in university, and currently mostly use Python extensively. But C gave me a job as a security researcher, for which I’m grateful for.

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.