Основы программирования. Борисенко
.pdf3.8.2. Пример: рекурсивный  | 
	обход дерева  | 
	181  | 
||
  | 
	struct  | 
	A *q;  | 
	// Указатель на структуру A  | 
	
  | 
};  | 
	
  | 
	
  | 
	
  | 
	
  | 
Д л я  | 
	доступа  | 
	к полям структуры через указатель на  | 
	структуру  | 
|
служит операция «стрелочка», которая обозначается двумя символа
ми —> (минус  | 
	и знак  | 
	больше), их нужно рассматривать ка к одну  | 
||
неразрывную лексему (т.е. единый знак, единое слово). Пусть S —  | 
||||
имя структуры,  | 
	/ — некоторое поле структуры S , p — указатель на  | 
|||
структуру  | 
	S. Тогда выражение  | 
	
  | 
||
p->f  | 
	
  | 
	
  | 
	
  | 
	
  | 
обозначает  | 
	поле / структуры S (само поле, а не указатель не него!).  | 
|||
Это выражение  | 
	можно  | 
	записать, используя операцию «звездочка»  | 
||
(доступ к объекту через указатель),  | 
	
  | 
|||
p->f  | 
	
  | 
	(*p).f  | 
	
  | 
|
но, конечно, первый способ гораздо нагляднее. (Во втором  | 
	случае  | 
|||
круглые скобки  | 
	вокруг  | 
	выражения *p обязательны, поскольку  | 
	прио¬  | 
|
ритет операции  | 
	«точка»  | 
	выше, чем операции «звездочка».)  | 
	
  | 
|
3.8.2.Пример: рекурсивный обход дерева
В качестве примера использования указателей на структуры приведем фрагмент программы, вычисляющий количество вершин бинарного де¬ рева. Бинарным деревом называется связный граф без циклов, у которого одна вершина отме¬ чена ка к корневая, а все вершины упорядочены иерархически по длине пути от корня к вершине. У каждой вершины должно быть не больше двух сыновей, причем задан их порядок (левый и пра¬ вый сыновья).
Вершина дерева описывается структурой TreeNode , которая рас¬ сматривалась в предыдущем разделе. Если у вершины один из сы¬ новей отсутствует, то соответствующий указатель содержит нулевой адрес.
182  | 
	
  | 
	
  | 
	
  | 
	
  | 
	
  | 
	3.8.  | 
	Структуры  | 
  | 
	Д л я  | 
	подсчета  | 
	числа вершин  | 
	дерева  | 
	используем  | 
	функцию  | 
|
numNodes с прототипом  | 
	
  | 
	
  | 
	
  | 
	
  | 
|||
  | 
	int numNodes(const struct TreeNode  | 
	*root);  | 
	
  | 
	
  | 
|||
Ей  | 
	передается константный указатель на корневую  | 
	вершину дере  | 
|||||
ва  | 
	ил и поддерева.  | 
	Ф у н к ц и я возвращает суммарное  | 
	число  | 
	вершин  | 
|||
дерева  | 
	или поддерева. Эта функция  | 
	легко  | 
	реализуется с  | 
	помощью  | 
|||
рекурсии: достаточно подсчитать число вершин для каждого из двух поддеревьев, соответствующих левому и правому сыновьям корневой
вершины, сложить их и прибавить  | 
	к сумме единицу. Если левый или  | 
||||
правый сын  | 
	отсутствует, то соответствующее слагаемое равно нулю.  | 
||||
Вот фрагмент программы, реализующий функцию  | 
	numNodes.  | 
||||
// Описание структуры, представляющей вершину дерева  | 
|||||
struct TreeNode {  | 
	*parent;  | 
	// Указатель на отца,  | 
|||
struct  | 
	TreeNode  | 
||||
struct  | 
	TreeNode  | 
	* l e f t ;  | 
	//  | 
	на левого  | 
	сына,  | 
struct  | 
	TreeNode  | 
	*right;  | 
	//  | 
	на правого  | 
	сына  | 
void *value;  | 
	
  | 
	// Значение в вершине  | 
|||
};  | 
	
  | 
	
  | 
	
  | 
	
  | 
	
  | 
//Рекурсивная реализация функции,
//вычисляющей число вершин дерева.
//Вход: указатель на корень поддерева
//Возвращаемое значение: число вершин поддерева int numNodes(const struct TreeNode *root) {
int  | 
	num = 0;  | 
	
  | 
	
  | 
	
  | 
i f  | 
	(root ==0) { // Для нулевого  | 
	указателя на корень  | 
||
}  | 
	return 0;  | 
	// возвращаем ноль  | 
	
  | 
|
  | 
	
  | 
	
  | 
	
  | 
|
i f  | 
	(root->left  | 
	!= 0) {  | 
	// Есть левый  | 
	сын =>  | 
}  | 
	num += numNodes(root->left);  | 
	// вызываем функцию  | 
||
  | 
	
  | 
	// для левого  | 
	сына  | 
|
i f  | 
	(root->right  | 
	!= 0) {  | 
	// Есть правый  | 
	сын =>  | 
  | 
	num += numNodes(root->right);  | 
	// вызываем ф-цию  | 
||
}  | 
	
  | 
	
  | 
	// для правого сына  | 
|
3.8.3. Структуры и оператор определения типа typedef  | 
	183  | 
return num +1; // Возвращаем суммарное число вершин
}
Здесь неоднократно применялась операциея «стрелочка» —> для до¬ ступа к полю структуры через указатель на нее.
3.8.3.Структуры и оператор определения типа typedef
Синтаксис языка Си позволяет в одном предложении определить структуру и описать несколько переменных структурного типа. На¬ пример, строка
struct R2_point { double x; double y; } t , *p;
одновременно определяет структуру R2_point (точка на двумерной
плоскости)  | 
	и описывает  | 
	дв е переменные t  | 
	и p. Первая  | 
	имеет тип  | 
|
struct R2_point (точка  | 
	плоскости), вторая  | 
	— struct  | 
	R2_point *  | 
||
(указатель  | 
	на точку плоскости). Таким образом, после  | 
	закрывающей  | 
|||
фигурной  | 
	скобки может  | 
	идти необязательный список  | 
	определяемых  | 
||
переменных, причем можно использовать все конструкции Си дл я построения сложных типов (указатели, массивы, функции) . Список всегда завершается точкой с запятой, поэтому д а ж е при пустом спис¬ ке точка с запятой после фигурной скобки обязательна.
Возможно анонимное определение структуры, когда имя струк туры после ключевого слова struct опускается; в этом случае список описываемых переменных должен быть непустым (иначе такое опи¬ сание совершенно бессмысленно). Пример:
struct { double x; double y; } t , *p;
Здесь имя структуры отсутствует. Определены две переменные t и p, первая имеет структурный тип с полями x и y типа double, вторая — указатель на данный структурный тип. Такие описания в чистом ви¬ де программисты обычно не используют, гораздо чаще анонимное определение структуры комбинируют с оператором определения име¬ ни типа typedef (см. c. 117). Например, можно определить два типа R2Point (точка вещественной двумерной плоскости) и R2PointPtr (указатель на точку вещественной двумерной плоскости) в одном предложении, комбинируя оператор typedef с анонимным определе¬ нием структуры:
184  | 
	3.8. Структуры  | 
typedef struct { double x; double y;
} R2Point, *R2PointPtr;
Такая технология довольно популярна среди программистов и приме¬ няется в большинстве системных h-файлов. Преимущество ее состо¬ ит в том, что в дальшейшем при описании переменных структурного типа не нужно использовать ключевое слово struct , например,
R2Point  | 
	a,b , c;  | 
	// Описываем  | 
	три точки a,b, c  | 
R2PointPtr p;  | 
	// Описываем  | 
	указатель на точку  | 
|
R2Point  | 
	*q;  | 
	// Эквивалентно R2PointPtr q;  | 
|
Сравните с описаниями, использующими приведенное выше опреде¬ ление структуры R2_point:
struct R2_Point a,b, c; struct R2_Point *p; struct R2_Point *q;
Первый способ лаконичнее и нагляднее.
Вовсе не обязательно комбинировать оператор typedef непремен¬ но с анонимным определением структуры; можно в одном предложе¬ нии ка к определить имя структуры, так и ввести новый тип. Напри¬ мер, предложение
typedef struct R2_point { double x;
double y;
} R2Point, *R2PointPtr;
определяет структуру R2_point, а т а к ж е два новых типа R2Point (структура R2_point) и R2PointPtr (указатель на структуру R2_point). К сожалению, имя структуры не должно совпадать с име¬ нем типа, именно поэтому здесь в качестве имени структуры при¬ ходится использовать несколько вычурное им я R2_point. Впрочем, обычно в дальнейшем оно не нужно .
Все вышесказанное касательно языка  | 
	Си справедливо и в C + + . Кро  | 
|
ме того, в C + + считается, что определение структуры S  | 
	одновре  | 
|
менно вводит и новый ти п с именем  | 
	S . Поэтому в случае  | 
	C + + нет  | 
3.9 Технология программирования на Си  | 
	
  | 
	
  | 
	185  | 
|
необходимости в использовании  | 
	оператора  | 
	typedef при задании  | 
	струк  | 
|
турных типов. Связано это с тем, что структура с точки зрения  | 
	C + +  | 
|||
является классом, а классы и определяемые ими типы — это основа  | 
||||
языка C + + . Сравните описания  | 
	Си и C + + :  | 
	
  | 
||
struct  | 
	S { ... };  | 
	struct S { ... };  | 
	
  | 
|
struct  | 
	S a, b, c;  | 
	S  | 
	a, b, c;  | 
	
  | 
struct  | 
	S *p, *q;  | 
	S  | 
	*p, *q;  | 
	
  | 
Конечно, описания C + + проще  | 
	и нагляднее .  | 
	
  | 
||
3.9.Технология программирования на Си
Вэтом разделе будут рассмотрены некоторые приемы программи¬ рования на Си (например, реализация матриц и многомерных мас¬ сивов), а также работа с текстами и файлами при помощи функций стандартной Си-библиотеки.
3.9.1.Представление матриц и многомерных массивов
Специального типа данных «матрица» или «многомерный массив» в Си нет, однако, можно использовать массив элементов типа мас¬ сив. Например, переменная a предстваляет матрицу размера 3 х 3 с вещественными элементами:
double a[3][3];
Элементы матрицы располагаются в памяти последовательно по стро¬ кам: сначала идут элементы строки с индексом 0, затем строки с индексом 1, в конце строки с индексом 2 (в программировании от¬ счет индексов всегда начинается с нуля, а не с единицы!). При этом выражение
a[i]  | 
	
  | 
	
  | 
	
  | 
где i — целая переменная, представляет  | 
	собой указатель  | 
	на  | 
	началь  | 
ный элемент i-й строки и имеет тип double*.  | 
	
  | 
	
  | 
|
Д л я обращения к элементу матрицы  | 
	надо записать  | 
	его  | 
	индексы  | 
в квадратных скобках, например, выражение  | 
	
  | 
	
  | 
|
186 3.9. Технология программирования на Си
представляет собой элемент матрицы a в строке с индексом i и столб¬ це с индексом j . Элемент матрицы можно использовать в любом
выражении ка к обычную  | 
	переменную  | 
	(например, можно  | 
	читать его  | 
|||
значение  | 
	ил и присваивать  | 
	новое).  | 
	
  | 
	
  | 
	
  | 
|
Такая  | 
	реализация матрицы удобна  | 
	и максимально  | 
	эффективна  | 
|||
с точки  | 
	зрения  | 
	времени  | 
	доступа к элементам. У  | 
	нее только один  | 
||
существенный  | 
	недостаток: так можно  | 
	реализовать  | 
	только матрицу,  | 
|||
размер которой  | 
	известен  | 
	заранее. Язык Си не позволяет  | 
	описывать  | 
|||
массивы переменного размера, размер массива должен быть известен до начала работы программы ещ е на стадии компиляции.
Пусть нужна матрица, размер которой определяется во время ра¬ боты программы. Тогда пространство под нее надо захватывать в динамической памяти с помощью функции malloc языка Си ил и опе¬ ратора new языка C + + (см. раздел 3.7.3). При этом в динамической
памяти  | 
	захватывается  | 
	линейный массив и возвращается указатель  | 
на него.  | 
	Рассмотрим  | 
	вещественную матрицу размером m строк на  | 
n столбцов. Захват памяти выполняется с помощью функции malloc языка Си
double *a; int m, n;
a = (double *) malloc(m * n * sizeof(double));
или с помощью оператора new языка C + + :
double *a; int m, n;
a = new double[m * n];
При этом считается, что элементы матрицы будут располагаться в массиве следующим образом: сначала идут элементы строки с ин¬ дексом 0, затем элементы строки с индексом 1 и т.д., последними идут элементы строки с индексом m — 1. Каждая строка состоит из n элементов, следовательно, индекс элемента строки i и столбца j в линейном массиве равен
i • n + j
3.9.1. Представление матриц и многомерных массивов  | 
	187  | 
(действительно, поскольку индексы начинаются с нуля, то i равно количеству строк, которые нужно пропустить, i • n — суммарное ко¬ личество элементов в пропускаемых строках; число j равно смеще¬ нию внутри последней строки). Таким образом, элементу матрицы в строке i и столбце j соответствует выражение
a [i * n + j]
Этот способ представления матрицы удобен и эффективен. Его основное преимущество состоит в том, что элементы матрицы хра¬ нятся в непрерывном отрезке памяти. Во-первых, это позволяет оп¬ тимизирующему компилятору преобразовывать текст программы, до¬ биваясь максимального быстродействия; во-вторых, при выполнении программы максимально используется механизм кеш-памяти, сводя¬ щий к минимуму обращения к памяти и значительно ускоряющий работу программы.
В некоторых книгах по Си рекомендуется реализовывать матрицу ка к массив указателей на ее строки, при этом память под к а ж д у ю строку захватывается отдельно в динамической памяти:
double  | 
	**a; // Адрес массива указателей  | 
||||||
int  | 
	m,  | 
	n;  | 
	// Размеры  | 
	матрицы: m  | 
	строк, n столбцов  | 
||
int i ;  | 
	
  | 
	
  | 
	
  | 
	
  | 
	
  | 
||
//  | 
	Захватывается память  | 
	под массив  | 
	указателей  | 
||||
a  | 
	=  | 
	(double  | 
	**) malloc(m * sizeof(double * ) ) ;  | 
||||
for  | 
	( i = 0;  | 
	i < m;  | 
	
  | 
	{  | 
	
  | 
||
  | 
	
  | 
	//  | 
	Захватывается память под строку с индесом i  | 
||||
}  | 
	
  | 
	a [ i ] =  | 
	(double  | 
	*) malloc(n * sizeof(double));  | 
|||
  | 
	
  | 
	
  | 
	
  | 
	
  | 
	
  | 
	
  | 
|
После этого к элементу  | 
	можно обращаться с помощью в ы р а ж е н и я  | 
||||||
a [ i ] [ j ]
Несмотря на всю сложность этого решения, никакого выигрыша нет, наоборот, программа проигрывает в скорости! Причина состоит в том, что матрица не хранится в непрерывном участке памяти, это меша¬ ет как оптимизации программы, так и э ф ф е к т и в н о м у использованию кеш - памяти . Так что л у ч ш е не применять такой метод представления матрицы .
188  | 
	3.9. Технология программирования на Си  | 
Многомерные массивы реализуются аналогично матрицам. На пример, вещественный трехмерный массив размера 4 х 4 х 2 описы¬ вается как
double a[4][4][2];
обращение к его элементу с индексами x, y, z осуществляется с помощью выражения
a[x][y][z]
Многомерные массивы переменного размера с числом индексов боль¬ шим двух встречаются в программах довольно редко, но никаких проблем с их реализацией нет: они реализуются аналогично матри¬ цам. Например, пусть надо реализовать трехмерный вещественный массив размера m х n х к. Захватывается линейный массив веще ственных чисел размером m • n • k:
double *a;
a = (double *) malloc(m * n * k * sizeof(double));
Доступ к элементу с индексами x, y, z осуществляется с помощью выражения
a[(x * n + y) * k + z]
3.9.2.Пример: приведение матрицы к ступенчатому виду методом Гаусса
В качестве примера работы с матрицами рассмотрим алгоритм Гаусса приведения матрицы к ступенчатому виду. Метод Гаусса — один из основных результатов линейной алгебры и аналитической геометрии, к нему сводятся множество других теорем и методов ли¬ нейной алгебры (теория и вычисление определителей, решение си¬ стем линейных уравнений, вычисление ранга матрицы и обратной матрицы, теория базисов конечномерных векторных пространств и т.д.).
Напомним, что матрица A с элементами aij называется ступен¬ чатой, если она обладает следующими двумя свойствами:
190  | 
	3.9. Технология программирования на Си  | 
рода меняем местами первую и к-ю строки, добиваясь того,  | 
|
чтобы первый  | 
	элемент первой строки был отличен от нуля;  | 
2) используя элементарные преобразования второго рода, обнуля¬
ем все элементы первого столбца, начиная со второго  | 
	элемен¬  | 
|
та. Д л я этого  | 
	от строки с номером к вычитаем первую  | 
	строку,  | 
умноженную  | 
	на коэффициент a k 1 / a n  | 
	
  | 
3)переходим ко второму столбцу (или j - му , если все элементы первого столбца были нулевыми), и в дальнейшем рассматри¬ ваем только часть матрицы, начиная со второй строки и ниже. Снова повторяем пункты 1) и 2) до тех пор, пока не приведем матрицу к ступенчатому виду.
«Программистский» вариант метода Гаусса имеет три отличия от «математического»:
1)индексы строк и столбцов матрицы начинаются с нуля, а не с единицы;
2)недостаточно найти просто ненулевой элемент в столбце. В программировании все действия с вещественными числами производятся приближенно, поэтому можно считать, что точно¬ го равенства вещественных чисел вообще не бывает. Некоторые компиляторы д а ж е выдают предупреждения на к а ж д у ю опера¬ цию проверки равенства вещественных чисел. Поэтому вместо проверки на равенство нулю числа aij следует сравнивать его
абсолютную величину |ai j | с очень маленьким числом е (напри мер, е = 0.0000001). Если |ai j| < е, то следует считать элемент aij нулевым;
3) при обнулении элементов j - г о столбца, начиная со строки i + 1, мы к k-й строке, где к > i, прибавляем i - ю строку, умноженную на коэффициент r =
-akj/aij
Такая схема работает нормально только тогда, когда коэффи¬ циент r по абсолютной величине не превосходит единицы. В
