One thing that stands out to me is that he uses for loops quite a bit where, to me anyway, a while loop seems more natural.
That part is already discussed in chapter 3.5 Loops While and For on p. 56 of the very first edition of The C Programming Language:
We have already encountered the while and for loops. In
while (expression)
statement
the expression is evaluated. If it is non-zero, statement is executed and expression is re-evaluated. This cycle continues until expression becomes zero, at which point execution resumes after statement.
The for statement
for (expr1 ; expr2 ; expr3)
statement
is equivalent to
expr1;
while (expr2) {
statement
expr3;
}
Grammatically, the three components of a for are expressions. Most commonly, expr1 and expr3 are assignments or function calls and expr2 is a relational expression.
As to your question:
So here's the retrocomputing part: Plauger was writing in 1992. Is there a specific reason that compilers at the time did better with for loops rather than while loops, or is this just a personal preference of Plauger?
Yes, it's only a matter of style as K&R notes on p. 57 of their book with:
Whether to use while or for is largely a matter of taste.
For example, in
while ((c = getchar()) == ' ' || c == '\n' || c == '\t')
; /* skip white space characters */
there is no initialization or re-initialization, so the while seems most natural.
The for is clearly superior when there is a simple initialization and re-initialization(*1), since it keeps the loop control statements close together and visible at the top of the loop. This is most obvious in
for (i = 0; i < N; i++)
which is the C idiom for processing the first N elements of an array, the analog of the Fortran or PL/l DO loop.
So Plauger might simply be used to a preference of for loops shown by the base material and many many examples derived from it.
Take Away #1: Code writing is always a matter of style preference.
The book goes on a bit about further savings in source lines, showing a clear preference of K&R for writing tight source code, almost as readable as APL (but... see below). Of course there could be other PoV.
The discussion about the merits of either version is already there in the very first book about C, more than a decade before Plauger's writing.
Take Away #2: Reading the basics first is a great idea before switching to secondary literature :))
But there's also the but-part about some compiler shortfall: Some of these compacting line saving techniques were also hints for the compiler - or better ways to make less 'intelligent' compilers generate the best code. A line like *s1++ = *s2++ can be turned into an assignment with auto increment operations without much lookahead or reshuffling. Even better, on a CPU which sets the flags according to the last item moved, it spares an additional test for a trailing zero byte (*2).
So even the dumbest compiler, used for a CPU with auto increment and flags set by move will choose the best possible code - and that's what a PDP-11 was. It's one of the artefacts showing that while C may be seen as CPU independent, it was in all practical use constructed to best support a CPU with certain features.
This kind of compiler support became useless if not superficial the moment C was ported to different CPUs (*3) as well with more sophisticated compilers.
Take Away #3: There is a part of compiler hacking included in using such constructs. Even though it was already way outdated when Plauger wrote his book.
A different POV about Style, readability and meaning.
In the mind of K&R for is kind of a combination of COBOL and APL: the readability of COBOL by putting all items necessary for (basic) loops into a defined structure, so no need to look around for initialization, while keeping it compact like some APL code.
But when looking at it in a more fundamental way I would consider both implementations presented in the question as bad implementations and note it in any code review.
To start with, using while with anything but a test expression is using side effects. By definition (check K&R) while is a repeating loop, one that is executed ZERO or more times. No execution of whatever it contains will ever happen if the condition is not true at the beginning of each iteration. Using it as shown goes against that basic principle.
After all, the task of copying a (zero) delimited string needs to transfer at least the trailing delimiter. So the task is to transfer 1..n elements, not 0..n.
Similarly, for does not fully fit the task, at least not as used. for defines an iteration as initialization, condition and reinitialization. Again none of the three should be manipulating anything outside of loop control. Except, when taking the items apart, using a for no longer satisfies the need for a 1..n loop - at least not without a lot of temporary variables/constructs.
The grammatically correct construct to describe this is a do/until as in do/while, so it should look rather like this:
do
st = *s1++ = *s2++;
while (st != '\0');
Yes, I know, unfamiliar to classic C programmers drilled to save on lines at all cost.
*1 - Note the word re-initialization got changed to increment in the second edition.
*2 - This is BTW why Plauger writes (*s++ = *s2++)!= '\0' as that forces an explicit test for a zero value transferred, independent of compiler and CPU type. One step toward real portable code.
*3 - Including x86, which does not set flags according to moves.
char* strcpy(char* s1, const char*s2)or that will break some compilations when a constant pointer is passed as second argument, and you could skip the copy of the second argument completely then – Jean-François Fabre Jun 22 '22 at 14:06s1is assigned tostwice: in the variable initialization, and in the for-loop initialization. – Leo B. Jun 22 '22 at 15:41for(s = s1; (*s++ = *s2++)!= '\0';)would never pass a code review nowdays. Spend some more lines of code, make it readable. It will compile to the same thing. – Kingsley Jun 23 '22 at 00:04forloops work, and only ever want to pattern match all code againstfor (int x = (…); x < (…); x++)so that they can pretend they understand what it does. – user3840170 Jun 23 '22 at 05:05strcpyimplementation has some issues: 1)s2should be declaredconst char *, notchar *. 2) Since C99, both arguments should be declaredrestrict. 3) Declaringsu2is unnecessary, ass2should already beconst. 4) You, too, are assigningstwice. And as a personal matter of taste, I despisefor/while/ifnot followed by{, and prefer making empty loops very much explicit, for clarity and robustness when code is added later (when a trailing semicolon might be overlooked). – DevSolar Jun 23 '22 at 08:10forloop. Thus, for many C programmers,forbecomes the idiomatic way of doing it. I certainly find it much easier to standardize onforthan having to think about all different kinds of loop structures/syntax. There are always multiple ways of writing the same code. Pick the one you find easiest to read and/or easiest to write without bugs. – Cody Gray - on strike Jun 23 '22 at 08:15.hfiles:#define EVER ;;. As a result, we had a too-cute local (/in-house) standard of sayingfor (EVER)instead ofwhile (true)– Flydog57 Jun 23 '22 at 16:09