Общие сведения о ссылках

Общие сведения о ссылках.

С этого урока мы начнем рассматривать другой механизм передачи параметров, в частности, с использованием ссылок.

Использование указателей в качестве альтернативного способа доступа к переменным таит в себе опасность - если был изменен адрес, хранящийся в указателе, то этот указатель больше не ссылается на нужное значение.

Язык C предлагает альтернативу для более безопасного доступа к переменным через указатели.Объявив ссылочную переменную, можно создать объект, который, как указатель, ссылается на другое значение, но, в отличие от указателя, постоянно привязан к этому значению. Таким образом, ссылка на значение всегда ссылается на это значение.

Ссылку можно объявить следующим образом:

<имя типа>& <имя ссылки> = <выражение>;

или

<имя типа>& <имя ссылки>(<выражение>);

Раз ссылка является другим именем уже существующего объекта, то в качестве инициализирующего объекта должно выступать имя некоторого объекта, уже расположенного в памяти. Значением ссылки после выполнения соответствующего определения с инициализацией становится адрес этого объекта. Проиллюстрируем это на конкретном примере:

#include "iostream"

using namespace std;

void main()

{

int ivar = 1234; //Переменной присвоено значение.

int *iptr = &ivar; //Указателю присвоен адрес ivar.

int &iref = ivar; //Ссылка ассоциирована с ivar.

int *p = &iref; //Указателю присвоен адрес iref.

cout << "ivar = " << ivar << "\n";

cout << "*iptr = " << *iptr <<"\n";

cout << "iref = " << iref << "\n";

cout << "*p = " << *p << "\n";

}

Результат работы программы:

ivar = 1234

*iptr = 1234

iref = 1234

*p = 1234

Комментарии к программе.Здесь объявляются четыре переменные. Переменнаяivar инициализирована значением 1234. Указателю на целое *iptr присвоен адресivar. Переменная iref объявлена как ссылочная. Эта переменная в качестве своего значения принимает адрес расположения в памяти переменной ivar. Оператор:

cout << "iref = " << iref << "\n";

выводит на экран значение переменной ivar. Это объясняется тем, что iref - ссылка на местоположение ivar в памяти.

Последнее объявление int *p = &iref; создает еще один указатель, которому присваивается адрес, хранящийся в iref. Строки:

int *iptr = &ivar;

и

int *p = &iref;

дают одинаковый результат. В них создаются указатели, ссылающиеся на ivar. На рис.1 проиллюстрирована взаимосвязь переменных из приведенной программы:

При использовании ссылок следует помнить одно правило: однажды инициализировав ссылку ей нельзя присвоить другое значение! Все эти конструкции:

a) int iv = 3; b) iref++; c) iref = 4321;

iref = iv;

приведут к изменению переменной ivar!

Замечания.

  • 1. В отличие от указателей, которые могут быть объявлены неинициализированными или установлены в нуль (NULL), ссылки всегда ссылаются на объект. Для ссылок не существует аналога нулевого указателя.
  • 2. Ссылки нельзя инициализировать в следующих случаях:
    • при использовании в качестве параметров функции.
    • при использовании в качестве типа возвращаемого значения функции.
    • в объявлениях классов.
  • 3. Не существует операторов, непосредственно производящих действия над ссылками

Ссылочные параметры. Передача аргументов по ссылке.

Ссылочные переменные используются достаточно редко: значительно удобнее использовать саму переменную, чем ссылку на нее. В качестве параметров функции ссылки имеют более широкое применение. Ссылки особенно полезны в функциях, возвращающих несколько объектов (значений). Для иллюстрации высказанного положения рассмотрим программу:

#include "iostream"

using namespace std;

//Обмен с использованием указателей.

void interchange_ptr (int *u,int *v)

{

int temp=*u;

*u = *v; *v = temp;

}

/* ------------- */

//Обмен с использованием ссылок.

void interchange_ref (int &u,int &v)

{

int temp=u;

u = v; v = temp;

}

/* ------------- */

void main ()

{

int x=5,y=10;

/* --------------------------------------- */

cout << "Обмен с использованием указателей:\n";

cout << "Вначале x = " << x << " и y = " <<y <<"\n";

interchange_ptr (&x,&y);

cout << "Теперь x = " << x << " и y = " << y <<"\n";

cout << "-----------------------------------------" <<"\n";

cout << "Обмен с использованием ссылок:\n";

cout << "Вначале x = " << x << " и y = " << y <<"\n";

interchange_ref (x,y);

cout << "Теперь x = " << x << " и y = " << y <<"\n";

}

В функции interchange_ptr() параметры описаны как указатели. Поэтому в теле этой функции выполняется их разименование, а при обращении к этой функции в качестве фактических переменных используются адреса (&x,&y) тех переменных, значения которых нужно поменять местами. В функции interchange_ref() параметрами являются ссылки. Ссылки обеспечивают доступ из тела функции к фактическим параметрам, в качестве которых используются обычные переменные, определенные в программе.

Ссылки и указатели в качестве параметров функций тесно связаны. Рассмотрим следующую небольшую функцию:

void f(int *ip)

{

*ip = 12;

}

Внутри этой функции осуществляется доступ к переданному аргументу, адрес которого хранится в указателе ip, с помощью следующего оператора:

f(&ivar); //Передача адреса ivar.

Внутри функции выражение *ip = 12; присваивает 12 переменной ivar, адрес которой был передан в функцию f(). Теперь рассмотрим аналогичную функцию, использующую ссылочные параметры:

void f(int &ir)

{

ir = 12;

}

Указатель ip заменен ссылкой ir, которой присваивается значение 12. Выражение:

f(ivar); //Передача ivar по ссылке.

присваивает значение ссылочному объекту: передает ivar по ссылке функции f(). Поскольку ir ссылается на ivar, то ivar присваивается значение 12.

Ссылки в качестве результатов функций

Здесь мы рассмотрим использование ссылок в качестве результатов функций.

Далее представлен ряд редакторов, рекомендуемых для использования и их основные возможности.

Функции могут возвращать ссылки на объекты при условии, что эти объекты существуют, когда функция неактивна. Таким образом, функции не могут возвращать ссылки на локальные автоматические переменные. Например, для функции, объявленной как:

double &rf(int p);

необходим аргумент целого типа, и она возвращает ссылку на объект double, предположительно объявленный где-то в другом месте.

Проиллюстрируем сказанное конкретными примерами.

Пример 1. Заполнение двумерного массива одинаковыми числами.

#include "iostream"

using namespace std;

int a[10][2];

void main ()

{

int & rf(int index); //Прототип функции.

int b;

cout << "Заполнение двумерного массива. ";

cout << "Первый столбец заполняется обычным способом, ";

cout << "а второй - через функцию.\n";

for (int i=0;i<10;i++)

{

cout << i+1 << "-й элемент: ";

cin >> b;

a[i][0] = b;

rf(i) = b;

}

cout << "Вывод двумерного массива.\n";

cout << "1-й столбец 2-й столбец" << "\n";

for (i=0;i<10;i++)

cout << a[i][0] << "\t\t" << rf(i) << "\n";

}

int &rf(int index)

{

return a[index][1]; //Возврат ссылки на элемент массива.

}

Здесь объявляется глобальный двумерный массив a, состоящий из целых чисел. В начале функции main() содержится прототип ссылочной функции rf(), которая возвращает ссылку на целое значение второго столбца массива a, которое однозначно идентифицируется параметром-индексом index. Так как функция rf() возвращает ссылку на целое значение, то имя функции может оказаться слева от оператора присваивания, что продемонстрировано в строке:

rf(i) = b;

Пример 2. Нахождение максимального элемента в массиве и замена его на нуль.

#include "iostream"

using namespace std;

//Функция определяет ссылку на элемент

//массива с максимальным значением.

int &rmax(int n, int d[])

{

int im=0;

for (int i=1; i<n; i++)

im = d[im]>d[i]?im:i;

return d[im];

}

void main ()

{

int x[]={10, 20, 30, 14};

int n=4;

cout << "\nrmax(n,x) = " << rmax(n,x) << "\n";

rmax(n,x) = 0;

for (int i=0;i<n;i++)

cout << "x[" << i << "]=" << x[i] << " ";

cout << "\n";

}

Результаты работы программы:

rmax (n,x) = 30

x[0]=10 x[1]=20 x[2]=0 x[3]=14

При выполнении строки:

cout << "\nrmax(n,x) = " << rmax(n,x) << "\n";

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

rmax(n,x) = 0;

снова осуществляется обращение к функции rmax(). Теперь уже по найденной ссылке максимальное значение заменяется на 0.

Операторы свободной памяти new и delete.

Массивы динамической памяти.

Операция new при использовании с массивами имеет следующий формат:

new <тип массива>.

Такая операция позволяет выделить в динамической памяти участок для размещения массива соответствующего типа, но не позволяет его инициализировать. В результате выполнения операция new возвратит указатель, значением которого служит адрес первого элемента массива. При выделении динамической памяти для массива его размеры должны быть полностью определены, например:

long (*lp)[2] [4] ; // Определили указатель.

lp = new long[3][2][4] ; // Выделили память для массива.

В данном примере использован указатель на объекты в виде двумерных массивов, каждый из которых имеет фиксированные размеры 2 на 4 и содержит элементы типа long. В определении указателя следует обратить внимание на круглые скобки, без которых обойтись нельзя. После выполнения приведенных операторов указатель lp становится средством доступа к участку динамической памяти с размерами 3 * 2 * 4 * sizeof(long) байтов. В отличие от имени массива (имени у этого массива из примера нет) указатель lp есть переменная, что позволяет изменять его значение и тем самым, например, перемещаться по элементам массива.

Изменять значение указателя на динамический массив нужно осторожно, чтобы не "забыть", где находится начало массива, так как указатель, значение которого определяется при выделении памяти для динамического массива, используется затем для освобождения памяти с помощью операции delete. Например, оператор:

delete [] lp;

освободит целиком всю память, выделенную для определенного выше трехмерного массива, если lp адресует его начало.

B отличие от определения массивов, не размещающихся в динамической памяти, инициализация динамических массивов не выполняется. Поэтому при выделении памяти для динамических массивов их размеры должны быть полностью определены явно. Кроме того, только первый (самый левый) размер массива может быть задан с помощью переменной. Остальные размеры многомерного массива могут быть определены только с помощью констант. Это затрудняет работу с многомерными динамическими массивами. Обойти это ограничение позволяет применение массивов указателей.

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

Пример 1. Создание динамического массива с использованием констант, задающих размерность массива.

#include "iostream"

void main ()

{

int const N=2; //Константы,

int const M=4; //задающие размерность

int const L=3; //массива.

/*-------------------*/

int (*ip)[M][L], //Описание указателя.

x=0; //Переменная для заполнения массива.

ip= new int [N][M][L]; //Выделение памяти из кучи.

//Заполнение массива.

for (int i=0;i<N;i++)

for (int j=0;j<M;j++)

for (int k=0;k<L;k++)

ip[i][j][k]=++x;

//Вывод массива.

cout << "Полученный массив:" << "\n";

for (i=0;i<N;i++)

{

cout << i << " строка: " << "\n";

for (int j=0;j<M;j++)

{

cout << '\t' << j << " столбец: " <<"\n";

cout << "\t\tНаходящиеся там значения: " ;

for (int k=0;k<L;k++)

cout << ip[i][j][k] << " ";

cout << "\n";

}

}

delete [] ip;//Возврат памяти в кучу.

}

В этом примере при описании указателя на массив используются константы, задающие размерность создаваемого массива. Однако так бывает далеко не всегда. Как правило, размерность создаваемого массива становится известной только в момент выполнения программы. Следующий пример иллюстрирует возможность определения одной размерности массива в момент выполнения программы.

Пример 2. Создание динамического массива с использованием ввода одной размерности.

#include "iostream"

void main ()

{

// int const N=2;

int const M=4;

int const L=3;

/*-------------------*/

int (*ip)[M][L],x=0,N;

cout << "Количество строк массива: ";

cin >> N;

ip= new int [N][M][L];

for (int i=0;i<N;i++)

for (int j=0;j<M;j++)

for (int k=0;k<L;k++)

ip[i][j][k]=++x;

cout << "Полученный массив:" << "\n";

for (i=0;i<N;i++)

{

cout << i << " строка: " << "\n";

for (int j=0;j<M;j++)

{

cout << '\t' << j << " столбец: " <<"\n";

cout << "\t\tНаходящиеся там значения: " ;

for (int k=0;k<L;k++)

cout << ip[i][j][k] << " ";

cout << "\n";

}

}

delete [] ip;

}

В этом примере количество строк вводится с клавиатуры при выполнении программы. А если нужно задать не одну, а две, три размерности? В этом случае такой вариант решения не проходит:

// int const N=2;

// int const M=4;

int const L=3;

/*-------------------*/

int x=0,N,M;

cout << "Количество строк массива: ";

cin >> N;

cout << "Количество столбцов массива: ";

cin >> M;

int (*ip)[M][L];

ip= new int [N][M][L];

в связи с тем, что конструкции, используемые при описании указателя, должны быть константами! В этом случае можно поступить следующим образом:

  • 1. Выделить память для одномерного массива для размещения всех элементов многомерного массива.
  • в тексте программы осуществлять доступ к этому массиву как к многомерному. Например, если ip - указатель на такой одномерный массив, а доступ к нему нужно осуществлять как к трехмерному ip[N][M][L], то доступ к элементу ip[i][j][k] можно записать следующим образом: *(ip+i*(M*L)+j*L+k). Если ip - указатель на одномерный массив, а доступ к нему нужно осуществлять как к двумерному ip[N][M], то доступ к элементу ip[i][j] можно записать следующим образом: *(ip+i*M+j).

Пример 3. Создание динамического массива с использованием ввода всех размерностей.

#include "iostream"

void main ()

{

int *ip,x=0,N,M,L;

cout << "Количество строк массива: ";

cin >> N;

cout << "Количество столбцов массива: ";

cin >> M;

cout << "Количество значений в столбце: ";

cin >> L;

//Резервируем место в куче для одномерного массива.

ip= new int [N*M*L];

//Заполняем массив значениями.

for (int i=0;i<N;i++)

for (int j=0;j<M;j++)

for (int k=0;k<L;k++)

//Моделируем доступ к многомерному массиву.

*(ip+i*(M*L)+j*L+k)=++x;

cout << "Полученный массив:" << "\n";

for (i=0;i<N;i++)

{

cout << i << " строка: " << "\n";

for (int j=0;j<M;j++)

{

cout << '\t' << j << " столбец: " < "\n";

cout << "\t\tНаходящиеся там значения: " ;

for (int k=0;k<L;k++)

cout << *(ip+i*(M*L)+j*L+k) << " ";

cout << "\n";

}

}

delete [] ip;

}

Домашнее задание

1. Пользователь вводит с клавиатуры количество строк и количество столбцов, причем эти числа обязательно должны быть нечетными (осуществить проверку). Программа показывает на экран общее количество всех элементов и значение центрального элемента.

2. Пользователь с клавиатуры вводит строку. Создается числовой одномерный массив с таким же количеством элементов, сколько у строки – значащих символов. Записывает коды этих символов в числовой массив и, оба массива показывает на экран.

3. Программа, генерирует число от 1 до 100. Потом пользователь говорит, сколько ему нужно элементов в массиве. Программа создает массив. Заданным количеством элементов. Пользователь вносит в массив все числа, на которые сгенерированное число делится без остатка. Программа проверяет, правильно ли пользователь указал числа. Выводит свой вариант.

Общие сведения о ссылках