1

I'm working on an assignment for my programming course, and in general, the user has to input a grade type (e.g. Exam, Quiz, Assignment, etc...) and a series of integer numbers. An example of an input would be "Quiz 55 53 60 70" (there can be up to 20 integers). I then have to store this information into a struct.

I'm able to store the grade type and the number of grades into the struct, but I'm having trouble storing the integer numbers into an array. Here's my code:

`

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct gradeData {
    int a_Nums[20];
    int numValues;
    const char* gradeType[20];
};

int main() {
    
    struct gradeData grades = {};
    char str[50];

    printf("Enter a student's grade type and grades (e.g. Quiz 41 20 90 81):\n");
    fgets(str, 50, stdin);

    // stores into numValues (total number of grades).
    for (int i = 0; i <= str[i]; i++) {
        if (str[i] == ' ') {
            grades.numValues ++;
        }
    }

    // stores into gradeType.
    sscanf_s(str, "%s", grades.gradeType, 20);

    // stores into a_Nums.
    char* ptr = str;
     
    for (int i = 0; i <= sizeof(grades.a_Nums); i++) {
        ptr = strchr(ptr + 1, ' ');
        sscanf_s(ptr + 1, "%d", grades.a_Nums[i]);
    }
}

`

I sought help from my professor and he sort of guided me into the right direction for storing the integer values into the array.

When I run my code in Visual Studio and input the string, it says "Debug Assertion Failed!"

pop
  • 19
  • 3
  • Your arguments for `scanf_s` are wrong: `grades.gradeType` this is an array of pointers, that decays to a pointer to the first pointer. You want to read a string. That requires a pointer to an array of characters. Change your struct member to `char gradeType[20];` Also `grades.a_Nums[i]` is wrong. This is an integer. `scanf_s` requires a pointer to an integer for be provided for format specifier `%d`. You need to pass `&grades.a_Nums[i]` – Gerhardh Nov 24 '22 at 04:17
  • 1
    You should turn on compiler warnings – CoffeeTableEspresso Nov 24 '22 at 04:21
  • `scanf` (or "`sscanf_s`") is not the ideal tool for this job; I would use [strtol](https://pubs.opengroup.org/onlinepubs/007904875/functions/strtol.html); easier and provides more details. – Neil Nov 24 '22 at 05:32
  • Please check out [this](https://stackoverflow.com/q/4513316/1025391) and [this](https://stackoverflow.com/q/7021725/1025391). – moooeeeep Nov 24 '22 at 08:11
  • `i <= str[i]` doesn't make sense. – Paul Hankin Nov 24 '22 at 08:33

1 Answers1

1

Your Existing Issues

  1. char str[50]; is far too short. Up to 20 grades as much as 3-digits each (for 100) with at least one space between the numbers -- that takes a bare minimum of 80 characters (including the nul-terminating character) PLUS storage for type and at least one additional whitespace. Rule: never SKIMP on buffer size. Users can and will type more than they should. Minimum of 1K buffers unless on embedded systems is recommended.
  2. for gradeType you need an array of char not an array of pointers-to char,
  3. good job using fgets() to read the input,
  4. there is no need to loop counting spaces, you can simply increment numValues as you perform your conversions to int,
  5. an empty brace-initializer is fine in C++, but not C. If you had full warnings enabled you would have learned "warning: ISO C forbids empty initializer braces". (either use a named-initializer or provide a initializer/value for each member),
  6. to enable warnings, for gcc use -Wall -Wextra -pedantic -Wshadow and consider -Werror to treat warnings as errors. Since you are attempting to use sscanf_s it is likely you are using cl.exe the compiler/linker for VS. There enabling /W3 is sufficient,
  7. you can't use any input or conversion function correctly unless you validate the input or conversion succeeded or failed by checking the return,
  8. for (int i = 0; i <= sizeof(grades.a_Nums); i++) invokes Undefined Behavior attempting to convert 80 values. sizeof(grades.a_Nums) is, e.g. 20 int times 4-bytes per int equals 80. If you do it that way, and presuming your loop counting spaces results in the correct number of values, your loop would be for (int i = 0; i < grades.numValues; i++).

Comments On Your Approach

While you can use sscanf() if you include the "%n" specifier to capture the number of characters consumed with each conversion, and add that value to the offset for your next call, you should really use strtol() to work through the string of integers converting to int. (that's what it was designed to do).

Further, all sscanf() can do it give you a succeed / fail diagnostic through the return value, leaving you to handle the accumulated offset for each successive call -- and the likelihood of a mistake along the way.

Compare with the prototype for strtol(), e.g.

  long int strtol(const char *nptr, char **endptr, int base);

Where nptr is a pointer to the beginning of the string containing numbers, endptr is the address of a pointer that strtol() will update for you to point 1-past the last digit converted. Think about that for a minute. On your first conversion, the number at the beginning of the string is converted and endptr is updated to 1-past the last digit in that number (already set up for your next call after you assign nptr = endptr;) And since the strtol() conversion ignores leading whitespace, this makes it extremely easy to step down a string converting all space separated integers one after the other.

Moreover, strtol() provides detailed error reporting of exactly what failed where allowing you to recover from minor variations in format.

The base parameter allows a conversion for any base between base-2 and base-36 (providing 0 as the base will automatically handle conversions to base-8, base-10 or base-16 depending on whether the number begins with a 0, no prefix, or 0X or 0x, respectively). Also note the return type is long, so you need to assign to a long and then validate your number is within the range of int.

Minimal Example Using strtol()

Putting the above together, you can write an example with the minimum required validation similar to what follows. Note, there are a large number of additional explanations provided inline in comments within the code, as well as identifying areas where you should provide additional checks or validations.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>

#define ARRSZ   20        /* if you need a constant, #define one (or more) */
#define BUFSZ 1024        /* don't skimp on buffer size */

typedef struct {          /* a typedef makes using a struct convenient, you */
    int a_Nums[ARRSZ];    /* don't need to type "struct" before gradeData */
    int numValues;
    char gradeType[ARRSZ];  /* array of 20 chars, not pointers, not const */
} gradeData;


/* simple function to print the contents of a gradeData struct */
void prngradeData (gradeData *g)
{
    if (!g->numValues || !*g->gradeType) {    /* no values or empty-string */
        return;
    }
    
    /* output type and prefix for grades */
    printf ("\ntype   : %s\ngrades :", g->gradeType);
    
    for (int i = 0; i < g->numValues; i++) {  /* output grades */
        printf (" %d", g->a_Nums[i]);
    }
    putchar ('\n');     /* tidy up with newline */
}


int main (void) {
    
    gradeData grades = { .a_Nums = {0} };     /* {} initializer is C++ */
    size_t nchars = 0;    /* numer of chars in gradeType */
    char str[BUFSZ],
         *p = str;        /* pointer to str */

    /* no conversions, fputs() is fine (see fputs() / fgets()) */
    fputs ("Enter a student's grade type and grades "
           "(e.g. Quiz 41 20 90 81):\n", stdout);
    /* validate EVERY input, handle Ctrl+d (manual EOF) */
    if (!fgets(str, sizeof str, stdin)) {
        puts ("(user canceled input)");
        return 0;
    }

    nchars = strcspn (str, " \t\n");  /* number of chars to 1st whitespace */
    /* if no chars or too many or space not found, invalid format */
    if (nchars == 1 || nchars >= ARRSZ || str[nchars] == '\n') {
        fputs ("error: invalid input format.\n", stderr);
        return 1;
    }

    memcpy (grades.gradeType, str, nchars);   /* store int gradeType */
    grades.gradeType[nchars] = 0;             /* nul-terminate string */
     
    p += nchars;    /* move pointer one beyond gradeType */
    
    while (grades.numValues < ARRSZ) {      /* loop while array not full */
        char *endptr = NULL;                /* endptr for strtol */
        long tmp = 0;                       /* temp long for conversion */
        
        errno = 0;                          /* reset errno */
        tmp = strtol (p, &endptr, 0);       /* attempt conversion */
        
        /* validate the conversion - minimum 3-conditions */
        if (p == endptr) {  /* check no digits converted */
            fprintf (stderr, "error: no digits converted: %s", p);
            break;
        }
        else if (errno) {   /* check error/overflow in conversion */
            perror ("strtol-grade");
            break;
        }
        else if (tmp < INT_MIN || INT_MAX < tmp) {  /* validate int */
            fputs ("error: conversion exceeds size of int.\n", stderr);
            break;
        }
        
        grades.a_Nums[grades.numValues++] = tmp;    /* assing to array */
        
        p = endptr;       /* update p to 1 past last digit converted */
        
        /* note: can scan forward in string to next +/- or digit,
         * to enhance check for end of string -- that's left to you.
         */
        
        if (!*p || *p == '\n') {  /* check if end of string */
            break;
        }
    }
    /* warn if array full and more grades remain */
    if (grades.numValues == ARRSZ && strpbrk (p, "+-0123456789")) {
        puts ("warning: array full and additional grades remain");
    }
    
    prngradeData (&grades);     /* output results */
}

(note: on Windows Ctrl + z is used to generate a manual EOF instead of Ctrl + d for Linux)

Example Use/Output

Your example:

$ ./bin/gradetype
Enter a student's grade type and grades (e.g. Quiz 41 20 90 81):
Quiz 55 53 60 70

type   : Quiz
grades : 55 53 60 70

Does it handle all 20 grades if provided?

$ ./bin/gradetype
Enter a student's grade type and grades (e.g. Quiz 41 20 90 81):
Daily 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

type   : Daily
grades : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

What if 21 are provided?

$ ./bin/gradetype
Enter a student's grade type and grades (e.g. Quiz 41 20 90 81):
Daily 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
warning: array full and additional grades remain

type   : Daily
grades : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

And invalid grades?

$ ./bin/gradetype
Enter a student's grade type and grades (e.g. Quiz 41 20 90 81):
Quiz 55 53 bananas 70
error: no digits converted:  bananas 70

type   : Quiz
grades : 55 53

There are many more things you can do, such as when faced with a no digits converted error, you can scan forward in the string to find the next (optional +/- followed by a) digit. You can then skip over the non-digit characters causing failure, etc... strtol() gives you that option with the detailed error reporting it provides.

Look things over and let me know if you have questions.

David C. Rankin
  • 81,885
  • 6
  • 58
  • 85