Собирая информацию для написания этой статьи, згадалося мені моє перше знайомство з покажчиками– смуток-печаль була… Тому після прочитання кількох розділів по цій темі з різних книг про програмування на 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 (Мінлива Масив Legth).
Це розширення дозволяє створювати локальні всередині функції масиви з динамічно обумовленими розмірами. При цьому масив створюється в стеці викликається функції. В принципі, в C був і раніше прихований спосіб робити те ж саме за допомогою виклику:
int *d = (int*)alloca( N * sizeof( int ) );
Користуватися VLA потрібно, Проте, з обережністю:
– це компілюється тільки якщо у компілятора встановлені опції на нові стандарти;
– це нововведення викликає багато суперечок, і може бути скасовано в майбутніх стандартах.
Спасибі за докладні роз'яснення. Я привласнював М = N через те, що автор відео показав, як безпосередньо, якщо не використовувати ключове слово const, НЕ компілюється. А я відразу вирішив перевірити, але трохи по іншому. До речі, в компіляторі давно я вказував з ++ 11.
див. : https://purecodecpp.com/archives/3205
Масив ваш займає пам'ять, динамічний масив працює як звичайний, ось тільки пам'ять потім очищається, у великих проектах дуже корисно
Дуже сумнівне твердження!
Масив оголошений локально (в функції) розміщується в стеку і очищається при завершенні функції. Особливо результативно це після дозволу масивів з динамічними межами стандартом C ++ 11.
А використання динамічно розміщуються масивів, при певних перевагах, має ще більше недоліків.
парадокс!!!
компілятор “компілятор: GNU компіляторів GCC” прийняв наступний код:
#include
#include
using namespace std;
int main()
{
setlocale(LC_ALL, “російський”);
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::блок 16.01.
————– будувати: Налагодження в Zadacha12 (компілятор: GNU компіляторів GCC)—————
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
Вихідний файл бен Debug Zadacha12.exe з розміром 1,01 мегабайт
Процес завершується зі статусом 0 (0 хвилин(s), 0 second(s))
0 error(s), 0 УВАГА(s) (0 хвилин(s), 0 second(s))
Парадокс то в чому?
Компілятор GCC набагато досконаліше компілятора від Microsoft, і набагато точніше в синтаксисі відповідає стандартам мови C ++.
ні про стек, не собака, @ INT відразу * PRT = новий INT;@. геніально. особливо відсутність виразного опису про амперсанд порадувало. відразу коду вам півсторінки.
Для обговорення і розуміння покажчиків ні стек, ні купа, як поняття, і даром не потрібні.
“… ви просто не вмієте їх готувати”.
на протязі всього вивчення з ++ задавався собі питанням – як же дати можливість користувачу самому визначити кількість елементів в масиві. Що тільки не пробував. А тут цілий урок на цю тему. Спасибо!
Доброго дня. В описі теми говориться що, для того, щоб передати адресу в функцію наприклад в функцію F(&a) , то потрібно застосувати оперсанд & для взяття адреси, а на вході функції оголосити покажчик F(int * p), але чому наступний код спрацював?
недійсний своп (INT * а, ІНТ * б) {
….
}
….
int a = 5;
INT B = 10;
своп(a,b); <- – – були за фактом передані значення даних змінних, але функція прийняла їх адресу і дозволила їх змінити.
Програма писалася в онлайн компіляторі REPL it , з цього приводу у мене питання ми завжди повинні при передачі даних в функцію використовувати &?
Чому програма була скомпільована, особливості онлайн компілятора?
А якщо так
//———————————————-
#include
using namespace std;
int main()
{
int a;
cin >> a;
const int b = a;
INT C[b];
return 0;
}
//——————————–
начебто працює
заміна функцій(зміна чисел місцями з поясненням). За приклад взято числа 2 і 3.
недійсний своп(int* p, int* q) // р = 2, q = 3
{
bool checkInput{};
temp = *p; // температура = 0, р = 2; температура = 2, р = 2
*p = *q; // р = 2, q = 3; р = 3, q = 3
*q = темп; // температура = 2, q = 3; температура = 2, q = 3
temp = NULL; // р = 3, q = 2
}
*q = темп; // температура = 2, q = 3; q = 2, температура = 2