- Introduction
- Compilers
- Hello World
- Memory Allocation with different Types
- Constants vs Directives
- Quotations
- Char Type, null terminator and memory allocation
- Pointers
This isn't a 'How to program in C' tutorial, this is just a grouping of topics/concepts from the C language that I found interesting while learning the language (from the perspective of an already establed developer). Some of the things I make mention of might appear obvious, but sometimes it's best to avoid ambiguity.
When writing a program in a language like C, that by itself is not executable (i.e. you can't run a C file). So you need to convert the C source code into machine code (i.e. something the computer's CPU can understand).
Machine code is as low-level as you can get when interacting with a computer. So the C language is considered a 'higher-level' abstraction to save us from having to write machine code ourselves. A language like Python is an even 'higher-level' abstraction to save us from having to write C (i.e. the Python language is actually written in C).
In order to convert C code into machine code, we need a compiler.
Strictly speaking you also need a linker which takes multiple compiled objects and places them into a single executable file. Generally speaking, when we say "compile a C file", we're really combining two separate processes (compiling and linking) into the single generic term "compile"
To compile C source code into an executable you need a compiler, of which there are many options. The two most popular being LLVM's clang and GNU's gcc.
You might find there is a cc available, but typically this is aliased to an existing compiler.
Also, Mac doesn't provide a compiler by default. But if you install X-Code you'll get the LLVM's suite of compilers. Below we see that we get quite a few alias' and all of them point to the same embeded LLVM compiler:
$ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 8.0.0 (clang-800.0.38)
Target: x86_64-apple-darwin15.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
$ llvm-gcc --version
Apple LLVM version 8.0.0 (clang-800.0.38)
Target: x86_64-apple-darwin15.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
$ clang --version
Apple LLVM version 8.0.0 (clang-800.0.38)
Target: x86_64-apple-darwin15.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
$ cc --version
Apple LLVM version 8.0.0 (clang-800.0.38)
Target: x86_64-apple-darwin15.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/binThe first two alias' gcc and llvm-gcc are confusing and a bit misleading as their not GNU's version. They're still the LLVM's compiler but in the first instance (gcc) the compiler is configured to use some additional libraries that are provided by c++
It's worth noting that even with a plain C file all these alias' work to compile the source code into an executable. It's just they allow you to utilise additional extensions not provided by the standard c language.
LLVM's licensing is BSD, meaning Apple can embed it within their own software that is not GPL-licensed. Typically LLVM's compiler is faster, but in some cases might not support all the same targets as GNU's.
For more comparison details see http://clang.llvm.org/comparison.html
#include <stdio.h> // pre-processor directive to include code file at compile time
#define NAME "World" // pre-processor directive to substitute any reference to NAME _before_ compilation
// returns an int type and takes in no arguments (void)
int main(void) {
printf("hello %s", NAME); // can't use single quotes
return 0; // zero indicates no problems
}It's important to note that the directives
#includeand#defineare 'processed' at the start of the compilation process. This is at the request of the compiler. It'll be one of the compiler's first steps (to pull in the preprocessor and have it ensure the file is setup ready for the reset of the compilation)
You compile it like so:
cc hello-world.c -o hwNow you have a macOS compatible executable:
./hw # prints the message "Hello World"To cross-compile for another OS (e.g. Linux) then use Docker or a VM
See here for understanding RAM and bits
In C you can define an integer to be either signed or unsigned. The former means the number can be both negative and positive as well as zero.
So typically, if a number is negative, you'll prefix it with -. If the number is positive, then it is just the number. For example, -1 and 1.
You don't need to explicitly provide the signed keyword (e.g. signed int <var_name>), it's just implied.
The latter (unsigned) is an integer that can only be positive. So if you need to store an integer and you know the value will always be zero or positive, then you can define it as being unsigned and the compiler can make appropriate optimisations based on that understanding.
So although the underlying memory allocation is the same for signed or unsigned, the actual values represented are slightly different in that unsigned allows for storing values that are twice the size of signed, because half of signed's values have to account for negatives.
We saw in the above 'Hello World' example the use of the directive #define which allowed us to use a single identifier (NAME in this case) throughout our program. The benefit is that we can change the value once and have it updated everywhere.
But do not get this confused with a variable. It is not. This is just a sequence of characters that are blindly replaced at the pre-processing stage. The value assigned to NAME will be replaced inside your program regardless of whether it's valid code or not. Meaning it could cause the compiler to error in an unclear way.
On the other hand you can define a proper constant like so:
#include <stdio.h>
int main(void) {
const char NAME[] = "World";
printf("Hello %s", NAME);
return 0;
}What this gives you is a variable that has an actual type assigned to it. Meaning the compiler will help you identify an incorrect value if necessary much more easily than using the #define directive.
In C single quotes denote a char type and double quotes denote a string.
So if you had the following code:
char foo = 'a';
printf("foo: %s\n", foo);It would error with:
format specifies type 'char *' but the argument has type 'char'
To get it to work you need to provide the memory address location of foo using the address-of operator (&):
char foo = 'a';
printf("foo: %s\n", &foo);We'll come back to the & operator later when discussing pointers.
When creating a variable, and assigning a string to it, the value assigned is really a pointer to a location in memory.
The char type is used when storing characters such as 'a' or full strings "abc".
Remember that when assigning a string, the value assigned is actually a pointer to an array. Even if the string you provide is just one character (although in that case maybe you should use single quotes which represent a char and not a string).
This is because of what's known as the null terminator.
Consider the following code:
char my_string[2] = "a";The reason we specify a length of 2 is because the underlying array that my_string is being pointed towards looks like this:
["a", "\0"] // yes it has two elementsThe last element is known as the null terminator. When this data is stored in memory, we can now start at the location in memory where it is stored (its address) and then step through memory until we reach the null terminator; where we'll then find the end of the string.
The following code shows how to print the location in memory:
char foo = 'a';
printf("address of foo: %p\n", (void *)(&foo));Here's a 5 minute guide to pointers and address-of vs value-of operators, be warned it get get hairy pretty quickly