Как сделать указатель в с

Обновлено: 03.07.2024

Благодаря наличию исключений, язык C++ позволяет разделить основную логику приложения и обработку ошибок, не мешая их в одну кучу. Что есть очень хорошо. Однако теперь по коду нельзя с уверенностью сказать, где может быть прервано его исполнение. Отсюда возникает опасность утечки ресурсов. Проблема эта решается при помощи деструкторов и идиомы RAII. Впрочем, придерживаться этой идиомы становится проблематично при использовании указателей. Особенно при использовании их не как членов класса, а просто как переменных в методах. На наше с вами счастье, в стандартной библиотеке языка есть умные указатели (smart pointers), придуманные именно для этого случая. Поскольку на C++ я пишу не регулярно, то иногда забываю некоторые нюансы использования умных указателей, в связи с чем решил вот набросать небольшую шпаргалку.

Важно! В старых книжках и статьях можно встретить упоминание auto_ptr. Этот тип умных указателей появился в C++, когда в языке еще не было move semantics. Из-за этого использование auto_ptr порой может приводить к трудным в обнаружении ошибкам. В стандарте C++17 auto_ptr был удален. Другими словами, все, что вы должны знать об auto_ptr — это то, что его не должно быть в современном коде. Вместо него всегда используйте unique_ptr.

unique_ptr

Шаблонный класс unique_ptr представляет собой уникальный указатель на объект. Указатель нельзя копировать, но можно передавать владение им с помощью std::move. При уничтожении указателя автоматически вызывается деструктор объекта, на который он указывает.

Создается unique_ptr так:

… но обычно используют шаблон make_unique, так короче:

Класс unique_ptr перегружает оператор -> , что позволяет обращаться к полям класса и вызывать его методы, словно мы работаем с обычным указателем:

Как уже отмечалось, unique_ptr запрещено копировать:

Однако владение им можно передать при помощи std::move, например:

Плюс к этому, мы всегда можем получать из unique_ptr обычный указатель на объект:

… хотя это и является code smell. Кроме того, ничто не мешает создавать ссылки (reference) на unique_ptr:

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

Интересно, что unique_ptr позволяет указать функцию, которую он будет вызывать вместо деструктора, так называемый custom deleter. Это позволяет использовать unique_ptr с ресурсами, возвращаемых из библиотек для языка C, и даже реализовать аналог defer из языка Go:

/* g++ custom-deleter.cpp -o custom-deleter */

template typename T >
using auto_cleanup = std :: unique_ptr T,std :: function void ( T * ) >> ;

static char dummy [ ] = "" ;

int main ( ) <
auto_cleanup FILE > f (
fopen ( "test.txt" , "w" ) ,
[ ] ( FILE * f ) < fclose ( f ) ; >
) ;

fwrite ( "Hello! \n " , 7 , 1 , f. get ( ) ) ;
>

Заметьте, что в макросе defer нам пришлось передать в unique_ptr фиктивный указатель. Если бы мы передали nullptr, custom deleter не был бы вызван.

Важно! Если в умном указателе вы держите указать на массив объектов, то обязаны указать custom deleter, вызывающий для этого массива delete[] вместо delete . Если этого не сделать, будет освобожден только первый объект из массива, остальные же утекут.

shared_ptr и weak_ptr

Класс shared_ptr является указатем на объект, которым владеет сразу несколько объектов. Указатель можно как перемещать, так и копировать. Число существующих указателей отслеживается при помощи счетчика ссылок. Когда счетчик ссылок обнуляется, вызывается деструктор объекта. Сам по себе shared_ptr является thread-safe, но он не делает магическим образом thread-safe объект, на который ссылается. То есть, если доступ к объекту может осуществляться из нескольких потоков, вы должны не забыть предусмотреть в нем мьютексы или что-то такое.

Для создания shared_ptr обычно используется шаблон make_shared:

В остальном работа с ним мало отличается от работы с unique_ptr, за тем исключением, что shared_ptr можно смело копировать.

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

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

class SomeClass <
public :
void sayHello ( ) <
std :: cout "Hello!" std :: endl ;
>

~SomeClass ( ) <
std :: cout "~SomeClass" std :: endl ;
>
> ;

int main ( ) <
std :: weak_ptr SomeClass > wptr ;

<
auto ptr = std :: make_shared SomeClass > ( ) ;
wptr = ptr ;

if ( auto tptr = wptr. lock ( ) ) <
tptr - > sayHello ( ) ;
> else <
std :: cout "lock() failed" std :: endl ;
>
>

if ( auto tptr = wptr. lock ( ) ) <
tptr - > sayHello ( ) ;
> else <
std :: cout "lock() failed" std :: endl ;
>
>

Можно думать о weak_ptr как об указателе, позволяющим получить временное владение объектом. Само собой разумеется, если все постоянные указатели на объект перестанут существовать, и останутся только временные, полученные при помощи метода lock() класса weak_ptr, объект продолжит свое существование. Он будет уничтожен только тогда, когда на объект не останется вообще никаких указателей.

Умные указатели и наследование

Вопрос, о котором часто забывают — это кастование умных указателей вверх и вниз по иерархии классов. Для shared_ptr в стандартной библиотеке есть шаблоны static_pointer_cast, dynamic_pointer_cast и другие. Для unique_ptr таких же шаблонов почему-то не занесли, но их нетрудно найти на StackOverflow.

template typename Derived, typename Base, typename Del >
std :: unique_ptr Derived, Del >
dynamic_unique_ptr_cast ( std :: unique_ptr Base, Del > && p )
<
if ( Derived * result = dynamic_cast Derived * > ( p. get ( ) ) ) <
p. release ( ) ;
return std :: unique_ptr Derived, Del > ( result,
std :: move ( p. get_deleter ( ) ) ) ;
>
return std :: unique_ptr Derived, Del > ( nullptr, p. get_deleter ( ) ) ;
>

class Base <
public :
Base ( int num ) : num ( num ) < >;

protected :
int num ;
> ;

class Derived : public Base <
public :
Derived ( int num ) : Base ( num )

void testUnique ( ) <
std :: cout "=== testUnique begin == sy1"> std :: endl ;

auto derived = std :: make_unique Derived > ( 1 ) ;
derived - > sayHello ( ) ;

std :: unique_ptr Base > base = std :: move ( derived ) ;
base - > sayHello ( ) ;

auto derived2 = static_unique_ptr_cast Derived > ( std :: move ( base ) ) ;
derived2 - > sayHello ( ) ;

std :: unique_ptr Base > base2 = std :: make_unique Derived > ( 2 ) ;
base2 - > sayHello ( ) ;

std :: cout "=== testUnique end == sy1"> std :: endl ;
>

void testShared ( ) <
std :: cout "=== testShared begin == sy1"> std :: endl ;

auto derived = std :: make_shared Derived > ( 1 ) ;
derived - > sayHello ( ) ;

auto base = std :: static_pointer_cast Base > ( derived ) ;
base - > sayHello ( ) ;

auto derived2 = std :: static_pointer_cast Derived > ( base ) ;
derived2 - > sayHello ( ) ;

std :: shared_ptr Base > base2 = std :: make_shared Derived > ( 2 ) ;
base2 - > sayHello ( ) ;

Указатели в C++

Программирование и разработка

Указатели в C++

Память компьютера — это длинный ряд ячеек. Размер каждой ячейки называется байтом. Байт — это пространство, занятое английским символом алфавита. Объект в обычном понимании — это последовательный набор байтов в памяти. Каждая ячейка имеет адрес, который представляет собой целое число, обычно записанное в шестнадцатеричной форме. Есть три способа доступа к объекту в памяти. Доступ к объекту можно получить с помощью так называемого указателя. Доступ к нему можно получить, используя так называемую ссылку. К нему по-прежнему можно получить доступ с помощью идентификатора. В этой статье основное внимание уделяется использованию указателей и ссылок. В C ++ есть заостренный объект и объект-указатель. У остроконечного предмета есть интересующий предмет. Объект-указатель имеет адрес указанного объекта.

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

У объекта-указателя и объекта-указателя есть свой идентификатор.

Оператор Address-Of, &

Это унарный оператор. Если за ним следует идентификатор, он возвращает адрес объекта идентификатора. Рассмотрим следующее объявление:

Ниже приведен код, следующее выражение, вернет адрес, идентифицированный ptdInt:

Вам не нужно знать точный адрес (номер) при кодировании.

Оператор косвенного обращения, *

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

Создание указателя

Взгляните на следующий фрагмент кода:

Сегмент начинается с объявления указанного объекта ptdFloat. ptdFloat — это идентификатор, который просто идентифицирует объект с плавающей запятой. Ему мог быть присвоен реальный объект (значение), но в этом случае ему ничего не было присвоено. Далее в сегменте идет объявление объекта-указателя. Оператор косвенного обращения перед этим идентификатором означает, что он должен содержать адрес указанного объекта. Тип объекта, плавающий в начале оператора, означает, что заостренный объект является плавающим. Объект-указатель всегда имеет тот же тип, что и заостренный объект. ptrFoat — это идентификатор, который просто идентифицирует объект-указатель.

В последнем операторе кода адрес указанного объекта присваивается объекту-указателю. Обратите внимание на использование оператора адресации &.

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

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

Первая строка предыдущего сегмента кода и эта совпадают. Здесь вторая и третья строки предыдущего сегмента кода объединены в один оператор.

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

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

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

Присвоение нуля указателю

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

int ptdInt = 5 ;
int * ptrInt ;

ptrInt = ;

or in the segment ,

int ptdInt = 5 ;
int * ptrInt = ;

В любом случае указатель (идентификатор) называется нулевым указателем; это означает, что он указывает в никуда. То есть у него нет адреса какого-либо указанного объекта. Здесь 0 — это десятичный ноль, а не шестнадцатеричный ноль. Шестнадцатеричный ноль будет указывать на первый адрес памяти компьютера.

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

Имя массива как постоянный указатель

Рассмотрим следующий массив:

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

В случае с массивом оператор приращения ++ ведет себя иначе. Вместо добавления 1 он заменяет адрес указателя на адрес следующего элемента в массиве. Однако имя массива — это постоянный указатель; это означает, что его содержимое (адрес) не может быть изменено или увеличено. Итак, для увеличения начальный адрес массива должен быть назначен непостоянному указателю следующим образом:

Теперь ptr можно увеличивать, чтобы указывать на следующий элемент массива. ptr был объявлен здесь как объект-указатель. Без * здесь не было бы указателя; это будет идентификатор для хранения объекта типа int, а не для хранения адреса памяти.

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

Следующий код выводит четвертое значение массива:

Имя функции как идентификатор

Имя функции — это идентификатор функции. Рассмотрим следующее определение функции:

fn — идентификатор функции. Выражение,

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

Идентификатор указанного объекта и идентификатор объекта-указателя различаются. func — это указатель на функцию. fn — идентификатор функции. Итак, func может указывать на fn следующим образом:

Значение (содержание) func — это адрес fn. Два идентификатора могли быть связаны с помощью оператора инициализации следующим образом:

Обратите внимание на различия и сходства в обработке указателей функций и скалярных указателей. func — указатель на функцию; это заостренный объект; он объявлен иначе, чем скалярный указатель.

Функцию можно вызвать с помощью,

Его нельзя вызвать с помощью * func ().

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

float fn ( float fl , int in )
<
return fl ;
>

float ( * func ) ( float , int ) = & fn ;

float val = func ( 2.5 , 6 ) ;

C++ Reference

Ссылка в C ++ — это просто способ создать синоним (другое имя) для идентификатора. Он использует оператор &, но не так, как & используется для указателей. Рассмотрим следующий фрагмент кода:

Первый оператор инициализирует идентификатор myInt; т.е. myInt объявлен и содержит значение 8. Второй оператор создает новый идентификатор yourInt, синоним myInt. Для этого в объявлении между типом данных и новым идентификатором помещается оператор &. Операторы cout показывают, что два идентификатора являются синонимами. Чтобы вернуть значение в этом случае, вам не нужно ставить перед ним *. Просто используйте идентификатор.

Здесь myInt и yourInt — это не два разных объекта. Это два разных идентификатора, которые ссылаются (идентифицируют) одно и то же место в памяти, имеющее значение 8. Если значение myInt изменяется, значение yourInt также изменится автоматически. Если значение yourInt изменится, значение myInt также изменится автоматически.

Ссылка на функцию

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

Обратите внимание на первый оператор в функции main, который делает func синонимом fn. Оба ссылаются на одну и ту же функцию. Обратите внимание на одноразовое использование и положение &. Таким образом, & является здесь ссылочным оператором, а не оператором адресации. Чтобы вызвать функцию, просто используйте любое имя.

Идентификатор ссылки — это не то же самое, что идентификатор указателя.

Функция, возвращающая указатель

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

Первый оператор в функции fn () предназначен только для создания объекта-указателя. Обратите внимание на одноразовое использование и положение * в сигнатуре функции. Также обратите внимание, как указатель (адрес) был получен в функции main () другим объектом-указателем.

Функция, возвращающая ссылку

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

Первый оператор в функции fn () предназначен только для создания ссылки. Обратите внимание на одноразовое использование и положение & в сигнатуре функции. Также обратите внимание, как ссылка была получена в функции main () по другой ссылке.

Передача указателя на функцию

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

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

Передача ссылки на функцию

В следующей программе ссылка отправляется в качестве аргумента функции:

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

Передача массива функции

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

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

Может ли функция C ++ вернуть массив?

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

Указатель может указывать на другой указатель. То есть объект-указатель может иметь адрес другого объекта-указателя. Они по-прежнему должны быть одного типа. Следующий фрагмент кода иллюстрирует это:

int ptdInt = 5 ;

int * ptrInt = & ptdInt ;

int ** ptrptrInt = & ptrInt ;

cout ** ptrptrInt ‘ \n ‘ ;

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

Массив указателей

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

Обратите внимание на использование и положение * в объявлении массива. Обратите внимание на использование * при возврате значения в массиве. С указателями указателей задействованы два *. В случае массива указателей об одном * уже позаботились, потому что идентификатор массива является указателем.

Массив строк переменной длины

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

Указатель на функцию, возвращающую указатель

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

Объявление указателя на функцию, возвращающую указатель, аналогично объявлению указателя на обычную функцию, но перед ним стоит звездочка. Первый оператор в функции main () иллюстрирует это. Чтобы вызвать функцию с помощью указателя, поставьте перед ней *.

Заключение

Чтобы создать указатель на скаляр, сделайте что-нибудь вроде:

* имеет два значения: в объявлении указывает указатель; чтобы что-то вернуть, это значение указанного объекта.

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

Чтобы создать указатель на функцию, вы можете:

где fn () — функция, определенная в другом месте, а func — указатель.

Чтобы создать ссылку на функцию, вы можете:

где fn () — функция, определенная в другом месте, а refFunc — ссылка.

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

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

При передаче массива функции параметр — это объявление, а аргумент — это имя массива без []. Функция C ++ не возвращает массив.

Для указателя на указатель требуется два * вместо одного, где это необходимо.

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

Указатели

Когда компилятор обрабатывает оператор определения переменной, например, int i=10;, он выделяет память в соответствии с типом (int) и инициализирует ее указанным значением (10). Все обращения в программе к переменной по ее имени (i) заменяются компилятором на адрес области памяти, в которой хранится значение переменной. Программист может определить собственные переменные для хранения адресов областей памяти. Такие переменные называются указателями.

Итак, указатели предназначены для хранения адресов областей памяти. В C++ различают три вида указателей:

  • указатели на объект,
  • указатели на функцию
  • указатели на void,

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

Указатель на функцию

задает указатель с именем fun на функцию, возвращающую значение типа int и имеющую два аргумента типа double.

Указатель на объект

Указатель на объект содержит адрес области памяти, в которой хранятся данные определенного типа (основного или составного). Простейшее объявление указателя на объект (в дальнейшем называемого просто указателем) имеет вид:

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

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

описываются два указателя на целое с именами а и с, а также целая переменная b.

Размер указателя зависит от модели памяти. Можно определить указатель на указатель и т. д.

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

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

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

Примеры объявления указателей

Указатель может быть константой или переменной, а также указывать на константу или переменную. Рассмотрим примеры:

int i; // целая переменная
const int ci = 1; // целая константа
int * pi; // указатель на целую переменную
const int * pci; // указатель на целую константу
int * const ср = &i; // указатель-константа на целую переменную
const int * const срс = &ci; // указатель-константа на целую константу

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

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

Инициализация указателей

Указатели чаще всего используют при работе с динамической памятью, называемой некоторыми эстетами кучей (перевод с английского языка слова heap). Это свободная память, в которой можно во время выполнения программы выделять место в соответствии с потребностями. Доступ к выделенным участкам динамической памяти, называемым динамическими переменными, производится только через указатели. Время жизни динамических переменных — от точки создания до конца программы или до явного освобождения памяти. В C++ используется два способа работы с динамической памятью. Первый использует семейство функций malloc и достался в наследство от С, второй использует операции new и delete.

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

Способы инициализации указателей

В операторе 1 операция new выполняет выделение достаточного для размещения величины типа 1 nt участка динамической памяти и записывает адрес начала этого участка в переменную n. Память под саму переменную п (размера, достаточного для размещения указателя) выделяется на этапе компиляции.

В операторе 2, кроме описанных выше действий, производится инициализация выделенной динамической памяти значением 10.

В операторе 3 операция new выполняет выделение памяти под 10 величин типа int (массива из 10 элементов) и записывает адрес начала этого участка в переменную q. которая может трактоваться как имя массива. Через имя можно обращаться к любому элементу массива.

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

В операторе 4 делается то же самое, что и в операторе 1, но с помощью функции выделения памяти та 11 ос, унаследованной из библиотеки С, В функцию передается один параметр — количество выделяемой памяти в байтах. Конструкция (int*) используется ц,ля приведения типа указателя, возвращаемого функцией, к требуемому типу.Если память выделить не удалось, функция возвращает 0.

Операцию new использовать предпочтительнее, чем функцию malloc, особенно при работе с объектами.

Освобождение памяти, выделенной с помощью операции new, должно выполняться с помощью delete, а памяти, выделенной функцией malloc — посредством функции free. При этом переменная-указатель сохраняется и может инициализироваться повторно. Приведенные выше динамические переменные уничтожаются следующим образом:

delete n; delete m; delete [ ] q; free (u);

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

объявляется массив из 10 указателей на функции без параметров, возвращающих указатели на int.

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

  1. если справа от имени имеются квадратные скобки, это массив, если скобки круглые — это функция;
  2. если слева есть звездочка, это указатель на проинтерпретированную ранее конструкцию;
  3. если справа встречается закрывающая круглая скобка, необходимо применить приведенные выше правила внутри скобок, а затем переходить наружу;
  4. в последнюю очередь интерпретируется спецификатор типа.

Для приведенного выше описания порядок интерпретации указан цифрами:

int *(*р[10])();
5 4 2 1 3 // порядок интерпретации описания;

Операции с указателями в C++

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

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

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

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

char а; // переменная типа char
char * р = new char; /* выделение памяти под указатель и под динамическую переменную типа char */
*р = ‘Ю’; а = *р; // присваивание значения обеим переменным

Как видно из примера, конструкцию *имя_указателя можно использовать в левой части оператора присваивания, так как она является L-значением, то есть определяет адрес области памяти. Для простоты эту конструкцию можно считать именем переменной, на которую ссылается указатель. С ней допустимы все действия, определенные для величин соответствующего типа (если указатель инициализирован). На одну и ту же область памяти может ссылаться несколько указателей различного типа. Примененная к ним операция разадресации даст разные результаты. Например, программа

на IBM РС-совместимом компьютере выведет на экран строку ():

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

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

При смешивании в выражении указателей разных типов явное преобразование типов требуется для всех указателей, кроме void*. Указатель может неявно преобразовываться в значение тина booд (например, в выражении условного оператора), при этом ненулевой указатель преобразуется в true, а нулевой в false.

Присваивание без явного приведения типов допускается в двух случаях:

  • указателям типа void*:
  • если тип указателей справа и слева от операции присваивания один и тот же.

Таким образом, неявное преобразование выполняется только к типу void*. Значение 0 неявно преобразуется к указателю на любой тип. Присваивание указателей на объекты указателям на функции (и наоборот) недопустимо. Запрещено и присваивать значения указателям-константам, впрочем, как и константам любого типа (присваивать значения указателям на константу и переменным, на которые ссылается указатель-константа, допускается).

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

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

short * р = new short [5];
p++; // значение р увеличивается на 2
long * q = new long [5];
q++; // значение q увеличивается на 4

Разность двух указателей — это разность их значений, деленная на размер типа в байтах (в применении к массивам разность указателей, например, на третий и шестой элементы равна 3). Суммирование двух указателей не допускается.

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

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

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

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

Ссылки в C++

Ссылка представляет собой синоним имени, указанного при инициализации ссылки. Ссылку можно рассматривать как указатель, который всегда разыменовывается. Формат объявления ссылки:

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

int коl;
int& pal = kol; // ссылка pal — альтернативное имя для коl
const char& CR = ‘n ‘; // ссылка на константу

Запомните следующие правила.

  • Переменная-ссылка должна явно инициализироваться при ее описании, кроме случаев, когда она является параметром функции, описана как extern или ссылается на поле данных класса.
  • После инициализации ссылке не может быть присвоена другая переменная.
  • Тип ссылки должен совпадать с типом величины, на которую она ссылается.
  • Не разрешается определять указатели на ссылки, создавать массивы ссылок и ссылки на ссылки.

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

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

[Объявление указателя]

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

Здесь звездочка * обозначает оператор разыменования (de-reference operator). Например, строка

объявляет переменную указателя ptr, которая может держать в себе адрес переменной типа int. Обратный оператор генерирования ссылки (reference operator, &) может использоваться для получения адреса переменной. Например, у нас есть переменная

Оператор &x даст нам адрес переменной x, который мы можем присвоить переменной указателя.

[Небезопасный код (Unsafe Code)]

Для того, чтобы пометить небезопасный контекст (т. е. код), используется ключевое слово unsafe. Мы можем использовать unsafe двумя различными способами. Ключевое слово unsafe может использоваться как модификатор метода, свойства, конструктора, и т. д. Например:

Кроме того, ключевое слово unsafe может также использоваться, чтобы пометить группу операторов как небезопасную:

[Прикрепление объекта (Pinning an Object)]

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

[Указатели и методы (Pointers & Methods)]

Указатели могут быть переданы в метод как аргументы. Методы также могут возвратить указатель. Пример:

[Указатели и преобразования типа (Pointers & Conversions)]

1. Из типа указателя на любой тип к типу указателя на тип void *.
2. Из типа null к любому другому типу указателя.

Оператор преобразования типа cast operator () необходим для любых явных преобразований типа. Имеются явные преобразования типа:

1. Из любого типа указателя на любой другой тип указателя.
2. Из типов sbyte, byte, short, ushort, int, uint, long, ulong к любому другому типу указателя.
3. Из любого типа указателя к типам sbyte, byte, short, ushort, int, uint, long, ulong.

[Арифметика указателей (Pointer Arithmetic)]

В небезопасном контексте операторы ++ и -- могут быть приложены к переменной указателя всех типов, за исключением типа void *. Таким образом, для каждого типа указателя T* следующие операторы будут неявно перегружены (implicitly overloaded).

T* operator ++ (T *x);
T* operator -- (T *x);

Оператор ++ добавляет sizeof(T) к адресу, содержащемуся в переменной указателя, и оператор -- вычитает sizeof(T) из этого адреса для переменной указателя на тип T*.

[Выделение памяти стека (Stack Allocation)]

В небезопасном контексте локальные определения переменных могут включать инициализатор выделения стека (stack allocation initialiser), который выделяет память из стека вызовов (call stack).

Оператор stackalloc T[E] требует T как необрабатываемый (unmanaged) тип и E как выражение типа int. Вышеуказанная конструкция выделяет E * sizeof(T) байт из стека и генерирует указатель типа T* на новый выделенный блок. Если E отрицательно, то выбрасывается исключение System.OverFlowException. Если недостаточно памяти, то срабатывает исключение System.StackOverflowException.

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

[Указатели и массивы (Pointers & Arrays)]

[Указатели и структуры (Pointers & Structures)]

[Выводы]

c-sharp-project-allow-unsafe-code

3. Получение указателя от объекта возможно только в том случае, если он определен с атрибутом fixed.

[Как передать в функцию ссылку на массив данных в безопасном контексте?]

Для решения проблемы можно либо перевести проект в режим небезопасного (unsafe) кода, либо надо поменять синтаксис вызова функции:

Аннотация: В лекции следует изучить указатель как средство доступа к данным. Научиться определять адреса переменных основных типов. Изучить допустимые операции с указателями. Научиться использовать указатели в элементарных задачах программирования.

Теоретическая часть

По краткому определению, указатель – это переменная , содержащая адрес другой переменной [7.1]. Так как указатель содержит адрес переменной (объекта), это дает возможность "косвенного" доступа к этой переменной (объекту) через указатель .

Для понимания работы и назначения указателей рассмотрим упрощенную схему организации памяти компьютера. Память представляет собой массив последовательно пронумерованных или адресованных ячеек, с которыми можно работать по отдельности или связанными кусками. Известно, что различным типам данных отводится определенное количество байтов памяти. Поэтому указатель – это группа ячеек, в которых может храниться адрес . Например, если переменная ch имеет тип char , а ptr (от латинского pointer – указатель ) есть указатель на переменную ch , то взятие адреса переменной ch осуществляется с помощью унарного (одноместного) оператора & , т.е.

Приведенная инструкция означает, что переменной ptr присваивается адрес ячейки ch . Принято считать, что ptr указывает на ch . Оператор & применяется только к объектам, расположенным в памяти: к переменным, элементам массива. Операндом оператора & не может быть ни выражение , ни константа, ни регистровая переменная [7.1]. Унарный оператор & называется еще оператором адресации [7.2].

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

Другая унарная операция * называется операцией ссылки по указателю ( indirection ), или разыменования ( dereferencing ). Если применить ее к указателю, то получим объект , на который он указывает. Рассмотрим пример. Пусть х и у – целые переменные, а *ptr – указатель на целую переменную. Поставим задачу присвоения переменной у значения переменной х с помощью указателя. Фрагмент С -кода будет следующий:

В приведенных объявлениях новым является объявление указателя:

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

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

Одноместные (унарные) операции * и & имеют более высокий приоритет для своих операндов, чем арифметические операции .

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

Тогда возможно произвести присвоение:

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

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

С помощью унарных операций "++" и "––" числовые (арифметические) значения переменных типа указатель меняются по -разному в зависимости от типа данных, с которыми связаны эти переменные [7.3]. Если указатель связан с типом char , то при выполнении операций "++" и "––" его числовое значение изменяется на 1 (единицу). Если указатель связан с типом int , то операции "++" и "––" изменяют числовые значения указателей на 2. Указатель , связанный с типами float или long , унарными операциями "++" и "––" изменяется на 4. Таким образом, при изменении указателя на единицу указатель "переходит к началу" следующего (или пр едыдущего) поля той длины, которая определяется типом.

\pi

Следует особо остановиться на указателях и квалификаторе (модификаторе) const . Как известно, квалификатор const превращает переменную в константу, значение которой не должно меняться. Например, нет смысла изменять число . Значение константы должно быть инициализировано в месте ее определения. В связи с этим различают указатели на константы и константные указатели [7.6]. Приведем следующий пример:

Последняя строчка приведенного кода определяет собой указатель на константу. Попытка указателю pvalue присвоить иное числовое значение будет восприниматься компилятором как ошибка [7.6]. Но само значение переменной value изменять допустимо. При этом указатель держит адрес переменной, значение которой изменилось. В тоже время саму переменную также можно объявить с помощью квалификатора const . В этом случае нельзя изменять ни переменную, ни значение указателя (т. е. присвоить иное числовое значение указателю). Указатели на константы часто используются как формальные параметры функций (о функциях будет сказано позднее).

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

В соответствии с приведенным кодом переменная count будет иметь значение 345 [7.6].

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

Указанные особенности для указателей с квалификатором const присущи и для переменных (объектов) других типов.

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

Практическая часть

Пример 1. Напишите программу определения адресов целых чисел от 0 до 9 и строчных букв латинского алфавита.

Программный код решения примера:

Результат выполнения программы показан на рис. 7.1.

Адреса цифр и строчных букв

В программе использован спецификатор формата %5p для определения адреса переменных . Число 5 определяет отступ от левого края на пять позиций.

  1. Добавьте вывод кодов цифр и букв, для которых определены адреса в памяти компьютера.
  2. В программе вместо операторов цикла for примените операторы while .
  3. В программу введите указатель на тип int и применить этот указатель по аналогии с указателем *ptr2 .
  4. Добавьте определение адресов прописных букв латинского алфавита и вывести их дополнительным столбцом к адресам строчных букв.
  5. Выведите в столбец свою фамилию (буквами латинского алфавита), имя и адреса соответствующих букв фамилии и имени.

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

Читайте также: