Выбор читателей
Популярные статьи
Пожалуйста, приостановите работу AdBlock на этом сайте.
Итак, строки в языке Си. Для них не предусмотрено отдельного типа данных, как это сделано во многих других языках программирования. В языке Си строка – это массив символов. Чтобы обозначить конец строки, используется символ "\0" , о котором мы говорили в прошлой части этого урока. На экране он никак не отображается, поэтому посмотреть на него не получится.
Так как строка – это массив символов, то объявление и инициализация строки аналогичны подобным операциям с одномерными массивами.
Следующий код иллюстрирует различные способы инициализации строк.
Листинг 1.
Char str; char str1 = {"Y","o","n","g","C","o","d","e","r","\0"}; char str2 = "Hello!"; char str3 = "Hello!";
Рис.1 Объявление и инициализация строк
В первой строке мы просто объявляем массив из десяти символов. Это даже не совсем строка, т.к. в ней отсутствует нуль-символ \0 , пока это просто набор символов.
Вторая строка. Простейший способ инициализации в лоб. Объявляем каждый символ по отдельности. Тут главное не забыть добавить нуль-символ \0 .
Третья строка – аналог второй строки. Обратите внимание на картинку. Т.к. символов в строке справа меньше, чем элементов в массиве, остальные элементы заполнятся \0 .
Четвёртая строка. Как видите, тут не задан размер. Программа его вычислит автоматически и создаст массив символов нужный длины. При этом последним будет вставлен нуль-символ \0 .
Дополним код выше до полноценной программы, которая будет выводить созданные строки на экран.
Листинг 2.
#include
Рис.2 Различные способы вывода строки на экран
Как видите, есть несколько основных способов вывести строку на экран.
Единственный нюанс у функций puts и fputs . Обратите внимание, что функция puts переносит вывод на следующую строку, а функция fputs не переносит.
Как видите, с выводом всё достаточно просто.
С вводом строк всё немного сложнее, чем с выводом. Простейшим способом будет являться следующее:
Листинг 3.
#include
Функция gets приостанавливает работу программы, читает строку символов, введенных с клавиатуры, и помещает в символьный массив, имя которого передаётся функции в качестве параметра.
Завершением работы функции gets
будет являться символ, соответствующий клавише ввод и записываемый в строку как нулевой символ.
Заметили опасность? Если нет, то о ней вас любезно предупредит компилятор. Дело в том, что функция gets
завершает работу только тогда, когда пользователь нажимает клавишу ввод. Это чревато тем, что мы можем выйти за рамки массива, в нашем случае - если введено более 20
символов.
К слову, ранее ошибки переполнения буфера считались самым распространенным типом уязвимости. Они встречаются и сейчас, но использовать их для взлома программ стало гораздо сложнее.
Итак, что мы имеем. У нас есть задача: записать строку в массив ограниченного размера. То есть, мы должны как-то контролировать количество символов, вводимых пользователем. И тут нам на помощь приходит функция fgets :
Листинг 4.
#include
Функция fgets принимает на вход три аргумента: переменную для записи строки, размер записываемой строки и имя потока, откуда взять данные для записи в строку, в данном случае - stdin . Как вы уже знаете из 3 урока, stdin – это стандартный поток ввода данных, обычно связанный с клавиатурой. Совсем необязательно данные должны поступать именно из потока stdin , в дальнейшем эту функцию мы также будем использовать для чтения данных из файлов.
Если в ходе выполнения этой программы мы введем строку длиннее, чем 10 символов, в массив все равно будут записаны только 9 символов с начала и символ переноса строки, fgets «обрежет» строку под необходимую длину.
Обратите внимание, функция fgets считывает не 10 символов, а 9 ! Как мы помним, в строках последний символ зарезервирован для нуль-символа.
Давайте это проверим. Запустим программу из последнего листинга. И введём строку 1234567890 . На экран выведется строка 123456789 .
Рис.3 Пример работы функции fgets
Возникает вопрос. А куда делся десятый символ? А я отвечу. Он никуда не делся, он остался в потоке ввода. Выполните следующую программу.
Листинг 5.
#include
Вот результат её работы.
Рис.4 Непустой буфер stdin
Поясню произошедшее. Мы вызвали функцию fgets . Она открыла поток ввода и дождалась пока мы введём данные. Мы ввели с клавиатуры 1234567890\n (\n я обозначаю нажатие клавиша Enter ). Это отправилось в поток ввода stdin . Функция fgets , как и полагается, взяла из потока ввода первые 9 символов 123456789 , добавила к ним нуль-символ \0 и записала это в строку str . В потоке ввода осталось ещё 0\n .
Далее мы объявляем переменную h . Выводим её значение на экран. После чего вызываем функцию scanf . Тут-то ожидается, что мы можем что-то ввести, но т.к. в потоке ввода висит 0\n , то функция scanf воспринимает это как наш ввод, и записывается 0 в переменную h . Далее мы выводим её на экран.
Это, конечно, не совсем такое поведение, которое мы ожидаем. Чтобы справиться с этой проблемой, необходимо очистить буфер ввода после того, как мы считали из него строку, введённую пользователем. Для этого используется специальная функция fflush . У неё всего один параметр – поток, который нужно очистить.
Исправим последний пример так, чтобы его работа была предсказуемой.
Листинг 6.
#include
Теперь программа будет работать так, как надо.
Рис.4 Сброс буфера stdin функцией fflush
Подводя итог, можно отметить два факта. Первый. На данный момент использование функции gets является небезопасным, поэтому рекомендуется везде использовать функцию fgets .
Второй. Не забывайте очищать буфер ввода, если используете функцию fgets .
На этом разговор о вводе строк закончен. Идём дальше.
В программе строки могут определяться следующим образом:
Кроме того, должно быть предусмотрено выделение памяти для хранения строки.
Любая последовательность символов, заключенная в двойные кавычки «» , рассматривается как строковая константа .
Для корректного вывода любая строка должна заканчиваться нуль-символом "\0" , целочисленное значение которого равно 0. При объявлении строковой константы нуль-символ добавляется к ней автоматически. Так, последовательность символов, представляющая собой строковую константу, будет размещена в оперативной памяти компьютера, включая нулевой байт.
Под хранение строки выделяются последовательно идущие ячейки оперативной памяти. Таким образом, строка представляет собой массив символов. Для хранения кода каждого символа строки отводится 1 байт.
Для помещения в строковую константу некоторых служебных символов используются символьные комбинации. Так, если необходимо включить в строку символ двойной кавычки, ему должен предшествовать символ «обратный слеш»: ‘\»‘ .
Строковые константы размещаются в статической памяти. Начальный адрес последовательности символов в двойных кавычках трактуется как адрес строки. Строковые константы часто используются для осуществления диалога с пользователем в таких функциях, как printf() .
При определении массива символов
необходимо сообщить компилятору требуемый размер памяти.
char
m;
Компилятор также может самостоятельно определить размер массива символов, если инициализация массива задана при объявлении строковой константой:
char
m2=;
char
m3={"Т","и","х","и","е"," ","д","о","л","и","н","ы"," ","п","о","л","н","ы"," ","с","в","е","ж","е","й"," ","м","г","л","о","й","\0"
};
В этом случае имена m2 и m3 являются указателями на первые элементы массивов:
При объявлении массива символов и инициализации его строковой константой можно явно указать размер массива, но указанный размер массива должен быть больше, чем размер инициализирующей строковой константы:
char
m2="Горные вершины спят во тьме ночной."
;
Для задания строки можно использовать указатель на символьный тип
.
char
*m4;
В этом случае объявление массива переменной m4 может быть присвоен адрес массива:
m4 = m3;
*m4 эквивалентно m3="Т"
*(m4+1) эквивалентно m3="и"
Здесь m3 является константой-указателем. Нельзя изменить m3 , так как это означало бы изменение положения (адреса) массива в памяти, в отличие от m4 .
Для указателя можно использовать операцию увеличения (перемещения на следующий символ):
Иногда в программах возникает необходимость описание массива символьных строк
. В этом случае можно использовать индекс строки для доступа к нескольким разным строкам.
char
*poet = {"Погиб поэт!", "- невольник чести -"
,
"Пал," , "оклеветанный молвой…"
};
В этом случае poet
является массивом, состоящим из четырех указателей на символьные строки. Каждая строка символов представляет собой символьный массив, поэтому имеется четыре указателя на массивы. Указатель poet
ссылается на первую строку:
*poet
эквивалентно "П"
,
*poet[l]
эквивалентно "-"
.
Инициализация выполняется по правилам, определенным для массивов.
Тексты в кавычках эквивалентны инициализации каждой строки в массиве. Запятая разделяет соседние
последовательности.
Кроме того, можно явно задавать размер строк символов, используя описание, подобное такому:
char
poet;
Разница заключается в том, что такая форма задает «прямоугольный» массив, в котором все строки имеют одинаковую длину.
Описание
сhar *poet;
Большинство операций языка Си, имеющих дело со строками, работает с указателями. Для размещения в оперативной памяти строки символов необходимо:
Для выделения памяти под хранение строки могут использоваться функции динамического выделения памяти . При этом необходимо учитывать требуемый размер строки:
char
*name;
name = (char
*)malloc(10);
scanf("%9s"
, name);
Для ввода строки использована функция scanf()
, причем введенная строка не может превышать 9 символов. Последний символ будет содержать "\0"
.
Для ввода строки может использоваться функция scanf() . Однако функция scanf() предназначена скорее для получения слова, а не строки. Если применять формат "%s" для ввода, строка вводится до (но не включая) следующего пустого символа, которым может быть пробел, табуляция или перевод строки.
Для ввода строки, включая пробелы, используется функция
char
* gets(char
*);
char
* gets_s(char
*);
В качестве аргумента функции передается указатель на строку, в которую осуществляется ввод. Функция просит пользователя ввести строку, которую она помещает в массив, пока пользователь не нажмет Enter .
Для вывода строк можно воспользоваться рассмотренной ранее функцией
printf("%s"
, str); // str - указатель на строку
или в сокращенном формате
printf(str);
Для вывода строк также может использоваться функция
int
puts (char
*s);
которая печатает строку s и переводит курсор на новую строку (в отличие от printf() ). Функция puts() также может использоваться для вывода строковых констант, заключенных в кавычки.
Для ввода символов может использоваться функция
char
getchar();
Для вывода символов может использоваться функция
char
putchar(char
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include
#include
#include
int
main() {
char
s, sym;
int
count, i;
system("chcp 1251"
);
system("cls"
);
printf("Введите строку: "
);
gets_s(s);
printf("Введите символ: "
);
sym = getchar();
count = 0;
for
(i = 0; s[i] != "\0"
; i++)
{
if
(s[i] == sym)
count++;
}
printf("В строке\n"
);
puts(s); // Вывод строки
printf("символ "
);
putchar(sym); // Вывод символа
printf(" встречается %d раз"
, count);
getchar(); getchar();
return
0;
}
Результат выполнения
Основные функции стандартной библиотеки string.h приведены в таблице.
Функция | Описание |
char *strcat(char *s1, char *s2) |
присоединяет s2 к s1, возвращает s1 |
char *strncat(char *s1, char *s2, int n) |
присоединяет не более n символов s2 к s1, завершает строку символом "\0", возвращает s1 |
char *strсpy(char *s1, char *s2) |
копирует строку s2 в строку s1, включая "\0", возвращает s1 |
); strncpy(m3, m1, 6); // не добавляет "\0" в конце строки puts("Результат strncpy(m3, m1, 6)" ); puts(m3); strcpy(m3, m1); puts("Результат strcpy(m3, m1)" ); puts(m3); puts("Результат strcmp(m3, m1) равен" ); printf("%d" , strcmp(m3, m1)); strncat(m3, m2, 5); puts("Результат strncat(m3, m2, 5)" ); puts(m3); strcat(m3, m2); puts("Результат strcat(m3, m2)" ); puts(m3); puts("Количество символов в строке m1 равно strlen(m1) : " ); printf("%d\n" , strlen(m1)); _strnset(m3, "f" , 7); puts("Результат strnset(m3, "f" , 7)" ); puts(m3); _strset(m3, "k" ); puts("Результат strnset(m3, "k" )" ); puts(m3); getchar(); return 0; } Результат выполнения |
Формат ANSI устанавливает, что значением первой позиции в строке является ее длина, а затем
следуют сами символы строки. Например, представление строки "Моя строка!" будет следующим:
11 ‘М’ ‘о’ ‘я’ ‘ ’ ‘с’ ‘т’ ‘р’ ‘о’ ‘к’ ‘а’ ‘!’
В строках с завершающим нулем, значащие символы строки указываются с первой позиции, а
признаком завершения строки является значение ноль. Представление рассмотренной ранее строки в этом формате
имеет вид:
‘М’ ‘о’ ‘я’ ‘ ’ ‘с’ ‘т’ ‘р’ ‘о’ ‘к’ ‘а’ ‘!’ 0
Объявление строк в СИ
Работа со строками в СИ
Массивы строк в СИ
Функции для работы со строками в СИ
Ввод и вывод строк в СИ
Функция gets предназначена для ввода строк и имеет следующий заголовок:
char * gets(char *buffer);
Преобразование строк
После символов E, e указывается порядок числа. Корректное представление целого числа в текстовой строке
должно удовлетворять формату:
[{+|-}] цифры
Помимо приведенных выше функций в библиотеке stdlib.h доступны также следующие функции
преобразования строк в вещественные числа:
float strtof(const char * restrict string, char ** restrict endptr);
double strtod(const char * restrict string, char ** restrict endptr);
long double strtold(const char * restrict string,char ** restrict endptr);
Аналогичные функции присутствуют и для преобразования строк в целочисленные значения:
long int strtol(const char * restrict string,
unsigned long strtoul(const char * restrict string,
char ** restrict endptr, int base);
long long int strtoll(const char * restrict string,
char ** restrict endptr, int base);
unsigned long long strtoull(const char * restrict string,char ** restrict endptr, int
base);
Функции обратного преобразования (численные значения в строки) в библиотеке stdlib.h присутствуют, но они не регламентированы стандартом, и рассматриваться не будут. Для преобразования численных значений в строковые наиболее удобно использовать функции sprintf и snprintf.
Обработка строк
Функции объединения (конкатенации) строк:
char * strcat(char * restrict dst, const char * restrict src);
char * strncat(char * restrict dst, const char * restrict src, size_t num);
Функции поиска символа в строке:
char * strchr(const char *string, int c);
char * strrchr(const char *string, int c);
Функция поиска строки в строке:
char * strstr(const char *str, const char *substr);
Пример:
char str = "Строка для поиска";
char *str1 = strstr(str,"для"); //str1 == "для поиска"
Функция поиска первого символа в строке из заданного набора символов:
size_t strcspn(const char *str, const char *charset);
Функции поиска первого символа в строке не принадлежащему заданному набору символов:
size_t strspn(const char *str, const char *charset);
Функции поиска первого символа в строке из заданного набора символов:
char * strpbrk(const char *str, const char *charset);
Функция поиска следующего литерала в строке:
char * strtok(char * restrict string, const char * restrict charset);
В современном стандарте C++ определен класс с функциями и свойствами (переменными) для организации работы со строками (в классическом языке C строк как таковых нет, есть лишь массивы символов char):
#include
#include#include
Для работы со строками также нужно подключить стандартный namespace:
Using namespace std;
В противном случае придётся везде указывать описатель класса std::string вместо string .
Ниже приводится пример программы, работающей со string (в старых си-совместимых компиляторах не работает!):
#include
Основные возможности, которыми обладает класс string:
Рассмотрим эти базовые возможности более подробно.
Инициализация строк при описании и длина строки (не включая завершающий нуль-терминатор):
String st("Моя строка\n"); cout << "Длина " << st << ": " << st.size() << " символов, включая символ новой строки\n";
Строка может быть задана и пустой:
String st2;
Для проверки того, пуста ли строка , можно сравнить ее длину с 0:
If (! st.size()) // пустая
или применить метод empty() , возвращающий true для пустой строки и false для непустой:
If (st.empty()) // пустая
Третья форма создания строки инициализирует объект типа string другим объектом того же типа:
String st3(st);
Строка st3 инициализируется строкой st . Как мы можем убедиться, что эти строки совпадают ? Воспользуемся оператором сравнения (==):
If (st == st3) // инициализация сработала
Как скопировать одну строку в другую ? С помощью обычной операции присваивания:
St2 = st3; // копируем st3 в st2
Для сцепления строк используется операция сложения (+) или операция сложения с присваиванием (+=). Пусть даны две строки:
String s1("hello, "); string s2("world\n");
Мы можем получить третью строку, состоящую из конкатенации первых двух, таким образом:
String s3 = s1 + s2;
Если же мы хотим добавить s2 в конец s1 , мы должны написать:
S1 += s2;
Операция сложения может сцеплять объекты класса string не только между собой, но и со строками встроенного типа. Можно переписать пример, приведенный выше, так, чтобы специальные символы и знаки препинания представлялись встроенным типом char * , а значимые слова – объектами класса string:
Const char *pc = ", "; string s1("hello"); string s2("world"); string s3 = s1 + pc + s2 + "\n"; cout << endl << s3;
Подобные выражения работают потому, что компилятор "знает", как автоматически преобразовывать объекты встроенного типа в объекты класса string . Возможно и простое присваивание встроенной строки объекту string:
String s1; const char *pc = "a character array"; s1 = pc; // правильно
Обратное преобразование при этом не работает . Попытка выполнить следующую инициализацию строки встроенного типа вызовет ошибку компиляции:
Char *str = s1; // ошибка компиляции
Чтобы осуществить такое преобразование, необходимо явно вызвать функцию-член с названием c_str() ("строка Си"):
Const char *str = s1.c_str();
Функция c_str() возвращает указатель на символьный массив, содержащий строку объекта string в том виде, в каком она находилась бы во встроенном строковом типе. Ключевое слово const здесь предотвращает "опасную" в современных визуальных средах возможность непосредственной модификации содержимого объекта через указатель.
К отдельным символам объекта типа string , как и встроенного типа, можно обращаться с помощью операции взятия индекса. Вот, например, фрагмент кода, заменяющего все точки символами подчеркивания:
String str("www.disney.com"); int size = str.size(); for (int i = 0; i < size; i++) if (str[i] == ".") str[ i ] = "_"; cout << str;
Replace(str.begin(), str.end(), ".", "_");
Правда, здесь использован не метод replace класса string , а одноимённый алгоритм:
#include
Поскольку объект string ведет себя как контейнер, к нему могут применяться и другие алгоритмы. Это позволяет решать задачи, не решаемые напрямую функциями класса string .
Ниже приводится краткое описание основных операторов и функций класса string , ссылки в таблице ведут к русскоязычным описаниям в интернете. Более полный список возможностей класса string можно получить, например, в Википедии или на сайте cplusplus.com .
Задание символов в строке |
|||
operator= |
присваивает значения строке |
||
assign |
назначает символы строке |
||
Доступ к отдельным символам |
|||
at |
получение указанного символа с проверкой выхода индекса за границы |
||
operator |
получение указанного символа |
||
front |
получение первого символа |
||
back |
получение последнего символа |
||
data |
возвращает указатель на первый символ строки |
||
c_str |
возвращает немодифицируемый массив символов С , содержащий символы строки |
||
Проверка на вместимость строки |
|||
empty |
проверяет, является ли строка пустой |
||
size
|
возвращает количество символов в строке |
||
max_size |
возвращает максимальное количество символов |
||
reserve |
резервирует место под хранение |
||
Операции над строкой |
|||
clear |
очищает содержимое строки |
||
insert |
вставка символов |
||
erase |
удаление символов |
||
push_back |
добавление символа в конец строки |
||
pop_back |
удаляет последний символ |
||
append |
|||
operator+= |
добавляет символы в конец строки |
||
compare |
сравнивает две строки |
||
replace |
заменяет каждое вхождение указанного символа |
||
substr |
возвращает подстроку |
||
copy |
копирует символы |
||
resize |
изменяет количество хранимых символов |
||
Хабра, привет! Не так давно у со мной произошел довольно-таки интересный инцидент, в котором был замешан один из преподавателей одного колледжа информатики. Разговор о программировании под Linux медленно перешел к тому, что этот человек стал утверждать, что сложность системного программирования на самом деле сильно преувеличена. Что язык Си прост как спичка, собственно как и ядро Linux (с его слов). У меня был с собой ноутбук с Linux, на котором присутствовал джентльменский набор утилит для разработки на языке Си (gcc, vim, make, valgrind, gdb). Я уже не помню, какую цель мы тогда перед собой поставили, но через пару минут мой оппонент оказался за этим ноутбуком, полностью готовый решать задачу. И буквально на первых же строках он допустил серьезную ошибку при аллоцировании памяти под… строку. Char *str = (char *)malloc(sizeof(char) * strlen(buffer));
Я думаю, определенно найдутся люди, которые спросят: «Разве что-то тут может быть не так?». А что именно - читайте по катом. Немного теории - своеобразный ЛикБез.Если знаете - листайте до следующего хэдера.Строка в C - это массив символов, который по-хорошему всегда должен заканчиваться "\0" - символом конца строки. Строки на стеке (статичные) объявляются вот так: Char str[n] = { 0 };
Присваивание { 0 } - «зануление» строки (опционально, объявлять можно и без него). Результат такой же, как у выполнения функций memset(str, 0, sizeof(str)) и bzero(str, sizeof(str)). Используется, чтобы в неинициализированных переменных не валялся мусор. Так же на стеке можно сразу проинициализировать строку: Char buf = "default buffer text\n";
Char *str = malloc(size);
В случае со стековой переменной, для определения размера массива я использовал обозначение n, в случае с переменной на куче - я использовал обозначение size. И это прекрасно отражает истинную суть отличия объявления на стеке от объявление с аллоцированием памяти на куче, ведь n как правило используется тогда, когда говорят о количестве элементов. А size - это уже совсем другая история… Нам поможет valgrindВ своей предыдущей статье я также упоминал о нем. Valgrind ( , два - небольшой how-to) - очень полезная программа, которая помогает программисту отслеживать утечки памяти и ошибки контекста - как раз те вещи, которые чаще всего всплывают при работе со строками.Давайте рассмотрим небольшой листинг, в котором реализовано что-то похожее на упомянутую мной программу, и прогоним ее через valgrind: #include $ gcc main.c
$ ./a.out
-> Hello, Habr!
$ 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== ERROR SUMMARY: 3 errors from 2 contexts (suppressed: 0 from 0)
Да очень просто. Весь «прикол» в том, что функция strlen не учитывает символ конца строки - "\0". Даже если его явно указать во входящей строке (#define HELLO_STRING «Hello, Habr!\n\0»), он будет проигнорирован. Чуть выше результата исполнения программы, строки -> Hello, Habr! есть подробный отчет, что и где не понравилось нашему драгоценному valgrind. Предлагаю самостоятельно посмотреть эти строчки и сделать выводы. Собственно, правильная версия программы будет выглядеть так: #include $ 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)
Что интересно, в большинстве случаев и первая и вторая программа будут работать одинаково, но если память, выделенная под строку, в которую не влез символ окончания, не была занулена, то функция printf(), при выводе такой строки, выведет и весь мусор после этой строки - будет выведено все, пока на пути printf() не встанет символ окончания строки. Однако, знаете, (strlen(str) + 1) - такое себе решение. Перед нами встают 2 проблемы:
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!");
Char * str = malloc(sizeof(char) * (strlen(str, "Hello, %s\n", "Habr!") + 1)); - не прокатит. Прототип функции strlen() выглядит так: #include Тут нам поможет то полезное свойство функции snprintf(), о котором я говорил выше. Давайте посмотрим на код следующей программы: #include $ 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)
$
Но с другой стороны, нам пришлось завести дополнительную переменную, да и конструкция Size_t needed_mem = snprintf(NULL, 0, "Hello, %s!\n", "Habr") + sizeof("\0");
Вообще, + sizeof("\0") можно убрать, если в конце строки формата явно указать "\0" (size_t needed_mem = snprintf(NULL, 0, «Hello, %s!\n\0
», «Habr»);), но это возможно отнюдь не всегда (в зависимости от механизма обработки строк мы можем выделить лишний байт). #define strsize(args...) snprintf(NULL, 0, args) + sizeof("\0")
Проверим наше решение на практике: #include $ 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)
Но, напоследок, скажу еще кое-что. В случае, если нам надо выделить память под какую-либо строку (даже с аргументами) есть уже полностью рабочее готовое решение . Речь идет о функции asprintf: #define _GNU_SOURCE /* See feature_test_macros(7) */
#include Наша программа, написанная с использованием asprintf() будет выглядеть так: #include $ 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)
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), с кем не бывает). Собственно говоря, это и сподвигло меня на создание сего творения, которое вы только что прочитали. Я надеюсь, кому-то эта статья будет полезной. К чему я это все городил - никакой язык не бывает прост. Везде есть свои тонкости. И чем больше тонкостей языка вы знаете, тем лучше ваш код. Я верю, что после прочтения этой статьи ваш код станет чуточку лучше:)
gastroguru © 2017
|