﻿# C basics: pointers, strings and files

> The C under everything else: argc/argv, pointers, strings that end in a 0 byte, and the two ways to a file – buffered fopen/fgets and raw open/read/write – with the loop conditions that keep them correct.

Source: https://laszlonemes.com/university/operating-systems/c-basics-files · Author: László Nemes

Session 1 · Operating Systems

The C under everything else: argc/argv, pointers, strings that end in a 0 byte, and the two ways to a file – buffered fopen/fgets and raw open/read/write – with the loop conditions that keep them correct.

20 min read `argc, argv and printf formats` `Pointers and the terminating 0` `fopen/fgets vs open/read/write` `File descriptors and permission bits` `lseek, binary records and stat`

This course talks to the operating system from C, so the first session makes sure the C under it is solid. Three things cause most of the trouble later: **pointers** (an address is just a number, and the compiler trusts you with it), **strings** (there is no string type – only arrays of characters that end in a 0 byte), and **files**, which C reaches in two different ways: the buffered C library and the raw system calls.

The examples are small programs that print their arguments, play with a pointer, cut a string in half, print and copy files, and ask a file when it was last modified. The same pieces – a struct, a text file of records, careful reading – make up the first take-home assignment (beadandó).

> **The model in one breath:** a variable lives at an address, a pointer stores such an address, a string is a char array up to the first 0, and a file is reached either through a buffered `FILE *` or through a small integer, the file descriptor – and every call that can fail returns something you have to check.

After this session you can

- Read command-line arguments through argc and argv, and check argc before touching argv.
- Explain what & and * do, and why a char pointer does not hold any text by itself.
- Handle strings with their terminating 0: length, copy, compare, and buffer sizes.
- Read a file line by line with fopen/fgets and copy one with open/read/write – with correct loop conditions.
- Create a file with the right permission bits, move inside it with lseek, and read its metadata with stat.

## main, argc and argv

A C program starts in `main`. When it is started from the shell, it gets the words of the command line as two parameters: `argc` (argument count) is how many there are, and `argv` (argument vector) is an array of strings with the words themselves. The type `char **argv` reads as "a pointer to pointers to char" – in practice, an array of strings (Fig. 1).

> **Fig. 1** · argv is an array of pointers. The program name is argv[0], so argc is one more than the number of real arguments.
>
> *Diagram:* For the command ./copy a.txt b.txt, argc is 3 and argv is an array of pointers: argv[0] points to the program name, argv[1] and argv[2] to the arguments, each a character array ending in 0, and argv[argc] is NULL.

1_sec.c – printing every argument

```c
printf("Number of command line arguments: %i\n", argc);
for (int i = 0; i < argc; i++)
{
    printf("%i. argument is %s\n", i, argv[i]);
}
```

```text
$ ./args a "b c"
Number of command line arguments: 3
0. argument is ./args
1. argument is a
2. argument is b c
```

Two details are visible in the output. `argc` counts arguments, not characters, and it includes the program name: `./args a "b c"` gives 3. And the shell, not your program, splits the line: the quotes kept `b c` together as one argument and were removed before `main` ran.

Common mistake

**Reading argv[2] when there is no argv[2]**

`argv[argc]` is `NULL`, and anything beyond it is not yours. A program that does `atoi(argv[2])` and is started without arguments reads memory it should not – usually a crash (segmentation fault). Check `argc` first, and print a usage line to `stderr`:

```c
if (argc != 3)
{
    fprintf(stderr, "Usage: %s copy_from copy_to\n", argv[0]);
    exit(1);
}
int count = atoi(argv[2]);   // "5" -> 5; a string that is not a number gives 0
```

### printf formats

`printf` takes a format string and then the values. The compiler cannot fix a mismatch for you – it only warns with `-Wall` – so the conversion must match the type:

```c
int n = 6;
char c = 'A';
printf("%i %d\n", n, n);        // 6 6   – an int, two names for the same thing
printf("%c %i\n", c, c);        // A 65  – a char is a small number (its code)
printf("%s\n", argv[0]);        // a string (char *)
printf("%p\n", (void *)&n);     // an address
printf("%zu\n", strlen("abc")); // 3     – size_t, what strlen and sizeof give back
```

| Conversion | Type | Note |
| --- | --- | --- |
| `%i` | `int` | Same as `%d` in `printf`. In `scanf`, `%i` also accepts hex (`0x1F`) and octal (`017`) input. |
| `%u` | `unsigned int` | Never negative. |
| `%c` | `char` | One character. A char is a small integer: `'A'` is 65, so `%i` prints 65. |
| `%s` | `char *` | Prints characters up to the terminating 0. |
| `%p` | `void *` | An address, in hex. Cast other pointers: `(void *)&n`. |
| `%zu` | `size_t` | What `strlen` and `sizeof` return. `%i` is wrong here. |

Good to know

**Compile with warnings on**

`gcc -Wall -Wextra file.c -o prog` reports format mismatches, missing headers and unused variables. A missing `#include <unistd.h>` before `read` or `close` is only a warning ("implicit declaration"), but the program then calls a function the compiler knows nothing about. Treat these warnings as errors. The details are in the [compiling guide](https://laszlonemes.com/university/operating-systems/compiling-and-running).

## Pointers and addresses

Every variable sits somewhere in memory, at an **address**. `&n` gives the address of `n`; a **pointer** is a variable that stores such an address. Its type says what is at the address: an `int *` points to an `int`. Putting `*` in front of a pointer **dereferences** it – follows the address to the value (Fig. 2).

> **Fig. 2** · k stores the address of n. &n reads an address, *k follows one.
>
> *Diagram:* A pointer is a variable that stores an address. int *k = &n stores the address of n in k; *k follows that address and reads or writes n itself.

2_pointers.c – the lines that matter

```c
int n = 25;
int *k = &n;                      // & gives the address of n; k stores it

printf("%p\n", (void *)&n);      // the address of n
printf("%p\n", (void *)k);       // the same address
printf("%i\n", *k);              // 25 – * follows the pointer to the value
*k = 30;                          // writes n through the pointer: n is 30 now
```

Pointers are how a C function changes something that belongs to its caller. C passes every argument by value – the function gets a copy – so `scanf("%u", &exp)` needs the address of `exp` to be able to write into it. Every `read`, `fread` and `stat` call below works the same way: you hand over the address of your variable, and the call fills it.

- **address** – The position of a byte in the memory of the process, printed with %p.
- **pointer** – A variable that holds an address. Its type (int *, char *) says what lives there.
- **& (address-of)** – Gives the address of a variable: &n.
- *** (dereference)** – Follows a pointer to the value it points to: *k.
- **NULL** – A pointer that points nowhere. Functions return it to say "failed" or "nothing".

## Strings: char arrays ending in 0

C has no string type. A string is an array of `char`, and it ends at the first byte with value 0 ( written `0` or `'\0'`). Everything that works with strings – `printf("%s")`, `strlen`, `strcpy` – walks the array until it finds that 0. The array can be longer than the string; whatever comes after the 0 is ignored (Fig. 3).

> **Fig. 3** · One 0 byte shortens the string: "Hello world" becomes "Hello", and "world" stays in memory as leftover bytes.
>
> *Diagram:* A C string is a char array that ends at the first 0 byte. char str1[] = "Hello world" takes 12 bytes. After str1[5] = 0, printf and strlen see only Hello; the bytes of world are still in memory, just no longer part of the string.

3_strings.c – cutting a string

```c
char str1[] = "Hello world";    // 12 bytes: 11 characters + the terminating 0
str1[5] = 0;                     // overwrite the space with 0
printf("'%s'\n", str1);         // 'Hello' – printing stops at the first 0
printf("%c\n", str1[6]);        // w – the rest is still in memory
```

That is also how `strlen` works inside. This hand-written version walks the pointer until it reaches the 0:

```c
int length(char *str)
{
    int l = 0;
    while (*str++ != 0)   // read a character, step the pointer to the next one
        l++;
    return l;             // the terminating 0 is not counted
}
```

### A pointer is not a copy

A `char *` variable holds an address and nothing else. `str2 = str1` makes it point to the same array – change a character through one name and the other sees it. To have text of its own, the pointer needs memory: `malloc` reserves a block on the **heap**, which stays until you `free` it, while `str1` is an ordinary local array on the **stack**, which disappears when the function returns (Fig. 4).

> **Fig. 4** · str2 = str1 shares the array; str2 = malloc(80) gives str2 a block of its own on the heap.
>
> *Diagram:* A char pointer owns no text. str2 = str1 copies only the address, so both names reach the same array; str2 = malloc(80) points str2 at a new block on the heap, which strcpy can fill and free must give back.

```c
char *str2 = str1;               // copies the address, not the text
str2 = malloc(80 * sizeof(char));  // a new, uninitialised block on the heap
if (str2 == NULL)
{
    perror("malloc");
    exit(1);
}
strcpy(str2, "New content");     // copies the characters AND the terminating 0
free(str2);                      // give it back – exactly once
str2 = NULL;
```

The functions of `string.h` you will use every week:

```c
strlen(s)          // number of characters before the 0 (size_t)
strcpy(dst, src)   // copy src into dst – dst must be big enough!
strcat(dst, src)   // append src to the end of dst – same warning
strcmp(a, b)       // 0 if equal, <0 / >0 for alphabetical order
snprintf(dst, sizeof(dst), "%s-%i", name, id)   // formatted, never overflows
```

Common mistake

**strcpy into a buffer that is too small**

`strcpy` and `strcat` do not know how big the destination is. Copying 60 characters into a `char name[50]` writes past the end and corrupts whatever is next in memory – often another field of the same struct. Size every buffer for the longest input plus one byte for the 0, and limit what you read: `fgets(buf, sizeof(buf), …)`, `scanf("%49s", buf)`, or `snprintf`.

## Two ways to a file

C programs on Linux can reach a file at two levels (Fig. 5). The **C library** (`stdio.h`) gives you `fopen`, `fgets`, `fprintf` and `fread`, which work on a `FILE *` and keep a buffer inside your process. The **system calls** (`fcntl.h`, `unistd.h`) – `open`, `read`, `write`, `close` – ask the kernel directly and work on an `int`, the file descriptor. `fopen` calls `open` itself; the library is a convenience layer on top.

> **Fig. 5** · stdio collects data in a buffer and calls the kernel rarely; the system calls go to the kernel every time.
>
> *Diagram:* There are two ways to a file. The stdio functions (fopen, fgets, fprintf, fread) work on a FILE * and collect data in a buffer inside your process; the system calls (open, read, write, close) go straight to the kernel with a file descriptor. stdio calls read and write itself when its buffer needs it.

|  | stdio (C library) | system calls |
| --- | --- | --- |
| Handle | `FILE *` | `int` file descriptor |
| Open / close | `fopen(name, "r")` / `fclose` | `open(name, O_RDONLY)` / `close` |
| Text | `fgets`, `fprintf`, `fscanf`, `fgetc` | none – only bytes |
| Bytes | `fread`, `fwrite` | `read`, `write` |
| Position | `fseek`, `ftell`, `rewind` | `lseek` |
| Buffering | yes – fewer kernel calls, output appears later | no – every call is a kernel call |
| Errors | `NULL` / `EOF`, then `perror` | `-1`, then `perror` |

Buffering is why stdio is fast for many small reads and writes, and why it is sometimes the wrong tool: with pipes between processes (later in the course) you want data to leave immediately, not when a buffer fills up. The buffer will also bite you in [the next session](https://laszlonemes.com/university/operating-systems/processes-fork), when `fork` copies it.

### File descriptors

A file descriptor is a small integer: an index into the process's table of open files (Fig. 6). Three entries are open before `main` starts – 0 standard input, 1 standard output, 2 standard error – and `open` always returns the lowest free number, so the first file you open is usually 3. When a call fails it returns `-1` and sets the global `errno`; `perror("open")` prints your text plus the reason, such as `open: No such file or directory`.

> **Fig. 6** · 0, 1 and 2 are inherited from the shell; the files you open get the next free numbers.
>
> *Diagram:* A file descriptor is an index into the process's table of open files. 0, 1 and 2 are open from the start (standard input, output and error); open returns the lowest free number, so the first file you open is usually 3.

Good to know

**perror only after a failed call**

`perror` describes the last failed system call. Use it right after `open`, `read` or `fopen` returned an error. For your own checks (wrong number of arguments) nothing failed, so `perror` would print a confusing `: Success` – use `fprintf(stderr, …)` there.

## Line by line with fopen and fgets

`fopen` opens a file with a mode string and returns a `FILE *`, or `NULL` if it could not. `fgets(line, size, f)` reads one line – up to and including the `'\n'`, but never more than `size - 1` characters – and adds the 0. At the end of the file it returns `NULL`. That return value is the loop condition:

4_open_fopen.c – reading lines

```c
FILE *f = fopen(fname, "r");
if (f == NULL)
{
    perror("fopen");
    exit(1);
}
char line[160];
while (fgets(line, sizeof(line), f) != NULL)   // NULL: end of file or error
{
    printf("%s", line);                          // line still ends with '\n'
}
fclose(f);
```

| Mode | Meaning |
| --- | --- |
| `"r"` | Read. The file must exist. |
| `"w"` | Write. Creates the file, or empties it if it exists. |
| `"a"` | Append. Creates the file if needed; every write goes to the end. |
| `"r+"` | Read and write an existing file. |
| `"rb"`, `"wb"` | Binary. On Linux the b changes nothing – text and binary are the same. |

Common mistake

**while (!feof(f)) prints the last line twice**

It looks natural, and it is wrong. `feof` does not look ahead: it only says that an earlier read already ran into the end. After the last line has been read, `feof` is still false, the loop runs once more, `fgets` fails and leaves `line` as it was – and the last line is printed again (Fig. 7). On an empty file the first `printf` even prints an uninitialised buffer.

```c
while (!feof(f))                    // WRONG – feof only looks back
{
    fgets(line, sizeof(line), f);   // the last call fails and leaves line unchanged...
    printf("%s", line);             // ...so the last line is printed twice
}
```

> **Fig. 7** · Test the result of the read, not feof: feof turns true only after a read has already failed.
>
> *Diagram:* feof only reports that an earlier read already hit the end of the file. After the third fgets has read the last line, feof is still false, so a while (!feof(f)) loop runs once more: the fourth fgets fails, leaves the buffer unchanged, and the last line is printed twice.

Copying a file character by character with stdio uses `fgetc` and `fputc`. Note the `int c`: `fgetc` returns either a byte (0–255) or `EOF` (−1), and a `char` cannot hold all 257 values.

5_fopen_readwrite.c – copy with fgetc/fputc

```c
FILE *in = fopen("fourth_text.txt", "r");
FILE *out = fopen("write_here.txt", "w");   // "w": create, or empty an existing file
if (in == NULL || out == NULL) { /* report and stop */ }

int c;                                      // int, not char: EOF is not a character
while ((c = fgetc(in)) != EOF)
    fputc(c, out);

fclose(in);
fclose(out);                                // writes out what is still in the buffer
```

`fclose` does more than tidy up: it writes out what is still in the buffer. A program that ends with `return` from `main` or `exit` flushes its streams too, but one that crashes loses the buffered data – close what you opened.

## open, read, write, close

The system-call version of the copy: `open` the source for reading, `open` the target for writing, then `read` and `write` until the source runs out. `open` takes **flags** combined with `|` instead of a mode string:

| Flag | Meaning |
| --- | --- |
| `O_RDONLY` | Open for reading only. |
| `O_WRONLY` | Writing only (`O_RDWR`: both). |
| `O_CREAT` | Create the file if it does not exist – then the third argument, the permissions, is required. |
| `O_TRUNC` | If it exists, empty it first. |
| `O_APPEND` | Every write goes to the end of the file. |

6_open_readwrite.c – opening both files

```c
int f = open(argv[1], O_RDONLY);
if (f < 0) { perror("open"); exit(1); }

int g = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (g < 0) { perror("open"); exit(1); }
```

The third argument of `open` is the **permission** of a newly created file: nine bits, read/write/execute for the owner, the group and everyone else – the same `rwx` triples that `ls -l` shows. The `S_I…` constants name single bits; the octal number writes all nine at once (Fig. 8).

> **Fig. 8** · 0644: the owner reads and writes, everyone else only reads. Each octal digit is one rwx triple.
>
> *Diagram:* The permission of a new file is nine bits: read, write and execute for the owner, the group and everyone else. Each group of three is one octal digit, so S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH is 0644, shown by ls -l as rw-r--r--.

The process's **umask** removes bits from the requested mode: with the common umask `022`, asking for `0666` gives `0644`. If `O_CREAT` is there and the third argument is missing, the file gets whatever garbage happens to be in that argument slot – always pass it.

### The copy loop

`read(fd, &buf, n)` returns how many bytes it actually read: a positive number, `0` at the end of the file, or `-1` on an error. `write` returns how many bytes it wrote. Both facts go into the loop:

6_open_readwrite.c – byte-by-byte copy

```c
char c;
while (read(f, &c, sizeof(c)) > 0)          // > 0: stop at end of file (0) AND on error (-1)
{
    if (write(g, &c, sizeof(c)) != sizeof(c))
    {
        perror("write");
        exit(1);
    }
}
close(f);
close(g);
```

Common mistake

**while (read(…)) loops forever on an error**

`-1` is true in C. With `while (read(f, &c, 1))`, a read error does not stop the loop – it spins, printing the same `c` forever. Compare with `> 0`, which stops at both the end of the file and an error.

Good to know

**One byte at a time is slow**

Every `read` here is a trip to the kernel for one byte. That is fine for a demonstration and for short files; for real copying, read into a buffer of a few kilobytes and write exactly as many bytes as `read` returned (Task 4). stdio does this buffering for you – that is its whole point.

## Moving in a file: lseek and fseek

The kernel keeps an **offset** for every open file: the position where the next `read` or `write` starts. Reading moves it forward. `lseek(fd, offset, whence)` moves it anywhere, counted from the start, the current position or the end, and returns the new position (Fig. 9).

> **Fig. 9** · The offset is where the next read starts. SEEK_SET, SEEK_CUR and SEEK_END say what the lseek offset is counted from.
>
> *Diagram:* Every open file has an offset: the position where the next read or write starts. read and write move it forward by the number of bytes they handle; lseek moves it to a position counted from the start (SEEK_SET), from the current position (SEEK_CUR) or from the end (SEEK_END).

```c
lseek(f, 0, SEEK_SET);           // back to the first byte
lseek(f, -1, SEEK_END);          // to the last byte
off_t size = lseek(f, 0, SEEK_END);   // the new offset = the size of the file

fseek(fp, 0, SEEK_SET);          // the same for a FILE * stream (also: rewind(fp))
```

The example program reads a whole file, then jumps back to the start and reads the first character again – once with `lseek` on a descriptor, once with `fseek` on a stream. Use each with its own family: `lseek` for descriptors from `open`, `fseek` for streams from `fopen`. Moving the descriptor under a `FILE *` leaves the stdio buffer out of step with the file.

Common mistake

**SEEK_CUR with offset 0 does not move**

`lseek(f, 0, SEEK_CUR)` moves zero bytes from where you are – it only tells you the current position. After reading to the end, the next `read` still returns 0, and the variable you read into keeps its old value. Check what `read` returns before you print what it "read".

## Byte by byte and binary records

`fread(ptr, size, count, f)` reads `count` elements of `size` bytes and returns how many **elements** it read. Reading one `char` at a time, the right loop condition is `== 1`; the same `feof` trap as with `fgets` would print the last byte twice.

4_open_fopen.c – binary read

```c
char c;
while (fread(&c, sizeof(c), 1, f) == 1)   // how many ELEMENTS were read: 1 or 0
{
    printf("%c", c);
}
```

Nothing forces the element to be a `char`. `read`, `write`, `fread` and `fwrite` move raw bytes, so you can write a whole struct and read it back – fixed size, no parsing:

```c
struct point { int x; int y; } p = { 3, 4 };
write(g, &p, sizeof(p));         // the raw bytes of the struct, not text

struct point q;
if (read(f, &q, sizeof(q)) == sizeof(q))
    printf("%i %i\n", q.x, q.y);
```

The price: the file is not readable in an editor, and it is only portable to machines with the same type sizes and byte order. A **text** file of records – one line per record, fields separated by a tab – is readable, easy to debug with `cat`, and can be edited by hand:

drivers.txt – one record per line, tab-separated

```text
1	Kiss Anna	0612345	3
2	Nagy Bence	0698765	7
```

```c
fprintf(out, "%u\t%s\t%s\t%u\n", d.id, d.name, d.phone, d.exp);   // one record = one line

while (fscanf(in, "%u\t%49[^\t]\t%49s\t%u", &d.id, d.name, d.phone, &d.exp) == 4)
{
    // a complete record – 4 fields matched; anything less means end of file or a broken line
}
```

In your assignment

**Pick the file format first**

The first assignment keeps records in a file and lets the user list, add, modify and delete them. Decide the format before writing code: a text file with one record per line and a tab between fields is the simplest to check. Use a tab, not a space, if a field (a name) can contain spaces – and read it with `%49[^\t]`, which stops at the tab and never overflows a `char[50]`.

Compare the return value of `fscanf` with the number of fields, and stop when it is less. The [CRUD skeleton guide](https://laszlonemes.com/university/operating-systems/crud-skeleton) builds the full read–modify–write cycle on top of this.

## Reading the keyboard

The keyboard is just another file: `stdin`, descriptor 0. `fgets(s, sizeof(s), stdin)` reads one line typed by the user, with the same rules as for a file – it keeps the `'\n'`, and it returns `NULL` when the input ends. The example writes every line it gets into a file until the user types `over`:

7_filewriter_loop.c – the loop

```c
char s[100];
do
{
    printf("Enter: ");
    if (fgets(s, sizeof(s), stdin) == NULL)   // Ctrl+D: end of input
        break;
    int length = strlen(s);                    // includes the '\n' fgets kept
    if (write(g, s, length) != length) { perror("write"); exit(1); }
} while (strcmp(s, "over\n") != 0);
```

Because `fgets` keeps the newline, the comparison is with `"over\n"`. If you want the line without it, cut it off:

```c
s[strcspn(s, "\n")] = '\0';   // cut the line at the first '\n' (if there is one)
```

Common mistake

**Ignoring the end of input**

Ctrl+D (or the end of a file redirected with `<`) makes `fgets` return `NULL` and leaves `s` untouched. Without the `NULL` check, the loop above would write the last line into the file again and again, as fast as it can – megabytes within seconds, until someone kills it. Every input loop needs an exit for "no more input".

## File metadata with stat

`stat(name, &st)` fills a `struct stat` with what the file system knows about a file, without opening it: size, owner, permission bits, and three timestamps. `st_mtime` is the time of the last modification as a `time_t` (seconds since 1970); `ctime` turns it into readable text.

8_mtime.c – the idea

```c
struct stat st;
if (stat(fname, &st) != 0)
{
    perror("stat");
    exit(1);
}
printf("size: %lld bytes\n", (long long)st.st_size);
time_t t = st.st_mtime;
printf("last modified: %s", ctime(&t));   // ctime's text already ends with '\n'
```

| Field | Contains |
| --- | --- |
| `st_size` | Size in bytes. |
| `st_mode` | File type and the permission bits (`st.st_mode & 0777`). |
| `st_uid` | The owner, as a user ID. |
| `st_mtime` | Last modification of the content. |
| `st_atime` | Last access (to save disk writes, Linux usually does not update it on every read). |
| `st_ctime` | Last change of the metadata (permissions, owner) – not the creation time. |

Good to know

**argv[0] is not always a path**

The course example asks for the modification time of `argv[0]` – the program's own file. That works for `./8_mtime`, but if the program is found through `PATH`, `argv[0]` is just the name and `stat` fails in another directory. That is why the return value of `stat` is checked: on failure the struct is never filled, and printing its fields would print garbage.

## Practice

Write these on the course server or any Linux machine, compile with `-Wall -Wextra`, and get rid of every warning. Each one is small – the point is to get the details right: the loop condition, the 0 at the end, the checked return value.

Task 1

**Arguments backwards**

Print the command-line arguments in reverse order, one per line, without the program name. With no arguments, print a usage line to standard error and exit with code 1.

**Approach:**

The last argument is `argv[argc - 1]`; stop before index 0.

```c
for (int i = argc - 1; i >= 1; i--)
    printf("%s\n", argv[i]);
```

Task 2

**Your own strcpy**

Write `void my_strcpy(char *dst, const char *src)` without `string.h`. Test it by copying `"Operating Systems"` into a `char[80]` and printing it. What happens if you leave out the last line of the hint?

**Approach:**

Copy until the 0 – then copy the 0 as well.

```c
int i = 0;
while (src[i] != '\0')
{
    dst[i] = src[i];
    i++;
}
dst[i] = '\0';          // the easiest line to forget
```

Task 3

**Count the lines of a file**

Take a file name as an argument and print how many lines it has. Compare your answer with `wc -l` on a few files, including an empty one and one whose last line has no `'\n'`.

**Approach:**

`fgets` in a loop on its return value. A line longer than your buffer arrives in several pieces, so count only the pieces that end in `'\n'` – that is exactly what `wc -l` counts.

```c
int lines = 0;
while (fgets(line, sizeof(line), f) != NULL)
    if (strchr(line, '\n') != NULL)    // a full line – a long one comes in several pieces
        lines++;
```

Task 4

**A faster copy**

Change the `open`/`read`/`write` copy to move 512 bytes per call. Copy a large file (for example one made with `head -c 5000000 /dev/urandom > big.bin`) and check with `cmp` that the copy is identical. Time both versions with `time`.

**Approach:**

The last `read` returns fewer than 512 bytes. Write `n` bytes, not `sizeof(buf)` – otherwise the copy ends in garbage.

```c
char buf[512];
ssize_t n;
while ((n = read(f, buf, sizeof(buf))) > 0)
{
    if (write(g, buf, n) != n)   // write n bytes – not sizeof(buf)
    { perror("write"); exit(1); }
}
if (n < 0) perror("read");
```

Task 5

**An append-only log**

Read lines from the keyboard and append each to `log.txt`, keeping what was already there. Stop on `over` or on Ctrl+D. Create the file as `rw-r--r--`, and check with `ls -l`.

**Approach:**

`O_APPEND` instead of `O_TRUNC` (or mode `"a"` with stdio), permission `0644`, and the `fgets` loop with its `NULL` check.

```c
int g = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
/* or with stdio: */
FILE *fp = fopen("log.txt", "a");
```

Task 6

**The last character**

Print the last character of a file given as an argument using `open`, `lseek` and `read` – without reading the rest. What does your program do with an empty file?

**Approach:**

`SEEK_END` with offset `-1`. On an empty file that position would be before the start, so `lseek` fails – check it.

```c
if (lseek(f, -1, SEEK_END) >= 0 && read(f, &c, 1) == 1)
    printf("last character: %c\n", c);
```

Task 7

**A tiny ls**

For every file name given as an argument, print the name, the size in bytes and the last modification time. A missing file should produce an error message for that file, and the program should go on with the next one.

**Approach:**

`stat` per argument, `perror(argv[i])` and `continue` on failure. `ctime` already ends the line.

```c
for (int i = 1; i < argc; i++)
{
    struct stat st;
    if (stat(argv[i], &st) != 0) { perror(argv[i]); continue; }
    printf("%s\t%lld\t%s", argv[i], (long long)st.st_size, ctime(&st.st_mtime));
}
```

## Summary

- `argc` counts the arguments including the program name; `argv[argc]` is `NULL`. Check `argc` before reading `argv[i]`.
- `&` takes an address, `*` follows one. A `char *` holds no text by itself – it points at an array or at `malloc`ed memory.
- A string ends at the first 0 byte. Every buffer needs room for it, and `strlen` returns `size_t` (`%zu`).
- stdio (`FILE *`, buffered) and system calls (file descriptor, unbuffered) are two layers over the same files. 0, 1, 2 are stdin, stdout, stderr.
- Loop on the result of the read: `fgets(…) != NULL`, `fread(…) == 1`, `read(…) > 0` – never on `feof`.
- `O_CREAT` needs a permission argument (e.g. `0644`); `lseek` moves the offset; `stat` reads size and `st_mtime`.
- Every call that can fail gets its return value checked; `perror` explains why a system call failed.
