Указатель на указатель + динамическое выделение памяти (часть 1)

Обращаюсь к новичкам, которые только начали изучать указатели: «Если вас заинтересовала эта тема и вы хотите в ней разобраться, что я могу вам сказать — ситуация не из приятных!» )))  Кто бы и как бы усердно и старательно не объяснял вам что к чему, понять указатели на указатели сложно. Сам указатель на указатель содержит в себе адрес, который ссылается на другой адрес, а он, в свою очередь,  ссылается на адрес в памяти, где хранятся данные. Вроде бы и можно понять. Но как это применять на практике? Зачем оно надо??? Это понять сложнее. А надо «оно», среди прочего, для возможности работы с массивами указателей, которые указывают на память с данными (строками, например).  Каждый элемент этого массива — это указатель, который содержит в себе адрес строки (первого элемента символьного массива):

Снимок

Наша ситуация усложняется еще и тем, что в данной статье мы постараемся доступно показать, как выделять динамическую память под двумерный массив указателей и как ее освобождать. Ну что, испугались? Тогда начнем разбираться! Если вы еще слабо знаете тему Указатели, прочтите все таки сначала эту статью. Она поможет вам подготовиться к восприятию темы Указатель на указатель.

А в данной статье мы будем рассматривать пример, в котором перед нами ставится следующая задача: у нас есть указатель на указатель 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.

Результат:

CppStudio.com

~~~~~Добавляем указатели на пять строк и заполняем строки данными~~~~~
11111111111111111
22222222222222222
33333333333333333
44444444444444444
55555555555555555

Условие этой задачи мы выполнили. Надеюсь, вы оценили главное преимущество использования указателей вместо обычных массивов. При входе в программу мы не знаем, сколько строк нам будет необходимо и какой объем памяти они будут занимать. Но мы не объявляли несколько десятков символьных массивов с размером [много памяти] (а вдруг пригодятся, если пользователь будет вводить много длинных  строк). Вместо этого у нас получился один динамический массив указателей на строки, память для которых так же выделяется динамически.

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

Практика

К сожалению, для данной темы пока нет подходящих задач. Если у вас есть таковые на примете, отправте их по адресу: admin@cppstudio.com. Мы их опубликуем!

Автор: Marienko L.
Дата: 17.02.2014
Поделиться:

Комментарии

  1. Даниил Семченков

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

    Скопировал код в VS2012, запустил с отладчиком, стал отслеживать, по каким адресам хранятся строки 111111…., 2222222….. и т.д. Для первой строки адрес был 0х00be2ce0, для второй — 0х00be2d30. При вычитании из второго адреса первого в hex-системе счисления получаем 50, в десятичной — 80. В каждой строке по 17 символов. Логику не понимаю

  2. DromanX

    DromanX

    Для тех у кого ошибка C4996 ‘strcpy’, вставьте в первую строку вот это:

    #pragma warning(disable : 4996)

     

  3. mpavelFax

    Заработок на дому официальная работа.

  4. Serg_v

    Serg_v

    В MSV 2013 не работает функция strcpy, вместо нее есть strcpy_s, но разобраться с ней для данного примера тяжело. Если кто то знает, как ее использовать для указателей на нестатические переменные напишите пожалуйста.

  5. Алексей Баранов

    В 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.

    Пробовал копировать код с сайта все равно ошибка. Помогите разобраться в чем причина.

  6. Igorilla_777

    Igorilla_777

    Долго осмысливал это выражение и понял его так. Изначально имеем одномерный массив указателей, в которых содержатся адреса начал соотв. строк str. Но, как стало ясно, увеличить размерность массива указателей напрямую нельзя, и поэтому его надо сначала уничтожить (очистить память), а только потом создать массив новой размерности. Мы создали временный массив copy, уже увеличенный как нам надо, записали в него «старые» указатели и с легкой душой грохнули старый массив pp. А потом просто переименовали copy в pp. Новый pp стал  больше.

    Извините за бытовую терминологию…)))

Оставить комментарий

Вы должны войти, чтобы оставить комментарий.