Обращаюсь к новичкам, которые только начали изучать указатели: «Если вас заинтересовала эта тема и вы хотите в ней разобраться, что я могу вам сказать — ситуация не из приятных!» ))) Кто бы и как бы усердно и старательно не объяснял вам что к чему, понять указатели на указатели сложно. Сам указатель на указатель содержит в себе адрес, который ссылается на другой адрес, а он, в свою очередь, ссылается на адрес в памяти, где хранятся данные. Вроде бы и можно понять. Но как это применять на практике? Зачем оно надо??? Это понять сложнее. А надо «оно», среди прочего, для возможности работы с массивами указателей, которые указывают на память с данными (строками, например). Каждый элемент этого массива — это указатель, который содержит в себе адрес строки (первого элемента символьного массива):
Наша ситуация усложняется еще и тем, что в данной статье мы постараемся доступно показать, как выделять динамическую память под двумерный массив указателей и как ее освобождать. Ну что, испугались? Тогда начнем разбираться! Если вы еще слабо знаете тему Указатели, прочтите все таки сначала эту статью. Она поможет вам подготовиться к восприятию темы Указатель на указатель.
А в данной статье мы будем рассматривать пример, в котором перед нами ставится следующая задача: у нас есть указатель на указатель char **pp
(он будет содержать адрес массива указателей на строки) и размер этого массива int size
, который изначально равен 0. Нам надо написать функцию, которая будет выделять динамическую память для новых элементов массива указателей и для хранения символов новых строк. Эта функция будет принимать, как параметры, указатель на указатель, размер массива указателей и строку, которую надо будет записать в выделенную под нее память. Чтобы не усложнять задачу, в ней не будет диалога с пользователем. Пять строк мы определим сразу при вызовах функции.
Если у вас есть возможность, пишите исходный код по мере прочтения. Так будет легче его понять. Детальные объяснения увидите под кодом.
#include <iostream>; #include <string.h>; using namespace std; char **AddPtr (char **pp, int size, char *str); //прототип функции int main() { setlocale(LC_ALL, "rus"); int size = 0;//количество указателей на строки char **pp = 0;//указатель на массив указателей, которые содержат адреса строк cout << "~~~~~Добавляем указатели на пять строк и заполняем строки данными~~~~~" << endl; //вызов функции и присваивание возвращаемого значения pp = AddPtr(pp, size, "11111111111111111"); size++; //=1 увеличиваем размер массива указателей pp = AddPtr(pp, size, "22222222222222222"); size++; //2 pp = AddPtr(pp, size, "33333333333333333"); size++; //3 pp = AddPtr(pp, size, "44444444444444444"); size++; //4 pp = AddPtr(pp, size, "55555555555555555"); size++; //5 for(int i = 0; i < size; i++) //показываем все строки на экран cout << pp[i] << endl; //достаточно обратиться к pp[i] - это адрес строки (0-й элемент) cout << endl; for(int i = 0; i < size; i++) //освобождаем память { delete [] pp[i]; // сначала выделенную под строки } 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 < size; i++) { delete [] pp[i]; }
Так освобождаем динамическую память, на которую ссылаются указатели из массива указателей. А далее освобождаем память, выделенную под сам массив указателей — строка 40.
Результат:
~~~~~Добавляем указатели на пять строк и заполняем строки данными~~~~~
11111111111111111
22222222222222222
33333333333333333
44444444444444444
55555555555555555
Условие этой задачи мы выполнили. Надеюсь, вы оценили главное преимущество использования указателей вместо обычных массивов. При входе в программу мы не знаем, сколько строк нам будет необходимо и какой объем памяти они будут занимать. Но мы не объявляли несколько десятков символьных массивов с размером [много памяти] (а вдруг пригодятся, если пользователь будет вводить много длинных строк). Вместо этого у нас получился один динамический массив указателей на строки, память для которых так же выделяется динамически.
Во второй части этой статьи мы добавим в программу еще две функции. Одна будет удалять выбранную нами строку и указатель на нее. Вторая будет вставлять указатель на строку в выбранную нами ячейку массива указателей. Не переживайте. Если вам более менее понятно, что произошло в примере выше, то дальше будет легче всё понять.
Комментарии
Даниил Семченков
Подскажите, пожалуйста, по какому принципу выделяется память для строк ? Точнее, по какому принципу определяется размер строки. на 1 символ char приходится 1 байт, как я понимаю.
Скопировал код в VS2012, запустил с отладчиком, стал отслеживать, по каким адресам хранятся строки 111111…., 2222222….. и т.д. Для первой строки адрес был 0х00be2ce0, для второй — 0х00be2d30. При вычитании из второго адреса первого в hex-системе счисления получаем 50, в десятичной — 80. В каждой строке по 17 символов. Логику не понимаю
DromanX
Для тех у кого ошибка C4996 ‘strcpy’, вставьте в первую строку вот это:
mpavelFax
Заработок на дому официальная работа.
Serg_v
В MSV 2013 не работает функция strcpy, вместо нее есть strcpy_s, но разобраться с ней для данного примера тяжело. Если кто то знает, как ее использовать для указателей на нестатические переменные напишите пожалуйста.
Алексей Баранов
В Visual Studio 2013 выбрасывает ошибку на строку
strcpy
(pp[size], str);
: Ошибка 2 error C4996: ‘strcpy': This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.Пробовал копировать код с сайта все равно ошибка. Помогите разобраться в чем причина.
Igorilla_777
Долго осмысливал это выражение и понял его так. Изначально имеем одномерный массив указателей, в которых содержатся адреса начал соотв. строк str. Но, как стало ясно, увеличить размерность массива указателей напрямую нельзя, и поэтому его надо сначала уничтожить (очистить память), а только потом создать массив новой размерности. Мы создали временный массив copy, уже увеличенный как нам надо, записали в него «старые» указатели и с легкой душой грохнули старый массив pp. А потом просто переименовали copy в pp. Новый pp стал больше.
Извините за бытовую терминологию…)))