НА ГЛАВНУЮ
Меню сайта
Категория
Ghost++ [1]
С++ [55]
Развлечение
ON - LINE
Опрос
На какие on-line игры вы тратите больше времени?
Всего ответов: 247
Оbserver Ward

Онлайн всего: 1
Гостей: 1
Пользователей: 0


Друзья сайта
Заведи себе Бота
Hаша кнопка
Для обмена банерами , наша кнопка для размещения у вас на сайте

Клансайт USSR


Главная » Статьи » Программирование » С++

7. Функции (2)
7.3.6. Многоточие

Иногда нельзя перечислить типы и количество всех возможных аргументов функции. В этих случаях список параметров представляется многоточием (...), которое отключает механизм проверки типов. Наличие многоточия говорит компилятору, что у функции может быть произвольное количество аргументов неизвестных заранее типов. Многоточие употребляется в двух форматах:

void foo( parm_list, ... );
void foo( ... );

Первый формат предоставляет объявления для части параметров. В этом случае проверка типов для объявленных параметров производится, а для оставшихся фактических аргументов – нет. Запятая после объявления известных параметров необязательна.
Примером вынужденного использования многоточия служит функция printf() стандартной библиотеки С. Ее первый параметр является C-строкой:

int printf( const char* ... );

Это гарантирует, что при любом вызове printf() ей будет передан первый аргумент типа const char*. Содержание такой строки, называемой форматной, определяет, необходимы ли дополнительные аргументы при вызове. При наличии в строке формата метасимволов, начинающихся с символа %, функция ждет присутствия этих аргументов. Например, вызов

printf( "hello, world\n" );

имеет один строковый аргумент. Но

printf( "hello, %s\n", userName );

имеет два аргумента. Символ % говорит о наличии второго аргумента, а буква s, следующая за ним, определяет его тип – в данном случае символьную строку.
Большинство функций с многоточием в объявлении получают информацию о типах и количестве фактических параметров по значению явно объявленного параметра. Следовательно, первый формат многоточия употребляется чаще.
Отметим, что следующие объявления неэквивалентны:

void f();
void f( ... );

В первом случае f() объявлена как функция без параметров, во втором – как имеющая ноль или более параметров. Вызовы

f( someValue );
f( cnt, a, b, с );

корректны только для второго объявления. Вызов

f();

применим к любой из двух функций.
Упражнение 7.4

Какие из следующих объявлений содержат ошибки? Объясните.
(a) void print( int arr[][], int size );
(b) int ff( int a, int b = 0, int с = 0 );
(c) void operate( int *matrix[] );
(d) char *screenInit( int height = 24, int width,
char background );
(e) void putValues( int (&ia)[] );
Упражнение 7.5

Повторные объявления всех приведенных ниже функций содержат ошибки. Найдите их.
(a) char *screenInit( int height, int width,
char background = ' ' );
char *screenInit( int height = 24, int width,
char background );

(b) void print( int (*arr)[6], int size );
void print( int (*arr)[5], int size );

(c) void manip( int *pi, int first, int end = 0 );
void manip( int *pi, int first = 0, int end = 0 );
Упражнение 7.6

Даны объявления функций.
void print( int arr[][5], int size );
void operate(int *matrix[7]);
char *screenInit( int height = 24, int width = 80,
char background = ' ' );
Вызовы этих функций содержат ошибки. Найдите их и объясните.
(a) screenInit();

(b) int *matrix[5];
operate( matrix );

(c) int arr[5][5];
print( arr, 5 );
Упражнение 7.7

Перепишите функцию putValues( vector<int> ), приведенную в подразделе 7.3.4, так, чтобы она работала с контейнером list<string>. Печатайте по одному значению на строке. Вот пример вывода для списка из двух строк:

( 2 )
<
"first string"
"second string"
>

Напишите функцию main(), вызывающую новый вариант putValues() со следующим списком строк:

"put function declarations in header files"
"use abstract container types instead of built-in arrays"
"declare class parameters as references"
"use reference to const types for invariant parameters"
"use less than eight parameters"

Упражнение 7.8

В каком случае вы применили бы параметр-указатель? А в каком – параметр-ссылку? Опишите достоинства и недостатки каждого способа.
7.4. Возврат значения

В теле функции может встретиться инструкция return. Она завершает выполнение функции. После этого управление возвращается той функции, из которой была вызвана данная. Инструкция return может употребляться в двух формах:

return;
return expression;

Первая форма используется в функциях, для которых типом возвращаемого значения является void. Использовать return в таких случаях обязательно, если нужно принудительно завершить работу. (Такое применение return напоминает инструкцию break, представленную в разделе 5.8.) После конечной инструкции функции подразумевается наличие return. Например:

void d_copy( double "src, double *dst, int sz )
{
    /* копируем массив "src" в "dst"
    * для простоты предполагаем, что они одного размера
    */

    // завершение, если хотя бы один из указателей равен 0
    if ( !src || !dst )
        return;

    // завершение,
    // если указатели адресуют один и тот же массив
    if ( src == dst )
        return;

    // копировать нечего
    if ( sz == 0 )
        return;

    // все еще не закончили?
    // тогда самое время что-то сделать
    for ( int ix = 0; ix < sz; ++ix )
        dst[ix] = src[ix];

    // явного завершения не требуется
}

Во второй форме инструкции return указывается то значение, которое функция должна вернуть. Это значение может быть сколь угодно сложным выражением, даже содержать вызов функции. В реализации функции factorial(), которую мы рассмотрим в следующем разделе, используется return следующего вида:
return val * factorial(val-1);
В функции, не объявленная с void в качестве типа возвращаемого значения, обязательно использовать вторую форму return, иначе произойдет ошибка компиляции. Хотя компилятор не отвечает за правильность результата, он сможет гарантировать его наличие. Следующая программа не компилируется из-за двух мест, где программа завершается без возврата значения:

// определение интерфейса класса Matrix
#include "Matrix.h"

bool is_equa1( const Matrix &ml, const Matrix &m2 )
{
    /* Если содержимое двух объектов Matrix одинаково,
    *  возвращаем true;
    *  в противном случае - false
    */

    // сравним количество столбцов
    if ( ml.colSize() != m2.co1Size() )
        // ошибка: нет возвращаемого значения
        return;

    // сравним количество строк
    if ( ml.rowSize() != m2.rowSize() )
        // ошибка: нет возвращаемого значения
        return;

    // пробежимся по обеим матрицам, пока
    // не найдем неравные элементы
    for ( int row = 0; row < ml.rowSize(); ++row )
        for ( int col = 0; co1 < ml.colSize(); ++co1 )
           if ( ml[row][col] != m2[row][col] )
               return false;
    // ошибка: нет возвращаемого значения
    // для случая равенства
}

Если тип возвращаемого значения не точно соответствует указанному в объявлении функции, то применяется неявное преобразование типов. Если же стандартное приведение невозможно, происходит ошибка компиляции. (Преобразования типов рассматривались в разделе 4.1.4.)
По умолчанию возвращаемое значение передается по значению, т.е. вызывающая функция получает копию результата вычисления выражения, указанного в инструкции return. Например:

Matrix grow( Matrix* p ) {
   Matrix val;
   // ...
   return val;
}

grow() возвращает вызывающей функции копию значения, хранящегося в переменной val.
Такое поведение можно изменить, если объявить, что возвращается указатель или ссылка. При возврате ссылки вызывающая функция получает l-значение для val и потому может модифицировать val или взять ее адрес. Вот как можно объявить, что grow() возвращает ссылку:

Matrix& grow( Matrix* p ) {
   Matrix *res;
   // выделим память для объекта Matrix
   // большого размера
   // res адресует этот новый объект
   // скопируем содержимое *p в *res
   return *res;
}

Если возвращается большой объект, то гораздо эффективнее перейти от возврата по значению к использованию ссылки или указателя. В некоторых случаях компилятор может сделать это автоматически. Такая оптимизация получила название именованное возвращаемое значение. (Она описывается в разделе 14.8.)
Объявляя функцию как возвращающую ссылку, программист должен помнить о двух возможных ошибках:
• возврат ссылки на локальный объект, время жизни которого ограничено временем выполнения функции. (О времени жизни локальных объектов речь пойдет в разделе 8.3.) По завершении функции такой ссылке соответствует область памяти, содержащая неопределенное значение. Например:

// ошибка: возврат ссылки на локальный объект
Matrix& add( Matrix &m1, Matrix &m2 )
{
   Matrix result:
   if ( m1.isZero() )
     return m2;
   if ( m2.isZero() )
     return m1;
// сложим содержимое двух матриц
// ошибка: ссылка на сомнительную область памяти
// после возврата
   return result;
}

В таком случае тип возврата не должен быть ссылкой. Тогда локальная переменная может быть скопирована до окончания времени своей жизни:
Matrix add( ... )

• функция возвращает l-значение. Любая его модификация затрагивает сам объект. Например:

#include <vector>

int &get_val( vector<int> &vi, int ix ) {
return vi [ix];
}

int ai[4] = { 0, 1, 2, 3 };
vector<int> vec( ai, ai+4 ); // копируем 4 элемента ai в vec

int main() {
// увеличивает vec[0] на 1
get_val( vec.0 )++;
// ...
}
Для предотвращения нечаянной модификации возвращенного объекта нужно объявить тип возврата как const:
const int &get_val( ... )
Примером ситуации, когда l-значение возвращается намеренно, чтобы позволить модифицировать реальный объект, может служить перегруженный оператор взятия индекса для класса IntArray из раздела 2.3.
7.4.1. Передача данных через параметры и через глобальные объекты

Различные функции программы могут общаться между собой с помощью двух механизмов. (Под словом "общаться” мы подразумеваем обмен данными.) В одном случае используются глобальные объекты, в другом – передача параметров и возврат значений.
Глобальный объект определен вне функции. Например:

int glob;
   int main() {
   // что угодно
}

Объект glob является глобальным. (В главе 8 рассмотрение глобальных объектов и глобальной области видимости будет продолжено.) Главное достоинство и одновременно один из наиболее заметных недостатков такого объекта – доступность из любого места программы, поэтому его обычно используют для общения между разными модулями. Обратная сторона медали такова:

    * функции, использующие глобальные объекты, зависят от этих объектов и их типов. Использовать такую функцию в другом контексте затруднительно;
    * при модификации такой программы повышается вероятность ошибок. Даже для внесения локальных изменений необходимо понимание всей программы в целом;
    * если глобальный объект получает неверное значение, ошибку нужно искать по всей программе. Отсутствует локализация;
    * используя глобальные объекты, труднее писать рекурсивные функции (Рекурсия возникает тогда, когда функция вызывает сама себя. Мы рассмотрим это в разделе 7.5.);
    * если используются потоки (threads), то для синхронизации доступа к глобальным объектам требуется писать дополнительный код. Отсутствие синхронизации – одна из распространенных ошибок при использовании потоков. (Пример использования потоков при программировании на С++ см. в статье "Distributing Object Computing in C++” (Steve Vinoski and Doug Schmidt) в [LIPPMAN96b].)

Можно сделать вывод, что для передачи информации между функциями предпочтительнее пользоваться параметрами и возвращаемыми значениями.
Вероятность ошибок при таком подходе возрастает с увеличением списка. Считается, что восемь параметров – это приемлемый максимум. В качестве альтернативы длинному списку можно использовать в качестве параметра класс, массив или контейнер. Он способен содержать группу значений.
Аналогично программа может возвращать только одно значение. Если же логика требует нескольких, некоторые параметры объявляются ссылками, чтобы функция могла непосредственно модифицировать значения соответствующих фактических аргументов и использовать эти параметры для возврата дополнительных значений, либо некоторый класс или контейнер, содержащий группу значений, объявляется типом, возвращаемым функцией.
Упражнение 7.9

Каковы две формы инструкции return? Объясните, в каких случаях следует использовать первую, а в каких вторую форму.
Упражнение 7.10

Найдите в данной функции потенциальную ошибку времени выполнения:

vector<string> &readText( ) {
  vector<string> text;

  string word;
  while ( cin >> word ) {
    text.push_back( word );

    // ...
  }

  // ....
  return text;
}

Упражнение 7.11

Каким способом вы вернули бы из функции несколько значений? Опишите достоинства и недостатки вашего подхода
7.5. Рекурсия

Функция, которая прямо или косвенно вызывает сама себя, называется рекурсивной. Например:

int rgcd( int vl, int v2 )
{
   if ( v2 != 0 )
     return rgcd( v2, vl%v2 );
   return vl;
}

Такая функция обязательно должна определять условие окончания, в противном случае рекурсия будет продолжаться бесконечно. Подобную ошибку так иногда и называют – бесконечная рекурсия. Для rgcd() условием окончания является равенство нулю остатка.
Вызов

rgcd( 15, 123 );

возвращает 3 (см. табл. 7.1).

Таблица 7.1. Трассировка вызова rgcd (15,123)
v1     v2     return
15     123     rgcd(123,15)
123     15     rgcd(15,3)
15     3     rgcd(3,0)
3     0     3

Последний вызов,

rgcd(3,0);

удовлетворяет условию окончания. Функция возвращает наибольший общий делитель, он же возвращается и каждым предшествующим вызовом. Говорят, что значение всплывает (percolates) вверх, пока управление не вернется в функцию, вызвавшую rgcd() в первый раз.
Рекурсивные функции обычно выполняются медленнее, чем их нерекурсивные (итеративные) аналоги. Это связано с затратами времени на вызов функции. Однако, как правило, они компактнее и понятнее.
Приведем пример. Факториалом числа n является произведение натуральных чисел от 1 до n. Так, факториал 5 равен 120: 1 ? 2 ? 3 ? 4 ? 5 = 120.
Вычислять факториал удобно с помощью рекурсивной функции:

unsigned long
factorial( int val ) {
   if ( val > 1 )
     return val * factorial( val-1 );
   return 1;
}

Рекурсия обрывается по достижении val значения 1.
Упражнение 7.12

Перепишите factorial() как итеративную функцию.
Упражнение 7.13

Что произойдет, если условием окончания factorial() будет следующее:

if ( val != 0 )

7.6. Встроенные функции

Рассмотрим следующую функцию min():

int min( int vl, int v2 )
{
   return( vl < v2 ? vl : v2 );
}

Преимущества определения функции для такой небольшой операции таковы:

    * как правило, проще прочесть и интерпретировать вызов min(), чем читать условный оператор и вникать в смысл его действий, особенно если v1 и v2 являются сложными выражениями;
    * модифицировать одну локализованную реализацию в приложении легче, чем 300. Например, если будет решено изменить проверку на:
      ( vl == v2 || vl < v2 )
      поиск каждого ее вхождения будет утомительным и с большой долей вероятности приведет к ошибкам;
    * семантика единообразна. Все проверки выполняются одинаково;
    * функция может быть повторно использована в другом приложении.

Однако этот подход имеет один недостаток: вызов функции происходит медленнее, чем непосредственное вычисление условного оператора. Необходимо скопировать два аргумента, запомнить содержимое машинных регистров и передать управление в другое место программы. Решение дают встроенные функции. Встроенная функция "подставляется по месту” в каждой точке своего вызова. Например:

int minVa12 = min( i, j );

заменяется при компиляции на

int minVal2 = i < j ? i : j;

Таким образом, не требуется тратить время на реализацию min() в виде функции.
Функция min() объявляется как встроенная с помощью ключевого слова inline перед типом возвращаемого значения в объявлении или определении:

inline int min( int vl, int v2 ) { /* ... */ }

Заметим, однако, что спецификация inline – это только подсказка компилятору. Компилятор может проигнорировать ее, если функция плохо подходит для встраивания по месту. Например, рекурсивная функция (такая, как rgcd()) не может быть полностью встроена в месте вызова (хотя для самого первого вызова это возможно). Функция из 1200 строк также скорее всего не подойдет. В общем случае такой механизм предназначен для оптимизации небольших, простых, часто используемых функций. Он крайне важен для поддержки концепции сокрытия информации при разработке абстрактных типов данных. Например, встроенной объявлена функция-член size() в классе IntArray из раздела 2.3.

Встроенная функция должна быть видна компилятору в месте вызова. В отличие от обычной, такая функция определяется в каждом исходном файле, где есть обращения к ней. Конечно же, определения одной и той же встроенной функции в разных файлах должны совпадать. Если программа содержит два исходных файла compute.C и draw.C, не нужно писать для них разные реализации функции min(). Если определения функции различаются, программа становится нестабильной: неизвестно, какое из них будет выбрано для каждого вызова, если компилятор не стал встраивать эту функцию.

Рекомендуется помещать определение встроенной функции в заголовочный файл и включать его во все файлы, где есть обращения к ней. Такой подход гарантирует, что для встроенной функции существует только одно определение и код не дублируется; дублирование может привести к непреднамеренному расхождению текстов в течение жизненного цикла программы.
Поскольку min() является общеупотребительной операцией, реализация ее входит в стандартную библиотеку С++; это один из обобщенных алгоритмов, описанных в главе 12 и в Приложении. Функция min() реализована как шаблон, что позволяет ей работать с операндами арифметического типа, отличного от int. (Шаблоны функций рассматриваются в главе 10.)
7.7. Директива связывания extern "C" A

Если программист хочет использовать функцию, написанную на другом языке, в частности на С, то компилятору нужно указать, что при вызове требуются несколько иные условия. Скажем, имя функции или порядок передачи аргументов различаются в зависимости от языка программирования.
Показать, что функция написана на другом языке, можно с помощью директивы связывания в форме простой либо составной инструкции:

// директива связывания в форме простой инструкции
extern "C" void exit(int);
// директива связывания в форме составной инструкции
extern "C" {
  int printf( const char* ... );
  int scanf( const char* ... );
}
// директива связывания в форме составной инструкции
extern "C" {
  #include <cmath>
 }

Первая форма такой директивы состоит из ключевого слова extern, за которым следует строковый литерал, а за ним – "обычное” объявление функции. Хотя функция написана на другом языке, проверка типов вызова выполняется полностью. Несколько объявлений функций могут быть помещены в фигурные скобки составной инструкции директивы связывания – второй формы этой директивы. Скобки отмечают те объявления, к которым она относится, не ограничивая их видимости, как в случае обычной составной инструкции. Составная инструкция extern "C" в предыдущем примере говорит только о том, что функции printf() и scanf() написаны на языке С. Во всех остальных отношениях эти объявления работают точно так же, как если бы они были расположены вне инструкции.

Если в фигурные скобки составной директивы связывания помещается директива препроцессора #include, все объявленные во включаемом заголовочном файле функции рассматриваются как написанные на языке, указанном в этой директиве. В предыдущем примере все функции из заголовочного файла cmath написаны на языке С.
Директива связывания не может появиться внутри тела функции. Следующий фрагмент кода вызывает ошибку компиляции:

int main() {
    // ошибка: директива связывания не может появиться
    // внутри тела функции
    extern "C" double sqrt( double );
    double getValue(); //правильно

    double result = sqrt ( getValue() );
    //...
    return 0;
}

Если мы переместим директиву так, чтобы она оказалась вне тела main(), программа откомпилируется правильно:

extern "C" double sqrt( double );
int main() {
   double getValue(); //правильно
   double result = sqrt ( getValue() );

  //...
  return 0;
 }

Однако более подходящее место для директивы связывания – заголовочный файл, где находится объявление функции, описывающее ее интерфейс.
Как сделать С++ функцию доступной для программы на С? Директива extern "C" поможет и в этом:

// функция calc() может быть вызвана из программы на C
extern "C" double calc( double dparm ) { /* ... */ }
Если в одном файле имеется несколько объявлений функции, то директива связывания может быть указана при каждом из них или только при первом – в этом случае она распространяется и на все последующие объявления. Например:
// ---- myMath.h ----
extern "C" double calc( double );

// ---- myMath.C ----
// объявление calc() в myMath.h
#include "myMath.h"

// определение функции extern "C" calc()
// функция calc() может быть вызвана из программы на C
double calc( double dparm ) { // ... }

В данном разделе мы видели примеры директивы связывания extern "C" только для языка С. Это единственный внешний язык, поддержку которого гарантирует стандарт С++. Конкретная реализация может поддерживать связь и с другими языками. Например, extern "Ada" для функций, написанных на языке Ada; extern "FORTRAN" для языка FORTRAN и т.д. Мы описали один из случаев использования ключевого слова extern в С++. В разделе 8.2 мы покажем, что это слово имеет и другое назначение в объявлениях функций и объектов.

Упражнение 7.14
exit(), printf(), malloc(), strcpy() и strlen() являются функциями из библиотеки С. Модифицируйте приведенную ниже С-программу так, чтобы она компилировалась и связывалась в С++.
const char *str = "hello";

void *malloc( int );
char *strcpy( char *, const char * );
int printf( const char *, ... );
int exit( int );
int strlen( const char * );

int main()
{ /* программа на языке С */

char* s = malloc( strlen(str)+l );
strcpy( s, str );
printf( "%s, world\n", s );
exit( 0 );
}
7.8. Функция main(): разбор параметров командной строки

При запуске программы мы, как правило, передаем ей информацию в командной строке. Например, можно написать

prog -d -o of lie dataO

Фактические параметры являются аргументами функции main() и могут быть получены из массива C-строк с именем argv; мы покажем, как их использовать.
Во всех предыдущих примерах определение main() содержало пустой список:

int main() { ... }

Развернутая сигнатура main() позволяет получить доступ к параметрам, которые были заданы пользователем в командной строке:

int main( int argc, char *argv[] ){...}

argc содержит их количество, а argv – C-строки, представляющие собой отдельные значения (в командной строке они разделяются пробелами). Скажем, при запуске команды
prog -d -o ofile data0
argc получает значение 5, а argv включает следующие строки:

argv[ 0 ] = "prog";
argv[ 1 ] = "-d";
argv[ 2 ] = "-o";
argv[ 3 ] = "ofile";
argv[ 4 ] = "dataO";

В argv[0] всегда входит имя команды (программы). Элементы с индексами от 1 до argc-1 служат параметрами.
Посмотрим, как можно извлечь и использовать значения, помещенные в argv. Пусть программа из нашего примера вызывается таким образом:

prog [-d] [-h] [-v]
[-o output_file] [-l limit_value]
file_name
[ file_name [file_name [ ... ]]]

Параметры в квадратных скобках являются необязательными. Вот, например, запуск программы с их минимальным количеством – одним лишь именем файла:
prog chap1.doc
Но можно запускать и так:

prog -l 1024 -o chap1-2.out chapl.doc chap2.doc
prog d chap3.doc
prog -l 512 -d chap4.doc

При разборе параметров командной строки выполняются следующие основные шаги:

   1. По очереди извлечь каждый параметр из argv. Мы используем для этого цикл for с начальным индексом 1 (пропуская, таким образом, имя программы):

      for ( int ix = 1; ix < argc; ++ix ) {
           char *pchar = argv[ ix ];
           // ...
      }

   2. Определить тип параметра. Если строка начинается с дефиса (-), это одна из опций { h, d, v, l, o}. В противном случае это может быть либо значение, ассоциированное с опцией (максимальный размер для -l, имя выходного файла для -o), либо имя входного файла. Чтобы определить, начинается ли строка с дефиса, используем инструкцию switch:

      switch ( pchar[ 0 ] ) {
           case '-': {
             // -h, -d, -v, -l, -o
           }
          default: {
            // обработаем максимальный размер для опции -1
            // имя выходного файла для -o
            // имена входных файлов ...
          }
      }

Реализуем обработку двух случаев пункта 2.
Если строка начинается с дефиса, мы используем switch по следующему символу для определения конкретной опции. Вот общая схема этой части программы:

case '-': {
  switch( pchar[ 1 ] )
  {
    case 'd':
      // обработка опции debug
      break;

    case 'v':
      // обработка опции version
      break;

    case 'h':
      // обработка опции help
      break;

    case 'o':
      // приготовимся обработать выходной файл
      break;

    case 'l':
      // приготовимся обработать макс.размер
      break;

    default:
      // неопознанная опция:
      // сообщить об ошибке и завершить выполнение
  }
}

Опция -d задает необходимость отладки. Ее обработка заключается в присваивании переменной с объявлением

bool debug_on = false;

значения true:

case 'd':
   debug_on = true;
   break;

В нашу программу может входить код следующего вида:

if ( debug_on )
   display_state_elements( obj );

Опция -v выводит номер версии программы и завершает исполнение:

case 'v':
   cout << program_name << "::"
         << program_version << endl;
   return 0;

Опция -h запрашивает информацию о синтаксисе запуска и завершает исполнение. Вывод сообщения и выход из программы выполняется функцией usage():

case 'h':
   // break не нужен: usage() вызывает exit()
   usage();

Опция -o сигнализирует о том, что следующая строка содержит имя выходного файла. Аналогично опция -l говорит, что за ней указан максимальный размер. Как нам обработать эти ситуации?
Если в строке параметра нет дефиса, возможны три варианта: параметр содержит имя выходного файла, максимальный размер или имя входного файла. Чтобы различать эти случаи, присвоим true переменным, отражающим внутреннее состояние:

// если ofi1e_on==true,
// следующий параметр - имя выходного файла
bool ofi1e_on = false;

// если ofi1e_on==true,
// следующий параметр - максимальный размер
bool limit_on = false;

Вот обработка опций -l и -o в нашей инструкции switch:

case 'l':
   limit_on = true;
   break;
case 'o':
  ofile_on = true;
  break;

Встретив строку, не начинающуюся с дефиса, мы с помощью переменных состояния можем узнать ее содержание:

// обработаем максимальный размер для опции -1
//            имя выходного файла для       -o
//            имена входных файлов ...
default: {
   // ofile_on включена, если -o встречалась
   if ( ofile_on ) {
     // обработаем имя выходного файла
     // выключим ofile_on
   }
   else if ( limit_on ) { // если -l встречалась
     // обработаем максимальный размер
     // выключим limit_on
   } else {
     // обработаем имя входного файла
   }
}

Если аргумент является именем выходного файла, сохраним это имя и выключим ofile_on:

if ( ofile_on ) {
   ofile_on = false;
   ofile = pchar;
}

Если аргумент задает максимальный размер, мы должны преобразовать строку встроенного типа в представляемое ею число. Сделаем это с помощью стандартной функции atoi(), которая принимает строку в качестве аргумента и возвращает int (также существует функция atof(), возвращающая double). Для использования atoi() включим заголовочный файл ctype.h. Нужно проверить, что значение максимального размера неотрицательно и выключить limit_on:

// int limit;
else
if ( limit_on ) {
  limit_on = false;
  limit = atoi( pchar );
  if ( limit < 0 ) {
    cerr << program_name << "::"
         << program_version << " : error: "
         << "negative value for limit.\n\n";
    usage( -2 );
  }
}

Если обе переменных состояния равны false, у нас есть имя входного файла. Сохраним его в векторе строк:

else
file_names.push_back( string( pchar ));

При обработке параметров командной строки важен способ реакции на неверные опции. Мы решили, что задание отрицательной величины в качестве максимального размера будет фатальной ошибкой. Это приемлемо или нет в зависимости от ситуации. Также можно распознать эту ситуацию как ошибочную, выдать предупреждение и использовать ноль или какое-либо другое значение по умолчанию.
Слабость нашей реализации становится понятной, если пользователь небрежно относится к пробелам, разделяющим параметры. Скажем, ни одна из следующих двух строк не будет обработана:
prog - d dataOl
prog -oout_file dataOl
(Оба случая мы оставим для упражнений в конце раздела.)
Вот полный текст нашей программы. (Мы добавили инструкции печати для трассировки выполнения.)

#include < iostream>

#include < string>
#include < vector>

#include < ctype.h>

const char *const program_name = "comline";
const char *const program_version = "version 0.01 (08/07/97)";

inline void usage( int exit_value = 0 )
{
  // печатает отформатированное сообщение о порядке вызова
  // и завершает программу с кодом exit_value ...

  cerr << "порядок вызова:\n"
       << program_name << " "
       << "[-d] [-h] [-v] \n\t"
       << "[-o output_file] [-l limit] \n\t"
       << "file_name\n\t[file_name [file_name [ ... ]]]\n\n"
       << "где [] указывает на необязательность опции:\n\n\t"
       << "-h: справка.\n\t\t"
       << "печать этого сообщения и выход\n\n\t"
       << "-v: версия.\n\t\t"
       << "печать информации о версии программы и выход\n\n\t"
       << "-d: отладка.\n\t\t включает отладочную печать\n\n\t"
       << "-l limit\n\t\t"
       << "limit должен быть неотрицательным целым числом\n\n\t"
       << "-o ofile\n\t\t"
       << "файл, в который выводится результат\n\t\t"
       << "по умолчанию результат записывается на стандартный вывод\n\n"
       << "file_name\n\t\t"
       << "имя подлежащего обработке файла\n\t\t"
       << "должно быть задано хотя бы одно имя --\n\t\t"
       << "но максимальное число не ограничено\n\n"
       << "примеры:\n\t\t"
       << "$command chapter7.doc\n\t\t"
       << "$command -d -l 1024 -o test_7_8 "
       << "chapter7.doc chapter8.doc\n\n";

  exit( exit_value );
}

int main( int argc, char* argv[] )
{
  bool debug_on = false;
  bool ofile_on = false;
  bool limit_on = false;
  int limit = -1;
  string ofile;
  vector file_names;

  cout << "демонстрация обработки параметров в командной строке:\n"
       << "argc: " << argc << endl;

  for ( int ix = 1; ix < argc; ++ix )
  {
    cout << "argv[ " << ix << " ]: "
         << argv[ ix ] << endl;

    char *pchar = argv[ ix ];
    switch ( pchar[ 0 ] )
    {
      case '-':
      {
        cout << "встретился \'-\'\n";
        switch( pchar[ 1 ] )
        {
          case 'd':
            cout << "встретилась -d: "
                 << "отладочная печать включена\n";

            debug_on = true;
            break;

          case 'v':
            cout << "встретилась -v: "
                 << "выводится информация о версии\n";

            cout << program_name
                 << " ::  "
                 << program_version
                 << endl;

            return 0;

          case 'h':
            cout << "встретилась -h: "
                 << "справка\n";

            // break не нужен: usage() завершает программу
            usage();

          case 'o':
            cout << "встретилась -o: выходной файл\n";
            ofile_on = true;
            break;
          case 'l':
            cout << "встретилась -l: "
                 << "ограничение ресурса\n";

            limit_on = true;
            break;

          default:
            cerr << program_name
                 << " : ошибка : "
                 << "неопознанная опция: - "
                 << pchar << "\n\n";

            // break не нужен: usage() завершает программу
            usage( -1 );
        }
        break;
      }
      default: // либо имя файла
        cout << "default: параметр без дефиса: "
             << pchar << endl;

        if ( ofile_on ) {
          ofile_on = false;
          ofile = pchar;
        }
        else
        if ( limit_on ) {
          limit_on = false;
          limit = atoi( pchar );
          if ( limit < 0 ) {
            cerr << program_name
                 << " : ошибка : "
                 << "отрицательное значение limit.\n\n";

            usage( -2 );
          }
        }
        else file_names.push_back( string( pchar ));
        break;
    }
  }

  if ( file_names.empty() ) {
    cerr << program_name
         << " : ошибка : "
         << "не задан ни один входной файл.\n\n";
    usage( -3 );
  }

  if ( limit != -1 )
    cout << "Заданное пользователем значение limit: "
         << limit << endl;

  if ( ! ofile.empty() )
    cout << "Заданный пользователем выходной файл: "
         << ofile << endl;

  cout << (file_names.size() == 1 ? "Файл, " : "Файлы, ")
       << "подлежащий(е) обработке:\n";


  for ( int inx = 0; inx < file_names.size(); ++inx )
    cout << "\t" << file_names[ inx ] << endl;
}
a.out -d -l 1024 -o test_7_8 chapter7.doc chapters.doc

Вот трассировка обработки параметров командной строки:

демонстрация обработки параметров в командной строке:
argc: 8
argv[ 1 ]: -d
встретился '-'
встретилась -d: отладочная печать включена
argv[ 2 ]: -l
встретился '-'
встретилась -l: ограничение ресурса
argv[ 3 ]: 1024
default: параметр без дефиса: 1024
argv[ 4 ]: -o
встретился '-'
встретилась -o: выходной файл
argv[ 5 ]: test_7_8
default: параметр без дефиса: test_7_8
argv[ 6 ]: chapter7.doc
default: параметр без дефиса: chapter7.doc
argv[ 7 ]: chapter8.doc
default: параметр без дефиса: chapter8.doc
Заданное пользователем значение limit: 1024
Заданный пользователем выходной файл: test_7_8
Файлы, подлежащий(е) обработке:
chapter7.doc
chapter8.doc

7.8.1. Класс для обработки параметров командной строки

Чтобы не перегружать функцию main() деталями, касающимися обработки параметров командной строки, лучше отделить этот фрагмент. Можно написать для этого функцию. Например:

extern int parse_options( int arg_count, char *arg_vector );
int main( int argc, char *argv[] ) {
  // ...
  int option_status;
  option_status = parse_options( argc, argv );
  // ...
}

Как вернуть несколько значений? Обычно для этого используются глобальные объекты, которые не передаются ни в функцию для их обработки, ни обратно. Альтернативной стратегией является инкапсуляция обработки параметров командной строки в класс.
Данные-члены класса представляют собой параметры, заданные пользователем в командной строке. Набор открытых встроенных функций-членов позволяет получать их значения. Конструктор инициализирует параметры значениями по умолчанию. Функция-член получает argc и argv в качестве аргументов и обрабатывает их:

#include
#include

class CommandOpt {
public:
    CommandOpt() : _limit( -1 ), _debug_on( false ) {}
    int parse_options( int argc, char *argv[] );

    string out_file() { return _out_file; }
    bool   debug_on() { return _debug_on; }
    int    files()    { return _file_names.size(); }

    string& operator[]( int ix );

private:
    inline void usage( int exit_value = 0 );

    bool _debug_on;
    int _limit;
    string _out_file;
    vector _file_names;

    static const char *const program_name;
    static const char *const program_version;
};

Так выглядит модифицированная функция main():

#include "CommandOpt.h"

int main( int argc, char "argv[] ) {
  // ...
  CommandOpt com_opt;
  int option_status;
  opttion_status = com_opt. parse_options (argc, argv);
  // ...
}

Упражнение 7.15

Добавьте обработку опций -t (включение таймера) и -b (задание размера буфера bufsize). Не забудьте обновить usage(). Например:

prog -t -b 512 dataO

Упражнение 7.16

Наша реализация не обрабатывает случая, когда между опцией и ассоциированным с ней значением нет пробела. Модифицируйте программу для поддержки такой обработки.
Упражнение 7.17

Наша реализация не может различить лишний пробел между дефисом и опцией:
prog - d dataO
Модифицируйте программу так, чтобы она распознавала подобную ошибку и сообщала о ней.
Упражнение 7.18

В нашей программе не предусмотрен случай, когда опции -l или -o задаются несколько раз. Реализуйте такую возможность. Какова должна быть стратегия при разрешении конфликта?
Упражнение 7.19

В нашей реализации задание неизвестной опции приводит к фатальной ошибке. Как вы думаете, это оправдано? Предложите другое поведение.
Упражнение 7.20

Добавьте поддержку опций, начинающихся со знака плюс (+), обеспечив обработку +s и +pt, а также +sp и +ps. Предположим, что +s включает строгую проверку синтаксиса, а +p допускает использование устаревших конструкций. Например:

prog +s +p -d -b 1024 dataO
Категория: С++ | Добавил: r2d2 (29.09.2011)
Просмотров: 1025 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Born in Ussr
Залогиниться
Турниры

/j clan ussr /j clan cccp