Собирая информацию для написания этой статьи, вспомнилось мне моё первое знакомство с указателями – грусть-печаль была… Поэтому после прочтения нескольких разделов по этой теме из разных книг о программировании на C++, было решено пойти иным путем и изложить тему Указатели C++ в той последовательности, в которой я считаю нужным. Сразу дам вам короткое определение и будем рассматривать указатели в работе – на примерах. В следующей статье (Указатели С++. Часть 2) будут изложены нюансы, применение указателей со строками в стиле Си (символьными массивами) и основное, что следует запомнить.
Указатель в С++ – переменная, которая в себе хранит адрес данных (значения) в памяти, а не сами данные.
Рассмотрев следующие примеры, вы поймете главное – зачем нам нужны в программировании указатели, как их объявлять и применять.
Допустим, в программе нам необходимо создать целочисленный массив, точный размер которого нам не известен до начала работы программы. То есть мы не знаем какое количество чисел понадобится пользователю внести в этот массив. Конечно, мы можем подстраховаться и объявить массив на несколько тысяч элементов (к примеру на 5 000). Этого (по нашему субъективному мнению) должно хватить пользователю для работы. Да – действительно – этого может быть достаточно. Но не будем забывать, что этот массив займет в оперативной памяти много места (5 000 * 4 (тип int) = 20 000 байт). Мы то подстраховались, а пользователь будет заполнять только 10 элементов нашего массива. Получается, что реально 40 байт в работе, а 19 960 байт напрасно занимают память.
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 | #include <iostream> using namespace std; int main() { setlocale(LC_ALL, "rus"); const int SizeOfArray = 5000; int arrWithDigits[SizeOfArray] = {}; cout << "Массив занял в памяти " << sizeof(arrWithDigits) << " байт" << endl; int amount = 0; cout << "Сколько чисел вы введёте в массив? "; cin >> amount; cout << "Реально необходимо " << amount * sizeof(int) << " байт" << endl; for (int i = 0; i < amount; i++) { cout << i + 1 << "-е число: "; cin >> arrWithDigits[i]; } cout << endl; for (int i = 0; i < amount; i++) { cout << arrWithDigits[i] << " "; } cout << endl; return 0; } |
В стандартную библиотечную функцию sizeof() передаем объявленный массив arrWithDigits строка 10. Она вернёт на место вызова размер в байтах, который занимает этот массив в памяти. На вопрос “Сколько чисел вы введете в массив?” ответим – 10. В строке 15, выражение amount * sizeof(int) станет равнозначным 10 * 4, так как функция sizeof(int) вернет 4 (размер в байтах типа int). Далее введем числа с клавиатуры и программа покажет их на экран. Получается, что остальные 4990 элементов будут хранить нули. Так что нет смысла их показывать.
Главная информация на экране: массив занял 20 000 байт, а реально для него необходимо 40 байт. Как выйти из этой ситуации? Возможно, кому-то захочется переписать программу так, чтобы пользователь с клавиатуры вводил размер массива и уже после ввода значения объявить массив с необходимым количеством элементов. Но это невозможно реализовать без указателей. Как вы помните – размер массива должен быть константой. То есть целочисленная константа должна быть инициализирована до объявления массива и мы не можем запросить её ввод с клавиатуры. Поэкспериментируйте – проверьте.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
В следующем коде мы будем использовать указатель и новые для вас операторы new (выделяет память) и delete (освобождает память).
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 | #include <iostream> #include <clocale> using namespace std; int main() { setlocale(LC_ALL, "rus"); int sizeOfArray = 0; // размер массива (введет пользователь) cout << "Чтобы создать массив чисел, введите его размер: "; cin >> sizeOfArray; // ВНИМАНИЕ! int* arrWithDigits - объявление указателя // на участок памяти, которую выделит new int* arrWithDigits = new int [sizeOfArray]; for (int i = 0; i < sizeOfArray; i++) { arrWithDigits[i] = i + 1; cout << arrWithDigits[i] << " "; } cout << endl; delete [] arrWithDigits; // освобождение памяти return 0; } |
Пользователь вводит значение с клавиатуры – строка 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 массивов например. И если вы будете освобождать память, каждый раз, когда она уже не потребуется в дальнейшем в запущенной программе – память будет расходоваться более разумно.
Допустим в нашей программе мы заполнили массив десятью значениями. Далее посчитали их сумму и записали в какую-то переменную. И всё – больше мы с этим массивом работать уже не будем. Программа же продолжает работу и в ней создаются новые динамические массивы для каких-то целей. В этом случае целесообразно освободить память, которую занимает первый массив. Тогда при выделении памяти под остальные массивы эта память может быть использована в программе повторно.
Рассмотрим использование указателей, как параметров функций. Для начала, наберите и откомпилируйте следующий код. В нем функция получает две переменные и предлагает внести изменения в их значения.
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 | #include <iostream> #include <clocale> using namespace std; void changeData(int varForCh1, int varForCh2); int main() { setlocale(LC_ALL, "rus"); int variableForChange_1 = 0; int variableForChange_2 = 0; cout << "variableForChange_1 = " << variableForChange_1 << endl; cout << "variableForChange_2 = " << variableForChange_2 << endl; cout << endl; changeData(variableForChange_1, variableForChange_2); cout << endl; cout << "variableForChange_1 = " << variableForChange_1 << endl; cout << "variableForChange_2 = " << variableForChange_2 << endl; return 0; } void changeData(int varForCh1, int varForCh2) { cout << "Введите новое значение первой переменной: "; cin >> varForCh1; cout << "Введите новое значение второй переменной: "; cin >> varForCh2; } |
Запустите программу и введите новые значения переменных. Вы увидите в итоге, что по завершении работы функции, переменные не изменились и равны 0.
Как вы помните, функция работает не на прямую с переменными, а создает их точные копии. Эти копии уничтожаются после выхода из функции. То есть функция получила в виде параметра какую-то переменную, создала её копию, поработала с ней и уничтожила. Сама переменная останется при этом неизменной.
Используя указатели, мы можем передавать в функцию адреса переменных. Тогда функция получит возможность работать непосредственно с данными переменных по адресу. Внесём изменения в предыдущую программу.
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 | #include <iostream> #include <clocale> using namespace std; void changeData(int* varForCh1, int* varForCh2); int main() { setlocale(LC_ALL, "rus"); int variableForChange_1 = 0; int variableForChange_2 = 0; cout << "variableForChange_1 = " << variableForChange_1 << endl; cout << "variableForChange_2 = " << variableForChange_2 << endl; cout << endl; changeData(&variableForChange_1, &variableForChange_2); cout << endl; cout << "variableForChange_1 = " << variableForChange_1 << endl; cout << "variableForChange_2 = " << variableForChange_2 << endl; return 0; } void changeData(int* varForCh1, int* varForCh2) { cout << "Введите новое значение первой переменной: "; cin >> *varForCh1; cout << "Введите новое значение второй переменной: "; cin >> *varForCh2; } |
В заголовке (строка 27) и прототипе функции (строка 5), добавляем операцию * перед именами параметров. Это говорит о том, что функция получит адреса, а не значения переменных. При вызове функции из main() добавляем перед именами передаваемых переменных операцию & (амперсанд – Shift + 7). & означает взятие адреса. Вы помните, что указатель хранит адрес. Поэтому мы не можем передать обычное значение, если в заголовке указано, что функция примет указатель. Используя & перед именами переменных, функция получит их адреса.
В теле функции, при вводе значений в переменные, необходимо использовать разыменование указателей. Делается это с помощью всё той же операции * : cin >> *varForCh1; Так мы внесем изменения в значения переменных, а не в адреса. Проверим работу программы:
Всё получилось – значения переменных были изменены в функции.
Такой способ (передача параметров в функцию через указатель) широко использовался в программировании на C. В C++ всё чаще используют передачу параметров в функцию по ссылке. Там отпадает необходимость использовать разыменование * и взятие адреса & переменных. Поэтому использовать передачу параметров по ссылке удобней. Этот способ мы с вами рассмотрим в следующих уроках.
Не переживайте, если что-то не совсем понятно. Вы получили много новой информации в этом уроке – и это вполне нормально, что не всё воспринялось сразу. Понимание указателей придет с практикой. Мы еще поговорим об указателях во второй части к этой статье и порешаем задачи. Так что все будет нормально.
По возможности посмотрите видео об указателях:
Примерно на 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;
}
И это у меня вполне компилируется.
Вы могли бы и не присваивать M=N, а просто написать: int d[N].
Но это не противоречит ничему сказанному – это расширение стандарта C++, пришедшее из стандарта C99 языка C (как видите, всё это стандарты самых последних лет!), и называется это VLA (Variable Legth Array).
Это расширение позволяет создавать локальные внутри функции массивы с динамически определяемыми размерами. При этом массив создаётся в стеке вызываемой функции. В принципе, в C был и раньше скрытый способ делать то же самое с помощью вызова:
int *d = (int*)alloca( N * sizeof( int ) );
Пользоваться VLA нужно, тем не менее, с осторожностью:
– это компилируется только если у компилятора установлены опции на новые стандарты;
– это нововведение вызывает много споров, и может быть отменено в будущих стандартах.
Спасибо за подробные разъяснения. Я присваивал М = N из-за того, что автор видео показал, как напрямую, если не использовать ключевое слово const, не компилируется. А я сразу решил проверить, но немного по другому. Кстати, в компиляторе давно я указывал с++11.
См. : https://purecodecpp.com/archives/3205
Массив ваш занимает память, динамический массив работает как обычный, вот только память потом очищается, в больших проектах очень полезно
Очень сомнительное утверждение!
Массив объявленный локально (в функции) размещается в стеке и очищается при завершении функции. Особенно результативно это после разрешения массивов с динамическими границами стандартом C++11.
А использование динамически размещаемых массивов, при определённых достоинствах, имеет ещё больше недостатков.
Парадокс!!!
Компилятор “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))
Парадокс то в чём?
Компилятор GCC намного совершеннее компилятора от Microsoft, и гораздо точнее в синтаксисе соответствует стандартам языка C++.
ни о стеке, ни о куче, сразу @int* prt= new int;@. гениально. особенно отсутствие внятного описания про амперсанд порадовало. сразу кода вам полстраницы.
Для обсуждения и понимания указателей ни стек, ни куча, как понятия, и даром не нужны.
“… вы просто не умеете их готовить”.
на протяжение всего изучения с++ задавался себе вопросом – как же дать возможность пользователю самому определить количество элементов в массиве. Что только не пробовал. А тут целый урок на эту тему. Спасибо!
Добрый день. В описании темы говорится что, для того, чтобы передать адрес в функцию например в функцию F(&a) , то нужно применить оперсанд & для взятия адреса, а на входе функции объявить указатель F(int * p), но почему следующий код сработал?
void swap (int *a, int *b) {
….
}
….
int a = 5;
int b = 10;
swap(a,b); <- – – были по факту переданы значения данных переменных, но функция приняла их адрес и позволила их изменить.
Программа писалась в онлайн компиляторе REPL it , по этому поводу у меня вопрос мы всегда должны при передаче данных в функцию использовать &?
Почему программа была скомпилирована, особенности онлайн компилятора?
А если так
//———————————————-
#include
using namespace std;
int main()
{
int a;
cin >> a;
const int b = a;
int c[b];
return 0;
}
//——————————–
Вроде работает
Функция 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
}
*q = temp; // temp = 2, q = 3; q = 2, temp = 2