Перейти к содержимому

Что означает две звездочки в c

  • автор:

Указатель на указатель + динамическое выделение памяти (часть 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. Мы их опубликуем!

Что означает две звездочки в 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 .

Кратко об указателях в Си: присваивание, разыменование и перемещение по массивам

Приветствую вас, дорогие читатели. В данной статье кратко описаны основные сведения об указателях в языке Си. Кроме основных операций с указателями (объявление, взятие адреса, разыменование) рассмотрены вопросы безопасности типов при работе с ними. К сожалению, в данной статье вы не найдёте информацию по операциям сравнений указателей. Однако, статья будет полезна новичкам, а также тем, кто работает с массивами. Все примеры в данной статье компилировались компилятором gcc (восьмой версии).

Введение

Указатель — переменная, которая хранит адрес сущностей (т.е. других переменных любого типа, будь то структура, или массив), и над которой возможно выполнять операцию разыменования (dereferencing). Адрес обычно выражен целым положительным числом. Диапазон адресов зависит от архитектуры компьютера. Указателю надо указать тип переменной, адрес которой он хранит, или же использовать ключевое слово void, для обозначения указателя, хранящего адрес чего-угодно (т.е. разрешён любой тип). Указатели объявляются как и обычные переменные, с той разницей, что имя типа переменной указателя имеет префикс, состоящий как минимум из одной звёздочки (*). Например:

int a = 12; /* usual variable */ int * ptr = &a; /* ptr-variable which contains address of variable a */ int **pptr = &ptr; /* ptr-variable which contains address of variable ptr */ int aval = **pptr; /* get value by adress which is contained in pptr. */ int aval2 = *ptr; /* get value of a by address (value of ptr) */

Количество звёздочек лишь указывает на длину цепочек хранимых адресов. Поскольку указатель также является переменной и имеет адрес, то его адрес также можно хранить в другом указателе. В выше приведённом примере адрес переменной a сохраняется в переменной-указателе ptr. Адрес же самой переменной ptr сохраняется в другом указателе pptr. Чтобы получить адрес переменной, перед её именем надо поставить знак амперсанда (&). Наконец, чтобы выполнить обратную операцию, т.е. получить значение (содержимое) по адресу, хранимому в указателе, имя указателя предваряется звёздочкой, почти как при объявлении. Почти, потому что одной звёздочки достаточно чтобы «распаковать» указатель. Поскольку pptr указывает по адресу на значение, хранимое в ptr, то необходимо два раза применить операцию разыменования.

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

int b = 0xff; void *pb = &b; void **ppb = &pb; int bval1 = *((int *) pb); int bval2 = *((int *) *ppb);

В данном примере адреса хранятся в указателе типа void. Перед получением значения по адресу, хранимым в pb, необходимо привести указатель pb к типу int*. Затем, воспользоваться стандартной операцией разыменования. Что касается указателя ppb, то он разыменовывается два раза. Первый раз до приведения к типу, для получения содержимого переменной pb, на которую он указывает. Второй раз — после приведения к типу int*.

Изменения значения переменной через указатель.

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

char a = 'x'; char *pa = &a; /* save address of a into pa */ *pa = 'y'; /* change content of variable a */ printf("%c\n", a); /* prints: y */

Как было сказано выше, указатели хранят адреса. Естественно, что адреса могут указывать не только на ячейки данных переменных в вашей программе, но и на другие вещи: адрес стека процедур, адрес начала сегмента кода, адрес какой-то процедуры ядра ОС, адрес в куче и т. д. Логично, что не все адреса можно использовать напрямую в программе, поскольку некоторые из них указывают на те участки памяти, которые нельзя изменять (доступ для чтения), или которые нельзя затирать. В случае, при обращении к участку, доступному только для чтения, при попытке изменить значение получим ошибку Segmentation Fault (SF).

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

И ещё, указатели могут указывать на один и тот же объект. Например:

int a = 123; int *p1 = &a; //Теперь p2 хранит тот же адрес, что и p1. int *p2 = &a; *p1 -= 3; // a = 123 - 3. printf("*p2 = %d\n", *p2); //Выведет 120

Этот простой пример показывает, что через адреса можно менять содержимое простых переменных, а также остальных указателей, ссылающихся на тоже самое. Таким образом, указатель p2 как бы является псевдонимом (alias) для p1.

Передача параметров через указатели.

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

int swap(int *a, int *b)

Здесь переменные а и b меняются своими значениями друг с другом (при условии, что параметры содержат не нулевой адрес). Отметим ещё раз, что мы можем изменить содержимое, указываемое по параметру-указателю методов. И, конечно, мы можем стереть данный адрес, присвоив параметру новое значение.

Проверка типов и массивы

Как было сказано, указатели хранят адреса переменных. Несмотря на указание типа для переменной указателя, это не мешает присвоить ему адрес переменной другого типа, если вы компилируете БЕЗ флагов. Например, следующий код не скомпилируется, если вы включили флаги -Werror -Wall .

#include #include int main(int argc, char **argv)

Конечно, компилятор gcc и без -Wall заметит недопустимую операцию в 7 строке кода. Флаг -Wall покажет все предупреждения компилятора. Главный флаг -Werror не позволит компилировать код, если есть предупреждения.

Что же касается массивов, то для массива не нужно предварять имя переменной амперсандом, поскольку компилятор автоматически при присваивании адреса массива присвоит адрес первого его элемента в указатель. Для многомерных массивов потребуются указатели на массивы, а не массивы указателей. Первые имеют форму объявления вида int (*arr)[] , а вторые вида int *arr[] . В квадратных скобках обязательно нужно указать размер массива. Для трёхмерных массивов потребуется уже две пары скобок, например int (*arr)[2][2] . Для четырёхмерных — три и так далее.

// В ПУСТОМ теле метода main. int A[2] = ; // A -> (int *) ptr to A[0] element, &A -> (int (*)[]) -> ptr to whole Array. int *ptr = A; printf("ptr -> A[1] = %d\n", *(ptr + 1)); // A[1] => 20. //Illegal usage of A. // int a_2 = ++A; //expected lvalue. //But with ptr you can do this. int b_2 = *++ptr; //Now ptr contains address of A[1]. (b_2 = A[1]); int (*ptr2)[2] = &A; //ptr to array, not to literal element. //*ptr2 => get array. //**ptr2 => get first element of array. //*ptr2 + 1 => get address of second element of array. printf("ptr2 -> A[1] = %d\n", *( *ptr2 + 1) ); int M[2][2] = < , >; // (*mp)[k] => (*mp)[k] => mp[0][k]. int (*mp)[2] = M; //again you must not add '&' to variable M. printf("M[0][0] = %d\n", **mp);//get array and extract it first element printf("M[1][0] = %d\n", **(mp + 1));//move to the address of second element printf("M[1][1] = %d\n", *( *(mp + 1) + 1));

В выше приведённом коде даны примеры для работы с массивами (одномерными и двумерными). В квадратных скобках указывается размер последнего измерения. Важно помнить, что первое разыменование приводит вас ко всему массиву (т. е. к типу int * ). А второе разыменование распаковывает элемент данного массива. В случае одномерного массива, у нас всего одна ячейка, и указатель ссылается на неё. В случае двумерного массива, у нас две ячейки — массивы, а указатель ссылается на первую. Для перемещения на второй массив, достаточно прибавить единицу к адресу, хранимому в переменной mp, например, так mp + 1 . Чтобы получить первый элемент второго массива, надо два раза распаковать указатель с соответствующим адресом массива, т.е. **(mp + 1) .

Постоянные (const) и указатели.

Напомним, чтобы сделать переменную с постоянным, фиксированным значением, надо добавить ключевое слово const перед её именем (до имени типа или после). Например:

const int i1 = 10; int const i2 = 222; // Warning: variable e3 is unitialized. With -Werror it won't be compiled. // (Внимание: переменной e3 не присвоено значение. С флагом gcc -Werror // данный код не скомпилируется). // const int e3;

Для объявления указателя на постоянное значение, ключевое слово const должно быть ПЕРЕД звёздочкой.

int A[2] = ; const int *a0 = A; printf("content of a0 = %d\n", *a0); //*a0 *= 10; //error: cannot change constant value. a0 = (A + 1); // A[1] printf("content of a0 = %d\n", *a0); //prints: A[1]

В примере выше была создана переменная-указатель, ссылающееся на постоянное значение. Слово const перед звёздочкой указывает, что нельзя менять содержимое напрямую (путём разыменования, обращения к ячейке). Но сама переменная указатель постоянной не является. А значит, ей можно присвоить новый адрес. Например, адрес следующей ячейки в массиве.

Чтобы запретить менять адрес (значение переменной) указателя, надо добавить слово const ПОСЛЕ звёздочки. Кроме того, можно добавить ключевые слова const перед и после ‘*’ , чтобы сделать переменную фиксированной ещё сильнее, например так:

// Переменная с постоянным адресом и постоянным содержимым. const int *const ptr = A; // constant address with constant content // Переменная с постоянным адресом (содержимое можно менять) int *const ptr2 = A; // constant address only. // Переменная с постоянным содержимым, но с изменяемым адресом (значение справа) const int *ptr3 = A; // constant content only (can change address (rvalue))
  • Программирование
  • Системное программирование
  • C

Указатель в языке Си

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

Указатели широко используются в программировании на языке Си. Указатели часто используются при работе с массивами.

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

Каждая переменная в памяти имеет

  • свой адрес — номер первой ячейки, где она расположена;
  • свое значение.

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

Указатель, как и любая переменная, должен быть объявлен. Общая форма объявления указателя

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

Тип указателя — это тип переменной, адрес которой он содержит.

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *