ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 14.05.2021
Просмотров: 571
Скачиваний: 3
Указатели
Объявления и инициализация переменных указателей
Указатели — это переменные, которые содержат в качестве своих значений адреса памяти. С другой стороны, указатель содержит адрес переменной, которая содержит определенное значение. В этом смысле имя переменной отсылает к значению прямо, а указатель — косвенно.
Ссылка на значение посредством указателя называется косвенной адресацией.
Указатели, подобно любым другим переменным, перед своим использованием должны быть объявлены.
Объявление
int *countPtr, count;
объявляет переменную countPtr типа int * (т.е. указатель на целое число) и читается как <<countPtr является указателем на целое число» или <<countPtr указывает на объект типа int*. Однако переменная count объявлена как целое число, но не как указатель на целое число. Символ * в объявлении относится только к countPtr. Каждая переменная, объявляемая как указатель, должна иметь перед собой знак звездочки (*).
Например, объявление
float *xPtr, *yPtr;
указывает, что и xPtr и yPtr являются указателями на значения типа float. Использование * подобным образом в объявлении показывает, что переменная объявляется как указатель. Указатели можно объявлять, чтобы указывать на объекты любого типа данных.
Операции над указателями
& или операция адресации, — унарная операция, которая возвращает адрес своего операнда. Например, если имеются объявления
int у = 5; int *yPtr;
то оператор
yPtr = &y;
присваивает адрес переменной у указателю yPtr. Говорят, что переменная yPtr «указывает» на у.
Рисунок показывает схематическое представление памяти после того, как выполнено указанное выше присваивание. На рисунке показана «связь указателя» с помощью стрелки от указателя к объекту, на который он указывает.
Рисунок показывает представление указателя в памяти в предположении, что целая переменная у хранится в ячейке 600000, а переменная указатель yPtr хранится в ячейке 500000. Операнд операции адресации должен быть L-вeличинoй (т.е. чем-то таким, чему можно присвоить значение так же, как переменной); операция адресации не может быть применена к константам, к выражениям, не дающим результат, на который можно сослаться, и к переменным, объявленным с классом памяти register.
Операция *, обычно называемая операцией косвенной адресации или операцией разыменования, возвращает значение объекта, на который указывает ее операнд (т.е. указатель).
Например, оператор
cout « *yPtr « endl;
печатает значение переменной у, а именно 5. Использование * указанным способом называется разыменованием указателя.
В С++ объекты могут быть
размещены либо статически – во время
компиляции, либо динамически – во время
выполнения программы, путем вызова
функций из стандартной библиотеки.
Основная разница в использовании этих
методов – в их эффективности и гибкости.
Статическое размещение более эффективно,
так как выделение памяти происходит до
выполнения программы, однако оно гораздо
менее гибко, потому что мы должны заранее
знать тип и размер размещаемого объекта.
К примеру, совсем не просто разместить
содержимое некоторого текстового файла
в статическом массиве строк: нам нужно
заранее знать его размер. Задачи, в
которых нужно хранить и обрабатывать
заранее неизвестное число элементов,
обычно требуют динамического выделения
памяти.
До сих пор во всех наших
примерах использовалось статическое
выделение памяти. Скажем, определение
переменной ival
int ival = 1024;
заставляет компилятор
выделить в памяти область, достаточную
для хранения переменной типа int, связать
с этой областью имя ival и поместить туда
значение 1024. Все это делается на этапе
компиляции, до выполнения программы.
С
объектом ival ассоциируются две величины:
собственно значение переменной, 1024 в
данном случае, и адрес той области
памяти, где хранится это значение. Мы
можем обращаться к любой из этих двух
величин. Когда мы пишем:
int ival2 = ival + 1;
то обращаемся к значению,
содержащемуся в переменной ival: прибавляем
к нему 1 и инициализируем переменную
ival2 этим новым значением, 1025. Каким же
образом обратиться к адресу, по которому
размещена переменная?
С++ имеет
встроенный тип “указатель”, который
используется для хранения адресов
объектов. Чтобы объявить указатель,
содержащий адрес переменной ival, мы
должны написать:
int *pint; // указатель на объект типа int
Существует также специальная операция взятия адреса, обозначаемая символом &. Ее результатом является адрес объекта. Следующий оператор присваивает указателю pint адрес переменной ival:
int *pint;
pint = &ival; // pint получает значение адреса ival
Мы можем обратиться к тому объекту, адрес которого содержит pint (ival в нашем случае), используя операцию разыменования, называемую также косвенной адресацией. Эта операция обозначается символом *. Вот как можно косвенно прибавить единицу к ival, используя ее адрес:
*pint = *pint + 1; // неявно увеличивает ival
Это выражение производит в точности те же действия, что и
ival = ival + 1; // явно увеличивает ival
Основные отличия между статическим и динамическим выделением памяти таковы:
-
статические объекты обозначаются именованными переменными, и действия над этими объектами производятся напрямую, с использованием их имен. Динамические объекты не имеют собственных имен, и действия над ними производятся косвенно, с помощью указателей;
-
выделение и освобождение памяти под статические объекты производится компилятором автоматически. Программисту не нужно самому заботиться об этом. Выделение и освобождение памяти под динамические объекты целиком и полностью возлагается на программиста. Это достаточно сложная задача, при решении которой легко наделать ошибок. Для манипуляции динамически выделяемой памятью служат операторы new и delete.
Оператор new имеет две формы. Первая форма выделяет память под единичный объект определенного типа:
int *pint = new int(1024);
Здесь
оператор new выделяет память под безымянный
объект типа int, инициализирует его
значением 1024 и возвращает адрес созданного
объекта. Этот адрес используется для
инициализации указателя pint. Все действия
над таким безымянным объектом производятся
путем разыменовывания данного указателя,
т.к. явно манипулировать динамическим
объектом невозможно.
Вторая форма
оператора new выделяет память под массив
заданного размера, состоящий из элементов
определенного типа:
int *pia = new int[4];
В
этом примере память выделяется под
массив из четырех элементов типа int. К
сожалению, данная форма оператора new не
позволяет инициализировать элементы
массива.
Некоторую путаницу вносит
то, что обе формы оператора new возвращают
одинаковый указатель, в нашем примере
это указатель на целое. И pint, и pia объявлены
совершенно одинаково, однако pint указывает
на единственный объект типа int, а pia –
на первый элемент массива из четырех
объектов типа int.
Когда динамический
объект больше не нужен, мы должны явным
образом освободить отведенную под него
память. Это делается с помощью оператора
delete, имеющего, как и new, две формы – для
единичного объекта и для массива:
// освобождение единичного объекта
delete pint;
// освобождение массива
delete[] pia;
Что
случится, если мы забудем освободить
выделенную память? Память будет
расходоваться впустую, она окажется
неиспользуемой, однако возвратить ее
системе нельзя, поскольку у нас нет
указателя на нее. Такое явление получило
специальное название утечка
памяти.
В конце концов программа аварийно
завершится из-за нехватки памяти (если,
конечно, она будет работать достаточно
долго). Небольшая утечка трудно поддается
обнаружению, но существуют утилиты,
помогающие это сделать.
Упражнение
Объясните разницу между четырьмя объектами:
(a) int ival = 1024;
(b) int *pi = &ival;
(c) int *pi2 = new int(1024);
(d) int *pi3 = new int[1024];
Упражнение
Что делает следующий фрагмент кода? В чем состоит логическая ошибка?
int *pi = new int(10);
int *pia = new int[10];
while
( *pi < 10 ) {
pia[*pi] = *pi;
*pi
= *pi + 1;
}
delete pi;
delete[] pia;
Указатель
–
это объект, содержащий адрес другого
объекта и позволяющий косвенно
манипулировать этим объектом. Обычно
указатели используются для работы с
динамически созданными объектами, для
построения связанных структур данных,
таких, как связанные списки и иерархические
деревья, и для передачи в функции больших
объектов – массивов и объектов классов
– в качестве параметров.
Каждый
указатель ассоциируется с некоторым
типом данных, причем их внутреннее
представление не зависит от внутреннего
типа: и размер памяти, занимаемый объектом
типа указатель, и диапазон значений у
них одинаков . Разница состоит в том,
как компилятор воспринимает адресуемый
объект. Указатели на разные типы могут
иметь одно и то же значение, но область
памяти, где размещаются соответствующие
типы, может быть различной:
-
указатель на int, содержащий значение адреса 1000, направлен на область памяти 1000-1003 (в 32-битной системе);
-
указатель на double, содержащий значение адреса 1000, направлен на область памяти 1000-1007 (в 32-битной системе).
Вот несколько примеров:
int *ip1, *ip2;
complex<double> *cp;
string *pstring;
vector<int> *pvec;
double *dp;
Указатель обозначается звездочкой перед именем.
В примере ниже lp – указатель на объект типа long, а lp2 – объект типа long:
long *lp, lp2;
В следующем случае fp интерпретируется как объект типа float, а fp2 – указатель на него:
float fp, *fp2;
Оператор разыменования (*) может отделяться пробелами от имени и даже непосредственно примыкать к ключевому слову типа. Поэтому приведенные определения синтаксически правильны и совершенно эквивалентны:
string *ps;
string* ps;
Однако рекомендуется использовать первый вариант написания: второй способен ввести в заблуждение, если добавить к нему определение еще одной переменной через запятую:
//внимание: ps2 не указатель на строку!
string* ps, ps2;
Можно
предположить, что и ps, и ps2 являются
указателями, хотя указатель – только
первый из них.
Если
значение указателя равно 0, значит, он
не содержит никакого адреса объекта.
Пусть
задана переменная типа int:
int ival = 1024;
Ниже приводятся примеры определения и использования указателей на int pi и pi2:
//pi инициализирован нулевым адресом
int *pi = 0;
//
pi2 инициализирован адресом ival
int *pi2
= &ival;
//
правильно: pi и pi2 содержат адрес ival
pi
= pi2;
//
pi2 содержит нулевой адрес
pi2 = 0;
Указателю не может быть присвоена величина, не являющаяся адресом:
// ошибка: pi не может принимать значение int
pi = ival
Точно так же нельзя присвоить указателю одного типа значение, являющееся адресом объекта другого типа. Если определены следующие переменные:
double dval;
double *ps = &dval;
то оба выражения присваивания, приведенные ниже, вызовут ошибку компиляции:
// ошибки компиляции
// недопустимое присваивание типов данных: int* <== double*
pi = pd
pi = &dval;
Дело
не в том, что переменная pi не может
содержать адреса объекта dval – адреса
объектов разных типов имеют одну и ту
же длину. Такие операции смешения адресов
запрещены сознательно, потому что
интерпретация объектов компилятором
зависит от типа указателя на них.
Конечно,
бывают случаи, когда нас интересует
само значение адреса, а не объект, на
который он указывает (допустим, мы хотим
сравнить этот адрес с каким-то другим).
Для разрешения таких ситуаций введен
специальный указатель void, который может
указывать на любой тип данных, и следующие
выражения будут правильны:
// правильно: void* может содержать
// адреса любого типа
void *pv = pi;
pv = pd;
Тип объекта, на который указывает void*, неизвестен, и мы не можем манипулировать этим объектом. Все, что мы можем сделать с таким указателем, – присвоить его значение другому указателю или сравнить с какой-либо адресной величиной.
Для того чтобы обратиться к объекту, имея его адрес, нужно применить операцию разыменования, или косвенную адресацию, обозначаемую звездочкой (*). Имея следующие определения переменных:
int ival = 1024;, ival2 = 2048;
int *pi = &ival;
мы можем читать и сохранять значение ival, применяя операцию разыменования к указателю pi:
// косвенное присваивание переменной ival значения ival2
*pi = ival2;
//
косвенное использование переменной
ival как rvalue и lvalue
*pi = abs(*pi); // ival =
abs(ival);
*pi = *pi + 1; // ival = ival + 1;
Когда
мы применяем операцию взятия адреса
(&) к объекту типа int, то получаем
результат типа int*
int *pi = &ival;
Если
ту же операцию применить к объекту типа
int* (указатель на int), мы получим указатель
на указатель на int, т.е. int**. int** – это
адрес объекта, который содержит адрес
объекта типа int. Разыменовывая ppi, мы
получаем объект типа int*, содержащий
адрес ival. Чтобы получить сам объект
ival, операцию разыменования к ppi необходимо
применить дважды.
int **ppi = π
int *pi2 = *ppi;
cout
<< "Значение ival\n"
<<
"явное значение: " << ival <<
"\n"
<< "косвенная адресация:
" << *pi << "\n"
<<
"дважды косвенная адресация: " <<
**ppi << "\n"
<< endl;
Указатели могут быть использованы в арифметических выражениях. Обратите внимание на следующий пример, где два выражения производят совершенно различные действия:
int i, j, k;
int *pi = &i;
//
i = i + 2
*pi = *pi + 2;
//
увеличение адреса, содержащегося в pi,
на 2
pi = pi + 2;
К
указателю можно прибавлять целое
значение, можно также вычитать из него.
Прибавление к указателю 1 увеличивает
содержащееся в нем значение на размер
области памяти, отводимой объекту
соответствующего типа. Если тип char
занимает 1 байт, int – 4 и double – 8, то
прибавление 2 к указателям на char, int и
double увеличит их значение соответственно
на 2, 8 и 16. Как это можно интерпретировать?
Если объекты одного типа расположены
в памяти друг за другом, то увеличение
указателя на 1 приведет к тому, что он
будет указывать на следующий объект.
Поэтому арифметические действия с
указателями чаще всего применяются при
обработке массивов; в любых других
случаях они вряд ли оправданы.
Вот
как выглядит типичный пример использования
адресной арифметики при переборе
элементов массива с помощью итератора:
int ia[10];
int *iter = &ia[0];
int *iter_end = &ia[10];
while
(iter != iter_end) {
do_something_with_value (*iter);
++iter;
}
Упражнение
Даны определения переменных:
int ival = 1024, ival2 = 2048;
int *pi1 = &ival, *pi2 = &ival2, **pi3 = 0;
Что происходит при выполнении нижеследующих операций присваивания? Допущены ли в данных примерах ошибки?
(a) ival = *pi3; (e) pi1 = *pi3;
(b) *pi2 = *pi3; (f) ival = *pi1;
(c) ival = pi2; (g) pi1 = ival;
(d) pi2 = *pi1; (h) pi3 = &pi2;
Упражнение
Работа с указателями – один из важнейших аспектов С и С++, однако в ней легко допустить ошибку. Например, код
pi = &ival;
pi = pi + 1024;
почти наверняка приведет к тому, что pi будет указывать на случайную область памяти. Что делает этот оператор присваивания и в каком случае он не приведет к ошибке?
Упражнение
Данная программа содержит ошибку, связанную с неправильным использованием указателей:
int foobar(int *pi) {
*pi = 1024;
return *pi;
}
int
main() {
int *pi2 = 0;
int ival = foobar(pi2);
return 0;
}
В чем состоит ошибка? Как можно ее исправить?
Строковые типы
В С++ поддерживаются два типа строк – встроенный тип, доставшийся от С, и класс string из стандартной библиотеки С++. Класс string предоставляет гораздо больше возможностей и поэтому удобней в применении, однако на практике нередки ситуации, когда необходимо пользоваться встроенным типом либо хорошо понимать, как он устроен.
Встроенный строковый тип
Как уже было сказано, встроенный строковый тип перешел к С++ по наследству от С. Строка символов хранится в памяти как массив, и доступ к ней осуществляется при помощи указателя типа char*. Стандартная библиотека С предоставляет набор функций для манипулирования строками. Например:
// возвращает длину строки
int strlen( const char* );
//
сравнивает две строки
int strcmp( const
char*, const char* );