Ликбез по указателям

Ячейки памяти нумеруются целыми числами с нуля. Каждая ячейка — один байт.

Указатель — адрес (номер) некоторой ячейки памяти.
Тип указателя включает в себя информацию о том, на объект какого типа он указывает.
Например, переменная типа int* содержит адрес некоторого int-а (точнее, его первого байта).

1. Получение адреса переменной.

У любой переменной можно узнать адрес в памяти, по которой лежит значение, которое она содержит.
Для этого используется оператор & перед названием переменной.

int x = 5;
printf("%p\n", &x); // выведет адрес (в шестнадцатеричной форме), по которому в памяти лежит число 5

int *a = &x; // сохранение адреса в переменной a типа int*
printf("%p\n", a); // выведет тот же адрес

x = 7;
printf("%p\n", a); // адрес не изменится

2. Разыменование указателя.

По указателю можно получить значение, которое лежит в памяти по соответствующему адресу.
Для этого используется оператор * перед названием переменной-указателя.

int x = 5;
printf("%d\n", *&x); // выведет 5: операция * обратна операции &

int *a = &x;
printf("%d\n", *a); // выведет 5

x = 7;
printf("%d\n", *a); // выведет 7
printf("%d\n", x); // выведет 7

*a = 3; // с помощью разыменования можно не только получать значение, но и изменять его
printf("%d\n", *a); // выведет 3
printf("%d\n", x); // выведет 3

3. Арифметика указателей.

Можно представлять себе указатель как целое число.
На 32-битных компиляторах указатель (на любой тип) занимает 4 байта (как int), на 64-битных — 8 байт (как long long).
Арифметика указателей несколько отличается от обычной арифметики целых типов.

int x = 5;
int *a = &x;
int *b = a + 1;
printf("%p\n", a); // выведет адрес
printf("%p\n", b); // выведет адрес на 4 байта больше

Вообще говоря, операция (p + k), где p имеет тип mytype*, а k имеет тип int, вернёт на (k * sizeof(mytype)) больший адрес.
Аналогично с вычитанием (p - k).

int x = 5;
int *a = &x;
int *b = a + 3;
char *c = ((char*) a) + 3;
printf("%p\n", a); // выведет адрес addr
printf("%p\n", b); // выведет адрес на 12 = 3 * sizeof(int) = 3 * 4 байт больший addr
printf("%p\n", c); // выведет адрес на 3 = 3 * sizeof(char) = 3 * 1 байт больший addr
Кроме того, указатели можно вычитать друг из друга (но не складывать).
int x = 5;
int *a = &x;
int *b = a + 3;
printf("%p\n", a); // выведет адрес
printf("%p\n", b); // выведет адрес на 12 = 3 * sizeof(int) = 3 * 4 байт больше
int d = b - a;
printf("%d\n", d); // выведет 3

4. Объявление указателей.

int *a; // так принято
int* a; // так не принято

Это связано с тем, что конструкция
int* a, b;
будет интерпретирована компилятором как объявление переменной a типа int* и переменной b типа int (а не int*).
Чтобы объявить два указателя, необходимо написать
int *a, *b;

5. Оператор ->.

Пусть объявлена структура следующего вида:

struct Node {
	int x, y;
	double z;
	Node() {
		z = x = y = -1;
	}
	Node(int _x, int _y) {
		x = _x;
		y = _y;
		z = sqrt(x * x + y * y);
	}
}
И пусть мы имеем указатель на переменную типа Node:
Node *nodeptr;
Если мы захотим вывести поле x объекта типа Node, который лежит по адресу, сохранённому в nodeptr, мы напишем
printf("%d\n", (*nodeptr).x);
Так писать не очень удобно. Поэтому в языке есть синтаксический сахар: можно писать
printf("%d\n", nodeptr->x);
Конструкция nodeptr->x эквивалентна (*nodeptr).x.

6. Оператор new.

Оператор new позволяет выделять память под переменные и массивы.
int *a = new int(); // выделили память под одну переменную типа int (4 байта) и адрес записали в a
*a = 5; // записали по этому адресу в памяти значение 5

Node *nodeptr = new Node(5, 3); // выделили память под одну переменную типа Node и вызвали на этом месте конструктор Node(int, int)
Также можно выделять память для массивов:
int *as = new int[100]();
Node *nodesptr = new Node[100](); // для всех элементов будет выполнен конструктор по умолчанию (от нуля аргументов); использовать другой конструктор нельзя

7. NULL

NULL — это нулевой указатель; так принято помечать отсутствие указателя.
На самом деле, NULL является просто нулём:
int *a = NULL;
printf("%p\n", a); // выведет 0
Разыменовывание нулевого указателя вызовет Runtime Error (так же, как и обращение к любой не принадлежащей вам памяти).