В первой части мы рассмотрели, як використовуючи покажчик і операторnew можно выделить участок памяти необходимого размера непосредственно в процессе работы программы. Так же узнали, как этот участок памяти можно освободить, используя delete. Увидели как параметры передаются в функцию по указателю. И то, что это даёт возможность внести изменения в значение переменных, которые передаются в функцию.
Сейчас мы рассмотрим пример где будем передавать в функцию указатели на строки (покажчики типуchar). При этом сама функция возвращает указатель. Задача следующая: Есть два указателя на строки, під які виділені необхідні їм ділянки пам'яті. Необходимо объединить эти две строки. Тобто треба виділити нову ділянку пам'яті для першого рядка, щоб стало можливим дописати в неї другий рядок.
Перед тем, как набирать код, щоб вас не “засыпало” помилками– установите свежую версию среды Microsoft Visual Studio. Наприклад, Microsoft Visual Studio 2013 Експрес. Якщо ви використовуєте більш ранню версію, то вместо функций strcat_s() застосовуйтеstrcat() іstrcpy() замістьstrcpy_s().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | #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 реально необхідний розмір пам'яті для запису двох рядків: int requiredSize = (strSize1 + strSize2) - 1; Одиницю віднімаємо тому, що в зміннихstrSize1 іstrSize2 уже включены два \0 , а нам необходим только один.
Перемістимося до визначення функції giveNewMem() – стр. 42 – 51. Вона прийме перший рядок (указатель на неё) и целое число (достаточный размер памяти). Дивіться – нам надо в этой функции выделить новый участок памяти для записи в него символов. Значит функция должна вернуть указатель на этот новый участок памяти. Так мы сможем записать в указатель pStr1 Нова адреса, по которому расположена память для записи двух строк. Тому в заголовку функції пишемо так char* giveNewMem(char *pstr1, int reqSize)
Так как будет выделен новый участок памяти, то ту память, которую занимает первая строка необходимо освободить. Если мы сделаем это сразу в начале функции – данные (символы строки) пропадут, потому что мы потеряем адрес по которому они расположены. Тому нам всередині функції треба запросити нову ділянку пам'яті, в который мы скопируем символы первой строки, перед тем как освободим занимаемую память.
Створюємо новий покажчик і відразу виділяємо під нього пам'ять, достаточную для размещения символов обеих строк (стр. 44). Далее копируем в выделенную память символы первой строки: strcpy_s(strInFunc, reqSize, pstr1); рядок skopirovana– потрібно звільнити пам'ять, которую она занимала, чтобы не произошло утечки памяти (стр. 48). Повертаємо з функції покажчик на нову ділянку пам'яті: return strInFunc;
Получается, когда мы вызываем эту функцию из main() (стр. 31) pStr1 = giveNewMem(pStr1, requiredSize); – в указатель pStr1запишется адрес нового участка памяти, который способен вместить две строки. Осталось только дописать в эту память вторую строку (стр. 33) и показать её на экран. Перед выходом из программы освобождаем память. Сначала ту что была выделена в функции, а потом ту где располагается вторая строка.
Откомпилируем программу:
Этот пример показал, как мы, используя указатели, можем распоряжаться оперативной памятью с точностью до байта.
Под конец подведем итоги и обозначим основное, что необходимо запомнить об указателях.
Зачем нужны указатели в C ?
- с помощью указателей, возможно выделение динамической памяти. Так пам'ять під дані виділяється в процесі роботи програми, а не на этапе компиляции. Це дуже вигідно, коли ми не знаємо до початку роботи програми скільки реально буде використано даних (переменных). Чтобы выделять память и освобождать ее, используются операторы new іdelete.
- покажчики часто використовуються для доступу до об'ємним ділянкам даних з функцій. К данным символьных и числовых массивов например, которые определены вне функции. Такой подход пришел из программирования на языке C. У C ++ щодо числових змінних і масивів зручніше використовуватипередачу по ссылке. Вскоре мы с вами рассмотрим эту тему. Относительно строк в стиле Си лучше применять передачу по указателю.
- используя указатели – мы работаем с памятью по адресам напрямую. Это быстрее, чем обращение по имени переменных.
Объявление указателей, взятие адреса, разыменование
Рассмотрим подробно на примере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #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;
Можно, конечно обращаться к данным массива, используя нотацию указателей, но это неудобно. Посмотрите, как бы выглядело присвоение значений элементам массива и показ значений на экран:
1 2 3 4 5 | for (int i = 0; i < 5; i++) { *(pFirstArr + i) = 55; cout << *(pFirstArr + i) << " "; } |
Строки 27 – 28 исходного кода – определение Си-строки і визначення покажчика на цей рядок. Указатели замечательно справляются с работой со строками. Коли ми в потоці cout звертаємося по імені до покажчика на символьний масив, він нам покаже всю рядок. Так же как и в случае с массивами, компилятор будет выводить символы на экран, поки не виявить в масиві символ кінця рядка \0
Подивіться на підсумок роботи програми і на вихідний код ще раз. Постарайтесь понять как он работает.
Еще одно отступление от темы, щоб підбадьорити тих, кому тяжело дается тема указателей :) Вы не одни. Всё приходит с практикой! И к вам дойдет! Не паникуйте, якщо це виглядає занадто заплутаним для вас. Решайте как можно больше задач по программированию. Навіть якщо ви щось будете робити не так в процесі написання коду, наши замечательные среды разработки дадут об этом знать.
Обязательно посмотрите видео об указателях, если вы не смотрели его в первой части статьи:
Не можу зрозуміти , в 15 рядку коду – strcpy_s(pStr1, strSize1, “рядок 1 “);
в функції strcpy_s використовується 3 аргументу, раніше при описі цієї функції в ній вказувалося тільки 2 аргументу (куди скопіювати і що скопіювати).
власне питання : чому тепер в ній 3 аргументу? Тому що ми зараз використовуємо покажчик як перший аргумент? Чи тому що це нова версія цієї функції (strcpy_s зЬгсру замість)? Або цієї особливість visual studio? т.к. в код:: blocks функція strcpy приймає 2 аргументу <>.
У прикладі коду використана функція strcpy_s(), а не strcpy(), яка дійсно має 2 аргументу. Дивіться імена уважніше.
P.S. Це зовсім не означає, що так слід робити: strcpy_s() – функція, яка не входить до стандартів ні POSIX, ні C, ні Linux і т.д. і т.п. … і використовується тільки в операційних системах Windows (ви не знайдете навіть її опису в літературі). Але конкретний цей приклад – коректний.
Зрозумів тільки те що опису strcpy_s я не знайду, а чому цієї функції потрібно знати розмір рядка не зрозуміло.
Розмір рядка їй треба знати тому що першим аргументом покажчик? Або це особливість оновленої функції? І скільки аргументів ця функція взагалі приймає (може приймати) ?
У мене на code::blocks теж не берет.Может це через те,що середовище розробки потрібно іншу?новішу?Тут написано про неї, пишуть що більш безпечна функція,так як попереджає переповнення буфера! https://msdn.microsoft.com/ru-ru/library/8ef0s5kh.aspx
Вказувати розмір копируемой рядки дуже корисно: якщо рядок джерело, випадково, довше приймача, то ви отримаєте помилку доступу до пам'яті з крахом всього програми.
Подивіться опис бібліотечної та стандартної функції strncpy(), яка безпечніше і частіше застосовується в професійному коді ніж strcpy().
Середовище розробки DEV CPP 5.11.
Перший приклад працює і без функції перевиделенія пам'яті :)
“Рядок скопійована - потрібно звільнити пам'ять, которую она занимала, чтобы не произошло утечки памяти (стр. 48).”
48:delete [] pstr1; // звільняємо пам'ять pstr1
покажчик pstr1 все одно відразу перезаписується в
31:pStr1 = giveNewMem(pStr1, requiredSize);
Нормально Компільо і виконується без рядка 48.
не можу зрозуміти чому замість можна вказати .
перший код, де до першої рядку додається другий рядок має такий вигляд:
#include
#include
using namespace std;
char* giveNewMem(char *pstr1, int reqSize);
int main()
{
setlocale(LC_ALL, “rus”);
int strSize1 = strlen(“Вітаю “) + 1;
int strSize2 = strlen(“+ тітка”) + 1;
char* pStr1 = new char[strSize1];
strcpy(pStr1, “Вітаю”);
char* pStr2 = new char[strSize2];
strcpy(pStr2, “+ тітка”);
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(pStr1, 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(strInFunc, pstr1);
delete [] pstr1; // звільняємо пам'ять pstr1
return strInFunc;
}
Ще:
#include
#include
і, код набирайте вручну, оскільки скопійований працювати не буде