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


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



ЗНАЕТЕ ЛИ ВЫ?

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



Указатель – это адрес поля памяти, занимаемого программным объектом.

Пусть в программе объявлены переменные:

int sum=255;

short age;

double averge;

int * ptrSum;

В памяти компьютера они располагаются как показано на рисунке 1.1.

Рисунок 1.1 – Расположение в памяти переменных.

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

тип *имя_переменной;

int * pti; // Указатель на переменную целого типа

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

int * p 1, * p 2, i; p 1 и p 2 – указатели на переменную типа int, i – переменная типа int. int * p 1, p 2, i; p 1 указатель на переменную типа int, p 2 и i – переменные типа int.

Соглашения по именованию указательных переменных предполагает использовать префикс p или суффикс Ptr. Например, iPtr, numberPtr, pNumber, pStudent.

Операции над указателями. Знаком &(амперсанд) обозначается операция взятия адреса. Применение ее к переменной дает в результате адрес переменной в памяти.

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

pti =& sum; //будет 90000000

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

int number;

int *pNumber=&number;

Рисунок 2 – Расположение в памяти указательных переменных

В библиотеке <stdio.h>определена константа NULL – нулевой указатель. Означает отсутствие конкретного адреса.

float * ptf;

ptf = NULL; // указатель ptf ни на что не указывает

Хорошей практикой программирования считается инициализация указателя значением NULL во время объявления.

В C++11 введено новое ключевое слово nullptr для обозначения отсутствия адреса.

Знаком * обозначается операция разадресации. С ее помощью можно получить значение переменной, расположенной по адресу, на которой ссылается указатель.

number =10;

int x;

x = number +2; //аналогично x =* pNnumber +2, в переменной x будет 12

* pNumber =5; //аналогично number =5, в переменной number будет 5

cout<< number; // выведет 5

cout<<pNumber <<&number);

 

вывод адреса в памяти

Ввод-вывод значений можно выполнять как через имена переменных, так и через их адреса:

cin>> number >>*pNumber;

cout<< number <<*pNumber;

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

Указатели и массивы. Имя массива в языке С++ трактуется как указатель-константа на массив.

int X [10];

X эквивалентно &X[0];

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

имя_массива[индекс]          или *(имя_массива+индекс)

int X [10];

X [4]   или    *(X +4)

В языке С++ символ «[» играет роль знака операции сложения адреса массива с индексом элемента массива. Поэтому индексация в массивах в языке С++ всегда начинаются с нуля. Поскольку имя массива является указателем-константой, то его в программе нельзя изменять, т.е. ему нельзя ничего присваивать

int X[10], Y[10];

X=Y//нельзя делать так!

Пересылать значение одного массива в другой можно только поэлементно. Аналогично со строками.

float x; float * ptx =& x;

float A []={0.1,2.3,3.4,4.5};

x=*A;// в x 0.1

*ptx=*ptx*2;// //в x 0.2

cout<<"x="<<x<<endl;// выведет x=0.2

cout <<*(A +2)<<" "<<* A +2;//выведет3.4 2.1

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

int A []={1,2,3,4,5,6,7,8};

int * X = A;

X +=2;// Переадресуем Х на второй элемент массива А.

cout <<* X;

В результате на экран будет выведено:

3

Пример сравнения указателей:

if (X == A) cout <<"Указатель Х указывает на начало массива А.";

else cout <<"Указатель Х не указывает на начало массива А.";

В результате на экран будет выведено:

Указатель Х не указывает на начало массива А.

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

Синтаксис объявления ссылки:

имя_типа &имя_ссылки= имя_переменной;

#include <iostream> using namespace std; int main () { int number = 88;     // объявление целочисленной переменной number int & refNumber = number; // объявление ссылки на переменную number /*И refNumber и number ссылаются (указывают) на одну и туже область памяти, на одно и тоже значение. */ //Вывод на экран значения переменной number cout << number << endl; // используется имя переменной (88) cout << refNumber << endl; // используется ссылка на переменную(88) //присваивание нового значения переменной number через ссылку refNumber = 99; cout<<refNumber<<endl; cout<<number<<endl; // Значение переменной number также изменено (99) //присваивание нового значения переменной number через имя переменной number = 55;    cout << number << endl; cout<<refNumber;// Значение по ссылке refNumber также изменено (55)

}

Рисунок 1.3 – Использование переменнах и ссылок

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

Ссылки работают также как и указатели: содержат адреса переменных.

Рисунок 1.4 – Размещение в памяти переменных и ссылок.

Различия между ссылками и указателями:

1. Ссылка – это имя-константа для адреса, поэтому ссылку необходимо инициализировать при объявлении. Команда

int & iRef;

приведет к ошибке «Error: 'iRef' declared as reference but not initialized».

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

2. При работе с указателями, для получения значения по адресу, используется операция *. При работе со ссылками операции взятие адреса и разадресация происходят автоматически.

#include <iostream> using namespace std; int main() { int number1 = 88, number2 = 22; // Создаем указатель на переменную number 1 int * pNumber 1 = & number 1;  // явная ссылка *pNumber1 = 99;        // явное разыменование cout << *pNumber1 << endl;  // 99 cout <<&number1 << endl;  // 0x22ff18 cout << pNumber 1 << endl;  // 0 x 22 ff 18 (содержимое указателлное переменной) cout <<& pNumber 1 << endl;  // 0 x 22 ff 10 (адрес указетльной переменной) pNumber 1 = & number 2;// указателю присваиваем другой адрес // Создаем ссылку на  number1 int & refNumber1 = number1;  // неявная ссылка  (NOT &number1) refNumber1 = 11;        // неявное разыменование  (NOT *refNumber1) cout << refNumber1 << endl;  // 11 cout <<&number1 << endl;  // 0x22ff18 cout <<&refNumber1 << endl; // 0x22ff18 //refNumber1 = &number2; // Error! Reference cannot be re-assigned // error: invalid conversion from 'int*' to 'int' refNumber1 = number2;   // refNumber1 ссылка (псевдоним) number1. // Присваиваем значение number2 (22) refNumber1 (и number1). number2++;    cout << refNumber1 << endl; // 22 cout << number1 << endl; // 22 cout << number 2 << endl; // 23

}

Ссылка обеспечивает новое имя переменной. Разыменование происходит неявно, поэтому нет необходимости использовать оператор * для получения значения по адресу. Другими словами указательная переменная хранит адрес. В ходе выполнения программы адрес может быть изменен. Чтобы получить значение по адресу требуется оператор *. Ссылка интерпретируется как указатель-константа, который должен быть проинициализирован при объявлении, и его значение не может быть изменено в ходе выполнения программы.

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

Раздел оперативной памяти, распределяемый статически, называется статической памятью, а динамически распределяемый – динамической памятью.

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

Рисунок 1. Расположение динамических данных в памяти.

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

Использование динамических величин предоставляет ряд преимуществ:

 - подключение динамической памяти позволяет увеличить объем обрабатываемых данных;

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

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

Приведенный ниже фрагмент программного кода содержит грубую логическиую ошибку:

int * iPtr; *iPtr = 55; cout << *iPtr << endl;

Указатель iPtr объявлен без инициализации, то есть указывает на произвольную область в памяти, что является недоступной областью памяти. Оператор * iPtr = 55; разрушает значение этой области памяти. Необходимо инициализировать указатель с корректным адресом. Большинство компиляторов выдают сообщение об ошибке или предупреждение в этом случае. В этом случае память под переменную нужно выделить динамически.

В С++ для выделения динамической памяти используется операторы new и new []. Выделить память под переменную целого типа можно:

int * p = new int;

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

Для освобождения в языке С++ используется операторы delete и delete [].

Освободить память, на которую указывает указатель p можно:

delete p;

Оператор new возвращает указатель на выделенную область памяти. Оператор delete принимает в качестве параметра указатель, как свой единственный аргумент. Операторы new and delete работают только с указателями.

Динамическое выделение памяти для массивов. Массив – это структура однотипных элементов, занимающих непрерывную область памяти. Название массива – это указатель на нулевой элемент массива доступ к элементам массива осуществляется через индексные имена. Чтобы выполнялись все выше сказанные условия, при динамическом распределении памяти для массивов следует объявить указатель соответствующего типа и присваивать ему значение, выделив блок памяти при помощи оператора new []. Одномерный массив a[10] из элементов типа float можно создать следующим образом:

float * a = new float [10];

Освободить память, выделенную под массив а можно с помощью оператора delete []:

delete [] a;

В более ранних версиях C++ нет возможности проинициализировать динамический массив. В С++ 11 допускается следующая конструкция:

int * p = new int [5] {1, 2, 3, 4, 5};

При работе с динамическими величинами следует принимать во внимание:

 - если динамическая величина теряет свой указатель, то она становится «мусором». То есть адрес в памяти безвозвратно утерян, информация уже не нужна, но занимает память.

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

 - при выполнении может быть сделано достаточно много запросов на выделение памяти, чтобы в конце концов исчерпать имеющуюся в наличии память. В этом случае пользователь получит сообщение «Out of memory», а программа вынужденно завершится.

Основные различия между статическим и динамическим размещением:

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

Обращение и обработка статически размещенных объектов осуществляется через их имена, а динамически размещенных – через указатели.

 

Контрольные вопросы и задания

1. Что такое указатели?

2. Приведите синтаксис объявления и использования указательных переменных в языке С++.

3. Что такое ссылки? Чем они отличаются от указателей?

4. Приведите синтаксис объявления и использования ссылок в языке С++.

5. Приведите различия между статическим и динамическим способами выделения памяти под программные объекты.

6. Какие средства для работы с динамическими переменными имеются в языке С++.

Функции

 

Процедурная методология программирования подразумевает, что программа разделяется на логические единицы или функции. В языке С++ функция является основной программной единицей, минимальным исполняемым программным модулем и представляет собой набор команд для выполнения некоторого вспомогательного алгоритма. При вызове функции ей при помощи аргументов (формальных параметров) могут быть переданы некоторые значения (фактические параметры), используемые во время выполнения функции. Функция может возвращать некоторое (одно!) значение. Это возвращаемое значение и есть результат выполнения функции, который при выполнении программы подставляется в точку вызова функции, где бы этот вызов ни встретился.

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

#include<iostream>

using namespace std;

//объявляется функция, вычисляющая максимум из двух переменных

int Max(int x, int y){

// нахождение максимума из двух переменных

if (x > y) return x;

else return y;

}

int main(){

int a, b, c;

cout <<"Введите три целых числа";

cin >> a >> b >> c;

/*вызывается функция вычисления максимума. Результат вычисления выводится на экран*/

cout <<"Максимальное значение"<< Max (Max (a, b), c);

}

Хорошим стилем программирования считается, когда функция

1. принимает параметры;

2. вычисляет по алгоритму некоторое значение;

3. возвращает вычисленное значение в точку вызова.

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

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

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

Синтаксис определения функции:

т ип имя (спецификация параметров){

Тело функции

}

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

Имя функции задавается программистом, либо main для основной функции. В качестве имени функции (имя_функции) может использоваться любой допустимый идентификатор. Рекомендуется, чтобы имя функции было глаголом, или фразой, выражающей действие. Например, getArea(), setRadius(), moveDown(), isPrime().

Спецификация параметров – это либо «пусто» либо список имен формальных параметров с указанием типа для каждого из них.

Тело функции – это либо составной оператор, либо блок, внутри которого действуют программные объекты. В С++ тело функции не может содержать в себе определение другой функции. Из всякой функции можно обращаться к другим функциям.

Строка т ип имя (спецификация параметров) – называют заголовок функции (header). Следует обратить внимание, что при объявлении функции между заголовком и телом функции; не ставится!

Оператором возврата в точку вызова является оператор return, который имеет следующий синтаксис:

1. Если функция не возвращает никакого значения:

return;

2. Если функция возвращает значение:

return выражение;

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

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

В теле функции может быть несколько операторов return. Т.е. может быть несколько мест выхода из функции.

Формат вызова функции:

имя(список фактических параметров);

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

Выполнение вызова функции происходит следующим образом:

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

2) Происходит присваивание значений фактических параметров соответствующим формальным параметрам.

3) Управление передается на первый оператор функции.

4) Выполнение оператора return в теле функции возвращает управление и значение в вызывающую программу. При отсутствии оператора return управление возвращается после выполнения последнего оператора тела функции, а возвращаемое значение не определено.

В С++ передача параметров происходит только по значению. Т.е выполнение функции не может изменить значение переменных, указанных в качестве фактических параметров. При вызове функции в стеке выделяется место для формальных параметров функции, и в это выделенное место заносится значение фактического параметра, т. е. значение параметра при вызове функции. Далее функция использует и изменяет значения в стеке. При выходе из функции измененные значения параметров теряются.

Прототип функции. Прототипом называется предварительное объявление функции, в котором содержатся все необходимые сведения для обращения к ней: имя и тип, а также типы формальных параметров. В прототипе имена формальных параметров ставить необязательно. В конце прототипа обязательно ставится «;». Прототип можно записывать и в теле основной функции, наряду с объявлением других программных объектов. Вопрос о размещении связан с областью видимости: функция должна быть объявлена, до того момента когда она вызывается.

Пример. Создать функцию для вывода на экран n раз символ c. Продемонстрировать работу данной функции в главной программе.

#include <stdio.h>

void line(int,char); // прототип функции line().

int main(){

line(10,'*'); // вызов функции line()

}

// определение функции line()

void line(int n, char c){

/* функция выводит на экран n раз символ c */

for (int i=0;i<n;i++){ cout<<c;}

}

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

# include < iostream >

using namespace std;

// Прототипы функции – содержат значения аргументов по умолчанию

int fun1(int = 1, int = 2, int = 3);

int fun2(int, int, int = 3);

int main() {

cout << fun1(4, 5, 6) << endl; // No default

cout << fun1(4, 5) << endl; // 4, 5, 3(default)

cout << fun1(4) << endl;  // 4, 2(default), 3(default)

cout << fun1() << endl;   // 1(default), 2(default), 3(default)

cout << fun2(4, 5, 6) << endl; // No default

cout << fun2(4, 5) << endl; // 4, 5, 3(default)

// cout << fun2(4) << endl;

// error: too few arguments to function 'int fun2(int, int, int)'

}

int fun1(int n1, int n2, int n3) {

// значения аргументов по умолчанию по умолчанию не дублируются

return n1 + n2 + n3;

}

int fun2(int n1, int n2, int n3) {

return n 1 + n 2 + n 3;

}

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



Поделиться:


Последнее изменение этой страницы: 2019-05-20; просмотров: 893; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

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