readlines(char *lineptr[],int maxlines)
{
int len, nlines;
char *p, *alloc(), line[maxlen];
nlines = 0;
while ((len = getline(line, maxlen)) > 0)
if (nlines >= maxlines)
return(-1);
else if ((p = alloc(len)) == null)
return (-1);
else
{
line[len-1] = '\0'; /* zap newline */
strcpy(p,line);
lineptr[nlines++] = p;
}
return(nlines);
}
Символ новой строки в конце каждой строки удаляется, так что он никак не будет влиять на порядок, в котором сортируются строки.
// Напечатать выходные строки
writelines(char *lineptr[],int nlines)
{
int i;
for (i = 0; i < nlines; i++)
printf("%s\n", lineptr[i]);
}
Существенно новым в этой программе является описание:
char *lineptr[lines];
которое сообщает, что lineptr является массивом из lines элементов, каждый из которых – указатель на переменные типа char. Это означает, что lineptr[i] – указатель на символы, а *lineptr[i] извлекает символ.
Так как сам lineptr является массивом, который передается функции writelines, с ним можно обращаться как с указателем точно таким же образом, как в наших более ранних примерах. Тогда последнюю функцию можно переписать в виде:
// Напечатать выходные строки
writelines(char *lineptr[],int nlines)
{
int i;
while (--nlines >= 0)
printf("%s\n", *lineptr++);
}
Здесь *lineptr сначала указывает на первую строку; каждое увеличение передвигает указатель на следующую строку, в то время как nlines убывает до нуля.
Справившись с вводом и выводом, мы можем перейти к сортировке. Программа сортировки по Шеллу из главы 4 требует очень небольших изменений: должны быть модифицированы описания, а операция сравнения выделена в отдельную функцию. Основной алгоритм остается тем же самым, и это дает нам определенную уверенность, что он по-прежнему будет работать.
// Отсортировать строки v[0] ... v[n-1]
// в возрастающем порядке
sort(char *v[],int n)
{
int gap, i, j;
char *temp;
for (gap = n/2; gap > 0; gap /= 2)
for (i = gap; i < n; i++)
for (j = i - gap; j >= 0; j -= gap)
{
if (strcmp(v[j], v[j+gap]) <= 0)
break;
temp = v[j];
v[j] = v[j+gap];
v[j+gap] = temp;
}
}
Так как каждый отдельный элемент массива v (имя формального параметра, соответствующего lineptr) является указателем на символы, то и temp должен быть указателем на символы, чтобы их было можно копировать друг в друга.
Мы написали эту программу по возможности более просто с тем, чтобы побыстрее получить работающую программу. Она могла бы работать быстрее, если, например, вводить строки непосредственно в массив, управляемый функцией readlines, а не копировать их в line, а затем в скрытое место с помощью функции alloc. Но мы считаем, что будет разумнее первоначальный вариант сделать более простым для понимания, а об «эффективности» позаботиться позднее.
Все же, по-видимому, способ, позволяющий добиться заметного ускорения работы программы состоит не в исключении лишнего копирования вводимых строк. Более вероятно, что существенной разницы можно достичь за счет замены сортировки по Шеллу на нечто лучшее, например, на метод быстрой сортировки.
В главе 2 мы отмечали, что поскольку в циклах while и for проверка осуществляется до того, как тело цикла выполнится хотя бы один раз, эти циклы оказываются удобными для обеспечения правильной работы программы при граничных значениях, в частности, когда ввода вообще нет. Очень полезно просмотреть все функции программы сортировки, разбираясь, что происходит, если вводимый текст отсутствует.
Упражнение 6-5. Перепишите функцию readlines таким образом, чтобы она помещала строки в массив, предоставляемый функцией main, а не в память, управляемую обращениями к функции alloc. Насколько быстрее стала программа?
6.9. Инициализация массивов указателей
Пример 6-15. Рассмотрим задачу написания функции:
month_name(n),
которая возвращает указатель на символьную строку, содержащую имя n-го месяца. Это идеальная задача для применения внутреннего статического массива. Функция month_name содержит локальный массив символьных строк и при обращении к ней возвращает указатель нужной строки. Тема настоящего раздела – как инициализировать этот массив имен.
// Получить название n-го месяца
char *month_name(int n)
{
static char *name[] =
{
"неправильный месяц",
"январь",
"февраль",
"март",
"апрель",
"май",
"июнь",
"июль",
"август",
"сентябрь",
"октябрь",
"ноябрь",
"декабрь"
};
return ((n < 1 || n > 12) ? name[0] : name[n]);
}
Описание массива указателей на символы name точно такое же, как аналогичное описание lineptr в примере с сортировкой. Инициализатором является просто список символьных строк; каждая строка присваивается соответствующей позиции в массиве. Более точно, символы i-ой строки помещаются в какое-то иное место, а ее указатель хранится в name[i]. Поскольку размер массива name не указан, компилятор сам подсчитывает количество инициализаторов и соответственно устанавливает правильное число.
6.10. Указатели и многомерные массивы
Начинающие изучать язык «С» иногда становятся в тупик перед вопросом о различии между двумерным массивом и массивом указателей, таким как name в приведенном выше примере. Если имеются описания:
int a[10][10];
int *b[10];
то a и b можно использовать сходным образом в том смысле, что как a[5][5], так и b[5][5] являются законными ссылками на отдельное число типа int. Но a – это настоящий массив: под него отводится 100 ячеек памяти и для нахождения любого указанного элемента проводятся обычные вычисления с прямоугольными индексами. Для b, однако, описание выделяет только 10 указателей; каждый указатель должен быть установлен так, чтобы он указывал на массив целых. Если предположить, что каждый из них указывает на массив из 10 элементов, то тогда где-то будет отведено 100 ячеек памяти плюс еще десять ячеек для указателей. Таким образом, массив указателей использует несколько больший объем памяти и может требовать наличие явного шага инициализации. Но при этом возникают два преимущества: доступ к элементу осуществляется косвенно через указатель, а не посредством умножения и сложения, и строки массива могут иметь различные длины. Это означает, что каждый элемент B не должен обязательно указывать на вектор из 10 элементов; некоторые могут указывать на вектор из двух элементов, другие - из двадцати, а третьи могут вообще ни на что не указывать.
Хотя мы вели это обсуждение в терминах целых, несомненно, чаще всего массивы указателей используются так, как мы продемонстрировали на функции month_name, – для хранения символьных строк различной длины.
Упражнение 6-6. Перепишите функции day_of_year и month_day, используя вместо индексации указатели.
6.11. Командная строка аргументов
Системные средства, на которые опирается реализация языка «С», позволяют передавать командную строку аргументов или параметров начинающей выполняться программе. Когда функция main вызывается к исполнению, она вызывается с двумя аргументами. Первый аргумент (условно называемый argc) указывает число аргументов в командной строке, с которыми происходит обращение к программе; второй аргумент (argv) является указателем на массив символьных строк, содержащих эти аргументы, по одному в строке. Работа с такими строками – это обычное использование многоуровневых указателей.
Пример 6-16. Самую простую иллюстрацию этой возможности и необходимых при этом описаний дает программа echo, которая просто печатает в одну строку аргументы командной строки, разделяя их пробелами. Таким образом, если дана команда:
echo Hello, World
то выходом будет
Hello, World
по соглашению argv[0] является именем, по которому вызывается программа, так что argc по меньшей мере равен 1. В приведенном выше примере argc равен 3, а argv[0], argv[1] и argv[2] равны соответственно "echo", "Hello," и "World". Первым фактическим агументом является argv[1], а последним будет argv[argc-1]. Если argc равен 1, то за именем программы не следует никакой командной строки аргументов. Все это показано в echo:
// Аргументы echo: 1-я версия
main(int argc, char *argv[])
{
int i;
for (i = 1; i < argc; i++)
printf("%s%c", argv[i], (i<argc-1) ? ' ' : '\n');
}
Поскольку argv является указателем на массив указателей, то существует несколько способов написания этой программы, использующих работу с указателем, а не с индексацией массива. Мы продемонстрируем два варианта.
Пример 6-17. Один вариант.
// Аргументы echo: 2-я версия
main(int argc, char *argv[])
{
while (--argc > 0)
printf("%s%c",*++argv, (argc > 1) ? ' ' : '\n');
}
Так как argv является указателем на начало массива строк-аргументов, то, увеличив его на 1 посредством ++argv, мы вынуждаем его указывать на подлинный аргумент argv[1], а не на argv[0]. Каждое последующее увеличение передвигает его на следующий аргумент; при этом *argv становится указателем на этот аргумент. Одновременно величина argc уменьшается; когда она обратится в нуль, все аргументы будут уже напечатаны.
Пример 6-18. Другой вариант:
// Аргументы echo: 3-я версия
main(int argc, char *argv[])
{
while (--argc > 0)
printf((argc > 1) ? "%s" : "%s\n", *++argv);
}
Эта версия показывает, что аргумент формата функции printf может быть выражением, точно так же, как и любой другой. Такое использование встречается не очень часто, но его все же стоит запомнить.
Пример 6-19. Давайте внесем некоторые усовершенствования в программу отыскания заданной комбинации символов из главы 5. Если вы помните, мы поместили искомую комбинацию глубоко внутрь программы, что очевидно является совершенно неудовлетворительным. Следуя утилите grep системы Unix, давайте изменим программу так, чтобы эта комбинация указывалась в качестве первого аргумента строки.
// Печать строк, содержащих образец, заданный первым
// аргументом
#define maxline 1000
main(int argc, char *argv[])
{
char line[maxline];
if (argc != 2)
printf ("Используйте в find образец! \n");
else
while (getline(line, maxline) > 0)
if (index(line, argv[1] >= 0)
printf("%s", line);
}
Теперь может быть развита основная модель, иллюстрирующая дальнейшее использование указателей. Предположим, что нам надо предусмотреть два необязательных аргумента. Один утверждает: «Напечатать все строки за исключением тех, которые содержат данную комбинацию», а второй гласит: «Перед каждой выводимой строкой должен печататься ее номер».