Новые книги

Книга эта непростая и подойдет не каждому. Автор анализирует то, к чему мы все давно привыкли до автоматизма, и объясняет, что интерфейс многих современных программ далек от совершенства. Как его улучшить, в каком направлении двигаться дальше? Попробуйте найти ответы вместе с самым известным специалистом в этой области – Джефом Раскиным, создателя проекта Apple Macintosh.

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

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

Массивы, строки, указатели.

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;

Обратите на этот пример особое внимание, поскольку, описав указатель (но никуда его не направив), новички успокаиваются, не заботясь о выделении памяти для хранения данных. Указатель должен указывать на ЧТО-ТО, в чем можно хранить данные, а не "висеть", указывая "пальцем в небо"! Запись информации по "висячему" указателю разрушает память программы и приводит к скорому (но часто не немедленному и потому таинственному) краху.

Вот программа, которая также использует неинициализированный указатель. На машине 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;
    }
Заметьте, что 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)
или более корректная функция:
    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 в каждом случае, когда он встречается.

Ответ:

    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;
          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 берется из УЖЕ перезаписанной ранее области, то есть испорченной! Вот программа, иллюстрирующая эту проблему:

    #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);
    }
Она печатает:
    #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, то вообще ничего не надо делать). Решением является копирование "от хвоста к голове":
    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');
    }
    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);
    }
Печатает
    #00 перед
                   S
            ...abcdefghijklmn...
                     D
    #00 после
                  S
            ...abcdefEhijklmn...
                    D
    #01 перед
                  S
            ...abcdefEhijklmn...
                    D
    #01 после
                 S
            ...abcdeDEhijklmn...
                   D
    #02 перед
                 S
            ...abcdeDEhijklmn...
                   D
    #02 после
                S
            ...abcdCDEhijklmn...
                  D
    #03 перед
                S
            ...abcdCDEhijklmn...
                  D
    #03 после
               S
            ...abcBCDEhijklmn...
                 D
    #04 перед
               S
            ...abcBCDEhijklmn...
                 D
    #04 после
              S
            ...abABCDEhijklmn...
                D

Теперь bcopy() - удобная функция для копирования и сдвига массивов, в частности массивов указателей. Пусть у нас есть массив строк (выделенных malloc-ом):

    char *lines[NLINES];
Тогда циклическая перестановка строк выглядит так:
    void scrollUp(){
            char *save = lines[0];
            bcopy((char *) lines+1, /* from */
                  (char *) lines,   /* to */
                  sizeof(char *) * (NLINES-1));
            lines[NLINES-1] = save;
    }
    void scrollDown(){
            char *save = lines[NLINES-1];
            bcopy((char *) &lines[0], /* from */
                  (char *) &lines[1], /* to */
                  sizeof(char *) * (NLINES-1));
            lines[0] = save;
    }

Возможно, что написание по аналогии функции для копирования массивов элементов типа (void *) - обобщенных указателей - может оказаться еще понятнее и эффективнее. Такая функция - memmove - стандартно существует в UNIX SVR4. Заметьте, что порядок аргументов в ней обратный по отношению к bcopy. Следует отметить, что в SVR4 все функции mem... имеют указатели типа (void *) и счетчик типа size_t - тип для количества байт (вместо unsigned long); в частности длина файла имеет именно этот тип (смотри системные вызовы lseek и stat).

    #include <sys/types.h>
    void memmove(void *Dst, const void *Src,
                 register size_t n){
                register caddr_t src = (caddr_t) Src,
                                 dst = (caddr_t) Dst;
                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);
    }
caddr_t - это тип для указателей на БАЙТ, фактически это (unsigned char *). Зачем вообще понадобилось использовать caddr_t? Затем, что для
    void *pointer;
    int n;
значение
    pointer + n

не определено и невычислимо, ибо sizeof(void) не имеет смысла - это не 0, а просто ошибка, диагностируемая компилятором!

2.59.

Еще об опечатках: вот что бывает, когда вместо знака `=' печатается `-' (на клавиатуре они находятся рядом...).

    #include <stdio.h>
    #include <strings.h>
    char *strdup(const char *s){
            extern void *malloc();
            return strcpy((char *)malloc(strlen(s)+1), s);
    }
    char *ptr;
    void main(int ac, char *av[]){
            ptr - strdup("hello"); /* подразумевалось ptr = ... */
            *ptr = 'H';
            printf("%s\n", ptr);
            free(ptr);
            exit(0);
    }

Дело в том, что запись (а часто и чтение) по *pointer, где pointer==NULL, приводит к аварийному прекращению программы. В нашей программе ptr осталось равным NULL - указателем в никуда. В операционной системе UNIX на машинах с аппаратной защитой памяти, страница памяти, содержащая адрес NULL (0) бывает закрыта на запись, поэтому любое обращение по записи в эту страницу вызывает прерывание от диспетчера памяти и аварийное прекращение процесса. Система сама помогает ловить ваши ошибки (но уже во время выполнения программы). Это ОЧЕНЬ частая ошибка - запись по адресу NULL. MS DOS в таких случаях предпочитает просто зависнуть, и вы бываете вынуждены играть аккорд из трех клавиш - Ctrl/Alt/Del, так и не поняв в чем дело.

© Copyright А. Богатырев, 1992-95
Си в UNIX

Назад | Содержание | Вперед