1

В продолжение изучения языка си я встретил такой код: "Split string with delimiters in C" Он отлично работает, но когда я компилирую программу, то получаю следующее предупреждение:

split.c: In function ‘split’: split.c:44:7: warning: assignment from incompatible pointer type [enabled by default] res = (char *)calloc(count,sizeof(char));

Я, конечно, заметил, что-то неправильно с распределением памяти и указателями - calloc для указателя на указатель, но я не могу понять, что именно неправильно и как сделать без багов. Не могли бы вы помочь прояснить ситуацию?

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

/**
 *  splits str on delim and dynamically allocates an array of pointers.
 *
 *  On error -1 is returned, check errno
 *  On success size of array is returned, which may be 0 on an empty string
 *  or 1 if no delim was found.
 *
 *  You could rewrite this to return the char ** array instead and upon NULL
 *  know it's an allocation problem but I did the triple array here.  Note that
 *  upon the hitting two delim's in a row "foo,,bar" the array would be:
 *  { "foo", NULL, "bar" }
 *
 *  You need to define the semantics of a trailing delim Like "foo," is that a
 *  2 count array or an array of one?  I choose the two count with the second entry
 *  set to NULL since it's valueless.
 *  Modifies str so make a copy if this is a problem
 */
size_t split(
    char * str,
    char delim,
    char ***array,
    size_t *length
)
{
    char *p = NULL;
    char **res = NULL;
    size_t count = 0;

    p = str;
    // Count occurance of delim in string
    while((p=strchr(p,delim)) != NULL) {
        *p = 0; // Null terminate the deliminator.
        p++; // Skip past our new null
        count++;
    }

    // allocate dynamic array
    if(count > 0){
        res = (char *)calloc(count,sizeof(char));
        if(!res){
            printf("Error: can't allocate memory!");
            exit(EXIT_FAILURE);
        }
    }else{
        return(0);
    }

    p = str;
    for(size_t k=0; k<count; k++ ) {
        if( *p ) res[k] = p;  // Copy start of string
        p = strchr(p, 0 );    // Look for next null
        p++; // Start of next string
    }

    *array = res;
    *length = count;

    return(0);
}

int main(void)
{
    char **res = NULL;
    size_t count = 0;
    size_t rc = 0;

    char str[] = "JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC,";

    rc = split(str,',',&res,&count );
    if(rc) {
        printf("Error: %s errno: %d \n", strerror(errno), errno);
    }

    printf("count: %zu\n", count );
    for(size_t k=0; k<count; k++) {
        printf("str: %s\n", res[k]);
    }

    free(res);
    return(0);
}
Dennis V
  • 287
  • char ** res; ...; res = (char*) calloc(...). res - указатель на указатель, а приводите вы к указателю на char. – andy.37 Jun 14 '17 at 07:13
  • Нужно res = (char**) calloc(count, sizeof(char*)); – andy.37 Jun 14 '17 at 07:18
  • Спасибо @andy.37 ! Действительно всё получилось! ) – Dennis V Jun 14 '17 at 07:21
  • Кстати, там была фактическая ошибка, т.к. sizeof(char) по определению равно 1. – andy.37 Jun 14 '17 at 07:22
  • По-факту оно так, но нигде в стандартах это не фиксируется. На буржуйских сайтах много флейма на эту тему. Перфекционисты рекомендуют именно sizeof(char), чтобы обеспечить 100% кросплатформенность. Т.к. эта операция выполняется только один раз в момент компиляции, то на производительности финального кода никак не сказывается. – Dennis V Jun 14 '17 at 07:26
  • @andy.37 напишите, пожалуйста, Ваш комментарий в виде ответа, чтобы я мог сделать отметку о том, что вопрос успешно решён. – Dennis V Jun 14 '17 at 07:31
  • @Dennis V.R.: "Нигде в стандартах не фиксируется" что именно? Стандарты языков С и С++ строго открытым текстом гарантируют, что sizeof(char) всегда равно 1. Это одно из фундаментальнейших свойств модели памяти и объектной модели С и С++. – AnT stands with Russia Jun 14 '17 at 08:14
  • Не исключено, что завтра выйдет новая железка, в компилляторе к которой char будет 2 байта. Странно, но ничего с этим не поделаешь. Например, для x86 long int всегда 32 бита. Кто мог предположить, что для x64 тот же long int будет уже 64 бита?! Ещё один аргумент: если прийдётся в будущем мигрировать этот код на wchar_t, например, то можно сделать серьёзную ошибку, пропустив sizeof(wchar_t). А так, правя код через поиск/замену вероятность ошибки сводится на нет. – Dennis V Jun 14 '17 at 09:45
  • @DennisV.R. В том то и суть, что сколько-бы байт/бит не занимал в памяти объект типа char, sizeof(char) обязан быть равен 1. Можно считать это "определением 1 в языках С/С++" (касательно sizeof). – andy.37 Jun 14 '17 at 17:15
  • Пруфлинк в студию! ) @andy.37 У меня есть противоположные утверждения – Dennis V Jun 15 '17 at 05:39
  • @DennisV.R. Стандарт С++ п 5.3.3 цитата: "sizeof(char), sizeof(signed сhar) and sizeof(unsigned char) are 1." ССылка: http://www.open-std.org/Jtc1/sc22/wg21/docs/papers/2013/n3797.pdf Значение sizeof(char) НЕ ИМЕЕТ ОТНОШЕНИЯ к реальному размеру char в битах/байтах. – andy.37 Jun 15 '17 at 05:47

1 Answers1

3
  • Просто оставьте карго-культовую привычку явно приводить тип результата функции выделения памяти. В языке С это - дурной тон. А также возьмите за правило размер выделяемой памяти вычислять через sizeof *указатель, а не путем явного указания типа под sizeof. Идиоматическое выделение памяти через calloc в С выглядит так

    res = calloc(count, sizeof *res);
    

    Это сделает код типонезависимым и тем самым избавит вас от ошибок типизации. А также исключит необходимость "перелопачивания" кода после смены типа указателя (если вдруг его придется изменить).

  • Логика

    if( *p ) res[k] = p;
    

    при формировании выходного массива приведет к тому, что пустые строки будут представлены в res нулевыми указателями. Тогда печать результата работы функции без проверки на нулевой указатель - это слишком "смело".

  • Также можно заметить, что если входной список пуст, то функция завершается по return досрочно, но при этом забывает проинициализировать нулем выходные значения *length и *array. Вся надежда на то, что вызывающий код не забудет их заранее проинициализировать. Это странно.

  • Отдельно режут глаз скобки вокруг аргумента return. Хотя роль возвращаемого значения в вашей функции вообще не ясна - она всегда возвращает 0, если не завершается по exit. При этом в вызывающем коде вы зачем-то проверяете возвращенное значение... Зачем?

P.S. И приведение типа функции выделения памяти, и скобки вокруг аргумента return - пережитки первых младенческих версий С ("это было давно и неправда"), в которых функции выделения памяти возвращали char *, а синтаксис return требовал этих скобок.

  • 1
    большое спасибо за ответ и комментарии. Касательно явного приведения на сишных ресурсах очень много споров. В основном все утверждают что НЕ нужно этого делать. Но есть и другие мнения, объясняющие почему это НУЖНО делать. Вот одно из таких, достаточно хорошо обоснованных мнений. Просто захотелось это здесь упомянуть, потому что не всё так однозначно. – Dennis V Jun 14 '17 at 09:58
  • Касательно скобок, оказывается, тоже не всё так однозначно. Олдскульная опция )) Спасибо @AnT раньше, даже, не обращал внимание что можно без скобок. – Dennis V Jun 14 '17 at 10:07
  • При этом в вызывающем коде вы зачем-то проверяете возвращенное значение... Зачем? Полностью согласен. Дело в том, что это заимствованный код. Я цитировал его с минимумом изменений, чтобы не упустить суть. – Dennis V Jun 14 '17 at 10:10
  • Подскажите, пожалуйста, @AnT, для пунктов 2 и 3 Вашего ответа как должен выглядеть код правильно? Сорри за ламерский вопрос. Я понимаю зачем это нужно, но уровень знания си пока не позволяет самостоятельно написать код проверок. Или, если не затруднит, оставьте, пожалуйста, ссылочку на документацию, литературу или примеры в чужом коде. – Dennis V Jun 14 '17 at 11:02
  • 1
    @Dennis V.R: Единственное соображение в пользу приведения типа, которое я вижу по вашей ссылке - это написание кросс-компилируемого С/С++ кода. Но с этим никто и не спорит. Однако применимо одно только к кросс-компилируемому С/С++ коду и никоим образом не оправдывает огульное использование таких приведений. Второе соображение - мимо кассы, ибо вышеприведенная идиома выделения памяти (типонезависимый вариант) полностью свободна от ошибок несоответствия типа. Третий пункт - не аргумент вообще, а просто напускание дыма. Четвертый - замаскированное повторение второго. – AnT stands with Russia Jun 14 '17 at 15:38
  • Пятый пункт - правилен, но опять же говорит об "assertions", призванных ловить проблемы типозависисмого кода. Воспользуйтесь же типонезависимым вариантом - и потенциал для ошибки сразу исчезнет и не надо будет ничего ловить никакими "assertions". Другими словами, автор ответа по вашей ссылке оправдывает этот каст, как решение проблем, которые этот автор сам же себе и создал на ровном месте. Не надо создавать себе проблем на ровном месте - и вам не понадобиться искать для них решения. – AnT stands with Russia Jun 14 '17 at 15:50
  • 1
    Что касается моего ответа: мой пункт 2 не говорит, что у вас что-то "неправильно". Это вопрос соглашения: что делать, если во входе найдена пустая строка (два разделителя подряд)? Можно возвращать null в массиве на этом месте. Можно возвращать указатель на пустую строку. И то, и другое - приемлемо. Ваш код возвращает null. Это нормально, но тогда при работе с результатом вашей функции (в вызывающем коде) имеет смысл проверять элементы массива на null. На вашей входной строке там не будет null. На какой-то другой - может быть... – AnT stands with Russia Jun 14 '17 at 15:54
  • 1
    Мой пункт 3: просто перед досрочным выходом из функции сделайте *length = 0; *array = NULL;. – AnT stands with Russia Jun 14 '17 at 15:56
  • 1
    @Ant, присоединяюсь к первому комментарию. Хотя я сам сторонник типонезависимого выделения памяти, я знаю как минимум одну проблему, которую оно не решит, и будете точно так же бегать по коду и менять. Можете ещё обнаружить и вторую проблему, из-за которой сейчас идёт дискуссия в списке рассылки ядра Linux касательно как раз типонезависимого выделения. – 0andriy Jun 14 '17 at 19:59