Что означает две звездочки в c
Указатели сами по себе представляют значения, которые можно хранить в массивах. То есть в итоге получится массив указателей.
Массив указателей определяется одним из трех способов:
тип *имя_массива [размер]; тип *имя_массива [] = инициализатор; тип *имя_массива [размер] = инициализатор;
Используем все эти способы:
int array[] = ; int *p1[3]; int *p2[] = < &array[1], &array[2], &array[0] >; int *p3[3] = < &array[3], &array[1], &array[2] >;
Массив указателей p1 состоит из трех элементов, но он не инициализирован и является пустым.
Массивы p2 и p3 в качестве элементов хранят адреса на элементы массива a.
Выведем на конслоль значения, на которые ссылаются указатели:
#include int main(void) < int array[] = ; int *p[] = < &array[1], &array[2], &array[0] >; for(int i = 0; i < 3; i++) < printf("%d", *p[i]); >return 0; >
Здесь выражение *p[i] означает, что мы сначала обращаемся к i-тому адресу в массиве p, а потом применяет операцию разыменования для получения данных по этому адресу. В итоге на консоль будет выведено в строку:
Вместо *p[i] мы могли бы написать **(p+i) :
- p+i — к адресу в указателе p прибавляем число i и таким образом перемещаемся по указателям в массиве p.
- *(p+i) — разыменовываем i-тый указатель в массиве и в результате получаем адрес одного из элементов из массива array .
- **(p+i) — получаем значение по полученному на предыдущем шаге адресу элемента из массива array.
#include int main(void) < int array[] = ; int *p[] = < &array[1], &array[2], &array[0] >; for(int i = 0; i < 3; i++) < printf("%d", **(p+i)); >return 0; >
Указатель и массив строк
Соответственно если указатель типа char можно представить в виде строки, то массив указателей типа char представляет собой массив строк:
#include int main(void) < char *fruit[] = ; for(int i=0; i < 5; i++) < printf("%s \n", fruit[i]); >return 0; >
Здесь массив указателей fruit хранит пять строк — фактически пять адресов, по которым размещены начальные символы каждой строки. Результат работы программы:
apricot apple banana lemon orange
Также мы могли бы написать:
#include int main(void) < char *fruit[] = ; for(int i=0; i < 5; i++) < printf("%s \n", *(fruit + i)); >return 0; >
Указатели на указатели
Кроме обычных указателей в языке Си мы можем создавать указатели на другие указатели. Если указатель хранит адрес переменной, то указатель на указатель хранит адрес указателя, на который он указывает. Такие ситуации еще называются многоуровневой адресацией.
int **ptr;
Переменная ptr представляет указатель на указатель на объект типа int . Две звездочки в определении указателя говорят о том, что мы имеем дело с двухуровневой адресацией. Например:
#include int main(void) < int x = 22; int *px = &x; // указатель px хранит адрес переменной x int **ppx = &px; // указатель ppx хранит адрес указателя px printf("Address of px: %p \n", (void *)ppx); printf("Address of x: %p \n", (void *)*ppx); printf("Value of x: %d \n", **ppx); return 0; >
Здесь указатель ppx хранит адрес указателя px . Поэтому через выражение *ppx можно получить значение, которое хранится в указателе px — адрес переменной x . А через выражение **ppx можно получить значение по адресу из px , то есть значение переменной x .
Указатель на указатель + динамическое выделение памяти (часть 1)
Обращаюсь к новичкам, которые только начали изучать указатели: «Если вас заинтересовала эта тема и вы хотите в ней разобраться, что я могу вам сказать — ситуация не из приятных!» ))) Кто бы и как бы усердно и старательно не объяснял вам что к чему, понять указатели на указатели сложно. Сам указатель на указатель содержит в себе адрес, который ссылается на другой адрес, а он, в свою очередь, ссылается на адрес в памяти, где хранятся данные. Вроде бы и можно понять. Но как это применять на практике? Зачем оно надо. Это понять сложнее. А надо «оно», среди прочего, для возможности работы с массивами указателей, которые указывают на память с данными (строками, например). Каждый элемент этого массива — это указатель, который содержит в себе адрес строки (первого элемента символьного массива):
Наша ситуация усложняется еще и тем, что в данной статье мы постараемся доступно показать, как выделять динамическую память под двумерный массив указателей и как ее освобождать. Ну что, испугались? Тогда начнем разбираться ! Если вы еще слабо знаете тему Указатели, прочтите все таки сначала эту статью. Она поможет вам подготовиться к восприятию темы Указатель на указатель.
А в данной статье мы будем рассматривать пример, в котором перед нами ставится следующая задача: у нас есть указатель на указатель char **pp (он будет содержать адрес массива указателей на строки) и размер этого массива int size , который изначально равен 0. Нам надо написать функцию, которая будет выделять динамическую память для новых элементов массива указателей и для хранения символов новых строк. Эта функция будет принимать, как параметры, указатель на указатель, размер массива указателей и строку, которую надо будет записать в выделенную под нее память. Чтобы не усложнять задачу, в ней не будет диалога с пользователем. Пять строк мы определим сразу при вызовах функции.
Если у вас есть возможность, пишите исходный код по мере прочтения. Так будет легче его понять. Детальные объяснения увидите под кодом.
#include ; #include ; using namespace std; char **AddPtr (char **pp, int size, char *str); //прототип функции int main() < setlocale(LC_ALL, "rus"); int size = 0;//количество указателей на строки char **pp = 0;//указатель на массив указателей, которые содержат адреса строк cout delete [] pp; // потом выделенную под массив указателей return 0; > char **AddPtr (char **pp, int size, char *str) < if(size == 0)< pp = new char *[size+1]; //выделяем память для указателя на строку >else < //если массив уже не пустой, данные надо скопировать во временный массив **copy char **copy = new char* [size+1]; //создаем временный массив for(int i = 0; i < size; i++) //копируем в него адреса уже определенных строк < copy[i] = pp[i]; >//теперь строки хранятся в адресах copy delete [] pp; //освобождаем память, которая указывала на строки pp = copy; //показываем указателю на какие адреса теперь ссылаться > pp[size] = new char [strlen(str) + 1]; //выделяем память на новую строку strcpy(pp[size], str); //и копируем новую строку в элемент pp[size]. return pp; >
В строке 6 объявляем прототип функции char **AddPtr (char **pp, int size, char *str); . Перед названием функции ставим две звездочки, так как функция будет возвращать указатель на указатель. В главной функции main() все достаточно просто. Создаем указатель на указатель типа char **pp , который изначально ни на что не указывает, и счетчик элементов массива указателей size — строки 12-13. Далее (строки 17 — 30) идет поочередное наращивание массива указателей и добавление в него данных, посредством вызова функции AddPtr() . При этом, каждый раз после вызова функции мы увеличиваем значение size на единицу. Теперь переместимся к самому интересному — к определению функции AddPtr() строки 44 — 66. Как уже говорилось выше, в виде параметров функция будет принимать уже объявленный нами указатель на указатель, счетчик элементов массива указателей и определённую нами строку. При первом вызове, в функцию передаётся нулевое значение счетчика size . Срабатывает if (size == 0) (строки 46 — 48) в котором мы выделяем динамическую память для первого элемента массива указателей pp = new char *[size+1]; . Перед квадратными скобками стоит оператор звездочка * , который показывает компилятору , что нужно выделить динамическую память под один указатель (а не просто под символ char , если бы звездочки не было). If отработал и мы перемещаемся в строку 62. Тут мы «говорим» — пусть 0-й элемент массива указателей (указатель pp[size] ) указывает на массив символов размером [strlen(str) + 1] (размер определённой нами строки + 1 символ для ‘\n’ ). И следующим логичным шагом будет копирование строки, переданной в функцию, в этот выделенный участок памяти — строка 63. И в завершении работы, функция возвращает в программу указатель на указатель (тот самый указатель, который хранит адрес нулевого элемента массива указателей на строки). И наш, объявленный в main() , char **pp теперь будет хранить в себе значение этого адреса, так как вызов функции выглядит так pp = AddPtr(pp, size, «11111111111111111»); (присвоить значение, которое вернет функция). Функция отработала — память выделена, данные внесены.
Вызываем функцию второй раз — строка 20. При этом вызове уже сработает блок else определённый в строках 49 — 60. У нас уже есть строка, данные которой нам надо не потерять и добавляется еще одна, для которой надо создать новый указатель в массиве указателей, выделить динамическую память и записать туда данные. Поэтому создаем временную копию нашего указателя и выделяем память уже под два элемента массива указателей char **copy = new char* [size+1]; . Копируем в него указатель на перовую строку (нулевой элемент массива указателей) — copy[i] = pp[i]; . Освобождаем память, которая указывала на первую строку. Так как это массив указателей (пусть даже пока с одним элементом) чтобы освободить занимаемую им память, надо перед именем указателя поставить квадратные скобки — delete [] pp; . Нам эта память больше не нужна, так как на нее уже указывает copy[0] . И показываем указателю pp на какой новый участок памяти надо теперь ссылаться — строка 59 . Так — первая строка у нас сохранена и на нее теперь указывает pp[0] . И теперь мы снова переходим к строкам 62 — 63, где выделяется память для второй строки и строка копируется в этот участок памяти.
Таких вызовов функций у нас пять. Постепенно массив указателей растет, а новые строки заполняются данными. Чтобы убедиться, что все работает правильно и все данные сохранены, показываем все строки на экран с помощью цикла for — строки 32-33. Как видите, мы обращаемся к элементам массива указателей. А так как они ссылаются на адреса строк (на 0-е элементы символьных массивов), на экран выводятся соответствующие строки.
Перед завершением работы программы, нам надо освободить память занимаемую строками. Это мы реализуем с помощью цикла:
for(int i = 0; i
Так освобождаем динамическую память, на которую ссылаются указатели из массива указателей. А далее освобождаем память, выделенную под сам массив указателей — строка 40.
CppStudio.com
~~~~~Добавляем указатели на пять строк и заполняем строки данными~~~~~
11111111111111111
22222222222222222
33333333333333333
44444444444444444
55555555555555555
Условие этой задачи мы выполнили. Надеюсь, вы оценили главное преимущество использования указателей вместо обычных массивов. При входе в программу мы не знаем, сколько строк нам будет необходимо и какой объем памяти они будут занимать. Но мы не объявляли несколько десятков символьных массивов с размером [много памяти] (а вдруг пригодятся, если пользователь будет вводить много длинных строк). Вместо этого у нас получился один динамический массив указателей на строки, память для которых так же выделяется динамически.
Во второй части этой статьи мы добавим в программу еще две функции. Одна будет удалять выбранную нами строку и указатель на нее. Вторая будет вставлять указатель на строку в выбранную нами ячейку массива указателей. Не переживайте. Если вам более менее понятно, что произошло в примере выше, то дальше будет легче всё понять.
К сожалению, для данной темы пока нет подходящих задач. Если у вас есть таковые на примете, отправте их по адресу: admin@cppstudio.com. Мы их опубликуем!
Почему указатели трудны и что с этим делать
Указатели сложны не потому, что это объективно что-то сложное, а потому, что авторы языка Си — козлы, которые позаботились об экономии числа нажатий при печатании, а о том, удобство чтения кода куда важнее, чем удобство его написания, они не подумали. Чего только стоят все эти название функций типа strcspn () и feof ().
Человек должен быть парсером, а это не то, что человеку хорошо удаётся. В случае с указателями, кроме того, что используются плохо читаемые символы, ещё и нет их однозначного «перевода» на человеческий. Например, звёздочка рядом с именем переменной в выражении означает обратное тому, что она означает в описании переменной. Си:
int n[10]; // здесь n хранит адрес, но нет ни звёздочки, ни амперсанда n[5] = 77; // незаметно поиграли в указатели *(n + 5) = 77; // здесь звёдочка означает «значение по адресу» (уже заметно) char *s; // здесь звёздочка уже означает «переменная хранит не значение, а адрес» unsigned int m = 2131; *((char *) &m + 1) = 'A'; // теперь, если у меня правильно взорвался мозг, m ""=="" 2113
Если бы вместо звёздочки и амперсанда использовались конструкции addressof () и valueat (), для объявления типов был бы модификатор address, а для кастинга использовался бы оператор as указатели бы понимало в 10 раз больше человек. Назовём такой язык Ди (хоть такой уже и есть).
Квадратные скобки в выражениях в Ди пусть означают «значение по адресу с указанным сдвигом», тогда есть для любого x будет справедливо:
x == x[0] == valueat (addressof (x) + 0).
Наличие квадратных скобок в объявлении переменной пусть само по себе не превращает переменную в указатель, то есть, если мы захотим указатель, нам придётся дописать слово address. Тогда запишем первые три строчки нашего кода на Ди:
int address n[10]; valueat (n)[5] = 77; // пока получается некрасиво valueat (n + 5) = 77; // а тут — нормально
Так обращение к элементам массива, как видно, получается слишком громоздким. Но кто нас заставляет вообще играть в указатели там, где это не нужно? Квадратные скобки в объявлении переменной у нас просто резервируют памяти на несколько таких переменных, но в указатель её не превращают. Так не будем этого делать и мы, выкинем слово address:
int n[10]; // так само n будет хранить значение нулевого инта (n == n[0], напомню) n[5] = 77; // значение со сдвигом — как раз то, что нам нужно valueat (addressof (n) + 5) = 77; // длинная запись того же самого
Теперь простая вещь выглядит просто, а игры с указателями выглядят как игры с указателями, но вдобавок не теряют понятности. Синонимичность последних двух строк тоже очевидна. Это прекрасно. А вот другие наши сишные строчки в переводе на Ди:
char address s; unsigned int m = 2131; valueat ((addressof (m) as char address) + 1) = 'A'
Теперь, если мы что-нибудь перепутаем, это сразу будет видно:
valueat ((m as char address) + 1) = 'A' // пытаемся представить значение в качестве адреса valueat (m) = 'A' // пытаемся взять значение по адресу m, в то время как m не является адресом
Такой язык не требует ни больших вычислительных ресурсов, чем Си, ни каких-либо ещё достижений современности, зато читать его легче. Чтобы его придумать, нужно было просто отнестись к задаче чуть внимательнее, чем к ней отнёсся тот, кто нажимал Шифт+цифры в поисках ещё незадействованных символов.
Не исключено, что я где-нибудь наошибался, потому что я вхожу в число тех людей, у кого с указателями дружба складывается весьма посредственно. Тогда подскажите, пожалуйста.
Поделиться
Поделиться
Когда я написал про окно Бирмана, у меня спросили, как я делаю окно браузера ровно 1024 в ширину. Разумеется, встроенными средствами компьютера
Это сумбурный пост, потому что редактировать его мне некогда
56 комментариев
Роман Добровенский 2009
Во-первых, тебе _не нужно_ знать о том, что int n[10] — это указатель. Это массив. Например, объявление функций
void f(int n[10]);
это не тоже самое, что и
void f(int n*);
Так же массив нельзя вернуть из функции, а указатель можно. То есть
int[10] f(); — запись недопустимая, в отличие от
int* f();
Во-вторых, голых указателей вообще следует избегать, пользуясь auto_ptr с typedef-ами:
typedef std::auto_ptr int_ptr;
int_ptr p = new int;
В-третьих, си-касты типа (char*) — так же оставлены только в целях совместимости, и пользоваться ими не следует (то есть вообще за них надо отрывать руки).
int *p = static_cast(q);
В-четвертых, следует избегать указателей там, где можно использовать ссылку (90% случаев). В том числе это здорово упрощает запись.
В-пятых, «m as char address» резко сокращает гибкость языка, так как такую конструкцию уже не запихнешь, например, в темплейт, а это автоматически снижает ценность языка процентов на 50.
Вообще 99% случаев критики C++ — это следствие того, что люди имеют лишь отдаленное о нем представление на уровне «С++ за 10 дней».
Илья Бирман 2009
Во-первых, у тебя в typedef’е синтаксическая ошибка на символе „ (q);## // никто не знает, что написано в этой строчке и как это работает
Sergey Azarkevich 2009
Понимали бы в 10 раз больше, а пользовались в 10 раз меньше.
В результате был бы популярен другой, более лаконичный, язык. И все бы жаловались уже на него.
Илья Бирман 2009
Я думаю, даже самые старые среды разработки на 80286 компьютерах могли себе позволить вставку слов valueat и addressof по горячим клавишам (например, Шифт+8 и Шифт+7 🙂
Дмитрий Желнин 2009
примерно те же мысли возникают у меня при работе с командами bash в линуксе — там создатели сократили почти все команды до 2-3 символов
вконец добивают слова, завершающие блок: if — fi, case — esac
Ярослав 2009
If—fi сделали просто потому, что поленились сделать нормально.
Сергей, самый лаконичный язык — это Brainfuck 🙂
bes island 2009
Сложны, трудны… Указатели не нужны. В прикладном программировании. Почти никогда.
Илья Бирман 2009
Конечно, но понимать их нужно просто для того, чтобы мозги работали. Даже если на ПХП пишешь.
Юрий Хан 2009
int[10] f(); — запись недопустимая, в отличие от
Если бы в C можно было вернуть массив из функции, согласно правилу «направо—налево», это бы выглядело так:
А указатели не нужны. Рулят контейнеры и итераторы.
Роман Добровенский 2009
Да, если говорить о Си, то это безусловно хлам. Так а зачем вообще о нем говорить? Его сечас используют только старперы, которые не желают учить ничего нового (ну и программисты под специализированное железо, да, тоже, но это отдельная история).
Насчет выучить язык за 10 дней — насколько я помню ты тут недавно рассуждал о профессинализме. То есть в дизайне быть профессионалом необходимо, и если кто-то где-то допустил малейшую ошибку, то его надо расстрелять. Почему ты считаешь, что язык программирования можно выучить за десять дней? Почему такая ассиметричность?
Илья Бирман 2009
Ты с лёгкостью меняешь местами понятия «выучить» и «получить отделённое представление».
Денис 2009
Мне кажется, что этот язык называется сейчас Дельфи. В нём указатели и работа с ними — сказка.
Если бы вместо звёздочки и амперсанда использовались конструкции addressof () и valueat (), для объявления
типов был бы модификатор address, а для кастинга использовался бы оператор as указатели бы понимало в
10 раз больше человек. Назовём такой язык Ди (хоть такой уже и есть).
Владимир 2009
«авторы языка Си — козлы, которые позаботились об экономии числа нажатий при печатании, >> а о том, удобство чтения кода куда важнее, чем удобство его написания, они не подумали»
Иван Коростелёв 2009
Объявление «int n[10];» в первых версиях C для простоты означали выделение _11_ машинных слов. 10 безымянных, которые содержат цыелые и 11-го, которое содержит адрес первого элемента массива и имеет имя n. Указатели тогда объявлялись так: «int[] n» (массив без элементов). Так было проще реализовать массивы (достаточно было уметь работать с указателями, а массивы получались, как небольшой хак). Отсюда и пошло то, что массив может быть неявно преобразован к указателю на первый элемент.
Затем такая семантика массивов привела к проблемам — сложно реализовать структуры содержащие массивы, в которых всегда неявно инициализируется одно поле (адресом начала массива в той же структуре), к тому же так их размер получался на одно машинное слово больше, чем кажется. Поэтому массивы были введены в язык по нормальному и понятия массива и указателя были разъединены.
Длина идентификаторов в символьной таблице первых компиляторов C была то ли 7, то ли 8 символов, что накладывало ограничения на имена функций.
А авторы языка Си были не козлы, а нормальные программисты, которые понимали, что без обратной совместимости ни один реально использующийся язык обойтись не может: если нельзя перекомпилировать программу написанную месяц назад, любой нормальный человек поищет более толерантный к своим пользователям язык. Это не значит, что сейчас (или даже всего через несколько лет после создания языка) авторам нравятся его странности. Просто так сложилось и исправить это уже нельзя.
P.S. «n == n[0]» это бред, который помимо отсутствия любого вменяемого физического смысла может так же приводить к идиотским ошибкам. (Что если я забуду указать один из индексов в цикле?)
Иван Коростелёв 2009
Мы не ожидаем, что человек изучающий высшую математику сразу (или даже всего за несколько дней) научится правильно записывать и читать частные производные высших порядков (не говоря уж о том, что начать их понимать). Почему-то в математике, не используют слова для записи формул, а используют сложные не использующиеся в обычном письме значки. Любая нотация, пока к ней не привыкнешь является сложной и неопонятной. То же самое касается указателей. Укзатели в C одна из основных и наиболее часто использующихся концепций, поэтому для её записи надо использовать достаточно выразительную _и краткую_ нотацию.
P.S. http://nsl.com/ — это не про Си но в тему. Языки программирования с очень сложной нотацией, которые тем не менее реально используются во многих областях. Многие программы занимающие на обычных языках целую страницу на них можно записать в строчку. И многим это нравится. Ссылка на тему: http://dr-klm.livejournal.com/42312.html
Илья Бирман 2009
В качестве краткой записи значения по адресу можно использовать знак ##@##, который означает «at». Адрес можно сократить до ##addr## (скобки сами по себе часто читают как «of»). Тогда:
##unsigned int m = 2131;##
##@ ((addr (m) as char addr) + 1) = ’A’##
И кратко, и смысл остаётся.
Степан Столяров 2009
Создатели Си справились с дизайнерской задачей создания лаконичного языка низкого уровня, в условиях, когда персональных компьютеров еще не было, и даже дисплеи с электронно-лучевой трубкой были редкостью, зато были терминалы. Опечатки в этом случае стоили довольно дорого, отсюда и все эти нечеловеческие сокращения.
Создатели ПХП, кстати, еще хуже себе представляли, что такое указатели (сравните работу оператора «=» для объектов в 4 и 5 версии), так что не комплексуйте, Илья! 🙂
Илья Бирман 2009
На мой взгляд, опечатка в названии ##strcspn ()## куда более вероятна, чем, допустим, в названии ##stringWithFormat:## или ##mysql_connect_db ()##.
Степан Столяров 2009
unsigned int m = 2131;
*((char *) &m + 1) = ’A’;
Результат тут будет зависеть от того, на каком компьютере вы его запускаете. Little-endian, big-endian, каков размер int в байтах, все дела. На то он и низкоуровневый язык.
На интеле вроде получается m = 16723, я на калькуляторе посчитал.
Илья Бирман 2009
Я исхожу из того, что чем левее бит, тем он старше (даже если если он оказывается в другом байте). Вроде бы это называется big-endian? Размер int в байтах я взял за 2 (от балды 🙂 А чтобы16723 получилось вам нужно 65 (’A’) записать в байт, который умножается на 256, т. е. во второй справа, и при этом его адрес должен быть на один байт правее начала m. стало быть, в ваших расчётах int имеет длину 3 байта. Разве так бывает? Или я где-то ошибся?
Антон Вернигор 2009
Мне кажется, что в той области, для которой создавался язык C (системное программирование, ЯП «среднего уровня»), такой синтаксис вполне допустим. Он краток и понятен тем, кто этим занимается.
А для прикладного программирования просто стоило бы использовать другой язык. Тот же паскаль выглядит в этом плане более привлекательно, хоть тоже и не лишен недостатков. Но, конечно, это не должен быть паскаль, а просто другой язык, с синтаксисом, более понятным человеку, чем компилятору.
Степан Столяров 2009
А, да, еще результат зависит от того, какая у вас кодировка — ASCII или EBCDIC. То есть, чему равно ’A’.
Илья Бирман 2009
Я исхожу из того, что ##’A’## — это 65 (константа такая, типа 🙂
Роман Добровенский 2009
Илья, я не в коем случае не путаю «выучить» и «получить представление». Я просто полагаю, что если человек берется что-то критиковать, то он не просто «имеет представление», а хотя бы знает о совеременном положении вещей в критикуемой области.
Твоя критика — это то же самое что прочитать кригу «HTML for Dummies», а потом ругать HTML за то, что оформление не отделено от содержания, а CSS имеет лишь примитивнейшие свойства типа color и font-size. CSS есть за что критиковать, но мы с тобой понимаем, что недостатки CSS кроются совершенно в другом, и они не являются следствием тупорылости W3C — на то есть другие причины, которые ненасильственными мерами просто не поборешь. Ты же рассуждаешь о C++ именно на уровне «представление за 10 дней».
И заметь, что о современном веб-программирование со всеми AJAX-ами тоже за 10 дней нормального представления тоже не получишь. Это не значит, что JavaScript говно.
Степан Столяров 2009
В 40 килобайт памяти такие длинные имена функции не влезают. 1972-й год на дворе.
#define stringWithFormat strcspn
Илья Бирман 2009
А с дифайном уже влезают. (Я плохо понимаю, как работает компилятор, но если такой простой способ научить его нормальным словам, то почему он не использовался с первого дня?)
Иван Коростелёв 2009
Потому что препроцессор Си появился намного позже компилятора.
Степан Столяров 2009
2131 = 0x0853;
’A’ = 65 = 0x41;
Предположим, что int действительно два байта В зависимости от архитектуры исходное значение 2131 будет идти в памяти как байты 08 53 (у байта 08 адрес младше), либо как 53 08 (у байта 53 адрес младше). При этом адрес m всего числа совпадает с адресом того байта, который «левее» :). Соответственно, меняя байт m + 1 на 0x41, мы получим либо 0x0841 = 2113, либо 0x4153 = 16723. Я могу ошибаться, но мне помнится, что в интеле у байт, представляющих старшие разряды числа, адрес старше. То есть получим мы 16723.
Кстати, очень интересный вопрос, почему интеловцы решили записывать числа в память «задом наперед». И ведь они правы.
Илья Бирман 2009
Да, я считал в прямом порядке 🙂
Степан Столяров 2009
’A’ в ASCII равно 65. ’A’ в EBCDIC = 193, если верить википедии. Непросто жилось древним программистам. 🙂
Антон 2009
С# (Java, VisualBasic, ObjectiveC. что подвернётся). Или, если уж припёрло C++ — то все указатели в контейнеры, чтобы «*» наружу и не торчала.
А С/C++ остаётся для тех задач, когда надо «очень быстро, на четвереньках и задом наперёд» (из старого анекдота). Для встраиваемых систем, низкоуровневых библиотек, слабого железа, и некторых идиотских мобилок, на которых нет ни Java, ни ObjectiveC))) Т. е. оказывается в той сравнительно узкой нише, где раньше был ассемблер.
З.Ы. а упомянутый «strcspn» — это уже не просто С, это POSIX (http://www.space.unibe.ch/comp_doc/c_manual/C/FUNCTIONS/funcref.htm). Уже успели даже стандарт принять (ISO/IEC 9945).
Anatoliy Knyazev 2009
char* s; же! Как можно писать иначе? 🙂
Илья Бирман 2009
Кстати, да, вроде такая запись имеет больше смысла. Если читать * как value at, то char value at (s) явно предпочтительнее, чем char (value at s). Спасибо 🙂
Степан Столяров 2009
Ну чтобы уже закрыть тему про злоключения числа 2131, расскажу про логику интеловских инженеров.
Арабы пишут справа налево, европейцы пишут слева направо. Числа при этом и те, и другие записывают одинаково. При заимствовании арабских цифр эта особенность не была учтена. Мы начинаем запись числа с его старшего разряда, арабы — с младшего. С первого взгляда кажется логичным расположить нумеровать байты памяти слева направо («нулевой» байт слева, «первый» правее, и т. д.) и расположить число 2131 = 0x0853 в памяти как 08 53. Так же наглядно и естественно, правда?
А теперь посмотрим на число в двоичной записи. Младший бит, указывающий множитель при слагаемом двойки в степени 0, располагается справа. Множитель двойки в степени 1 находится слева от него. И так далее. Выходит, биты нашего числа в выбраной нами записи на распечатке будут выглядеть перемешанными и идти в порядке:
7 6 5 4 3 2 1 0 15 14 13 12 11 10 9 8
Чтобы этого избежать, исправим ошибку, допущенную при заимствовании арабских цифр и начнем все записывать справа налево. Нулевой байт, как и нулевой бит любого числа будет находиться справа. Возрастающие адреса будут соответствовать возрастающим степеням двухсотпятидесятишестерки в двухсотпятидесятишестеричной записи числа байтами. Все становится на свои места, если только на распечатках памяти развернуть ось адресов в обратную сторону.
И да, если мы называем старшие адреса памяти «верхними», то, вероятно, при распечатке больших диапазонов памяти старшие адреса должны быть вверху, что мы и видим в интеловской документации. Вот блок памяти 64 КБ строками по 256 байт, заполненный двухбайтовыми числами-указателями на самих себя:
Старшие адреса
FFFF . FF00
FEFF . FE00
.
00FF . 0000
Младшие адреса
А вот диаграммы, изображающие формат сетевых протоколов и файлов, устроены так, что их читают справа налево, сверху вниз. Это потому что по сети и из файла байты приходят по порядку, а не все сразу, и писать код заполнения шаблона заголовка пакета удобнее по картинке, в которой порядок байтов соответствует привычному способу чтения.
Степан Столяров 2009
Anatoliy Knyazev, прочитайте этот код и скажите, сколько здесь переменных типа «указатель на char»:
Правильный ответ: один.
Илья Бирман 2009
Чёрт, что за долбанутый язык. 🙂
выдеру из комментариев:
int n[10];
n[5] = 77;
@ (addr (n) + 5) = 77;
не совсем понял зачем заменять «*» на «@». Ну то есть совсем не понял. Если вам очень хочется, это может сделать препроцессор.
Илья Бирман 2009
Так ##@## понятнее, чем ##*##, потому, что ##@## — это at. At address of n + 5 assign 77 — это почти предложение на английском языке, в котором делается как раз то, что написано. А звёздочка вообще ничего не значит.
Странно, как раз с указателями мне все более-менее понятно и логично, возможно тк изучал язык в юном возрасте)) Мне кажется тут предъявлять претензии не оч верно, все-таки Си низкоуровневый язык, вроде ассемблера.
И мне кстати лаконичность нравится, <> в 100 раз лучше begin..end.
Есть куда более ужасные вещи, например необходимость объявлять функцию *до* ее использования, или дурацкая система инклюдов (+ необходимость писать каждый раз по 2 файла, заголовочный и с кодом, + отсутствие директивы вроде include_once). Ну и еще необходимость ручного управления памятью, куча 2-смысленностей, из-за которых программировать на этом языке можно (имхо) только любителям острых извращений.
Кстати, язык D, очень даже ничего, если бы не сборщик мусора, и огромные бинарники, который делает компилятор (ну и еще пару моментов, вроде плохо реализованных строк :), был бы вообще идеальным языком ))
Anatoliy Knyazev 2009
Степан Столяров, всё верно, но можно же:
Согласён с Ильёй в том, что сейчас, в век широкоэкранных мониторов и code autocompletion, можно не экономить символы.
Роман Добровенский 2009
То что @ читается как «at» знает не так много людей. Попытка приблизить язык программирования к естесственному языку — самая дурацкая затея.
Во-первых, естесственный язык намного сложнее любого языка программирования (хотя бы запомнить где когда какой предлог использовать — уже большая проблема), так же естесственный язык часто допускает многие варианты записи одного и того же, что для языка программирования не допустимо.
Во-вторых обычный текст гораздо хуже читается, когда это программа, так как он не структурирован. С ходу найти в куске текста конкретное предложение, где говорится о чем-либо весьма сложно. Уродливый reinterpret_cast(value) сразу бросается в глаза.
В-третьих, человеческие языки не обладают достаточной краткостью для обозначения строгих формальных понятий (вспомним уродливое «тогда и только тогда»). Как ты в двух словах написашь название операции, которая проверяет корректность приведения типа во время выполнения так, чтобы ее однозначно можно было отделить от приведения типа, который выполняет проверку во время компиляции и от приведения типа, который отменяет свойство const? Кажется, const_cast, reinterpret_cast, static_cast и dynamic_cast — лучшие варианты на это.
Можно на самом деле продолжать бесконечно.
Вообще я не понимаю почему такое внимание уделяется указателям и приведениям типов. Это редкие и опасные операции, которых следует избегать, о чем уродливость конструкции reinterpret_cast(p) нам еще раз напоминает. Если кто-то работает с указателями и приведениями типов — пускай витамины чтоли пьет, для развития мозга.
Илья Бирман 2009
@ никак иначе, чем «эт», не читается, об этом знают все, кто говорит по-английски. Это настолько же очевидно, как то, что & читается «энд».
«Естесственный язык часто допускает многие варианты записи одного и того же, что для языка программирования не допустимо» — чушь какая-то, я могу сотней способов одно и то же написать на куче ЯП.
Ты передёргиваешь вообще, я не предлагаю приближать язык к человеческому, я предлагаю давать конструкциям вменяемые и осмысленные названия или использовать понятные символы. Если бы оператор ##?:## использовал вместо вопросительного знака обратный слеш, я бы тоже предложил заменить на вопросительный знак. Звёздочка могла бы работать, допустим, как оператор goto (типа, сноска), а как оператор «значение по адресу» она работает херово, а собака — хорошо, вот и всё.
Писать на Си без указателей невозможно, это не Паскаль тебе.
Kalan 2009
«Да. Этот чёртов язык на редкость логичен. Только мозги об его логику сломать можно.» © один из моих знакомых
Павел Малинников 2009
##x «„==“» x[0] «„==“» valueat (addressof (x) + 0)##
— я надеюсь, это псевдокод, потому что в реальном коде такая конструкция имеет мало шансов быть оцененной однозначно, тут несколько операторов внутри одной точки следования.
Указатели сложны для многих людей, многие работают программистами, не понимая их, предпочитая Визуал Бейсик и прочее.
Человеку стремящемуся, все равно придется принять саму идею, заложенную в указателях. Если он не в состоянии понять идею, то слова valueat и addressof ему всё равно не помогут. А когда идея будет понята, он первый попросит заменить valueat на «*», а addressof на «&», потому что краткосрочный период обучения — это одно, а всю жизнь потом писать addressof — это другое.
Насчёт прикладного программирования трудно представить, чтобы человек не понимал значение указателей. По крайней мере, если он понимает, что такое полиморфизм и для чего он нужен.
Антон Вернигор 2009
Егор, да, мне тоже C кажется далеким от языков высокого уровня, поэтому и в голову не приходило предъявлять к нему такие же требования.
А его лаконичность — это красиво. Такие короткие и емкие записи получаются, вроде while (*s++ = *t++); — сказка просто 🙂
Павел Малинников 2009
. дурацкая система инклюдов . отсутствие директивы вроде include_once .
но есть ведь #pragma once ? Не стандарт, но это другое дело.
#ifndef __MYFILE_H__
#define __MYFILE_H__
Александр 2009
Например, звёздочка рядом с именем переменной в выражении означает обратное тому, что она означает в описании переменной
Для понимания достаточно знать, что типы переменных в Си именуются не так, как в более новых языках, т. е. не слева-направо. Они называются так, как будут использоваться
int *x; // *x has type int
int n[10]; // n[i] has type int
И всё. Достаточно прочесть учебник. Это другой способ, а не кривой, хотя, конечно, каждый знает единственно верный способ.
int n[10]; // здесь n хранит адрес, но нет ни звёздочки, ни амперсанда
n не хранит ничего (т. е. нигде не написано, что там лежит указатель), это массив, который неявно преобразуется в указатель на нулевой элемент (что прекрасно имплементируется и при Вашем способе хранения).
Зато это считается нормальным и однозначным.
По поводу козлов авторов есть хорошая поговорка «Был бы я такой умный вчера, как моя жена сейчас». Задним числом все умны.
Алик Кириллович 2009
Мне правильно показалось, что название этой статьи «Почему указатели трудны и что с этим делать» — намек на статью Джоэла Спольски: «Why are the Microsoft Office file formats so complicated? (And some workarounds)»: http://www.joelonsoftware.com/items/2008/02/19.html?
Илья Бирман 2009
Иван Коростелёв 2009
Сергей К. 2009
Илья, ты пытаешься выдумать новый язык, а он давно выдуман. Это java. Или C#. Или python. Любой из них выглядит проще и понятнее. В яве можно написать «Just a string».length(), и получится длина строки (надеюсь, эти кавычки не станут ёлочками). Питон вообще задумывался как язык, на котором будет трудно писать неразборчиво (это не главное его свойство, но об этом автор тоже подумал). Есть ещё такой язык модный Vala, который по синтаксу похож на яву и сишарп, но является просто надстройкой над си. То есть он транслирует более-менее читаемый код в код на языке си.
Я давно для себя решил, что в нормальном объектном языке для обращения к методам и атрибутам используется точка. Если используется что-то другое, значит это плохой объектный язык.
Игорь 2009
Илья, а тебе не кажется, что фраза «Указатели сложны не потому, что это объективно что-то сложное, а потому, что авторы языка Си — козлы» это какой-то разрыв мозга? На кой сравнивать трудность понимания указателей и язык? Указатели в С есть потому что они есть на уровне архитектуры большей части ныне существующих процессоров. И их понимание довольно слабо связано с языком. Я в этом скорее встану на позицию Джоэла: есть люди, которые понимают указатели, и есть люди, которые не понимают указатели. Если ты их понимаешь, то синтаксис Си(++) будет тебя смущать недолго — достаточно взять и написать что-нибудь. Если ты их не понимаешь, то тут и изменения синтаксиса слабо помогут. А если ты хотел показать, что на С(++) с восхитительной легкостью можно создавать совершенно нечитаемый код, то пример выбран слабоватый: если уж собирался ругать указатели, то лучше приводить код с указателями функций, ну или что-нибудь хрестоматийное с программой в одну строку записанной и т. п., благо примеры нечитабельности в каждой второй книжке по С приводятся.
2 Павел Малинников:
Я знаю, про использвоание дефайнов для этого, их сейчас некоторые иды даже сами подставляют, но все равно это уродство. А между pragma once и include_once есть разница — одна применяется в включаемом файле, другая — в том, который включает.
Павел Малинников 2009
ну, если «уродство» — это свойство явления, вызывающее отвращение, то это субъективное высказывание, т. к. у вас вызывает, а у кого-то не вызывает 🙂
между pragma once и include_once есть разница
а как эта разница мешает вам обеспечить одноразовое включение .заголовочного файла?
Павел Малинников 2009
ну что же, попробуем повысить читаемость кода:
было:
pCar->goForward(); //машина — вперёд!
стало:
valueat (pCar).goForward(); // что-то по адресу «машина». а что там? оно умеет вперёд.
Илья Бирман 2009
У вас не получилось 🙂
Павел Малинников 2009
да, не получилось при помощи такого подхода. Может быть, не такой уж Страуструп и козёл? 🙂
Илья Бирман 2009
Про Страуструпа вообще никто ничего не говорил. Си-плюс-плюс и Си — это разные языки.
Павел Малинников 2009
Илья, из вашего ответа на 1-й комментарий я понял, что примеры из C++ не против правил: вы писали «(мы тут говорим про Си-плюс-плюс)».
Ну ладно, хоть Страуструпа отмазали 🙂 Кериниган и Ричи по-прежнему опасносте! 🙂
Попробуем применить методику на трудночитаемом фрагменте чистого Си:
стало:
void address address (address func) (int ref, char address address (address) (char address, char address address));
Илья Бирман 2009
Я имел в виду «не про Си-плюс-плюс», это опечатка (что следует из остального текста моего ответа).
Стало ли лучше я не знаю, потому, что я не понимаю что написано. Расскажите?
Павел Малинников 2009
а, ну func — это указатель на функцию с двумя аргументами:
- ссылка на int
- указатель на функцию, которая тоже имеет два параметра:
- указатель на char
- указатель на указатель на char
эта функция возвращает указатель на указатель на char
а func возвращает указатель на указатель на void
Илья Бирман 2009
Приведите, пожалуйста, пример такой функции.
Павел Малинников 2009
а как код обрамить? code?
Илья Бирман 2009
Павел Малинников 2009Она могла бы использоваться примерно так:
func pFunc = (func) GetProcAddress (hSomeDll, «funcFromSomeLibrary»);
int a = 5;
void **p;p = pFunc (a, callback);%%
понятно, что это учебный пример, но у меня есть реальный проект, где я писал объектные обёртки для одной географической библиотеки. Если интересно, могу показать, как в «прикладном ПО» выглядит, только там Си++ и кода надо приводить страницы на полторы-две, иначе контекста не уловить.
Илья Бирман 2009
А в конце строчки про тайпдеф не пропущено слово ##func## случайно? А то иначе я вообще не могу связать это всё 🙂
Павел Малинников 2009
неа. func теперь и есть новый тип. Переменные этого типа хранят указатель на функцию, которая . см. #43
Илья Бирман 2009
А, всё, распарсил про func. А что значит звезда в скобках?
Это всё жесть, конечно.
Павел Малинников 2009
звезда в скобках — это запись того, что аргументом будет ещё один указатель на функцию, но специального имени для неё не нужно придумывать. Будет называться callback или ещё как-нибудь, не важно.
Жесть, но на первый взгляд, а вообще, в работе встречается. Для меня лично жесть — это наследование шаблонов.
Павел Малинников 2009
для того, чтобы придать немножко больше смысла примеру:
%%typedef void ** (*RENDER_MAP_FUNC) (int, char (*)(char *, char ));
char ** callback (char *pc, char **ppc)
/* pCurRenderFunc могла бы вызывать её
по ходу длительной операции, типа «n% complete. » */
.
>RENDER_MAP_FUNC pCurRenderFunc =
(RENDER_MAP_FUNC) GetProcAddress (hSomeDll, «funcFromSomeLibrary»);
int zoomLevel = 5;
void **pResult; /* массив массивов с результатами */pResult = pCurRenderFunc (zoomLevel, callback);%%
Василий Журавлёв 2009
У «Ай-Би-Эм» есть такой язык программирования, РПГ, выросший из языка создания отчётов и имеющий нечеловеколюбивую природу: команды языка нужно начинать писать со строго определённых отступов (не как в Питоне, а гораздо неудобнее: звёздочка в восьмой колонке обозначает начало комментария, например), все переменные объявляются в самом начале файла, а процедуры оформлены зачастую безусловными переходами.
На самом деле, на РПГ можно писать и в более удобном для программиста стиле (в «Ай-Би-Эм» тоже нашёлся свой Илья Бирман), но большинство профессиональных программистов на РПГ начинают с поддержки существующего кода, который оформлен в старом стиле. Переписать весь код по-новому — задача неподъёмная, поэтому проще научить програмистов понимать неудобную запись.Мне нравится следующая аналогия матанализа и указателей C. Оператор взятия адреса «&» это как взятие дифференциала от функции, оператор разыменовывания указателя «*» это как взятие интеграла : ). Чтобы получить нормальную функцию, а не дифференциал какой-нибудь бесконечно малый, надо дифференциал n-го порядка проинтегрировать n раз. Так же и с указателями. Например, у нас есть Type** type. Если написать *type то получим «дифференциал», а он нам не нужен и надо четыре раза «проинтегрировать» **type.f(). =)
Про синтаксис языков давно есть нормальная идея отделить содержимое от представления. Надо исходники хранить в каком-нибудь подходящем формате, хоть, XML, где тегами обрамлять семантические конструкции, а программист будет в IDE выбирать какой ему нравится стиль оформления, фигурные скобки или бегин-энд, а может вообще в прямоугольничке все это зафигачить, картинки добавить, анимацию можно включить : )
P. S. В предыдущем сообщении я набрал четыре звездочки подряд, очевидно тут полужирное начертание делается с помощью обрамления в двойные звездочки. Может сделать тег ?
Дмитрий Акимов 2009
Да, указатели действительно сложны для первоначального освоения. Когда я в детстве изучал Паскаль, я долго не мог «въехать» в концепцию указателей, так же, как и объектов, кстати говоря.
Хотя идея с предложенной вами нотацией неплоха, мне кажется, что в указателях трудна не столько нотация, сколько сами указатели.
Трудность состоит в том, что в языке есть переменные (они же объекты), которые хранят некоторые значения, а есть указатели, которые, хоть и выглядят, как остальные переменные, но кардинально отличаются от них тем, что сами как будто бы ничего не хранят, а лишь неким образом ссылаются на другие переменные. Таким образом, в языке создается два типа разнородных сущностей, внешне выглядящих одинаково.
А основная сложность состоит как раз в обращении к значению по указателю. Чтобы понять подобную конструкцию, нужно произвести более сложные умственные действия, чем для того, чтобы понять просто присваивание значения переменной, например.
А стоит лишь немного усложнить использование — например, использовать указатель на указатель, и многие даже опытные программисты могут начать испытывать трудности с пониманием таких конструкций. Не сомневаюсь, что указатель на указатель на указатель вызовет трудности у всех без исключения.
Параллель с дифференцированием и интегрированием, кстати, неплохо это иллюстрирует. Думаю, не такая это тривиальная задача — представить, как связаны между собой некоторая функция и ее вторая производная, не говоря уже о третьей.
Я думаю, оптимальным решением было бы вообще не использовать указатели в таком виде, в каком они используются в C и «„C++“».
Указатель в C и «„C++“» — это лишь частный ограниченный случай ссылки, заставляющий размещать объекты только в оперативной памяти, занимая непрерывную ее область, и давать им строго определенную структуру. А ведь объект, в принципе, может размещаться и в памяти другого процесса или компьютера, на диске или в сети. К тому же, объекта вообще может не существовать физически — его значение может вычисляться в результате работы какого-то алгоритма, он может быть разбит на части в оперативной памяти, он может занимать несколько битов внутри различных байт, не находящихся рядом друг с другом, и так далее.
Интересно, что понятие ссылки является фундаментальным в программировании, и реализация ссылок в языке программирования весьма существенно влияет на стиль, характер и возможности языка. Например, некоторые авторы основной характеристикой языка C называют именно указатели.
Указатель — про указатель в языке СИ
Забегая вперёд скажу, указатель это очень
странный предметпростая вещь, вообще в языке СИ нет ничего проще чем указатель, и в тоже время это пожалуй самый мощный инструмент, с помощью которого можно творить великие дела . Однако многие падаваны не понимают что это такое, поэтому я попробую внести свою лепту.Для лучшего понимания, вначале мы разберёмся с «обычными» переменными (впрочем указатель, это тоже обычная переменная, но пока мы условно разделим эти понятия).
Переменная
Итак, у нас есть переменные 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);
Это всё, всем спасибо
- 8 ноября 2020, 08:27
- stD
- 23497
—>
Поддержать автора
Задать вопрос по статье
Известит Вас о новых публикациях