Основы программирования на С++ для начинающих

Указатели С++. Часть 2

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

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

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

Идем по порядку. В строке 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 исключена комментарием. В ней осуществлена попытка дописать вторую строку в первую. Попробуйте удалить //  и откомпилировать программу. Выполнение программы прервется ошибкой. Вы увидите на экране следующее:

указатели на строки с++, указатели на строки c++, new, deleteКонечно – ведь выделенный участок памяти под первую строку слишком мал, для того чтобы дописать в него еще какие-либо данные.  В строке 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);  Строка скопирована  – нужно освободить память, которую она занимала, чтобы не произошло утечки памяти (стр. 48).   Возвращаем из функции указатель на новый участок памяти:  return strInFunc;

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

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

указатели на строки с++, указатели на строки c++, new, delete

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

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

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

    • с помощью указателей, возможно выделение динамической памяти. Так память под данные выделяется в процессе работы программы, а не на этапе компиляции.  Это очень выгодно, когда мы не знаем до начала работы программы сколько реально будет использовано данных (переменных). Чтобы выделять память и освобождать ее, используются операторы new и  delete.

 

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

 

  • используя указатели – мы работаем с памятью по адресам напрямую. Это быстрее, чем обращение по имени переменных.

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

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

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

инициализация указателя

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

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

разыменование указателя

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

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

инициализация указателя

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

инициализация указателя

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

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

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

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

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

инициализация указателя, разыменование указателя, & взятие адреса

Еще одно отступление от темы, чтобы подбодрить  тех, кому тяжело дается тема указателей :) Вы не одни. Всё приходит с практикой! И к вам дойдет! Не паникуйте, если это  выглядит слишком запутанным для вас. Решайте как можно больше задач по программированию. Даже если вы что-то будете делать не так в процессе написания кода, наши замечательные среды разработки дадут об этом знать.

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

11 thoughts on “Указатели С++. Часть 2

  1. Не могу понять , в 15 строке кода – strcpy_s(pStr1, strSize1, “строка 1 “);
    в функции strcpy_s используется 3 аргумента, ранее при описании этой функции в ней указывалось только 2 аргумента (куда скопировать и что скопировать).
    Собственно вопрос : почему теперь в ней 3 аргумента? Потому что мы сейчас используем указатель как первый аргумент? Или потому что это новая версия этой функции (strcpy_s вместо strcpy)? Или этой особенность visual studio? Т.к. в code:: blocks функция strcpy принимает 2 аргумента <>.

    1. В примере кода использована функция strcpy_s(), а не strcpy(), которая действительно имеет 2 аргумента. Смотрите имена внимательнее.

      P.S. Это вовсе не означает, что так следует делать: strcpy_s() – функция, не входящая в стандарты ни POSIX, ни C, ни Linux и т.д. и т.п. … и используется только в операционных системах Windows (вы не найдёте даже её описания в литературе). Но конкретный этот пример – корректен.

      1. Понял только то что описания strcpy_s я не найду, а почему этой функции нужно знать размер строки не понятно.
        Размер строки ей надо знать потому что первым аргументом указатель? Или это особенность обновленной функции? И сколько аргументов эта функция вообще принимает (может принимать) ?

      2. У меня на code::blocks тоже не берет.Может это из-за того,что среду разработки нужно другую?более новую?Здесь написано про нее, пишут что более безопасная функция,так как предупреждает переполнение буфера! https://msdn.microsoft.com/ru-ru/library/8ef0s5kh.aspx

    2. Указывать размер копируемой строки очень полезно: если строка источник, по случайности, длиннее приёмника, то вы получите ошибку доступа к памяти с крахом всего приложения.

      Посмотрите описание библиотечной и стандартной функции strncpy(), которая безопаснее и чаще применяется в профессиональном коде чем strcpy().

  2. Среда разработки DEV CPP 5.11.
    Первый пример работает и без функции перевыделения памяти :)

  3. “Строка скопирована — нужно освободить память, которую она занимала, чтобы не произошло утечки памяти (стр. 48).”
    48:delete [] pstr1; // освобождаем память pstr1
    указатель pstr1 все равно сразу перезаписывается в
    31:pStr1 = giveNewMem(pStr1, requiredSize);
    Нормально компилится и исполняется без строки 48.

  4. Первый код, где к первой строке добавляется вторая строка имеет такой вид:
    #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;
    }

  5. Еще:
    #include
    #include
    и, код набирайте вручную, поскольку скопированный работать не будет

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

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