Questioning Assumptions

This essay is going to examine control structures from the standpoint of machine language (ML). As I was writing it, I came across a few surprises. Hopefully, you'll find a few too.

From a machine standpoint, the only two instructions that matter are the current one and the next one. Programs are sequenced from a program counter (PC), and unless the order is otherwise changed, instructions are fetched in numeric order from the smaller addresses in program memory toward the larger.

The first possible change comes by way of an instruction that can load a new value into the program counter and start a new sequence from a new address. For the first part of this blog, we'll assume such an instruction is "immediate", meaning the address that gets loaded into the PC comes right after the Jump instruction itself.

Such a Jump is a "forced jump". There is no choice about it, the program counter will be modified and a new sequence of instructions will begin. An alternative to this would be a "tested jump". A test bit is generated from some source and depending on its value, the PC may or may not be loaded with a new address. If not, then the current sequence can continue, interrupted only by the jump instruction and its immediate data word. Think speed bump.

The new address in the PC can either be higher or lower than the one it replaces. If higher, the Branch will pass over a number of instructions that otherwise could have been part of the original sequence. If lower, a Loop will likely happen. Sequencing will start from the lower address and proceed until it again reaches the tested jump. If the test bit is the same as before, the loop will repeat and keep repeating until the test bit is finally different.

In the previous paragraph, I chose my terms carefully. Generally, jump and branch are used interchangeably. In my life, I've certainly used pretty much one the other at random. But now I'm designing a language for the Flexible System Architecture (FSA), and it occurs to me that jump, branch, and loop may make useful keywords if I consistently give them specific definitions. So from now on I will try to stick to these usages for the purposes of this essay:

jump  == forced jump
branch == tested forward jump
loop  == tested backward jump

It is certainly possible that a loop will jump ('jump' here is a verb not a noun so I can get away with using it!) so far back in program memory that the new sequence will hit its own speed bumps and maybe never again reach the looping point. In this case the loop would effectively be a branch as far as its non-looping effect goes ... I'm still working out the details.

In higher level languages (HLLs), two common kinds of loop constructs are "do" and "while". Here's an example of each in C:

do {
  dostuff...;
  domorestuff...;
  etcetera...;
}
while (sometest == someval);

while (sometest == someval) { ditto...; }

The only difference between these two is that the 'while' construct may skip the sequence within the loop altogether; the 'do-while' will run through the sequence at least once. It is the difference between a "top tested loop" and a "bottom tested loop".

Every English-like HLL has its own names for looping, variations on 'do', 'while', 'loop', 'begin-until', whatever (what about 'for'? I'll get to it soon).

Here's a realization I had while I was doing the outline for this essay: in machine language, every loop should be a bottom tested loop. Think about it: If you're at the top of a loop, how did you get here in the first place? You probably already did a test somewhere else to get to this point. So make your choice at that earlier point and either branch past the new "do at least once" sequence, or don't.

The only exception to this might be where the sequence just above it is running into, or "falling into" the new sequence. Here a test has to be done, if only to branch beyond the loop's sequence. My experience in these cases is that if the loop is 'embedded' in a longer sequence, the algorithm, whatever it is, usually wants to do the loop code at least one time - no test needed anyway. But that is not a hard and fast rule.

A problem with top tested loops is that they are less efficient. The TTL needs a forced Jump at its bottom in order to loop back to the test itself. The BTL only needs one tested jump at the bottom, none at the top. Jumps aren't outrageous in their use of resources, but they do take up at least a word or two, the time to run them - and - the time for the new address to work its way through the program counter and then for that address to propagate through the memory and deliver the new instruction.

Maybe it's too harsh to say every loop should be a bottom tested loop, but definitely most of them should, when possible.

What about the for-loop? To ML it's just a standard loop with specialized testing set up for it:

FOR
   FROM beginning_number
   TO ending_number
   BY some_increment

It seems obvious that a for-loop is going to run its internal code sequence at least one time, else why jump to it at all? So a bottom tested loop fills the bill, right? Right? Gotta dig deeper. Here's a for-loop in C:

for (j = 0; j < k; j++) {
  dostuff...;
  etcetera...;
}

The ending number is a variable, meaning if k == 0, the loop will run zero times. And chances are, if somewhere along the way the application sets k to 0 it is because right then it doesn't want the loop to run. But the bottom tested loop would still do so. Oopsy. I don't know, but I suspect most C compilers always generate a top tested loop just to be on the safe side.

Sometimes you might want to break out of a loop before its official end. Other times you might want to continue the loop but go back to the top from somewhere in the middle. C has two operators that do those things. By sheer coincidence they are called "break" and "continue."

I got in trouble with a boss one time because I turned in some C code which had a medium sized function that had a few "returns" spaced throughout it. Based on some test, the function had done as much as I wanted it to, so I branched out of it instead of letting it run all the way to its end. I was informed: "That is improper programming style. Functions can only have one return. Take them out!" I argued back, "Look, they are clearly marked with explanatory comments." But being the boss, he prevailed.

When I got back to my station, my first impulse was to surround all the stuff in the function with one big do loop. Then I could replace all my 'returns' with 'breaks', which is considered good C style. I knew that would just insult him, so I reworked the function so it conformed to the standard. Maybe I broke it into smaller functions, I don't remember.

I did understand where he was coming from, actually. Programming style is a set of ground rules that help another programmer read and understand your code. There are generalized ground rules for programming in general, and specific ground rules for each different language. I was creating a function with unusual break points in a language where people wouldn't be used to dealing with them.

But I was right too. Imagine a program that has a really tiny six line function in one place containing two returns, and in another place has a really long loop (one you have to scroll down several screens to see it all) filled with breaks and continues. Although the loop is maybe not very good code that should be broken down into smaller loops or even separate functions, it nevertheless does follow the rules. Yet the tiny function, honestly, is likely more readable than an equivalent one artificially reworked so it has only the one "legal" return. There's another, general, rule of style that sometimes trumps: if a code segment, of whatever type, can fit on one screen, that's a good thing.

If you want to get into lots of arguments with bosses, or professors, or other authority figures, make use of another control structure, the infamous GOTO. The once popular BASIC language fell into disfavor in no small part because it made goto-ing so easy.

Every line of source code in BASIC is numbered. It made it easy to write "GOTO [line] 500". There'd be a few lines of straight-line code at 500, then there'd be a "GOTO 30". Then at line 36, there might be a test: "IF something GOTO 55 ELSE GOTO 160" Pretty soon nobody, not even the original programmer, can follow all the jumping around. It has a name: "spaghetti code". On the printout of a bad BASIC program, if you draw arrows from every GOTO to its associated line number, all the arrows do eventually start to look like a plate of spaghetti.

Here's the dirty little secret about machine language: it is all GOTOs! It is all made up of only sequences & branch points.

In truth, machine language is a line-numbered language, only the line numbers are memory addresses. Machines are dumb. Remember the second paragraph? "From a machine standpoint, the only two instructions that matter are the current one and the next one..." And "the next one" might immediately be followed by a whole new address to direct the PC to a different "number".

From a spaghetti standpoint, it is possible to make machine code look like Italy on a feast day.

In my intro to these blogs, I told how fascinated I was by the relationship between human languages and computer machine languages. It's interesting that in a way, we're coming full circle. After looking at ways to get English connected to various computer operations, we found that the act of compilation was a way of bypassing the human symbolism. In fact, fully optimized compiling squeezes it out entirely. Why have the unnecessary alphabetic stuff around taking up space?

Here we are beginning to discover that the English in the source code of higher level languages might impose a useful formalism on the control structure of a program. Even if the resulting machine code isn't perfectly efficient (eg, the for-loop ending up top tested), the gain in readability and maintainability may be worth it.


Back to back to back

If-Then-Else - If this were some sort of tutorial, then it is backward compared to most books on languages and the like. Usually the "conditionals" are covered first, then the loop constructs. But in ML, looping is simple, conditionals can be tricky.

"If" isn't tricky, it's simply a Branch, as defined awhile back. A bit is tested, and either the current sequence continues onward or there's a branch to somewhere down the road.

"Then" isn't tricky, it's not even necessary. A lot of HLLs don't support it. It merely refers to the sequence immediately following the If - the sequence that runs if the branch is not taken.

"Else" too is a tested jump, and specifically it is the opposite test to whatever If it is associated with. If there's an If like:

IF (this EQUALS that)

then the Else is:

IF (this NOT_EQUALS that)

Else can be tricky. Or not. At about this point, I began to wonder if I weren't using too many examples from the C language. Then I went and faked myself out with a couple.

I started with an if-else example:

if (counter == 0)
{
    counter = 1;
    etcetera ...;
}
else
{
    counter = 0;
    etcetera ...;
}

and compared it to a double-if example:

if (counter == 0)
{
    counter = 1;
    etcetera ...;
}
if (counter != 0)
{
    counter = 0;
    etcetera ...;
}

I explained how they weren't equivalent because the code is supposed to flip counter to its opposite state. That doesn't happen in the double-if because counter gets changed before it's retested, and the retest may flip it back to its original state. So I wrote that some other variable had to be set equal to counter, and that variable tested in its place. I then used up several paragraphs how to do this with a minimum hit to machine code efficiency.

Wrong, wrong, wrong, I was letting myself get fooled by the layout of the C code. Let's replace it with some C that definitely won't compile:

if (counter == 0)
if (counter != 0)
{                 <-- Branch here from first if
    counter = 1;
    etcetera ...;
}                 <-- Branch beyond next code block
{                 <-- Branch here from second if
    counter = 0;
    etcetera ...;
}

Now it is laid out like the underlying machine language should be. And this also works if the language supports the "multi-way-branch" of If - Else If - Else. Doesn't matter how many such get strung together,

if elif elif elif ... else

the tests come first, the code sequences come next, each ending (except for the very last one) with a forced jump beyond any remaining sequences. Only one of the multiple sequences will actually be processed; the computer plays 'hopscotch'.

Humans are dumb! This one can be anyway. Too intent on writing, not intent enough on thinking. And yet, this is another good example of how the English version of even something as simple as if-else is advantageous. If I have to read program text with my own eyes, I'd rather see the code blocks directly under the tests, rather than split apart as they have to be in ML.

Let's move on to two new control structures, AND & OR. Each is a compound If test, such as: IF (this AND that AND other). If all three tests are true, do the sequence immediately following, otherwise branch past it. AND, like if-elif-else, can also be evaluated by simply chaining tests. No hopscotching this time, there's only one associated sequence to either run or not run.

This blog is getting long, and yet just above was the first appearance of the word "true". And here's the first appearance of: "false". Now's a good time to talk about them. Here's a C statement:

if (this == that && other == nother)

Note that && means AND, and also note the double ==, which is different than the single =. In C, the meaning of = by itself is very similar to the mathematical meaning (actually, it's called an "assignment"). The double-equals on the other hand evaluates to a 1 or 0.

Computers are binary. A test in ML can only load a fresh address into the PC, or let it keep counting. Two and only two choices. And until quantum computers give us the choice of "0 or 1 or maybe-kinda-sorta", it's going to stay that way.

True or a False - the objects of tests and their test result are known as "booleans". An operator like AND or && or whatever it's called evaluates between two boolean values, and itself becomes a boolian. In C, a False is a Zero, and a True is anything else - any other bit pattern. Simple and logical.

I haven't flamed anything for awhile, so let me go off on higher level languages that implement a special boolean data type. Nothing wrong with that - if it's an option! In some HLLs, it is not. Which would you rather type, for your entire professional life, something like:

if (this)

or something like:

IF (myboolean .eq. FALSE)

OK, I made that second one up. Yet I've seen all those operators at some point in my life (and yes, C allows you the option of not using == in conditionals if you simply want to test a variable for 'Not-Zeroness').

I mentioned that the English of HLLs can provide a formalism, a convenient conceptualization that can become more and more useful as you begin to 'think' in the language. Oh, you can comment the heck out of machine language, but you're still 'thinking' in ML. If you're a programmer, you know what I'm talking about. If not, well trust me, you really do begin to think in a language you use a lot.

But there is a tendency for HLLs to become overblown, either because standards committees have over time added the kitchen sink and a bathtub or two, or because they were designed that way in the first place. There are those who believe that many languages are made to be used by large groups of average programmers working for big companies. The aim of these HLLs is more to prevent mistakes than to be as powerful as possible. I don't know how true that is, but it would explain a lot.

Some languages I've looked at seem like they are trying to baby me. In C, you delimit a block of code by curly braces: { ... }. In some other languages it's: BEGIN ... END. Which would you rather type over and over? Do they think I cannot connect the concepts of beginning and ending to left curly and right curly? Apparently so. This is just one example. There are many. Too many.

Thanks. Glad I could get that out of my system.

Where were we? Oh, yeah, ANDs & ORs. Look at this C:

if (a == b && c == d && e == f) {
  run some code ...;
}

The code inside the block won't be run unless all three =='s evaluate to True. In machine language, it becomes a chain of tested jumps end to end to end. If the first test evaluates True, it falls into the next test. If, rather, it evaluates False, then it branches past everything - past the other tests and past the code block of the If. This happens at every test; they all either fall through or branch past the end. The last test, if also True, falls into the sequence the If is intended for.

It doesn't matter how many ANDs there are, they are looking for a string of Trues:

True True True True True True True

One single FALSE anywhere,

True True True True False True True

and the If-code will be skipped.

The OR, on the other hand, is happy with just one single True. It certainly 'likes':

True True True True True True True

but it will run the associated code block even with, say,

False False False False False True False

Each test that's False falls through to the next test. Hit a True, and branch past all the tests to the target code sequence.

There's a tiny "Gotcha" - do you see it? ... What if they are all False?

False False False False False False False

The Falses all fall through to one another, but if that last False falls through, it will run the If-True code! So that very last False had better branch past the target sequence instead. Its test needs to be 'upside down'.

The very first assembly language I studied (I think it was for the 8080 chip) had mnemonics for two simple tests, "JZ" and "JNZ", meaning Jump Zero and Jump Not Zero. The ALU, when testing two values for equality, set a status bit that the machine language could test. Different assemblers may use different mnemonics, but all MLs support equivalents to JZ and JNZ. If a test jumps on one value, it falls through on the other one. And vice-versa.

So if I was right about the chip being an 8080, and if as I remember, equality yielded a 1, then the test instructions in the 8080 for a seven-test OR would consist of

JNZ JNZ JNZ JNZ JNZ JNZ JZ

If you have a pretty good idea which OR test is most likely to be usually True, you can put it first in line and increase the average overall efficiency - save doing unnecessary tests. The same holds for the most likely False of an AND. This approach can work in higher level code also, but watch out - C evaluates AND & OR expressions like you read them: left to right. Some HLLs evaluate right to left. Gotta check those specs.

It's worth putting a link in here about something called Binary-Decision Based Programming (BD). It consists on only two instructions: Jump and Output. Such a simple approach doesn't even require a traditional ALU. That's as far as I'll detail it here except to say that it is very fast for evaluating a serial string of bits to be tested. And similarly to the AND & OR, if the first bit presented is the one most likely to yield a final result, then the remaining bits may not even have to be processed.

Only one more common control structure to go - whew! It is called a "case" statement. By now you can probably guess that it is some sort of bundle of tested jumps. In this construct, the "one is tested against the many." A C example is actually a good idea here:

switch (somevar) {
  case 4:
    dostuff...;
    break;
  case 99:
    dostuff...;
    break;
  case 30000:
    dostuff...;
    break;
  default:
    dostuff...;
    break;
}

The "bundle of tested jumps" for this logic compares somevar against the constants 4, 99, and 30000 in turn, and branches to the associated code segment. If no match is found, then the code at "default:" is run. The default is optional. On a given pass, if there's no match and no default is specified, the whole case statement is passed over. It becomes a glorified speed bump within the enveloping sequence.

The case format is interesting because one case can 'fall through' into another. The 'break' statements are forced jumps that branch to the end - the switch has switched. But if a break statement is left out ... for example, if the break between case 4 and case 99 is removed, then the sequence for 99 will run not only if 99 is the match to somevar, but also if 4 is the match. 4 runs, immediately followed by 99.

Some HLLs don't allow the fall through mechanism. Some implement it slightly differently. In the Perl language, cases automatically break out unless the keyword 'continue' is inserted at the end of a case to allow for falling through. That is probably a better design than C has, honestly. It's easy to forget to add that break statement, and then you have to chase a bug. A specific fall through command stands out, making the logic flow more readable, IMHO.

The case statement is very similar to a "jump table". The top of a JT is at some preassigned address in memory wherein are stored a number of forced jumps. A variable similar to the somevar of the case example can be added to the JT start address, and this computed address can then be loaded into the PC, transferring control to one of the jumps in the jump table. Voila - jump and rejump.

This only makes sense if the variables potentially added to the JT address are small. The values 4, 99 & 30000 of the case example would make for a ridiculously sized jump table. Small integers like 2, 4 & 6 will work much better. Why not 1, 2 & 3? They're fine if the forced jump of the particular machine language only takes up one instruction. Two instructions is more common, and sometimes three or more, depending on the computer architecture. Adjust your index into the table appropriately. One limitation of the JT compared to the case statement is that the fall through mechanism is not available.

It is possible to have a JT that is made up only of the jump addresses themselves. This removes the repetition of the forced jump instruction throughout the table, making it more condensed, but making the mechanism to access the table more complicated. In other words, the table is now made up only of data - a bunch of addresses, one right after another. The ML program can no longer 'go inside' the table because there are no longer any forced jumps inside to get the program back out! If it does end up inside, it will treat the addresses as if they are instructions - and they will no doubt turn out to be a series of CRASH instructions ...

Many processors have a set of 'indirect' instructions that essentially say, "Take an address from somewhere in memory and load it into the PC." Problem solved.

Let's back up. As promised, until now, every Jump, Branch, Loop and test in this essay has involved an Immediate instruction, meaning that if a new address is going to overwrite the PC, that address will come right after the instruction itself. But many computer architectures also support Direct instructions, and even Indirect instructions.

Suppose you're in a strange city and want to find a Post Office. You stop a stranger and ask for directions. He looks at you funny and says, "You're standing right in front of it! See the flag, see the sign that says USPS?" Red faced, you go up the steps, open the door, you're 'immediately' inside.

Maybe you're not quite that unobservant, and the stranger instead tells you, "Go down to the first stoplight, turn left, it's right up the street." You get there because you have been correctly 'directed' there.

Finally, the stranger might say, "I dunno, I'm new here myself, but I noticed the library is two blocks down and a block over, they should know." Now, when you find the PO, you've taken an 'indirect' path. The stranger gave you an "address of an address." Or, to use terminology from the previous blog, a "pointer to a pointer".

These are not, unfortunately, hard and fast definitions of these "addressing modes". Different processors with different MLs might use different words for the same effective operation. In fact, what I've been calling immediate addressing is sometimes referred to as absolute addressing.

Whenever I, myself, start getting confused I recall looking for that Post Office, and think:

Here - There - Elsewhere  /  Immediate - Direct - Indirect


Remembrance of things recent

It's not exactly a control structure, but let's finish up with subroutines. The idea of a subroutine (SR) is that one sequence has branched to another, and when when the second sequence has reached its end, the programmer wants to get back to where things left off in the original. So the combination needs a memory - before it "calls" the SR, the first sequence has to save its own place. It has to store an address that the SR can use to "return" to the original spot in the sequence that called it.

To be clear, the subroutine doesn't have to be a simple sequence, it can be a whole mini-program with its own internal loops and branches. But it does have to have a definite end, at which point it will use the stored address to get back to where it was called from.

Even the most primitive, low level languages usually have support for subroutines. They provide a special memory area for saving SR return addresses. In fact, they usually provide room for saving a whole stack of them so that SRs can call other SRs which can call still other SRs, and so on up to a 'reasonable' level of "nesting". Subroutines can even call themselves - a concept known as "recursion", which is nothing more than another way to loop.

A subroutine is the closest thing to a function that you can have in pure machine language. And what is a function? It is a bogus entity foisted on computer science by overly cerebral mathematicians.

I sure am having fun writing outrageous statements in this blog.

Actually, the idea of a function is a pretty good concept for programming if you don't go too nuts with its mathematical definition. There was one HLL, Pascal, that made a distinction between procedures and functions. They both would work on/with data structures, but functions returned a value, while procedures didn't. Big whoop. Why not have some other format that could return multiple values? Well, because "true" math functions only have one output for each input. Don't get me started on Pascal.

In a more general sense, having a self-contained bundle of sequences and tests that you can give an English name to is a very handy idea. Functions in high level languages might be the most useful human language formalism ever imposed on machine language. But keep in mind that bare bones ML doesn't know from names. Names in machine code are merely glorified comments. Here I'm talking about pure Ones and Zeros code, not assembly language, which often does provide for "macros" and other naming conventions.

Remember from above when I had that talk with my boss about the bad programming style of returning from the middle of functions? It is considered even worse style to branch INTO the middle of a function. Yet in ML, this gets a bit fuzzy.

I also mentioned above that I'm designing a language for my FSA computer architecture. I will detail this work in future essays, but for background here I'll say that I've begun by writing a "kernel" for a version of the Forth language. The English of Forth is made up of a "dictionary" of "words" - Forth definitely has an unusual terminology for an HLL.

Anyway, this kernel consists of a number of more or less self contained machine language routines that convert the Forth words into FSA operations. For my own convenience, I've named some of these routines, but again, any such names are really just glorified comments. The central kernel routine is "Main dictionary traversal", which sometimes may branch to "Transfer thread addresses from dictionary to stack", which eventually falls through to "Run from thread stack" (the thread stack is Forth's version of a return stack).

"Run from thread stack" may sometimes branch to "Stack more threads", which falls through to "Transfer thread addresses from dictionary to stack". Listing those last three in order may make it more clear:

Stack more threads
    (falls through)
Transfer thread addresses from dictionary to stack
    (falls through)
Run from thread stack

The point of all this is that "Stack more threads" consists of exactly one instruction. It increments a counter associated with the thread stack, a counter that is in its proper state when reached from "Main dictionary traversal", but which needs a 'kick' when "Run from thread stack" branches.

For practical purposes, "Stack more threads" is part of "Transfer thread addresses from dictionary to stack". In a high level language, there would have to be an extra test at the top of this combo to decide whether to increment the counter. And most likely it would require an additional function parameter to provide the bit to test with. Or, there'd at least have to be more complicated code in the calling function to increment it there before the branch. Yet the most direct approach is simply to have two entries into "Transfer thread addresses ...", rather like the fall through mechanism of the case statement.

In programming, many routines, sometimes even simple loops, follow the general form:

setup - do stuff - finish

If the 'do stuff' is at all complicated, there are typically a few lines of 'setup' code to initialize or modify entities that will be used or further modified in the main routine. In ML, it is easy to branch into the middle of the setup code. In HLLs, it isn't.

Even though it is argued that such 'goINtos' are bad form, chances are that if you're programming in machine language, you're more interested in efficiency than style. Compilers have become extremely sophisticated at generating efficient object code, but they still ultimately have to work from HLL source code that enforces various conventions. And rightly so - the secondary theme of this essay has been that English can be our friend. A human-ish language HLL can big a huge aid in conceptualization and readability.

Nevertheless, ML has an ultimate flexibility with its de facto GOTOs. Ya just gotta be careful! Comment, comment, and then comment some more. Have both 'strategic' and 'tactical' comments (both overviews and details) at important sequences and branch points. Remember: machines are dumb - mindless automatons, chugging away at 'this instruction / next instruction'. So a machine language programmer has to be smart enough to counterbalance this automated stupidity.


Recursion, and Pointers

In The Perils of Java Schools, Joel Spolsky writes, "If I may be so brash, it has been my humble experience that there are two things traditionally taught in universities as a part of a computer science curriculum which many people just never really fully comprehend: pointers and recursion." He goes on to say that Computer Science departments sometimes use these subjects to weed out those students who basically should be studying say, Political Science.

Hmmmm. A bit drastic, I think. Yes, both can be a bit tricky at times, and both can require a bit of hard thinking, but their underlying concepts are not truly egregious.

As mentioned above, recursion, where a subroutine or function calls itself, is nothing more than another way to loop. That's all it is — honest. One way I think people get confused by it is that, like any other loop, it will run forever unless there is some sort of test used to break out of it at some point. That's easy to forget at first. Oops.

Actually, recursive calls are kind of fun once you get used to them. They somehow seem to be extra clever. However, I rarely use recursion anymore because it is usually expensive in terms of resources. Especially in high level languages, every time it loops, all the overhead of a function call has to be processed. All its variables and anything else the HLL associates with a function has to be saved on a stack somewhere. Lots of time and space are consumed, where a simpler loop structure would probably do the same job, and more cheaply. There are a few languages that do support "tail-recursion", which keeps just one instance of a function in place no matter how many times it calls itself.

Pointers were discussed a lot in the previous essay. Hopefully it was clear by the end that 'pointer' is merely another word for 'address'. Pointer to = address of. Address of = pointer to. Check back above about how to find a Post Office for a different take on a similar subject.

A memory address is not all that different than the address of a building in a city. Just envision a single long street, 'Memory Street,' reaching some very, very high addresses way uptown (by the way, "Memory Street" is going to be the name of my Great American Novel. Or my really awful screenplay. I'm not sure which yet.).

I think to some degree the confusion about pointers is because many programmers first encounter them when they begin to learn the C language. Here is a C code fragment showing the syntax of copying from someplace in memory with a starting address of 'q' to another memory area starting at 'p':

while (( *p++ = *q++ ) ...

The asterisks '*' stand for 'contents of' while the '++'s stand for 'increment address by one'. If you forget that '++' has operator precedence over '*', you can add some more parentheses to help make it clearer.

while (( *(p++) = *(q++) ) ...

Yeah, much clearer. Whenever possible, which is about 99% of the time, I avail myself of C's alternate array notation:

while (( p[j] = q[j] ) ... j++

Yes, I have to add an array index 'j' (most people use 'i' as an index variable, yet if you have to search the source code for some reason, you'll trip over a lot more unwanted 'i's than unwanted 'j's), but if I ever want to see where I am to debug things, printing out values of 'j' will give me nice readable numbers like 0, 1, 2, 3 ... Printing out 'p' or 'q' on the other hand will yield, starting at some random address, numbers like 7854223, 7854224, 7854225 ... I've been told I'm "not a real C programmer" for doing things like this. OK, I'm not a real C programmer. A good compiler will compile efficient machine code for either format, so I don't really care.

Here, directly from Harbison & Steel's excellent "C A Reference Manual" is the array notation for the third element of the second subarray of the 2-by-3 array named 't' (C array numbering starts at Zero):

t[1][2]

compared to its pointer notation:

*(*(t+1)+2)

Use whichever one you like!

Pointers do get more daunting when you start dealing with pointers to pointers (or addresses of addresses). Just now I did a search of my simulator code and found out I passed "doubly indirect" parameters to 48 different functions - more than I would have guessed. How did I keep things straight in order to write such brilliant, masterful code? My solution was to cheat - sort of. A truly brilliant, masterful programmer would have carefully studied the descriptions in the H-S, or similar reference, and written perfect code the first time.

I, on the other hand, wrote a mini-stand-alone program to start with, made my best guess of the proper syntax from a quick scan of the book, and watched the errors come out of the compiler. Then I changed an operator or two and got fewer errors the next time. Got it to compile eventually but the output was wrong. Made a few more changes and when it finally worked right, transferred the correct format to the simulator code. After the first one, the other 47 were pieces of cake. Only had to cut, paste, and change some variable names to protect the guilty. It has all been working fine for years.

OK, not exactly brilliant and masterful on my part, I'll admit. Yet do understand that by this time my coding was into the 'elsewhere' part of "Here - There - Elsewhere". Indeed, I was into 'elsewherewhere'. I had enough to keep in mind without adding the burden of learning exact syntax. In programming, the end justifies the means. Unless it crashes. Which it hasn't.





         Introduction to "The Perfect Language"    —   Table of Contents
         Hashing it Out    —   Previous
         Type Casting    —   Next
         Glossary




Started September 7, 2010