Указатели и ссылки
Содержание
Указатели
Указатель - это переменная, содержащая адрес другой переменной. Указатели очень широко используются в языке С++. Это происходит отчасти потому, что иногда они дают единственную возможность выразить нужное действие, а отчасти потому, что они обычно ведут к более компактным и эффективным программам, чем те, которые могут быть написаны другими способами. Так как указатель содержит адрес объекта, это дает возможность "косвенного" доступа к этому объекту через указатель.
Пример создания указателя на 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, ' ', &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, ' ', &pos) {...}
Работа с указательями напрямую синтаксически неудобна. Для достижения удобства используют ссылки:
int a = 5;
int &b = a; //b - ссылка на а
b = 10; //в а тоже стало 10
Функция swap с использованием ссылок будет выглядеть так:
void swap (int &a, int &b)
{
int t=b;
b=a;
a=t;
}
Вызов:
int c = 30;
swap (a, c);
При передаче параметров в функцию по ссылке копирования данных не происходит.
Различия ссылок и указателей
- Ссылку нельзя не проинициализировать (ссылка не может быть неинициализированной); ссылку нельзя переинициализировать.
- Нельзя получить адрес ссылки или ссылку на ссылку.
- Нельзя создавать массивы ссылок.
Замечание:
- &q взятие адреса переменной q
- *pa разименование
- int &a передача переменной а по ссылке
- int *pa передача переменной по адресу.