This commit is contained in:
Miloslav Ciz 2024-03-27 23:15:52 +01:00
parent 099fca51b7
commit 860b62cc6a
10 changed files with 2197 additions and 1746 deletions

View file

@ -1,10 +1,14 @@
# C Tutorial
{ Still a work in progress, but 99% complete. ~drummyfish }
{ Constant work in progress, mostly done but may still have some bugs. ~drummyfish }
This is a relatively quick [C](c.md) tutorial.
You should probably know at least the completely basic ideas of programming before reading this (what's a [programming language](programming_language.md), [source code](source_code.md), [command line](cli.md) etc.). If you're as far as already knowing another language, this should be pretty easy to understand.
You should probably how at least some basic awareness of essential programming concepts before reading this (what's a [programming language](programming_language.md), [source code](source_code.md), [command line](cli.md) etc.). If you're as far as already somewhat knowing another language, this should be pretty easy to understand.
This tutorial focuses on teaching pure C, i.e. **mostly just command line text-only programs**. There is a small bonus that shows some very basics of doing graphics programming at the end, but bear in mind it's inevitable to learn step by step, as much as you want to start programming graphical games, you first HAVE TO learn the language itself well. Don't rush it. Trust this advice, it is sincere.
If you do two chapters a day (should take like half and hour), in a week you'll know some basic C.
## About C And Programming
@ -507,7 +511,7 @@ Another local variable is `number` -- it is a local variable both in `main` and
And a last thing: keep in mind that not every command you write in C program is a function call. E.g. control structures (`if`, `while`, ...) and special commands (`return`, `break`, ...) are not function calls.
## More Details (Globals, Switch, Float, Forward Decls, ...)
## More Details (Globals, Switch, Float, Forward Decls, Program Arguments, ...)
We've skipped a lot of details and small tricks for simplicity. Let's go over some of them. Many of the following things are so called [syntactic sugar](sugar.md): convenient syntax shorthands for common operations.
@ -703,6 +707,41 @@ which prints
The functions `printDecorated1` and `printDecorated2` call each other, so this is the case when we have to use a forward declaration of `printDecorated2`. Also note the condition `if (fancy)` which is the same thing as `if (fancy != 0)` (imagine `fancy` being 1 and 0 and about what the condition evaluates to in each case).
And one more important thing: our program as a whole can be passed parameters when it's executed, which inside the program we can access as so called **command line arguments** (also known as *flags*, *switches* etc.). This is important especially under [Unix](unix.md) operating systems where we run programs from command line and where programs often work in non-interactive ways and are composed into bigger programs (similarly to how we compose small C functions into one big program); command line arguments are similar to arguments we pass to functions, they can inform our program to behave in certain way, for example to open a certain config file at start, to run in fullscreen mode, to print help and so on. When we compile our programs with the gcc compiler, e.g. like `gcc -o myprogram myprogram.c`, all the text after `gcc` are in fact arguments telling gcc which program to compile, how to compile it, how to name the output and so on. To allow our program to receive these arguments we add two parameters to the `main` function, one called `argc` (argument count) of integer type, saying how many arguments we get, and another called `argv` (argument [vector](vector.md)) of pointer to a pointer to char type (please don't get scared), holding an array of strings (each argument will be of string type). Operating system will automatically fill these arguments in when our program is started. Here is a short example demonstrating this:
```
#include <stdio.h>
int main(int argc, char **argv)
{
puts("You passed these arguments:");
for (int i = 0; i < argc; ++i)
printf("- \"%s\"\n",argv[i]);
return 0;
}
```
If you compile this program and run it e.g. like
```
./program hello these are "my arguments"
```
The program will output:
```
You passed these arguments:
- "./program"
- "hello"
- "these"
- "are"
- "my arguments"
```
Things to notice here are following: we passed 4 arguments but got 5 -- the first argument is the path of our program itself, i.e. we will always get at least this argument. Then we also see that our arguments are separated by spaces, but if we put them into double quotes (like the last one), it will become just one argument, keeping the spaces (but not the quotes). For now this knowledge will suffice, you will most definitely encounter command line arguments in real programs -- now you know what they are.
## Header Files, Libraries, Compilation/Building
So far we've only been writing programs into a single source code file (such as `program.c`). More complicated programs consist of multiple files and libraries -- we'll take a look at this now.
@ -1679,7 +1718,414 @@ The very basic thing we can do is to turn on automatic optimization with a compi
## Final Program
TODO
Now is the time to write a final program that showcases what we've learned, so let's write a quite simple but possibly useful [hex viewer](hex_editor.md). The program will allow us to interactively inspect bytes in any file, drawing their hexadecimal values along with their addresses, supporting one character commands to move within the file. If you want, you can take it and improve it as an exercise, for example by adding more viewing modes (showing decimal octal or ASCII values), maybe even allowing editing and saving the file. Here is the source code:
```
/* Simple interactive hex file viewer. */
#include <stdio.h>
#include <stdlib.h>
#define ROWS 10 // how many rows and columns to print at one screen
#define COLS 16
unsigned char *fileContent = NULL; // this will contain the loaded file
unsigned long long fileSize = 0; // size of fileContent in bytes
unsigned long long fileOffset = 0; // current offset within the file
const char *fileName = NULL;
// Loads file with given name into fileContent, returns 1 on success, 0 on fail.
int loadFile(const char *fileToOpen)
{
FILE *file = fopen(fileToOpen,"rb");
if (file == NULL)
return 0;
fseek(file,0L,SEEK_END); // get to the end of the file
fileSize = ftell(file); // our position now says the size of the file
rewind(file); // get back to start of the file
fileContent = malloc(fileSize); // allocate memory to load the file into
if (fileContent == NULL)
{
fclose(file); // don't forget to close the file
return 0;
}
if (fread(fileContent,1,fileSize,file) != fileSize)
{
fclose(file);
free(fileContent);
return 0;
}
fclose(file);
return 1;
}
// Call when loaded file is no longer needed.
void unloadFile(void)
{
free(fileContent); // free the allocated memory
}
// Draws the current screen, i.e. hex view of the file at current offset.
void drawScreen(void)
{
for (int i = 0; i < 80; ++i) // scroll the old screen our of the view
putchar('\n');
printf("%s: %llu / %llu\n\n",fileName,fileOffset,fileSize);
unsigned long long offset = fileOffset;
for (int i = 0; i < ROWS * COLS; ++i)
{
if (offset % COLS == 0)
printf("%04X ",(int) offset);
if (offset < fileSize)
printf("%02X ",fileContent[offset]);
else
printf(".. ");
offset++;
if (offset % COLS == 0) // break line after each COLS values
putchar('\n');
}
}
int main(int argc, char **argv)
{
if (argc < 2)
{
puts("ERROR: please pass a file to open");
return 1;
}
fileName = argv[1]; // (argv[0] is the name of our program, we want argv[1])
if (!loadFile(fileName))
{
printf("ERROR: couldn't open the file \"%s\"\n",fileName);
return 1;
}
int goOn = 1;
while (goOn) // the main interactive loop
{
drawScreen();
puts("\ntype command (w = end, s = start, a = back, d = next, q = quit)");
char userInput = getchar();
switch (userInput)
{
case 'q':
goOn = 0;
break;
case 's':
if (fileOffset + COLS < fileSize)
fileOffset += COLS;
break;
case 'w':
if (fileOffset >= COLS)
fileOffset -= COLS;
break;
case 'a':
fileOffset = 0;
break;
case 'd':
fileOffset = ((fileSize - COLS) / COLS) * COLS; // aligns the offset
break;
default:
puts("unknown command, sorry");
break;
}
}
unloadFile();
return 0;
}
```
To add a few comments: the program opens a file whose name it gets passed as a command line argument, so it is used as: `./hexview myfile`. We try to correctly perform all safety checks, e.g. if we actually get passed the file name, if we manage to open it and so on. Our program (a bit inefficiently) loads the whole file into memory (advanced programs only load parts of the file) -- for this it first checks the file size, allocates sufficient memory for it with `malloc` (also checking for errors) and loads it there. Then we have a function to draw the current file view and inside the main program body we have an interactive loop that loads and handles user commands and issues the view drawing. That is basically it!
## Bonus: Introduction To Graphics (ASCII, PPM, SDL2, ...)
Let's stress you should only get into graphics after you've written several purely command-line programs and are comfortable with the language. Don't try graphics programming until you can easily work with 2D arrays, structs and so on. [Graphics](graphics.md) is a huge topic of its own, there is so much we can't cover here, remember this is just a quick, basic starting point for making pictures with C.
For start please note that:
- **C itself doesn't know anything about graphics**. C is just trying to be a good programming language, it leaves the vast area of graphics for others to solve, therefore though you can try to avoid it (see below), typically you will use a third party [library](library.md) to draw some real pixels to the screen, there isn't a universal way of doing it, you have to choose specific solution based on what you want to achieve, what's available etc.
- **By graphics we really just mean drawing [pixels](pixel.md)**. Things like keyboard and mouse [input](io.md) (which you need for anything [interactive](interactivity.md) like [games](game.md)), loading [PNG](png.md) pictures, playing sounds, loading and displaying 3D models, detecting [collisions](collision.md) of virtual objects and so on won't necessarily be covered here, it's all too much to learn at once. We will ONLY be trying to show basic shapes on the screen.
- We'll be doing things in simplified ways here, omitting common [optimizations](optimization.md), safety checks and so on. Just know that in practice things will be yet a bit more complex.
So, how to actually do graphics? As said, graphics is a super wide topic, there is no [silver bullet](silver_bullet.md), all depends on what we really need. Consider the following:
- **Need to quickly draw something quite basic (e.g. a graph)? [Keep it simple](kiss.md) and just use [ASCII art](ascii_art.md).** You can draw simple pictures to the console with ASCII art, i.e. you emulate real screen pixels with text characters. This is a nice, natural transition from text to graphics when studying programming, so you may start with this. The disadvantage is you can only draw very simple, rough and low resolution pictures, usually without colors, but you can animate them and make your program a little bit interactive. By doing things yourself you'll also get an idea what graphics is about and will better understand why libraries you'll use later work the way they do. A big advantage is that ASCII art graphics can be done without any library (there are libraries like [ncurses](ncurses.md), but you probably won't need them) and will keep your program quite simple, nice and [portable](portability.md). You can use this for simple visualization, animations, games and so on.
- **Need to just produce one or two static pictures (e.g. function plot)? Output a picture file**. You can make a C program that will simply save a picture to a file which you can open in any image viewer. For this you can use quite simple libraries but it is also possible to load and save simple formats without any libraries at all! You can very easily export bitmap images (e.g. [PPM](ppm.md), [farbfeld](farbfeld.md), ...) as well as beautiful [vector](vector.md) images (e.g. by exporting [SVG](svg.md)) with curves, [antialiasing](antialiasing.md), fancy fonts and so on, you can auto-convert them with other tools to other formats and so on. This will suffice for many things like data visualizations, function plots, photo processing, even 3D rendering, while keeping your program highly [portable](portability.md), i.e. it will be usable everywhere, even on computers without any GUI or screen, it will be much less [bloated](bloat.md).
- **Need a fast, real time interactive program (e.g. a game)? Use a [library](library.md) for that**. If you want the "real deal", i.e. interactive, fully colorful high-res graphics, e.g. for a serious game, you'll typically have to use a library -- in C this library is traditionally [SDL2](sdl.md) (but there are many alternatives, e.g. [SFML](sfml.md), [Allegro](allegro.md), [SAF](saf.md), ...). This is a bit more complex, so only go this way if you really must -- you have to install the library, learn to use it and your program will become more bloated, less portable, bigger in size, harder to compile and so on.
We will show an example of each of these approaches further on.
But first let's quickly mention what graphics programming at this level is essentially about, i.e. the kind of "workflow" we'll always try to implement:
- The most essential thing is basically to be able to **draw a [pixel](pixel.md)**, i.e. set a [color](color.md) of one point in the picture. Once you can draw a single pixel, you can draw anything, just like to build any kind of house you have to be able to lay bricks -- every shape is just some formation of pixels that you can construct with C code: a [line](line.md) is just a series of pixels one next to another, [cricle](circle.md) is a curved line, rectangle is just area filled with pixels of some color and so on. So at the beginning we'll just have some way of drawing a single pixel. Typically this can be e.g. a function `drawPixel(x,y,color)` -- graphic libraries will normally offer you a function like this, letting you draw pixels without actually caring about what magic is going on inside the function. (Sometimes you will also encounter a lower level way in which the library maps a screen to memory and you will draw pixels by literally writing values to memory, i.e. with pointers or arrays.)
- With the basic pixel drawing function we'll draw our picture however we want -- if we're using a library, there may be helper functions and of course we can write our own functions too, for example `drawLine(fromX,fromY,toX,toY,color)`, `drawText(x,y,text,size,color)` and so on. The picture itself is just a virtual canvas, a computer memory holding numbers, typically a two dimensional [array](array.md) whose values are manipulated by the `drawPixel` function. At this point we are doing nothing else than changing values in memory.
- At the end, once drawing is complete, we have to **show (*present*) the picture**. This is to say that when we're drawing, the picture isn't actually seen, it is only changing in memory, it is shown to the user only when it's completed, i.e. when we issue a special command such as `drawingDone()`. Why can't the picture just be shown at all times? In theory it can, but you encounter problems, imagine e.g. a game that quickly redraws the picture on the screen -- here the user would see flickering, he might even see enemies show briefly behind a wall before the wall is actually drawn and so on. So a way to solve this is to do the drawing off screen and only at the end say "now we're done drawing, show the image" (for more details see [double buffering](double_buffering.md)).
- Also note that usually there is some kind of management around graphic code, i.e. some initialization of the program's window, setting its resolution, allocation of memory for the screen pixels, setting the pixel formats, [callbacks](callback.md) and so on. Similarly at the end you often have to clean things up and as many graphic systems are based on events, you have to periodically check events like key presses, window resizes etc. Interactive programs will furthermore have an infinite loop (so called *game loop*) in which they check events, redraw the screen, wait for a while (to keep the right [FPS](fps.md)) and so on. Libraries try to do many thing for you but you have to at least tell them some very basic things. So be prepared for a lot extra code.
Now let's finally do this. We'll set up some basic code for drawing a rectangle and try to draw it with different approaches.
The ASCII approach:
```
#include <stdio.h>
#define SCREEN_WIDTH 60
#define SCREEN_HEIGHT 25
char screen[SCREEN_WIDTH * SCREEN_HEIGHT]; // our virtual screen
// sets a single pixel at given coordinates
void drawPixel(int x, int y, char pixel)
{
int index = y * SCREEN_WIDTH + x;
if (index >= 0 && index < SCREEN_WIDTH * SCREEN_HEIGHT)
screen[index] = pixel;
}
// presents the drawn picture on the screen
void drawScreen(void)
{
for (int i = 0; i < 30; ++i) // shift old picture out of view
putchar('\n');
const char *p = screen;
for (int y = 0; y < SCREEN_HEIGHT; ++y)
{
for (int x = 0; x < SCREEN_WIDTH; ++x)
{
putchar(*p);
p++;
}
putchar('\n');
}
}
// fills rectangle with given pixel value
void drawRectangle(int x, int y, int width, int height, char pixel)
{
for (int j = 0; j < height; ++j)
for (int i = 0; i < width; ++i)
drawPixel(x + i,y + j,pixel);
}
int main(void)
{
int quit = 0;
int playerX = SCREEN_WIDTH / 2, playerY = SCREEN_HEIGHT / 2;
while (!quit) // main game loop
{
drawRectangle(0,0,SCREEN_WIDTH,SCREEN_HEIGHT,'.'); // clear screen with dots
drawRectangle(playerX - 2,playerY - 1,5,3,'X'); // draw player
drawScreen(); // present the picture
puts("enter command (w/s/a/d/q):");
char input = getchar();
switch (input)
{
case 'w': playerY--; break;
case 's': playerY++; break;
case 'a': playerX--; break;
case 'd': playerX++; break;
case 'q': quit = 1; break;
}
}
return 0;
}
```
With this we have a simple interactive program that draws a dotted screen with rectangle that represents the player, you can compile it like any other program, it uses no external libraries. User can move the rectangle around by typing commands. There is a main infinite loop (this is the above mentioned *game loop*, a typical thing in interactive applications) in which we read the user commands and redraw the picture on the screen. Notice we have our basic `drawPixel` function as well as the `drawScreen` function for presenting the finished picture, we also have a helper `drawRectangle` function. The `screen` array represents our virtual picture (it is declared as one dimensional array but in reality it is treated as two dimensional by the `setPixel` function). As an exercise you can try to draw other simple shapes, for example horizontal and vertical lines, non-filled rectangles -- if you're brave enough you can also try a filled circle (hint: points inside a circle mustn't be further away from the center than the circle radius).
Now let's try to do something similar, but this time creating a "real picture" made of true pixels, exported to a file:
```
#include <stdio.h>
#define SCREEN_WIDTH 640 // picture resolution
#define SCREEN_HEIGHT 480
unsigned char screen[SCREEN_WIDTH * SCREEN_HEIGHT * 3]; // screen, 3 is for RGB
// sets a single pixel at given coordinates
void drawPixel(int x, int y, int red, int green, int blue)
{
int index = y * SCREEN_WIDTH + x;
if (index >= 0 && index < SCREEN_WIDTH * SCREEN_HEIGHT)
{
index *= 3;
screen[index] = red;
screen[index + 1] = green;
screen[index + 2] = blue;
}
}
// outputs the image in PPM format
void outputPPM(void)
{
printf("P6 %d %d 255\n",SCREEN_WIDTH,SCREEN_HEIGHT); // PPM file header
for (int i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT * 3; ++i)
putchar(screen[i]);
}
// fills rectangle with given pixel
void drawRectangle(int x, int y, int width, int height, int red, int green,
int blue)
{
for (int j = 0; j < height; ++j)
for (int i = 0; i < width; ++i)
drawPixel(x + i,y + j,red,green,blue);
}
int main(void)
{
drawRectangle(0,0,SCREEN_WIDTH,SCREEN_HEIGHT,128,128,128); // clear with grey
drawRectangle(SCREEN_WIDTH / 2 - 32, SCREEN_HEIGHT / 2 - 32,64,64,255,0,0);
outputPPM();
return 0;
}
```
Wow, this is yet simpler! Although we have no interactivity now, we get a nice picture of a red rectangle on grey background, and don't even need any library (not even the file library!). We just compile this and save the program output to a file, e.g. with `./program > picture.ppm`. The picture we get is stored in [PPM](ppm.md) format -- a very simple format that basically just stores raw [RGB](rgb.md) values and can be opened in many viewers and editors (e.g. [GIMP](gimp.md)). Notice the similar functions like `drawPixel` -- we only have a different parameter for the pixel (in ASCII example it was a single ASCII character, now we have 3 color values: red, green and blue). In the main program we also don't have any infinite loop, the program is non-interactive.
And now finally to the more complex example of a fully interactive graphic using SDL2:
```
#include <SDL2/SDL.h> // include SDL library
#define SCREEN_WIDTH 640
#define SCREEN_HEIGHT 480
#define COLOR_WHITE 0xff // some colors in RGB332 format
#define COLOR_RED 0xe0
unsigned char _SDL_screen[SCREEN_WIDTH * SCREEN_HEIGHT];
SDL_Window *_SDL_window;
SDL_Renderer *_SDL_renderer;
SDL_Texture *_SDL_texture;
const unsigned char *_SDL_keyboardState;
int sdlEnd;
static inline void drawPixel(unsigned int x, unsigned int y, unsigned char color)
{
if (x < SCREEN_WIDTH && y < SCREEN_HEIGHT)
_SDL_screen[y * SCREEN_WIDTH + x] = color;
}
void sdlStep(void)
{
SDL_Event event;
SDL_UpdateTexture(_SDL_texture,NULL,_SDL_screen,SCREEN_WIDTH);
SDL_RenderClear(_SDL_renderer);
SDL_RenderCopy(_SDL_renderer,_SDL_texture,NULL,NULL);
SDL_RenderPresent(_SDL_renderer);
while (SDL_PollEvent(&event))
if (event.type == SDL_QUIT)
sdlEnd = 1;
SDL_Delay(10); // relieve CPU for 10 ms
}
void sdlInit(void)
{
SDL_Init(0);
_SDL_window = SDL_CreateWindow("program",SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH,SCREEN_HEIGHT,SDL_WINDOW_SHOWN);
_SDL_renderer = SDL_CreateRenderer(_SDL_window,-1,0);
_SDL_texture = SDL_CreateTexture(_SDL_renderer,SDL_PIXELFORMAT_RGB332,
SDL_TEXTUREACCESS_STATIC,SCREEN_WIDTH,SCREEN_HEIGHT);
_SDL_keyboardState = SDL_GetKeyboardState(NULL);
SDL_PumpEvents();
}
void sdlDestroy(void)
{
SDL_DestroyTexture(_SDL_texture);
SDL_DestroyRenderer(_SDL_renderer);
SDL_DestroyWindow(_SDL_window);
}
int sdlKeyPressed(int key)
{
return _SDL_keyboardState[key];
}
void drawRectangle(int x, int y, int width, int height, unsigned char color)
{
for (int j = 0; j < height; ++j)
for (int i = 0; i < width; ++i)
drawPixel(x + i,y + j,color);
}
int main(void)
{
int playerX = SCREEN_WIDTH / 2, playerY = SCREEN_HEIGHT / 2;
sdlInit();
while (!sdlEnd) // main loop
{
sdlStep(); // redraws screen, refreshes keyboard etc.
drawRectangle(0,0,SCREEN_WIDTH,SCREEN_HEIGHT,COLOR_WHITE);
drawRectangle(playerX - 10,playerY - 10,20,20,COLOR_RED); // draw player
// update player position:
if (sdlKeyPressed(SDL_SCANCODE_D))
playerX++;
else if (sdlKeyPressed(SDL_SCANCODE_A))
playerX--;
else if (sdlKeyPressed(SDL_SCANCODE_W))
playerY--;
else if (sdlKeyPressed(SDL_SCANCODE_S))
playerY++;
}
sdlDestroy();
return 0;
}
```
This program creates a window with a rectangle that can be moved with the WSAD keys. To compile it you first need to install the SDL2 library -- how to do this depends on your system (just look it up somewhere; on Debian like systems this will typically be done with `sudo apt-get install libsdl2-dev`), and then you also need to link SDL2 during compilation, e.g. like this: `gcc -O3 -o graphics_sdl2 -lSDL2 graphics_sdl2.c`.
This code is almost a bare minimum template for SDL that doesn't even perform any safety checks such as validating creation of each SDL object (which in real code SHOULD be present, here we left it out for better clarity). Despite this the code is quite long with a lot of [boilerplate](boilerplate.md); that's because we need to initialize a lot of stuff, we have to create a graphical window, a texture to which we will draw, we have to tell SDL the format in which we'll represent our pixels, we also have to handle operating system events so that we get the key presses, to know when the window is closed and so on. Still in the end we are working with essentially the same functions, i.e. we have `drawPixel` (this time with pixel as a single char value because we are using the simple [332](rgb332.md) format) and `drawRectangle`. This time `sdlStep` is the function that presents the drawn image on screen (it also does other things like handling the SDL events and pausing for a while to not heat up the CPU).
## Where To Go Next