Передача строки в функцию по указателю. Возврат указателя из функции

В первой части мы рассмотрели, как используя указатель и оператор new можно выделить участок памяти необходимого размера непосредственно в процессе работы программы. Так же узнали, как этот участок памяти можно освободить, используя delete. Увидели как параметры передаются в функцию по указателю. И то, что это даёт возможность внести изменения в значение переменных, которые передаются в функцию.

Сейчас мы рассмотрим пример где будем передавать в функцию указатели на строки (указатели типа char). При этом сама функция возвращает указатель. Задача следующая: Есть два указателя на строки, под которые выделены необходимые им участки памяти. Необходимо объединить эти две строки. То есть надо выделить новый участок памяти для первой строки, чтобы стало возможным дописать в неё вторую строку.

Перед тем, как набирать код, чтобы вас не «засыпало» ошибками — установите свежую версию среды Microsoft Visual Studio. Например Microsoft Visual Studio 2013 Express. Если вы используете более раннюю версию, то вместо функций strcat_s() применяйте strcat() и strcpy() вместо strcpy_s().

#include <iostream>

#include <cstring>

using namespace std;

 

char* giveNewMem(char *pstr1, int reqSize);

 

int main()

{

setlocale(LC_ALL, "rus");

 

int strSize1 = strlen("строка 1 ") + 1;

int strSize2 = strlen("+ строка 2") + 1;

char* pStr1 = new char[strSize1];

strcpy_s(pStr1, strSize1, "строка 1 ");

char* pStr2 = new char[strSize2];

strcpy_s(pStr2, strSize2, "+ строка 2");

 

cout << "1)" << pStr1 << endl;

cout << "2)" << pStr2 << endl << endl;

 

cout << "pStr1 занимает " << strSize1 << " байт памяти c \\0" << endl;

cout << "pStr2 занимает " << strSize2 << " байт памяти c \\0" << endl;

 

// strcat_s(pStr1, strSize1, pStr2); // НЕПРАВИЛЬНО! НЕДОСТАТОЧНО ПАМЯТИ В pStr1

int requiredSize = (strSize1 + strSize2) - 1;

cout << "\nНеобходимо " << requiredSize << " байт памяти для объединения строк." << endl << endl;

 

pStr1 = giveNewMem(pStr1, requiredSize); //функция, которая перевыделит память

strcat_s(pStr1, requiredSize, pStr2);

cout << "pStr1: " << pStr1 << endl << endl;

delete[] pStr1; // освобождаем память, которая была перевыделена в функции для strInFunc

delete[] pStr2; // освобождаем память, которая была выделена в main

 

return 0;

}

 

char* giveNewMem(char *pstr1, int reqSize)

{

char* strInFunc = new char[reqSize]; // для копирования строки pstr1 перед удалением памяти

strcpy_s(strInFunc, reqSize, pstr1);

delete [] pstr1; // освобождаем память pstr1

 

return strInFunc;

}

Идем по порядку. В строке 4 находится прототип функции. Её задача — выделить новый участок памяти для строки, в конец которой запишется другая строка. Об этом мы поговорим ниже, когда дойдем до определения этой функции. Переходим к строкам 11 — 12. В них определены переменные, которые будут хранить значения длины строк "строка 1 " и "+ строка 2". Длина подсчитывается с помощью встроенной функции strlen(). Она вернет значение длины строки без учета символа \0 . Поэтому при инициализации переменных strSize1 и strSize2 мы прибавляем к тому что вернет strlen(…) единицу.

В строке 14 определён указатель pStr1 на первую строку. Чтобы записать строку, сначала необходимо выделить под нее память:char* pStr1 = new char[strSize1]; Ниже копируем строку в выделенную память: strcpy_s(pStr1, strSize1, "строка 1 "); . Первый параметр, который принимает функция strcpy_s() — указатель на адрес куда надо скопировать строку; второй — размер строки; третий — сама строка. Второй указатель pStr2 определяем точно так же.

В стр. 20 — 21, просим показать на экран записанные строки. Для этого достаточно обратиться к ним по имени указателей. Указатель хранит адрес нулевой ячейки. Когда мы просим показать его на экран — будут поочередно отображаться символы массива (участка памяти, который был выделен ранее), пока не встретится символ конца строки — \0 .

Стр. 23-24 — смотрим сколько байт памяти занимает каждая строка. Строка 26 исключена комментарием. В ней осуществлена попытка дописать вторую строку в первую. Попробуйте удалить //и откомпилировать программу. Выполнение программы прервется ошибкой. Вы увидите на экране следующее:

Конечно — ведь выделенный участок памяти под первую строку слишком мал, для того чтобы дописать в него еще какие-либо данные. В строке 28 записываем в переменную requiredSize реально необходимый размер памяти для записи двух строк: intrequiredSize = (strSize1 + strSize2) - 1; Единицу отнимаем потому, что в переменных strSize1 и strSize2 уже включены два \0 , а нам необходим только один.

Переместимся к определению функции giveNewMem() — стр. 42 — 51. Она примет первую строку (указатель на неё) и целое число (достаточный размер памяти). Смотрите — нам надо в этой функции выделить новый участок памяти для записи в него символов. Значит функция должна вернуть указатель на этот новый участок памяти. Так мы сможем записать в указатель pStr1 новый адрес, по которому расположена память для записи двух строк. Поэтому в заголовке функции пишем так char* giveNewMem(char*pstr1, int reqSize)

Так как будет выделен новый участок памяти, то ту память, которую занимает первая строка необходимо освободить. Если мы сделаем это сразу в начале функции — данные (символы строки) пропадут, потому что мы потеряем адрес по которому они расположены. Поэтому нам внутри функции надо запросить новый участок памяти, в который мы скопируем символы первой строки, перед тем как освободим занимаемую память.

Создаем новый указатель и сразу выделяем под него память, достаточную для размещения символов обеих строк (стр. 44). Далее копируем в выделенную память символы первой строки:strcpy_s(strInFunc, reqSize, pstr1); Строка скопирована — нужно освободить память, которую она занимала, чтобы не произошло утечки памяти (стр. 48). Возвращаем из функции указатель на новый участок памяти: return strInFunc;

Получается, когда мы вызываем эту функцию из main() (стр. 31)pStr1 = giveNewMem(pStr1, requiredSize); — в указатель pStr1 запишется адрес нового участка памяти, который способен вместить две строки. Осталось только дописать в эту память вторую строку (стр. 33) и показать её на экран. Перед выходом из программы освобождаем память. Сначала ту что была выделена в функции, а потом ту где располагается вторая строка.

Откомпилируем программу:

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

Под конец подведем итоги и обозначим основное, что необходимо запомнить об указателях.

Зачем нужны указатели в C++?

  • с помощью указателей, возможно выделение динамической памяти. Так память под данные выделяется в процессе работы программы, а не на этапе компиляции. Это очень выгодно, когда мы не знаем до начала работы программы сколько реально будет использовано данных (переменных). Чтобы выделять память и освобождать ее, используются операторы new и delete.
  • указатели часто используются для доступа к объемным участкам данных из функций. К данным символьных и числовых массивов например, которые определены вне функции. Такой подход пришел из программирования на языке C. В C++ относительно числовых переменных и массивов удобнее использовать передачу по ссылке. Вскоре мы с вами рассмотрим эту тему. Относительно строк в стиле Си лучше применять передачу по указателю.
  • используя указатели — мы работаем с памятью по адресам напрямую. Это быстрее, чем обращение по имени переменных.

Объявление указателей, взятие адреса, разыменование

Рассмотрим подробно на примере:

#include <iostream>

using namespace std;

 

int main()

{

setlocale(LC_ALL, "rus");

 

int firstVar = 77;

//int* pfirstVar = firstVar; // ОШИБКА! Нельзя записать данные в указатель.

int* pFirstVar = &firstVar; // запись  адреса переменной firstVar в указатель

cout << "firstVar = " << firstVar << endl; // 77

cout << "&(адрес) firstVar = " << &firstVar << endl; // покажет адрес firstVar firstVar

cout << "pFirstVar хранит адрес " << pFirstVar << endl; // тоже покажет адрес firstVar

cout << "просмотр значения (разыменование) указателя: *pfirstVar = " << *pFirstVar << endl; // 77

cout << endl;

 

int firstArr[5] = {1, 2, 3, 4, 5};

int* pFirstArr = firstArr; // операция взятия адреса ( & ) к массивам не применяется

 

cout << "firstArr = " << firstArr << endl; // адрес нулевой ячейки массива

cout << "pfirstArr = " << pFirstArr << endl; // адрес нулевой ячейки массива

 

cout << "firstArr[0] = " << firstArr[0] << endl; // значение нулевой ячейки массива

cout << "pfirstArr[0] = " << pFirstArr[0] << endl << endl; // значение нулевой ячейки массива

 

char firstStr[] = "Массив символов";

char* pFirstStr = firstStr;

 

cout << "firstStr = " << firstStr << endl; // покажет всю строку

cout << "pfirstStr = " << pFirstStr << endl; //  покажет всю строку

 

cout << "firstStr[0] = " << firstStr[0] << endl; // значение нулевой ячейки массива

cout << "pfirstStr[0] = " << pFirstStr[0] << endl; // значение нулевой ячейки массива

 

return 0;

}

В строке 8 определена обычная целочисленная переменная. В строке 9 закомментировано объявление и инициализация указателя. Попробуйте удалить знаки комментирования // и откомпилировать. Нам сообщат об ошибке. Все правильно. Ведь указатель должен хранить адрес (шестнадцатеричное число), а не значение (десятеричное число или символ). Для того, чтобы получить адрес переменной используется операция взятия адреса & (амперсанд: Shift + 7). В строке 10 создаем указатель и записываем в него адрес переменной:

Чтобы определить указатель, надо объявить его тип, за типом поставить звездочку и дать ему имя. Инициализировать указатель можно только адресом. Если вы не присваиваете адрес указателю при его объявлении, обязательно инициализируйте его нулем. Тогда такой указатель не сможет привести к сложным ошибкам, которые тяжело найти. Он просто ни на что не будет указывать.

Обычно нам не особо интересно, какой адрес хранит указатель. Нас интересуют данные, расположенные по этому адресу. Чтобы посмотреть эти данные (или внести в них изменения) к имени указателя надо применить операцию разыменования. Это такая же звездочка *, как и при объявлении.

Только в потоке cout она трактуется иначе — не так как при объявлении указателя. Она помогает обратиться к данным, хранящимся по адресу. Это продемонстрировано в строке 15 исходного кода.

В строке 18 определен целочисленный массив на пять элементов. Ниже определен указатель. Обратите внимание — чтобы инициализировать указатель адресом массива, операция & не применяется. Это потому, что имя массива ссылается на адрес нулевой ячейки. Получается что запись

вполне нормально компилируется. В переменную-указатель pFirstArr будет записан адрес нулевой ячейки массива firstArr. Эта запись аналогична следующей

В строке 25 показан пример обращения к данным элементов массива через указатель. Используется нотация массивов. То есть нам не надо применять операцию разыменования, чтобы обратиться к данным массива:

cout << "pfirstArr[0] = " << pFirstArr[0] << endl << endl;

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

for (int i = 0; i < 5; i++)

{

*(pFirstArr + i) = 55;

cout << *(pFirstArr + i) << "  ";

}

Строки 27 — 28 исходного кода — определение Си-строки и определение указателя на эту строку. Указатели замечательно справляются с работой со строками. Когда мы в потоке cout обращаемся по имени к указателю на символьный массив, он нам покажет всю строку. Так же как и в случае с массивами, компилятор будет выводить символы на экран, пока не обнаружит в массиве символ конца строки \0

Посмотрите на итог работы программы и на исходный код еще раз. Постарайтесь понять как он работает.