K&R C Exercise 2-2: Loop Without && or ||

Exercise 2-2. Write a loop equivalent to the for loop below without using && or ||:

for (i = 0; i < lim - 1 && (c = getchar()) != '\n' && c != EOF; ++i)
    s[i] = c;

Understanding the Problem

The && operator in C is a short-circuit operator: if the left operand evaluates to false (zero), the right operand is never evaluated at all. This matters enormously in the loop above because the second condition, (c = getchar()), is not a pure test — it reads a character from standard input as a side effect.

The three conditions in the original for loop are evaluated strictly left to right, and evaluation stops the moment one fails:

  1. i < lim - 1 — if the buffer is full, stop immediately. Do not call getchar().
  2. (c = getchar()) != '\n' — only if (1) passed: read one character, stop if it is a newline.
  3. c != EOF — only if (2) passed: stop if end-of-file was reached.

To replicate this without &&, you need the same sequence: check each condition in order, and exit the loop immediately when one fails — using break. The trap to avoid is calling getchar() before checking the buffer limit; doing so would consume a character from the input stream that should never have been read.

Solution — Using break (primary)

/* Compile: gcc -ansi -Wall exercise2-2.c -o exercise2-2 */
#include <stdio.h>
#define MAXLINE 1000

int getline2(char s[], int lim);

int main(void)
{
    char line[MAXLINE];
    int len;
    while ((len = getline2(line, MAXLINE)) > 0)
        printf("%s", line);
    return 0;
}

int getline2(char s[], int lim)
{
    int c, i;
    i = 0;
    while (1) {
        if (i >= lim - 1)   /* buffer full: stop, do NOT call getchar() */
            break;
        c = getchar();       /* only reached when there is still room */
        if (c == EOF)
            break;
        if (c == '\n')
            break;
        s[i++] = c;
    }
    if (c == '\n')           /* preserve the newline in the stored line */
        s[i++] = '\n';
    s[i] = '\0';
    return i;
}

Each if … break pair corresponds exactly to one condition in the original for. Because break exits the loop the moment it fires, later checks are skipped — the same effect as short-circuit evaluation. In particular, getchar() is only ever called after confirming that the buffer has room, exactly mirroring the original.

Alternative Solution — Using a Flag Variable

Another approach replaces the short-circuit with an explicit done flag. It is more verbose, but makes the control flow visible without any break statements:

int getline2_flag(char s[], int lim)
{
    int c = 0, i = 0, done = 0;
    while (done == 0) {
        if (i >= lim - 1) {
            done = 1;             /* buffer full */
        } else {
            c = getchar();
            if (c == EOF)
                done = 1;
            else if (c == '\n')
                done = 1;
            else
                s[i++] = c;
        }
    }
    if (c == '\n')
        s[i++] = '\n';
    s[i] = '\0';
    return i;
}

The flag version works correctly, but it requires an extra variable and more indentation. Notice that c is initialized to 0 here — this makes the final if (c == '\n') safe even if getchar() was never called (which happens when lim is 1). The break version shares the same subtle edge case; in practice it never occurs because lim is always MAXLINE (1000). The break approach is the cleaner, more idiomatic C solution.

Why the Order of Checks Matters

Consider a student who writes this instead:

/* WRONG — getchar() is called even when the buffer is full */
while (1) {
    c = getchar();
    if (i >= lim - 1) break;
    if (c == EOF)      break;
    if (c == '\n')     break;
    s[i++] = c;
}

This silently discards one character every time the buffer limit terminates the loop. The original for loop would not have called getchar() in that case — the short-circuit stopped evaluation at condition 1. Faithfully replicating short-circuit behaviour means preserving the order and the guards.

Compile and Run

gcc -ansi -Wall exercise2-2.c -o exercise2-2
./exercise2-2

The program reads lines from standard input and echoes them back. Type text and press Enter, or pipe input in:

printf "Hello, world!\nSecond line\n" | ./exercise2-2

Sample Output

$ printf "Hello, world!\nSecond line\n" | ./exercise2-2
Hello, world!
Second line

$ printf "abc" | ./exercise2-2
abc

The second example shows end-of-file without a trailing newline — getchar() returns EOF, the loop breaks, and the string is stored without appending '\n'.

What This Exercise Teaches

  • Short-circuit evaluation: && and || do not evaluate their right operand if the result is already determined by the left. This is a guarantee in C, not an optimisation hint.
  • Side effects in conditions: (c = getchar()) inside a condition both reads a character and produces a value — code that relies on short-circuit behaviour depends on the left operand being tested first.
  • Replicating short-circuit with break: The equivalence holds only if each guard is checked in the same order as the original conditions, and later guards are skipped when an earlier one fires.
  • Trade-offs between break and flag variables: Both are correct; break is more concise and direct, while a flag makes state transitions explicit — understanding both prepares you for real-world code review.

Set Up Your C Environment

To compile and run this solution, you need GCC installed. If you have not set up C on your machine yet:

← Exercise 2-1  | 
Chapter 2 Solutions  | 
Exercise 2-3 →

Book:

The C Programming Language, 2nd Ed — Kernighan & Ritchie

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>