student@ubuntu:~$
c-foundations Lesson 4 14 min read

Variables, printf & scanf

The types, formatted I/O, strings, and comparisons you need to finish Lab 1

Based on content from Dr. Stu Steiner, Eastern Washington University.

Reading: Hanly & Koffman: §2.2 (pp. 53–58), §2.5 (pp. 72–86), §2.6 (pp. 87–90)

In a nutshell

In CSCD 210, you typed Scanner and System.out.println without thinking about how they worked. In C, you write scanf and printf directly, and the small set of traps that come with them is what separates “the program compiles” from “the program runs correctly.” This lesson covers every type Lab 1 uses, every scanf pattern Lab 1 uses, how strings work in C, and how to compare them without the == mistake.

Practice this topic: C Variables drill, or browse the practice gallery.

After this lesson, you will be able to:

  • Declare int, double, char, and fixed-size character arrays in C90
  • Read input with scanf for every type Lab 1 uses, and explain when & is needed and when it is not
  • Print formatted output with printf, including doubles to a given number of decimal places
  • Compare C strings correctly with strcmp and explain why == does not work
  • Avoid the integer-division trap, the %f vs %lf trap, and the “char after int” whitespace trap
  • Name the CWE behind each of those traps so the Lab 1 reflection looks like review

Quick reference for Lab 1

Use Ctrl-F to find what you need.

Goal C code Notes
Declare an integer int age; top of block in C90
Declare a floating-point value double gpa; default to double, not float
Declare a single character char standing;  
Declare a string char name[50]; size required; no separate String type
Prompt (no newline) printf("Enter your age: "); printf never adds \n
Print text and a newline printf("Name: %s\n", name); %s means string
Print an int printf("Age: %d\n", age); %d or %i
Print a double, two decimals printf("GPA: %.2f\n", gpa); %.2f means two places
Print a single char printf("Standing: %c\n", standing); %c means one character
Read an int scanf("%d", &age); & required
Read a double scanf("%lf", &gpa); %lf in scanf; & required
Read a word scanf("%s", name); no &; array name is already an address
Read one char after a number scanf(" %c", &standing); leading space skips whitespace
Compare strings strcmp(name, "Alice") == 0 needs #include <string.h>
String length strlen(name) returns size_t; use %zu or cast to (int)
C90 for loop int i;
for (i = 1; i <= 5; i++) {
    /* body */
}
counter declared at the top of the block
Block comment /* ... */ only form legal under -std=c90

Coming from CSCD 210

You already know declarations, primitive types, formatted output, reading input, if/else, for loops, and calling methods. In Java those were System.out.printf, Scanner.nextInt(), and friends. The concepts map over directly; the syntax is new, and three traps are specific to C: %lf (not %f) for double in scanf, no & on arrays, and a leading space before %c after reading a number. The rest of this page walks through each one.


Declaring data

Types

C type Typical size What it holds printf scanf
int 4 bytes whole numbers, signed %d (or %i) %d
double 8 bytes floating-point, default %f or %.2f %lf
float 4 bytes smaller floating-point, rarely useful %f %f
char 1 byte a single character ('A') %c %c
short 2 bytes smaller whole number %hd %hd
long 8 bytes larger whole number %ld %ld
char name[N] N bytes fixed-size string buffer %s %s

Use double, not float, for the same reason CSCD 210 did: float trades precision for nothing you need.

%d and %i both mean “decimal int” in printf, so pick whichever you prefer there. In scanf, stick to %d: %i accepts hex and octal prefixes, which is not usually what you want.

Declarations at the top (C90)

In C90, every declaration must come before the first statement of its enclosing block.

int main(void)
{
    /* declarations first */
    int    age;
    double gpa;
    char   standing;
    char   name[50];
    int    i;              /* for loop counter lives here too */

    /* statements after */
    printf("Enter your age: ");
    scanf("%d", &age);
    /* ... */

    return 0;
}

Mixing declarations and statements, including for (int i = 0; ...) inline, is a C99 feature. Lab 1 is graded with -std=c90 -pedantic and will refuse it.

Uninitialized variables hold whatever was there

int x;                /* reserves 4 bytes but does not set them to 0 */
printf("%d\n", x);    /* prints whatever was in that memory. Could be 0, could be 32761. */

Java zero-initialized your fields (and refused to compile if you read a local before assigning to it). C does neither. The fix is to initialize at declaration (int x = 0;) or to write to the variable through an assignment or a successful scanf before reading it. -Wall -Wextra catches some of these at compile time; the debugger catches the rest.

Arithmetic traps

Integer division truncates toward zero. 5 / 2 is 2, not 2.5. -7 / 2 is -3, not -4.

int    a = 7;
int    b = 2;

printf("%d\n", a / b);           /* 2 */
printf("%.2f\n", (double)a / b); /* 3.50 */
printf("%.2f\n", a * 5.0 / 4.0); /* full precision; classic Lab 1 pattern */
printf("%.2f\n", a * (5 / 4));   /* 7.00, because 5/4 evaluates to 1 first */

The last line is the classic Lab 1 trap. For “GPA on 5.0 scale,” write gpa * 5.0 / 4.0. Writing gpa * (5 / 4) silently gives gpa * 1.

Zero is false. Every other number is true. C90 has no separate boolean type. if tests an integer; zero counts as false, any non-zero value as true.

int x = -7;
if (x) printf("yes\n");         /* prints. -7 is non-zero, so true. */
if (0) printf("never\n");       /* never prints. */

Null pointers and 0.0 also count as false in any truth test. In C99, <stdbool.h> defines bool, true, false, but underneath they are still integer 1 and 0.

= assigns, == compares. Confusing them compiles: = is a legal expression in C. if (age = 18) assigns 18 to age, evaluates to 18, which is non-zero, which is true. The if body always runs. This is CWE-481; -Wall warns on it unless you wrap the assignment in double parens.

The four meanings of &

The ampersand is overloaded. You meet all four this quarter; Lab 1 uses the first and the third.

Usage Meaning Example Seen in
Unary &x Address-of scanf("%d", &age) this lesson
Binary a & b Bitwise AND flags & 0x01 Operators
Binary a && b Logical AND age >= 18 && age <= 65 this lesson, below
int& (declaration) C++ reference not used in C never in this course

Reading and writing

printf

printf writes a format string to standard output. Every % specifier gets replaced by the matching argument. printf never adds a newline; you put \n in the format string yourself.

int    age     = 20;
double gpa     = 3.72;
char   initial = 'J';

printf("Age: %d\n", age);           /* Age: 20     */
printf("GPA: %.2f\n", gpa);          /* GPA: 3.72   */
printf("Initial: %c\n", initial);    /* Initial: J  */
printf("Name: %s\n", "Alice");       /* Name: Alice */
printf("%d and %.2f\n", age, gpa);   /* 20 and 3.72 */
printf("50%%\n");                    /* 50% (escape literal %) */

Sidebar: diagnostics on stderr. There are two output streams: stdout for normal output and stderr for error messages. Use fprintf(stderr, "...") for diagnostics so they do not pollute output pipelines:

if (meter_end < meter_begin) {
    fprintf(stderr, "warning: meter_end < meter_begin\n");
}

Lab 1 does not require stderr, but Quiz 1 asks about it.

scanf: the four patterns Lab 1 needs

int    age;
double gpa;
char   standing;
char   name[50];

scanf("%d", &age);          /* 1. int: & required */
scanf("%lf", &gpa);         /* 2. double: %lf in scanf, & required */
scanf("%s", name);          /* 3. word: no & (array is already an address) */
scanf(" %c", &standing);    /* 4. char after a number: LEADING SPACE */

Why %d and %lf need &. C passes arguments by value. To modify your variable, scanf needs the variable’s address, and &age is how you ask for it. For the full machine-level story of what the address actually is, see How & and pointers actually work.

Why scanf("%s", name) does not use &. The name of an array in C is automatically a pointer to its first element (the deep-dive page calls this “array-to-pointer decay”). name already is an address; &name is a different type and will warn.

Why " %c" needs the leading space. Every scanf conversion except %c silently skips leading whitespace. %c does not. After scanf("%d", &age) reads 42, the \n the user pressed is still in the input buffer. A following scanf("%c", &standing) would read that newline into standing. The one-character fix:

scanf("%d", &age);
scanf(" %c", &standing);    /* the space says "skip whitespace, then read one char" */

%f vs %lf in scanf. float is 4 bytes; double is 8. In printf this rarely matters because float is promoted to double before a variadic call. In scanf, no promotion happens; the specifier must match the pointer’s type exactly. Writing scanf("%f", &my_double) stores 4 bytes into an 8-byte slot and corrupts the value with zero warning. Always %lf for double in scanf.

Check your understanding (what are the bugs?)

A student writes:

double gpa;
char   standing;
int    age;

printf("Enter your age: ");
scanf("%d", &age);

printf("Enter your GPA: ");
scanf("%f", &gpa);

printf("Enter your class standing: ");
scanf("%c", &standing);

The program compiles with no warnings (the student turned them off to “clean up”), but at runtime the class-standing prompt never waits for input and standing ends up as '\n'. The GPA also looks wrong when printed later. What are the three distinct bugs?

Reveal answer
  1. %f should be %lf. gpa is a double.
  2. %c should be " %c" (leading space). The newline left over from scanf("%f", ...) gets read into standing without it.
  3. Warnings should not be turned off. -Wall -Wextra would have flagged the %f mismatch. Zero-warnings is the Lab 1 grading target.

Strings in C are character arrays

There is no String class in C. A “string” is an array of char ending with a null byte '\0'. Every string function scans forward until it hits that zero byte.

char name[50];                  /* reserve 50 bytes; 49 usable + null */
scanf("%s", name);              /* reads one word; writes '\0' at the end */
printf("Hello, %s!\n", name);   /* prints until '\0' */

Key points for Lab 1:

  • Size is required at the declaration. char name[50] reserves 50 bytes, so 49 usable characters plus one for the terminator.
  • scanf("%s", ...) stops at whitespace. It reads one “word.” Lab 1’s inputs are single words.
  • No bounds checking by default. If the user types more characters than the array can hold, scanf("%s", name) keeps writing past the end. That is CWE-120. The width-capped form scanf("%49s", name) stops after 49 characters and leaves room for '\0'. Lab 1 does not require the cap, but the reflection asks you to explain it.
  • Never use gets. gets(buf) has no length argument; it was removed from C11 entirely. Use fgets(buf, sizeof(buf), stdin) for whole-line reads.

For the historical reason C strings look this way (BCPL heritage, PDP-11 register pressure), see Why C strings are null-terminated.

Comparing strings: use strcmp, not ==

Because a string is an address, str1 == str2 compares the two addresses, not the contents. Two arrays holding the same text compare as not equal. That is CWE-597.

The correct tool is strcmp from <string.h>:

#include <string.h>

if (strcmp(lang, "C") == 0) {
    printf("Excellent choice.\n");
} else if (strcmp(lang, "Java") == 0) {
    printf("We are upgrading you to C this quarter.\n");
} else {
    printf("Interesting choice.\n");
}

strcmp returns 0 when equal, a negative value when the first sorts before the second, and a positive value when it sorts after. Zero means equal: memorize it. The idiom is strcmp(a, b) == 0 for equality. Never write strcmp(a, b) == 1: the standard promises only the sign, not the specific value.

strlen(s) returns the number of characters before '\0'. Its return type is size_t; print it with %zu or cast to (int).


Deciding and looping

if/else and for work the same way they did in Java. Three small differences in C90:

if / else if / else

if (age > 18) {
    printf("You are an adult.\n");
} else if (age == 18) {
    printf("You just became an adult.\n");
} else {
    printf("You are a minor.\n");
}

Braces are optional for a single-statement body. Always use them anyway. Missing braces on a multi-statement body is CWE-483; you will see the full Apple “goto fail” story in If/Else, Switch & Loops and in the CVE deep-dive.

Logical OR for case-insensitive comparisons

if (standing == 'F' || standing == 'f') {
    printf("Standing: Freshman\n");
} else if (standing == 'O' || standing == 'o') {
    printf("Standing: Sophomore\n");
}

|| is logical OR. | (single pipe) is bitwise OR: different operation, different result. Same distinction with && vs &.

for loop in C90 (counter at the top)

int i;     /* declared at the top of main with the other variables */

for (i = 1; i <= 5; i++) {
    printf("In %d year(s) you will be %d\n", i, age + i);
}

In C99 you can write for (int i = 1; ...) and declare the counter inside the loop. Lab 1 compiles with -std=c90 -pedantic, so you cannot.


Lab 1 pitfalls and security

Common mistakes

You wrote What C does Fix
scanf("%f", &gpa) for double writes 4 bytes into an 8-byte slot scanf("%lf", &gpa)
scanf("%s", &name) wrong type; warning scanf("%s", name)
scanf("%c", &standing) after a number reads the leftover '\n' scanf(" %c", &standing)
printf("Name: %s", name) no newline printf("Name: %s\n", name)
// my comment under -std=c90 -pedantic error /* my comment */
int months = age * 12; mid-block declaration mixed with statements declare at top
for (int i = 1; ...) C99 int i; at top, for (i = 1; ...)
if (lang == "C") compares addresses, not contents strcmp(lang, "C") == 0
strcmp(a, b) == 1 non-portable strcmp(a, b) == 0 for equality
gpa * (5 / 4) 5/4 is 1 gpa * 5.0 / 4.0
strcmp without header implicit declaration warning #include <string.h>
if (age = 18) assigns 18, always true if (age == 18)

The five CWEs behind the reflection questions

Each trap has a matching entry in the Common Weakness Enumeration. You do not need to memorize the numbers; the reflection walks you through each one.

Reflection Q CWE What went wrong Where above
Q1 CWE-481 if (x = 1) instead of if (x == 1) = assigns, == compares”
Q2 CWE-597 str1 == str2 instead of strcmp “Comparing strings”
Q3 CWE-483 No braces on a multi-line if body “Always use them anyway”
Q4 CWE-134 printf(user_input) as format string see deep-dive
Q5 CWE-120 scanf("%s", buf) with no length cap “No bounds checking by default”

For each CWE’s concrete exploit mechanics and the historic CVE it produced (Morris Worm, Baron Samedit, Apple goto fail, the WhatsApp GIF overflow), see The Lab 1 CWEs, with exploit mechanics.

Check your understanding (multiple correct)

Select every statement that is true about C90 I/O.
Ascanf("%d", &age) needs & because C passes arguments by value, and scanf needs the address of age to write back.
Bscanf("%s", &name) is the correct way to read a word into char name[50].
CAfter scanf("%d", &n), the user's '\n' remains in the input buffer and can trip up a following %c read.
Dstrcmp(a, b) == 0 is the correct equality test; a == b compares memory addresses, not contents.
Efgets(buf, 100, stdin) reads up to 100 characters into buf before stopping.
FIn printf, %f works for both float and double, but in scanf, %f is for float only and a double needs %lf.
Correct: A, C, D, F.
  • B is wrong: name is already an address; &name is a different type and will warn.
  • E is wrong: fgets(buf, N, stream) reads at most N − 1 characters, plus the null terminator. With N = 100 you get 99 readable characters.

What comes next

You can now declare, read, print, and compare every value Lab 1 asks for. Operators and Control Flow go deeper. You already have enough to write the whole Lab 1 program. Open lab1.c and translate Lab1.java section by section. Want to drill this page first? C Variables or the practice gallery.