Си в UNIX 2.18. Что означают описания? int i; // целое. int *pi; // указатель на целое. int *api[3]; // массив из 3х ук-лей на целые. int (*pai)[3]; // указатель на массив из 3х целых. // можно описать как int **pai; int fi(); // функция, возвращающая целое. int *fpi(); // ф-ция, возвр. ук-ль на целое. int (*pfi)(); // ук-ль на ф-цию, возвращающую целое. int *(*pfpi)(); // ук-ль на ф-цию, возвр. ук-ль на int. int (*pfpfi())(); // ф-ция, возвращающая указатель на // "функцию, возвращающую целое". int (*fai())[3]; // ф-ция, возвр. ук-ль на массив // из 3х целых. иначе ее // можно описать как int **fai(); int (*apfi[3])(); // массив из 3х ук-лей на функции, // возвращающие целые. Переменные в Си описываются в формате их использования. Так описание int (*f)(); означает, что f можно использовать в виде int value; value = (*f)(1, 2, 3 /* список аргументов */); Однако из такого способа описания тип самой описываемой переменной и его смысл довольно неочевидны. Приведем прием (позаимствованный из журнала "Communications of the ACM"), позволяющий прояснить смысл описания. Описание на Си переводится в описа- ние в стиле языка Algol-68. Далее ref ТИП означает "указатель на ТИП" proc() ТИП "функция, возвращающая ТИП" array of ТИП "массив из элементов ТИПа" x: ТИП "x имеет тип ТИП" Приведем несколько примеров, из которых ясен и способ преобразования: int (*f())(); означает (*f())() : int *f() : proc() int f() : ref proc() int f : proc() ref proc() int то есть f - функция, возвращающая указатель на функцию, возвращающую целое. int (*f[3])(); означает (*f[])() : int *f[] : proc() int f[] : ref proc() int f : array of ref proc() int f - массив указателей на функции, возвращающие целые. Обратно: опишем g как указа- тель на функцию, возвращающую указатель на массив из 5и указателей на функции, возв- ращающие указатели на целые. А. Богатырев, 1992-95 - 91 - Си в UNIX g : ref p() ref array of ref p() ref int *g : p() ref array of ref p() ref int (*g)() : ref array of ref p() ref int *(*g)() : array of ref p() ref int (*(*g)())[5] : ref p() ref int *(*(*g)())[5] : p() ref int (*(*(*g)())[5])(): ref int *(*(*(*g)())[5])(): int int *(*(*(*g)())[5])(); В Си невозможны функции, возвращающие массив: proc() array of ... а только proc() ref array of ... Само название типа (например, для использования в операции приведения типа) получа- ется вычеркиванием имени переменной (а также можно опустить размер массива): g = ( int *(*(*(*)())[])() ) 0; 2.19. Напишите функцию strcat(d,s), приписывающую строку s к концу строки d. Ответ: char *strcat(d,s) register char *d, *s; { while( *d ) d++; /* ищем конец строки d */ while( *d++ = *s++ ); /* strcpy(d, s) */ return (d-1); /* конец строки */ } Цикл, помеченный "strcpy" - это наиболее краткая запись операторов do{ char c; c = (*d = *s); s++; d++; } while(c != '\0'); На самом деле strcat должен по стандарту возвращать свой первый аргумент, как и функ- ция strcpy: char *strcat(d,s) register char *d, *s; { char *p = d; while( *d ) d++; strcpy(d, s); return p; } Эти два варианта демонстрируют, что функция может быть реализована разными способами. Кроме того видно, что вместо стандартной библиотечной функции мы можем определить свою одноименную функцию, несколько отличающуюся поведением от стандартной (как возв- ращаемое значение в 1-ом варианте). 2.20. Напишите программу, которая объединяет и распечатывает две строки, введенные с терминала. Для ввода строк используйте функцию gets(), а для их объединения - strcat(). В другом варианте используйте sprintf(result,"%s%s",s1,s2); 2.21. Модифицируйте предыдущую программу таким образом, чтобы она выдавала длину (число символов) объединенной строки. Используйте функцию strlen(). Приведем нес- колько версий реализации strlen: /* При помощи индексации массива */ А. Богатырев, 1992-95 - 92 - Си в UNIX int strlen(s) char s[]; { int length = 0; for(; s[length] != '\0'; length++); return (length); } /* При помощи продвижения указателя */ int strlen(s) char *s; { int length; for(length=0; *s; length++, s++); return length; } /* При помощи разности указателей */ int strlen(register char *s) { register char *p = s; while(*p) p++; /* ищет конец строки */ return (p - s); } Разность двух указателей на один и тот же тип - целое число: если TYPE *p1, *p2; то p2 - p1 = целое число штук TYPE лежащих между p2 и p1 если p2 = p1 + n то p2 - p1 = n Эта разность может быть и отрицательной если p2 < p1, то есть p2 указывает на более левый элемент массива. 2.22. Напишите оператор Си, который обрубает строку s до длины n букв. Ответ: if( strlen(s) > n ) s[n] = '\0'; Первое сравнение вообще говоря излишне. Оно написано лишь на тот случай, если строка s короче, чем n букв и хранится в массиве, который также короче n, т.е. не имеет n- ого элемента (поэтому в него нельзя производить запись признака конца). 2.23. Напишите функции преобразования строки, содержащей изображение целого числа, в само это число. В двух разных вариантах аргумент-адрес должен указывать на первый байт строки; на последний байт. Ответ: #define isdigit(c) ('0' <= (c) && (c) <= '9') int atoi(s) register char *s; { register int res=0, neg=0; for(;;s++){ switch(*s){ case ' ': case '\t': continue; case '-': neg++; case '+': s++; } break; } while(isdigit(*s)) res = res * 10 + *s++ - '0'; return( neg ? -res : res ); } int backatoi(s) register char *s; { int res=0, pow=1; while(isdigit(*s)){ А. Богатырев, 1992-95 - 93 - Си в UNIX res += (*s-- - '0') * pow; pow *= 10; } if(*s == '-') res = -res; return res; } 2.24. Можно ли для занесения в массив s строки "hello" написать char s[6]; s = "hello"; или char s[6], d[] = "hello"; s = d; Ответ: нет. Массивы в Си нельзя присваивать целиком. Для пересылки массива байт надо использовать функцию strcpy(s,d). Здесь же мы пытаемся изменить адрес s (имя массива - это адрес начала памяти, выделенной для хранения массива), сделав его равным адресу безымянной строки "hello" (или массива d во втором случае). Этот адрес является константой и не может быть изменен! Заметим однако, что описание массива с инициализацией вполне допустимо: char s[6] = "hello"; или char s[6] = { 'h', 'e', 'l', 'l', 'o', '\0' }; или char s[] = "hello"; или char s[] = { "hello" }; В этом случае компилятор резервирует память для хранения массива и расписывает ее байтами начального значения. Обратите внимание, что строка в двойных кавычках (если ее рассматривать как массив букв) имеет длину на единицу больше, чем написано букв в строке, поскольку в конце массива находится символ '\0' - признак конца, добавленный компилятором. Если бы мы написали char s[5] = "hello"; то компилятор сообщил бы об ошибке, поскольку длины массива (5) недостаточно, чтобы разместить 6 байт. В третьей строке примера написано s[], чтобы компилятор сам пос- читал необходимую длину массива. Наконец, возможна ситуация, когда массив больше, чем хранящаяся в нем строка. Тогда "лишнее" место содержит какой-то мусор (в static-памяти изначально - байты \0). char s[12] = "hello"; содержит: h e l l o \0 ? ? ? ? ? ? В программах текстовой обработки под "длиной строки" обычно понимают количество букв в строке НЕ считая закрывающий байт '\0'. Именно такую длину считает стандартная функция strlen(s). Поэтому следует различать такие понятия как "(текущая) длина строки" и "длина массива, в котором хранится строка": sizeof(s). Для написанного выше примера эти значения равны соответственно 5 и 12. Следует также отличать массивы от указателей: char *sp = "bye bye"; sp = "hello"; будет вполне законно, поскольку в данном случае sp - не имя массива (т.е. константа, равная адресу начала массива), а указатель (переменная, хранящая адрес некоторой области памяти). Поскольку указатель - это переменная, то ее значение изменять можно: в данном случае sp сначала содержала адрес безымянного массива, в котором находится "bye bye"; затем мы занесли в sp адрес безымянного массива, хранящего А. Богатырев, 1992-95 - 94 - Си в UNIX строку "hello". Здесь не происходит копирования массива, а происходит просто присва- ивание переменной sp нового значения адреса. Предостережем от возможной неприятности: char d[5]; char s[] = "abcdefgh"; strcpy(d, s); Длины массива d просто не хватит для хранения такой длинной строки. Поскольку это ничем не контролируется (ни компилятором, ни самой strcpy, ни вами явным образом), то при копировании строки "избыточные" байты запишутся после массива d поверх других данных, которые будут испорчены. Это приведет к непредсказуемым эффектам. Некоторые возможности для контроля за длиной строк-аргументов вам дают функции strncpy(d,s,len); strncat(d,s,len); strncmp(s1,s2,len). Они пересылают (сравнивают) не более, чем len первых символов строки s (строк s1, s2). Посмотрите в документа- цию! Напишите функцию strncmp (сравнение строк по первым len символам), посмотрев на функцию strncpy: char *strncpy(dst, src, n) register char *dst, *src; register int n; { char *save; for(save=dst; --n >= 0; ) if( !(*dst++ = *src++)){ while(--n >= 0) *dst++ = '\0'; return save; } return save; } Отметьте, что strncpy обладает одним неприятным свойством: если n <= strlen(src), то строка dst не будет иметь на конце символа '\0', то есть будет находиться в некор- ректном (не каноническом) состоянии. Ответ: int strncmp(register char *s1, register char *s2, register int n) { if(s1 == s2) return(0); while(--n >= 0 && *s1 == *s2++) if(*s1++ == '\0') return(0); return((n < 0)? 0: (*s1 - *--s2)); } 2.25. В чем ошибка? #include <stdio.h> /* для putchar */ char s[] = "We don't need no education"; main(){ while(*s) putchar(*s++); } Ответ: здесь s - константа, к ней неприменима операция ++. Надо написать char *s = "We don't need no education"; сделав s указателем на безымянный маccив. Указатель уже можно изменять. 2.26. Какие из приведенных конструкций обозначают одно и то же? А. Богатырев, 1992-95 - 95 - Си в UNIX char a[] = ""; /* пустая строка */ char b[] = "\0"; char c = '\0'; char z[] = "ab"; char aa[] = { '\0' }; char bb[] = { '\0', '\0' }; char xx[] = { 'a', 'b' }; char zz[] = { 'a', 'b', '\0' }; char *ptr = "ab"; 2.27. Найдите ошибки в описании символьной строки: main() { char mas[] = {'s', 'o', 'r', 't'}; /* "sort" ? */ printf("%s\n", mas); } Ответ: строка должна кончаться '\0' (в нашем случае printf не обнаружив символа конца строки будет выдавать и байты, находящиеся в памяти после массива mas, т.е. мусор); инициализированный массив не может быть автоматическим - требуется static: main() { static char mas[] = {'s', 'o', 'r', 't', '\0'}; } Заметим, что main(){ char *mas = "sort"; } законно, т.к. сама строка здесь хранится в статической памяти, а инициализируется лишь указатель на этот массив байт. 2.28. В чем ошибка? Программа собирается из двух файлов: a.c и b.c командой cc a.c b.c -o ab a.c b.c --------------------------------------------------- int n = 2; extern int n; char s[] = "012345678"; extern char *s; main(){ f(){ f(); s[n] = '+'; printf("%s\n", s ); } } Ответ: дело в том, что типы (char *) - указатель, и char[] - массив, означают одно и то же только при объявлении формального параметра функции: f(char *arg){...} f(char arg[]){...} это будет локальная переменная, содержащая указатель на char (т.е. адрес некоторого байта в памяти). Внутри функции мы можем изменять эту переменную, например arg++. Далее, и (char *) и char[] одинаково используются, например, оба эти типа можно индексировать: arg[i]. Но вне функций они объявляют разные объекты! Так char *p; это скалярная переменная, хранящая адрес (указатель): -------- ------- p:| *--|----->| '0' | char -------- | '1' | char ... А. Богатырев, 1992-95 - 96 - Си в UNIX тогда как char a[20]; это адрес начала массива (а вовсе не переменная): ------- a:| '0' | char | '1' | char ... В нашем примере в файле b.c мы объявили внешний массив s как переменную. В резуль- тате компилятор будет интерпретировать начало массива s как переменную, содержащую указатель на char. ------- s:| '0' | \ это будет воспринято как | '1' | / адрес других данных. | '2' | ... И индексироваться будет уже ЭТОТ адрес! Результат - обращение по несуществующему адресу. То, что написано у нас, эквивалентно char s[] = "012345678"; char **ss = s; /* s - как бы "массив указателей" */ /* первые байты s интерпретируются как указатель: */ char *p = ss[0]; p[2] = '+'; Мы же должны были объявить в b.c extern char s[]; /* размер указывать не требуется */ Вот еще один аналогичный пример, который пояснит вам, что происходит (а заодно пока- жет порядок байтов в long). Пример выполнялся на IBM PC 80386, на которой sizeof(char *) = sizeof(long) = 4 a.c b.c --------------------------------------------------- char s[20] = {1,2,3,4}; extern char *s; main(){ f(){ /*печать указателя как long */ f(); printf( "%08lX\n", s ); } } печатается 04030201. 2.29. Что напечатает программа? static char str1[ ] = "abc"; static char str2[4]; strcpy( str2, str1 ); /* можно ли написать str2 = str1; ? */ printf( str1 == str2 ? "равно":"не равно" ); Как надо правильно сравнивать строки? Что на самом деле сравнивается в данном при- мере? Ответ: сравниваются адреса массивов, хранящих строки. Так А. Богатырев, 1992-95 - 97 - Си в UNIX char str1[2]; char str2[2]; main(){ printf( str1 < str2 ? "<":">"); } печатает <, а если написать char str2[2]; char str1[2]; то напечатается >. 2.30. Напишите программу, спрашивающую ваше имя до тех пор, пока вы его правильно не введете. Для сравнения строк используйте функцию strcmp() (ее реализация есть в главе "Мобильность"). 2.31. Какие значения возвращает функция strcmp() в следующей программе? #include <stdio.h> main() { printf("%d\n", strcmp("abc", "abc")); /* 0 */ printf("%d\n", strcmp("ab" , "abc")); /* -99 */ printf("%d\n", strcmp("abd", "abc")); /* 1 */ printf("%d\n", strcmp("abc", "abd")); /* -1 */ printf("%d\n", strcmp("abc", "abe")); /* -2 */ } 2.32. В качестве итога предыдущих задач: помните, что в Си строки (а не адреса) надо сравнивать как if( strcmp("abc", "bcd") < 0) ... ; if( strcmp("abc", "bcd") == 0) ... ; вместо if( "abc" < "bcd" ) ... ; if( "abc" == "bcd" ) ... ; и присваивать как char d[80], s[80]; strcpy( d, s ); вместо d = s; 2.33. Напишите программу, которая сортирует по алфавиту и печатает следующие ключе- вые слова языка Си: int char double long for while if 2.34. Вопрос не совсем про строки, скорее про цикл: чем плоха конструкция? char s[] = "You're a smart boy, now shut up."; int i, len; for(i=0; i < strlen(s); i++) putchar(s[i]); Ответ: в соответствии с семантикой Си цикл развернется примерно в А. Богатырев, 1992-95 - 98 - Си в UNIX i=0; LOOP: if( !(i < strlen(s))) goto ENDLOOP; putchar(s[i]); i++; goto LOOP; ENDLOOP: ; Заметьте, что хотя длина строки s не меняется, strlen(s) вычисляется на КАЖДОЙ итера- ции цикла, совершая лишнюю работу! Борьба с этим такова: for(i=0, len=strlen(s); i < len; i++ ) putchar(s[i]); или for(i=0, len=strlen(s); len > 0; i++, --len ) putchar(s[i]); Аналогично, в цикле while( i < strlen(s))...; функция тоже будет вычисляться при каждой проверке условия! Это, конечно, относится к любой функции, используемой в условии, а не только к strlen. (Но, разумеется, случай когда функция возвращает признак "надо ли продолжать цикл" - совсем другое дело: такая функция обязана вычисляться каждый раз). 2.35. Что напечатает следующая программа? #include <stdio.h> main(){ static char str[] = "До встречи в буфете"; char *pt; pt = str; puts(pt); puts(++pt); str[7] = '\0'; puts(str); puts(pt); puts(++pt); } 2.36. Что напечатает следующая программа? main() { static char name[] = "Константин"; char *pt; pt = name + strlen(name); while(--pt >= name) puts(pt); } 2.37. Что напечатает следующая программа? char str1[] = "abcdef"; char str2[] = "xyz"; main(){ register char *a, *b; a = str1; b = str2; while( *b ) *a++ = *b++; printf( "str=%s a=%s\n", str1, a ); a = str1; b = str2; А. Богатырев, 1992-95 - 99 - Си в UNIX while( *b ) *++a = *b++; printf( "str=%s a=%s\n", str1, a ); } Ответ: str=xyzdef a=def str=xxyzef a=zef 2.38. Что печатает программа? char *s; for(s = "Ситроен"; *s; s+= 2){ putchar(s[0]); if(!s[1]) break; } putchar('\n'); 2.39. Что напечатает программа? Рассмотрите продвижение указателя s, указателей - элементов массива strs[]. Разберитесь с порядком выполнения операций. В каких случаях ++ изменяет указатель, а в каких - букву в строке? Нарисуйте себе картинку, изобража- ющую состояние указателей - она поможет вам распутать эти спагетти. Уделите разбору этого примера достаточное время! #include <stdio.h> /* определение NULL */ /* Латинский алфавит: abcdefghijklmnopqrstuvwxyz */ char *strs[] = { "abcd","ABCD","0fpx","159", "hello","-gop","A1479",NULL }; main(){ char c, **s = strs, *p; c = *++*s; printf("#1 %d %c %s\n", s-strs, c, *s); c = **++s; printf("#2 %d %c %s\n", s-strs, c, *s); c = **s++; printf("#3 %d %c %s\n", s-strs, c, *s); c = ++**s; printf("#4 %d %c %s\n", s-strs, c, *s); c = (**s)++; printf("#5 %d %c %s\n", s-strs, c, *s); c = ++*++*s; printf("#6 %d %c %s\n", s-strs, c, *s); c = *++*s++; printf("#7 %d %c %s %s\n", s-strs, c, *s, strs[2]); c = ++*++*s++; printf("#8 %d %c %s %s\n", s-strs, c, *s, strs[3]); c = ++*++*++s; printf("#9 %d %c %s\n", s-strs,c,*s); c = ++**s++; printf("#10 %d %c %s\n",s-strs,c,*s); p = *s; c = ++*(*s)++; printf("#11 %d %c %s %s %s\n",s-strs,c,*s,strs[6],p); c = ++*((*s)++); printf("#12 %d %c %s %s\n", s-strs, c, *s, strs[6]); c = (*++(*s))++; printf("#13 %d %c %s %s\n", s-strs, c, *s, strs[6]); for(s=strs; *s; s++) printf("strs[%d]=\"%s\"\n", s-strs, *s); putchar('\n'); } Печатается: А. Богатырев, 1992-95 - 100 - Си в UNIX #1 0 b bcd strs[0]="bcd" #2 1 A ABCD strs[1]="ABCD" #3 2 A 0fpx strs[2]="px" #4 2 1 1fpx strs[3]="69" #5 2 1 2fpx strs[4]="hello" #6 2 g gpx strs[5]="iop" #7 3 p 159 px strs[6]="89" #8 4 6 hello 69 #9 5 h hop #10 6 i A1479 #11 6 B 1479 1479 B1479 #12 6 2 479 479 #13 6 7 89 89 Учтите, что конструкция char *strs[1] = { "hello" }; означает, что в strs[0] содержится указатель на начальный байт безымянного массива, содержащего строку "hello". Этот указатель можно изменять! Попробуйте составить еще подобные примеры из *, ++, (). 2.40. Что печатает программа? char str[25] = "Hi, "; char *f(char **s){ int cnt; for(cnt=0; **s != '\0'; (*s)++, ++cnt); return("ny" + (cnt && (*s)[-1] == ' ') + (!cnt)); } void main(void){ char *s = str; if( *f(&s) == 'y') strcat(s, "dude"); else strcat(s, " dude"); printf("%s\n", str); } Что она напечатает, если задать char str[25]="Hi,"; или char str[25]=""; 2.41. В чем состоит ошибка? (Любимая ошибка начинающих) main(){ char *buf; /* или char buf[]; */ gets( buf ); printf( "%s\n", buf ); } Ответ: память под строку buf не выделена, указатель buf не проинициализирован и смот- рит неизвестно куда. Надо было писать например так: char buf[80]; или char mem[80], *buf = mem; Обратите на этот пример особое внимание, поскольку, описав указатель (но никуда его не направив), новички успокаиваются, не заботясь о выделении памяти для хранения дан- ных. Указатель должен указывать на ЧТО-ТО, в чем можно хранить данные, а не "висеть", указывая "пальцем в небо"! Запись информации по "висячему" указателю разрушает память программы и приводит к скорому (но часто не немедленному и потому таинственному) краху. А. Богатырев, 1992-95 - 101 - Си в UNIX Вот программа, которая также использует неинициализированный указатель. На машине SPARCstation 20 эта программа убивается операционной системой с диагностикой "Segmentation fault" (SIGSEGV). Это как раз и значит обращение по указателю, указы- вающему "пальцем в небо". main(){ int *iptr; int ival = *iptr; printf("%d\n", ival); } 2.42. Для получения строки "Life is life" написана программа: main(){ char buf[ 60 ]; strcat( buf, "Life " ); strcat( buf, "is " ); strcat( buf, "life" ); printf( "%s\n", buf ); } Что окажется в массиве buf? Ответ: в начале массива окажется мусор, поскольку автоматический массив не инициали- зируется байтами '\0', а функция strcat() приписывает строки к концу строки. Для исп- равления можно написать *buf = '\0'; перед первым strcat()-ом, либо вместо первого strcat()-а написать strcpy( buf, "Life " ); 2.43. Составьте макроопределение copystr(s1, s2) для копирования строки s2 в строку s1. 2.44. Составьте макроопределение lenstr(s) для вычисления длины строки. Многие современные компиляторы сами обращаются с подобными короткими (1-3 опера- тора) стандартными функциями как с макросами, то есть при обращении к ним генерят не вызов функции, а подставляют текст ее тела в место обращения. Это делает объектный код несколько "толще", но зато быстрее. В расширенных диалектах Си и в Си++ компиля- тору можно предложить обращаться так и с вашей функцией - для этого функцию следует объявить как inline (такие функции называются еще "intrinsic"). 2.45. Составьте рекурсивную и нерекурсивную версии программы инвертирования (зер- кального отображения) строки: abcdef --> fedcba. 2.46. Составьте функцию index(s, t), возвращающую номер первого вхождения символа t в строку s; если символ t в строку не входит, функция возвращает -1. Перепишите эту функцию с указателями, чтобы она возвращала указатель на первое вхождение символа. Если символ в строке отсутствует - выдавать NULL. В UNIX System-V такая функция называется strchr. Вот возможный ответ: char *strchr(s, c) register char *s, c; { while(*s && *s != c) s++; return *s == c ? s : NULL; } А. Богатырев, 1992-95 - 102 - Си в UNIX Заметьте, что p=strchr(s,'\0'); выдает указатель на конец строки. Вот пример исполь- зования: extern char *strchr(); char *s = "abcd/efgh/ijklm"; char *p = strchr(s, '/'); printf("%s\n", p==NULL ? "буквы / нет" : p); if(p) printf("Индекс вхождения = s[%d]\n", p - s ); 2.47. Напишите функцию strrchr(), указывающую на последнее вхождение символа. Ответ: char *strrchr(s, c) register char *s, c; { char *last = NULL; do if(*s == c) last = s; while(*s++); return last; } Вот пример ее использования: extern char *strrchr(); char p[] = "wsh"; /* эталон */ main(argc, argv) char *argv[];{ char *s = argv[1]; /* проверяемое имя */ /* попробуйте вызывать * a.out csh * a.out /bin/csh * a.out wsh * a.out /usr/local/bin/wsh */ char *base = (base = strrchr(s, '/')) ? base+1 : s; if( !strcmp(p, base)) printf("Да, это %s\n" , p); else printf("Нет, это %s\n", base); /* еще более изощренный вариант: */ if( !strcmp(p,(base=strrchr(s,'/')) ? ++base : (base=s)) ) printf("Yes %s\n", p); else printf("No %s\n", base); } 2.48. Напишите макрос substr(to,from,n,len) который записывает в to кусок строки from начиная с n-ой позиции и длиной len. Используйте стандартную функцию strncpy. Ответ: #define substr(to, from, n, len) strncpy(to, from+n, len) или более корректная функция: А. Богатырев, 1992-95 - 103 - Си в UNIX char *substr(to, from, n, len) char *to, *from; { int lfrom = strlen(from); if(n < 0 ){ len += n; n = 0; } if(n >= lfrom || len <= 0) *to = '\0'; /* пустая строка */ else{ /* длина остатка строки: */ if(len > lfrom-n) len = lfrom - n; strncpy(to, from+n, len); to[len] = '\0'; } return to; } 2.49. Напишите функцию, проверяющую, оканчивается ли строка на ".abc", и если нет - приписывающую ".abc" к концу. Если же строка уже имеет такое окончание - ничего не делать. Эта функция полезна для генерации имен файлов с заданным расширением. Сде- лайте расширение аргументом функции. Для сравнения конца строки s со строкой p следует использовать: int ls = strlen(s), lp = strlen(p); if(ls >= lp && !strcmp(s+ls-lp, p)) ...совпали...; 2.50. Напишите функции вставки символа c в указанную позицию строки (с раздвижкой строки) и удаления символа в заданной позиции (со сдвижкой строки). Строка должна изменяться "на месте", т.е. никуда не копируясь. Ответ: /* удаление */ char delete(s, at) register char *s; { char c; s += at; if((c = *s) == '\0') return c; while( s[0] = s[1] ) s++; return c; } /* либо просто strcpy(s+at, s+at+1); */ /* вставка */ insert(s, at, c) char s[], c; { register char *p; s += at; p = s; while(*p) p++; /* на конец строки */ p[1] = '\0'; /* закрыть строку */ for( ; p != s; p-- ) p[0] = p[-1]; *s = c; } 2.51. Составьте программу удаления символа c из строки s в каждом случае, когда он встречается. Ответ: А. Богатырев, 1992-95 - 104 - Си в UNIX delc(s, c) register char *s; char c; { register char *p = s; while( *s ) if( *s != c ) *p++ = *s++; else s++; *p = '\0'; /* не забывайте закрывать строку ! */ } 2.52. Составьте программу удаления из строки S1 каждого символа, совпадающего с каким-либо символом строки S2. 2.53. Составьте функцию scopy(s,t), которая копирует строку s в t, при этом символы табуляции и перевода строки должны заменяться на специальные двухсимвольные последо- вательности "\n" и "\t". Используйте switch. 2.54. Составьте функцию, которая "укорачивает" строку, заменяя изображения спецсим- волов (вроде "\n") на сами эти символы ('\n'). Ответ: extern char *strchr(); void unquote(s) char *s; { static char from[] = "nrtfbae", to [] = "\n\r\t\f\b\7\33"; char c, *p, *d; for(d=s; c = *s; s++) if( c == '\\'){ if( !(c = *++s)) break; p = strchr(from, c); *d++ = p ? to[p - from] : c; }else *d++ = c; *d = '\0'; } 2.55. Напишите программу, заменяющую в строке S все вхождения подстроки P на строку Q, например: P = "ура"; Q = "ой"; S = "ура-ура-ура!"; Результат: "ой-ой-ой!" 2.56. Кроме функций работы со строками (где предполагается, что массив байт заверша- ется признаком конца '\0'), в Си предусмотрены также функции для работы с массивами байт без ограничителя. Для таких функций необходимо явно указывать длину обрабатывае- мого массива. Напишите функции: пересылки массива длиной n байт memcpy(dst,src,n); заполнения массива символом c memset(s,c,n); поиска вхождения символа в массив memchr(s,c,n); сравнения двух массивов memcmp(s1,s2,n); Ответ: #define REG register char *memset(s, c, n) REG char *s, c; { REG char *p = s; while( --n >= 0 ) *p++ = c; return s; } char *memcpy(dst, src, n) REG char *dst, *src; REG int n; { REG char *d = dst; А. Богатырев, 1992-95 - 105 - Си в UNIX while( n-- > 0 ) *d++ = *src++; return dst; } char *memchr(s, c, n) REG char *s, c; { while(n-- && *s++ != c); return( n < 0 ? NULL : s-1 ); } int memcmp(s1, s2, n) REG char *s1, *s2; REG n; { while(n-- > 0 && *s1 == *s2) s1++, s2++; return( n < 0 ? 0 : *s1 - *s2 ); } Есть такие стандартные функции. 2.57. Почему лучше пользоваться стандартными функциями работы со строками и памятью (strcpy, strlen, strchr, memcpy, ...)? Ответ: потому, что они обычно реализованы поставщиками системы ЭФФЕКТИВНО, то есть написаны не на Си, а на ассемблере с использованием специализированных машинных команд и регистров. Это делает их более быстрыми. Написанный Вами эквивалент на Си может использоваться для повышения мобильности программы, либо для внесения поправок в стандартные функции. 2.58. Рассмотрим программу, копирующую строку саму в себя: #include <stdio.h> #include <string.h> char string[] = "abcdefghijklmn"; void main(void){ memcpy(string+2, string, 5); printf("%s\n", string); exit(0); Она печатает abababahijklmn. Мы могли бы ожидать, что кусок длины 5 символов "abcde" будет скопирован как есть: ab[abcde]hijklmn, а получили ab[ababa]hijklmn - цикличес- кое повторение первых двух символов строки... В чем дело? Дело в том, что когда области источника (src) и получателя (dst) перекрываются, то в некий момент *src берется из УЖЕ перезаписанной ранее области, то есть испорченной! Вот программа, иллюстрирующая эту проблему: А. Богатырев, 1992-95 - 106 - Си в UNIX #include <stdio.h> #include <string.h> #include <ctype.h> char string[] = "abcdefghijklmn"; char *src = &string[0]; char *dst = &string[2]; int n = 5; void show(int niter, char *msg){ register length, i; printf("#%02d %s\n", niter, msg); length = src-string; putchar('\t'); for(i=0; i < length+3; i++) putchar(' '); putchar('S'); putchar('\n'); printf("\t...%s...\n", string); length = dst-string; putchar('\t'); for(i=0; i < length+3; i++) putchar(' '); putchar('D'); putchar('\n'); } void main(void){ int iter = 0; while(n-- > 0){ show(iter, "перед"); *dst++ = toupper(*src++); show(iter++, "после"); } exit(0); } Она печатает: А. Богатырев, 1992-95 - 107 - Си в UNIX #00 перед S ...abcdefghijklmn... D #00 после S ...abAdefghijklmn... D #01 перед S ...abAdefghijklmn... D #01 после S ...abABefghijklmn... D #02 перед S ...abABefghijklmn... D #02 после S ...abABAfghijklmn... D #03 перед S ...abABAfghijklmn... D #03 после S ...abABABghijklmn... D #04 перед S ...abABABghijklmn... D #04 после S ...abABABAhijklmn... D Отрезки НЕ перекрываются, если один из них лежит либо целиком левее, либо целиком правее другого (n - длина обоих отрезков). dst src src dst ######## @@@@@@@@ @@@@@@@@ ######## dst+n <= src или src+n <= dst dst <= src-n или dst >= src+n Отрезки перекрываются в случае ! (dst <= src - n || dst >= src + n) = (dst > src - n && dst < src + n) При этом опасен только случай dst > src. Таким образом опасная ситуация описывается условием src < dst && dst < src + n (если dst==src, то вообще ничего не надо делать). Решением является копирование "от А. Богатырев, 1992-95 - 108 - Си в UNIX хвоста к голове": void bcopy(register char *src, register char *dst, register int n){ if(dst >= src){ dst += n-1; src += n-1; while(--n >= 0) *dst-- = *src--; }else{ while(n-- > 0) *dst++ = *src++; } } Или, ограничиваясь только опасным случаем: void bcopy(register char *src, register char *dst, register int n){ if(dst==src || n <= 0) return; if(src < dst && dst < src + n) { dst += n-1; src += n-1; while(--n >= 0) *dst-- = *src--; }else memcpy(dst, src, n); } Программа #include <stdio.h> #include <string.h> #include <ctype.h> char string[] = "abcdefghijklmn"; char *src = &string[0]; char *dst = &string[2]; int n = 5; void show(int niter, char *msg){ register length, i; printf("#%02d %s\n", niter, msg); length = src-string; putchar('\t'); for(i=0; i < length+3; i++) putchar(' '); putchar('S'); putchar('\n'); printf("\t...%s...\n", string); length = dst-string; putchar('\t'); for(i=0; i < length+3; i++) putchar(' '); putchar('D'); putchar('\n'); } А. Богатырев, 1992-95 - 109 - Си в UNIX void main(void){ int iter = 0; if(dst==src || n <= 0){ printf("Ничего не надо делать\n"); return; } if(src < dst && dst < src + n) { dst += n-1; src += n-1; while(--n >= 0){ show(iter, "перед"); *dst-- = toupper(*src--); show(iter++, "после"); } }else while(n-- > 0){ show(iter, "перед"); *dst++ = toupper(*src++); show(iter++, "после"); } exit(0); } Печатает А. Богатырев, 1992-95 - 110 - Си в UNIX #00 перед S ...abcd