Основы программирования. Борисенко
.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 по абсолютной величине не превосходит единицы. В