Текст книги "Программирование на Objective-C 2.0"
Автор книги: Стивен Кочан
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 17 (всего у книги 32 страниц)
В Foundation framework некоторые методы принимают в качестве аргумента указатель на функцию. Например, метод sortUsingFunction:context: определен в классе NSMutableArray и вызывает указанную функцию, когда требуется сравнение двух элементов массива.
Еще один применением указателей на функции является создание таблиц вызовов (dispatch table). М ы не можем сохранять сами функции в элементах мас-сива, но можем сохранять внутри массива указатели на функции. Это позволяет создавать таблицы, которые содержат указатели на вызываемые функции. Например, можно создать таблицу обработки различных команд, которые вводит пользователь. Каждая запись в таблице может содержать как имя команды, так и указатель на функцию, вызываемую для обработки этой конкретной команды. Когда пользователь вводит команду, ее можно найти в этой таблице и вызвать соответствующую функцию для ее обработки. Указатели и адреса памяти
Прежде чем закончить разговор об указателях в Objective-C, мы должны описать их реализацию. Память компьютера можно рассматривать как последовательный набор ячеек памяти. Каждая ячейка памяти компьютера имеет свой номер, называемый адресом. Обычно первый адрес памяти имеет номер 0. В большинстве компьютерных систем ячейка занимает 1 байт.
Компьютер использует память для хранения команд программы и хранения значений переменных, связанных с программой. Например, если мы объявим переменную с именем count типа int, то система выделит ячейки в памяти, чтобы сохранять значение count во время выполнения профаммы. Это может быть, например, адрес 1000FF16 в памяти компьютера.
К счастью, нам не нужно думать о конкретных адресах памяти, связанных с переменными, поскольку система делает это автоматически. Однако знание того, что каждая переменная связана со своим адресом в памяти, помогает понять, как действу ют указатели.
Когда мы применяем адресный оператор & к переменной в Objective-C, ге-нерируемое значение – это конкретный адрес данной переменной в памяти компьютера. (Именно поэтому мы называем этот оператор адресным.) Например, оператор intPtr = &count;
присваивает указателю intPtr адрес в памяти компьютера, который был выделен для переменной count. Так, если count размешена по адресу 1000FF6, этот опера-тор присвоит указателю intPtr значение OxlOOOFF.
Применение оператора косвенного доступа * к переменной-указателю, как в выражении *intPtr
означает интерпретацию значения, содержащегося в этой переменной, как адреса памяти. Значение, хранящееся по этому адресу, считывается и интерпретируется в соответствии с типом, объявленным для этой переменной. Например, если переменная intPtr является указателем на тип int, то система будет интерпретировать значение, хранящееся по адресу памяти, заданному с помощью *intPtr, как целое значение. 13.5. Объединения
Одной из наиболее необычных конструкций в языке программирования Objective-C является объединение (union). Эта конструкция используется в ос-новном для сложных программных приложений, когда требуется сохранять раз-личные типы данных в одном и том же месте памяти. Предположим, что нужно определить одну переменную с именем х, которую можно было бы использовать для хранения одного символа, числа с плавающей точкой или целого числа. Для этого можно определить объединение, например, с именем mixed. union mixed { char с; float f; int i; };
Объявление объединения совпадает со структурой (за исключением ключевого слова union). Принципиальным отличием между структурами и объединениями является способ выделения памяти. Объявление переменной типа union mixed, как в union mixed х;
не означает, что х содержит три отдельных компонента с именами с, f или i. На самом деле х содержит один компонент, который называется с, f или L Тем самым мы можем использовать переменную х для хранения элемента типа char, float или int, но не всех трех типов одновременно. Чтобы сохранить какой-либо символ в переменной х, используется оператор х.с = 'К';
Чтобы сохранить в х значение с плавающей точкой, используется форма за-писи x.f: x.f = 786.3869;
И, наконец, чтобы сохранить в х результат деления целой переменной count на 2, используется оператор x.i = count / 2;
Поскольку элементы х типа float, char и int находятся в одном месте памяти, мы можем единовременно сохранять в х только одно из этих значений. Кроме того, значение, которое считывается из объединения, должно соответствовать тому, что было в последний раз записано в это объединение.
При определении объединения имя объединения указывать не обязательно, и переменные можно объявлять одновременно с определением этого объединения. В объединениях можно объявлять указатели с таким же синтаксисом и правилами выполнения операций, как для структур. Мы можем инициализировать переменную типа union следующим образом. union mixed х = {'#'};
Первому члену х, то есть с, присваивается символ #. Определенный член объединения можно также инициализировать по имени следующим образом. union mixed х = {.f= 123.4;};
Мы можем инициализировать автоматическую union-переменную типа присвоив ей другую union-переменную того же типа.
С помощью объединения можно определять массивы для хранения элементов данных различного типа. Ниже задается массиве именем table, содержащий kTableEntries элементов. struct { char *name; int type; union { int i; float f; char c; } data; } table [kTableEntries];
Каждый элемент этого массива содержит структуру, включающую указатель на типа char с именем паше, целый компонент с именем type и union-компонент с именем data. Каждый элемент data может содержать компонент типа int, float или char. Целый компонент type позволяет следить за типом значения, сохраняемого в data. Мы можем присвоить type значение INTEGER (определенное соответствую-щим образом), если содержится значение типа int; значение FLOATING, если со-держится значение типа float; CHARACTER, если содержится значение типа char. Эта информация позволяет узнать, как обращаться к определенному компоненту структуры data определенного элемента массива.
Чтобы сохранить символ '#' в элементе table[5] и затем записать в поле type, что в этом месте хранится символ, применяются два оператора table[5].data.c = '#'; table[5].type = CHARACTER;
Во время перебора элементов table можно определять тип значения данных, хранящегося в каждом элементе, с помощью набора проверок. Например, в следующем цикле выводится имя и его значение из массива table. enum symbolType {INTEGER, FLOATING, CHARACTER }; ... for (j = 0; j < kTableEntries; ++j) { NSLog (@"%s", table[j].name); switch (table [j], type ) { case INTEGER: NSLog (@"%i", tableOl-data.i); break; case FLOATING: NSLog (@"%g", table[j].data.f); break; case CHARACTER: NSLog (@"%cn, table[j].data.c); break; default: NSLog (@"Unknown type (%i), element %i", table(j]-type, j); break; } }
На практике такое приложение можно использовать для хранения таблицы символов, содержащей имя каждого символа, его тип и его значение (и другую информацию). 13.6. Это не объекты!
Теперь вы знаете, как определять массивы, структуры, символьные строки и объединения и как работать с ними в программах. Помните главное: они не яв-ляются объектами. Мы не можем передавать им сообщения. Они не позволяют полностью использовать такие удобные возможности, как стратегию выделения памяти, которая обеспечивается в Foundation framework. Это одна из причин, по которым я советовал пропустить эту главу и вернуться к ней позже. Вы лучше подготовлены к изучению того, как использовать классы Foundation, в которых массивы и строки определяются как объекты, чем к использованию таких средств, встроенных в язык. Прибегайте к использованию типов, описанных в этой главе, только в случае реальной необходимости! 13.7. Различные средства языка
Некоторые средства языка не совсем согласуются с материалом других глав, поэтому мы включили их в этот раздел. Составные литералы
Составной литерал (compound literal) представляет собой имя типа, заключенное в круглые скобки, после которого следует список инициализации. В результате создается неименованное значение указанного типа, область действия которого ограничена блоком, в котором оно создано, или глобальной областью действия, если оно определено вне блока. В последнем случае все инициализаторы должны включать только константные выражения.
Рассмотрим следующий пример. (struct date) {.month = 7, .day = 2, .year = 2004}
Это выражение создает структуру типа struct date с указанными начальными значениями. Ее можно присвоить другой структуре типа struct date, например, theDate = (struct date} {.month = 7, .day = 2, .year = 2004};
Ее можно передать функции или методу как аргумент типа struct date, напри-мер, setStartDate ((struct date) {.month = 7, .day = 2, .year = 2004});
Кроме того, можно определять типы, отличные от структур. Например, если intPtr имеет тип int *, оператор intPtr = (int (100]) {(0) = 1, [50] = 50, [99] = 99 };
который может появиться в любом месте программы, задает intptr, указывающий на массив из 100 целых элементов, первые 3 элемента которого инициализируются указанным образом.
Если размер массива не задан, он определяется списком инициализации. Оператор goto
Оператор goto вызывает непосредственный переход в указанную точку програм-мы. Чтобы указать это место, требуется метка. Метка (label) – это имя, форми-руемое по таким же правилам, как имена переменных; сразу после него ставится двоеточие. Метка ставится непосредственно перед оператором, на который выполняется переход, и должна присутствовать в той же функции или методе, где находится соответствующий goto.
Например, оператор goto out_of_data;
вызывает переход к оператору, перед которым стоит метка out_of_data;. Эта метка должна находиться где-либо в функции или методе (до или после goto) и может использоваться, например, как показано ниже. out_of_data: NSLog (@"Unexpected end of data.");
Ленивые профаммисты часто злоупотребляют оператором goto для перехода к другим частям своего кода. Оператор goto нарушает нормальную последователь-ность программы, что затрудняет отслеживание ее выполнения. В практике на-дежною профаммирования не рекомендуется использовать операторы goto. Пустой оператор
Objective-C позволяет помещать отдельный символ «точка с запятой» в любом месте, где можно ставить обычный программный оператор. Такой оператор (его называют пустым (null) оператором) ничего не выполняем Это может показаться бесполезным, но программисты часто используют его в операторах while, for и do. Например, следующий оператор используется для сохранения всех символов, прочитанных со стандартного устройства ввода (по умолчанию это терминал), в массив символов, на который указывает text. Ввод продолжается, пока не встретится символ новой строки (перевода строки). В этом операторе ис-пользуется библиотечная процедура getchar, которая читает и возвращает по од-ному символу со стандартного устройства ввода. while ((*text++ = getchar ())!='') ;
Все операции выполняются в соответствии с условиями цикла оператора while. Здесь требуется пустой оператор, поскольку компилятор интерпретирует оператор, который следует за выражением цикла, как тело цикла. Без пустого оператора компилятор будет обрабатывать как тело цикла любой следующий программный оператор. Оператор «запятая»
На самом нижнем уровне приоритетов находится оператор «запятая». В главе 5 мы указывали, что внутри оператора for можно включать в любое из полей не-сколько выражений, отделяя их запятыми. Например, в операторе for (i = 0, j = 100; i != 10; ++i, j -= 10) ...
до начала цикла инициализируется значение i, равное 0, и j, равное 100. После выполнения тела цикла значение i увеличивается на 1 ,а из значения) вычитается 10.
Поскольку все операторы в Objective-C дают какое-то значение, значением оператора «запятая» является результат правого выражения. Оператор sizeof
В программах никогда не следует делать какие-либо предположения о размере определенного типа данных, но иногда нужно знать эту информацию – например, при выделении динамической памяти, использовании библиотечных процедур, при записи или архивации данных в файл. В Objective-C имеется оператор с именем sizeof, который можно использовать для определения размера типа данных или объекта. Оператор sizeof возвращает размер указанного элемента в байтах. Аргументом для оператора sizeof может быть переменная, имя массива, имя базового типа данных, объект, имя производного типа данных или выражение. Например, написав sizeof (int)
мы получим число байтов для сохранения целого значения. На моем Mac Book Air получается значение 4 (то есть 32 бита). Если объявить х как массив из 100 значений типа int, то выражение sizeof (х)
даст количество памяти, необходимой для сохранения 100 целых элементов массива х.
Если myFract является объектом класса Fraction, содержащим две переменных экземпляра типа int (numerator и denominator – числитель и знаменатель), то выражение sizeof (myFract)
даст значение 4 на любом компьютере, где для представления указателей ис-пользуются 4 байта. На самом деле sizeof дает это значение для любого объекта, потому что мы запрашиваем размер указателя на данные объекта. Чтобы получить размер реальной структуры данных для хранения экземпляра объекта класса Fraction, нужно написать sizeof (myFract)
На моем MacBook Air это дает значение 12. Эту сумма складывается из 4 байтов для numerator, 4 байтов для denominator, плюс еще 4 байта для наследуемого компонента isa, о котором говорится в разделе «Как это действует» в конце главы. Выражение sizeof (struct data_entry)
дает количество памяти, необходимой для хранения одной структуры data_entry. Если data определен как массив элементов struct data_entry, то выражение sizeof (data) / sizeof (struct dataentry)
дает число элементов, содержащихся в data (data должен быть заранее опреде-ленным массивом, а не формальным параметром или массивом по внешней ссылке). Выражение sizeof (data) / sizeof (data[0])
дает тот же результат.
Используйте оператор sizeof, чтобы избежать вычислений или задания фик-сированных размеров в программах. Аргументы командной строки
Довольно часто программы запрашивают на терминале пользователя ввод не-большого количества информации.
Вместо запроса такую информацию можно вводить при запуске программы. Это осуществляется с помощью аргументов командной строки (commandline arguments).
Мы уже указывали, что единственным отличительным свойством функции main является специальное имя; она указывает, где должно начинаться выполнение программы. На самом деле функцию main в начале выполнения программы вызывает система выполнения (runtime) так же, как мы вызываем функцию из своей собственной программы. Когда main заканчивает выполнение, управление передается системе runtime, которая знает, что ваша программа завершена.
Когда система runtime вызывает main, этой функции передаются два аргумен-та. Первый аргумент, который называется arge (сокращение от argument count – число аргументов), является целым значением, которое указывает число аргу-ментов, вводимых в командной строке. Второй аргумент для main – это массив указателей на символьные значения с именем argv (сокращение от argument vector). В этом массиве содержатся aigc + I символьных указателей. Первым элементом этого массива является указатель на имя выполняемой программы или указатель на нуль-строку, если имя программы недоступно. Последующие записи этого массива указывают значения, заданные в той же строке, что и команда, инициировавшая выполнение данной программы. Последний указатель массива argv, argvfargc], определен как пустой указатель.
Для доступа к аргументам командной строки функция main должна быть объявлена с двумя аргументами. Во всех программах этой книги мы использовали объявление int main (int arge, char *argv[]) { ... }
Напомним, что объявление argv определяет массив, который содержит эле-менты типа «указатель на тип char». Для практического использования аргументов командной строки предположим, что мы разрабатываем программу, которая ищет нужное слово в словаре и выводит его смысл. С помощью следующей команды можно использовать аргументы командной строки, чтобы слово, смысл которого нужно определить, было указано одновременно с запуском программы. lookup aerie
Это позволяет обойтись без запроса ввода пользователя, поскольку слово вво-дится из командной строки.
При запуске этой команды система автоматически передает функции main ука-затель на символьную строку "aerie" вагду[1]. Напомним, что argv[0] содержит указатель на имя программы (в данном случае это "lookup").
Процедура main может иметь следующий вид. #include
Процедура main при запуске программы проверяет, было ли введено слово после имени профаммы. Если не было или было введено больше одного слова, то значение arge не равно 2, программа выводит сообщение об ошибке и завершает работу, возвращая при выходе значение состояния 1.
Если значение arge равно 2, то вызывается функция lookup для поиска в сло-варе слова, на которое указывает argv[1]. Сели это слово найдено, выводится его смысл (определение).
Аргументы командной строки всегда сохраняются как символьные строки. Например, при запуске профаммы power (возведение в степень) с аргументами командной строки 2 и 16 power 2 16
в argv[1] сохраняется указатель на символьную строку "2" и в argv[2] сохраняется указатель на символьную строку "16". Если программа должна интерпретировать аргументы как числа (например, в случае программы power), их должна преобразовывать сама эта программа. Для таких преобразований в библиотеке профамм содержится несколько процедур: sscanf, atof, atoi, strtod и strotol. В части 11 вы узнаете, как использовать класс NSProcessInfo для доступа к аргументам ко-мандной строки как к строковым объектам, а не С-строкам. 13.8. Как это действует
Было бы упущением закончить эту главу без описания связей между некоторыми элементами. Поскольку в основе языка Objective-C лежит язык С, имеетсмысл описать некоторые связи между ними. Обратите внимание на эти детали реали-зации, чтобы лучше понять, как все это действует. Мы не будем здесь вдаваться в подробности, а просто приведем четыре факта о связях между Objective-C и С. Факт 1: переменные экземпляра сохраняются в структурах
Когда мы определяем новый класс и его переменные экземпляра, эти переменные на самом деле сохраняются в структуре. На самом деле это структуры, ком-понентами которых являются наши переменные экземпляра. Поэтому насле-дуемые переменные экземпляра плюс переменные экземпляра, которые мы добавляем в свой класс, представляют одну структуру. Если мы выделяем память для нового объекта с помощью alloc, резервируется достаточное пространство, чтобы включить одну из этих сфуктур.
Одним из наследуемых компонентов этой структуры (он поступает из кор-невого объекта) является защищенный компонент с именем isa, который указывает класс, которому принадлежит объект. Поскольку этот компонент является частью структуры (и гем самым частью объекта), он переносится вместе с объектом. Это позволяет системе runtime всегда идентифицировать класс объекта (даже если он присваивается обобщенной переменной-объекту типа id) по информации его компонента isa.
Чтобы получить непосредственный доступ к компонентам структуры объекта, можно объявить их как @public (см. главу 10). Если сделать это, например, для компонентов numerator и denominator класса Fraction, то можно писать в про-граммах такие выражения, как myFract->numerator
для непосредственного доступа к компоненту numerator объекта myFract класса Fraction. Но мы настоятельно рекомендуем не делать этого. Как говорилось в главе К), это противоречит основам инкапсуляции данных. Факт 2: переменная-объект на самом деле является указателем
Определяя переменную-объект класса Fraction, например, Fraction *myFract;
мы фактически определяем переменную-указатель с именем myFract. Эта пере-менная определяется для указания элемента типа Fraction (это имя нашего класса). При выделении памяти д ля нового экземпляра типа Fraction с помощью строки myFract = [Fraction alloc];
мы выделяем пространство в памяти для хранения нового объекта класса Fraction (то есть пространство для структуры) и затем сохраняем указатель на эту струк-туру, который возвращается в перемен ной-указателе myFract.
Присваивая один объект-переменную другому, как в myFract2 = myFract 1;
мы просто копируем указатели. В результате обе переменные будут указывать на одну структуру, хранящуюся в определенном месте памяти. Поэтому внесение изменений в один из компонентов, который указывается с помощью myFract2, вызывает изменения в той же переменной экземпляра (то есть в компоненте структуры), которую указывает myFractl. Факт 3: методы и функции, а также выражения с сообщениями – это вызовы функций
Методы – это на самом деле функции. При вызове метода мы вызываем функцию, связанную с классом получателя. Аргументы, передаваемые функции, это аргументы получателя (self) и метода. Поэтому все правила, касающиеся передачи аргументов функциям, возвращаемых значений, а также автоматических и статических переменных, одинаковы для функции и метода. Компилятор Objective-C создает уникальное имя для каждой функции в виде комбинации из имени класса и имени метода. Факт 4: тип id – это обобщенный тип указателя
Поскольку обращение к объектам выполняется через указатели, которые являются просто адресами памяти, мы можем легко присваивать им переменные тина id. Поэтому метод, который возвращает тип id, просто возвращает указатель на некоторый объект в памяти. Мы можем затем присвоить это значение любому объекту-переменной. Поскольку объект при его перемещении сопровождается своим компонентом isa, его класс можно всегда идентифицировать, даже если он хранится в обобщенном объекте-переменной типа id. Упражнения
Напишите функцию, которая вычисляет среднее значение 10 элементов массива с плавающей точкой и возвращает результат.
Метод reduce из класса Fraction находит наибольший общий делитель числи-теля и знаменателя (numerator и denominator) для сокращения дроби. Внесите изменения в этот метод, чтобы в нем можно было использовать функцию gcd из программы 13.5. Где следует поместить определение этой функции? Будет ли удобнее сделать эту функцию статической? Какой подход вы считаете более подходящим: использование функции gcd или включение этого кода непосредственно в метод, как мы делали это раньше? Почему?
Алгоритм, известный под названием «Решето Эратосфена», позволяет по-лучать простые числа. Ниже приводится алгоритм для этой процедуры. На-пишите программу, которая реализует этот алгоритм. Сделайте так, чтобы программа находила все простые числа до п = 150. Что вы можете сказать об этом алгоритме по сравнению с алгоритмами в этой книге для расчета простых чисел?
Шаг 1. Определить массив Р с целыми значениями. Присвоить всем элементам Pi значение 0, 2 <= i <= п. Шаг 2. Присвоить i значение 2. Шаг 3. Если i > п, алгоритм завершается. Шаг 4. Earn Pi равно 0, i – простое число. Шаг 5. Для всех положительных целых значений j, удовлетворяющих условию ixj<=n, присвоить Лх) значение 1. Шаг 6. Увеличить i на I и перейти к шагу 3.
Напишите функцию для сложения всех дробей (объектов Fraction), передава-емых ей в массиве, и возвращения результата в виде объекта Fraction.
Напишите определение typedef для структуры struct date с именем Date, чтобы в вашей программе можно было делать, например, следующие объявления. Date todaysDate;
Определение класса Date вместо структуры date больше согласуется с прин-ципами объектно-ориентированного программирования. Определите такой класс с соответствующими методами-установщиками (setter) и получателями (getter). Добавьте метод dateUpdate, чтобы возвращать день по его аргументу. Покажите преимущества определения Date в виде класса, а не в виде струк-туры.
Усматриваете ли вы какие-то недостатки?
В соответствии со следующими определениями char *message = "Программировать на Objective-C интересно"; char me$sage2[] = "Вы сказали это"; int х = 100; определите, является ли допустимым каждый вызов NSLog в следующих на-борах и является ли вывод одинаковым для всех вызовов из этого набора. /*** набор 1 ***/ NSLog {@"Программировать на Objective-C интересно’); NSLog (@"%s',l "Программировать на Objective-C интересно"); NSLog (@"%s message); /*** набор 2 ***/ NSLog (@"Вы сказали это"); NSLog (@"%s", message2); NSLog (@"%s", &message2[0j); /*** набор 3 ***/ NSLog (@"сказали это"); NSLog (@"%s", message2 + 4); NSLog (@"%s", &message2[4]);
Напишите программу, которая выводит на терминал все аргументы командной строки (по одному на строку). Обратите внимание на использование кавычек для аргументов, которые содержат пробелы.
Какие из следующих операторов дают на выходе строку "Это проверка"? Объясните результаты. NSLog {@"Это проверка"); NSLog ("Это проверка"); NSLog (@"%s", "Это проверка"); NSLog (@"%s", @"Это проверка"); NSLog ("%s", "Это проверка"); NSLog ("%s", @"Это проверка"}; NSLog (@"%@", @"Это проверка"); NSLog (@"%@", "Это проверка");