Почему неудобно заменять строки массивами символов
Перейти к содержимому

Почему неудобно заменять строки массивами символов

  • автор:

Все, что вы хотели знать о подстановке переменных в строках

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

Оригинал этой статьи впервые был опубликован в блоге автора @KevinMarquette. Группа разработчиков PowerShell благодарит Кевина за то, что он поделился с нами этими материалами. Читайте его блог — PowerShellExplained.com.

Объединение

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

$name = 'Kevin Marquette' $message = 'Hello, ' + $name 

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

$first = 'Kevin' $last = 'Marquette' 
$message = 'Hello, ' + $first + ' ' + $last + '.' 

Этот простой пример читать уже труднее.

Подстановка переменных

В PowerShell есть более простой вариант. Переменные можно задавать непосредственно в строках.

$message = "Hello, $first $last." 

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

Подстановка команд

Если вы пытаетесь получить значения свойств в строку, ситуация немного усложняется. Многие новички испытывают с этим проблемы. Позвольте мне сначала показать, что, по их мнению, должно сработать (по идее, все выглядит логично).

$directory = Get-Item 'c:\windows' $message = "Time: $directory.CreationTime" 

Казалось бы, вы должны получить CreationTime из $directory , но вместо этого вы получаете в качестве значения Time: c:\windows.CreationTime . Причина в том, что подстановка такого типа распознает только базовую переменную. Она интерпретирует точку как часть строки, поэтому не сопоставляет значение на более глубоких уровнях.

Выходит так, что при помещении в строку этот объект предоставляет строку в качестве значения по умолчанию. Некоторые объекты вместо этого предоставляют имя типа, например System.Collections.Hashtable . Это то, на что нужно обратить внимание.

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

$message = "Time: $($directory.CreationTime)" 

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

Выполнение команды

Команды можно выполнять внутри строки. Хотя так можно делать, мне это не нравится. Код быстро становится перегруженным, и его трудно отлаживать. Я либо использую команду и сохраняю ее результат в переменную, либо использую строку форматирования.

$message = "Date: $(Get-Date)" 

Строка форматирования

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

# .NET string format string [string]::Format('Hello, .',$first,$last) # PowerShell format string 'Hello, .' -f $first, $last 

В этом случае строка анализируется на наличие токенов и , а затем этот номер используется для выбора среди предоставленных значений. Если вы хотите повторить одно значение в строке, можно повторно использовать его номер.

При таком подходе чем сложнее строка, тем больше значений будет получено.

Форматирование значений в виде массивов

Если строка форматирования слишком длинная, можно сначала поместить значения в массив.

$values = @( "Kevin" "Marquette" ) 'Hello, .' -f $values 

Это не сплаттинг, поскольку я передаю весь массив, но идея аналогична.

Расширенное форматирование

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

"" -f (Get-Date) "Population " -f 8175133 
20211110 Population 8,175,133 

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

Объединение строк

Иногда требуется сцепить список значений вместе. Это можно сделать с помощью оператора -join . Он даже позволяет указать символ для объединения строк.

$servers = @( 'server1' 'server2' 'server3' ) $servers -join ',' 

Если с помощью -join необходимо объединить несколько строк без разделителя, необходимо указать пустую строку » . Но если нужно сделать только это, существует более быстрый вариант.

[string]::Concat('server1','server2','server3') [string]::Concat($servers) 

Также стоит отметить, что с помощью оператора -split строки можно разделить.

Join-Path

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

$folder = 'Temp' Join-Path -Path 'c:\windows' -ChildPath $folder 

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

Он также прекрасно работает с Split-Path и Test-Path . Я также рассказываю о нем в публикации о чтении и сохранении в файлы.

Строки как массивы

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

Взгляните на этот пример.

$message = "Numbers: " foreach($number in 1..10000)

Он выглядит очень простым, но на самом деле при каждом добавлении строки в $message создается полностью новая строка. Для этого выделяется память, в нее копируются данные, а старая строка удаляется. Ничего особенного, если такая операция выполняется лишь несколько раз, но в случае цикла, подобного этому, это может быть проблемой.

StringBuilder

Класс StringBuilder также очень часто используется для создания больших строк из множества строк меньшего размера. Его популярность связана с тем, что он просто собирает все добавленные в него строки, а затем сцепляет их в конце при извлечении значения.

$stringBuilder = New-Object -TypeName "System.Text.StringBuilder" [void]$stringBuilder.Append("Numbers: ") foreach($number in 1..10000) < [void]$stringBuilder.Append(" $number") >$message = $stringBuilder.ToString() 

Опять же, именно поэтому я использую .NET. Пользуюсь я им нечасто, но знать о его существовании будет не лишним.

Разграничение с помощью фигурных скобок

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

$test = "Bet" $tester = "Better" Write-Host "$test $tester $ter" 

Благодарю /u/real_parbold за эту идею.

Вот альтернатива данному подходу.

Write-Host "$test $tester $($test)ter" Write-Host "  ter" -f $test, $tester 

Лично я использую для этого строку форматирования, но знать о такой возможности будет не лишним на тот случай, если вам она где-то встретится.

Токены для поиска и замены

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

Предположим, что вы извлекли шаблон из файла, который содержит большой объем текста.

$letter = Get-Content -Path TemplateLetter.txt -RAW $letter = $letter -replace '#FULL_NAME#', 'Kevin Marquette' 

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

Недавно я обнаружил новый подход для решения этой задачи. Так как это довольно распространенная схема, я не стану вдаваться в дальнейшие подробности в этом разделе.

Замена нескольких токенов

Если у меня есть список токенов, которые нужно заменить, я использую более универсальный подход. Я помещаю их в хэш-таблицу и перебираю их методом итерации, чтобы выполнить замену.

$tokenList = @ < Full_Name = 'Kevin Marquette' Location = 'Orange County' State = 'CA' >$letter = Get-Content -Path TemplateLetter.txt -RAW foreach( $token in $tokenList.GetEnumerator() ) < $pattern = '##' -f $token.key $letter = $letter -replace $pattern, $token.Value > 

При необходимости эти токены можно загрузить из файла JSON или CSV.

Метод ExpandString класса ExecutionContext

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

$message = 'Hello, $Name!' $name = 'Kevin Marquette' $string = $ExecutionContext.InvokeCommand.ExpandString($message) 

При вызове .InvokeCommand.ExpandString в текущем контексте выполнения для подстановки используются переменные в текущей области. Ключевым моментом здесь является то, что $message можно определить заранее, когда переменные еще даже не существуют.

Немного дополнив этот код, можно обеспечить повторное выполнение этой замены для различных значений.

$message = 'Hello, $Name!' $nameList = 'Mark Kraus','Kevin Marquette','Lee Dailey' foreach($name in $nameList)

Развивая эту идею, можно импортировать большой шаблон электронной почты из текстового файла. Я благодарю Марка Крауса за это предложение.

Оптимальный вариант для конкретной ситуации

Мне нравится подход со строками форматирования. Я применяю его для более сложных строк или при наличии нескольких переменных. Но для коротких строк подойдет любой из описанных методов.

Что-нибудь еще?

Я лишь вкратце затронул эту тему. Я надеюсь, что вы сможете двигаться дальше, узнав для себя что-то новое.

Ссылки.

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

  • Объединение использует оператор сложения
  • Переменная и подстановка команд соответствуют правилам кавыкания
  • Форматирование использует оператор форматирования
  • Присоединение строк использует оператор соединения и ссылки Join-Path , но вы также можете прочитать о Join-String
  • Массивы документируются в массивах About
  • StringBuilder — это класс .NET с собственной документацией
  • Фигурные скобки в строках также рассматриваются в правилах кворирования
  • Замена маркера использует оператор замены
  • В этом методе $ExecutionContext.InvokeCommand.ExpandString() содержится справочная документация по API .NET

Совместная работа с нами на GitHub

Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.

Почему элементы в массиве не изменяются?

Есть задачка, которую я в теории решил, однако не изменяется значение в массиве temp. В чем загвоздка?

=begin Заполнить квадратную матрицу размера n на n натуральными числами от 1 до n**2 в указанном порядке: | 1 2 3 | | 6 5 4 | | 7 8 9 | =end glob_arr = [] temp = [] puts "Введите n" n = gets.chomp.to_i for i in 1..n temp end glob_arr.each

Отслеживать
19.5k 29 29 золотых знаков 47 47 серебряных знаков 139 139 бронзовых знаков
задан 18 июл 2016 в 20:14
300 1 1 серебряный знак 12 12 бронзовых знаков

3 ответа 3

Сортировка: Сброс на вариант по умолчанию

Можно поступить следующим образом

glob_arr = [] puts "Введите n" n = gets.chomp.to_i temp = Array.new(n, 0).fill < |i| i + 1 >n.times do |i| if i.even? glob_arr end glob_arr.each

В результате получается следующий вывод

Введите n 3 [1, 2, 3] [6, 5, 4] [7, 8, 9] 

Загвоздка в том, что вы элементам массива glob_arr присваиваете один и тот же массив temp . В ruby все является объектом, а объекты передаются по ссылкам. В результате меняя потом одну строку матрицы, вы затронете все три строки. Поэтому лучше temp клонировать, например, при помощи метода dup — тогда строки матрицы будут разными массивами, которые можно будет преобразовывать независимо друг от друга.

Отслеживать
ответ дан 18 июл 2016 в 20:38
19.5k 29 29 золотых знаков 47 47 серебряных знаков 139 139 бронзовых знаков

Вновь вы выручили. У меня были мысли по этому поводу. Но про dup не слышал. Надо теорию бы подтянуть. Мне стало как то неудобно.

18 июл 2016 в 20:50
@QWD666 Не стесняйтесь, спрашивайте, что знаем расскажем, чем сможем — поможем 🙂
18 июл 2016 в 20:52

@cheops я правильно понимаю, что вместо этой сточки glob_arr
18 июл 2016 в 21:21

@jisecayeyo да, совершенно, верно, в этой строчке dup лишний, так как reverse сам создает копию массива. Спасибо за замечание, поправил ответ.

19 июл 2016 в 5:09

n=9 result=(1..n).to_a.each_slice(Math.sqrt(n)).each_with_index.map < |x, i| (i.even?) ? x : x.reverse >p result 
[[1, 2, 3], [6, 5, 4], [7, 8, 9]] 

Правда задача гласит «1 до n**2», но автор берет n как конечное число, а не возводит в квадрат, поэтому для вычисления размера массива берем корень.

Отслеживать
ответ дан 19 июл 2016 в 7:10
3,887 1 1 золотой знак 16 16 серебряных знаков 26 26 бронзовых знаков
А хотите ещё интенсивнее магию? Её есть у меня! (1..9).each_slice(3).map.with_index < |x, i| i.even? ? x : x.reverse >
– user181100
19 июл 2016 в 16:30

@D-side на том же tryruby.org «#«. map.with_index у меня чет не так заработало изначально и не стал мудриь просто развернул.

22 июл 2016 в 6:41
Там старый, небось. Я гонял на 2.2.
– user181100
22 июл 2016 в 7:21

Для изменения значений массива нужно использвать функцию map . Функция возвратит новый массив с изменениями, не затронув данные в исходном массиве. Чтобы заменить данные в исходном массиве, нужно использовать функцию map! .
В вашем случае temp.reverse!.map! < |e| e+=1 >, или temp.map! < |e| e+=1 >reverse!.
Восклицательный знак в конце методов в Ruby означает, что изменение коснется исходных данных.
Если мы выполним temp.reverse.map! < |e| e+=1 >, то метод reverse возвратит новый массив с измененными данными и метод map! будет вызван уже для нового массива, поэтому изменений вы не заметите.
Описанное выше поможет только изменить значение массива, но для првильного решения задачи смотрите ответ @cheops.

Отслеживать
ответ дан 18 июл 2016 в 20:26
jisecayeyo jisecayeyo
1,265 1 1 золотой знак 11 11 серебряных знаков 25 25 бронзовых знаков

пробовал и через мап, однако в этом и есть проблема он или изменяет все данные на одинаковые, или работает так же как и у меня в примере.

18 июл 2016 в 20:29
@QWD666, Вы использовали map! (именно с восклицательным знаком)?
18 июл 2016 в 20:32
Да. А так же и без него.
18 июл 2016 в 20:33

@QWD666, проблема в методе reverse . Сделайте сначала temp.map! < |e| e+=1 >, а затем temp.reverse! . Или же temp.map!< |e| e+=1 >.reverse!

Немного о строках в Си, или несколько вариантов оптимизировать неоптимизируемое

Не так давно у со мной произошел довольно-таки интересный инцидент, в котором был замешан один из преподавателей одного колледжа информатики.

Разговор о программировании под Linux медленно перешел к тому, что этот человек стал утверждать, что сложность системного программирования на самом деле сильно преувеличена. Что язык Си прост как спичка, собственно как и ядро Linux (с его слов).

У меня был с собой ноутбук с Linux, на котором присутствовал джентльменский набор утилит для разработки на языке Си (gcc, vim, make, valgrind, gdb). Я уже не помню, какую цель мы тогда перед собой поставили, но через пару минут мой оппонент оказался за этим ноутбуком, полностью готовый решать задачу.

И буквально на первых же строках он допустил серьезную ошибку при аллоцировании памяти под… строку.

char *str = (char *)malloc(sizeof(char) * strlen(buffer));

buffer — стековая переменная, в которую заносились данные с клавиатуры.

Я думаю, определенно найдутся люди, которые спросят: «Разве что-то тут может быть не так?».
Поверьте, может.

А что именно — читайте по катом.

Немного теории — своеобразный ЛикБез.

Если знаете — листайте до следующего хэдера.

Строка в C — это массив символов, который по-хорошему всегда должен заканчиваться ‘\0’ — символом конца строки. Строки на стеке (статичные) объявляются вот так:

char str[n] = < 0 >; 

n — размер массива символов, то же, что и длина строки.

Присваивание < 0 >— «зануление» строки (опционально, объявлять можно и без него). Результат такой же, как у выполнения функций memset(str, 0, sizeof(str)) и bzero(str, sizeof(str)). Используется, чтобы в неинициализированных переменных не валялся мусор.

Так же на стеке можно сразу проинициализировать строку:

char buf[BUFSIZE] = "default buffer text\n";

Помимо этого строку можно объявить указателем и выделить под нее память на куче (heap):

char *str = malloc(size);

size — количество байт, которые мы выделяем под строку. Такие строки называются динамическими (вследствие того, что нужный размер вычисляется динамически + выделенный размер памяти можно в любой момент увеличить с помощью функции realloc() ).

В случае со стековой переменной, для определения размера массива я использовал обозначение n, в случае с переменной на куче — я использовал обозначение size. И это прекрасно отражает истинную суть отличия объявления на стеке от объявление с аллоцированием памяти на куче, ведь n как правило используется тогда, когда говорят о количестве элементов. А size — это уже совсем другая история…

Думаю. пока хватит. Идем дальше.

Нам поможет valgrind

В своей предыдущей статье я также упоминал о нем. Valgrind (раз — вики-статья, два — небольшой how-to) — очень полезная программа, которая помогает программисту отслеживать утечки памяти и ошибки контекста — как раз те вещи, которые чаще всего всплывают при работе со строками.

Давайте рассмотрим небольшой листинг, в котором реализовано что-то похожее на упомянутую мной программу, и прогоним ее через valgrind:

#include #include #include #define HELLO_STRING "Hello, Habr!\n" void main() < char *str = malloc(sizeof(char) * strlen(HELLO_STRING)); strcpy(str, HELLO_STRING); printf("->\t%s", str); free(str); >

И, собственно, результат работы программы:

[indever@localhost public]$ gcc main.c [indever@localhost public]$ ./a.out -> Hello, Habr! 

Пока ничего необычного. А теперь давайте запустим эту программу с valgrind!

[indever@localhost public]$ valgrind --tool=memcheck ./a.out ==3892== Memcheck, a memory error detector ==3892== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al. ==3892== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info ==3892== Command: ./a.out ==3892== ==3892== Invalid write of size 2 ==3892== at 0x4005B4: main (in /home/indever/prg/C/public/a.out) ==3892== Address 0x520004c is 12 bytes inside a block of size 13 alloc'd ==3892== at 0x4C2DB9D: malloc (vg_replace_malloc.c:299) ==3892== by 0x400597: main (in /home/indever/prg/C/public/a.out) ==3892== ==3892== Invalid read of size 1 ==3892== at 0x4C30BC4: strlen (vg_replace_strmem.c:454) ==3892== by 0x4E89AD0: vfprintf (in /usr/lib64/libc-2.24.so) ==3892== by 0x4E90718: printf (in /usr/lib64/libc-2.24.so) ==3892== by 0x4005CF: main (in /home/indever/prg/C/public/a.out) ==3892== Address 0x520004d is 0 bytes after a block of size 13 alloc'd ==3892== at 0x4C2DB9D: malloc (vg_replace_malloc.c:299) ==3892== by 0x400597: main (in /home/indever/prg/C/public/a.out) ==3892== -> Hello, Habr! ==3892== ==3892== HEAP SUMMARY: ==3892== in use at exit: 0 bytes in 0 blocks ==3892== total heap usage: 2 allocs, 2 frees, 1,037 bytes allocated ==3892== ==3892== All heap blocks were freed -- no leaks are possible ==3892== ==3892== For counts of detected and suppressed errors, rerun with: -v ==3892== ERROR SUMMARY: 3 errors from 2 contexts (suppressed: 0 from 0) 

==3892== All heap blocks were freed — no leaks are possible — утечек нет, и это радует. Но стоит опустить глаза чуть пониже (хотя, хочу заметить, это лишь итог, основная информация немного в другом месте):

==3892== ERROR SUMMARY: 3 errors from 2 contexts (suppressed: 0 from 0)
3 ошибки. В 2х контекстах. В такой простой программе. Как!?

Да очень просто. Весь «прикол» в том, что функция strlen не учитывает символ конца строки — ‘\0’. Даже если его явно указать во входящей строке (#define HELLO_STRING «Hello, Habr!\n\0»), он будет проигнорирован.

Чуть выше результата исполнения программы, строки -> Hello, Habr! есть подробный отчет, что и где не понравилось нашему драгоценному valgrind. Предлагаю самостоятельно посмотреть эти строчки и сделать выводы.

Собственно, правильная версия программы будет выглядеть так:

#include #include #include #define HELLO_STRING "Hello, Habr!\n" void main() < char *str = malloc(sizeof(char) * (strlen(HELLO_STRING) + 1)); strcpy(str, HELLO_STRING); printf("->\t%s", str); free(str); >

Пропускаем через valgrind:

[indever@localhost public]$ valgrind --tool=memcheck ./a.out -> Hello, Habr! ==3435== ==3435== HEAP SUMMARY: ==3435== in use at exit: 0 bytes in 0 blocks ==3435== total heap usage: 2 allocs, 2 frees, 1,038 bytes allocated ==3435== ==3435== All heap blocks were freed -- no leaks are possible ==3435== ==3435== For counts of detected and suppressed errors, rerun with: -v ==3435== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) 

Отлично. Ошибок нет, +1 байт выделяемой памяти помог решить проблему.

Что интересно, в большинстве случаев и первая и вторая программа будут работать одинаково, но если память, выделенная под строку, в которую не влез символ окончания, не была занулена, то функция printf(), при выводе такой строки, выведет и весь мусор после этой строки — будет выведено все, пока на пути printf() не встанет символ окончания строки.

Однако, знаете, (strlen(str) + 1) — такое себе решение. Перед нами встают 2 проблемы:

  1. А если нам надо выделить память под формируемую с помощью, например, s(n)printf(..) строку? Аргументы мы не поддерживаем.
  2. Внешний вид. Строка с объявлением переменной выглядит просто ужасно. Некоторые ребята к malloc еще и (char *) умудряются прикручивать, будто под плюсами пишут. В программе где регулярно требуется обрабатывать строки есть смысл найти более изящное решение.
snprintf()

int snprintf(char *str, size_t size, const char *format, . ); — функция — расширение sprintf, которая форматирует строку и записывает ее по указателю, переданному в качестве первого аргумента. От sprintf() она отличается тем, что в str не будет записано байт больше, чем указано в size.

Функция имеет одну интересную особенность — она в любом случае возвращает размер формируемой строки (без учета символа конца строки). Если строка пустая, то возвращается 0.

Одна из описанных мною проблем использования strlen связана с функциями sprintf() и snprintf(). Предположим, что нам надо что-то записать в строку str. Конечная строка содержит значения других переменных. Наша запись должна быть примерно такой:

char * str = /* тут аллоцируем память */; sprintf(str, "Hello, %s\n", "Habr!");

Встает вопрос: как определить, сколько памяти надо выделить под строку str?

char * str = malloc(sizeof(char) * (strlen(str, "Hello, %s\n", "Habr!") + 1));

— не прокатит. Прототип функции strlen() выглядит так:

#include size_t strlen(const char *s);

const char *s не подразумевает, что передаваемая в s строка может быть строкой формата с переменным количеством аргументов.

Тут нам поможет то полезное свойство функции snprintf(), о котором я говорил выше. Давайте посмотрим на код следующей программы:

#include #include #include void main() < /* Т.к. snprintf() не учитывает символ конца строки, прибавляем его размер к результату */ size_t needed_mem = snprintf(NULL, 0, "Hello, %s!\n", "Habr") + sizeof('\0'); char *str = malloc(needed_mem); snprintf(str, needed_mem, "Hello, %s!\n", "Habr"); printf("->\t%s", str); free(str); >

Запускаем программу в valgrind:

[indever@localhost public]$ valgrind --tool=memcheck ./a.out -> Hello, Habr! ==4132== ==4132== HEAP SUMMARY: ==4132== in use at exit: 0 bytes in 0 blocks ==4132== total heap usage: 2 allocs, 2 frees, 1,041 bytes allocated ==4132== ==4132== All heap blocks were freed -- no leaks are possible ==4132== ==4132== For counts of detected and suppressed errors, rerun with: -v ==4132== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) [indever@localhost public]$ 

Отлично. Поддержка аргументов у нас есть. Благодаря тому, что мы в качестве второго аргумента в функцию snprintf() передаем ноль, запись по нулевому указателю никогда не приведет к Seagfault. Однако, несмотря на это функция все равно вернет необходимый под строку размер.

Но с другой стороны, нам пришлось завести дополнительную переменную, да и конструкция

size_t needed_mem = snprintf(NULL, 0, "Hello, %s!\n", "Habr") + sizeof('\0');

выглядит еще хуже, чем в случае с strlen().

Вообще, + sizeof(‘\0’) можно убрать, если в конце строки формата явно указать ‘\0’ (size_t needed_mem = snprintf(NULL, 0, «Hello, %s!\n\0», «Habr»);), но это возможно отнюдь не всегда (в зависимости от механизма обработки строк мы можем выделить лишний байт).

Надо что-то сделать. Я немного подумал и решил, что сейчас настал час воззвать к мудрости древних. Опишем макрофункцию, которая будет вызывать snprintf() с нулевым указателем в качестве первого аргумента, и нулем, в качестве второго. Да и про конец строки не забудем!

#define strsize(args. ) snprintf(NULL, 0, args) + sizeof('\0')

Да, возможно, для кого-то будет новостью, но макросы в си поддерживают переменное количество аргументов, и троеточие говорит препроцессору о том, что указанному аргументу макрофункции (в нашем случае это args) соответствует несколько реальных аргументов.

Проверим наше решение на практике:

#include #include #include #define strsize(args. ) snprintf(NULL, 0, args) + sizeof('\0') void main() < char *str = malloc(strsize("Hello, %s\n", "Habr!")); sprintf(str, "Hello, %s\n", "Habr!"); printf("->\t%s", str); free(str); >

Запускаем с valgrund:

[indever@localhost public]$ valgrind --tool=memcheck ./a.out -> Hello, Habr! ==6432== ==6432== HEAP SUMMARY: ==6432== in use at exit: 0 bytes in 0 blocks ==6432== total heap usage: 2 allocs, 2 frees, 1,041 bytes allocated ==6432== ==6432== All heap blocks were freed -- no leaks are possible ==6432== ==6432== For counts of detected and suppressed errors, rerun with: -v ==6432== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) 

Да, ошибок нет. Все корректно. И valgrind доволен, и программист наконец может пойти поспать.

Но, напоследок, скажу еще кое-что. В случае, если нам надо выделить память под какую-либо строку (даже с аргументами) есть уже полностью рабочее готовое решение.

Речь идет о функции asprintf:

#define _GNU_SOURCE /* See feature_test_macros(7) */ #include int asprintf(char **strp, const char *fmt, . ); 

В качестве первого аргумента она принимает указатель на строку (**strp) и аллоцирует память по разыменованному указателю.

Наша программа, написанная с использованием asprintf() будет выглядеть так:

#include #include #include void main() < char *str; asprintf(&str, "Hello, %s!\n", "Habr"); printf("->\t%s", str); free(str); >

И, собственно, в valgrind:

[indever@localhost public]$ valgrind --tool=memcheck ./a.out -> Hello, Habr! ==6674== ==6674== HEAP SUMMARY: ==6674== in use at exit: 0 bytes in 0 blocks ==6674== total heap usage: 3 allocs, 3 frees, 1,138 bytes allocated ==6674== ==6674== All heap blocks were freed -- no leaks are possible ==6674== ==6674== For counts of detected and suppressed errors, rerun with: -v ==6674== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) 

Все отлично, но, как видите, памяти всего было выделено больше, да и alloc’ов теперь три, а не два. На слабых встраиваемых системах использование это функции нежелательно.
К тому же, если мы напишем в консоли man asprintf, то увидим:

CONFORMING TO These functions are GNU extensions, not in C or POSIX. They are also available under *BSD. The FreeBSD implementation sets strp to NULL on error.

Отсюда ясно, что данная функция доступна только в исходниках GNU.

Заключение

В заключение я хочу сказать, что работа со строками в C — это очень сложная тема, которая имеет ряд нюансов. Например, для написания «безопасного» кода при динамическом выделении памяти рекомендуется все же использовать функцию calloc() вместо malloc() — calloc забивает выделяемую память нулями. Ну или после выделения памяти использовать функцию memset(). Иначе мусор, который изначально лежал на выделяемом участке памяти, может вызвать вопросы при дебаге, а иногда и при работе со строкой.

Больше половины моих знакомых си-программистов (большинство из них — начинающие), решивших по моей просьбе задачу с выделением памяти под строки, сделали это так, что в конечном итоге это привело к ошибкам контекста. В одном случае — даже к утечке памяти (ну, забыл человек сделать free(str), с кем не бывает). Собственно говоря, это и сподвигло меня на создание сего творения, которое вы только что прочитали.

Я надеюсь, кому-то эта статья будет полезной. К чему я это все городил — никакой язык не бывает прост. Везде есть свои тонкости. И чем больше тонкостей языка вы знаете, тем лучше ваш код.

Я верю, что после прочтения этой статьи ваш код станет чуточку лучше 🙂
Удачи, Хабр!

Обработка строк через преобразование в массив — PHP: Массивы

Решить эту задачу можно множеством способов — чем больше назовете, тем лучше. Есть как минимум три варианта:

  1. Посимвольный перебор строки. Есть два способа перебора, в том числе конечные автоматы, которые мы рассмотрим в отдельном курсе
  2. Регулярные выражения, о них тоже есть отдельный курс
  3. Преобразование строки в массив слов

Рассмотрим последний способ. Первым делом нужно превратить строку в массив. Для этого мы воспользуемся функцией explode() , которая разделяет строку, используя указанный разделитель. В нашем случае разделитель — это пробел:

 function capitalizeWords($sentence)  $words = explode(' ', $sentence); // . > 

Теперь можно использовать цикл и выполнить операцию capitalize, то есть привести первую букву каждого слова к верхнему регистру:

 function capitalizeWords($sentence)  $words = explode(' ', $sentence); $capitalizedWords = []; foreach ($words as $word)  $capitalizedWords[] = ucfirst($word); > // . > 

Последнее действие обратно первому — нужно соединить слова и вернуть получившуюся строку наружу. Чтобы соединить элементы массива, воспользуемся функцией implode() . Как и explode() , она принимает на вход разделитель, который теперь используется для сборки строки:

 function capitalizeWords($sentence)  $words = explode(' ', $sentence); $capitalizedWords = []; foreach ($words as $word)  $capitalizedWords[] = ucfirst($word); > return implode(' ', $capitalizedWords); > $greeting = 'hello from Malasia'; print_r(capitalizeWords($greeting)); // => Hello From Malasia 

Если строку нужно разбить по символам, а не по словам, можно воспользоваться функцией mb_str_split() :

 $chars = mb_str_split($text); foreach ($chars as $char)  print_r($char); > 

Функция mb_str_split() принимает второй параметр, в котором можно указать количество символов в каждой группе (в каждом элементе получившегося массива). Значение по умолчанию равно единице, поэтому мы получаем массив, в котором каждый элемент — это один символ. Например, если мы захотим указать число 3, то в каждом элементе массива будет по три символа:

 $text = 'Hello Friend'; $parts = mb_str_split($text, 3); print_r($parts); // => Array // => ( // => [0] => Hel // => [1] => lo // => [2] => Fri 

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов

Наши выпускники работают в компаниях:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *