Неправильное употребление указателей
Использование указателей может быть наиболее трудно для понимания начинающему программисту на С++. Когда надо использовать указатели, а когда нет? Когда использовать косвенный оператор (*)? Когда использовать адресный оператор (&)? И как избежать ошибок операционной системы во время выполнения программы?
На все эти и многие другие вопросы ответ будет дан ниже.
Использование неинициализированных указателей
Одна серьезная опасность таится в присвоении значения по адресу, содержащемуся в указателе, без первоначального присвоения адреса этому указателю.
Например:
void main()
{
int *iptr;
*iptr = 421;
printf("*iptr = %d\n", *iptr);
}
Эта ловушка опасна тем, что программа, содержащая ее, может быть “верна” и компилятор может не выдать никаких сообщений во время компиляции такой программы. В примере, указанном выше, указатель iptr имеет некоторый произвольный адрес, по которому запоминается значение 421. Эта программа настолько мала, что шанс что-нибудь затереть в памяти с ее помощью ничтожно мал, однако в больших программах возрастает вероятность разрушения других данных, поскольку вполне возможно, что по адресу iptr уже хранится другая информация. Если вы используете модель самой маленькой памяти (tiny), в которой сегменты программы и данных занимают одну и ту же область памяти, то вы подвергаете себя риску испортить свой же загрузочный модуль. Поэтому старайтесь не рубить сук, на котором вы сами же сидите и внимательно пишите программы, использующие указатели.
Ошибки при передаче по адресу
Посмотрите на следующую программу и найдите, что здесь неправильно:
void main()
{
int a, b, sum;
printf("Введите два значения:");
scanf("%d %d", a, b);
sum = a + b;
printf("Сумма значений равна: %d\n", sum);
}
Нашли? Ошибка в операторе
scanf("%d %d", a, b);
Вспомните, что scanf требует от вас передачи адресов, а не значений!
То же самое относится к любым функциям, содержащим в качестве формальных параметров указатели. Программа, написанная выше, оттранслируется и выполнится, но при этом scanf возьмет какие-то случайные значения (мусор), находящиеся в a и b и использует их как адреса, по которым будут записаны введенные вами значения.
Правильно этот оператор необходимо записать так:
scanf(“%d %d”, &a, &b);
Здесь функции scanf передаются адреса a и b, и введенные значения правильно запоминаются в этих переменных по их адресам.
Та же неприятность может случиться с вашей собственноручно написанной функцией. Помните функцию swap, которую мы определили в параграфе об указателях?
Что произойдет, если вы вызовете ее следующим образом:
void main()
{
int i,j;
i = 42;
j = 53;
printf("До обработки: i = %fd j = %fd\n", i, j);
swap(i, j);
printf("После обработки: i = %fd j = %fd\n", i, j);
}
Переменные i и j будут иметь одни и те же значения как до, так и после их обработки путем обращения к функции swap; однако значения адресов чисел 421 и 53 будут переставлены, что породит некоторые хитрые и труднообнаружимые ошибки. Как избежать этого?
Пользуйтесь прототипами функций и полными определениями функций.
И действительно, вы получили бы ошибку при выполнении main, если бы swap была описана так, как в этой главе. Если же вы определите ее таким образом, как представлено далее, то программа выполнится без ошибок:
void swap(a, b)
int *a, *b;
{
}
Вынесение описаний переменных a и b из скобок отключает контроль над ошибками, проводимый в противном случае; это является лучшим доводом в пользу того, чтобы не использовать классический стиль при определении функции.
Массивы
Большинство языков высокого уровня – включая С++ – позволяют определять МАССИВЫ, т.е. индексированный набор данных определенного типа. Используя массив, вы можете переписать предыдущую программу следующим образом:
void main()
{
#define NUMINTS 3
int list[NUMINTS], i;
list[0] = 421;
list[1] = 53;
list[2] = 1806;
printf("Список адресов:");
for(i=0; i<NUMINTS; i++)
printf("%p ",&list[i]);
printf("\nСписок значений: ");
for (i=0; i<NUMINTS; i++)
printf("%4p ", list[i]);
printf("\n");
}
Выражение int list[NUMINTS] объявляет list как массив переменных типа int, с объемом памяти, выделяемым для трех целых переменных.
К первой переменной массива можно обращаться как к list[0], ко второй – как к list[1], и третьей как к list[2].
В общем случае описание любого массива имеет следующий вид:
Type name[size]
(тип имя[размер]),
где: type – тип данных элементов массива (любой из допустимых в языке);
name – имя массива.
Первый элемент массива – это name[0], последний элемент – name[size-1]; общий объем памяти в байтах определяется выражением size*(sizeof(nype)).
Массивы и указатели
Вы, наверное, уже поняли, что существует определенная связь между массивами и указателями. Поэтому, если вы выполните только что рассмотренную программу, полученный результат будет вам уже знаком:
список адресов: 163A 163C 163E
список значений: 421 53 1806
Начальный адрес другой, но это единственное различие. В самом деле, имя массива можно использовать как указатель; более того, вы можете определить указатель как массив. Рассмотрим следующие важные тождества:
(list + i) == &(list[i])
*(list + i) == list[i]
В обоих случаях выражение слева эквивалентно выражению справа; и вы можете использовать одно вместо другого, не принимая во внимание, описан ли list как указатель, или как массив.
Единственное различие между описанием list, как указателя или как массива состоит в размещении самого массива. Если вы описали list как массив, то программа автоматически выделяет требуемый объем памяти. Если же вы описали переменную list как указатель, то вы сами обязательно должны выделить память под массив, используя для этого функцию calloc или сходную с ней функцию, или же присвоить этой переменной адрес некоторого сегмента памяти, который уже был определен ранее.
Массивы и строки
Мы говорили о строках в предыдущей главе и обращались при описании строк к двум немного различным способам: мы описывали строку как указатель на символы и как массив символов. Теперь вы можете лучше понять это различие.
Если вы описываете строку как массив типа char, то память для этой строки резервируется автоматически. Если же вы описываете строку как указатель на тип данных char, то память не резервируется: вы должны либо сами выделить ее (используя функцию malloc или ей подобную), или же присвоить ей адрес существующей строки.
Многомерные массивы
Да, вы действительно можете использовать многомерные массивы, и они описываются именно так, как вы себе и представляли:
type name[size1][size2]...[sizeN];
(тип имя [размер1][размер2]...[размерN];)
Рассмотрим следующую программу, которая определяет два двумерных массива, а затем выполняет их матричное умножение:
void main()
{
int a[3][4] = {{ 5, 3, -21, 42}, {44,15, 0, 6},
{97, 6, 81, 2}};
int b[4][2] = {{22, 7}, {97,-53}, {45, 0},
{72, 1}};
int c[3][2], i, j, k;
for (i=0, i<3, i++)
{
for (j=0; j<2; j++)
{
c[i][j] = 0;
for (k=0; k<4, k++)
c[i][j] += a[i][k] * b[k][j];
}
}
for (i=0; i<3; i++)
{
for (j=0; j<2, j++)
printf ("c[%d][%d] = %d ", i, j, c[i][j]);
printf("\n");
}
}
Отметим два момента в этой программе. Синтаксис определения двумерного массива состоит из набора {...} списков, разделенных запятой. Квадратные скобки ([ ]) используются для записи каждого индекса.
Некоторые языки для определения массивов используют синтаксис [i, j]. Так можно написать и на С++, но это все равно, что сказать просто [j], т.к. запятая интерпретируется как оператор, означающий (“определить i, затем определить j, затем присвоить всему выражению значение j”).
Для полной уверенности ставьте квадратные скобки вокруг каждого индекса.
Многомерные массивы хранятся в памяти слева направо по правилу “строки – столбцы”. Это означает, что последний индекс изменяется быстрее. Другими словами, в массиве arr[3][2] элементы arr хранятся в памяти в следующем порядке:
arr[0][0]
arr[0][1]
arr[1][0]
arr[1][1]
arr[2][0]
arr[2][1]
Тот же принцип сохраняется для массивов двух-, трех-, и большей размерности.
Многомерные массивы и динамическое распределение памяти. Зачастую в программах необходимо использовать многомерные массивы большого объема, однако существует некоторое ограничение на размер непрерывной области памяти, которую способен выделить компилятор. Преодолеть это ограничение можно с помощью динамического распределения, которое позволяет выделить множество небольших областей памяти и работать с ними как с одним массивом. Ниже приводится пример, иллюстрирующий такое выделение памяти:
void main()
{
clrscr();
int *a[200], i;
for (i=0; i<200; i++)
{
a[i]=(int*)malloc(200*sizeof(int));
if (a[i] == NULL)
{
printf("Нет памяти");
exit(1);
}
}
}
В примере выделяется память для массива размером 200х200 элементов: 200 раз порциями по 200*sizeof(int) байт.
Для того, чтобы освободить память (например, из-под не нужного более массива), используется функция:
void free(void *block);
Освободить память, выделенную под массив в предыдущей программе, можно так:
for (i=0; i<200; i++)
{
free(a[i]);
}
Массивы и функции
Что произойдет, если вы захотите передать массив в функцию? Рассмотрим следующую функцию, возвращаемую индекс минимального числа массива int:
int imin(int list[], int size)
{
int i, minindx, min;
minindx = 0;
min = list[minindx];
for (i = 1; i < size; i++)
if (list[i] < min)
{
min = list[i];
minindx = i;
}
return(minindx);
}
Здесь вы видите одну из важных особенностей С++: вам необязательно знать при трансляции величину list[]. Почему? Потому что компилятор считает list[] начальным адресом массива, и не заботится о том, где его конец.
Программа, обращающаяся к функции imin, может выглядеть так:
void main()
{
#define VSIFE 22
int i, vector[VSISE];
for (i = 0; i < VSIZE; i++)
{
vector[i] = rand();
printf("vector[%2d] = %6d\n", i, vector[i]);
}
i = imin(vector, VSIZE);
printf("minimum: vector[%2d] = %6d\n",
i, vector[i]);
}
Может возникнуть вопрос: что именно передается в imin? В функцию imin передается начальный адрес массива vector. Это означает, что если вы производите какие-либо изменения массива list в imin то, те же изменения будут произведены и в массиве vector. Например, вы можете написать следующую функцию:
void setrand(int list[], int size);
{
int i;
for(i=0; i < size; i++) list[i] = rand();
}
Теперь для инициализации массива vector вы можете написать в main setrand(vector, VSIZE). Следует заметить, что массиву vector будут присвоены некие случайные числа, являющиеся результатом работы датчика случайных чисел, эмулируемого функцией Си rand().
А как передавать многомерный массив? Имеется ли такая возможность? Предположим, вы хотите модифицировать setrand для работы с двумерным массивом. Вы должны написать приблизительно следующее:
void setrand(int matrix[ ][CSIZE],int rsize)
{
int i,j;
for (i = 0; i < rsize; i++)
{
for (j=0, j < CSIZE; I++)
matrix[i][j] = rand();
}
}
где CSIZE – это глобальная константа, определяющая второе измерение массива.
Другими словами, любой массив, передаваемый setrand, получит второе измерение массива, равное CSIZE.
Однако, есть еще одно решение. Предположим, у вас есть массив matrix[15,7], который вы хотите передать в setrand. Если вы используете следующее описание:
setrand (int list[], int size);
Обращение к функции будет иметь вид:
setrand(matrix, 15*7);
Массив matrix будет рассматриваться функцией setrand как одномерный массив, содержащий 105 элементов (15 строк * 7 столбцов), с которым будут произведены необходимые вам действия.
Индексы массивов
Не забудьте, что индекс массива начинается с элемента [0], а не с элемента [1]. Наиболее распространенная ошибка может быть проиллюстрирована на примере следующей программы:
void main()
{
int list[100], i;
for (i =1; i <= 100; i++);
list[i] = i + 1;
}
Данная программа оставляет первый элемент list – list[0] – неинициализированным, и записывает значение в несуществующий элемент list – list[100], возможно испортив при этом другие данные.
Правильная программа будет иметь следующий вид:
void main()
{
int list[100], i;
for (i = 0; i < 100; i++)
list[i] = i + 1;
}
Структуры
Массивы и указатели позволяют вам создавать список элементов одного типа. А что, если вы хотите создать нечто, содержащее элементы различного типа? Для этого используются структуры.
Структура – это конгломерат элементов различного типа. Допустим, вы хотите сохранить информацию о звезде: ее имя, спектральный класс, координаты и прочее. Вы можете описать это следующим образом:
typedef struct
{
char name[25];
char class;
short subclass;
float decl, RA, dist;
} star;
Здесь определена структура (struct) типа star. Сделав такое описание в начале своей программы, вы можете дальше использовать этот определенный вами тип данных:
void main()
{
star mystar;
strcpy(mystar.name, "Епсилон Лебедя");
mystar.class = ‘N’;
mystar.subclass = 2;
mystar.dec1 = 3.5167;
mystar.RA = -9.633;
mystar.dist = 0.303
/* конец функции main() */
}
Вы обращаетесь к каждому элементу структуры, используя его составное имя, состоящее из имени структуры (на первом месте) и, в порядке иерархической подчиненности, имен ее образующих элементов, разделенных точками (.). Конструкции вида: vername.memname (имя переменной.имя элемента) считается эквивалентной имени переменной того же типа, что и memname, и вы можете выполнять с ней те же операции.
Структуры и указатели
Вы можете описывать указатели на структуры точно так же, как и указатели на другие типы данных. Это необходимо для создания связных списков и других динамических структур данных элементами которых, в свою очередь, являются структуры данных.
Фактически указатели на структуры так часто используются в С++, что существует специальный символ для ссылки на элемент структуры, адресованной указателем. Рассмотрим следующий вариант предыдущей программы:
#include <alloc.h>
void main()
{
star *mystar;
mystar = (star*)malloc(sizeof(star));
strcpy (mystar -> name, "Эпсилон Лебедя");
mystar -> class = ‘N’;
mystar -> subclass = 2;
mystar -> dec1 = 3.5167;
mystar -> RA = -9.633;
mystar -> dist = 0.303;
/* Конец функции main() */
}
В этом варианте mystar объявляется как указатель типа star, а не как переменная типа star. Память для mystar резервируется путем обращения к функции malloc. Теперь, когда вы ссылаетесь на элементы mystar, используйте ptrname -> memname. Символ -> означает, что “элемент структуры направлен в...”; это сокращенный вариант от обозначения (*ptrname).memname принятый в С++.
Операции с файлами
Часто в программах возникает необходимость работать с внешними источниками данных – файлами. Она может быть обусловлена, например, большим объемом обрабатываемой информации. Рассмотрим несколько базовых функций, с помощью которых возможно осуществлять операции с файлами.
Доступ к файлам может осуществляться в двух режимах: текстовом и двоичном. Режим определяется установкой глобальной переменной _fmode (библиотека fcntl.h), которая может принимать значения O_TEXT (по умолчанию) и O_BINARY. Отличие между режимами состоит в том, что в текстовом режиме воспринимаются символы переноса строки, а в двоичном – нет.
Создание нового файла производится следующей функцией:
int creat(const char *path, int amode);
где: *path – определяет путь к файлу;
amode – определяет режим доступа к созданному файлу (необходимо подключение библиотеки include <sys\stat.h>): S_IWRITE – разрешения записи, S_IREAD – разрешение чтения, S_IREAD|S_IWRITE – разрешения чтения/записи.
Если файл с указанным именем уже существует на диске, то функция creat выполняет запись поверх имеющегося файла. В случает неудачного завершения функция creat возвращает –1, в случае удачного – положительное число.
Пример:
a = creat("H:\file", S_IREAD|S_IWRITE);
if (a==-1)
{
printf("Cannot create");
exit(1)
}
Если есть необходимость работать с уже имеющимся файлом, то используется функция open:
int open(const char *path, int access);
где: access – определяет режим доступа к открытому файлу: O_RDONLY – только чтение, O_WRONLY – только запись, O_RDWR – чтение и запись.
Пример использования функции open аналогичен примеру использования creat.
Запись и чтение данных из файла осуществляются с помощью функций write и read. Их синтаксисы аналогичны:
int write(int handle, void *buf, unsigned len);
int read(int handle, void *buf, unsigned len);
где: handle – идентификатор файла, с которым производится операция;
buf – указывает либо данные, которые необходимо записать в файл (для write), либо данные, которые нужно считать из файла (для read);
len – определяет количество байт для записи/чтения.
В случае неудачи эти функции возвращают –1, в случае удачи – количество записанных/прочитанных байт.
Для изменения текущей позиции записи/чтения внутри файла используется функция lseek:
long lseek(int handle, long offset, int fromwhere);
где: offset – сдвиг в байтах относительно позиции, определяемой fromwhere;
fromwhere – определяет начальную позицию и может принимать значения: SEEK_SET – начало файла, SEEK_CUR – текущая позиция, SEEK_END – конец файла.
Если заранее неизвестно, сколько байт необходимо прочитать из файла, и нет уверенности в том, что не будет достигнут конец файла, можно использовать функцию eof:
int eof(int handle);,
которая возвращает 1, если достигнут конец файла, и 0 – если нет.
После окончания работы с файлом его необходимо закрыть. Отсутствие этой процедуры может привести к нежелательным последствиям: потере части данных или всего файла. Она выполняется следующим способом:
int close(int handle);
В случае удачи функция close возвращает 0, в случае неудачи –1.
Приведем пример программы, которая использует функции работы с файлами.
#include<io.h>
#include<stdio.h>
#include<string.h>
#include<sys\stat.h>
void main()
{
int handle;
char msg[] = "This is a test";
char ch;
/* создание файла */
handle = creat("a", S_IREAD|S_IWRITE);
/* запись в файл */
write(handle, msg, strlen(msg));
/* перемещение в начало файла */
lseek(handle, 0, SEEK_SET);
/* чтение до конца файла */
do
{
read(handle, &ch, 1);
printf("%c", ch);
} while (!eof(handle));
/* закрытие файла */
close(handle);
}
5.5. Использование директивы typedef
В классическом Си определенные пользователем типы данных именуются редко, за исключением структур и объединений, перед любым объявлением которых вы ставите ключевые слова struct или union.
В современном С++ обеспечивается другой уровень информационной содержательности путем использования директивы typedef. Она позволяет связать нужный тип данных (включая struct и enum) с некоторым именем, а затем объявить переменные этого типа. Далее представлен пример определения типа и определение переменных этого нового, введенного пользователем, типа данных:
typedef int *intptr;
typedef char namestr[30];
typedef enum (male, female, unknown) sex;
typedef struct
{
namestr last, first;
char ssn[9];
sex gender;
short age;
float gpa;
} student;
typedef student class[100];
class hist104,ps102;
student valedictorian;
intptr iptr;
Использование typedef делает программу более читабельной, а также позволяет вам не ограничиваться одним участком программы для определения типов данных, а распространить их определение на всю программу по мере их появления и использования в ней.
Приложение А