The previous two tutorials might have been interesting, or even enjoyable, but this particular one isn't gonna be very nice. Many programmers go for years writing non-portable programs when they could have easily written the program portably, so I'm gonna nip this one in the bud straight away and talk about how to properly write programs. If you learn this stuff from the start, you're on the right track for the rest of your C career. So, here goes.
Portability is about being able to write a program just once, and to have it work on a variety of machines. For some programs, this means it can work on Linux, Mac, and W******. For other programs, it might mean being able to work on anything from a tiny 8-Bit microcontroller to a large 256-Bit supercomputer. In order for us to have portability, there must be some sort of "standard" by which we write our programs.
C is a bit of a shape-shifter, a mutant. It came to be that C should work on every machine. Systems that support C are radically different from each other. Some have a 16-Bit "int", others have a 64-Bit "int", and even some have a 36-Bit "int". The C Standard gives huge freedom. Here's some of the freedoms it gives:
1) A byte must be at least 8 bits, but there's no upper limit on its size. You can have a 9-Bit byte, or even a 64-Bit byte.
2) For storing negative numbers, the system can use sign-magnitude, or 1's complement, or 2's complement.
3) The byte order for storing numbers can be any order (e.g. big endian, little endian, mixed endian).
Now the problem with C is that there are two standards. There's the standard of 1989, and the standard of 1999. The latter standard was an improvement over its predecessor, it made the following changes:
1) It removed stupid features that only caused problems (such as implicit function declarations)
2) It added some new features
Now you might think that C99 should have overtaken C89... but the problem is that people were so happy with C89 that they didn't pay much attention to C99. If you write a program that conforms to the C99 standard, you can't be sure that it will compile on every machine because a lot of machines still only have C89. (Well that isn't quite true... they have C89 as well as a few more features that came with C99). C compilers today are sort of half way between C89 and C99. Some of the C99 features they support are as follows:
1) The "inline" feature, which allows you to inject function code instead of calling a function.
2) The "long long" integer type that's at least 64-Bit. Prior to this, the biggest integer type was "long" and it was only at least 32-Bit.
3) The ability to define a variable anywhere within a block of code (as opposed to only at the start).
I'm a great fan of portability, so I stick to C89... but there are a handful of C99 features that I use because I know that every compiler supports them. For instance I define a variable wherever I want in a function, reason being that it's a thousand times better than defining all the variables at the start.
Thankfully, the whole C89 versus C99 thing works out OK, so long as you don't use some wild C99 features (such as variable-length stack arrays), your program will be portable.
Now moving on, the first thing to consider in writing portable programs is the integer types:
Even though C gives all sorts of wonderful freedoms, it still had to be sensible and give a few limits. First of all, I'm going to talk about the types used for storing unsigned integer numbers. Here they are, along with their minimum ranges as specified by the C standard:
First thing to note is that "int" is the default type, so you can just write "long" intead of "long int". Second thing is that the words can be in any order you like, e.g. "unsigned int short".
char unsigned (0 through 255, that's 8 bits)
short int unsigned (0 through 65535, that's 16 bits)
int unsigned (0 through 65535, that's 16 bits)
long int unsigned (0 through 4294967296, that's 32 bits)
long long int unsigned (0 through 18446744073709551615, that's 64 bits)
And here's another restriction:
range of char <= range of short <= range of int <= range of long <= range of long long
Therefore, if you know that a certain number will fit inside a short, it will definitely fit inside an int.
In the C Standard, it says that "int" is the "natural size type for the system". Maybe this means that on a 32-Bit system, "int" will be 32-Bit. Some people speculate that this means that "int" will be the fastest integer type. When people just want to store a number, they'll typically use int. If you're a portable programmer such as myself, then the important information you need is that "int" is only guaranteed to be at least 16-Bit -- therefore do not use it to store a 32-Bit number, not in a fully-portable program.
The smaller integer types, i.e. char and short, are typically used when you want to save memory. For instance if you want to store half a million integers in memory, you'll probably use the smallest type possible. The larger integer types are for when you want to store a bigger number, e.g. a 32-Bit number or a 64-Bit number.
I myself like my code to run lightning fast on everything from a toaster to a supercomputer, so I don't use the built-in integer types. Instead, I include a header file called "stdint.h", and I use the special integer types defined in it. This header file contains such integer types as "uint_fast8_t", which means the fastest integer type that can store at least 8 bits. Now there's a little problem here. The header file, "stdint.h", is something new that came with C99, it's not mentioned in the C89 standard. None the less, this header file is included in the majority of compilers, but not all of them. You might at some stage encouter a compiler that doesn't have "stdint.h", but there's a simple remedy for that, there's a "one size fits all" version of "stdint.h" that works on every system.
Here's these integer types in action:
(I'll talk about the little "u" written after the numbers in a few minutes)
uint_fast8_t i = 255u;
uint_fast64_t k = 872387u;
As the name suggests, "uint_fast8_t" is the fastest integer type that is capable of storing an 8-Bit number. So on a tiny microchip, it'll probably be an 8-Bit integer type. On a supercomputer though, it might be a 128-Bit integer type. I don't care how big it is though, I just want to make sure it's at least 8 bits.
If I wanted to store a bazillion small numbers in memory, I might use the "uint_least8_t" integer type, which means the smallest integer type that can store at least 8 bits.
Once upon a time, before I started using the "uint_fastX_t" family of types, I used to use plain old "unsigned int" whenever I wanted to store a simple integer number, even if the number was small like 7 or 52. This is the way the vast majority of C programmers operate. If you look at the limits I gave above though for the integer types, you'll see that "unsigned int" must be at least 16-Bit. This is gonna be a problem when compiling a program for an 8-Bit microchip, the program will be only half as fast because it's using two bytes of memory where it could have used just one! If you had used "uint_fast8_t" instead of "unsigned", you'd get optimal performance on everything from a toaster to a supercomputer.
Next thing to consider is "integer literals". A integer literal is a number written in your program, such as follows:
In this little snippet, "6" is an integer literal. More precisely, it's an expression of type "signed int", and it's value is 6. Notice that it's signed as opposed to unsigned. The default in C is signed, but 9 times out of 10 you only need positive numbers. As discussed previously, the "int" type is only guaranteed to be 16-Bit, which means the greatest value you can store in a 16-Bit integer is 32767. Because of this, you do not want to write the following in your code:
a = 6;
256 * 256 * 256 (the asterisk means multiply in C)
The result of this multiplication is 16777216, which is too big for a 16-Bit number, so you can't do that in a portable program. (You'll get away with in in Linux because "int" is 32-Bit in the gcc compiler). To change the type of an integer literal, you have two choices:
1) Write "u" and "l" after it. The "u" means unsigned, and the "l" means long. Therefore "6u" is of type "unsigned int", and "6ul" is of type "long unsigned it". (Remember you can write that as "long unsigned int", or even "long int unsigned").
2) The other way is to perform a "cast". To perform a cast, you just put the type's name in parentheses behind the integer literal, for instance:
At this early stage, my advice is to put "u" after every integer literal because we never want to work with signed integer types (not unless they're needed). So in the previous example, we would have needed:
256ul * 256ul * 256ul
(It's possible to leave out the "ul" on the last two, but I won't get into that now)
My advice is avoid signed integer types like the plague, only use unsigned integer types unless you have an explicit need for negative numbers. There are two reasons why I don't like signed integer types:
1) They can behave differently on different systems because there are three possible number systems for storing negative numbers.
2) If you "overflow" a signed integer type, bad things happen. (I'll go into further detail on this later).
Unsigned integer types don't have either of these problems.
So to summarise, here's my advice on writing portable programs:
1) Only use unsigned integer types (unless you need negative numbers)
2) Use the integer types defined in "stdint.h"
Now if that didn't turn your stomach, you can hit me back with an "I'm listening". Next I think I'll talk about loops.