Партнерка на США и Канаду по недвижимости, выплаты в крипто
- 30% recurring commission
- Выплаты в USDT
- Вывод каждую неделю
- Комиссия до 5 лет за каждого referral
Семестр 2
Практическое занятие 2. Указатели и массивы.
Что нужно предварительно знать для выполнения задания:
1. Указатели. |
При выполнении программы фрагменты программного кода и элементы данных располагаются в различных участках оперативной памяти, каждый из которых имеет свой (уникальный) адрес. Для представления адреса предназначен тип данных, называемый указателем. Указатель – это переменная, содержащая адрес другого объекта С-программы. Если переменная содержит адрес некоторого другого объекта, то говорят, что переменная указывает на этот объект. Указатель может указывать на обычную переменную, на более сложные типы данных (массив, строку, структуру...), на другой указатель, на функцию. Указатели подразделяются на две основные категории: указатели на объекты и указатели на функции. Хотя оба типа указателей представляют собой адрес, они имеют различные свойства, назначение и правила работы с ними. В данном задании рассматриваются только указатели на объекты.
1.1. Объявление переменной-указателя:
int* pnPointer; //(1)
int *pnPointer; //или (2)
означает, что объявляется указатель (с именем pnPointer) на объект типа int ( Замечание: так как в документации MFC используются обозначения типа (1), то рекомендую придерживаться первой версии). Этим объектом может быть переменная типа int, массив (одномерный или многомерный) из элементов целого типа. При объявлении указателя выделяется память для переменной типа указатель, но сам указатель пока еще никуда не «указывает». Указатели можно, но не обязательно инициализировать во время объявления, но они должны быть обязательно проинициализированы до использования. После объявления указателя на конкретный тип объекта С++ попытка использования этого указателя для ссылки на другой тип (без явного преобразования типа) приведет к ошибке при компиляции.
1.2. Инициализация указателя ( и операция получения адреса объекта – “&”):
а) неявная инициализация (по умолчанию) – как и обычные переменные неявно инициализируются нулем глобальные указатели и указатели, объявленные с ключевым словом “static”
б) явная инициализация
int nObject1; //объект типа int
int* pnPointer=&nObject1; // объявление указателя на объект типа int и инициализация указателя, то есть присвоение переменной-указателю адреса объекта (в данном случае - с помощью унарной операции получения адреса - «&»)
char* pcString=”Строка”; //компилятор отводит память для массива символов-констант, а указателю присваивается адрес первого символа
1.3. Получение значения объекта, на который указывает указатель ( операция «разыменования» - «*»):
int* pnObject; // (1) объявление указателя
... инициализация pnObject...
int nObject;
nObject=*pnObject; // (2) унарная операция «*» рассматривает свой операнд как адрес и обращается по этому адресу, чтобы извлечь содержимое
(*pnObject)++; // (3) получение значения по указателю и увеличение этого значения на единицу
Замечание: сравните объявление указателя(1) с операцией получения значения по указателю(2, 3). Это поможет Вам сформулировать простое мнемоническое правило, которое будет полезно при использовании любых сложных описаний: если объявлен указатель ”p”на объект типа “type”, то выражение «*p» эквивалентно переменной типа “type”
1.4. Арифметика указателей:
Важной особенностью арифметических операций с указателями является то, что «физическое» изменение значения указателя зависит от типа указателя, то есть от размера того объекта, на который указатель ссылается.
int* pnPointer1;
int* pnPointer2;
int nObject;
pnPointer1=&nObject;
pnPointer2=pnPointer1; //копирует указатель pnPointer1 в pnPointer2 (то есть теперь оба указателя “указывают ” на nObject)
pnPointer1++; //увеличение указателя на единицу означает на самом деле увеличение его на величину элемента типа type (в данном случае на sizeof(int))
pnPointer2=pnPointer1+5; //аналогично: значение, которое будет присвоено pnPointer2, вычисляется как сумма значения pnPointer1 и 5*sizeof(type) (в данном случае 5*sizeof(int).
Замечание: к указателю можно прибавлять только целое значение!
int number=pnPointer2 – pnPointer1; //разность двух указателей – это разность их значений, поделенная на sizeof(type) (в данном случае – 5)
BOOL bBigger;
if(pnPointer2 > pnPointer1) bBigger=TRUE; //при условии, что оба указателя указывают на однотипные и последовательно расположенные объекты, их можно сравнивать
(операции: <, >,==,!=).
1.5. Указатель типа «void*»
Существует специальный вид указателя на объекты С++ любого типа, исключающий ошибки компилятора, – void*(указатель на объект любого типа). Ключевое слово void говорит об отсутствии данных о размере объекта в памяти. Такие указатели обладают важным свойством: void-указатель может «указывать» на объект любого типа:
void* pVoid; //объявление void-указателя
int nObject;
char cObject;
int* pnObject; //объявление указателя на объект типа int
pVoid=&nObject;
pVoid=&cObject;
pVoid=pnObject;
но обратное неверно! Чтобы присвоить указателю на объект типа «type» указатель типа «void», необходимо явное приведение типа (причем в случае приведения void-указателя к указателю на требуемый тип Вы можете пользоваться оператором приведения static_cast). При этом следует учесть, что компилятор не знает тип объекта, на который указывает void-указатель, поэтому корректность явного преобразования типа полностью на совести программиста:
void* pVoid; //объявление void-указателя
int* pnObject; //объявление указателя на объект типа int
pnObject=static_cast<int*> (pVoid); //явное приведение типа void* к типу int* (1)
pnObject=(int*) pVoid; // (2) то же самое, но согласно Страуструпу данная форма явного приведения типа устарела и предпочтительной является форма (1)
Как правило, небезопасно использовать явное преобразование void-указателя к указателю на тип, отличный от того, на который на самом деле void-указатель в данный момент указывает. Например, форматы представления в памяти целых и плавающих чисел совершенно разные, поэтому в следующем примере можно получить «странные» результаты:
int nObject = 99;
void* pVoid = &nObject; // void-указателю присвоили адрес переменной типа int
double* pDouble = reinterpret_cast<double*>(pVoid); // после явного приведения типа указатель pDouble указывает отнюдь не на число 99
1.6. Нулевой указатель (NULL-pointer):
в С++ существует специальное значение указателя – нулевое, определяемое как (void*)0. Так как ни один корректный указатель, указывающий на объект, не может иметь нулевое значение, то это значение используется как признак «некорректности» указателя. В частности рекомендуется присваивать нуль-значение указателям при объявлении, если этого не делает компилятор, и если они не инициализируются конкретным значением (это делает Вашу программу «более устойчивой», так как выявить нулевой указатель гораздо легче, чем неинициализированный).
int* pnObject = 0; //объявление указателя и инициализация его нулем
... какие-то команды...
if(pnObject == 0) cout<<“Внимание! Нулевой указатель!!!”;
1.7. Указатель на указатель:
сам указатель (как и любая переменная) тоже имеет адрес, значение которого можно получить аналогично получению адреса любой переменной.
int nObject1=1, nObject2;
int* pnObject=&nObject1;
int** ppnObject=&pnObject; //переменная ppnObject содержит адрес указателя pnObject
nObject2=*(*ppnObject); //последовательно «разадресовывая» ppnObject, получаем значение объекта
1.8. Преобразование указателя:
С помощью операции явного приведения типа указатель на объект одного типа может быть преобразован в указатель на другой тип (при этом используется оператор явного преобразования одного типа в другой - reinterpret_cast). При этом следует учитывать, что объект, адресуемый указателем, будет интерпретироваться в соответствии с переопределенным типом. Такие преобразования нужно применять с большой осторожностью (и только если Вы, с одной стороны, уверены, что такое преобразование необходимо, а с другой стороны четко представляете последствия такого преобразования)
int nObject;
int* pnObject;
nObject=0x;
pnObject=&nObject;
unsigned char cObject;
unsigned char* pucObject;
pucObject= reinterpret_cast<unsigned char*>(pnObject); //будет указывать на младший байт
cObject=*( reinterpret_cast<unsigned char*>(pnObject) ); //примет значение младшего байта (то есть 0x44)
Замечание: используя преобразования такого рода, следует четко представлять себе расположение данных разных типов в памяти(количество занимаемых объектом байтов и порядок их следования), иначе результаты могут превзойти все Ваши ожидания.
2. Массивы. |
Массивы – это один из примеров структурированного типа данных. Массив – это упорядоченные (расположенные последовательно в памяти) элементы данных одного и того же типа (массив занимает непрерывную область памяти). Каждый массив имеет имя. Имя массива является указателем-константой, равной адресу начала массива (первого байта первого элемента массива). Доступ к отдельным элементам массива осуществляется по имени массива и индексу (порядковому номеру) элемента.
Замечание: элементы многомерных массивов хранятся в памяти в порядке возрастания самого правого индекса (например, для двумерного массива – по строкам).
2.1. Объявление массива:
char cArray[10]; // декларирует одномерный массив с именем «cArray», состоящий из «10» элементов типа «char».
int nArray[10][5]; // декларирует двухмерный массив с именем «nArray», состоящий из «10*5» элементов типа «int».
float fArray[2][10][50]; // ...
Замечание1: количество элементов может быть задано только константным выражением (то есть значение этого выражения должно быть равно константе на момент компиляции)
Замечание2: также как внешние пременные можно объявлять и внешние массивы.
extern int ar[10][5]; //такое объявление информирует компилятор, что на самом деле глобальный массив из 10*5 элементов int объявлен в каком-то другом модуле, поэтому память под данный массив уже отведена
2.2. Инициализация массива:
По умолчанию все элементы глобальных и статических массивов инициализируются компилятором нулями
При объявлении можно явно инициализировать элементы массива, причем существуют разные формы инициализации:
1) явное указание числа элементов массива и список начальных значений (причем, инициализирующий список может содержать меньше начальных значений, чем количество элементов в массиве, тогда остальные инициализируются нулем)
char cArray[10]={‘A’,’B’,’C’}; // объявлен одномерный массив из 10 элементов типа char. Первые три элемента инициализируются символами ‘A’,’B’,’C’. Значения остальных семи элементов равно 0
Замечание: если список начальных значений содержит больше элементов, чем число в квадратных скобках, выдается ошибка компилятора.
int nArray[3][4]={ 1, 1, 1, 1,
2, 2, 2, 2,
3, 3, 3, 3}; //объявление и инициализация двухмерного массива
char cStringAray[2][80]={ “ Первая ASCIIZ строка ”,
“ Вторая ASCIIZ строка ” }; //несколько особняком стоит инициализация двухмерного массива строками – строки массива (80 байт каждая) инициализируются символами строковых литералов. В конце каждой строки помещается нулевой байт.
2)можно объявить массив без указания числа элементов массива, а только со списком начальных значений. Тогда компилятор сам определяет размерность массива по списку инициализации
char cArray[]={‘A’,’B’,’C’}; //в результате создается одномерный массив из трех элементов и эти элементы получают начальные значения из списка инициализации
Многомерные массивы могут инициализироваться без указания одной (самой левой) из размерностей массива в квадратных скобках:
int nArray[][3] ={ 00, 01, 02,
10, 11, 12,
20, 21, 22};
Если в многомерном массиве необходимо проинициализировать не все элементы, а только несколько первых элементов строки (слоя), в списке инициализации можно использовать фигурные скобки, охватывающие значения для строки (слоя...):
int nArray[][3] ={ {0}, //0-ая строка
{10, 11}, //1-ая строка
{20, 21, 22} }; //2-ая строка
2.3. Обращение к элементу массива:
Доступ к отдельным элементам массива может выполняться с помощью индекса, при этом индексы нумеруются с 0, а не с 1. Замечание: индекс может быть любым целым выражением.
char cArray[10]; //объявление одномерного массива
char cObject=cArray[0]; //обращение к первому элементу массива
cObject=cArray[9]; //обращение к последнему элементу массива
cObject=cArray[i]; //обращение к i-тому элементу массива
Замечание: в языке С не выполняется контроль допустимости значения индекса массива, поэтому компилятор не выдаст ошибки при следующем присвоении, а результат будет непредсказуем. cObject=cArray[10]; обращение к несуществующему элементу массива
Замечание: число элементов массива можно определить следующим образом: число=sizeof(cArray) / sizeof(char)
int nArray[3][4]; //объявление двухмерного массива
int nObject=nArray[0][0]; //обращение к первому элементу массива
int nObject=nArray[2][3]; //обращение к последнему элементу массива
Замечание: число элементов в строке массива можно определить следующим образом: число=sizeof(nArray[0]) / sizeof(int)
3. Связь массивов и указателей |
В языке С существует взаимосвязь между указателями и массивами, настолько сильная, что указатели и массивы следует рассматривать одновременно:
3.1. Одномерные массивы
Для вычисления адреса произвольного i-ого элемента одномерного массива компилятору достаточно знать адрес начала массива (адрес первого элемента) и размер элемента:
адрес_элемента_i = адрес_начала_массива + i*размер_элемента
поэтому имя одномерного массива компилятор интерпретирует как константный указатель (константный, потому что ему ничего нельзя присвоить) на первый элемент массива.
Если объявлен одномерный массив
int nArray[10];
,то:
int* pn1 = nArray; //при присвоении происходит неявное преобразование имени массива в указатель на его первый элемент (то есть преобразование: int[] в int*)
int* pn2 = &nArray[0]; //также указатель на первый элемент массива, но в данном примере и левая, и правая части имеют тип int*
3.2. Многомерные массивы
Многомерные массивы можно рассматривать как массивы массивов (при этом полезно помнить, что в памяти они занимают непрерывную область). Например, двухмерный массив k*n в памяти располагается следующим образом:

Для вычисления адреса произвольного элемента многомерного массива компилятору необходимо знать адрес начала массива, все младшие размерности массива и размер элемента. Тогда для двухмерного массива адрес m*n адрес произвольного элемента [i][j] компилятор вычисляет следующим образом:
адрес_элемента_i_j = адрес_начала_массива + i*n* размер_элемента + j*размер_элемента
Для трехмерного массива m*n*l:
адрес_элемента_i_j_k = адрес_начала_массива + i*(n*l)*размер_элемента + j*l*размер_элемента + l*размер_элемента
Cинтаксис С позволяет для доступа к элементам использовать самые разные варианты объявления указателя и использования комбинаций: указатель-индекс. При этом компилятор в большинстве случаев генерит одинаковый код как при использовании указателей, так и при использовании индексов.
Примеры объявления и использования указателей в случае двухмерного массива:
int nArray[3][2];
int* pnPointer;
pnPointer= nArray; // ошибка! - “Не могу привести тип int[3][2] к типу int*”
pnPointer=&nArray[0][0]; //корректное получение указателя на нулевой элемент нулевой строки
int* arPtr[2]; //объявлен массив из двух указателей на int
int (*pnRows)[2]; //объявлен указатель на одномерный массив из двух элементов типа int
pnRows = &nArray[0]; //указатель на нулевую строку
pnRows=nArray; //неявное приведение типа int[3][2] к типу int[][2]
pnRows = &nArray[1]; //указатель на первую строку
При этом выражения nArray[0], nArray[1], nArray[2] будут указателями-константами на соответствующие строки, то есть следующие выражения будут тождественны:
nArray[0] == &nArray[0][0], - можно использовать как указатель на первую строку
nArray[1] == &nArray[1][0],
nArray[2] == &nArray[2][0]
3.3. Любую операцию, которую можно выполнить с помощью индексов, можно сделать и с помощью механизма указателей
(в ряде случаев это дает более короткий код программы), причем «точкой отсчета» при использовании указателей может быть как начало массива, так и начало каждой строки:
nArray[i][j] == *( nArray[i] + j) == *(*( nArray+i) + j)
3.4. Массивы указателей
Элементы массива могут иметь любой тип, в том числе и быть указателями-переменными. Наиболее часто массивы указателей используются для компактного расположения в памяти строк текста, структурных переменных и других «протяженных» объектов данных. Особенно удобны массивы указателей при динамическом управлении памятью (см. пункт Динамические массивы с размерностью, вычисляемой в ходе выполнения программы:).
Как и любой другой массив, массив указателей должен быть объявлен (а) и может быть проинициализирован при объявлении (б):
а)
char* cStrings[20]; //объявлен одномерный массив из 20 указателей (компилятор резервирует место под 20 указателей)
б)
char* cStrings[]={ “Ошибка ввода”, //объявление и инициализация //компилятор отводит
“Не могу открыть файл”, //место под три указателя и //присваивает
“Тайм-аут”}; //каждому значение, равное адресу начала соответствующей строки
Замечание: существует принципиальное различие в расположении в памяти массива указателей и, на первый взгляд, подобного ему двумерного массива! Сравним:
char cArray[][25]={“Ошибка ввода ”, //объявлен и проинициализирован
“Не могу открыть файл ”, //двухмерный массив 3*25
“Тайм-аут”};
При объявлении двухмерного массива (cArray) компилятор отводит 3*25*sizeof(char) байт для хранения трех заданных строк (по 25* sizeof(char) – байт на каждую строку), а при объявлении и инициализации массива указателей (cStrings) компилятор отводит 3*sizeof(char*) – байт для хранения трех указателей и размещает где-то в памяти заданные строки, причем памяти под каждую строку отводится ровно столько, сколько она занимает (количество символов + нулевой байт).
3.5. Динамическое задание массивов (оператор new ):
С помощью оператора new можно создавать массивы двумя способами. В обоих случаях при успешном выделении памяти возвращается ненулевой указатель, указывающий на первый элемент массива, при нехватке памяти – нулевое значение (а также генерируется исключение - bad_alloc). Однако существенно различаются способы задания и размещение в памяти при разных способах задания.
3.5.1. Динамические массивы с известной на момент компиляции размерностью
1) одномерные:
int* pnPointer;
pnPointer=new int[10];
2) многомерные: - обратите внимание на объявление указателя – при любом другом объявлении указателя компилятор выдаст ошибку
float (*pfPointer)[25][10]; (1)
pfPointer = new float[10][25][10]; (2)
Замечание 1: объявление указателя pfPointer (1) выделяет память под указатель на массив объектов типа float с размерностью [25][10], а не массив указателей!
Замечание 2: в выражении (2) все размерности массива, кроме самой левой, должны быть константными выражениями, которые при вычислении дают положительное целое.
int n, m, k;
... вычисляются значение n, m, k...
float (*pfPointer)[25][10];
pfPointer = new float[n+m+k][25][10];
3.5.2. Динамические массивы с размерностью, вычисляемой в ходе выполнения программы:
часто возникает потребность работать с массивами, размерность которых априори неизвестна. Последовательность действий при этом приблизительно следующая:
для одномерного массива:
int nNumber;
...вычисляется значение nNumber...
int* pnPointer = new int[nNumber];
для двухмерного массива:
int nColumns, nRows;
...вычисляются значения nColumns, nRows...
int** ppArray; //объявление указателя на указатель
ppArray=new int*[nRows]; //выделяется память под массив из nRows-указателей на строки массива типа int*
for (int i=0; i<nRows; i++)
{
ppArray[i]=new int[nColumns]; //выделение память под nColumns-элементов (для строки массива)
}
Замечание 1: следует отдавать себе отчет в том, что при таком формировании массива он не занимает непрерывную область в памяти!
Замечание 2: как бы не были сформированы массивы (статически или динамически), обращение к их элементам с точки зрения программиста осуществляется одинаково.
Замечание 3: при разных способах задания массивов компилятор по-разному осуществляет вычисление адреса элемента, хотя мнемонически обращение к элементу в обоих случаях выглядит одинаково (смотри замечание 2)
3.6. Инициализация динамических массивов
оператор new не позволяет инициализировать динамически выделенные массивы, поэтому инициализация остается на совести программиста.
3.7. Освобождение динамически-занятой памяти (оператор delete)
Создав динамический объект, Вы несете ответственность за его удаление. В С++ существуют две формы оператора: a) delete и b) delete[].
Int* pnValue = new int;
int* pnArray = new int[10];
.......
delete pnValue;
delete[] pnArray;
4. Модификатор const, массивы и указатели |
4.1. const вместо #define
В C++ рекомендуется использовать ключевое слово const вместо директивы препроцессора #define для обозначения константных значений. Компилятор, встречая значения, объявленные с модификатором const, с одной стороны, осуществляет проверку типов, а, с другой стороны, позволяет использовать такие значения вместо константных выражений
Например, объявить массив можно следующим образом:
const int maxarray = 255; //объявлена константа
char store_char[maxarray]; //корректно,
тогда как:
int maxarray = 255; //объявлена переменная типа int
char store_char[maxarray]; //ошибка компилятора,
4.2. В C++ Вы можете использовать const-значения в заголовочных файлах
//файл 1.h
const int NN=2;
//файл 1.cpp
#include “1.h”
использование NN
//файл 2.cpp
#include “1.h”
использование NN
4.3. Ключевое слово const может быть использовано при объявлении указателя:
1) Указатель является константой:
char* const pc = mybuf; // Указатель является константой (т. е. попытка изменить сам указатель вызовет ошибку компилятора, но ничто не мешает изменять значение по этому указателю)
*pc = 'a'; // корректно
char buffer[10];
pc = buffer; // ошибка компилятора
2) Указываемое значение является константой:
int nN1=1, nN2=2;
const int* pn =&nN1; //указатель на константное значение
*pn = 5; //ошибка компилятора
pn = &nN2; //корректно
3) И указатель, и указываемое значение являются константами
int nN1=1, nN2=2;
const int* const pn =&nN1; //константный указатель на //константное значение
*pn = 5; //ошибка компилятора
pn = &nN2; // ошибка компилятора
4) Указатель на переменную, объявленную с ключевым словом const должен быть также объявлен как указатель на const
const int nN = 1;
const int* pn1 = &nN; //указатель на константу =>корректно
int* pn2 = &nN; // ошибка компилятора


