Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Основы программирования. Борисенко

.pdf
Скачиваний:
1531
Добавлен:
09.04.2015
Размер:
9.31 Mб
Скачать

3.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 называется ступен¬ чатой, если она обладает следующими двумя свойствами:

r =
-akj/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 по абсолютной величине не превосходит единицы. В