Your Existing Issues
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.
- for
gradeType
you need an array of char
not an array of pointers-to char
,
- good job using
fgets()
to read the input,
- there is no need to loop counting spaces, you can simply increment
numValues
as you perform your conversions to int
,
- 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),
- 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,
- you can't use any input or conversion function correctly unless you validate the input or conversion succeeded or failed by checking the return,
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.