2020-03-17 23:30:42 (edited by amerikranian 2020-03-17 23:32:41)

So I wrote this small guess the number game in C and want some feedback on my code. My code is below. My questions are below the code. I would appreciate someone answering them.

#include <stdio.h>
#include <string.h>
#include <time.h>
#include <ctype.h>

int main()
{
    time_t timeOfDay;
    srand(time(&timeOfDay));
    int actualNum = rand() % 8 + 1;
    int guesses = 5;
    int guessed;
    do
    {
        printf("You have %d guesses remaining. Your guess?\n", guesses);
        scanf("%d", &guessed);
        if (guessed < actualNum)
        {
            puts("Too low!");
        }
        else if (guessed > actualNum)
        {
            puts("Too high!");
        }
        else
        {
            printf("You got it! It's %d!\n", guessed);
            break;
        }
        guesses --;
        }
        while (guesses > 0);
    puts("Thanks for playing!");
    return 0;
}

The program works... for the most part.
1. Whenever a letter is entered instead of integer, the program continues to subtract guesses from the user without giving them a chance to enter another guess. Why does it do so?
2. I don't exactly understand the rand function. I understand that it returns a number from 0 to 32767, but I don't get how to, get a number from, say, 1 to 10. The C programmer's Absolute beginner's guide mentions that to get a number anywhere from 1 to 6 you'd do rand() % 5 + 1, which implies the following formula to derive a random number up to a given value:
rand() % (value - 1) + 1,
where value is the maximum number you wish to obtain. I.e, to get a number from 1 to 10, the expression would look like so:
rand() % (10 - 1) + 1.
Trouble is, the book doesn't really mention any more random number generation as of chapter 21, giving me a brief example and moving on to the arrays.
Is the method I am using to get a number from 1 to 9 in the guess the number correct? If not, what needs to be tweaked?
3. Why do you need to use the & character when passing the timeOfDay to the time() function?
4. Are there anymore improvements one could make to my code, and if so, why?
Thank you for any assistance.

2020-03-18 00:33:21

1. scanf() has a return value: it will either return EOF if an input failure occurs, or the number of items read, which may be fewer than the amount you want to read. So you need to check the return value; if its EOF or zero, you've gotten nothing. If its one, then you've read what you need to. So change

scanf("%d", &guessed);

to

if (scanf("%d", &guessed)) {
//...
} else {
//...
}

.
2. No, rand() does not return a number between 0 to 32767. The rand function computes a sequence of pseudo-random integers in the range 0 to RAND_MAX inclusive. RAND_MAX is implementation-defined; that is, the implementation (the C library) is free to set it to whatever it likes. Do not assume it is 32767 or 2147483647 on all implementations. Though those are the most common values, it is best to not make such assumptions. As such, your formula then becomes

rand() % maximum

.
The method your using is correct, though it is pseudo-random. The C standard does have this to say: "There are no guarantees as to the quality of the random sequence produced and some implementations are known to produce sequences with distressingly non-random low-order bits. Applications with particular requirements should use a generator that is known to be sufficient for their needs." In other words, the randomness of the numbers that you get back is not guaranteed. If you need higher-quality random numbers (e.g.: for cryptography) do not use srand()/rand().
3. The time() function is defined as

time_t time(time_t *timer);

. As such, it takes a pointer to a struct time_t. The & operator, in this context, retrieves the memory address of the time_t you constructed and converts it into a pointer, then passes that to time(). When time() is done, it fills in the time_t structure that you pointed it to, and also returns it.
4. Not that I can see. Good practice thus far.

"On two occasions I have been asked [by members of Parliament!]: 'Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out ?' I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question."    — Charles Babbage.
My Github

2020-03-18 00:35:29 (edited by camlorn 2020-03-18 00:38:49)

I'll bite, being as I do C every day.

1. You're not checking the return value of scanf, which means that when it fails, you don't know that.  From here:

Return value
Number of receiving arguments successfully assigned (which may be zero in case a matching failure occurred before the first receiving argument was assigned), or EOF if input failure occurs before the first receiving argument was assigned.

I don't use scanf at all, but if I recall correctly the argument just remains unassigned. You need to check the return value, and if it's anything but 1, print an error.

2. Rand doesn't return 1 to 32767 but rather 1 to RAND_MAX, again grabbed from cppreference.  RAND_MAX is implementation defined.  In general C random numbers suck, and if you want to do anything even remotely cool with them you either need to use C++ (which has a somewhat involved random number facility that is powerful enough to do basically anything) or bring in a library for it.

The % operator is remainder.  That is: 9%5 = 4 because 9 divided by 5 is 1 remainder 4.  If you take any number and do number % x, you'll get numbers between 0 and x-1.  Some examples:

0%5 = 0
1%5 = 1
2%5 = 2
3%5 = 3
4%5 = 4
5%5 = 0
6%5 = 1

And so on.  Note that 0%5 and 5%5 both equal 0, because there's no remainder.

So, to get numbers between 1 and 9 the formula is (rand()%9)+1.  We derive this because rand()%9 gives numbers between 0 and 8, which is 1 too small on both ends.  I don't know if the parens are optional, but with rarely used C operators it's usually best to be explicit about it because "oops precedence" surprises abound and it's not worth consulting a precedence table when you work with them.

Your resources on C random numbers don't go beyond this because going beyond this isn't built into C, and they don't explain in detail because modulus/remainder operator is a bit fiddly.  Also anything more complicated with random numbers beyond exactly this formula and "get me a number between 0 and 1", in general, needs a treatment of probability distributions.

3. & is the addressof operator.  But before getting to that, you can just pass NULL to time, and avoid the variable entirely.  NULL comes from (among many others) stdlib.h.  That is, time(NULL) does what you want (and you don't need the & there. It is somewhat best to think of NULL as magic at your level, but if you're curious NULL is just a way of writing 0 that you use when you're dealing with pointers to make it clear that you're dealing with pointers and if you want every C programmer ever to eviscerate you you can also do time(0)).

I don't know if you've done pointers yet, so this might be confusing for the time being.  If you know what they are, skip this paragraph.  In C there's ints, floats, and etc. as you're familiar with, but also pointer to int, float, etc. which are references to an int.  If you want you can pass variables to a function by saying that the function takes a pointer, then passing the address of the variable in memory to the function, and the function can write to that variable by dereferencing the pointer, as C calls it.  Examples below.

The & operator in C has two uses.  If it's between variables--a binary operator--like x & y, it's bitwise and, which you won'd tneed to use for anything for a long time yet.  If it's unary, that is before a variable like you put - before negative nuymbers, it's the addressof operator, which takes a variable and produces a pointer to that variable.

The opposite is unary *, which converts a pointer into a not-pointer...if it's on the left of = you're saying "write to what this pointer is pointed to", but on the right, "read this pointer".

This makes more sense with examples. So, some examples, with comments:

int x = 0; /* Normal int. */
int *p = NULL; /* A pointer to an int, currently pointing at NULL, i.e. nothing. */

x = 5; /* You know this. */
p = &x; /* p now points at x. */
*p = 6; /* Set x to 6, but do it via p. */

int y = *p; /* y = 6, but instead of just using x, we read what p is pointing to. Since p is pointing at x, we get x's value. */
y += 1; /* Normal increment. Nothing happens to x. */
p = &y; /* Now p points at y. */
x = *p; /* Now x = 7, like above with y. */

What time does is something like the following:

time_t now = now(); /* Not real, just magically gets now in some magic way. */
if(parameter != NULL)
    *parameter = now;
return now;

So if you just pass NULL, it won't also write to the variable.  There is probably a very good historical reason as to why it does both, but you will probably never actually use the write-to-a-variable functionality.

As for 4, some comments:

Your code is the first time I have seen puts used in practice, and I had to look up what it does.  It is much more typical to use printf("A line of text\n"), and you pay no extra cost in using printf that way unless you start using format strings.

You can use a while loop instead of a do loop.  Do loops are kinda scary, because they are always guaranteed to run once.  It's not bad that they always run once, but when I see a do loop my first immediate question is "Why does this need to run once? What don't I understand?"  By the nature of C getting loops wrong can cause weird hard to debug crashes, so it's always an interesting question.  Additionally, the while loop puts the condition at the top, which makes it immediately obvious that the loop ends when there's no more guesses.
But you can actually do one better.  Observe (not exactly plug and play with yours, but will get the concept):

for(int i = 1; i <= maxGuesses; i++) {
    int currentGuess;
    /* get guess. */
    if (currentGuess == goal) {
        printf("Yay!\n");
        return 0;
    }
    printf("%i guesses remaining\n", maxGuesses - i);
}
printf("Sorry, you lose.\n");

Which both makes it immediately clear what the loop is for, and makes sure that the maximum number of times the loop can run is clear to anyone used to reading C, since that's (almost) the idiomatic way to make a loop run up to a certain number of times.

I like one true brace style.  That is:

if(condition) {
    stuff
}

Instead of what you do where the brace is always on the second line.   In sighted programming land, this is the typical style, but additionally for us you don't have to always read the left brace with the screen reader.

Additionally to that, for one-line if statements, I (and many sighted people) do it as:

if(condition)
    printf("Yay\n");
else
    printf("No!\n");

Which avoids the noisy braces.  If you're sighted or have indentation indication on, the indentation indicates which statement goes with which if statement.  In general I don't suggest dropping braces for loops, and even though you can put it on the same line as the if statement if you want, putting it on the same line makes skimming harder because you can't just find out what the long if condition does without reading the whole condition first with the screen reader.  Plus, putting it on the same line is a really long line which is bad if you're collaborating with sighted colleagues since it can be wider than their screen.

Initialized guessed.  In general, always initialize variables.  You don't have to if you have the right compiler warnings on and have configured compilation to treat warnings as errors, as modern compilers will give you notice that you used something without initializing.  But I doubt you've done either, and in general you won't want to do either for other reasons at the stage you're at, so you will avoid much weird behavior by initializing numbers to 0 and pointers to NULL.

I could possibly go on , but overall this is actually pretty good code.

EDIT: my example loop needed return 0, not break, because if you break you'll tell them that they failed.

My Blog
Twitter: @ajhicks1992

2020-03-18 01:09:14

Thank you both for your feedback.
I will most likely pick up another book after I finish reading, as I have found small issues with the absolute beginners guide. I appreciate your explanations. I used puts because the book introduced it to me and I wanted to cement it in my mind. I do agree that printf is more useful, primarily because it is not limited to just outputting strings.  Will use that in the future.  Same thing with they do while loop, no specific reason besides cementing the syntax in my mind. I have worked with it before a couple of times, so I wanted to make sure that it function the same way in the language.  I would have used a while loop instead, but hey. For loop works just as well. Didn’t even think about that, to be honest.
Again, thank you. I will probably be back sometime soon with some more questions.

2020-03-18 01:36:39 (edited by Ethin 2020-03-18 01:37:35)

@3, I generally don't approve of non-braced compound statements (if, for, while, ...). Its generally a bad practice and triggered the (not too old) Apple Goto Fail; bug. I always brace my compound statements and would recommend everyone else do the same, because it makes reading code, IMO, a lot easier, and its a lot harder to introduce a bug like that this way. (You could still do it trivially but it'd be pretty damn obvious.) Good advice in this post though -- our posts complimented each others quite nicely. smile

"On two occasions I have been asked [by members of Parliament!]: 'Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out ?' I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question."    — Charles Babbage.
My Github

2020-03-18 01:41:46

The one nice thing about C, as opposed to any other language, is that things usually do work as you expect.  Unfortunately the flip side is that the lack of magic and advanced features can make things take lots of effort and be uglier.  Also, nothing's wrong with do loops.  Just at scale they're rare, and rare things are usually used with reason, and so when rare things are seen usually you eventually learn to go looking for the why.

Find something that covers pointers well.  I don't know what languages you're coming from, but the big sticking point for most people in C is pointers.  Unfortunately I learned this 10 years ago and even if I still had references and tutorials handy, they'd be dead links by now.

And FYI, look at CMake or Meson when you start dealing with multiple files.  They're both CLI and they're both able to be used on Windows (though I haven't personally run Meson there).  Very simple use of either is a few lines and a couple shell commands to initialize, then one shell command to build.  CMake is much more powerful; Meson is much simpler.  They're both massively easier than the venerable but quite archaic make that tutorials will eventually try to get you to use, and you can avoid VS on Windows with them, which you might or might not want to do depending on your preference.

My Blog
Twitter: @ajhicks1992

2020-03-18 01:56:45

I tried to set up GCC, but everything I have found online didn’t give me the actual executable. It gave me the source and told me to build it, something which I could not do. I am currently using the developer power shell for VS 2019 and have not encountered any issues with it so far.  I do eventually want to look into a more official compiler, though.

2020-03-18 01:58:08

Agreed with 6. Pointers can be difficult to grasp, but once you've gotten the hang of them they're quite powerful and nice to use. And second here for the build system. Make is archaic and difficult to use properly (if you want a good example, check out any of the makefiles of any of the GNU projects).

"On two occasions I have been asked [by members of Parliament!]: 'Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out ?' I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question."    — Charles Babbage.
My Github

2020-03-18 02:00:11

@7, MSVC is official but only to an extent. Microsoft is well-known for being slow on the uptake on implementing C/C++ standards, whereas projects like Clang or GCC are pretty quick at implementing them. You'll get the latest standards with MSVC eventually; it might take a lot longer though. You can use Clang on Windows, but I'm not sure how well that works. GCC is not officially supported on Windows at all.

"On two occasions I have been asked [by members of Parliament!]: 'Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out ?' I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question."    — Charles Babbage.
My Github

2020-03-18 02:04:19

So what is officially supported, then?

2020-03-18 02:05:17

@5
Everyone I've worked with professionally would disagree with you on that. There is actually a higher level meta-rule that I sort of use for this, but trying to articulate when I choose to and when I choose not to use braces is well beyond the scope of this thread and not actually as simple is "is a single line".  Suffice it to say that in typical C code, you're looking at half the code if you don't have the braces in many cases, especially for mathematical algorithms with absurd levels of nesting, or when forwarding errors.

The proper fix is -Wmisleading-indentation.  Typically that'll get brought in via -Wall -Werror which you'd be using at scale anyway, though admittedly -Wall is probably too many warnings for someone new to C to deal with.  I believe that option is newer than the article you linked (indeed: the article you linked is likely one of the reasons it got added).  I'd suggest looking at it, but the short explanation is that that Apple code wouldn't compile under the typical -Wall -Werror setup that you use at scale anymore.

My Blog
Twitter: @ajhicks1992

2020-03-18 02:21:20

@7 @9
Clang on Windows is officially supported by Microsoft, with support in Visual Studio for building with it, and Synthizer has taken the stance that you will need to use Clang or nothing.  If you install Clang with the Clang Windows installer and install CMake with the CMake Windows installer, it works out of the box.  In detail, what you need to do is:

1. Install VS 2019.  This gets you the developer command prompts.
2. Install Clang. This gets you a version that's not buried in the VS internals somewhere.  VS has one, but it's only usable by VS because reasons.
3. Install CMake.  This gets you a version newer than the version that comes with VS, which you need because more reasons.

All of those are doable with normal Windows installers from the official sites, and default configurations should work save that you'll want to make sure to check the C++ workloads in the VS installer.

Then you make a basic CMakeLists.txt, which I will leave as an exercise to the reader.  Open X64 native tools command prompt for VS 2019 (which you will always need to do before programming), then run the following, once, from your project's directory:

mkdir build
cd build
cmake -G Ninja -DCMAKE_C_COMPILER="c:/program files/llvm/bin/clang-cl.exe" -DCMAKE_CXX_COMPILER="c:/program files/llvm/bin/clang-cl.exe" ..

After that you can just cd into the build directory and type ninja.  If you don't have Ninja you might have to install it separately, but mine came with VS.

If you start trying to add Clang compiler options and they don't work, revisit this topic or something and ask me how to use clang.exe instead of clang-cl.exe and I'll dig up the flag you need to make clang.exe work here.  Clang-cl.exe is a compatibility layer that exists for archaic reasons, and I need to dig around in Synthizer's CMakeLists.txt to find the one you need to do without it.

Among other things, Clang on Windows is officially required by Chromium, if I recall. So it's definitely stable.


And specifically for @!7, GCC doesn't have first class windows support, and is very, very hard to build yourself for that environment.  However, if you want to play with it, install WSL and you can get into a fully working Linux environment from any command prompt by typing bash at any time.  This will let you get  all the Linux tools you want, at the cost of not compiling things for Windows and needing to run them from inside WSL (how this works is magic faerie secret sauce).

That said at your level, nothing is wrong with forgoing all of this and continuing to just use the VC++ compiler you're already using.  It's very real, and is used to build all of windows, it just tends to lag behind everyone else with respect to new language features.  But you're very far from using advanced C++ features where this comes up, so the cost of using something else is perhaps not worth it yet.

My Blog
Twitter: @ajhicks1992

2020-03-18 02:29:03

Also, just to be really clear: VC++ is official. VC++ actually does keep up these days.  Microsoft is very active at the standardization meetings, and is responsible for things like coroutines that are in the process of revolutionizing the way C++ people do networking.  They just have more tech debt than almost anyone else, and it takes them an additional 6 months to a year to be where Clang and GCC are today.  But unless you're using post-C++14 features, which almost no one on this forum even knows the meaning of, going out of your way for an alternative compiler isn't something that's required.  VC++'s compiler isn't going anywhere anytime soon.  There's a couple things they don't implement in C specifically, but they don't matter to new or even intermediate programmers and I have to look up what they are.

It was true in 2015 or so that VC++ lagged everyone by miles because Microsoft didn't care about keeping up with it, but that has rapidly changed and they're now doing releases every 2 months.  Synthizer isn't going clang because VC++'s compiler sucks, but rather because Clang has a specific language extension for doing mathematical code which I would have to go into great length to explain that's not part of any official standards which makes Synthizer an order of magnitude faster while also making the code simpler at the same time.

My Blog
Twitter: @ajhicks1992

2020-03-18 02:37:08 (edited by Ethin 2020-03-18 02:37:29)

@11, if you don't mind I'd appreciate it if you could PM me your reasons that you listed in that post. And, 12-13, thanks for the corrections of my information, my info can be a tad dated sometimes, though I try to keep up to date with things.

"On two occasions I have been asked [by members of Parliament!]: 'Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out ?' I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question."    — Charles Babbage.
My Github

2020-03-18 14:03:18

For setting things up, just for learning C and not for building production ready code with C, I'd recommend you to get Windows subsystem for linux if you are on windows 10. This will give you a fully featured Linux shell on your machine.

There it is super simple to install a compiler and build tool. I think most distributions come with gcc and make built in, but if not just try to use a command and if you dont have it then the terminal will tell you don't and it will even tell you the command for getting it. Here is examexample output of what I am talking about:
$ cmake

Command 'cmake' not found, but can be installed with:

sudo apt install cmake

Also added benefit of compiling on the linux platform is that most textbooks for C give examples of Unix specific stuff that you'd have to find windows alternatives for.

2020-03-18 15:59:04

@15
The magic commands are (iirc, maybe slightly wrong):

sudo apt-get update
sudo apt install cmake gcc g++ make
My Blog
Twitter: @ajhicks1992

2020-03-18 16:47:53

One quick thing that may pose a problem @1:
This may; because of how my class is set up, (I'm in a C class right now and to get credit we arreat all warnings as errors and adhere to the c99 standard), but there have been times where my code has failed to work correctly because I didn't initialize my variables, like in this case the variable guessed. Sometimes this causes odd behavior in your code to happen, so initializing all of your variables (whether to NULL or an arbitrn# constant value) *shd* help out with this.
I also don't know if that book dives into structs at all (I would abbsume so), but it's super helpful  to get an understanding of structs before going into pointers, as things can get really confusing really quickly in certain situations.

A winner is you!
—Urban Champion

2020-03-18 18:19:29

@17
If you don't initialize variables in C, they get set to what is effectively a random value instead of zero.  This isn't to do with your setup.  C doesn't initialize them for you because efficiency so it just gets backed by some random Ram somewhere and whatever happened to be there before is the value.

My Blog
Twitter: @ajhicks1992

2020-03-18 18:36:32

@18:
Ahh, makes sense. Now I ,fully understand why that's sometimes a trouble-spot for me.

A winner is you!
—Urban Champion

2020-03-18 20:17:56

@19
Useful to you because you're ahead by enough. If you're using GCC or Clang, add -Wall -Werror to your command line and it will help you find these issues.  Skimming the GCC docs, you need to be compiling with optimization on to get warnings about uninitialized variables being used, but it nonetheless can.

I don't point new-enough people at -Wall because you spend more time learning what the warnings mean than coding.  They can be quite cryptic.

My Blog
Twitter: @ajhicks1992

2020-03-18 20:38:53

Always initialize variables. In some industries, like MISRA, failure to do this means failing the tests. So I'd just do it all the time.

"On two occasions I have been asked [by members of Parliament!]: 'Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out ?' I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question."    — Charles Babbage.
My Github

2020-03-18 21:07:27

Lol, my class didn't ever not use -Wall -Werror, so we had to basically figure out the errors as we went. This was after taking a Java course, so we at least knew basic concepts that could apply.

A winner is you!
—Urban Champion

2020-03-18 22:20:03 (edited by amerikranian 2020-03-18 22:24:45)

Hi.
I just got back home and had time to sit down and try to fix my code. What posts two and 3 suggested, changing scanf, worked, to some extent. The program still looped endlessly, however.
To get around this, I edited my code in the do while to look like so:

        if (!scanf("%d", &guessed)) {
            getchar();
            printf("Invalid guess!\n");
            continue;
        }

I'm guessing that the method fails to read the new line character should it happen to fail.
Just wanted to pop in here if anybody stumbles onto this in the future, tries the solutions, and is still confused as to why the program is not working as it should.
@15, doing a bit of googling yields multiple distros of linux. Any particular you'd recommend to install for this?

2020-03-18 23:10:05 (edited by Mitch 2020-03-18 23:19:34)

I originally thought that there wasn't a check to see if you ran out of guesses. My suggestion would be to change the do while to a for loop (if that wasn't suggested already), because you know how many times (or guesses) that you have.  In this case, you wouldn't even need a guesses variable as you could do something like this:

/* looping through the guesses */
for (int i = 5; i > 0; i——) {
  / * insert your scanf here */
}
If the guess is correct, print out that it was the correct answer and return 1, else after the entire for loop, return 0 and state that no guesses matched.

A winner is you!
—Urban Champion

2020-03-18 23:14:16

No... The amount of guesses does get checked. It's in the do while loop. You are correct in that I do not tell the user they've ran out of guesses, though.