Что означает две звездочки в c
Перейти к содержимому

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

  • автор:

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

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

Что значит звёздочка после типа указателя?

*pointer — разыменование указателя,
а что значить Звёздочка после типа?
int*
Например:

double* ptd; ptd = (double *)malloc(10 * sizeof(double));

Зачем перед malloc стоит (double*), если это пишется в sizeof(double), Для чего после double стоит *, ведь это символ разыменования, а что мы разыменуем?

  • Вопрос задан более трёх лет назад
  • 8221 просмотр

1 комментарий

Простой 1 комментарий

В первом случае * относится не к double , а к ptd и означает то же самое, что и double *ptd; (а также double*ptd; или double * ptd; ).
Подобная запись может служить источником заблуждений, т. к.
double* ptd, x;
означает то же, что и
double *ptd; double x; ,
а не
double *ptd; double *x; .
(Действия подобного второму объявлению можно достичь при помощи typedef :

typedef double *pdouble; pdouble ptd, x;

Решения вопроса 2

myjcom

что значить Звёздочка после типа?

Для чего после double стоит *, ведь это символ разыменования, а что мы разыменуем?

В данном случае звездочка не является оператором а относится к типу.

Зачем перед malloc стоит (double*)

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

N.B. В современном Си такой необходимости нет.

приведение к типу (double*) сообщает компилятору, что вы хотите.

Ответ написан более трёх лет назад
Нравится 4 2 комментария
Zakhar Delov @Del0v Автор вопроса
Спасибо за ответ и ссылки.

myjcom

CityCat4

Внимание! Изменился адрес почты!

Первая запись обьявляет переменную ptd указателем на double. Компилятор ожидает, что в данной переменной хранится адрес памяти ячейки, который указывает на переменную типа double, хотя сразу после обьявления в ней находится мусор — неопределенное значение.
Вторая запись выделяет память под хранение переменных.
sizeof(double) — получить размер переменной double для текущей платформы
malloc — выделить память под десять переменных размером в double — обычная вещь для массива например.
(double *) — привести тип возвращаемого значения (malloc всегда возвращает void) к типу «указатель на double»

Надо сказать, что данный код можно использовать только в том случае, если сразу за malloc() идет чтение или инициализация массива. Потому что malloc() вернул область памяти, заполненную случайными данными. Для гарантии, что там не будет мусора лучше использовать calloc:
ptd = (double *) calloc(sizeof(double), 10);

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

Указатели

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

Определение указателя

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

тип_данных* название_указателя;

Сначала идет тип данных, на который указывает указатель, и символ звездочки *. Затем имя указателя.

Например, определим указатель на объект типа int:

int* p;

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

#include int main()

Например, в моем случае консоль вывела «0x8» — некоторый адрес в шестнадцатеричном формате (обычно для представления адресов в памяти применяется шестнадцатеричная форма). Но также можно инициализировать указатель некоторым значением:

int* p<>;

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

int* p;

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

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

int* p1<>; int *p2<>;

Также стоит отметить, что размер значения указателя (хранимый адрес) не зависит от типа указателя. Он зависит от конкретной платформы. На 32-разрядных платформах размер адресов равен 4 байтам, а на 64-разрядных — 8 байтам. Например:

#include int main() < int *pint<>; double *pdouble<>; std::cout

В данном случае определены два указателя на разные типы — int и double. Переменные этих типов имеют разные размеры — 4 и 8 байт соответственно. Но размеры значений указателей будут одинаковы. В моем случае на 64-разрядной платформе размер обоих указателей равен 8 байтам.

Получение адреса и оператор &

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

int number ; int *pnumber ; // указатель pnumber хранит адрес переменной number

Выражение &number возвращает адрес переменной number . Поэтому переменная pnumber будет хранить адрес переменной number . Что важно, переменная number имеет тип int, и указатель, который указывает на ее адрес, тоже имеет тип int. То есть должно быть соответствие по типу. Однако также можно использовать ключевое слово auto :

int number ; auto *pnumber ; // указатель pnumber хранит адрес переменной number

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

#include int main() < int number ; int *pnumber ; // указатель pnumber хранит адрес переменной number std::cout

Консольный вывод программы в моем случае:

number addr: 0x1543bffc74

В каждом отдельном случае адрес может отличаться и при разных запусках программы может меняться. К примеру, в моем случае машинный адрес переменной number — 0x1543bffc74 . То есть в памяти компьютера есть адрес 0x1543bffc74, по которому располагается переменная number. Так как переменная x представляет тип int , то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом, переменная типа int последовательно займет ячейки памяти с адресами 0x1543bffc74, 0x1543bffc75, 0x1543bffc76, 0x1543bffc77.

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

И указатель pnumber будет ссылаться на адрес, по которому располагается переменная number, то есть на адрес 0x1543bffc74.

Итак, указатель pnumber хранит адрес переменной number, а где хранится сам указатель pnumber? Чтобы узнать это, мы также можем применить к переменной pnumber операцию &:

#include int main() < int number ; int *pnumber ; // указатель pnumber хранит адрес переменной number std::cout

Консольный вывод программы в моем случае:

number addr: 0xe1f99ff7cc pnumber addr: 0xe1f99ff7c0

Здесь мы видим, что переменная number располагается по адресу 0xe1f99ff7cc , а указатель, который хранит этот адрес, — по адресу 0xe1f99ff7c0 . Из вывода видно, что обе переменные хранятся совсем рядом в памяти

Получение значения по адресу

Но так как указатель хранит адрес, то мы можем по этому адресу получить хранящееся там значение, то есть значение переменной number. Для этого применяется операция * или операция разыменования («indirection operator» / «dereference operator»). Результатом этой операции всегда является объект, на который указывает указатель. Применим данную операцию и получим значение переменной number:

#include int main() < int number ; int *pnumber ; std::cout Address = 0x44305ffd4c Value = 25

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

int n1 ; int *pn1 ; // указатель pn1 хранит адрес переменной n1 int n2 < *pn1>; // n2 получает значение, которое хранится по адресу в pn1 std::cout int x = 10; int *px = &x; *px = 45; std::cout

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

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