1
\$\begingroup\$

I am a beginner. I have really struggled with fgets() when using the function twice. I am on Windows with VS Code and C++ extension. My test program is working, but it is IMHO lengthy and complicated for such a simple task. Furthermore, the use of goto is bad practice, I have read. Is there a simpler solution?

#include <iostream>
#include <string.h>
using namespace std;
#define max 5

void killNL(char *str) {            
    int i = 0;                      
    while (*(str+i) != EOF){

            if (*(str+i) == '\n'){
            *(str+i) = '\0';
            *(str+i+1) = EOF;
            }
            i++;
    }
}

int main()
{
    size_t len = 0;
    char string[5] = {0};
    int c = 0;

    begin:
    printf("Enter string: ");
    fgets(string, max, stdin);
    killNL(string);                 //Is used only for the len count when length is 1 less
                                    //than max. It depends on the space that is left for
                                    //'\n' AND '\0' which raises the count to when the 
                                    //(getchar()) != gets activated. Which needs a leftover
                                    //'\n' in the buffer to not go into endless loop.

    len = strlen(string);

    for (int i = 0 ; i <= len ; i++) {
        if (string[i] == '\n'){ printf("\\n \n");}
        else if (string[i] == '\0'){ printf("\\0 \n");}
        else { printf("%c \n", string[i]);}
    }

    askAgain:
    if (len >= max - 1) {
        while (c = (getchar()) != '\n' && c != EOF) {}
    }

    printf("Continue? 'y'/'n' :");
    fgets(string , 3, stdin);

    if (*(string) == 'y'){
        goto begin;
    } else if (*(string) == 'n') {
        goto end;
    } else {
        printf("\nWrong answer!\n");
        goto askAgain;
    }
    
    end:
    printf("END!\n");

    return 0;
} 

I have been searching here, on Youtube and then in a book where I found the killNL function, which finally did make it work. This question has many variants on Stack Overflow, with a combination with sscanf, with reading a file and more. But not this variant, which is why I am posting it despite the fact that it is an old subject.

\$\endgroup\$
14
  • 7
    \$\begingroup\$ This is C++ code, using C-like functions that could be C++. But in function killNL() you are looking for EOF but that isn't a character that should appear in a string: it is a flag which is returned by file reading functions. \$\endgroup\$ Commented Nov 6, 2023 at 20:49
  • 5
    \$\begingroup\$ If the killNL() function is supposed to remove the trailing newline retained by fgets() then please see this easy one-line solution: Removing trailing newline character from fgets() input \$\endgroup\$ Commented Nov 6, 2023 at 21:24
  • 5
    \$\begingroup\$ Using while (*(str+i) != EOF){ is problematic on two counts: (1) it is better to use str[i] than *(str+i), and (2) comparing characters with EOF is very dubious (you should be looking for end of string, EOS, which is a null byte, '\0', not EOF (which is a negative integer value, usually -1). If the base char type is signed, you might stop on a valid character (in some code sets, that would be ÿ — small Latin letter Y with diaeresis in Unicode speak). If the base char type is unsigned, you'll never match. In both cases, as written, the code is more likely to crash than behave. \$\endgroup\$ Commented Nov 6, 2023 at 22:11
  • 2
    \$\begingroup\$ while (c = (getchar()) != '\n' && c != EOF) is definitely wrong. The second ( is misplaced. It should be while ((c = getchar()) != '\n' && c != EOF) (do you see the difference?). \$\endgroup\$
    – vnp
    Commented Nov 7, 2023 at 0:41
  • 5
    \$\begingroup\$ If you really found the killNL() function in a book as you say, please destroy that book so that it can't harm any more people. It makes absolutely no sense to compare or assign char with EOF, which is an int value distinct from any valid char. (Not to mention its unidiomatic practice of spelling out the indexing arithmetic in longhand, rather than using the more readily understood [] operator) \$\endgroup\$ Commented Nov 7, 2023 at 7:50

2 Answers 2

12
\$\begingroup\$

Well, for starters, you really want to decide whether you are coding in C or C++. You #include <iostream>, but make no use of any iostream functions (presumably you thought about using std::cout or the like). You also specify using namespace std;, but that is not valid in C. Rule: only include necessary headers for the functions used in your code.

We will presume you are wanting to write code in C given your choice of input and output functions. If not, drop a comment and I'm happy to help with C++ as well. Working though your code:

  • max 5 -- Don't SKIMP on buffer size. While you would use std::string if writing in C++, when declaring an array to hold input in C, don't skimp. While fgets() will only attempt to write size number of characters (minus 1 to provide room for the nul-terminating character) your purpose in using fgets(), among others, is to read a complete line of input at a time without leaving extraneous characters in the input stream. You do that by providing a sufficiently sized buffer.
  • When taking input, you generally loop-continually and then break your read-loop if the input fails stream error or manual EOF (generated by the user pressing Ctrl + d (or Ctrl + z on Windows)), or successful input is received, or one of your exit conditions is satisfied (e.g. the user pressing Enter alone for the string indicating they are done with input)
  • In C, to trim the '\n' from the end of the string read by fgets() (e.g. fgets (string, MAXC, stdin)), then you simply call string[strcspn (string, "\n")] = 0; effectively overwriting the '\n' with the nul-terminating character '\0' (which is just ASCII 0)
  • Declare your variables in the scope where they are needed, and
  • There is absolutely nothing wrong with goto used properly. It is the only way to break nested loops in a single expression, etc..

With that as a backdrop, you can simplify and clean up your code similar to the following. Many of the choices are up to you, but this is one general way to approach

  • reading input,
  • checking if the input is valid,
  • checking if the user generated an EOF,
  • checking if the user is done with input,
  • checking if the input matches a string you want, and finally
  • looping again if the input isn't what is needed.

(a rough interpretation of what it looked like you were wanting to do)

The code (with additional comments) can be:

// #include <iostream>  /* only include needed headers for functions used */
#include <stdio.h>      /* or <cstdio> for modern C++ */
#include <string.h>     /* or <cstring> for modern C++ */

// using namespace std; /* using nothing from std:: namespace */

#define MAXC 1024       /* don't SKIMP on buffer size, UPPERCASE defines */
#define ANS "guess"

int main()
{
    char string[MAXC] = {0};    /* declare string */
    
    /* read-loop continually, break on condition of your choice */
    while (1) {
        fputs ("\nEnter string: ", stdout); /* no conversion, fputs is fine */
        
        /* validate EVERY input based on return of read function */
        if (!fgets (string, MAXC, stdin)) {
            puts ("(user generated manual EOF)");
            return 0;
        }
        
        /* trim '\n' from string */
        string[strcspn (string, "\n")] = 0;
        
        /* exit if empty-string (user just pressed [Enter], or any condition) */
        if (*string == '\0') {
            puts ("(all done)");
            break;
        }
        
        /* check if correct answer ANS given */
        if (strcmp (string, ANS) == 0) {
            puts ("(correct!)");
            break;
        }
        
        puts ("(answer didn't match)");   /* otherwise, loop again */
    }
    
    puts ("(That's All Folks!)");
}

Example Use/Output

$ ./bin/read-loop

Enter string: my dog has fleas
(answer didn't match)

Enter string: why?
(answer didn't match)

Enter string: what if I guess?
(answer didn't match)

Enter string: guess
(correct!)
(That's All Folks!)

Or canceling with a Ctrl + d:

$ ./bin/read-loop

Enter string: (user generated manual EOF)

Or using the exit-condition of the user just pressing Enter alone at the prompt indicating they are done guessing:

$ ./bin/read-loop

Enter string: next we will just press Enter when prompted, to quit input
(answer didn't match)

Enter string:
(all done)
(That's All Folks!)

You can add (or remove) as many of the exit conditions for the read-loop as you like. The key take-aways are:

  • loop continually until the user provides the required input,
  • always condition exiting your read-loop on the return of your read-function,
  • validate EVERY user-input,
  • provide the needed exit conditions to control exit from your read-loop, and
  • handle any errors that occur as needed.

Let me know if this is what you needed. If not, I'm happy to help further.

\$\endgroup\$
4
  • \$\begingroup\$ Lots of unneeded zeroing with char string[MAXC] = {0};. OK for learner code, I guess. \$\endgroup\$ Commented Nov 7, 2023 at 7:26
  • 1
    \$\begingroup\$ Good call. More readable. (and preservers the intended *string access to prompt the OP to look further into array-pointer conversion). \$\endgroup\$ Commented Nov 7, 2023 at 7:38
  • \$\begingroup\$ @DavidC.Rankin I am really greatful with your answer. Thank you. It works very well on my maschine and is very simple, only one variable! No functions. Very informative comments. it is clear to distinguish an amateur from a professional. The program I wrote was for practise and luckily a site like this exists. I tried to reduce the MAXC to 6, but then the program skips the next fgets because of a hidden '\n' in the stdin. I will test the getchar() != ' \n' && EOF function to further test. Best wishes. \$\endgroup\$
    – mortenlund
    Commented Nov 7, 2023 at 19:42
  • 1
    \$\begingroup\$ Glad to help! Rewrite it 10 different ways. That is great learning. What I always found helpful when working with input and small buffers is to write out blocks for each byte in the buffer (on a real piece of paper). As you fill the buffer using getchar() write that character in the correct block on your paper (don't forget the '\n' and '\0' (equivalent to plain 0)). That teaches you to track how you are using the storage you have available. The builds one of the most important skills in using C correctly. Protect your array/storage bounds! Good luck with your coding! \$\endgroup\$ Commented Nov 7, 2023 at 23:07
0
\$\begingroup\$

This is DavidC.Rankin's code with modification for overflow.


    #include <stdio.h>     
    #include <string.h>     
    
    
    
    #define MAXC 6      //test with minimum storage
    #define ANS "guess"
    
    int c = 8;
    int d = 9;
    int e = 0;
    int len = 0;
    
    
    int main()
    {
        char string[MAXC] = {0};    
        
        /* read-loop continually, break on condition of your choice */
        while (1) {
            fputs ("\nEnter string: ", stdout); 
            
            /* validate EVERY input based on return of read function */
            if (c = !fgets (string, MAXC, stdin)) {     //c = 1 when EOF is entered, else 0.
                puts ("(user generated manual EOF)");   //ctrl + 'z' (manual EOF)
                return 0;
            }
            
            /* trim '\n' from string */
            string[strcspn (string, "\n")] = 0;     //compares string with \n and counts position.
    
            len = strlen(string);                   //needed when input is overflowing
            
            if (len >= (MAXC - 1)) {                //ditto
                 while ((d = getchar()) != '\n' && d != '\0') {}
            }
            
            /* exit if empty-string (user just pressed [Enter], or any condition) */
            if (*string == '\0') {
                puts ("(all done)");
                break;
            }
            
            /* check if correct answer ANS given */
            if (d = strcmp (string, ANS) == 0) {
                puts ("(correct!)");
                break;
            }
            
            puts ("(answer didn't match)");   /* otherwise, loop again */
        }
        
        puts ("(That's All Folks!)");
    
        return 0;
    }

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Good job. You can trim the \n AND save the string length all at the same time. See the man-page for strcspn() man 3 strspn. Now just do string[(len = strcspn (string, "\n"))] = 0; Now len holds the string length of string after the '\n' has been trimmed :) (note: the (...) around the assignment inside the string[(...)]... for proper operator-precedence) before len is used as an index. \$\endgroup\$ Commented Nov 7, 2023 at 23:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.