Current Topic: The C programming language is really quite simple. In spite of that it can, and has, generated some impressively powerful and creative programs.
Programming Before the C Compiler...
I started programming long before C compilers were widely available. In fact... I programmed in Assembly language probably up to the early 1990's. And even when I had access to a compiler I still preferred, for the simple programs I was writing at the time, to write most of my code in Assembly. In those days I was primarily a Hardware designer and I only trusted the one-to-one relationship Assembly-code has to machine-code. My primary interest then was to stress the hardware into revealing any bugs as well as providing guidance to production departments which would ultimately produce the final code. Eventually though Silicon Manufacturers and their single-chip-solutions to every problem took all the fun away from designing hardware. Oh well!
Eventually... The advances in hardware design led me more into serious programming. Now I get to break other designers products. A.K.A. Pushing the envelope. Or if your Canadian... 'Just Give Her'.
The C Language Is Simple, Elegant And Rocket Fuel For A Good Programmer...
The beauty of the C language is that it is really an abstraction of the ideal processor architecture. Not quite as sophisticated as a pseudo machine. But the closest your going to get otherwise. It is designed to closely match the instruction capability of the target processor. Including generating multiple instructions for some targets while producing single operations for others. Its real elegance however is the way it exposes data to the programmer. Let me tell you... Managing data in Assembly language is tedious and meticulous with temporary stacks and register manipulation using mostly anonymous memory blocks. So is branching. You know... Conditionals (if, else, do, while, for).
All the C language really offers is the ability to define functions (subroutines), manipulate data and conditional branching. For manipulating data it offers a set of math and logic operations, the ability to move data about and a unique extension to array management providing a simplified method of handling compound data types like 'objects'. Additionally through simplification of mathematical expressions it provides a good abstraction of typical processor branch instructions exposing a more intuitive method for defining conditional processing.
Good Programming Practices Are Universal...
One of the most important things I was taught is that a function (subroutine) should have only one entry point and one exit point. This applies to a program as well which is really just a subroutine (or task) for an operating system extending to even the operating system itself which really shouldn't have an exit. Should it? Anyway... This concept is far more important in Assembly then in C. Break this rule and you generally are opening the code up to maintenance issues. It also makes the code difficult to follow. C does a pretty good job at enforcing this however there is one exception. The 'goto' statement. Which, no surprise, I NEVER use. Additionally most Operating-Systems allow a program to call an 'exit()' function. Programs do this when they are in trouble and just want a lazy way to terminate and hope the O.S. is good at cleaning up any mess (dangling resources) the program left behind. Again I completely disapprove of the use of either of these shortcuts.
Secondly... When I started programming, in Assembly language, I had only a single-pass assembler. Other then within the scope of a subroutine there was no forward-referencing for external labels. As a result of this I learned to write code where 'Everything' is defined before it is referenced. Basically... For any reference to data or code the object being referenced should be defined before it is used. Really! When you think about it... Under this rule you can inherit a body of code and as your reading it for the first time you never come across an item where you have to scan forward to resolve a definition which you are reading for the first time. This is actually a personal preference. The multi-pass C compiler does not penalize for this technique. Sadly... it somewhat encourages it.
The C Compiler Preprocessor...
A preprocessor scans source code and using certain syntax as triggers it can be used to substitute a code 'label' with a 'defined' alternative. Along its scan it also generates a database of definitions within the code. Both of these mechanisms are used to resolve unknowns, usually forward or external references, that the compiler may encounter while translating to machine code.
It is best to look at the 'Preprocessor' as merely a text substitution utility. In fact... My first C Compiler had to call an external program (the preprocessor) before the compiler which generated an intermediate file that the compiler actually consumed. Still mostly coding in Assembly then I ignored my compiler for many years however I used the external preprocessor to translate pseudo 'Macro' operations into Assembly code. My first Multi-Pass Macro Assembler.
State Machines...
Originally... State machines were simplified processors made with simple hardware (logic) performing very limited and specialized 'processing'. Really quite simple a state machine evaluates a single input expression (the state) and selects a processing path for each defined case (state). When no defined case is specified for a given state then default processing occurs. Thus for every state some action occurs. Even if the action is 'ignore'. In any event it is part of the state machine case processing to also prepare (if necessary) the state for the next pass. And with the extended capability that 'software' offers will certainly include objects outside the scope of the state machine itself.
The C language provides for a very efficient state machine implementation model with vastly more ability then pure hardware could ever produce. A good programmer should recognize and implement this method appropriately.
Brackets And Scope...
Brackets (braces) '{} () []' are used to define (to the compiler) 'Scope'. Generally... The compiler will consider references within the bracket pairs first when trying to resolve for a set of rules. During the compiling process ambiguities will arise which a compiler must resolve. For the general (most common) uses there is an 'Order Of Precedence'. A set of rules that determine how a compiler decides the order of operations for a given code expression. Sometimes this is not an adequate option. This is where the use of brackets (within context) can be used to guide the compiler by reducing the number of rules it has to chose from. Proper use of this mechanism can eliminate ambiguity completely forcing the compiler to generate, exactly, the code the programmer intended.
The bracket pair '{}' define a code block. Anything defined within the brackets is local (withing the scope) of the brackets. Typically this applies to function definitions and conditional statements however it can also be used to localize any valid code 'block' within. You might see this, for instance, within a case statement to define local variables which the compiler might not understand (or accept) otherwise.
Parenthesis '()', on the other hand, are more localized belonging to the statement directly preceding them usually defining a set of operands (parameter list) or arithmetic/logic operations and never contain actual executable code but can additionally be nested ie...
a = 2 * (2 + ((1 / 2) - (1 / 3)) + (1 / 4));
In all arithmetic/logic cases statements within a parenthesis pair are always evaluated first by a compiler and the resultant term is substituted within the statement etc... completing the evaluation with a single term.
Braces '[]', are used to define an array or reference an array element. Arrays are defined as a table of elements of a certain simple or maybe complex data type including arrays making it multidimensional. Arrays (pointers) are most commonly passed as arguments to subroutines. And because of the way arguments are defined the compiler 'can not' always properly 'bounds check' when referencing.
7600