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

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

Собирая информацию для написания этой статьи, вспомнилось мне моё первое знакомство с указателями –  грусть-печаль была… Поэтому после  прочтения нескольких разделов по этой теме из разных книг о программировании на C++, было решено пойти иным путем и изложить тему Указатели C++ в той последовательности, в которой я считаю нужным. Сразу дам вам короткое определение и будем рассматривать указатели в работе – на примерах. В следующей статье (Указатели С++. Часть 2) будут изложены нюансы, применение указателей со строками в стиле Си (символьными массивами) и основное, что следует запомнить.

Указатель в С++ – переменная, которая в себе хранит адрес данных (значения) в памяти, а не сами данные.

Рассмотрев следующие примеры, вы поймете главное – зачем нам нужны  в программировании  указатели, как их объявлять и применять.

Допустим, в программе нам необходимо создать целочисленный массив, точный размер которого нам не известен до начала работы программы. То есть мы не знаем какое количество чисел понадобится пользователю внести в этот массив. Конечно, мы можем подстраховаться и объявить массив на несколько тысяч элементов (к примеру на 5 000). Этого (по нашему субъективному мнению) должно хватить пользователю для работы. Да – действительно –  этого может быть достаточно. Но не будем забывать, что этот массив займет в оперативной памяти много места (5 000 * 4 (тип int) = 20 000 байт).  Мы то подстраховались, а пользователь будет заполнять только 10 элементов нашего массива.  Получается, что реально 40 байт в работе, а 19 960 байт напрасно занимают память.

В стандартную библиотечную функцию  sizeof()  передаем объявленный массив arrWithDigits строка 10. Она вернёт на место вызова размер в байтах, который занимает этот массив в памяти. На вопрос “Сколько чисел вы введете в массив?” ответим – 10. В строке 15,  выражение  amount * sizeof(int)  станет равнозначным 10 * 4, так как функция sizeof(int) вернет 4 (размер в байтах типа int).  Далее введем числа с клавиатуры и программа покажет их на экран. Получается, что остальные 4990 элементов будут хранить нули. Так что нет смысла их показывать.

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

Главная информация на экране: массив занял 20 000 байт, а реально для него необходимо 40 байт. Как выйти из этой ситуации? Возможно, кому-то захочется переписать программу так, чтобы пользователь с клавиатуры вводил размер массива и уже после ввода значения объявить массив с необходимым количеством элементов. Но это невозможно реализовать без указателей. Как вы помните – размер массива должен быть константой. То есть целочисленная константа должна быть инициализирована до объявления массива и мы не можем запросить её ввод с клавиатуры.  Поэкспериментируйте – проверьте.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

указатели с++, указатели c++, new, delete
Тут нам подсвечивает красным оператор >> так как изменять константное значение нельзя.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

указатели с++, указатели c++, new, delete
Тут нас предупреждают о том, что размером массива НЕ может быть значение обычной переменной. Необходимо константное значение!

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

В следующем коде мы будем использовать указатель  и новые для вас операторы new (выделяет память) и delete (освобождает память).

Пользователь вводит значение с клавиатуры – строка 12. Ниже определён указатель:  int* arrWithDigits  Эта запись означает, что arrWithDigits  – это указатель. Он создан для хранения адреса ячейки, в которой будет находиться целое число. В нашем случае arrWithDigits   будет указывать на ячейку массива с индексом 0. Знак * – тот же что применяется при умножении. По контексту компилятор “поймет”, что это объявление указателя, а не умножение. Далее следует знак =  и оператор new, который выделяет участок памяти. Мы помним, что у нас память должна быть выделена под массив, а не под одно число. Запись new int [sizeOfArray] можно расшифровать так: new (выдели память) int (для хранения целых чисел) [sizeOfArray] (в количестве sizeOfArray).

Таким образом в строке 16 был определён динамический массив. Это значит, что память под него выделится (или не выделится) во время работы программы, а не во время компиляции, как это происходит с обычными массивами. То есть выделение памяти зависит от развития программы и решений, которые принимаются непосредственно в её работе. В нашем случае – зависит от того, что введёт пользователь в переменную  sizeOfArray

В строке 25 применяется оператор delete. Он освобождает выделенную оператором new память. Так как new выделил память под размещение массива, то и при её освобождении надо дать понять компилятору, что необходимо освободить память массива, а не только его нулевой ячейки, на которую указывает arrWithDigits. Поэтому между delete и именем указателя ставятся квадратные скобки [] –  delete [] arrWithDigits;   Следует запомнить, что каждый раз, когда выделяется память с помощью new, необходимо эту память освободить используя delete.  Конечно, по завершении программы память, занимаемая ей, будет автоматически освобождена. Но пусть для вас станет хорошей привычкой использование операторов new и delete в паре. Ведь в программе могут располагаться 5-6 массивов например.  И если вы будете освобождать память, каждый раз, когда она уже не потребуется в дальнейшем в запущенной программе – память будет расходоваться более разумно.

Допустим в нашей программе мы заполнили массив десятью значениями. Далее посчитали их сумму и записали в какую-то переменную. И всё –  больше мы с этим массивом работать уже не будем. Программа же продолжает работу и в ней создаются новые динамические массивы для каких-то целей. В этом случае целесообразно освободить память, которую занимает первый массив. Тогда при выделении памяти под остальные массивы эта память может быть использована в программе повторно.

Рассмотрим использование указателей, как параметров функций. Для начала, наберите и откомпилируйте следующий код. В нем функция получает две переменные и предлагает внести изменения в их значения.

Запустите программу и введите новые значения переменных. Вы увидите в итоге, что по завершении работы функции, переменные не изменились и равны 0.

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

Как вы помните, функция работает не на прямую с переменными, а создает их точные копии. Эти копии уничтожаются после выхода из функции. То есть функция получила в виде параметра какую-то переменную, создала её копию, поработала с ней и уничтожила. Сама переменная останется при этом неизменной.

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

В заголовке (строка 27) и прототипе функции (строка 5), добавляем операцию * перед именами параметров. Это говорит о том, что функция получит адреса, а не значения переменных.  При вызове функции из main()  добавляем перед именами передаваемых переменных операцию & (амперсанд – Shift + 7). & означает взятие адреса. Вы помните, что указатель хранит адрес. Поэтому мы не можем передать обычное значение, если в заголовке указано, что функция примет указатель. Используя &  перед именами переменных, функция получит их адреса.

В теле функции, при вводе значений в переменные, необходимо использовать разыменование указателей. Делается это с помощью всё той же операции * : cin >> *varForCh1;  Так мы внесем изменения в значения переменных, а не в адреса. Проверим работу программы:

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

Всё получилось – значения переменных были изменены в функции.

Такой способ (передача параметров в функцию через указатель) широко использовался в программировании на C. В C++ всё чаще используют передачу параметров в функцию по ссылке. Там отпадает необходимость использовать разыменование * и взятие адреса &  переменных. Поэтому использовать передачу параметров по ссылке удобней. Этот способ мы с вами рассмотрим в следующих уроках.

Не переживайте, если что-то не совсем понятно. Вы получили много новой информации в этом уроке – и это вполне нормально, что не всё воспринялось сразу. Понимание указателей придет с практикой. Мы еще поговорим об указателях во второй части к этой статье и порешаем задачи. Так что все будет нормально.

По возможности посмотрите видео об указателях:

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

  1. Примерно на 45-ой минуте 8-го урока говорится, что если не использовать константу, то по любому только выделять память через new. Но во время этого я подумал, что дело решается ещё одной переменной.
    int N;
    cin>>N;
    const int M=N;
    int d[M];

    for (int i = 0;i!=M;i++)
    {
    d[i] = i;
    cout <<d[i]<<endl;
    }

    И это у меня вполне компилируется.

    1. Вы могли бы и не присваивать M=N, а просто написать: int d[N].
      Но это не противоречит ничему сказанному – это расширение стандарта C++, пришедшее из стандарта C99 языка C (как видите, всё это стандарты самых последних лет!), и называется это VLA (Variable Legth Array).
      Это расширение позволяет создавать локальные внутри функции массивы с динамически определяемыми размерами. При этом массив создаётся в стеке вызываемой функции. В принципе, в C был и раньше скрытый способ делать то же самое с помощью вызова:

      int *d = (int*)alloca( N * sizeof( int ) );

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

      1. Спасибо за подробные разъяснения. Я присваивал М = N из-за того, что автор видео показал, как напрямую, если не использовать ключевое слово const, не компилируется. А я сразу решил проверить, но немного по другому. Кстати, в компиляторе давно я указывал с++11.

    2. Массив ваш занимает память, динамический массив работает как обычный, вот только память потом очищается, в больших проектах очень полезно

      1. Очень сомнительное утверждение!
        Массив объявленный локально (в функции) размещается в стеке и очищается при завершении функции. Особенно результативно это после разрешения массивов с динамическими границами стандартом C++11.
        А использование динамически размещаемых массивов, при определённых достоинствах, имеет ещё больше недостатков.

  2. Парадокс!!!
    Компилятор “compiler: GNU GCC Compiler” принял следующий код:

    #include
    #include

    using namespace std;

    int main()
    {
    setlocale(LC_ALL, “Russian”);

    int SizeOfArray;
    cout << "Сколько чисел вы введёте в массив? " <> SizeOfArray;
    int arrWithDigits[SizeOfArray] = {};

    for (int i = 0; i < SizeOfArray; i++)
    {
    cout << i + 1 <> arrWithDigits[i];
    }
    cout << endl;

    for (int i = 0; i < SizeOfArray; i++)
    {
    cout << setw(3) << arrWithDigits[i] << " |";
    }
    cout << endl;
    return 0;
    }

    Программа, в которой работаю Code::Block 16.01.
    ————– Build: Debug in Zadacha12 (compiler: GNU GCC Compiler)—————

    mingw32-g++.exe -Wall -g -c C:\2\codeblocks-16.01\Code\Zadacha12\Zadacha12.cpp -o obj\Debug\Zadacha12.o
    mingw32-g++.exe -o bin\Debug\Zadacha12.exe obj\Debug\Zadacha12.o
    Output file is bin\Debug\Zadacha12.exe with size 1,01 MB
    Process terminated with status 0 (0 minute(s), 0 second(s))
    0 error(s), 0 warning(s) (0 minute(s), 0 second(s))

    1. Парадокс то в чём?
      Компилятор GCC намного совершеннее компилятора от Microsoft, и гораздо точнее в синтаксисе соответствует стандартам языка C++.

  3. ни о стеке, ни о куче, сразу @int* prt= new int;@. гениально. особенно отсутствие внятного описания про амперсанд порадовало. сразу кода вам полстраницы.

    1. Для обсуждения и понимания указателей ни стек, ни куча, как понятия, и даром не нужны.

      “… вы просто не умеете их готовить”.

  4. на протяжение всего изучения с++ задавался себе вопросом – как же дать возможность пользователю самому определить количество элементов в массиве. Что только не пробовал. А тут целый урок на эту тему. Спасибо!

  5. Добрый день. В описании темы говорится что, для того, чтобы передать адрес в функцию например в функцию F(&a) , то нужно применить оперсанд & для взятия адреса, а на входе функции объявить указатель F(int * p), но почему следующий код сработал?
    void swap (int *a, int *b) {
    ….
    }
    ….
    int a = 5;
    int b = 10;
    swap(a,b); <- – – были по факту переданы значения данных переменных, но функция приняла их адрес и позволила их изменить.
    Программа писалась в онлайн компиляторе REPL it , по этому поводу у меня вопрос мы всегда должны при передаче данных в функцию использовать &?
    Почему программа была скомпилирована, особенности онлайн компилятора?

  6. А если так
    //———————————————-
    #include

    using namespace std;

    int main()
    {
    int a;
    cin >> a;
    const int b = a;
    int c[b];

    return 0;
    }
    //——————————–
    Вроде работает

  7. Функция swap(изменение чисел местами с объяснением). За пример взяты числа 2 и 3.
    void swap(int* p, int* q) // p = 2, q = 3
    {
    int temp{};
    temp = *p; // temp = 0, p = 2; temp = 2, p = 2
    *p = *q; // p = 2, q = 3; p = 3, q = 3
    *q = temp; // temp = 2, q = 3; temp = 2, q = 3
    temp = NULL; // p = 3, q = 2
    }

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

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