Вопрос 28. Ссылочные типы данных и их применение. 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Вопрос 28. Ссылочные типы данных и их применение.



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

int ival = 1024;

// правильно: refVal - ссылка на ival

int &refVal = ival;

// ошибка: ссылка должна быть инициализирована

int &refVal2;

Хотя, как мы говорили, ссылка очень похожа на указатель, она должна быть инициализирована не адресом объекта, а его значением. Таким объектом может быть и указатель:

int ival = 1024;

 

// ошибка: refVal имеет тип int, а не int*

int &refVal = &ival;

int *pi = &ival;

// правильно: ptrVal - ссылка на указатель

int *&ptrVal2 = pi;

Определив ссылку, вы уже не сможете изменить ее так, чтобы работать с другим объектом (именно поэтому ссылка должна быть инициализирована в месте своего определения). В следующем примере оператор присваивания не меняет значения refVal, новое значение присваивается переменной ival – ту, которую адресует refVal.

int min_val = 0;

// ival получает значение min_val,

// а не refVal меняет значение на min_val

refVal = min_val;

Все операции со ссылками реально воздействуют на адресуемые ими объекты. В том числе и операция взятия адреса. Например:

refVal += 2;

прибавляет 2 к ival – переменной, на которую ссылается refVal. Аналогично

int ii = refVal;

присваивает ii текущее значение ival,

int *pi = &refVal;

инициализирует pi адресом ival.

Если мы определяем ссылки в одной инструкции через запятую, перед каждым объектом типа ссылки должен стоять амперсанд (&) – оператор взятия адреса (точно так же, как и для указателей). Например:

// определено два объекта типа int

int ival = 1024, ival2 = 2048;

 

// определена одна ссылка и один объект

int &rval = ival, rval2 = ival2;

 

// определен один объект, один указатель и одна ссылка
int inal3 = 1024, *pi = ival3, &ri = ival3;

 

// определены две ссылки

int &rval3 = ival3, &rval4 = ival2;

Константная ссылка может быть инициализирована объектом другого типа (если, конечно, существует возможность преобразования одного типа в другой), а также безадресной величиной – такой, как литеральная константа. Например:

double dval = 3.14159;

 

// верно только для константных ссылок
const int &ir = 1024;
const int &ir2 = dval;
const double &dr = dval + 1.0;

Если бы мы не указали спецификатор const, все три определения ссылок вызвали бы ошибку компиляции. Однако, причина, по которой компилятор не пропускает таких определений, неясна. Попробуем разобраться.
Для литералов это более или менее понятно: у нас не должно быть возможности косвенно поменять значение литерала, используя указатели или ссылки. Что касается объектов другого типа, то компилятор преобразует исходный объект в некоторый вспомогательный. Например, если мы пишем:

double dval = 1024;

const int &ri = dval;

то компилятор преобразует это примерно так:

int temp = dval;

const int &ri = temp;

Если бы мы могли присвоить новое значение ссылке ri, мы бы реально изменили не dval, а temp. Значение dval осталось бы тем же, что совершенно неочевидно для программиста. Поэтому компилятор запрещает такие действия, и единственная возможность проинициализировать ссылку объектом другого типа – объявить ее как const.
Вот еще один пример ссылки, который трудно понять с первого раза. Мы хотим определить ссылку на адрес константного объекта, но наш первый вариант вызывает ошибку компиляции:

const int ival = 1024;

// ошибка: нужна константная ссылка
int *&pi_ref = &ival;

Попытка исправить дело добавлением спецификатора const тоже не проходит:

const int ival = 1024;

// все равно ошибка

const int *&pi_ref = &ival;

В чем причина? Внимательно прочитав определение, мы увидим, что pi_ref является ссылкой на константный указатель на объект типа int. А нам нужен неконстантный указатель на константный объект, поэтому правильной будет следующая запись:

const int ival = 1024;

// правильно
int *const &piref = &ival;

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

int *pi = 0;

мы инициализируем указатель pi нулевым значением, а это значит, что pi не указывает ни на какой объект. В то же время запись

const int &ri = 0;
означает примерно следующее:
int temp = 0;
const int &ri = temp;

Что касается операции присваивания, то в следующем примере:

int ival = 1024, ival2 = 2048;

int *pi = &ival, *pi2 = &ival2;

pi = pi2;

переменная ival, на которую указывает pi, остается неизменной, а pi получает значение адреса переменной ival2. И pi, и pi2 и теперь указывают на один и тот же объект ival2.
Если же мы работаем со ссылками:

int &ri = ival, &ri2 = ival2;

ri = ri2;

то само значение ival меняется, но ссылка ri по-прежнему адресует ival.
В реальных С++ программах ссылки редко используются как самостоятельные объекты, обычно они употребляются в качестве формальных параметров функций. Например:

// пример использования ссылок

// Значение возвращается в параметре next_value
bool get_next_value(int &next_value);

// перегруженный оператор

Matrix operator+(const Matrix&, const Matrix&);

Как соотносятся самостоятельные объекты-ссылки и ссылки-параметры? Если мы пишем:

int ival;

while (get_next_value(ival))...

это равносильно следующему определению ссылки внутри функции:

int &next_value = ival;

 

Чтобы упростить процесс изменения параметров, С++ вводит такое понятие как ссылка. Как вы узнаете из этого урока, ссылка представляет собой псевдоним (или второе имя), который ваши программы могут использовать для обращения к переменной. К концу данного урока вы освоите следующие основные концепции:

• Для объявления и инициализации ссылки внутри программы объявите переменную, размещая амперсанд (&) сразу же после типа переменной, и затем используйте оператор присваивания для назначения псевдонима, например int& alias_name = variable',.

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

• Внутри функции вам следует объявить параметр как ссылку, размещая амперсанд (&) после типа параметра, затем можно изменять значение параметра внутри функции без помощи указателей.

ПРАВИЛА РАБОТЫ СО ССЫЛКАМИ

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

• Вы не можете получить адрес ссылки, используя оператор адреса C++.

• Вы не можете присвоить ссылке указатель.

• Вы не можете сравнить значения ссылок, используя операторы сравнения C++.

• Вы не можете выполнить арифметические операции над ссылкой, например добавить смещение.

•Вы не можете изменить ссылку.

Например:

а) int a=1; // объявлена целочисленная переменная a; int &b=&a; // объявлена переменная ссылочного типа b, адрес которой совпадает с адресом a, т.е. это алияс a.

б) int *p; // объявлен указатель на поле целого типа; int &r=&p; //

 

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

  // структура объявления ссылок /*тип*/ &/*имя ссылки*/ = /*имя переменной*/;

При объявлении ссылки перед её именем ставится символ амперсанда &, сама же ссылка должна быть проинициализирована именем переменной, на которую она ссылается. Тип данных, на который указывает ссылка, может быть любым, но должен совпадать с объектом, на который ссылается, то есть с типом данных ссылочной переменной. Для удобства, будем называть переменную, на которую ссылается ссылка «ссылочной переменной». Любое изменение значения содержащегося в ссылке повлечёт за собой изменение этого значения в переменной, на которую ссылается ссылка. Разработаем программу, в которой объявим ссылку на объект типа int.

  // №1.cpp: определяет точку входа для консольного приложения.   #include "stdafx.h" #include <iostream> using namespace std;   int main(int argc, char* argv[]) { int value = 15; int &reference = value; // объявление и инициализация ссылки значением переменной value cout << "value = " << value << endl; cout << "reference = " << reference << endl; reference+=15; // изменяем значение переменной value посредством изменения значения в ссылке cout << "value = " << value << endl; // смотрим, что получилось, как будет видно дальше значение поменялось как в ссылке, cout << "reference = " << reference << endl; // так и в ссылочной переменной system("pause"); return 0; }

В строке 10 объявлена ссылка referenceтипаintна переменную value. В строке 13 суммируется значение переменной valueс числом 15, через ссылку reference. Результат работы программы смотреть на рисунке 1.

Рисунок 1 — Ссылки в С++

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

Ссылки, как правило, в большинстве случаев используют в функциях как ссылки-параметры или ссылки-аргументы. Напомню, что в языке программирования С++ в функции передаются данные по значению и по ссылке. Так вот, когда происходит передача по значению, те данные, которые необходимо передать, нужно сначала скопировать, а когда передаётся большой объём данных, то только на передачу затрачивается большое количество времени и ресурсов. В таком случае необходимо использовать передачу по ссылке, в этом случае данные копировать нет необходимости, так как к ним обеспечен прямой доступ, но нарушают безопасность данных, хранимых в ссылочных переменных, так как открывают прямой доступ к этим данным. Хотя далее мы рассмотрим, как обеспечить целостность данных и скорость их передачи. Для этого, разработаем программу, в которой создадим три функции, аргументы в которых будут передаваться по значению и по ссылке. Вдобавок ко всему этому ещё и передачу через указатель осуществим. Частенько возникает путаница между указателями и ссылками, на первый взгляд и ссылки и указатели работают одинаково, но разница все, же есть и весьма значительная.

  // reference.cpp: определяет точку входа для консольного приложения.   #include "stdafx.h" #include <iostream> using namespace std;   int sum_by_value(int);// суммирование по значению int sum_by_reference(int &);// суммирование по ссылке int sum_by_pointer(int *); // суммирование по указателю   int _tmain(int argc, _TCHAR* argv[]) { int value = 10; cout << "sum_by_value = " << sum_by_value(value) << endl; cout << "value = " << value << endl; // значение переменной осталось неизменным cout << "sum_by_reference = " << sum_by_reference(value) << endl; cout << "value = " << value << endl; // значение переменной изменилось cout << "sum_by_pointer = " << sum_by_pointer(&value) << endl; cout << "value = " << value << endl; // значение переменной изменилось ещё раз system("pause"); return 0; }   int sum_by_value(int value)// функция принимающая аргумент по значению { value += value; return value; }   int sum_by_reference(int &reference) // функция принимающая аргумент по ссылке { reference += reference; return reference; }   int sum_by_pointer(int *ptrvalue)// функция принимающая аргумент через указатель { *ptrvalue += *ptrvalue;// арифметика с указателем return *ptrvalue; }

Начальное значение осталось неизменным в случае передачи по значению, тогда как передача по ссылке и через указатель изменили значение передаваемой переменной. Таким образом, нет необходимости использовать глобальные переменные при необходимости изменения значения передаваемой переменной, нужно воспользоваться ссылкой или указателем. В случае использования указателя строка 18 нельзя забывать про операцию взятия адреса, так как аргументом является указатель. В случае со ссылкой, достаточно указать только имя переменной и всё строка 16. Результат работы программы показан на рисунке 2.

Рисунок 2 — Ссылки в С++

В чём же разница между указателями и ссылками? Основное назначение указателя – это организация динамических объектов, то есть размер, которых может меняться (увеличиваться или уменьшаться). Тогда как ссылки предназначены для организации прямого доступа к тому, или иному объекту. Главное отличие состоит во внутреннем механизме работы. Указатели ссылаются на участок в памяти, используя его адрес. А ссылки ссылаются на объект, по его имени (тоже своего рода адрес). Если нет необходимости изменить передаваемое значение в ссылочной переменной, но нужно выиграть в скорости, используйте спецификатор const в объявлении параметров функций. Только так и можно защитить данные от случайного их изменения или полной потере.

 

Вопрос 29. Указатели в языке С++. Доступ к данным по указателю.

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

Указатель существенно связан с типом объекта, на который он ссылается. Если в описании перед обозначением объекта поставить символ "*", то оно будет описывать указатель на объект того же типа и класса памяти, которые соответствуют данному обозначению без звездочки.

Указатели и целые не являются взаимозаменяемыми объектами. Константа - единственное исключение из этого правила: ее можно присвоить указателю и указатель можно сравнить с нулевой константой. Чтобы показать, что это специальное значение для указателя, вместо числа 0, как правило, записывают NULL - константу, определенную, в том числе, в файле stdio.h.

Унарная операция "*", называемая операцией косвенной адресации, рассматривает свой операнд как адрес объекта и обращается по этому адресу, возвращая его содержимое.

Указатель – это переменная, значением которой является адрес некоторого объекта (обычно другой переменной) в памяти компьютера. Подобно тому, как переменная типа char имеет в качестве значения символ, а переменная типа int – целочисленное значение, переменная типа указателя имеет в качестве значения адрес ячейки оперативной памяти. Допустимые значения для переменной-указателя – множество адресов оперативной памяти компьютера.

Указатель является одной из наиболее важных концепций языка C.

Правильное понимание и использование указателей особенно необходимо для составления хороших программ по следующим причинам:

· указатели являются средством, при помощи которого функции могут изменять значения передаваемых в нее аргументов;

· при помощи указателей выполняется динамическое распределение памяти;

· указатели позволяют повысить эффективность программирования;

· указатели обеспечивают поддержку динамических структур данных (двоичные деревья, связные списки).

Однако указатель может вызвать и ряд затруднений, например, если указатель содержит неправильное значение, программа может быть неработоспособной. Можно легко ошибиться при использовании указателей; к тому же ошибки, связанные с неправильными значениями указателей, найти очень трудно.

Итак, указатель – это новый тип данных. Для него определены понятия константы, переменной, массива. Как и любую переменную, указатель необходимо объявить. Объявление указателя состоит из имени базового типа, символа * (звездочка) и имени переменной.

Общая форма объявления указателя:

тип *имя;

Тип указателя определяет тип объекта, на который указатель будет ссылаться, например,

int *p1;

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

Понятие указателя тесно связано с понятием адреса объекта. В C есть специальная операция, позволяющая получить адрес любой переменной:

&p – получение адреса, где p – идентификатор переменной. Результатом операции является адрес переменной p.

Пример программы:

# include <stdio.h>

int main()

{

int x=2,*p;

p=&x;

printf("\n x=%d address of x=%u",x,p);

return 0;

}

Понятие переменной типа указатель также связано с операцией косвенной адресации *, называемой еще операцией разыменования, которая имеет структуру: *р – разыменование, где р – идентификатор переменной-указателя. Эта запись означает, что в ячейку с адресом, записанным в переменную р, помещено значение некоторой величины.

Операторы ptr=&a; и val=*ptr; равнозначны оператору val=a;

Например,

n=32;

p=&n; /* p–адрес ячейки, куда записано n */

v=*p;

В результате выполнения этих действий в переменную v будет помещено число 32.

Над указателями определено 5 основных операций.

§ Определение адреса указателя: &p, где p – указатель (&p – адрес ячейки, в которой находится указатель).

§ Присваивание. Указателю можно присвоить адрес переменной p=&q, где p – указатель, q – идентификатор переменной.

§ Определение значения, на которое ссылается указатель: *p (операция косвенной адресации).

§ Увеличение (уменьшение) указателя. Увеличение выполняется как с помощью операции сложения (+), так и с помощью операции инкремента (++). Уменьшение – с помощью операции вычитания (–) либо декремента (––).

Например, пусть p1 – указатель, тогда р1++ перемещает указатель на:

o 1 байт, если *p1 имеет тип char;

o 4 байта, если *p1 имеет тип int (в 32 разрядной операционной системе) или 2 байта (в 16 разрядной операционной системе);

o 4 байта, если *p1 имеет тип float.

§ Разность двух указателей. Пусть р1 и р2 – указатели одного и того же типа. Можно определить разность р1 и р2, чтобы найти, на каком расстоянии друг от друга находятся элементы массива.

Пример программы.

Даны адреса переменных &a=63384,&b=64390,&c=64404. Что напечатает ЭВМ?

# include <stdio.h>

int main()

{

float a,*p1;

int b,*p2;

char c,*p3;

a=2.5; b=3; c='A';

p1=&a; p2=&b; p3=&c;

p1++; p2++; p3++;

printf("\n p1=%u, p2=%u, p3=%u",p1,p2,p3);

return 0;

}

Ответ: р1=63388, р2=64392, р3=64405.

Операции адресной арифметики подчиняются следующим правилам. После увеличения значения переменной-указателя на 1 данный указатель будет ссылаться на следующий объект своего базового типа. После уменьшения – на предыдущий объект. Для всех указателей адрес увеличивается или уменьшается на величину, равную размеру объекта того типа, на который они указывают. Поэтому указатель всегда ссылается на объект с типом, тождественным базовому типу указателя.

Применительно к указателям на объект типа char операции адресной арифметики выполняются как обычные арифметические операции, потому что длина объекта char всегда равна 1.

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

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

Все остальные операции над указателями запрещены.

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

Указатель на символьную строку.

Как вы уже знаете, указатель содержит адрес памяти. Когда ваша программа передает массив (например, символьную строку) в функцию, C++ передает адрес первого элемента массива. В результате совершенно обычно для функции использовать указатель на символьную строку. Чтобы объявить указатель на символьную строку, функция просто предваряет имя переменной звездочкой, как показано ниже:

void some_function(char *string);

Звездочка, которая предваряет имя переменной, указывает C++, что переменная будет хранить адрес памяти — указатель. Следующая программа PTR_STR.CPP использует указатель на символьную строку внутри функции show_string для вывода содержимого строки по одному символу за один раз:

#include <iostream.h>

void show_string(char *string)

{
while (*string!= '\0')

{
cout << *string;
string++;
}
}

void main(void)

{
show_string("Учимся программировать на языке C++!");
}

Обратите внимание на цикл while внутри функции show_slring. Условие while (*string!= '\0') проверяет, не является ли текущий символ, указываемый с помощью указателя string, символом NULL, который определяет последний символ строки. Если символ не NULL, цикл выводит текущий символ с помощью cout. Затем оператор string++; увеличивает указатель siring таким образом, что он указывает на следующий символ строки. Когда указатель string указывает на символ NULL, функция уже вывела строку и цикл завершается.

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

Указатели могут ссылаться на функции. Имя функции, как и имя массива само по себе является указателем, то есть содержит адрес входа.

  // объявление указателя на функцию /*тип данных*/ (* /*имя указателя*/)(/*список аргументов функции*/);

Тип данных определяем такой, который будет возвращать функция, на которую будет ссылаться указатель. Символ указателя и его имя берутся в круглые скобочки, чтобы показать, что это указатель, а не функция, возвращающая указатель на определённый тип данных. После имени указателя идут круглые скобки, в этих скобках перечисляются все аргументы через запятую как в объявлении прототипа функции. Аргументы наследуются от той функции, на которую будет ссылаться указатель. Разработаем программу, которая использует указатель на функцию. Программа должна находить НОД – наибольший общий делитель. НОД – это наибольшее целое число, на которое без остатка делятся два числа, введенных пользователем. Входные числа также должны быть целыми.

  // pointer_onfunc.cpp: определяет точку входа для консольного приложения.   #include "stdafx.h" #include <iostream> using namespace std; int nod(int, int); // прототип указываемой функции int main(int argc, char* argv[]) { int (*ptrnod)(int, int); // объявление указателя на функцию ptrnod=nod; // присваиваем адрес функции указателю ptrnod int a, b; cout << "Enter first number: "; cin >> a; cout << "Enter second number: "; cin >> b; cout << "NOD = " << ptrnod(a, b) << endl; // обращаемся к функции через указатель system("pause"); return 0; } int nod(int number1, int number2) // рекурсивная функция нахождения наибольшего общего делителя НОД { if (number2 == 0) //базовое решение return number1; return nod(number2, number1 % number2); // рекурсивное решение НОД }

Данная задача решена рекурсивно, чтоб уменьшить объём кода программы, по сравнению с итеративным решением этой же задачи. В строке 9 объявляется указатель, которому в строке 10 присвоили адрес функции. Как мы уже говорили до этого, адресом функции является просто её имя. То есть данный указатель теперь указывает на функцию nod(). При объявлении указателя на функцию ни в коем случае не забываем о скобочках, в которые заключаются символ указателя и его имя. При объявлении указателя в аргументах указываем то же самое, что и в прототипе указываемой функции. Результат работы программы (см. Рисунок 5).

 



Поделиться:


Последнее изменение этой страницы: 2016-08-15; просмотров: 903; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 18.223.0.53 (0.087 с.)