Вопросы начинающего - функции для работы со строковыми лексемами

Andrey Vahromkin
Дата: 09.05.2015 16:10:35
Доброго времени суток, уважаемые!

На старости лет возникла необходимость переписать часть имеющихся наработок на perl/pascal на языке Си - "политика партии" поменялась, плюс кое-где возникают проблемы с производительностью. Плюс есть желание к саморазвитию. Засел опять за книги, но дело идёт туго, Си для меня оказался очень причудливым языком, в особенности в части работы со строковыми данными, а как раз обработка строк в моих программах - краеугольный камень.
Для начала попытался написать несколько функций, которые занимаются подсчетом лексем в строке, вычисляют позицию заданной лексемы в строке и извлекают из строки лексему по порядковому номеру. И вроде бы всё даже работает, но поскольку некоторые моменты работы с указателями я постиг интуитивно, хотелось бы услышать мнение более опытных программистов о качестве данного кода, какие ошибки я мог допустить, и т.п. В общем - посмотрите, пожалуйста, и прокомментируйте, что стоит изменить-исправить.
Заранее спасибо.
Да, ещё момент - возможно, я изобретаю велосипед (хотя что-то не находил пока готовых библиотек с таким функционалом) - если так, то с благодарностью приму ссылки на готовенькое-проверенное, и тем не менее, все равно прошу прокомментировать мой код - учиться-то надо...

Собственно, код:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define STR_BUFFER 256                // максимальная длина строки
#define WD " ,.:;?!\t\n\r\"\"\'"      // символы-разделители

// Функции для работы с лексемами

int wordcount(const char *str, const char *worddelim)
/*
 Возвращает число лексем в строке str, разделенных разделителями worddelim
*/
{
  int i, count, len;

  count = i = 0;
  len = strlen(str);

  while (i<=len)
    {
      while ((i<=len) && (strchr(worddelim, str[i]) != NULL)) i++; // пока текущий символ из worddelim, двигаемся к концу
      if (i<=len) count++;                                         // начало лексемы, увеличиваем счётчик лексем
      while ((i<=len) && (strchr(worddelim, str[i]) == NULL)) i++; // пока текущий символ НЕ из worddelim, двигаемся к концу
    }
  return count;
}

int wordpos(const unsigned int n, const char *str, const char *worddelim)
/*
 Возвращает позицию лексемы номер n в строке str с разделителями worddelim
 Если лексемы номер n не существует, возвращает -1
*/
{
  int i, count, len;

  count = i = 0;
  len = strlen(str);

  while ((i<=len) && (count != n))
    {
      while ((i<=len) && (strchr(worddelim, str[i]) != NULL)) i++;   // пока текущий символ из worddelim, двигаемся к концу
      if (i<=len) count++;                                           // начало лексемы, увеличиваем счётчик лексем
      if (count != n)                                                // если это не лексема номер n - ищем следующую.
        while ((i<=len) && (strchr(worddelim, str[i]) == NULL)) i++; // пока текущий символ НЕ из worddelim, двигаемся к концу
      else
        return i;                                                    // это лексема номер n - возвращаем её позицию
    }
  return -1;                                                         // нет лексемы с номером n - возвращаем -1
}

char *extractword(const unsigned int n, char *str, char *worddelim)
/*
 Извлекает лексему номер n из строки str с разделителями worddelim и возвращает указатель на нее
 Если лексемы номер n нет, возвращает NULL
*/
{
  int i, j, len;
  char *word;

  word = NULL;
  len = strlen(str);
  i = wordpos(n, str, worddelim); // ищем позицию лексемы номер n

  if (i == -1) return word;      // лексемы номер n нет - возвращаем NULL

  if (wordpos(n+1, str, worddelim) != -1) j = wordpos(n+1, str, worddelim)-i; 
  // лексема не последняя - ее длина равна позиция следующей лексемы минус позиция искомой лексемы
  else j = len - i;
  // лексема последняя - ее длина равна длина строки минус позиция искомой лексемы

  word = (char *) calloc(j+1, sizeof(char)); // выделяем память под искомую лексему - длина лексемы плюс '\0'
  j = 0;
  while ((i<len) && (strchr(worddelim, str[i]) == NULL)) word[j++] = str[i++]; 
  word[strlen(word)] = '\0'; // копируем лексему из строки str во временную переменную word и завершаем ее '\0'
  return word;
}

// Проверяем, как это работает

int main()
{
  int i, wcount;
  char *s, *d1;

  s = (char *) calloc(STR_BUFFER, sizeof(char));
  d1 = (char *) calloc(STR_BUFFER, sizeof(char));

  strncpy(s, "Hello, my crazy world!", STR_BUFFER);
  printf(" String - \"%s\", delimeters - \"%s\"\n\n", s, WD);

  wcount = wordcount(s, WD);
  printf(" Word count - %u\n\n", wcount);

  for (i = 1; i <= 5; i++)
  {
    printf(" Word number %i start position - %i\n", i, wordpos(i, s, WD));
  }
  printf("\n");

  for (i = 1; i <= wcount; i++)
  {
    d1 = extractword(i, s, ". ");
    printf(" Word number %d - %s\n", i, d1);
  }

  free(s); free(d1);

  return 0;
}


Результат:

String - "Hello, my crazy world!", delimeters - " ,.:;?!
""'"

Word count - 4

Word number 1 start position - 0
Word number 2 start position - 7
Word number 3 start position - 10
Word number 4 start position - 16
Word number 5 start position - -1

Word number 1 - Hello,
Word number 2 - my
Word number 3 - crazy
Word number 4 - world!

YesSql
Дата: 09.05.2015 17:43:14
Andrey Vahromkin,

есть готовая функция в стандартной библиотеке C/C++ strtok
Andrey Vahromkin
Дата: 10.05.2015 11:20:41
YesSql
есть готовая функция в стандартной библиотеке C/C++ strtok

Благодарю, о наличии этой функции я знаю, здесь я сознательно пошёл на изобретение велосипеда, дабы лучше понять принципы работы с указателями и строками.
Andrey Vahromkin
Дата: 10.05.2015 12:24:36
Для работы с strtok я написал вот такую обертку:
char *extractword2(const unsigned int n, char *str, char *worddelim)
/*
 Извлекает лексему номер n из строки str с разделителями worddelim с помощью strtok и возвращает указатель на нее
 Если лексемы номер n нет, возвращает NULL
*/
{
  int i;
  char *s, *word;

  s = strdup(str);
  word = strtok(s, worddelim);
  if (n == 1) return word;
  for (i = 2; i <= n; i++) word = strtok(NULL, worddelim);
  return word;
}

И кстати, здесь у меня появился ещё один вопрос: я правильно понимаю - все объявленные внутри функции переменные и выделяемая им память после выхода из функции уничтожаются автоматически, а вся распределенная память также автоматически освобождается и в вызовах free перед выходом из функции нет нужды?
YesSql
Дата: 10.05.2015 13:53:28
Andrey Vahromkin,

А что делает эта обертка? Кроме того что повторяет одно и то же. Зачем она нужна?
Dimitry Sibiryakov
Дата: 10.05.2015 14:05:50

Andrey Vahromkin
дабы лучше понять принципы работы с указателями и строками.

Дабы понять принципы работы с указателями, достаточно иметь представление об устройстве
оперативной памяти хотя бы на школьном уровне: в виде ряда пронумерованных коробочек.
Указатель это и есть коробочка, хранящая номер другой коробочки.

Posted via ActualForum NNTP Server 1.5

Andrey Vahromkin
Дата: 10.05.2015 15:34:47
YesSql
А что делает эта обертка? Кроме того что повторяет одно и то же. Зачем она нужна?

Мне кажется, это самоочевидно.
Допустим, нужно извлечь из строки source "aaa bbb ccc ddd eee" вторую и четвертую лексему.
Это можно сделать так:
  int i;
  char *source, *w1, *w2, *s;
  [...]
  s = strdup(source);
  w1 = strtok(s, worddelim);
  w1 = strtok(NULL, worddelim);

  s = strdup(source);
  w2 = strtok(s, worddelim);
  for (i = 2; i<=4; i++) 
    w2 = strtok(NULL, worddelim);

Или так:
  int i;
  char *source, *w1, *w2;

  w1 = extractword2(2, source, worddelim);
  w2 = extractword2(4, source, worddelim);


Второй вариант с функцией-оберткой над strtok _мне_ нравится больше. Потому что строка может быть существенно длиннее, и нужно извлекать из нее не 2, а 12 слов.
Если вы считаете, что лучше первый вариант - поясните, почему...

Dimitry Sibiryakov
Дабы понять принципы работы с указателями, достаточно иметь представление об устройстве
оперативной памяти хотя бы на школьном уровне

Практика написания программ на Си _очень_ отличается от практики паскаля и даже Перла... Мне пока _очень_ сложно работать со строками на Си.
Dimitry Sibiryakov
Дата: 10.05.2015 16:05:04

Andrey Vahromkin
Практика написания программ на Си _очень_ отличается от практики
паскаля и даже Перла.

Это потому, что в Си нет строк.

Posted via ActualForum NNTP Server 1.5

Andrey Vahromkin
Дата: 10.05.2015 16:11:13
Dimitry Sibiryakov
Andrey Vahromkin
Практика написания программ на Си _очень_ отличается от практики
паскаля и даже Перла.

Это потому, что в Си нет строк.

Да, я заметил.
Anatoly Moskovsky
Дата: 10.05.2015 16:33:31
Andrey Vahromkin
И кстати, здесь у меня появился ещё один вопрос: я правильно понимаю - все объявленные внутри функции переменные и выделяемая им память после выхода из функции уничтожаются автоматически, а вся распределенная память также автоматически освобождается и в вызовах free перед выходом из функции нет нужды?

Нет. Неправильно поняли.
Если вы выделили память напрямую через malloc() или неявно через одну из стандартных функций (например strdup и многие другие - см. доки соотв. функций) то и удалять ее надо через free() вручную.