ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.10.2023
Просмотров: 427
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
134 Глава 4
цикла мы добавляем значение поля grade текущего узла к значению переменной sum
\
. Мы инкрементируем переменную count
]
, а за- тем копируем значение поля next текущего узла в указатель цикла
^
Это приводит к перемещению нашего обхода на один узел вперед.
Это достаточно сложная часть кода, так что давайте убедимся, что вы все поняли правильно. На рис. 4.15 я продемонстрировал, как меняется указатель цикла. Буквы от (a) до (г) отмечают различные моменты во время выполнения кода нашего типичного примера, де- монстрируя различные моменты времени существования указателя loopPtr
, а также источники его значений. Момент (а) представля- ет начало цикла, указатель loopPtr только что был инициирован с помощью значения указателя sc. Таким образом, loopPtr указывает на первый узел списка, также как и указатель sc. Дальше во время первой итерации цикла значение балла первого узла 78 добавляется в переменную sum. Значение поля next первого узла присваивается указателю loopPtr, то есть теперь loopPtr указывает на второй узел в списке, и это уже момент (б). Во время второй итерации мы добав- ляем 93 к значению переменной sum и копируем значение поля next второго узла в указатель loopPtr, и это момент (в). И наконец, во вре- мя третьей и последней итерации мы добавляем 85 к значению пере- менной sum и копируем NULL из поля next третьего узла в указатель loopPtr
, и это момент (г). Когда мы подходим к началу цикла while в следующий раз, цикл заканчивается, потому что указатель loopPtr содержит NULL. Так как мы инкрементировали переменную count в каждой итерации, то она равна трем.
Рис. 4.15. Изменение локальной переменной
loopPtr в итерациях цикла при вызове функ-
ции
averageRecord
Когда выполнение цикла заканчивается, мы просто делим sum на count и возвращаем результат
_
Для типичного случая код работает, но, как всегда, мы должны проверить потенциальные специальные случаи. И опять мы будем проверять самый очевидный специальный случай при работе со спис ком — пустой список . Что случится с нашим кодом, если указа- тель sc будет содержать NULL при вызове функции?
Решение задач с указателями и динамической памятью
1 ... 12 13 14 15 16 17 18 19 ... 34
135
Догадались? Код рухнет. Я просто был обязан сделать так, чтобы один из специальных случаев закончился плохо, иначе вы не будете воспринимать меня всерьез. На самом деле нет никаких проблем в ра- боте цикла, если у нас пустой связный список. Если указатель sc со- держит NULL, то указатель loopPtr инициализируется значением NULL, цикл заканчивается сразу после начала, и переменная sum остается равной нулю, что совершенно обоснованно. Проблема возникает в тот момент, когда мы совершаем действие деления для вычисления среднего значения
_
, переменная count тоже равна нулю, что означа- ет деление на ноль , которое приводит либо к обрушению программы, либо к неверному результату. Чтобы разобраться с этим специальным случаем, нужно в конце цикла проверять переменную count на нуле- вое значение. Но почему бы не разрешить эту ситуацию путем провер- ки указателя sc? Давайте добавим следующий код в качестве новой первой строки в функцию averageRecord:
if (sc == NULL) return 0;
Как мы убедились на этом примере, решать специальные случаи совсем не сложно. Мы только должны убедиться, что верно их опре- делили.
Заключение и дальнейшие шаги
Эта глава всего лишь слегка затронула задачи, которые решаются с помощью указателей и динамического распределения памяти. Вы еще столкнетесь с указателями и выделением памяти в куче по мере изучения этой книги. Например, в методах объектно ориентирован- ного программирования, которые будут рассматриваться в главе 5, использование указателей особенно полезно. Они позволяют инкап- сулировать указатели таким образом, чтобы обезопасить нас от уте- чек памяти, висячих указателей и прочих распространенных про- блем при работе с указателями.
Даже при том, что еще многое осталось неизученным в данной теме, вы вполне можете развивать свои навыки работы со струк- турами, основанными на указателях, увеличивая сложность задач, если будете следовать основным идеям, описанным в этой главе. Во- первых, применяйте основные методы решения задач. Во-вторых, применяйте специфические методы для работы с указателями и со- ставляйте схемы для визуализации каждого решения, перед тем как приступить к написанию кода.
Упражнения
Я не шучу по поводу выполнения упражнений. Вы же не собираетесь просто читать главу за главой, не так ли?
4.1.
Придумайте свое собственное упражнение: возьмите задачу, ко- торую вы умеете решать с применением массивов, но которая
136 Глава 4
ограничена размером массива. Перепишите код, сняв ограниче- ние с помощью динамических массивов.
4.2.
Создайте для нашей динамической строки функцию substring, которая получает три параметра: массив arrayString, начальную позицию в числовом формате и количество элементов подстро- ки, также в числовом формате. Функция возвращает указатель на вновь созданный динамический массив. Этот строковый мас- сив содержит символы из первоначальной строки, начиная с указанной позиции и указанной длины. Первоначальная строка остается неизменной. Так что, если изначально строка представ- ляла собой abcdefg, позиция была 3, а длина 4, то новая строка будет содержать cdef.
4.3.
Создайте для нашей динамической строки функцию replaceS- tring
, которая получает три параметра в формате arrayString: source, target и replaceText. Функция заменяет каждое появ- ление строки target в строке source на строку replaceText. На- пример, если source указывает на массив, содержащий abcdabee, target указывает на ab, а replaceText — на xyz, то после заверше- ния работы функции source будет указывать на массив, содержа- щий xyzcdxyzee
4.4.
Измените реализацию нашей строки таким образом, чтобы эле- мент массива location[0] содержал размер массива (и, соответ- ственно, элемент массива location[1] содержал первый храня- щийся в массиве символ) и уже не пришлось бы использовать нулевой завершающий байт. Реализуйте каждую из трех функ- ций append, concatenate и charactertAt, используя, насколько это возможно, преимущество сохраненной информации о раз- мере массива. Так как мы больше не будем использовать согла- шение о нулевом завершающем байте, которое необходимо для работы со стандартным потоком вывода, то вам необходимо на- писать свою собственную функцию output, которая с помощью цикла переберет строковый параметр и выведет на экран сим- волы.
4.5.
Реализуйте функцию removeRecord, которая принимает в каче- стве параметров указатель на коллекцию studentCollection и номер студента, а затем удаляет учетную карточку с этим студен- ческим номером из коллекции.
4.6.
Напишите реализацию строковой переменной, которая исполь- зует связный список для хранения символов вместо динамиче- ского массива. То есть у нас будет связный список, в котором в ка- честве полезных данных будут храниться одиночные символы.
Такая реализация позволит увеличивать размер строки без соз- дания нового массива. Вам стоит начать с реализации функций append и charactertAt.
4.7.
В продолжение предыдущего упражнения реализуйте функ- цию concatenate. Не забудьте, что, если мы вызываем функцию
concatenate(s1, s2)
, где оба параметра — это указатели на пер- вые узлы соответствующих связных списков, функция должна создавать копию каждого узла из списка s2 и добавлять ее в ко- нец списка s1, а не просто помещать указатель на первый узел списка s2 в поле next последнего узла списка s1.
4.8.
Добавьте в связный список, реализующий строковую перемен- ную, функцию removeChars, которая получает в качестве пара- метров позицию и длину, а затем удаляем подстроку символов из первоначальной строки. Например, при вызове функции removeChars(s1, 5, 3)
из строки будут удалены три символа, начиная с пятой позиции. Удостоверьтесь, что удаляемые узлы были надлежащим образом удалены из памяти.
4.9.
Представьте, что существует связный список, в узлах которого вместо символов хранятся цифры от 0 до 9 в формате int. Ис- пользуя подобный связный список, мы можем реализовать по- ложительные числа любого размера. Число 149, например, бу- дет представлено в виде связного списка, в первом узле которого будет храниться цифра 1, во втором – 4, а в третьем и послед- нем — 9. Напишите функцию intToList, которая принимает в качестве параметра число и создает подобный связный список.
Подсказка: возможно, вам покажется легче строить связный список в обратном порядке, например, если число 149, то в пер- вом узле хранится 9.
4.10.
Создайте для списка с цифрами из предыдущего упражнения функцию, которая принимает в качестве параметров два таких списка и возвращает новый список, представляющий сумму их чисел.
, где оба параметра — это указатели на пер- вые узлы соответствующих связных списков, функция должна создавать копию каждого узла из списка s2 и добавлять ее в ко- нец списка s1, а не просто помещать указатель на первый узел списка s2 в поле next последнего узла списка s1.
4.8.
Добавьте в связный список, реализующий строковую перемен- ную, функцию removeChars, которая получает в качестве пара- метров позицию и длину, а затем удаляем подстроку символов из первоначальной строки. Например, при вызове функции removeChars(s1, 5, 3)
из строки будут удалены три символа, начиная с пятой позиции. Удостоверьтесь, что удаляемые узлы были надлежащим образом удалены из памяти.
4.9.
Представьте, что существует связный список, в узлах которого вместо символов хранятся цифры от 0 до 9 в формате int. Ис- пользуя подобный связный список, мы можем реализовать по- ложительные числа любого размера. Число 149, например, бу- дет представлено в виде связного списка, в первом узле которого будет храниться цифра 1, во втором – 4, а в третьем и послед- нем — 9. Напишите функцию intToList, которая принимает в качестве параметра число и создает подобный связный список.
Подсказка: возможно, вам покажется легче строить связный список в обратном порядке, например, если число 149, то в пер- вом узле хранится 9.
4.10.
Создайте для списка с цифрами из предыдущего упражнения функцию, которая принимает в качестве параметров два таких списка и возвращает новый список, представляющий сумму их чисел.
138 Глава 5
В этой главе мы обсудим классы и объектно ориентированное про- граммирование. Как и раньше, я предполагаю, что вы уже знакомы с объ- явлением классов в языке C++ и понимаете прин- ципы синтаксиса при создании класса, вызовах методов класса и так далее. Я проведу небольшой обзор в следующем разделе, но в основном обсуж- дение коснется проблем решения задач в классах.
Это другой аспект, в котором, я считаю, язык C++ выигрывает по сравнению с другими языками. Дело в том, что C++ гибридный язык и программист на C++ может создавать классы там, где это необходимо, но совершенно не обязан этого делать. В отличие от языков Java или
+ "
5
Решение задач с классами 139
С#, весь код которых должен быть заключен в объявление класса. В ру- ках опытного программиста это не вызывает больших проблем, но в руках новичка может привести к плохим привычкам. Для программи- ста на Java или С# все является объектом. Хотя весь код, написанный на этих языках, должен быть инкапсулирован в объекты, результат не всегда отражает разумное объектно ориентированное проектирова- ние. Объект должен быть осмысленным и тесно связывать набор дан- ных и код, который работает с этими данными. Он не должен напоми- нать капризную шляпу фокусника, наполненную объедками. Поскольку мы программируем на языке C++ и, следовательно, имеем возможность выбора между процедурным и объектно ориентированным программи- рованием, то поговорим о хорошем проектировании классов, а также о том, когда следует использовать классы, а когда нет. Для достижения высокого уровня программирования существенен навык распознава- ния ситуаций, при которых классы полезны. Однако не менее важно уметь распознавать ситуации, когда классы все только портят.
Обзор основных свойств классов
Как всегда, я предполагаю, что вы знакомы с основами и правилами синтаксиса языка C++, тем не менее, давайте рассмотрим основы син- таксиса классов, чтобы мы использовали одинаковую терминологию.
Класс — схема для создания конкретного набора кода и данных. Каждая переменная, созданная согласно схеме класса, называется объектом это- го класса. Находящийся вне класса код, который создает и использу- ет объект класса, называется клиент класса. Объявление класса именует класс и содержит список всех членов или элементов, находящихся вну- три класса. Каждый элемент является либо полем класса — переменной, объявленной внутри класса, — либо методом (также известным как функ-
ция класса), то есть функцией, объявленной внутри класса. Среди функ- ций класса встречается специальный тип, называемый конструктор — эта функция имеет то же имя, что и класс, и вызывается неявно при объявлении нового объекта класса. Помимо обычных атрибутов объяв- ления переменной или функции (таких как тип и, для функции, список параметров) у каждого члена класса есть спецификатор доступа , опре- деляющий, какие функции имеют доступ к этому члену. Публичный член доступен любому коду, использующему этот объект: коду внутри класса, клиенту класса или коду в подкласс е, то есть в классе, который «наследу- ет» весь код и данные существующего класса. Приватный член доступен только коду внутри класса. Защищенные члены , которые будут кратко рас- смотрены в этой главе, похожи на приватные с единственным отличи- ем, что методы в подклассах могут также обращаться к ним. Однако как приватные, так и защищенные члены недоступны из клиентского кода.
В отличие от других атрибутов, например типа возвращаемого значения, спецификатор доступа внутри объявления класса действу- ет до тех пор, пока не будет заменен другим спецификатором. Таким образом, обычно каждый спецификатор используется только один
140 Глава 5
раз, а члены класса группируются по принципу доступа. Поэтому программисты используют термины «публичная секция» или «при- ватная секция» класса, например, так: «Мы должны поместить этот метод в приватной секции».
Давайте взглянем на крошечный пример объявления класса:
class
Xsample {
Y public:
Z sample();
[ sample(int num);
\ int doesSomething(double param);
private:
] int intData;
}
^;
Это объявление начинается с имени класса
X
и после этого sample становится именем типа. Объявление начинается с public специфи- катора доступа
Y
, и, таким образом, пока мы не достигнем private спецификатора
]
1
, все является публичным. Многие программисты ставят публичную секцию в начало, подразумевая, что публичный интерфейс более интересен другим пользователям. В данном случае публичными объявлениями являются два конструктора с названием sample
(
Z
и
[
) и метод doesSomething
\
. Эти конструкторы неявно вызываются при объявлении объекта этого класса. sample object1;
sample object2(15);
Здесь объект object1 вызовет первый конструктор
Z
, называе- мый конструктор по умолчанию . У этого конструктора нет параме- тров. А объект object2 вызовет второй конструктор
[
, поскольку он содержит одно целое значение и, таким образом, соответствует па- раметрам сигнатуры второго конструктора.
Объявление заканчивается приватным полем класса intData
]
Помните, что объявление класса заканчивается закрывающей скобкой и точкой с запятой
^
. Возможно, эта точка с запятой слегка озадачи- вает, поскольку мы не ставим их после функций, блоков инструкций if или любых других закрывающихся скобок. В действительности на- личие точки с запятой показывает, что объявление класса может быть объявлением объекта. Мы могли бы поместить идентификаторы между закрывающей скобкой и точкой с запятой и создать объекты так же, как создаем классы. Однако это не слишком распространенная практика в языке C++, особенно учитывая тот факт, что большинство программи- стов размещают объявления классов в файлы, отдельные от программ, которые их используют. Также точка с запятой появляется после закры- вающей скобки при использовании ключевого слова struct.
Говоря о ключевом слове struct, вы должны знать, что в языке
C++ слова struct и class обозначают примерно одно и то же. Един-
1
По аналогии следовало бы поставить (6) перед словом private (примеч. пер.).
Решение задач с классами 141
ственное отличие касается членов (данных или методов), объяв- ленных до первого спецификатора доступа. В struct эти члены бу- дут публичными, а в class они будут приватными. Однако хорошие программисты используют эти две структуры разными способами.
Аналогичным образом любой цикл for может быть написан как цикл while
, однако хороший программист может сделать код более чита- емым, используя циклы for для большинства циклов, считываемых вперед. Большинство программистов приберегают struct для про- стых структур, в которых либо отсутствуют поля класса кроме кон- структоров, либо в тех, которые планируется использовать в каче- стве параметров в методах больших классов.
Цели использования классов
Чтобы распознавать правильные и неправильные ситуации для ис- пользования классов, а также правильные и неправильные способы создания классов, мы сначала должны определить цели использова- ния классов. Говоря об этом, мы должны помнить, что использование классов необязательно. То есть классы не предоставляют нам новые возможности, как, например, массивы или структуры, основанные на указателях. Если вам нужна программа, которая использует массив для сортировки 10 000 записей, ее нельзя написать без массивов. Если вам нужна программа, которая, основываясь на возможностях связного списка, со временем растет или сокращается, вы не сможете добиться такого эффекта, не используя связные списки или аналогичные, ос- нованные на указателях структуры. Однако если вы уберете классы из объектно ориентированной программы и перепишете ее, программа будет выглядеть иначе, но ее возможности и эффективность не умень- шатся. В самом деле, ранние компиляторы языка C++ работали как предпроцессоры. Компилятор C++ прочитывает исходный код языка
C++ и выдает на лету новый код, соответствующий синтаксису языка
C. Этот модифицированный исходный код передается компилятору
C. Таким образом, основное дополнение, которое имеет язык C++ по сравнению с языком C, связано не с функциональными возможностя- ми языка, а с тем, как исходный код читается программистом.
Следовательно, выбирая основную цель проектирования клас- сов, мы выбираем цель помощи нам, программистам, выполнять наши задачи. В частности, поскольку эта книга посвящена решению задач, мы будем думать, как классы помогают решать эти задачи.
Инкапсуляция
Слово инкапсуляция — это причудливый способ сказать, что классы соединяют многочисленные части данных и кода в единый пакет.
Если вы когда-либо видели желатиновую медицинскую капсулу, на- полненную маленькими шариками, то это хорошая аналогия: паци- ент принимает одну капсулу и проглатывает все отдельные ингреди- енты, заключенные в ней.