Операции с указателями
С указателями можно выполнять следующие операции: разадресация, или косвенное обращение к объекту (*), присваивание, сложение с константой, вычитание, инкремент (++), декремент (—), сравнение, приведение типов. При работе с указателями часто используется операция получения адреса (&).
Операция разадресации,или разыменования, предназначена для доступа к величине,
адрес которой хранится в указателе. Эту операцию можно использовать как для получения, так и для изменения значения величины (если она не объявлена как константа):
char а; // переменная типа char
char * р = new char; /* выделение памяти под указатель и под динамическую переменную типа char */
*р = 'Ю'; а = *р; // присваивание значения обеим переменным
Как видно из примера, конструкцию *имя_указателя можно использовать в левой части оператора присваивания, так как она является L-значением, то есть определяет адрес области памяти. Для простоты эту конструкцию можно считать именем переменной, на которую ссылается указатель. С ней допустимы все действия, определенные для величин соответствующего типа (если указатель инициализирован). На одну и ту же область памяти может ссылаться несколько указателей различного типа. Примененная к ним операция разадресации даст разные результаты. Например, программа:
#include <stdio.h>
int main(){
unsigned long int A = 0Xcc77ffaa:
unsigned short int* pint = (unsigned short int*) &A
unsigned char* pchar = (unsigned char *) &A:
printf(" | % x | % x | % x |", A, *pint, *pchar);
return 0;
на IBM РС-совместимом компьютере выведет на экран :
| cc77ffaa | ffaa | аа |
Значения указателей pint и pchar одинаковы, но разадресация pchar дает в результате
один младший байт по этому адресу, а pint — два младших байта.
В приведенном выше примере при инициализации указателей были использованы операции приведения типов. Синтаксис операции явного приведения типа прост: перед именем переменной в скобках указывается тип, к которому ее требуется преобразовать. При этом не гарантируется сохранение информации, поэтому в общем случае явных преобразований типа следует избегать. Подробнее операция приведения типов рассмотрена на.
При смешивании в выражении указателей разных типов явное преобразование типов требуется для всех указателей, кроме void*. Указатель может неявно преобразовываться в значение тина bool (например, в выражении условного оператора), при этом ненулевой указатель преобразуется в true,а нулевой в false.
Присваивание без явного приведения типов допускается в двух случаях:
• указателям типа void*:
• если тип указателей справа и слева от операции присваивания один и тот же.
Таким образом, неявное преобразование выполняется только к типу void*. Значение 0 неявно преобразуется к указателю на любой тип. Присваивание указателей на объекты указателям на функции (и наоборот) недопустимо. Запрещено и присваивать значения указателям-константам, впрочем, как и константам любого типа (присваивать значения указателям на константу и переменным, на которые ссылается указатель-константа, допускается).
Арифметические операциис указателями (сложение с константой, вычитание, инкремент и декремент) автоматически учитывают размер типа величин, адресуемых указателями. Эти операции применимы только к указателям одного типа и имеют смысл в основном при работе со структурами данных, последовательно размещенными в памяти, например, с массивами.
Инкремент перемещает указатель к следующему элементу массива, декремент — к предыдущему. Фактически значение указателя изменяется на величину sizeof (тип). Если указатель на определенный тип увеличивается или уменьшается на константу, его значение изменяется на величину этой константы, умноженную на размер объекта данного типа, например:
short * р = new short [5];
Р++; // значение р увеличивается на 2
long * q = new long [5];
q++; // значение q увеличивается на 4
Разность двух указателей — это разность их значений, деленная на размер типа в байтах. Суммирование двух указателей не допускается. При записи выражений с указателями следует обращать внимание на приоритеты операций. В качестве примера рассмотрим последовательность действий, заданную в операторе
*р++ = 10;
Операции разадресации и инкремента имеют одинаковый приоритет и выполняются справа налево, но, поскольку инкремент постфиксный, он выполняется после выполнения операции присваивания. Таким образом, сначала по адресу, записанному в указателе р, будет записано значение 10, а затем указатель будет увеличен на количество байт, соответствующее его типу. То же самое можно записать подробнее:
*р = 10; Р++;
Выражение (*р)++. напротив, инкрементирует значение, на которое ссылается указатель.
Унарная операция получения адреса & применима к величинам, имеющим имя и размещенным в оперативной памяти. Таким образом, нельзя получить адрес скалярного выражения, неименованной константы или регистровой переменной.
Ссылки
Ссылка представляет собой синоним имени, указанного при инициализации ссылки. Ссылку можно рассматривать как указатель, который всегда разыменовывается. Формат объявления ссылки:
тип & имя;
где тип — это тип величины, на которую указывает ссылка, & — оператор ссылки, означающий, что следующее за ним имя является именем переменной ссылочного типа, например:
int коl;
int& pal = kol; // ссылка pal - альтернативное имя для ко!
const char& CR = ' \ n ' : // ссылка на константу
Запомните следующие правила.
• Переменная-ссылка должна явно инициализироваться при ее описании, кроме случаев, когда она является параметром функции, описана как extern или ссылается на поле данных класса.
• После инициализации ссылке не может быть присвоена другая переменная.
• Тип ссылки должен совпадать с типом величины, на которую она ссылается.
• Не разрешается определять указатели на ссылки, создавать массивы ссылок и ссылки на ссылки.
Ссылки применяются чаще всего в качестве параметров функций и типов возвращаемых функциями значений. Ссылки позволяют использовать в функциях переменные, передаваемые по адресу, без операции разадресации, что улучшает читаемость программы (подробнее об этом рассказывается в разделе «Параметры функции»).
Ссылка, в отличие от указателя, не занимает дополнительного пространства в памяти и является просто другим именем величины. Операция над ссылкой приводит к изменению величины, на которую она ссылается.
Массивы
При использовании простых переменных каждой области памяти для хранения данных соответствует свое имя. Если с группой величин одинакового типа требуется выполнять однообразные действия, им дают одно имя, а различают по порядковому номеру. Это позволяет компактно записывать множество операций с помощью циклов. Конечная именованная последовательность однотипных величин называется массивом. Описание массива в программе отличается от описания простой переменной наличием после имени квадратных скобок, в которых задается количество элементов массива (размерность):
float а [10]; // описание массива из 10 вещественных чисел
При описании массивов квадратные скобки являются элементом синтаксиса, а не указанием на необязательность конструкции.
Элементы массива нумеруются с нуля. При описании массива используются те же модификаторы (класс памяти, const и инициализатор), что и для простых переменных. Инициализирующие значения для массивов записываются в фигурных скобках. Значения элементам присваиваются по порядку. Если элементов в массиве больше, чем инициализаторов, элементы, для которых значения не указаны, обнуляются:
int b[5] = {3. 2. 1}; / / b[0]=3, b[l]=2, b[2]=l, b[3]=0, b[4]=0
Размерность массива вместе с типом его элементов определяет объем памяти, необходимый для размещения массива, которое выполняется на этапе компиляции, поэтому размерность может быть задана только целой положительной константой или константным выражением. Если при описании массива не указана размерность, должен присутствовать инициализатор, в этом случае компилятор выделит память по количеству инициализирующих значений. В дальнейшем мы увидим, что размерность может быть опущена также в списке формальных параметров.
Для доступа к элементу массива после его имени указывается номер элемента (индекс) в квадратных скобках. В следующем примере подсчитывается сумма элементов массива.
#include <iostream.h>
int main(){
const int n = 10;
int i, sum;
int marks[n] = {3. 4. 5. 4. 4};
for (i = 0, sum = 0; i<n; i++) sum += marks[i];
cout << "Сумма элементов: " << sum;
return 0;
Размерность массивов предпочтительнее задавать с помощью именованных констант, как это сделано в примере, поскольку при таком подходе для ее изменения достаточно скорректировать значение константы всего лишь в одном месте программы. Обратите внимание, что последний элемент массива имеет номер, на единицу меньший заданной при его описании размерности.
При обращении к элементам массива автоматический контроль выхода индекса за границу массива не производится, что может привести к ошибкам.
Пример. Сортировка целочисленного массива методом выбора. Алгоритм состоит в том, что выбирается наименьший элемент массива и меняется местами с первым элементом, затем рассматриваются элементы, начиная со второго, и наименьший из них меняется местами со вторым элементом, и так далее п-1 раз (при последнем проходе цикла при необходимости меняются местами предпоследний и последний элементы массива).
#include <iostream.h>
int ma1n(){
const int n = 20: // количество элементов массива
int b[n]; // описание массива
int i;
for (i = 0; i<n: i++) cin >> b[i]; // ввод массива
for (i = 0: i<n-l; i++){ // п-1 раз ищем наименьший элемент
// принимаем за наименьший первый из элементов:
int imin = 1:
// поиск номера минимального элемента из неупорядоченных:
for (int j = i + 1; j<n; j++)
// если нашли меньший элемент, запоминаем его номер:
if (b[j] < b[iтin]) imin = j;
int a = b[i]; // обмен элементов
b[i] = b[imin]; // с номерами
b[imin] = a; // 1 и imin
}
// вывод упорядоченного массива:
for (i = 0; i<п; i++) cout << b[i] << ' ';
return 0;
}
Процесс обмена элементов массива с номерами i и imin через буферную переменную а на i-м проходе цикла проиллюстрирован. Цифры около стрелок обозначают порядок действий.
Oбмен значений двух переменных
Идентификатор массива является константным указателем на его нулевой элемент. Например, для массива из предыдущего листинга имя b — это то же самое, что &b[0], а к i-му элементу массива можно обратиться, используя выражение *(b+1). Можно описать указатель, присвоить ему адрес начала массива и работать с массивом через указатель. Следующий фрагмент программы копирует все элементы массива а в массив b:
int а[100], b[100];
int *ра = а; // или int *р = &а[0]:
int *pb = b;
for(int i = 0; i<100; i++)
*pb++ = *pa++; // или pb[1] = pa[1];
Динамические массивысоздают с помощью операции new, при этом необходимо указать тип и размерность, например:
int п = 100;
float *р = new float [n];
В этой строке создается переменная-указатель на float, в динамической памяти отводится непрерывная область, достаточная для размещения 100 элементов вещественного типа, и адрес ее начала записывается в указатель р. Динамические массивы нельзя при создании инициализировать, и они не обнуляются. Преимущество динамических массивов состоит в том, что размерность может быть переменной, то есть объем памяти, выделяемой под массив, определяется на этапе выполнения программы. Доступ к элементам динамического массива осуществляется точно так же, как к статическим, например, к элементу номер 5 приведенного выше массива можно обратиться как р[5] или *(р+5).
Альтернативный способ создания динамического массива — использование функции mallос библиотеки С:
int п = 100;
float *q - (float *) malloc(n * sizeof(float));
Операция преобразования типа, записанная перед обращением к функции mallос, требуется потому, что функция возвращает значение указателя тина void*, а инициализируется указатель на float.
Память, зарезервированная под динамический массив с помощью new [], должна освобождаться оператором delete [], а память, выделенная функцией mallос — посредством функции free,например:
delete [ ] р: free (q):
При несоответствии способов выделения и освобождения памяти результат не определен. Размерность массива в операции delete не указывается, но квадратные скобки обязательны.
Многомерные массивызадаются указанием каждого измерения в квадратных скобках, например, оператор
int matr [6][8];
задает описание двумерного массива из 6 строк и 8 столбцов. В памяти такой массив располагается в последовательных ячейках построчно. Многомерные массивы размещаются так, что при переходе к следующему элементу быстрее всего изменяется последний индекс. Для доступа к элементу многомерного массива указываются все его индексы, например, matr[i][j], или более экзотическим способом: *(matr[i]+j) или *(*(matr+i)+j). Это возможно, поскольку matr[i] является адресом начала i-й строки массива.
При инициализации многомерного массива он представляется либо как массив из массивов, при этом каждый массив заключается в свои фигурные скобки (в этом случае левую размерность при описании можно не указывать), либо задается общий список элементов в том порядке, в котором элементы располагаются в памяти:
int mass2 [][] = { {1, 1}, {0, 2}. {1, 0} };
int mass2 [3][2] = {1, 1, 0, 2, 1, 0};
Пример.Программа определяет в целочисленной матрице номер строки, которая содержит наибольшее количество элементов, равных нулю.
#include <stdio.h>
int main(){
const int nstr = 4, nstb = 5; // размерности массива
int b[nstr][nstb]; // описание массива
int i. j;
for (i = 0; i<nstr; i++) // ввод массива
for (j = 0; j<nstb; j++) scanf("%d", &b[i][j]);
int istr = -1, MaxKol = 0;
for (i = 0; i<nstr; i++){ // просмотр массива по строкам
int Коl = 0:
for (j = 0; j<nstb; j++) i f ( b [ i ] [ j ] == 0)Kol++;
i f (Kol > MaxKol){istr = i ; MaxKol = Kol;}
}
Printf("Исходный массив :\n"):
for (i = 0; i<nstr; i++){
for (j = 0; j<nstb; j++) printf(" %d ", b[i][j]):
printf( " \ n " ) ;}
i f (istr == -1(printf ("Нулевых элементов нет");
else printf ("Hoмep строки: %d", istr):
return 0;}
Номер искомой строки хранится в переменной istr, количество нулевых элементов в текущей (i-й) строке — в переменной Kol, максимальное количество нулевых элементов — в переменной MaxKol. Массив просматривается по строкам, в каждой из них подсчитывается количество нулевых элементов (обратите внимание, что переменная Kol обнуляется перед просмотром каждой строки). Наибольшее количество и номер соответствующей строки запоминаются.
Для создания динамического многомерного массива необходимо указать в операции new все его размерности (самая левая размерность может быть переменной), например:
int nstr = 5;
int ** m = (int **) new int [nstr][10];
Более универсальный и безопасный способ выделения памяти под двумерный массив, когда обе его размерности задаются на этапе выполнения программы, приведен ниже:
int nstr, nstb;
cout << " Введите количество строк и столбцов :";
cin >> nstr >> nstb;
int **a = new int * [nstr] ; // 1
for(int i = 0; i<nstr; i++) // 2
a [ i ] = new int [nstb]; // 3
В операторе 1 объявляется переменная типа «указатель на указатель на int» и выделяется память под массив указателей на строки массива (количество строк — nstr). В операторе 2 организуется цикл для выделения памяти под каждую строку массива. В операторе 3 каждому элементу массива указателей на строки присваивается адрес начала участка памяти, выделенного под строку двумерного массива. Каждая строка состоит из nstbэлементов типа int.
Выделение памяти под двумерный массив.
Освобождение памяти из-под массива с любым количеством измерений выполняется с помощью операции delete [ ] . Указатель на константу удалить нельзя.
ПРИМЕЧАНИЕ
Для правильной интерпретации объявлений полезно запомнить мнемоническое правило: «суффикс привязан крепче префикса». Если при описании переменной используются одновременно префикс * (указатель) и суффикс [] (массив), то переменная интерпретируется как массив указателей, а не указатель на массив:
int *р[10]; — массив из 10 указателей на int.
Строки
Строка представляет собой массив символов, заканчивающийся нуль-символом. Нуль-символ — это символ с кодом, равным 0, что записывается в виде управляющей последовательности '/0'. По положению нуль-символа определяется фактическая длина строки. Строку можно инициализировать строковым литералом:
char str[10] = "Vasia";
// выделено 10 элементов с номерами от 0 до 9
// первые элементы - 'V', 'а', 's', 'i', 'а', '\0'
В этом примере под строку выделяется 10 байт, 5 из которых занято под символы строки, а шестой — под нуль-символ. Если строка при определении инициализируется, ее размерность можно опускать (компилятор сам выделит соответствующее количество байт):
char str [ ] = "Vasia": // выделено и заполнено 6 байт
Оператор
char *str = "Vasia"
создает не строковую переменную, а указатель на строковую константу, изменить которую невозможно (к примеру, оператор str[l]='o' не допускается). Знак равенства перед строковым литералом означает инициализацию, а не присваивание. Операция присваивания одной строки другой не определена (поскольку строка является массивом) и может выполняться с помощью цикла или функций стандартной библиотеки. Библиотека предоставляет возможности копирования, сравнения, объединения строк, поиска подстроки, определения длины строки и т. д. (возможности библиотеки описаны в разделе «Функции работы со строками и символами», и в приложении 6), а также содержит специальные функции ввода строк и отдельных символов с клавиатуры и из файла.
Пример. Программа запрашивает пароль не более трех раз.
#include <stclio.h>
#include <str1ng.h>
int main(){
char s[80], passw[] = "kuku"; // passw - эталонный пароль.
// Можно описать как *passw = "kuku";
int i, к = 0;
for (i = 0: !k && <3; i++){
pr1ntf("\n введитеe пароль;\n");
gets(s); // функция ввода строки
i f (strstr(s, passw)) k = 1; // функция сравнения строк
}
i f (k) printf("\n пapoль принят");
else printf("\n пapoль не принят");
return 0;
}
При работе со строками часто используются указатели.
Распространенные ошибки при работе со строками — отсутствие пуль- символа и выход указателя при просмотре строки за ее пределы.
Рассмотрим процесс копирования строки src в строку dest. Очевидный алгоритм имеет вид:
char src[10], dest[10];
for (int i = 0; i<=strlen(src); i++) dest[i] = src[i];
Длина строки определяется с помощью функции strlen, которая вычисляет длину, выполняя поиск нуль-символа. Таким образом, строка фактически просматривается дважды. Более эффективным будет использовать проверку на нуль-символ непосредственно в программе. Увеличение индекса можно заменить инкрементом указателей (для этого память под строку src должна выделяться динамически, а также требуется определить дополнительный указатель и инициализировать его адресом начала строки dest):
#include <iostream.h>
int main(){
char *src = new char [10];
char *clest = new char [10], *d = dest;
cin >> src;
while ( *src != 0) *d++ = *src++;
*d = 0; // завершающий нуль
cout << dest;
return 0;
}
В цикле производится посимвольное присваивание элементов строк с одновременной инкрементацией указателей. Результат операции присваивания — передаваемое значение, которое, собственно, и проверяется в условии цикла, поэтому можно поставить присваивание на место условия, а проверку на неравенство нулю опустить (при этом завершающий нуль копируется в цикле, и отдельного оператора для его присваивания не требуется). В результате цикл копирования строки принимает вид:
while ( *d++ = *src++);
Оба способа работы со строками (через массивы или указатели) приемлемы и имеют свои плюсы и минусы, но в общем случае лучше не изобретать велосипед, а пользоваться функциями библиотеки или определенным в стандартной библиотеке C++ классом string, который обеспечивает индексацию, присваивание, сравнение, добавление, объединение строк и поиск подстрок, а также преобразование из С-строк, то есть массивов типа char, в string, и наоборот.
ПРИМЕЧАНИЕ
Строки string не обязательно должны состоять из символов типа char. Это позволяет использовать любой набор символов (не только ASCII), но для произвольного набора следует определить собственную операцию копирования, что может снизить эффективность работы.
Список использованной литературы
1. Т.А. Павловская «C++ Программирование на языке высокого уровня.» 2003г.
2. http://firstdev.org «Указатели, массив указателей. Ссылки на члены класса в C++