Указатели и ссылки

Материал из SEWiki
Перейти к: навигация, поиск

Указатели

Указатель - это переменная, содержащая адрес другой переменной. Указатели очень широко используются в языке С++. Это происходит отчасти потому, что иногда они дают единственную возможность выразить нужное действие, а отчасти потому, что они обычно ведут к более компактным и эффективным программам, чем те, которые могут быть написаны другими способами. Так как указатель содержит адрес объекта, это дает возможность "косвенного" доступа к этому объекту через указатель.

Пример создания указателя на int и на double:

int a=5;
int *pa = &a;
double d = 1.5;
double *pd = &d;

pa = pd //нельзя! т.к. указатели на данные разного типа

pa = (int*)pd; //компилируется

Последняя операция лишена смысла, т.к. элемент типа double занимает 8 байт, а элемент типа int - 4 байта (в х32). Поэтому указатель pa будет указывать на первые 4 байта данных, расположенных по адресу, хранящемуся в pd.

Рассмотрим пример функции, которая должна менять местами два значения:

void swap (int a, int b)
{
	int t = a;
	a = b;
	b = t;
}

Вызов swap:

int k=10, m=20;
swap (k, m);

Несмотря на то, что все написано правильно, после возврата из функции swap значения k и m не изменились. Это обусловлено тем, что при передаче в функцию происходит копирование параметров. Таким образом, изменились скопированные значения, а исходные остались прежними. Чтобы иметь возможность менять параметры их следует передавать по указателю:

void swap (int *a, int *b)
{
	int t = *a;
	*a = *b;
	*b = *t;
}

Вызов swap:

int k=10, m=20;
swap (&k, &m);

Теперь значения k и m изменились, так как при передаче в функцию произошло копирование адресов переменных, а адреса мы менять не хотели.

Встроенные массивы

Опр. Массив - это непрерывная область памяти, в которой расположены однотипные элементы.

int a[5] = {1, 2, 3}; //в памяти лежат 1,2,3,0,0
_ _ _  ___________________ _ _
_ _ _ |_1_|_2_|_3_|_0_|_0_|_ _

Связь массивов и указателей: имя встроенного массива преобразуется в указатель (на первый элемент массива):

int *p = 0; //"нулевой" указатель, nil. Означает, что он не указывает ни на какую ячейку памяти.
p = a; // &a[0]
a = p; // обратное (р = а) неверно

Передача массива в функцию в качестве параметра:

int max_element (int m[10]) {}

Массив передается по ссылке (не копируется). Вызов: max_element(a); Функция max_element будет работать только с массивами из 10 элементов (с другими - не скомпилируется). Правильнее делать так:

int max_element (int *m, int size) 
{
	int max = *m; 
	for (int i=1; i != size; ++i)
		if (m[i] >= max)
			max = m[i];
	return max;
}

Вызов: max_element(a, 5);

Проблемы такой реализации:

  • функция не работает с массивами нулевой длины;
  • не отследить случаи выхода за границы массива.

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

Пусть q, p - указатели, а i - число. Возможные операции:

  • p+i, p-i сдвиг по массиву на i элементов вправо/влево
  • p[i] <=> *(p+i)
  • q-p количество элементов массива между p и q. Разность указателей имеет тип size_t.

Рассмотрим две реализации функции подсчета длины символов в строке:

int strlen1 (char *s)
{
	int len=0;
	while (s[len]!=0)
		++len;
	return len;
}

int strlen2 (char *s)
{
	char *p = s;
	while (*p!=0)
		++p;
	return p-s;
}

Первая реализация более трудоемка, так как s[len] <=> *(s+len) то есть смещению по массиву и разименованию (2 операции), в то время, как во второй реализации мы работаем с указателями напрямую. Но второй вариант менее читабелен. Второй способ (работу с указателями напрямую) следует использовать и при передаче массива в функцию.

Рекомендуется следующая сигнатура:

int foo (int *first, int *last);

Где first является указателем на первый элемент массивва (a[0]), а last указателем на область памяти сразу за массивом ("a[n]"). Такой выбор указателя last обеспечивает работу с массивами нулевой длины. Перепишем функцию max_element:

int* max_element (int *m, int *last)
{
	int *max = m;
	for (; m != last; ++m)
		if (*m >= *max)
			max = m;
	return max;
}

Вызов: max_element(a, a+5); Следует отметить, что теперь функция max_element возвращает не значение максимального элемента массива, а указатель на него.

Указатели на указатель

Рассмотрим функцию поиска символа в строке, возвращающую позицию искомого элемента:

int findch (char *s, char p)
{
	for (int i = 0; s[i] != 0; ++i)
		if (s[i] == p)
			return i;
	return -1;
}

Вообще данная функция должна информировать о двух вещах: во-первых, найден ли искомый символ, а во-вторых, если да, то его позицию. Будет правильно возвращать 2 значения соответственно, а не совмещать все в одном. Перепишем функцию:

bool findch (char *s, char p, int *n=0)
{
	for (int i = 0; s[i] != 0; ++i)
		if (s[i] == p)
		{
			*n = i;
			return true;
		}
	return false;
}

Вызов:

int idx = 0;
if (findch(s, ' ', &amp;idx)) {...}

Чтобы возвращать не индекс элемента, а его позицию в массиве используют указатель на указатель:

bool findch (char *s, char p, char **pos=0)
{
	while (*s!=0)
	{
		if (*s==p)
		{
			if (pos)
				*pos=s;
			return true;
		}
		++s;
	}
	return false;
}

Вызов:

char *pos = 0;
if (findch(s, ' ', &amp;pos) {...}

Работа с указательями напрямую синтаксически неудобна. Для достижения удобства используют ссылки:

int a = 5;
int &amp;b = a; //b - ссылка на а

b = 10; //в а тоже стало 10

Функция swap с использованием ссылок будет выглядеть так:

void swap (int &amp;a, int &amp;b)
{
	int t=b;
	b=a;
	a=t;
}

Вызов:

int c = 30;
swap (a, c);

При передаче параметров в функцию по ссылке копирования данных не происходит.

Различия ссылок и указателей

  • Ссылку нельзя не проинициализировать (ссылка не может быть неинициализированной); ссылку нельзя переинициализировать.
  • Нельзя получить адрес ссылки или ссылку на ссылку.
  • Нельзя создавать массивы ссылок.

Замечание:

  • &q взятие адреса переменной q
  • *pa разименование
  • int &a передача переменной а по ссылке
  • int *pa передача переменной по адресу.