Указатель на тип char
Я как-то давно спрашивал по поводу указателей, разобрался, всем ответившим спасибо. Но сейчас мне пришлось столкнутся с типом char :
int *a; char *b; a = new int(10); b = "bla bla bla"; // Странно ведь мы записываем в адрес, а не в разыменованный указатель cout
Хотелось бы понять, как работает указатель на тип char (хотелось бы увидеть аргументированные ответы, почему так различается, и что-то от себя как рекомендации). Шерстил интернет — по теме указателей в C++ ничего не нашел P. S.: Прошу прощения, если эта тема всех достала или мое сообщение является дубликатом (есть темы похожие, но, вроде, это не то, что мне надо).
Отслеживать
11.5k 8 8 золотых знаков 42 42 серебряных знака 69 69 бронзовых знаков
задан 12 ноя 2015 в 10:00
3,913 7 7 золотых знаков 45 45 серебряных знаков 85 85 бронзовых знаков
4 ответа 4
Сортировка: Сброс на вариант по умолчанию
Давайте рассмотрим предложения из вашего примера шаг за шагом.
В этом предложении
int *a;
вы объявили переменную a как указатель на объект с типом int
В этом предложении
char *b;
вы объявили переменную b как указатель на объект с типом char
В этом предложении
a = new int(10);
был создан объект в динамической памяти типа int , который был инициализирован значением 10.
В этом предложении
b = "bla bla bla";
в левой части выражения с оператором присваивания стоит переменна с типом char * . В правой части этого выражения используется строковый литерал "bla bla bla" . Строковые литералы в C++ имеют типы константных символьных массивов.
(C++ Стандарт, 2.14.5 String literals)
8 Ordinary string literals and UTF-8 string literals are also referred to as narrow string literals. A narrow string literal has type “array of n const char”, where n is the size of the string as defined below, and has static storage duration (3.7).
Строковый литерал "bla bla bla" имеет тип const char [12] . В выражениях массивы неявно преобразуются к указателям на свой первый элемент.
Согласно стандарту C++ (4.2 Array-to-pointer conversion)
1 An lvalue or rvalue of type “array of N T” or “array of unknown bound of T” can be converted to a prvalue of type “pointer to T”. The result is a pointer to the first element of the array.
Таким образом в приведенном предложении в правой части выражения с оператором присваивания используется значение типа const char * , которое указывает на первый символ строкового литерала "bla bla bla" . То есть имеет место попытка указателю типа char * присвоить значение указателя типа const char * . Так как не существует неявного преобразования из типа указателя на константный объект в тип указателя на неконстантный объект, то компилятор должен выдать диагностическое сообщение.
Правильно было бы объявить переменную b следующим образом
const char *b;
Тогда вышеприведенное предложение с присваиванием было бы корректно, и переменной b был бы присвоен адрес первого символа строкового литерала, стоящего в правой части выражения от знака присваивания.
b = "bla bla bla";
фактически, эквивалентно следующему предложению
В этом предложении
cout
целочисленное значение, адресуемое указателем a выводится на консоль.
Было бы более корректно записать
cout
В этом предложении
cout
при условии, что компилятор каким-то образом сгенерировал объектный модуль, несмотря на ошибку, о которой я сказал выше, выведет на консоль строковый литерал, так как для указателей типа char * оператор operator
Если вы хотите вывести на консоль именно значение самого указателя, то вам в таком случае следует написать либо
cout
cout ( b )
А если переменная b была объявлена с квалификатором const , то
Указатели в C++ для начинающих.
Указатели в C++ для начинающих обычно очень непонятны. Связано это с двойственностью природы указателей. Указатель – это переменная, которая хранит адрес памяти.
В статье Указатели в C++ для начинающих. Поверхностное знакомство было описано очень мало, но что такое указатели знать нужно и надо иметь представление как с ними работать.
В C++ указатель объявляется с помощью звездочки
int * x ; //переменная x есть указатель на int;
так как указатель хранит адрес памяти, то попытка обращения к указателю вернет этот адрес
cout //На экран выводится какой-то адрес памяти
чтобы указатель обрабатывал значение, а не адрес памяти, используется операция разыменования
cout /звездочка – это оператор разыменования
* x = 100 ; / звездочка – это оператор разыменования
/В этом примере сначала на экран выведется какое-то любое значение, после чего по адресу памяти на который указывает x будет записано число 100. Так как в обоих случаях было применено разыменование, то будут выведены именно значения, а не адреса памяти.
Код C++ Указатель + Разыменованный указатель
==================
int x = 20 ; //Переменная x = 20
int * ptr ; // ptr есть указатель на int
void main ()
clrscr ();
//Выводим на экран различные значения
cout //Указатель ptr = Адрес памяти
cout <<“ *ptr” <* ptr //Разыменованный указатель ptr = Значение по адресу
getch ();
return;
>
==================
Будьте внимательны в приведенном коде, где была операция разыменования, стоит звездочка. В первый раз было обращение непосредственно к указателю, а так как указатель есть переменная, хранящая адрес памяти, то именно с этим адресом памяти и работает программа при прямом обращении к указателю, поэтому и был выведен некоторый адрес памяти вместо значения. Во второй раз при обращении к указателю была поставлена звездочка. Такое обращение к указателю называется разыменование и указатель обращается к объекту на который ссылается. На экран было выведено значение, а не адрес памяти
Я бы порекомендовал любому начинающему поупражняться в выводе на экран адреса, который хранит указатель и в выводе значения, которое располагается по адресу, на который указатель ссылается. Очень важно понимать различие первого от второго. Это стоит знать как Hello World
=============================================================
Если вы добросовестно поупражнялись с выводом на экран значения и адреса этого значения и теперь понимаете, что к чему, значит имеет смысл перейти к следующему этапу.
В C++ у любой переменной, в том числе и не указателя можно узнать адрес памяти, по которому она расположена. Делается это с помощью оператора &
Код C++ Взять адрес у переменной. Ссылка
=============================
Так вот, после объявления указатель не связан с конкретным значением и указывает пальцем в небо. Для того, чтоб его связать с каким-то объектом используется оператор &. Вы уже знаете, что оператор & обозначает взятие памяти у переменной. А так как указатель есть адрес памяти , то и работает он с адресами памяти .
Следовательно, чтобы связать указатель с каким-то значением, нужно обратиться к адресу нужного значения . Надеюсь логика проста и понятна.
void main()
clrscr ();
int * ptr ; // ptr есть указатель на int
int a = 100 ; // a является обычной переменной
cout //Чтобы получить значение ptr – разыменовали указатель ptr
//так как указатель не был связан ни с какими значениями, можно получить что угодно
ptr =& a ; // Взяли адрес у переменной a и присвоили этот адрес в указатель
cout “* ptr = “ //Указатель теперь указывает на a . Значит *ptr = a = 100
getch ();
return;
>
=============================
Очень важный момент
Если указатель указывает на некоторый один адрес памяти, то и работает он со значением из этого адреса.
а) Изменяя значение переменной по адресу на который указатель указывает – изменится и значение разыменовываемого указателя
б) При присвоении значений разыменованному указателю – изменится значение переменной по указываемому указателем адресу
Код C++ двойственность природы указателей
================================
void main ()
clrscr ();
int a , b ; //Разные переменные
int * ptr ; // ptr есть указатель на int
//Вариант а. Изменение значения переменной для указателя
ptr =& a ; //Указатель ptr = адрес переменной a
a = 100 ; //присвоили любое значение в переменную на адрес которой указатель указывает
cout <“a = “ //т.к. указатель опирается на адрес переменной a, любое изменение a влияет на то, что отображает разыменованный указатель. *ptr=a=100
a = 999 ; //поменяли значение переменной
cout << “a = “ //указатель опирается на адрес переменной a . *ptr=a=999
/* Только что была продемонстрирована одна из сторон указателей. Тут менялось значение переменной */
//Вариант б. Изменение разыменованного указателя влияет на переменную по адресу указателя
a = 100 ; //чтоб было понятнее. изменим a на новое значение
с = 55 ; //вторая переменная
ptr =& a ; // Указатель = адрес переменной a => *ptr=a=100;
* ptr = 999 ; //По адресу переменной а записалось новое значение. a=*ptr=999;
ptr =& c ; //Указатель теперь указывает на адрес с . Влияние указателя на а прекращено
* ptr = 88 ; / /с изменилось было 55 – стало 88
cout //a=100 a=999
cout //c=55 c=88
getch ();
return;
>
================================
Двойственность природы указателей причина многих недопониманий и ошибок. Понять для начинающих очень непросто, но понять можно. А вот, чтобы с ними дружить надо понимать природу указателей и как с ними работать на превосходное отлично.
Подводя итоги я повторюсь и напишу самое главное из этой статьи про указатели.
- Указатель – это вид переменной, которая хранит адрес в памяти
- Объявляется указатель с помощью звездочки
- Чтобы обратиться к значению по адресу указателя используется разыменование
- У любой переменной можно взять адрес памяти. Для этого используется символ &
- Так как указатель есть адрес памяти, то ему можно присвоить адрес памяти от любой переменной
- У указателя двойственная природа. Можно обрабатывать адрес памяти, а можно значение внутри этого адреса.
Понять сложно, но можно. самое главное старайтесь понять, а не заучить
Какой тип имеет разыменованный указатель
Указатели поддерживают ряд операций: присваивание, получение адреса указателя, получение значения по указателю, некоторые арифметические операции и операции сравнения.
Присваивание адреса
Указателю можно присвоить адрес объекта того же типа, либо значение другого указателя. Для получения адреса объекта используется операция & :
int a ; int *pa ; // указатель pa хранит адрес переменной a
При этом указатель и переменная должны иметь один и тот же тип, в данном случае это тип int.
Разыменование указателя
Операция разыменования указателя представляет выражение в виде *имя_указателя . Эта операция позволяет получить объект по адресу, который хранится в указателе.
#include int main() < int a ; int *pa ; // хранит адрес переменной a std::cout #include int main() < int a ; int b ; int *pa ; // указатель на переменную a int *pb ; // указатель на переменную b std::cout pa: address=0x56347ffc5c value=10 pb: address=0x56347ffc58 value=2 pa: address=0x56347ffc58 value=2 b value=125
Нулевые указатели
Нулевой указатель (null pointer) - это указатель, который не указывает ни на какой объект. Если мы не хотим, чтобы указатель указывал на какой-то конкретный адрес, то можно присвоить ему условное нулевое значение. Для определения нулевого указателя можно инициализировать указатель нулем или константой nullptr :
int *p1; int *p2<>;
Ссылки на указатели
Так как ссылка не является объектом, то нельзя определить указатель на ссылку, однако можно определить ссылку на указатель. Через подобную ссылку можно изменять значение, на которое указывает указатель или изменять адрес самого указателя:
#include int main() < int a ; int b ; int *p<>; // указатель int *&pRef
; // ссылка на указатель pRef = &a; // через ссылку указателю p присваивается адрес переменной a std::cout &:
int a ; int *pa ; std::cout >, >=, , ,==, !=. Операции сравнения применяются только к указателям одного типа. Для сравнения используются номера адресов:
#include int main() < int a ; int b ; int *pa ; int *pb ; if(pa > pb) std::cout
Консольный вывод в моем случае:
pa (0xa9da5ffdac) is greater than pb (0xa9da5ffda8)
Приведение типов
Иногда требуется присвоить указателю одного типа значение указателя другого типа. В этом случае следует выполнить операцию приведения типов с помощью операции (тип_указателя *) :
#include int main() < char c ; char *pc ; // указатель на символ int *pd <(int *)pc>; // указатель на int void *pv ; // указатель на void std::cout std::cout
Указатель - про указатель в языке СИ
Забегая вперёд скажу, указатель это очень странный предмет простая вещь, вообще в языке СИ нет ничего проще чем указатель, и в тоже время это пожалуй самый мощный инструмент, с помощью которого можно творить великие дела . Однако многие падаваны не понимают что это такое, поэтому я попробую внести свою лепту.
Для лучшего понимания, вначале мы разберёмся с «обычными» переменными (впрочем указатель, это тоже обычная переменная, но пока мы условно разделим эти понятия).
Переменная
Итак, у нас есть переменные char, uint16_t, uint32_t, и прочие. Всё это «типизированные» переменные, то есть переменные хранящие определённый тип данных. Переменная char (8 бит) хранит однобайтовое число/символ, uint16_t (16 бит) хранит двухбайтовое число, и uint32_t (32 бита) хранит четырёхбайтовое число.
Теперь разберёмся что значит «переменная хранит» и как это вообще выглядит внутри «железа». Напомню, что бы мы не делали в компьютере или микроконтроллере, мы всего лишь оперируем значениями в ячейках памяти (ну или регистрами в случае с микроконтроллером).
Предположим что мы объявили и инициализировали (то есть записали в них значения) две переменные…
char sim = 'a'; uint16_t digit = 2300;
Что это за переменные, глобальные или нет значения не имеет, пускай будут глобальными.
Представим себе небольшой кусочек памяти компьютера где-то ближе к началу…
Клетки это ячейки памяти, а цифры это номера ячеек, то есть адреса. В каждой ячейке может храниться один байт данных (8 бит). Когда мы хотим обратится к тем или иным данным находящимся в памяти, мы обращаемся к ним по нужным нам адресам.
Но вот вопрос, откуда же мы знаем эти самые, нужные нам адреса, а ответ очень прост. Когда мы создали переменную char c именем sim , компилятор выделил для этой переменной одну ячейку (char у нас однобайтный) памяти, например ячейку 5676, после чего он ассоциировал имя sim с адресом этой ячейкой, а само имя уничтожил. То есть имена переменных это просто метки для адресов в памяти, которые нужны компилятору на определённом этапе компиляции. И теперь программа знает что когда происходит обращение к имени sim , это значит что нужно обратится к содержимому ячейки 5676.
После того как мы инициализировали переменную значением 'a' , это значение записалось в эту ячейку.
Когда же мы создали переменную uint16_t , компилятор посмотрел на её тип, понял что она двухбайтовая, и соответственно выделил под неё две ячейки — 5677 и 5678. И так же как и в первом случае, он ассоциировал эти две ячейки с именем digit . То есть обращаясь к имени digit , мы обращаемся к тому что хранится в ячейках с адресами 5677 и 5678. Ну и соответственно при инициализации, в эти ячейки записалось число 2300 …
Если мы захотим получить не содержимое ячейки, а её адрес, то для этого есть специальная конструкция, но об этом позже.
Я специально подчеркнул две фразы ибо в них кроется ключевой смысл отличающий «обычную» переменную от указателя, поэтому повторю — когда мы обращаемся к имени «обычной» переменной, мы обращаемся непосредственно к содержимому ячейки/ячеек. То есть мы оперируем именно данными, хранящимися в этой ячейке/ячейках.
Указатель
Наконец пришло время дать определение указателю.
Указатель это переменная, которая хранит в себе не какие-то данные (как это делает «обычная» переменная), а адрес какой-либо ячейки памяти, то есть указывает на какую-либо ячейку памяти .
Указатель объявляется так же как и «обычная» переменная, с той лишь разницей, что перед именем ставиться звёздочка…
char *ptr = NULL;
Тут стоит отметить, что при использовании указателя, звёздочка выступает в двух ипостасях, первая это как сейчас, при объявлении, а про вторую мы узнаем позже.
И да, звездочку можно ставить как угодно…
char *ptr = NULL; char * ptr = NULL; char* ptr = NULL;
Теперь увеличим наш кусочек памяти на две ячейки, чтобы было удобнее…
… и рассмотрим что же произойдёт внутри системы после объявления указателя.
Компилятор выделил в памяти четыре ячейки для указателя, например 5681, 5682, 5683 и 5684 (см. ниже).
Размер указателя в современных компьютерах бывает либо 32-ух битный (4 байта), либо 64-ёх битный (8 байт), так как он должен хранить в себе какой-то адрес памяти, к которому мы будем обращаться через этот указатель.
Таким образом в первом случае мы можем адресовать (обратится по адресу) до 4Гб, а во втором свыше восемнадцати квинтиллионов байт . Если бы указатель был меньшей разрядности, например 16-ти битный, то не смог бы хранить в себе адреса выше 65535.
Размер указателя связан отчасти с разрядностью ОС, отчасти с шиной данных, отчасти от режима компилятора (эмуляция 32-ух битных программ на 64-ёх битных системах), и ещё чёрт знает от чего, нам это совершенно не важно.
И так же как и в случае с «обычными» переменными, компилятор ассоциировал имя ptr с этими четырьмя ячейками, и произошла инициализация указателя нулём.
что такое NULL
NULL это дефайн из хедера стандартной библиотеки stdio…
#define NULL 0
В результате мы создали указатель, который хранит адрес нулевой ячейки памяти, то есть указывает на нулевую ячейку памяти…
Главное отличие указателя от «обычной» переменной: если бы ptr был «обычной» переменной и мы бы решили к ней обратится (например прочитать), то нам бы вернулось значение 0. С указателем же всё по другому: если бы мы сейчас обратились по имени ptr , то программа бы заглянула в эти четыре ячейки, увидела бы там адрес 0, и полезла бы в ячейку 0, то есть в нулевой адрес памяти.
Зачем же мы инициализировали наш указатель нулём, ведь обращение к нулевому адресу привело бы к мгновенному падению программы? Всё очень просто, давайте представим что мы объявили указатель без инициализации.
char *ptr;
Тогда в ячейках 5681, 5682, 5683 и 5684 скорее всего оказался бы какой-то «мусор» (какие-то бессмысленные цифры), и если бы мы в дальнейшем забыли присвоить указателю какой-то конкретный, нужный нам, адрес, и потом обратились бы к этому указателю, то скорее всего «мусор» оказался бы каким-то адресом, и мы сами того не зная случайно что-то сделали с хранящимися по этому адресу данными. Во что бы это вылилось неизвестно, скорее всего программа не упала бы сразу, а накуролесила страшных делов в процессе работы. Поэтому пока мы не присвоили указателю какого-то конкретного адреса, мы его «занулили» для собственной безопасности.
Итак, прежде чем двигаться дальше подобьём итоги: указатель это 32-ух битная (или 64-ёх битная) переменная, которая хранит в себе не данные, а адрес какой-то одной ячейки памяти.
Типы
Теперь разберёмся с типами, на которые указывает указатель. Сейчас мы создали указатель с типом char потому что будем присваивать ему адрес переменной с типом char .
Важно! Мы должны присваивать указателю адрес переменной того же типа что и сам указатель (ниже объясню почему). То есть мы не можем нашему указателю присвоить адрес переменной digit , компилятор на это изрыгнёт предупреждение. Для переменной digit нужен указатель с соответствующим типом uint16_t , и тогда всё будет окей…
uint16_t *ptr = NULL; ptr = &digit;
То же самое касается и других типов переменных. Например для типа float будет так…
float my_float = 34.0; float *ptrf = NULL; ptrf = &my_float;
Ниже мы ещё вернёмся к переменной digit и другим типам данных.
Присваивание адреса указателю и «взятие адреса» обычной переменной
Далее давайте присвоим нашему указателю конкретный адрес, на который он будет указывать.
Мы хотим сделать так чтобы наш указатель указывал на ячейку памяти, в которой храниться значение переменной sim , то есть на ячейку в которой лежит символ 'a' . Вопрос в том как это сделать — мы же не можем просто взять и присвоить указателю значение переменной, то есть сделать так…
char sim = 'a'; uint16_t digit = 2300; char *ptr = NULL; ptr = sim;
Указатель должен хранить адрес, а мы пытаемся запихать в него символ 'a' , так мы получим предупреждение компилятора.
Нам нужно узнать адрес ячейки в которой лежит символ 'a' , и записать его в указатель (присвоить указателю). Делается это очень просто, надо перед именем переменной добавить амперсанд (&), то есть сделать так…
Эта операция называется " взятие адреса ". Выше я писал — «Если мы захотим получить не содержимое ячейки, а её адрес, то для этого есть специальная конструкция», это оно и есть. Таким образом мы можем получить адрес любой переменной.
То есть наша программа будет выглядеть так…
char sim = 'a'; uint16_t digit = 2300; char *ptr = NULL; ptr = ∼
Теперь наш указатель хранит адрес ячейки (5676), в которой храниться символ 'a' , то есть указывает на неё. В железе это выглядит так…
Если добавим вот такой вывод на печать…
printf("Var sim %c\n", sim); printf("Adr sim %p\n", &sim); printf("Ptr sim %p\n", ptr);
… то получим искомые данные…
Переменная sim хранит символ 'a' , Adr это её адрес, ну и указатель указывает на тот же адрес (0x7ffecc3133ed это то, что на схеме выше обозначено как 5676) .
Здесь, и ниже, на картинках, у меня 64-ёх битный указатель — не обращайте на это внимание. Просто я поленился рисовать восемь клеточек на схемах выше.
Вот тоже самое, только выполнено на микроконтроллере stm32.
Здесь указатель 32-ух битный.
Разыменования указателя
Теперь разберёмся с ещё одной важной вещью. Выше я писал что при работе с указателем звёздочка выступает в двух ипостасях, с первой мы познакомились, это объявление указателя, а вторая это получение данных из ячейки на которую указывает указатель, или запись данных в эту ячейку. Это называется «разыменование указателя».
По сути это действо обратно «взятию адреса» обычной переменной, только вместо амперсанда используется звёздочка, а вместо имени переменной, имя указателя.
Для примера создадим ещё одну переменную типа char и с помощью " разыменования указателя " запишем в неё значение, которое храниться в ячейке на которую указывает указатель ptr , то есть символ 'a' …
char sim = 'a'; uint16_t digit = 2300; char *ptr = NULL; ptr = ∼ char sim2; // новая переменная sim2 = *ptr; // записываем в новую переменную символ 'a' с помощью "разыменования указателя" printf("Var sim2 %c\n", sim2);
Результат будет таков…
Мы прочитали значение из ячейки на которую указывает указатель, и записали его в переменную sim2 .
Разыменование указателя работает в обе стороны, то есть мы можем не только прочитать значение, но и записать в разыменованный указатель. То есть мы запишем новое значение в ячейку на которую указывает указатель.
Изменим наш пример…
char sim = 'a'; uint16_t digit = 2300; char *ptr = NULL; ptr = ∼ printf("Var sim %c\n", *ptr); // выводим старое значение (с помощью разыменования указателя) *ptr = 'b'; // записываем новое значение в разыменованный указатель printf("Var sim %c\n", *ptr); // выводим новое значение
Смотрим что получилось…
В результате наш указатель будет по прежнему указывать на ту же ячейку 5676, но значение в этой ячейке изменилось. То есть изменилось значение переменной sim .
Можем в функциях printf() заменить разыменованный указатель (*ptr) на имя переменной…
char sim = 'a'; uint16_t digit = 2300; char *ptr = NULL; ptr = ∼ printf("Var sim %c\n", sim); // выводим старое значение *ptr = 'b'; // записываем новое значение в разыменованный указатель printf("Var sim %c\n", sim); // выводим новое значение
Как вы уже наверно начинаете понимать, указатель это весьма любопытный инструмент, и мы уже начали использовать его по разному, но погодите, дальше будет интересней.
Термин «разыменованный указатель» вовсе не означает что указатель куда-то пропадает из-за того что мы «лишили его имени» и теперь он где-то бродит безымянный и неприкаянный, нет, просто это такой не самый удачный термин, а указатель как был так остаётся указателем со своим именем.
Если хотим изменить адрес на который указывает указатель, тогда просто присваиваем указателю новый адрес. Для примера поочерёдно присвоим одному и тому же указателю адреса разных переменных…
char sim1 = 'a'; char sim2 = 'b'; char sim3 = 'c'; char *ptr = NULL; ptr = &sim1; printf("Var sim1 %c, Adr %p\n", sim1, ptr); // адрес переменной sim1 ptr = &sim2; printf("Var sim2 %c, Adr %p\n", sim2, ptr); // адрес переменной sim2 ptr = &sim3; printf("Var sim3 %c, Adr %p\n", sim3, ptr); // адрес переменной sim3
Сколько раз хотим столько раз и меняем адреса. Разумеется типы всех переменных должны быть char .
Видно что указатель указывает на три разных адреса трёх наших переменных. Заодно видим что переменные расположились в памяти друг за дружкой.
Ну и конечно можем для каждой переменной создать свой указатель…
char sim1 = 'a'; char sim2 = 'b'; char sim3 = 'c'; char *ptr1 = NULL; char *ptr2 = NULL; char *ptr3 = NULL; ptr1 = &sim1; printf("Var sim1 %c, Adr %p\n", sim1, ptr1); // адрес переменной sim ptr2 = &sim2; printf("Var sim2 %c, Adr %p\n", sim2, ptr2); // адрес переменной sim2 ptr3 = &sim3; printf("Var sim3 %c, Adr %p\n", sim3, ptr3); // адрес переменной sim3
Поскольку в использовании звёздочки прослеживается некое противоречие (сначала она означает объявленный указатель, потом разыменованный), стоит повторить всё что касается этого вопроса для закрепления информации.
Первое. Когда мы объявляем указатель, мы ставим звёздочку — здесь всё просто и понятно.
Второе. При использовании указателя по ходу программы. Когда мы используем имя указателя без звёздочки, мы получаем адрес ячейки на которую он указывает…
ptr1 = &sim1; printf("Adr %p\n", ptr1); // адрес переменной sim1
Разумеется в дальнейшем мы будем использовать указатель без звёздочки не только для вывода адреса на печать.
Когда мы ставим звёздочку перед именем указателя, мы получаем содержимое ячейки на которую он указывает…
ptr1 = &sim1; printf("Var %c\n", *ptr1); // содержимое переменной sim1
Или делаем запись нового значения в ячейку на которую он указывает…
ptr1 = &sim1; printf("Var %c\n", *ptr1); // содержимое переменной sim1 *ptr1 = 'b'; // записываем новое значение printf("Var %c\n", *ptr1);
С этой звёздочкой у людей частенько возникают трудности из-за неправильного использования, так что будьте внимательны.
Теперь давайте разберёмся с переменной digit . Поскольку эта переменная 16-ти битная, соответственно и указатель на неё должен иметь 16-ти битный тип, то есть такой…
uint16_t digit = 2300; uint16_t *ptr16 = NULL; ptr16 = &digit;
Создали указатель и присвоили ему адрес переменной digit .
Здесь стоит заострить внимание читателя. Как я уже говорил выше, сам указатель либо 32-ух битный, либо 64-ёх битный (это для нас не имеет никакого значения), но вот тип данных на которые он указывает, может быть различный, и это очень важно. Поэтому когда мы при объявлении указателя прописываем тип, этот тип относится именно к типу данных на которые будет указывать указатель.
В прошлый раз мы создавали указатель с типом char так как он указывал на однобайтовую переменную sim . Теперь же мы создали указатель на двухбайтовую переменную и поэтому объявили указатель с соответствующим типом.
В памяти получилась следующая картина (рисунок я оставил прежний чтоб не перерисовывать)…
Теперь указатель хранит адрес первой ячейки 16-ти битной переменной (стрелочкой указывает на неё), а поскольку при объявлении указателя мы сообщили компилятору что указатель будет указывать на 16-ти битный тип, то программа знает что при обращении к указателю нужно прочитать ячейку на которую он указывает, и следующую за ней ячейку, то есть две ячейки — 5677 и 5678…
Таким образом, благодаря типу прописанному при объявлении указателя, программа знает какое количество ячеек нужно прочитать при обращении к этому указателю.
Если нужен указатель который будет в дальнейшем указывать на однобайтовую переменную char или uint8_t , тогда создаём указатель с соответствующим типом…
char *ptr = NULL;
uint8_t *ptr = NULL;
Программа будет знать что при обращении к этому указателю нужно прочитать только одну ячейку, на которую он указывает.
Если нужен указатель который будет в дальнейшем указывать на двухбайтовую переменную uint16_t , тогда прописываем двухбайтовый тип…
uint16_t *ptr16 = NULL;
Программа будет знать что при обращении к этому указателю нужно прочитать ячейку, на которую он указывает, и следующую за ней.
Если нужен указатель который будет в дальнейшем указывать на четырёхбайтовую переменную uint32_t , тогда прописываем четырёхбайтовый тип…
uint32_t *ptr32 = NULL;
Программа будет знать что при обращении к этому указателю нужно прочитать ячейку, на которую он указывает, и ещё три следующие за ней.
Если нужен указатель который будет в дальнейшем указывать на переменную float , тогда прописываем тип float…
float *ptr_f = NULL;
Программа будет знать что при обращении к этому указателю нужно прочитать ячейку, на которую он указывает, и ещё три следующие за ней (тип float занимает четыре байта).
Таким образом, тип указателя должен всегда точно соответствовать типу переменной на которую он будет указывать.
Теперь когда мы немного познакомились с указателем, давайте посмотрим как он работает на практике. Создадим простейшую программу…
#include #include void func_var(uint16_t var) < var++; printf("var %d\n", var); >int main(void)
Результат работы будет таков…
Мы передали переменную digit в функцию func_var(), увеличили на единичку и вывели на печать. Потом в основной функции тоже вывели на печать эту переменную, и разумеется получили результат без увеличения. Это произошло потому, что когда мы передавали переменную в функцию, мы её как-бы скопировали в другую переменную, объявленную в аргументе (uint16_t var). Переменная var в функции func_var() увеличилась, а оригинал как был так и остался равен 2300.
А сейчас изменим нашу программу вот так…
void func_var(uint16_t *ptr_var) < *ptr_var = *ptr_var + 1; printf("var %d\n", *ptr_var); >int main(void) < uint16_t digit = 2300; uint16_t *ptr_digit = NULL; // объявили указатель ptr_digit = &digit; // присвоили указателю адрес переменной digit func_var(ptr_digit); // передали указатель (хранящий адрес переменной digit) в функцию printf("digit %d\n", digit); >
Результат получим иной…
В обоих случаях значение увеличилось на единицу.
В основной функции мы объявили указатель, присвоили ему адрес переменной digit, и передали этот указатель в функцию func_var(). Аргументом этой функции мы объявили указатель (ptr_var) в который при передаче записался адрес переменной digit. Это значит, что теперь указатель ptr_var так же как и указатель ptr_digit указывает на адрес переменной digit, и следовательно манипулируя указателем ptr_var мы можем изменить значение этой переменной.
В функции мы разыменовываем ptr_var, то есть получаем доступ к значению хранящемуся в ячейках, прибавляем к этому значению единицу — *ptr_var + 1 (2300 + 1), и опять же с помощью разыменования записываем в ячейки новое значение — *ptr_var = *ptr_var + 1 . Теперь переменная digit хранит значение 2301. Следовательно в обоих функциях на печать выводится одно и то же значение.
Основную функцию мы можем немного упростить, сделав её такой…
void func_var(uint16_t *ptr_var) < *ptr_var = *ptr_var + 1; printf("var %d\n", *ptr_var); >int main(void)
Результат мы получим тот же, что и в предыдущем примере.
Здесь мы не стали объявлять указатель и присваивать ему адрес переменной, а просто воспользовались операцией «взятие адреса» и передали этот адрес в функцию.
Оба варианта идентичны по своему смыслу, однако я хотел показать, что можно передавать и указатель, и «голый» адрес.
Суть этих примеров с переменной digit заключалась в том, чтобы показать, что когда мы передаём переменную в какую-то функцию, то её изначальное значение не изменится, а если мы передаём указатель на эту переменную, то можем менять изначальное значение локальной переменной откуда угодно. Однако во всей красе возможности указателя раскрываются с другими типами данных, например с массивом.
Массив
Все программисты используют в своих программах массивы, но не все знают, что массив, а точнее имя массива это указатель указывающий на первый элемент этого массива. При этом объявляется он без всяких звёздочек. То есть в чём то он похож на «обычную» переменную. Если быть ещё более точным, то массив можно представить себе как набор однотипных переменных, расположенных в памяти друг за дружкой, а каждая из этих «переменных» является элементом массива.
Значение в квадратных скобочках говорит о том, сколько элементов содержится в этом массиве, а тип говорит о том, какого размера элементы этого массива, то есть сколько ячеек памяти занимает один элемент. Для примера возьмём такой массив…
char array[4] = ;
Массив из четырёх элементов. Каждый элемент занимает в памяти одну ячейку (об этом говорит тип char). В каждый из элементов мы записали по одному символу, то есть инициализировали весь массив конкретными значениями.
Чтобы вывести массив на печать делаем так…
printf("array %s\n", array);
Здесь всё выглядит так, как будто мы обратились к «обычной» переменной и вывели её на печать. Тем не менее легко доказать что array всё таки указатель. Достаточно изменить форматирующий символ «s» на «p»…
printf("array %p\n", array);
И мы получим адрес…
Если же мы сделаем разыменование array …
printf("array %c\n", *array);
То получим первый элемент массива…
Что доказывает сказанное выше — имя массива это указатель на первый элемент этого массива.
То же самое мы получим если добавим к имени индекс нулевого элемента массива…
printf("array %c\n", array[0]);
В памяти это представляется следующим образом…
Имя array указывает на первый элемент массива (ячейка 5676), а следом идут остальные три элемента.
Чтобы нам было удобно обращаться к отдельным элементам этого массива компилятор любезно присвоил элементам индексы, начиная с нулевого. То есть ячейка 5676 получает индекс 0, ячейка 5677 получает индекс 1, ячейка 5678 получает индекс 2, и т.д. Важно помнить что отсчёт элементов ведётся от ноля.
На схеме индексов не видно, но программа знает какой ячейке присвоен какой индекс.
Квадратные скобки при использовании массива имеют двойное назначение. При объявлении массива в них указывается количество элементов, а в процессе работы индекс ячейки, то есть её порядковый номер в данном массиве.
Благодаря индексации мы легко и просто можем обращаться к любому элементу…
printf("array1 %c\n", array[0]); printf("array2 %c\n", array[1]); printf("array3 %c\n", array[2]); printf("array4 %c\n", array[3]);
Чаще всего индексацию используют в циклах для записи в массив новых значений…
int main(void) < char array[4] = ; for(uint8_t i = 0; i < 4; i++) < array[i] = 'Z'; >printf("array %s\n", array); >
Переменная «i» приращивается в цикле и выступает в роли индекса элемента массива. Таким образом мы заполним все элементы символом «Z»…
Теперь создадим массив из двух элементов с типом uint16_t …
uint16_t array16[2] = ;
В таком массиве каждый элемент занимает две ячейки памяти…
Имя массива указывает на первую ячейку памяти первого элемента, а сами элементы хранят значения которые мы записали туда при инициализации массива.
Здесь индексы опять же присваиваются элементам массива. Индекс [0] отвечает за ячейки 5676 и 5677, а индекс [1] за ячейки 5678 и 5679. То есть индекс перескакивает через одну ячейку так как благодаря указанному типу uint16_t программа знает что каждый элемент массива занимает две ячейки памяти.
Чтоб проверить как работает индексация мы сначала прочитаем что храниться в элементах массива, а следом запишем в них число 999…
int main(void) < uint16_t array16[2] = ; printf("Read array16\n"); for(uint8_t i = 0; i < 2; i++) < printf("array16[%d] %d\n", i, array16[i]); >printf("\nWrite array16\n"); for(uint8_t i = 0; i < 2; i++) < array16[i] = 999; printf("array16[%d] %d\n", i, array16[i]); >>
Получим что ожидали…
Думаю понятно, что при использовании типа uint32_t , каждый элемент массива будет занимать четыре ячейки памяти, и соответственно каждый индекс отвечает за четыре ячейки.
Как и в случае с «обычной» переменной, мы можем к элементу массива применить операцию «взятия адреса»…
int main(void) < uint16_t array16[2] = ; printf("Adr array16[0] %p\n", &array16[0]); printf("Adr array16[1] %p\n", &array16[1]); >
Видно что адрес второго элемента больше первого на два. То есть адрес первой ячейки первого элемента . 374, а второй ячейки будет . 375. То же самое со вторым элементом — адрес первой ячейки . 376, а второй будет . 377.
А теперь давайте зафиксируем мысль на этом последнем примере и перейдём к следующему, очень важному понятию в теме про указатели, к «адресной арифметике» или «арифметики с указателями».
Адресная арифметика
Оперируя указателями мы оперируем хранящимися в указателях адресами, а адреса в свою очередь это всего лишь цифры, а раз это цифры, то значит мы можем производить над ними арифметические действия. То есть если вычесть или прибавить к указателю какую-то цифру, то этот указатель будет указывать уже на другую ячейку памяти. Вроде бы всё просто, но здесь есть существенный нюанс — вся эта арифметика жёстко связана с типом указателя. Сейчас мы убедимся в этом воспользовавшись нашим последним примером.
Освежим в голове нашу схему…
И добавим в последний пример ещё одну строчку…
int main(void) < uint16_t array16[2] = ; printf("Adr array16[0] %p\n", &array16[0]); printf("Adr array16[1] %p\n", &array16[1]); printf("Adr array16[0] %p\n", &array16[0] + 1); >
Как мы помним первые две строчки напечатают адреса первых ячеек первого и второго элемента массива (5676 и 5678), а в последней строчки мы прибавили единицу к адресу первой ячейки первого элемента. Таким образом мы предполагаем что получим адрес второй ячейки первого элемента, то есть адрес 5677 .
А теперь смотрим что получилось на самом деле…
В первых двух строках мы получили что хотели (как и в предыдущем примере), а в третьей строке мы вроде как должны были получить . e85 (адрес второй ячейки первого элемента), но наши надежды не оправдались, мы получили адрес второго элемента. Как же так, в чём ошибка? А ошибки никакой и нет, программа всё сделала правильно.
Как я уже говорил выше, адресная арифметика жёстко привязана к типу указателя, поэтому когда мы прибавили к указателю единицу он увеличивается не на 1, а на размер элемента массива. То есть наша конструкция выглядела как «плюс один элемент». Тип массива у нас uint16_t, значит размер элемента два байта, поэтому программа увеличила адрес на 2, и поэтому мы получили адрес первой ячейки второго элемента, а не то, что предполагали. А если бы мы применили эту конструкцию ко второму элементу, то ещё и вылетели бы за границы массива.
Этот нюанс нужно хорошенько запомнить, ибо многим начинающим программистам он стоил немалого количества вырванных волос и сломанных клавиатур .
Вот если мы будем работать с массивом типа char (или uint8_t), тогда адресная арифметика будет работать как обычная. Размер элемента один байт, значит и адрес будет увеличиваться на единицу.
int main(void) < char array[4] = ; printf("array[0] %p\n", array); printf("array[1] %p\n", array + 1); printf("array[2] %p\n", array + 2); printf("array[3] %p\n", array + 3); >
Все адреса подряд.
Адресную арифметику удобно применять при парсинге строк. Например у нас есть массив со строкой (в языке СИ нету строк, есть только массивы), и нам нужно вывести на печать эту строку начиная с четвёртого символа, тогда делаем так…
int main(void)
Отрезали три первых символа.
Или допустим мы хотим перегнать из одного массива в другой строку начиная с четвертого символа…
int main(void) < char src[] = "istarik.ru"; // массив источник char dst[8] = ; // массив приёмник char *p = NULL; // создаём указатель p = src + 3; // присваиваем новому указателю адрес массива-источника начиная с четвёртой ячейки for(uint8_t i = 0; i < 8; i++) < dst[i] = *p; // разыменовываем указатель и записываем значение в элемент массива-приёмника p++; // увеличиваем адрес на единицу >printf("Src - %s\n", src); printf("Dst - %s\n", dst); >
Все действия я прокомментировал.
И вот вам ещё один пример демонстрирующий крутость указателя. В этой программе мы легко и непринуждённо уберём все нижние подчёркивания и запятые из строки…
void clear_str(char *src) < char *dst = NULL; dst = src; for(; *src != 0; src++) < if(*src == '_' || *src == ',') continue; *dst = *src; dst++; >*dst = 0; > int main(void)
Здесь я не буду ничего комментировать. В среде программистов бытует мнение, что указатель нельзя выучить, его можно только понять, как озарение. Сам через это проходил. Поэтому когда вы поймёте что происходит в этом примере, это будет означать что вы поняли указатель
Ну, а после понимания, всякие штуки типа указателя на указатель, и функции-указатели вы будете щёлкать как орешки.
Кстати, любопытная вещь — имя обычной функции, только без скобочек — это указатель на эту функцию, то есть адрес в памяти где расположена эта функция. Ради интереса можете добавить в последний пример строчку.
printf("F %p\n", clear_str);
Это всё, всем спасибо
-->
Поддержать автора
Задать вопрос по статье
Известит Вас о новых публикациях