Указатели на типы данных си. Операции с указателями

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

Что такое указатели и зачем они нужны?

Указатели похожи на метки, которые ссылаются на места в памяти. Представьте сейф с депозитными ячейками различного размера в местном банке. Каждая ячейка имеет номер, уникальный номер, который связан только с этой ячейкой, таким образом можно быстро идентифицировать нужную ячейку. Эти цифры аналогичны адресам ячеек компьютерной памяти. К примеру, у вас есть богатый дядя, который хранит все свои ценности в своем сейфе. И чтобы обезопасить все свои сбережения, он решил завести меньший сейф, в который положит карту, на которой показано местоположение большого сейфа и указан 16-й пароль от большого сейфа, в котором и хранятся реальные драгоценности. По сути, сейф с картой будет хранить расположение другого сейфа. Вся эта организация сбережения драгоценностей эквивалентна указателям в языке Си. В компьютере, указатели просто переменные, которые хранят адреса памяти, как правило, адреса других переменных.

Идея в том, что зная адрес переменной, вы можете пойти по этому адресу и получить данные, хранящиеся в нем. Если вам нужно передать огромный кусок данных в функцию, намного проще передать адрес в памяти, по которому хранятся эти данные, чем скопировать каждый элемент данных! Более того, если программе понадобится больше памяти, вы можете запросить больше памяти из системы. Как же это работает? Система просто возвращает адрес ячейки памяти, и мы должны сохранить этот адрес в переменной-указателе. Так мы сможем взаимодействовать с данными из указанного участка памяти.

Синтаксис указателей

Если у нас есть указатель, значит мы можем получить его адрес в памяти и данные на которые он ссылается, по этой причине указатели имеют несколько необычный синтаксис, отличающийся от объявления простых переменных. Более того, поскольку указатели — это не обычные переменные, то, необходимо сообщить компилятору, что переменная является указателем и сообщить компилятору тип данных, на которые ссылается указатель. Итак, указатель объявляется следующим образом:

Data_type *pointerName;

где, data_type — тип данных, pointerName — имя указателя.

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

Int *integerPointer;

Обратите внимание на использование символа * , при объявлении указателя. Этот символ является ключевым в объявлении указателя. Если в объявлении переменной, непосредственно перед именем переменной, добавить этот символ, то переменная будет объявлена как указатель. Кроме того, если вы объявляете несколько указателей в одной строке, каждый из них должен предваряться символом звездочки. Рассмотрим несколько примеров:

// Объявление указателя и простой переменной в одной строке int *pointer1, // это указатель variable; // это обычная переменная типа int // Объявление двух указателей в одно строке int *pointer1, // это указатель с именем pointer1 *pointer2; // это указатель с именем pointer2

Как я и говорил, если имя переменной не предваряется символом * , то это обычная переменная, в противном случае — это указатель. Именно это и показывает пример объявления указателей, выше.

Есть два способа использования указателя:

  1. Использовать имя указателя без символа * , таким образом можно получить фактический адрес ячейки памяти, куда ссылается указатель.
  2. Использовать имя указателя с символом * , это позволит получить значение, хранящееся в памяти. В рамках указателей, у символа * есть техническое название — операция разыименования. По сути, мы принимаем ссылку на какой-то адрес памяти, чтобы получить фактическое значение. Это может быть сложно для понимания, но в дальнейшем постараемся разобраться во всем этом.

Объявление указателя, получение адреса переменной

Для того чтобы объявить указатель, который будет ссылаться на переменную, необходимо сначала получить адрес этой переменной. Чтобы получить адрес памяти переменной (её расположение в памяти), нужно использовать знак & перед именем переменной. Это позволяет узнать адрес ячейки памяти, в которой хранится значение переменной. Эта операция называется — операция взятия адреса и выглядит вот так:

Int var = 5; // простое объявление переменной с предварительной инициализацией int *ptrVar; // объявили указатель, однако он пока ни на что не указывает ptrVar = &var; // теперь наш указатель ссылается на адрес в памяти, где хранится число 5

В строке 3 использовалась операция взятия адреса, мы взяли адрес переменной var и присвоили его указателю ptrVar . Давайте рассмотрим программу, которая наглядно покажет всю мощь указателей. Итак, вот исходник:

#include int main() { int var; // обычная целочисленная переменная int *ptrVar; // целочисленный указатель (ptrVar должен быть типа int, так как он будет ссылаться на переменную типа int) ptrVar = &var; // присвоили указателю адрес ячейки в памяти, где лежит значение переменной var scanf("%d", &var); // в переменную var положили значение, введенное с клавиатуры printf("%d\n", *ptrVar); // вывод значения через указатель getchar(); }

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

В строке 10 , printf() выводит значение, хранящееся в переменной var . Почему так происходит? Что ж, давайте посмотрим на код. В строке 5 , мы объявили переменную var типа int . В строке 6 — указатель ptrVar на целое значение. Затем указателю ptrVar присвоили адрес переменной var , для этого мы воспользовались оператором присвоения адреса. Затем пользователь вводит номер, который сохраняется в переменную var , помните, что это то же самое место, на которое указывает ptrVar . В самом деле, так как мы используем амперсанд чтобы присвоить значение переменной var в функции scanf() , должно быть понятно, что scanf() инициализирует переменную var через адрес. На этот же адрес указывает указатель ptrVar .

Затем, в строке 10 , выполняется операция «разыменования» — *ptrVar . Программа, через указатель ptrVar , считывает адрес, который хранится в указателе, по адресу попадает в нужную ячейку памяти, и возвращает значение, которое там хранится.

Обратите внимание, что в приведенном выше примере, перед тем как использовать указатель, он сначала инициализируется, это нужно для того, чтобы указатель ссылался на определенный адрес памяти. Если бы мы начали использовать указатель так и не инициализировав его, он бы ссылался на какой угодно участок памяти. И это могло бы привести к крайне неприятным последствиям. Например, операционная система, вероятно, помешает вашей программе получить доступ к неизвестному участку памяти, так как ОС знает, что в вашей программе не выполняется инициализация указателя. В основном это просто приводит к краху программы.

Если бы такие приемчики были позволены в ОС, вы могли бы получить доступ к любому участку памяти. А это значит, что для любой запущенной программы вы могли бы внести свои изменения, например, если у вас открыт документ в Word, вы могли бы изменить любой текст программно. К счастью, Windows и другие современные операционные системы остановит вас от доступа к этой памяти и преждевременно закроют вашу программу.

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

P.S.:Если у вас нет денег на телефоне, и нет возможности его пополнить, но при этом, вам срочно нужно позвонить, вы всегда можете использовать доверительный платеж билайн . Сумма доверительного платежа может быть самой разнообразной, от 50 до 300р.

Немного о памяти

Память можно представить по-разному.

Объяснение для военных на примере взвода. Есть взвод солдат. Численность - 30 человек. Построены в одну шеренгу. Если отдать им команду рассчитаться, у кажого в этой шеренге будет свой уникальный номер. Обязательно у каждого будет и обязательно уникальный. Этот взвод - доступная нам память. Всего нам здесь выделено для работы 30 ячеек. Можно использовать меньше. Больше - нельзя. К каждой ячейке можно обратиться и быть уверенным, что обратился именно к ней. Любому солдату можно дать что-то в руки. Например, цветы. То есть поместить по адресу данные.

Объяснение для Маленького Принца. Здравствуй, Маленький Принц. Представим, что твоему барашку стало одиноко. И ты попросил нарисовать ему друзей. Ты выделил для барашков целую планету (точнее, астероид) по соседству. Эта планета - доступная память. Вся она уставлена коробочками, в которых будут жить барашки. Чтобы не запутаться, все коробочки пронумерованы. Коробочки - это ячейки памяти. Барашек в коробочке - это данные. Допустим, что попался какой-то особо упитанный барашек. Ему понадобится две коробочки. Или даже больше. Барашек - неделимая структура (для нас с тобой, Маленький Принц, это точно так), а коробочки идут подряд. Нет ничего проще. Мы вынимает стенки между двумя рядом стоящими коробочками и кладем туда барашка. Места в коробочке не очень много. И барашек не может свободно развернуться. Поэтому мы всегда знаем, где его голова, а где хвост. И если нам что-то нужно будет сказать барашку, мы обратимся к той коробочке, где у него голова.

Объяснение для хулиганов. Есть забор. Забор из досок. Забор - доступная память. Доска - ячейка памяти. Забор длинный. И чтобы потом похвастаться друзьям, где ты сделал надпись, надо как-то обозначить место. Я знаю, о уважаемый хулиган, что ты нашел бы что-то поинтереснее, чем нумеровать каждую доску. Но в программировании не такие выдумщики. Поэтому доски просто пронумерованы. Возможно, твоя надпись поместится на одну доску. Например, %знак футбольного клуба%. Тогда ты просто скажешь номер и друзья увидят серьезность твоего отношения к футболу. А возможно, что одной доски не хватит. Ничего, главное, чтобы хватило забора. Пиши подряд. Просто потом скажи, с какой доски читать. А что если не подряд? Бывает и не подряд. Например, ты хочешь признаться Маше в любви. Ты назначаешь ей встречу под доской номер 40. Если все пройдет хорошо, ты возьмешь Машу и поведешь ее к доске 10, где заранее написал «Хулиган + Маша = любовь». Если что-то пошло не так, ты поведешь Машу к доске 60, на которой написано все нехорошее, что ты думаешь о Маше. Примерно так выглядит условный переход. То есть оба его исхода помещаются в память заранее. На каком-то этапе вычисляется условие. Если условие выполнилось - переходим к одному месту памяти и начинаем идти дальше подряд. Если условие не выполнилось - переходим к другому месту, с другими инструкциями. И тоже продолжаем выполнять их подряд. Инструкции всегда выполняются одна за другой, если только не встретился переход (с условием или без условия). Ну, или что-то поломалось.

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

В памяти хранятся числа. Ни с чем кроме чисел компьютер работать не умеет. Если вы поместили в память какую-то комплексную структуру, она все равно будет представлена числами. Даже если вы работаете с ней как со структурой. Примером комплексной структуры в терминах языков C и C++ может быть, например, экземпляр структуры или объект класса.

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

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

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

Система в компьютере двоичная (хотя есть и тернарные машины). В 1 байте 8 бит. Английское bit означает binary digit, то есть двоичный разряд. Получается, что байт может принимать числовые значения от 0 до 2 в 8 степени без единицы. То есть от 0 до 255. Если представлять числа в шестнадцатеричной системе, то от 0x00 до 0xFF.

Представим область памяти.

0x01 0x02 0x03 0x04
0x05 0x06 0x07 0x08
0x09 0x0A 0x0B 0x0C
0x0D 0x0E 0x0F 0x10

В ней лежат числа от 1 до 16. Направление обхода обычно задается слева направо и сверху вниз. Помните, что никакой таблицы на самом деле нет (почти как ложки в Матрице). Она нужна человеку для удобства восприятия. Каждая такая ячейка описывается двумя величинами: значением и адресом. В приведенной таблице значение и адрес совпадают.

Понятие указателя

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

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

Int *pointerToInteger;

Здесь объявляется переменная pointerToInteger. Ее тип - указатель на переменную типа int.

Немного лирики.

Как следует писать звездочку относительно типа и имени переменной? Встречаются, например, такие формы записи, и все они имеют право на существование:

Int* p1; int * p2; int *p3;

Аргументы за первую форму. Чтобы объявить переменную следует указать ее тип, а затем имя. Звездочка является частью типа, а не частью имени. Это также подтверждается тем, что при привидении типов пишется тип со звездочкой, а не тип отдельно. Следовательно, должна писаться слитно с типом. Минус в том, что при объявлении нескольких переменных после объявления int*, только первая из них будет указателем, а вторая будет просто переменной типа int. Не объявляйте несколько указателей в одной строчке. Это не очень хороший стиль.

Аргументы за вторую форму. Есть люди, которым нравится «когда код дышит» Они ставят пробел до скобок и после скобок. И здесь тоже ставят. Возможно, это просто такой компромисс.

Аргументы за третью форму. Если писать так, то с объявлением нескольких указателей в одной строчке проблем быть не должно (хотя это все равно плохой тон). Некоторая идеология нарушается. Но этот стиль - самый распространенный, так как точно видно, что переменная - указатель.

И помните, что компилятору все это безразлично.

Адрес переменной и значение переменной по адресу

Рассмотрим две переменные: целочисленную переменную x и указатель на целочисленную переменную.

Int x; int *p;

Чтобы получить адрес переменной, нужно перед ее именем написать амперсанд.

Данная конструкция будет выполняться справа налево. Сначала с помощью оператора &, примененного к переменной x, будет получен адрес x. Затем адрес x будет сохранен в указателе p.

Есть и обратная операция. Чтобы получить значение переменной по ее адресу, следует написать звездочку перед именем указателя.

Int y = *p;

Такая операция в русском языке называется не слишком благозвучным словом «разыменование». В английском - dereference.

В данном примере с помощью оператора * мы получим то значение, которое находится в памяти по адресу p. Затем мы сохраним его в переменную y. В итоге получится, что значения x и y совпадают.

Все это несложно увидеть на экране.

#include int main(void) { int x; int y; int *p; x = 13; y = 0; p = &x; y = *p; printf("Value of xt%d", x); printf("Address of xt%p", &x); printf("n"); printf("Value of pt%p", p); printf("Address of pt%p", &p); printf("n"); printf("Value of yt%d", y); printf("Address of yt%p", &y); printf("n"); return 0; }

В указанном примере значение x и y будут одинаковы. А также адрес x и значение p.

Адресная арифметика

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

Int array = {1, 2, 3, 4, 5}; int *p = &array; p++;

Первая строка простая и понятая. Объявлен массив и заполнен числами от 1 до 5.

Во второй строке объявляется указатель на int и ему присваивается адрес нулевого элемента массива. Некоторые компиляторы разрешают писать такое присвоение так, считая, что имя массива означает адрес его нулевого элемента.

Int *p = array;

Но если вы хотите избежать неоднозначности, пишите явно. Таким образом в p лежит адрес начала массива. А конструкция *p даст 1.

Третья строчка увеличивает значение p. Но не просто на 1, а на 1 * sizeof(int). Пусть в данной системе int занимает 4 байта. После увеличения p на 1, p указывает не на следующий байт, а на первый байт из следующей четверки байтов. Программисту не нужно думать в данном случае о размере типа.

С вычитанием ситуация такая же.

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

Конструкция array[i] будет преобразована компилятором к *(array + i). К начальному адресу массива будет прибавлено число с учетом размерности типа данных. А затем будет взято значение по вычисленному адресу. Обратите внимание, что никто не запрещает написать и так i. Ведь конструкция будет преобразована к виду...

С указателем можно складывать число, представленное переменной или целочисленной константой. Вычесть можно не только число, но и указатель из указателя. Это бывает полезно. А вот сложить два указателя, умножить или разделить указатель на число или на другой указатель - нельзя.

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

Char c; char *pc = &c; int *pi = pc;

С точки зрения языка C все корректно. А вот в C++ будет ошибка, потому что типы указателей не совпадают.

Int *pi = (int*)pc;

Вот такая конструкция будет принята C++.

Небольшое резюме.

Int x; //объявление переменной целого типа int *p; //объявление указателя на переменную целого типа p = &x; //присвоить p адрес переменной x x = *p; //присвоить x значение, которое находится по адресу, сохраненному в p

Применение указателей

Обычно функция возвращает одно значение. А как вернуть больше одного? Рассмотрим код функции, которая меняет местами две переменные.

Int swap(double a, double b) { double temp = a; a = b; b = temp; }

Пусть есть переменные x и y с некоторыми значениями. Если выполнить функцию, передав в нее x и y, окажется, что никакого обмена не произошло. И это правильно.

При вызове этой функции в стеке будут сохранены значения x и y. Далее a и b получат значения x и y. Будет выполнена перестановка. Затем функция завершится и значения x и y будут восстановлены из стека. Все по-честному.

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

Void swap(double* a, double* b) { double temp = *a; *a = *b; *b = temp; }

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

Swap(&x, &y);

Теперь в функцию передаются адреса. И работа ведется относительно переданных адресов.

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

Если функция должна менять значение переменной, нужно передавать ей адрес этой переменной.

У тех, кто только начинает программировать на C, есть одна распространенная ошибка. При вводе с клавиатуры с помощью функции scanf() они передают значение переменной, а не ее адрес. А ведь scanf() должна менять значение переменной.

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

Немного посчитаем.

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

Теперь нам нужно передать 10 таких переменных. Это уже 40 байт. Тоже невелика задача.

Вообразим себя проектировщиками Большого Адронного Коллайдера. Вы отвечаете за безопасность системы. Именно вас окружают люди с недобрыми взглядами и факелами. Нужно показать им на модели, что конца света не будет. Для этого нужно передать в функцию collaiderModel(), скажем, 1 Гб данных. Представляете, сколько информации будет сохранено в стек? А скорее всего программа не даст вам стек такого объема без специальных манипуляций.

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

Указатели - это мощный инструмент. Указатели эффективны и быстры, но не слишком безопасны. Потому как вся ответственность за их использования ложится на разработчика. Разработчик - человек. А человеку свойственно ошибаться.

Представим ситуацию.

Int x; int *p;

В большинстве компиляторов C и С++ неинициализированные локальные переменные имеют случайное значение. Глобальные обнуляются.

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

*p = 10;

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

Указатели можно и нужно обнулять. Для этого есть специальное значение NULL.

Int *p = NULL;

Это запись больше соответствует стилю C. В C++ обычно можно инициализировать указатель нулем.

Int *p = 0;

Ловкость рук и никакого мошенничества. На самом деле, если изучить библиотечные файлы языка, можно найти определение для NULL.

#define NULL (void*)0

Для C NULL - это нуль, приведенный к указателю на void. Для C++ все немного не так. Стандарт говорит: «The macro NULL is an implementation-defined C++ null pointer constant in this International Standard. Possible definitions include 0 and 0L, but not (void*)0». То есть это просто 0 или 0, приведенный к long.

Предлагаю вам такую задачку. Папа Карло дал Буратино 5 яблок. Злой Карабас Барабас отобрал 3 яблока. Сколько яблок осталось у Буратино?

Ответ: неизвестно. Так как нигде не сказано, сколько яблок у Буратино было изначально.

Мораль: обнуляйте переменные.

Ссылки

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

#include void swap(double& a, double& b) { double temp = a; a = b; b = temp; }

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

Swap(x, y);

Для взятия адреса переменной и для объявления ссылки используется одинаковый символ - амперсанд. Но в случае взятия адреса & стоит в выражении, перед именем переменной. А в случае объявления ссылки - в объявлении, после объявления типа.

Использование ссылок и указателей - это очень широкая тема. Описание основ на этом закончим.

За мысли и замечания спасибо Юрию Борисову,

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

Вопрос

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

Object *myObject = new Object;

Object myObject;

Аналогично с методами. Почему вместо этого:

MyObject.testFunc();

мы должны писать вот это:

MyObject->testFunc();

Я так понимаю, что это дает выигрыш в скорости, т.к. мы обращаемся напрямую к памяти. Верно? P.S. Я перешел с Java.

Ответ

Заметим, кстати, что в Java указатели не используются в явном виде, т.е. программист не может в коде обратиться к объекту через указатель на него. Однако на деле в Java все типы, кроме базовых, являются ссылочными: обращение к ним происходит по ссылке, хотя явно передать параметр по ссылке нельзя. И еще, на заметку, new в C++ и в Java или C# - абсолютно разные вещи.

Для того, чтобы дать небольшое представление, что же такое указатели в C++, приведем два аналогичных фрагмента кода:

Object object1 = new Object(); // Новый объект Object object2 = new Object(); // Еще один новый объект object1 = object2;// Обе переменные ссылаются на объект, на который раньше ссылалась object2 // При изменении объекта, на который ссылается object1, изменится и // object2, потому что это один и тот же объект

Ближайший эквивалент на C++:

Object * object1 = new Object(); // Память выделена под новый объект // На эту память ссылается object1 Object * object2 = new Object(); // Аналогично со вторым объектом delete object1; // В C++ нет системы сборки мусора, поэтому если этого не cделать, // к этой памяти программа уже не сможет получить доступ, // как минимум, до перезапуска программы // Это называется утечкой памяти object1 = object2; // Как и в Java, object1 указывает туда же, куда и object2

Однако вот это – совершенно другая вещь (C++):

Object object1; // Новый объект Object object2; // Еще один object1 = object2;// Полное копирование объекта object2 в object1, // а не переопределение указателя – очень дорогая операция

Но получим ли мы выигрыш в скорости, обращаясь напрямую к памяти?

Строго говоря, этот вопрос объединяет в себе два различных вопроса. Первый: когда стоит использовать динамическое распределение памяти? Второй: когда стоит использовать указатели? Естественно, здесь мы не обойдемся без общих слов о том, что всегда необходимо выбирать наиболее подходящий инструмент для работы. Почти всегда существует реализация лучше, чем с использованием ручного динамического распределения (dynamic allocation) и / или сырых указателей.

Динамическое распределение

В формулировке вопроса представлены два способа создания объекта. И основное различие заключается в сроке их жизни (storage duration) в памяти программы. Используя Object myObject; , вы полагаетесь на автоматическое определение срока жизни, и объект будет уничтожен сразу после выхода из его области видимости. А вот Object *myObject = new Object; сохраняет жизнь объекту до того момента, пока вы вручную не удалите его из памяти командой delete . Используйте последний вариант только тогда, когда это действительно необходимо. А потому всегда делайте выбор в пользу автоматического определения срока хранения объекта, если это возможно .

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

  • Вам необходимо, чтобы объект существовал и после выхода из области его видимости - именно этот объект, именно в этой области памяти, а не его копия. Если для вас это не принципиально (в большинстве случаев это так), положитесь на автоматическое определение срока жизни. Однако вот пример ситуации, когда вам может понадобиться обратить к объекту вне его области видимости, однако это можно сделать, не сохраняя его в явном виде: записав объект в вектор, вы можете “разорвать связь” с самим объектом - на самом деле он (а не его копия) будет доступен при вызове из вектора.
  • Вам необходимо использовать много памяти , которая может переполнить стек. Здорово, если с такой проблемой не приходится сталкиваться (а с ней сталкиваются очень редко), потому что это “вне компетенции” C++, но к сожалению, иногда приходится решать и эту задачу.
  • Вы, например, точно не знаете размер массива, который придется использовать . Как известно, в C++ массивы при определении имеют фиксированный размер. Это может вызвать проблемы, например, при считывании пользовательского ввода. Указатель же определяет только тот участок в памяти, куда будет записано начало массива, грубо говоря, не ограничивая его размер.

Если использование динамического распределения необходимо, то вам стоит инкапсулировать его с помощью умного указателя ( можете прочитать в нашей статье) или другого типа, поддерживающего идиому “Получение ресурса есть инициализация” (стандартные контейнеры ее поддерживают - это идиома, в соответствии с которой ресурс: блок памяти, файл, сетевое соединение и т.п. - при получении инициализируется в конструкторе, а затем аккуратно уничтожается деструктором). Умными являются, например, указатели std::unique_ptr и std::shared_ptr .

Указатели

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

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

  • Ссылочная семантика . Иногда может быть необходимо обратиться к объекту (вне зависимости от того, как под него распределена память), поскольку вы хотите обратиться в функции именно в этому объекту, а не его копии - т.е. когда вам требуется реализовать передачу по ссылке. Однако в большинстве случаев, здесь достаточно использовать именно ссылку, а не указатель, потому что именно для этого ссылки и созданы. Заметьте, что это несколько разные вещи с тем, что описано в пункте 1 выше. Но если вы можете обратиться к копии объекта, то и ссылку использовать нет необходимости (но заметьте, копирование объекта - дорогая операция).
  • Полиморфизм . Вызов функций в рамках полиморфизма (динамический класс объекта) возможен с помощью ссылки или указателя. И снова, использование ссылок более предпочтительно.
  • Необязательный объект . В этом случае можно использовать nullptr , чтобы указать, что объект опущен. Если это аргумент функции, то лучше сделайте реализацию с аргументами по умолчанию или перегрузкой. С другой стороны, можно использовать тип, который инкапсулирует такое поведение, например, boost::optional (измененный в C++14 std::optional).
  • Повышение скорости компиляции . Вам может быть необходимо разделить единицы компиляции (compilation units) . Одним из эффективных применений указателей является предварительная декларация (т.к. для использования объекта вам необходимо предварительно его определить). Это позволит вам разнести единицы компиляции, что может положительно сказаться на ускорении времени компиляции, внушительно уменьшив время, затрачиваемое на этот процесс.
  • Взаимодействие с библиотекой C или C-подобной . Здесь вам придется использовать сырые указатели, освобождение памяти из-под которых вы производите в самый последний момент. Получить сырой указатель можно из умного указателя, например, операцией get . Если библиотека использует память, которая впоследствии должна быть освобождена вручную, вы можете оформить деструктор в умном указателе.

Указатель – это производный тип, который представляет собой адрес какого-либо значения. В языке Си++ используется понятие адреса переменных. Работа с адресами досталась Си++ в наследство от языка Си. Предположим, что в программе определена переменная типа int:

int x;

Можно определить переменную типа "указатель" на целое число:

int* xptr;

и присвоить переменной xptr адрес переменной x:

xptr = &x;

Надо запомнить:

  • Операция &, примененная к переменной, – это операция взятия адреса.
  • Операция *, примененная к адресу (другими словами, примененная к указателю) – это операция обращения по адресу.

Таким образом, для предыдущих определений x и y два оператора эквивалентны:

// присвоить переменной y значение x

int y = x;

// присвоить переменной y значение,

// находящееся по адресу xptr

int y = *xptr;

С помощью операции обращения по адресу можно записывать значения:

// записать число 10 по адресу xptr

*xptr = 10;

После выполнения этого оператора значение переменной x станет равным 10, поскольку xptr указывает на переменную x.

Указатель – это не просто адрес, а адрес величины определенного типа. Указатель xptr – адрес целой величины. Определить адреса величин других типов можно следующим образом:

// указатель на целое число без знака

unsigned long* lPtr;

// указатель на байт

char* cp;

// указатель на объект класса Complex

Complex* p;

Если указатель ссылается на объект некоторого класса, то операция обращения к атрибуту класса вместо точки обозначается "->", например p->real. Если вспомнить один из предыдущих примеров:

void Complex::Add(Complex x)

This->real = this->real + x.real;

This->imaginary = this->imaginary +

X.imaginary;

то this – это указатель на текущий объект, т.е. объект, который выполняет метод Add. Запись this-> означает обращение к атрибуту текущего объекта.

Можно определить указатель на любой тип, в том числе на функцию или метод класса. Если имеется несколько функций одного и того же типа:

int foo(long x);

int bar(long x);

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

int (*functptr)(long x);

functptr = &foo;

(*functptr)(2);

functptr = &bar;

(*functptr)(4);

Для чего нужны указатели? Указатели появились, прежде всего, для нужд системного программирования. Поскольку язык Си предназначался для "низкоуровневого" программирования, на нем нужно было обращаться, например, к регистрам устройств. У этих регистров устройств вполне определенные адреса, т.е. необходимо было прочитать или записать значение по определенному адресу. Благодаря механизму указателей, такие операции не требуют никаких дополнительных средств языка.

int* hardwareRegiste =0x80000;

*hardwareRegiste =12;

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

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

Указатели похожи на метки, которые ссылаются на места в памяти. Они тоже имеют адрес, а их значение является адресом некоторой другой переменной. Переменная, объявленная как указатель, занимает 4 байта в оперативной памяти (в случае 32-битной версии компилятора).

Синтаксис указателей

тип *ИмяОбъекта;

Тип указателя- это тип переменной, адрес которой он содержит. Для работы с указателями в Си определены две операции:

  • операция * (звездочка) - позволяет получить значение объекта по его адресу – определяет значение переменной, которое содержится по адресу, содержащемуся в указателе;
  • операция & (амперсанд) - позволяет определить адрес переменной.

Например:

Сhar c; // переменная char *p; // указатель p = &c; // p = адрес c

Объявление указателя, получение адреса переменной

Для того чтобы объявить указатель, который будет ссылаться на переменную, необходимо сначала получить адрес этой переменной. Чтобы получить адрес памяти переменной, нужно использовать знак «&» перед именем переменной. Это позволяет узнать адрес ячейки памяти, в которой хранится значение переменной. Эта операция называется - операция взятия адреса:

Int var = 5; // простое объявление переменной с предварительной инициализацией int *ptrVar; // объявили указатель, однако он пока ни на что не указывает ptrVar = &var; // теперь наш указатель ссылается на адрес в памяти, где хранится число 5

Указатель на указатель

Указатель хранит адрес области памяти. Можно создать указатель на указатель, тогда он будет хранить адрес указателя и сможет обращаться к его содержимому. Указатель на указатель определяется как:

<тип> **<имя>;

Пример работы указателя на указатель:

#include #include #define SIZE 10 void main() { int A; int B; int *p; int **pp; A = 10; B = 111; p = &A; pp = &p; printf("A = %d\n", A); *p = 20; printf("A = %d\n", A); *(*pp) = 30; //здесь скобки можно не писать printf("A = %d\n", A); *pp = &B; printf("B = %d\n", *p); **pp = 333; printf("B = %d", B); getch(); }

Указатели и приведение типов

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

В следующем примере мы пользуемся тем, что размер типа int равен 4 байта, а char 1 байт. За счёт этого, получив адрес первого байта, можно пройти по остальным байтам числа и вывести их содержимое.

#include #include #define SIZE 10 void main() { int A = 10; int *intPtr; char *charPtr; intPtr = &A; printf("%d\n", *intPtr); printf("--------------------\n"); charPtr = (char*)intPtr; printf("%d ", *charPtr); charPtr++; printf("%d ", *charPtr); charPtr++; printf("%d ", *charPtr); charPtr++; printf("%d ", *charPtr); getch(); }

NULL pointer – нулевой указатель

Указатель до инициализации хранит мусор, как и любая другая переменная. Но в, то, же время, этот “мусор” вполне может оказаться валидным адресом. Например, есть указатель. Каким образом узнать, инициализирован он или нет? В общем случае никак. Для решения этой проблемы был введён макрос NULL библиотеки stdlib.

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

Int *ptr = NULL;

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