9.1. Описание строк и примеры строковых функций
Вообще говоря, строки в Си, как и в Паскале, представляют собой просто массивы символов (char). Но если в Паскале строки представляют собой, хоть и удобный, но все же несколько "игрушечный" инструмент (поскольку длина строки ограничена 255 байтами, что, в свою очередь, вызвано внутренним представлением строк — длина записывается в нулевом байте (символе) строки, а туда число больше 255 не запишешь), то в Си такого ограничения нет и строки гораздо удобнее использовать в реальных производственных задачах. Но что же делает строку строкой, если мы сказали, что это всего лишь массивы символов? (В Паскале все ясно, там есть специальный тип string.) Строкой в Си называется массив символов (массив байт), заканчивающийся нулем (символом с кодом 0, если это понятнее). Вот, собственно, и все. Все функции работы со строками (а их имеется достаточно много; прототипы описаны в заголовочном файле string.h) используют именно 0, как признак конца строки. Так, функция strlen, возвращающая длину строки, будучи вызванной от строки s (strlen (s) ) просто просматривает символы строки и считает их количество до тех пор, пока не встретит 0. Встретит — остановится и вернет сколько символов насчитала.
Все сказанное выше можно считать неформальным введением и попыткой объяснить, что такое строки с точки зрения компилятора Си. Теперь все сначала и подробнее.
Описание строки ничем не отличается от описания обычного символьного массива (обычно используется тип unsigned char). Приведем примеры:
unsigned char s[10];
unsigned char s[10]="123";
unsigned char *s:="123";
unsigned char *s={'1','2','3','\0'};
unsigned char *s={'1','2','3',0};
unsigned char *s={'1','2','3'};
В первом случае просто описывается символьный массив из десяти элементов. Надо иметь в виду, что если мы собираемся использовать его как строку, то реально мы сможем использовать лишь девять символов, а еще один нам понадобится для "нулевого" символа, ограничивающего строку.
Во втором случае описывается символьный массив из десяти элементов (т.е. в памяти выделяется 10 байт), но инициализирующая строка содержит четыре символа (а максимум может содержать девять — опять не следует забывать о нулевом!). Значения остальных элементов массива не определены (но память под них выделена).
Третий пример отличается от второго тем, что в памяти выделяется ровно четыре байта под строку s, Обратиться к s [3] мы еще можем (это символ ' \0' ), а вот запись s [4 ] , хотя у компилятора к ней не будет претензий, уже неверна. Нет у строки s такого элемента!
Четвертый и пятый примеры полностью эквивалентны третьему, отличаются они лишь тем, как записан символ с кодом 0. Но это совершенно безразлично.
А вот в шестом примере строка не описывается вовсе, потому что у нее нет завершающего нулевого символа. Можно считать, что мы просто описываем и инициализируем символьный массив из трех элементов. Если мы захотим обращаться с s как со строкой, то в нашем распоряжении будут всего два элемента (один мы должны будем "отдать" под признак конца строки).
Отметим, что присваивать значение строке целиком можно только при инициализации в описании (как и в случае с массивами). Так, как показано ниже, делать нельзя:
unsigned char s[10];
s="123";
Но это простой случай, поскольку его не пропустит компилятор.
А вот пример, приведенный ниже, значительно "хуже".
unsigned
char *s;
s="123";
В принципе ошибки в этом примере нет, но писать так не следует. Почему — вам станет ясно чуть позже. Автор же может сказать, что когда он ищет ошибки в детских программах, использующих строки, то в первую очередь обращает внимание именно на такие фрагменты.
Приведем несколько примеров строковых функций. Мы будем называть их так, как они называются в Турбо Си (см. файл. string, h).
unsigned
int strlen(char *s) /* функция
strlen возвращает длину строки */
{ char *s0=s;
while (*s) s++;
return
s0~s;
}
char * strcpy(char *dest. char *src)
/* функция strcpy копирует строку src в строку dest */
{ char *ret;
while (*src) {*dest=*src;src++;dest++} ;
*dest='\0' ;
return ret;
}
Можно привести более короткий вариант функции strcpy:
char
* strcpy(char *dest, char *src)
{ char *
ret;
while (*dest++=*src++);
return ret;
}
char digit(char с) /* функция digit преобразует цифру-символ в цифру-число */
{ return с—'0';}
int atoi(char *s)
{ int ret=0, sign=l;
switch (*s)
{ '-':
sign=-l;
'+' :
s++;
}
while (*s) ret=10*ret+digit(*s++);
return
ret*sign;
}
9.2. Параметры командной строки
До сих пор во всех примерах мы объявляли функцию main следующим образом: void main (void) , — хотя на самом деле ее прототип может быть иным. Как и всякая другая функция, main может иметь параметры и возвращать значения. Но, поскольку main все же не "всякая другая", а особенная функция, механизм передачи в нее параметров строго регламентирован. А именно: передавать можно лишь определенные параметры и в определенной последовательности. Истинный прототип функции main выглядит следующим образом:
int main (int argc, char **argv)
Замечание. В некоторых реализациях Си в main передаются еще и переменные среды, что добавляет еще один массив параметров. Мы здесь этот вопрос не рассматриваем.
Сначала, как обычно, приведем пример, а потом обсудим подробности. Пусть в файле param.c имеется следующая программа:
#include <stdio.h>
int main(int argc, char ** argv)
{ int i;
printf("\n%d", argc);
for (i=0;i<argc;i++) printf("\n%d параметр = %s", argv[i]);
return argc;
}
Мы получаем исполняемый файл param.exe и вызываем его следующим образом:
param.exe дедка бабка репка
В результате исполнения программы на экран будет выведено;
4
0 параметр = С:\TC\PARAM.EXE
1 параметр = дедка
2 параметр = бабка
3 параметр = репка
Значение нулевого параметра, в котором находится полный путь к программе, у вас может быть, конечно, иным. Таким образом, можно сделать некоторые выводы. В первый целочисленный параметр функции main помещается количество параметров в командной строке, которое заведомо не меньше 1, ибо нулевой параметр присутствует всегда. В элементы строкового массива, который является вторым параметром main, помещаются сами параметры из командной строки вызова программы (в строке вызова они разделяются пробелами). Необходимо отметить, что память под массив argv распределяется системой, нам об этом заботиться не надо.
Замечание. Все написанное выше можно найти практически во всех книжках по языку Си. Механизм передачи параметров в функцию main стандартизован, и что-то новое здесь придумать трудно. Имеется лишь одна проблема: если вы используете Турбо Си 2.0, то вопреки тому, что утверждают разработчики (утверждали, ибо эти версии языков уже давно устарели и не применяются в производственных целях, хотя вполне могут использоваться, и успешно используются, в образовании), описанный механизм не работает. То есть вы можете описать параметры у функции main, но реально в них ничего помещено не будет. Зато можно вовсе не описывать параметры у main, но получить доступ к параметрам командной строки. Приведем "демонстративный" работающий пример, который много раз в многочисленный электронных конференциях приводили разработчикам Турбо Си (и который они демонстративно игнорировали).
#include <stdio.h>
#include <dos.h>
void main(void)
{ int i;
printf("\n%d", argc);
for (i=0;i<_argc;i++) printf("\n%d параметр = %s", _argv[i]);
return _argc;
}
В заголовочном файле dos.h объявлены переменные argc и _arg", в которые и помещаются параметры из командной строки. Автор этих строк потратил некоторое время, чтобы разобраться, что же на самом деле происходит при использовании этого механизма передачи параметров, но результаты этих "исследований" сейчас уже, конечно, мало кому интересны. Хотя то, что в Турбо Си 2,0 не работает стандартный механизм передачи параметров, а об этом нигде явно не объявлено, попортило немало крови программистам.
Еще один вопрос, который мы не обсудили: куда же функция main возвращает значение? Значение она возвращает операционной системе, а проверить, какое именно значение возвращено, можно посредством системной переменной errorlevel.
Пусть у нас имеется следующий ВАТ-файл par.bat:
param.exe
if errorievel=0 echo "He может быть"
if errorlevel=l echo "Нет параметров, кроме имени файла"
if errorlevel>l echo "Имеются параметры в командной строке"
Если мы запустим его, то получим одно из двух последних сообщений (первую строчку мы добавили, чтобы еще раз продемонстрировать, что количество параметров не может быть меньше единицы).
9.3. '"Свободные" массивы
Строки в массиве argv имеют (могут иметь) разную длину, Таким образом, фактически мы имеем дело с двухмерным массивом символов, строки которого могут иметь разную длину. В следующих разделах (при изучении динамических структур данных) мы научимся создавать массивы со строками переменной длины, элементами которых будут являться не только символы, но и данные производных типов. Такие массивы принято называть "свободными". Свободные массивы выгодно отличаются от обычных двухмерных тем, что занимают в памяти ровно столько, сколько требуется для хранения имеющихся данных.
9.4. Вычисление выражения
В качестве более сложного примера работы со строками рассмотрим задачу вычисления арифметического выражения, содержащего вещественные числа (возможно, со знаком), скобки и знаки четырех основных арифметических операций. Разобравшись с этой программой, вы легко можете модифицировать ее, добавив возможность использовать в выражении функции и т.д.
Рекурсивный алгоритм вычисления выражения непосредственно следует из определения, которое мы запишем с использованием БНФ.
<ВЫРАЖЕНИЕ>::-<ВЫРАЖЕНИЕ>+<СЛАГАЕМОЕ>|<ВЫРАЖЕНИЕ>-<СЛАГАЕМОЕ> <СЛАГАЕМОЕ>::=<МНОЖИТЕЛЬ>|<СЛАГАЕМОЕ>*<МНОЖИТЕЛЬ>|<СЛАГАЕМОЕ>/<МНОЖИТЕЛЬ>> <МНОЖИТЕЛЬ>::=+<МНОЖИТЕЛЬ>|-<МНОЖИТЕЛЬ>|<ВЕЩЕСТВЕННОЕ ЧИСЛО|(<ВЫРАЖЕНИЕ>)
Мы не станем "расписывать" определение <вещественного числа>, поскольку намерены для преобразования строковой записи в число использовать библиотечную функцию Си, конкретно — strtod.
Практически все необходимые пояснения даны в комментариях в тексте программы. Объясним лишь назначение функции split. Она носит чисто служебный характер, но в ней выполняется значительная часть работы, Эта функция имеет следующий прототип:
int split(char *source,char *operations,char *lex1,char *lex2,char *op)
В данной функции проверяется, можно ли "расщепить" строку source на две строки — lex1 и 1ех2, используя одну из операций, содержащихся в строке operations. Если сделать этого нельзя, то функция split возвращает значение 0, а значения параметров lex1, 1ех2 и ор не определены. Если строку source "расщепить" можно, то функция split возвращает значение 1, а в lex1, 1ех2 и ор содержатся, соответственно, строки, на которые мы разбили source, и операция, которой мы ее разбили.
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <alloc.h>
#inciude <conio.h>
#define MAX_LEN_LEXEM 100 double
expression(char*);
double item(char*);
double factor(char*);
void copy(char *source,int index,int count,char *dest)
/* Выделяет из строки source подстроку, начиная с номера index длиной count.
Здесь используются функции работы с динамической памятью malloc и free, которые
будут рассмотрены далее.*/
{int size=l+sizeof(char)*(((index+count-1)<=(strlen(source)-1))?count:(strlen(source)-1-index+l)) ;
char *target=(char *)malloc(size);
if (target==NULL) exit(l);
/*if ( (index<0) | |(index>(strlen(source)-1))) { free(target);return NULL;}*/
target[count]=' \0 ' ;
for(--count;count>=0;count--) target[count]=source[index+count] ;
strcpy(dest,target);
free(target) ;
}
int posch(char ch,char *source)
/* Ищет вхождение символа с в строку. Возвращает номер или -1 при неудаче */
{ int i;
for(i=0; i<= (strlen (source)-1); i++) if (source[i]==ch) return i;
return -1;
}
double expression(char *source)
{ char lex1[MAX_LEN_LEXEM],lex2[MAX_LEN_LEXEM],o;
if (split(source,"+-",lexl,lex2,&o))
return item(source) ;
else switch (o)
{ case '+':return expression(lexl)+item(lex2)
;
case '-':return
expression(lexl)-item(lex2);
}
}
double item(char *source)
{ char lex1[MAX_LEN_LEXEM],lex2[MAX_LEN_LEXEM],o;
if (split(source,"*/",lex1,lex2,&o)} return factor(source);
else switch (о)
{
case '*':return item(lex1)*factor(lex2);
case '/':return item(lex1)/factor(lex2);
}
}
if (source[0]=='-') return (-1)*factor(source+1);
else if (source[0]=='+') return factor(source+1);
else if (source[0]!='(') return strtod(source,NULL);
else return copy(source,1,strlen(source)-2,lexnul),expression(lexnul);
}int split(char *source,char *operations,char *lex1,char *lex2,char *op)
{ int i=strlen(source),count=0,t;
do { if (source[i]==')') count++;
else if (source[i]=='(') count--;
t=(i>0) && (posch( source [i], operations) !=(-1))&& (posch (source [i-1], "+-*/")==-1) &&(!count);
i--;
}
while ((i>=0)&& (!t)) ;
if (t) { copy(source,0,i+1,lex1);
copy(source,i+2,strlen(source),lex2) ;
(*op)=source[i+1];
return 0;};
return 1;
}void main(void)
{ char test[MAX_LEN_LEXEM] ;
putchar('\n') ;
do { scanf("%s",test);
printf("\n%f\n",expression(test)) ;
}
while (srtcmp(test,"666"));}