Динамически выделять `char*` с конкатенацией и литералами формата?

С поддержкой macOS, Windows (MSVC) и Linux, как мне сделать следующее?

char *s;
func(&s, "foo");
if (<condition>) func(&s, "bar%s", "can")
/* want "foobarcan", and I don't know `strlen(s)` AoT */

Я пробовал с asprintf (смог найти реализацию MSVC), но, похоже, это не сработало для такого рабочего процесса. fopencookie и funopen кажутся удобными, но недоступными в MSVC.

Может быть, есть какой-то чистый способ с realloc создать NUL, заканчивающийся char* в C?

Вы не можете передать char *s и realloc() внутри функции, не возвращая и не перезаписывая исходный адрес, удерживаемый указателем. (либо передайте адрес s, например, char **s, либо измените тип возвращаемого значения функции на char * и верните перераспределенное s. В противном случае вы должны знать длину s, чтобы не выходить за пределы s. Вы не можете вызывать realloc() на s для начального распределения, если только s не инициализирован NULL.

David C. Rankin 23.03.2022 06:05

Используйте vsnprintf с n=0, чтобы определить длину выходной строки. Тогда malloc не менее length+1 байт памяти. Вызовите vsnprintf еще раз с правильным значением n, чтобы создать строку. Наконец, free предыдущая строка, если она есть. Кстати, вам нужно инициализировать s, например. char *s = NULL;

user3386109 23.03.2022 06:19
func(&s, "foo") и func(&s, "bar%s", "can") -- C не допускает перегрузки функций, как C++, поэтому, вероятно, вы имеете в виду func1(...) и func2(...)?
David C. Rankin 23.03.2022 06:58

Дэвид: да. Я просто использовал эту настройку для соответствия asprintf, это не обязательно должны быть эти типы (и можно инициализировать NULL или 0). user3386109 хорошо, так что это самый чистый способ?

A T 23.03.2022 15:02

@AT Это самый чистый способ выяснить, сколько памяти вам нужно. Обратите внимание, что я бы использовал malloc для выделения буфера, а не realloc. Это позволяет вам делать такие вещи, как добавление существующей строки к себе, например. char *s = NULL; func(&s, "foo"); func(&s, "%s", s); создаст строку «foofoo».

user3386109 23.03.2022 19:43
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
47
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как указано в комментариях, (v)snprintf всегда возвращает количество байтов, которые были записаны в бы (исключая нулевой завершающий байт), даже если он усечен. Это приводит к тому, что предоставление функции с аргументом size, равным 0, возвращает длину форматируемой строки.

Используя это значение плюс длину нашей существующей строки (если применимо) плюс единицу, мы (повторно) выделяем соответствующий объем памяти.

Для конкатенации просто напечатайте отформатированную строку с правильным смещением.

Пример без проверки ошибок.

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

char *dstr(char **unto, const char *fmt, ...) {
    va_list args;
    size_t base_length = unto && *unto ? strlen(*unto) : 0;

    va_start(args, fmt);
                /* check length for failure */
    int length = vsnprintf(NULL, 0, fmt, args);
    va_end(args);

                /* check result for failure */
    char *result = realloc(unto ? *unto : NULL, base_length + length + 1);

    va_start(args, fmt);
                /* check for failure*/
    vsprintf(result + base_length, fmt, args);
    va_end(args);

    if (unto)
        *unto = result;

    return result;
}

int main(void) {
    char *s = dstr(NULL, "foo");

    dstr(&s, "bar%s%d", "can", 7);

    printf("[[%s]]\n", s);

    free(s);
}

stdout:

[[foobarcan7]]

Оговорка здесь в том, что вы не можете написать:

char *s;
dstr(&s, "foo");

s должен быть инициализирован как NULL, или функция должна использоваться непосредственно как инициализатор с первым аргументом, установленным на NULL.

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

Пример эксплойта:

/* exploit */
char buf[128];
fgets(buf, sizeof buf, stdin);

char *str = dstr(NULL, buf);
puts(str);
free(str);

stdin:

%d%s%s%s%s%d%p%dpapdpasd%d%.2f%p%d

Результат: Неопределенное поведение

Также стоит отметить, что s не может быть NULL, если строка, переданная в fmt, содержит для нее спецификатор преобразования (например, "%s%s"). Это становится немного глубже, так как вам нужен способ анализа fmt, чтобы определить количество присутствующих спецификаторов преобразования, а затем сопоставить это с заданными аргументами, отличными от NULL. (угловой шкаф)

David C. Rankin 23.03.2022 07:20

@DavidC.Rankin Извините, я не уверен, что полностью понимаю. Не могли бы вы уточнить «fmt содержит спецификатор преобразования»? Я думаю, что пример вызова, который вызывает описанное поведение, поможет.

Oka 23.03.2022 07:41

Конечно, скажем, str — это NULL, и вы хотите объединить его с "foo" в func (&s, "%s%s", "foo"), чтобы получить "foo". В этом случае ваш vsprintf с fmt из "%s%s" должен включать как *s, так и "foo", но *s — это NULL. Результирующая строка будет "(null)foo". Таким образом, вопрос, показывающий, что func(&s, "foo"); и func(&s, "bar%s", "can") заканчиваются на "foobarcan", представляет собой проблему. Если вы ограничите один аргумент, как в первом случае, значением по умолчанию "%s", чтобы получить "foo", у вас все в порядке, но как объединить это с "%scan", чтобы получить "foobarcan" - это рассол.

David C. Rankin 23.03.2022 08:06

Насколько я понимаю вопрос, первый аргумент функции не соотносится ни с одной частью строки формата. Или, скорее, начальный %s подразумевается, если применимо. Желаемая операция аналогична concat(s ? s : calloc(INFINITY, 1), format(fmt, ...args)) (где мы притворяемся, что format имитирует sprintf, но возвращает буфер, а s всегда имеет достаточно места для результата; игнорируя освобождение памяти). "%s%s" всегда должны следовать два varargs; УБ иначе. Хотя, возможно, я неправильно понял.

Oka 23.03.2022 08:43

Одна проблема, которую я заметил, заключается в том, что dstr(&s, "hello %s", s), конечно, взорвется, так как s будет аннулирован до написания. Это должно быть решаемо с помощью отдельного распределения вместо перераспределения.

Oka 23.03.2022 08:47

Спасибо, в основном это работает, хотя иногда я получаю AddressSanitizer: attempting free on address which was not malloc()-ed в строке realloc, даже с этим простым тестом: char *s;dstr(&s, "foo%s", "bar");dstr(&s, "can%s", "haz");ASSERT_EQ(strcmp(s, "foofbarcanhaz"), 0);free(s);PASS();

A T 23.03.2022 23:37

О, это должно быть установлено на NULL, понял, изменил это сейчас. +1

A T 23.03.2022 23:47

Другие вопросы по теме