Библиотека работы со строками c. Ввод и вывод символьных строк в си. Массивы символьных строк
Хабра, привет!
Не так давно у со мной произошел довольно-таки интересный инцидент, в котором был замешан один из преподавателей одного колледжа информатики.
Разговор о программировании под 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 = "default buffer text\n";
Помимо этого строку можно объявить указателем и выделить под нее память на куче (heap):
Char *str = malloc(size);
size - количество байт, которые мы выделяем под строку. Такие строки называются динамическими (вследствие того, что нужный размер вычисляется динамически + выделенный размер памяти можно в любой момент увеличить с помощью функции realloc()).
В случае со стековой переменной, для определения размера массива я использовал обозначение n, в случае с переменной на куче - я использовал обозначение size. И это прекрасно отражает истинную суть отличия объявления на стеке от объявление с аллоцированием памяти на куче, ведь n как правило используется тогда, когда говорят о количестве элементов. А size - это уже совсем другая история…
Нам поможет valgrind
В своей предыдущей статье я также упоминал о нем. Valgrind ( , два - небольшой how-to) - очень полезная программа, которая помогает программисту отслеживать утечки памяти и ошибки контекста - как раз те вещи, которые чаще всего всплывают при работе со строками.Давайте рассмотрим небольшой листинг, в котором реализовано что-то похожее на упомянутую мной программу, и прогоним ее через valgrind:
#include
И, собственно, результат работы программы:
$ gcc main.c
$ ./a.out
-> Hello, Habr!
Пока ничего необычного. А теперь давайте запустим эту программу с valgrind!
$ 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
Пропускаем через valgrind:
$ 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 проблемы:
- А если нам надо выделить память под формируемую с помощью, например, s(n)printf(..) строку? Аргументы мы не поддерживаем.
- Внешний вид. Строка с объявлением переменной выглядит просто ужасно. Некоторые ребята к 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
const char *s не подразумевает, что передаваемая в s строка может быть строкой формата с переменным количеством аргументов.
Тут нам поможет то полезное свойство функции snprintf(), о котором я говорил выше. Давайте посмотрим на код следующей программы:
#include
Запускаем программу в valgrind:
$ 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)
$
Отлично. Поддержка аргументов у нас есть. Благодаря тому, что мы в качестве второго аргумента в функцию 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
Запускаем с valgrund:
$ 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
В качестве первого аргумента она принимает указатель на строку (**strp) и аллоцирует память по разыменованному указателю.
Наша программа, написанная с использованием asprintf() будет выглядеть так:
#include
И, собственно, в valgrind:
$ 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), с кем не бывает). Собственно говоря, это и сподвигло меня на создание сего творения, которое вы только что прочитали.
Я надеюсь, кому-то эта статья будет полезной. К чему я это все городил - никакой язык не бывает прост. Везде есть свои тонкости. И чем больше тонкостей языка вы знаете, тем лучше ваш код.
Я верю, что после прочтения этой статьи ваш код станет чуточку лучше:)
Удачи, Хабр!
В современном стандарте C++ определен класс с функциями и свойствами (переменными) для организации работы со строками (в классическом языке C строк как таковых нет, есть лишь массивы символов char):
#include
#include#include
Для работы со строками также нужно подключить стандартный namespace:
Using namespace std;
В противном случае придётся везде указывать описатель класса std::string вместо string .
Ниже приводится пример программы, работающей со string (в старых си-совместимых компиляторах не работает!):
#include
Основные возможности, которыми обладает класс string:
- инициализация массивом символов (строкой встроенного типа) или другим объектом типа string . Встроенный тип не обладает второй возможностью;
- копирование одной строки в другую. Для встроенного типа приходится использовать функцию strcpy() ;
- доступ к отдельным символам строки для чтения и записи. Во встроенном массиве для этого применяется операция взятия индекса или косвенная адресация с помощью указателя;
- сравнение двух строк на равенство. Для встроенного типа используются функции семейства strcmp() ;
- конкатенация (сцепление) двух строк, дающая результат либо как третью строку, либо вместо одной из исходных. Для встроенного типа применяется функция strcat() , однако чтобы получить результат в новой строке, необходимо последовательно задействовать функции strcpy() и strcat() , а также позаботиться о выделении памяти;
- встроенные средства определения длины строки (функции-члены класса size() и l ength()). Узнать длину строки встроенного типа можно только вычислением с помощью функции strlen() ;
- возможность узнать, пуста ли строка.
Рассмотрим эти базовые возможности более подробно.
Инициализация строк при описании и длина строки (не включая завершающий нуль-терминатор):
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 |
изменяет количество хранимых символов |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Пожалуйста, приостановите работу 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 приостанавливает работу программы, читает строку символов, введенных с клавиатуры, и помещает в символьный массив, имя которого передаётся функции в качестве параметра. Итак, что мы имеем. У нас есть задача: записать строку в массив ограниченного размера. То есть, мы должны как-то контролировать количество символов, вводимых пользователем. И тут нам на помощь приходит функция 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 . На этом разговор о вводе строк закончен. Идём дальше. 34--- Руководство по C# --- Строки С точки зрения регулярного программирования строковый тип данных string относится к числу самых важных в C#. Этот тип определяет и поддерживает символьные строки. В целом ряде других языков программирования строка представляет собой массив символов. А в C# строки являются объектами. Следовательно, тип string относится к числу ссылочных. Построение строкСамый простой способ построить символьную строку - воспользоваться строковым литералом . Например, в следующей строке кода переменной ссылки на строку str присваивается ссылка на строковый литерал: String str = "Пример строки"; В данном случае переменная str инициализируется последовательностью символов "Пример строки". Объект типа string можно также создать из массива типа char. Например: Char chararray = {"e", "x", "a", "m", "p", "l", "e"}; string str = new string(chararray); Как только объект типа string будет создан, его можно использовать везде, где только требуется строка текста, заключенного в кавычки. Постоянство строкКак ни странно, содержимое объекта типа string не подлежит изменению. Это означает, что однажды созданную последовательность символов изменить нельзя. Но данное ограничение способствует более эффективной реализации символьных строк. Поэтому этот, на первый взгляд, очевидный недостаток на самом деле превращается в преимущество. Так, если требуется строка в качестве разновидности уже имеющейся строки, то для этой цели следует создать новую строку, содержащую все необходимые изменения. А поскольку неиспользуемые строковые объекты автоматически собираются в "мусор", то о дальнейшей судьбе ненужных строк можно даже не беспокоиться. Следует, однако, подчеркнуть, что переменные ссылки на строки (т.е. объекты типа string) подлежат изменению, а следовательно, они могут ссылаться на другой объект. Но содержимое самого объекта типа string не меняется после его создания. Рассмотрим пример: Static void addNewString() { string s = "This is my stroke"; s = "This is new stroke"; } Скомпилируем приложение и загрузим результирующую сборку в утилиту ildasm.exe . На рисунке показан CIL-код, который будет сгенерирован для метода void addNewString(): Обратите внимание на наличие многочисленных вызовов кода операции ldstr (загрузка строки). Этот код операции ldstr в CIL предусматривает выполнение загрузки нового объекта string в управляемую кучу. В результате предыдущий объект, в котором содержалось значение "This is my stroke", будет в конечном итоге удален сборщиком мусора. Работа со строкамиВ классе System.String предоставляется набор методов для определения длины символьных данных, поиска подстроки в текущей строке, преобразования символов из верхнего регистра в нижний и наоборот, и т.д. Далее мы рассмотрим этот класс более подробно. Поле, индексатор и свойство класса StringВ классе String определено единственное поле: Public static readonly string Empty; Поле Empty обозначает пустую строку, т.е. такую строку, которая не содержит символы. Этим оно отличается от пустой ссылки типа String, которая просто делается на несуществующий объект. Помимо этого, в классе String определен единственный индексатор, доступный только для чтения: Public char this { get; } Этот индексатор позволяет получить символ по указанному индексу. Индексация строк, как и массивов, начинается с нуля. Объекты типа String отличаются постоянством и не изменяются, поэтому вполне логично, что в классе String поддерживается индексатор, доступный только для чтения. И наконец, в классе String определено единственное свойство, доступное только для чтения: Public int Length { get; } Свойство Length возвращает количество символов в строке. В примере ниже показано использование индексатора и свойства Length: Using System; class Example { static void Main() { string str = "Простая строка"; // Получить длину строки и 6й символ в строке используя индексатор Console.WriteLine("Длина строки - {0}, 6й символ - "{1}"", str.Length, str); } } Операторы класса StringВ классе String перегружаются два следующих оператора: == и!=. Оператор == служит для проверки двух символьных строк на равенство. Когда оператор == применяется к ссылкам на объекты, он обычно проверяет, делаются ли обе ссылки на один и тот же объект. А когда оператор == применяется к ссылкам на объекты типа String, то на предмет равенства сравнивается содержимое самих строк. Это же относится и к оператору!=. Когда он применяется к ссылкам на объекты типа String, то на предмет неравенства сравнивается содержимое самих строк. В то же время другие операторы отношения, в том числе =, сравнивают ссылки на объекты типа String таким же образом, как и на объекты других типов. А для того чтобы проверить, является ли одна строка больше другой, следует вызвать метод Compare(), определенный в классе String. Как станет ясно дальше, во многих видах сравнения символьных строк используются сведения о культурной среде. Но это не относится к операторам == и!=. Ведь они просто сравнивают порядковые значения символов в строках. (Иными словами, они сравнивают двоичные значения символов, не видоизмененные нормами культурной среды, т.е. региональными стандартами.) Следовательно, эти операторы выполняют сравнение строк без учета регистра и настроек культурной среды. Методы класса StringВ следующей таблице перечислены некоторые наиболее интересные методы этого класса, сгруппированные по назначению:
Пример следующей программы использует несколько из вышеуказанных методов: Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { static void Main(string args) { // Сравним первые две строки string s1 = "это строка"; string s2 = "это текст, а это строка"; if (String.CompareOrdinal(s1, s2) != 0) Console.WriteLine("Строки s1 и s2 не равны"); if (String.Compare(s1, 0, s2, 13, 10, true) == 0) Console.WriteLine("При этом в них есть одинаковый текст"); // Конкатенация строк Console.WriteLine(String.Concat("\n" + "Один, два ","три, четыре")); // Поиск в строке // Первое вхождение подстроки if (s2.IndexOf("это") != -1) Console.WriteLine("Слово \"это\" найдено в строке, оно "+ "находится на: {0} позиции", s2.IndexOf("это")); // Последнее вхождение подстроки if (s2.LastIndexOf("это") != -1) Console.WriteLine("Последнее вхождение слова \"это\" находится " + "на {0} позиции", s2.LastIndexOf("это")); // Поиск из массива символов char myCh = {"Ы","х","т"}; if (s2.IndexOfAny(myCh) != -1) Console.WriteLine("Один из символов из массива ch "+ "найден в текущей строке на позиции {0}", s2.IndexOfAny(myCh)); // Определяем начинается ли строка с заданной подстроки if (s2.StartsWith("это текст") == true) Console.WriteLine("Подстрока найдена!"); // Определяем содержится ли в строке подстрока // на примере определения ОС пользователя string myOS = Environment.OSVersion.ToString(); if (myOS.Contains("NT 5.1")) Console.WriteLine("Ваша операционная система Windows XP"); else if (myOS.Contains("NT 6.1")) Console.WriteLine("Ваша операционная система Windows 7"); Console.ReadLine(); } } } Немного о сравнении строк в C#Вероятно, из всех операций обработки символьных строк чаще всего выполняется сравнение одной строки с другой. Прежде чем рассматривать какие-либо методы сравнения строк, следует подчеркнуть следующее: сравнение строк может быть выполнено в среде.NET Framework двумя основными способами: Во-первых, сравнение может отражать обычаи и нормы отдельной культурной среды, которые зачастую представляют собой настройки культурной среды, вступающие в силу при выполнении программы. Это стандартное поведение некоторых, хотя и не всех методов сравнения. И во-вторых, сравнение может быть выполнено независимо от настроек культурной среды только по порядковым значениям символов, составляющих строку. Вообще говоря, при сравнении строк без учета культурной среды используется лексикографический порядок (и лингвистические особенности), чтобы определить, является ли одна строка больше, меньше или равной другой строке. При порядковом сравнении строки просто упорядочиваются на основании невидоизмененного значения каждого символа. В силу отличий способов сравнения строк с учетом культурной среды и порядкового сравнения, а также последствий каждого такого сравнения настоятельно рекомендуется руководствоваться лучшими методиками, предлагаемыми в настоящее время корпорацией Microsoft. Ведь выбор неверного способа сравнения строк может привести к неправильной работе программы, когда она эксплуатируется в среде, отличающей от той, в которой она разработана. Выбор способа сравнения символьных строк представляет собой весьма ответственное решение. Как правило и без всяких исключений, следует выбирать сравнение строк с учетом культурной среды, если это делается для целей отображения результата пользователю (например, для вывода на экран ряда строк, отсортированных в лексикографическом порядке). Но если строки содержат фиксированную информацию, не предназначенную для видоизменения с учетом отличий в культурных средах, например, имя файла, ключевое слово, адрес веб-сайта или значение, связанное с обеспечением безопасности, то следует выбрать порядковое сравнение строк. Разумеется, особенности конкретного разрабатываемого приложения будут диктовать выбор подходящего способа сравнения символьных строк. В классе String предоставляются самые разные методы сравнения строк, которые перечислены в таблице выше. Наиболее универсальным среди них является метод Compare(). Он позволяет сравнивать две строки полностью или частично, с учетом или без учета регистра, способа сравнения, определяемого параметром типа StringComparison , а также сведений о культурной среде, предоставляемых с помощью параметра типа CultureInfo . Те перегружаемые варианты метода Compare(), которые не содержат параметр типа StringComparison, выполняют сравнение символьных строк с учетом регистра и культурной среды. А в тех перегружаемых его вариантах, которые не содержат параметр типа CultureInfo, сведения о культурной среде определяются текущей средой выполнения. Тип StringComparison представляет собой перечисление, в котором определяются значения, приведенные в таблице ниже. Используя эти значения, можно организовать сравнение строк, удовлетворяющее потребностям конкретного приложения. Следовательно, добавление параметра типа StringComparison расширяет возможности метода Compare() и других методов сравнения, например, Equals(). Это дает также возможность однозначно указывать способ предполагаемого сравнения строк. В силу имеющих отличий между сравнением строк с учетом культурной среды и порядковым сравнением очень важно быть предельно точным в этом отношении.
В любом случае метод Compare() возвращает отрицательное значение, если первая сравниваемая строка оказывается меньше второй; положительное значение, если первая сравниваемая строка больше второй; и наконец, нуль, если обе сравниваемые строки равны. Несмотря на то что метод Compare() возвращает нуль, если сравниваемые строки равны, для определения равенства символьных строк, как правило, лучше пользоваться методом Equals() или же оператором ==. Дело в том, что метод Compare() определяет равенство сравниваемых строк на основании порядка их сортировки. Так, если выполняется сравнение строк с учетом культурной среды, то обе строки могут оказаться одинаковыми по порядку их сортировки, но не равными по существу. По умолчанию равенство строк определяется в методе Equals(), исходя из порядковых значений символов и без учета культурной среды. Следовательно, по умолчанию обе строки сравниваются в этом методе на абсолютное, посимвольное равенство подобно тому, как это делается в операторе ==. Несмотря на большую универсальность метода Compare(), для простого порядкового сравнения символьных строк проще пользоваться методом CompareOrdinal(). И наконец, следует иметь в виду, что метод CompareTo() выполняет сравнение строк только с учетом культурной среды. В приведенной ниже программе демонстрируется применение методов Compare(), Equals(), CompareOrdinal(), а также операторов == и!= для сравнения символьных строк. Обратите внимание на то, что два первых примера сравнения наглядно демонстрируют отличия между сравнением строк с учетом культурной среды и порядковым сравнением в англоязычной среде: Using System; class Example { static void Main() { string str1 = "alpha"; string str2 = "Alpha"; string str3 = "Beta"; string str4 = "alpha"; string str5 = "alpha, beta"; int result; // Сначала продемонстрировать отличия между сравнением строк // с учетом культурной среды и порядковым сравнением result = String.Compare(str1, str2, StringComparison.CurrentCulture); Console.Write("Сравнение строк с учетом культурной среды: "); if (result 0) Console.WriteLine(str1 + " больше " + str2); else Console.WriteLine(str1 + " равно " + str2); result = String.Compare(str1, str2, StringComparison.Ordinal); Console.Write("Порядковое сравнение строк: "); if (result 0) Console.WriteLine(str1 + " больше " + str2); else Console.WriteLine(str1 + " равно " + str4); // Использовать метод CompareOrdinal() result = String.CompareOrdinal(str1, str2); Console.Write("Сравнение строк методом CompareOrdinal():\n"); if (result 0) Console.WriteLine(str1 + " больше " + str2); else Console.WriteLine(str1 + " равно " + str4); Console.WriteLine(); // Определить равенство строк с помощью оператора == // Это порядковое сравнение символьных строк if (str1 == str4) Console.WriteLine(str1 + " == " + str4); // Определить неравенство строк с помощью оператора!= if(str1 != str3) Console.WriteLine(str1 + " != " + str3); if(str1 != str2) Console.WriteLine(str1 + " != " + str2); Console.WriteLine(); // Выполнить порядковое сравнение строк без учета регистра, // используя метод Equals() if(String.Equals(str1, str2, StringComparison.OrdinalIgnoreCase)) Console.WriteLine("Сравнение строк методом Equals() с " + "параметром OrdinalIgnoreCase:\n" + str1 + " равно " + str2); Console.WriteLine (); // Сравнить части строк if(String.Compare(str2, 0, str5, 0, 3, StringComparison.CurrentCulture) > 0) { Console.WriteLine("Сравнение строк с учетом текущей культурной среды:" + "\n3 первых символа строки " + str2 + " больше, чем 3 первых символа строки " + str5); } } } Выполнение этой программы приводит к следующему результату: Строка – это последовательность ASCII или UNICODE символов. Строки в си , как и в большинстве языков программирования высокого уровня рассматриваются как отдельный тип, входящий в систему базовых типов языка. Так как язык СИ по своему происхождению является языком системного программирования, то строковый тип данных в нем как таковой отсутствует, а в качестве строк в си используются обычные массивы символов.Исторически сложилось два представления формата строк: 1. формат ANSI; 2. строки с завершающим нулем (используется в СИ). Формат ANSI устанавливает, что значением первой позиции в строке является ее длина, а затем
следуют сами символы строки. Например, представление строки "Моя строка!" будет следующим: Объявление строк в СИ Строки реализуются посредством массивов символов. Поэтому объявление ASCII строки имеет следующий синтаксис: char имя[длина]; Объявление строки в си имеет тот же синтаксис, что и объявление одномерного символьного массива. Длина строки должна представлять собой целочисленное значение (в стандарте C89 – константа, в стандарте C99 может быть выражением). Длина строки указывается с учетом одного символа на хранение завершающего нуля, поэтому максимальное количество значащих символов в строке на единицу меньше ее длины. Например, строка может содержать максимально двадцать символов, если объявлена следующим образом: char str; Инициализация строки в си осуществляется при ее объявлении, используя следующий синтаксис: char str[длина] = строковый литерал; Строковый литерал – строка ASCII символов заключенных в двойные кавычки. Примеры объявления строк с инициализацией: char str1 = "Введите значение: ", str2 = ""; Пример: const char message = "Сообщение об ошибке!"; Работа со строками в СИ Так как строки на языке СИ являются массивами символов, то к любому символу строки можно обратиться по его индексу. Для этого используется синтаксис обращения к элементу массива, поэтому первый символ в строке имеет индекс ноль. Например, в следующем фрагменте программы в строке str осуществляется замена всех символов ‘a’ на символы ‘A’ и наоборот. for(int i=0;str[i]!=0;i++) { if(str[i] == ‘a’) str[i] = ‘A’; else if(str[i] == ‘A’) str[i] = ‘a’; } Массивы строк в СИ Объявление массивов строк в языке СИ также возможно. Для этого используются двумерные массивы символов, что имеет следующий синтаксис: char имя[количество][длина]; Первым размером матрицы указывается количество строк в массиве, а вторым – максимальная (с учетом завершающего нуля) длина каждой строки. Например, объявление массива из пяти строк максимальной длиной 30 значащих символов будет иметь вид: char strs; При объявлении массивов строк можно производить инициализацию: char имя[количество][длина] = {строковый литерал №1, ... строковый литерал №N}; Число строковых литералов должно быть меньше или равно количеству строк в массиве. Если число строковых литералов меньше размера массива, то все остальные элементы инициализируются пустыми строками. Длина каждого строкового литерала должна быть строго меньше значения длины строки (для записи завершающего нуля). Например: char days = { "Январь", "Февраль", "Март", ”Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь","Октябрь", "Ноябрь", "Декабрь" }; При объявлении массивов строк с инициализацией допускается не указывать количество строк в квадратных скобках. В таком случае, количество строк в массиве будет определено автоматически по числу инициализирующих строковых литералов. Например, массив из семи строк: char days = { "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье" }; Функции для работы со строками в СИ Все библиотечные функции, предназначенные для работы со строками, можно разделить на три группы: 1. ввод и вывод строк; 2. преобразование строк; 3. обработка строк. Ввод и вывод строк в СИ Для ввода и вывода строковой информации можно использовать функции форматированного ввода и вывода (printf и scanf). Для этого в строке формата при вводе или выводе строковой переменной необходимо указать спецификатор типа %s. Например, ввод и последующий вывод строковой переменной будет иметь вид: char str = ""; printf("Введите строку: "); scanf("%30s”,str); printf("Вы ввели: %s”,str); Недостатком функции scanf при вводе строковых данных является то, что символами разделителями данной функции являются: 1. перевод строки, 2. табуляция; 3. пробел. Поэтому, используя данную функцию невозможно ввести строку, содержащую несколько слов, разделенных пробелами или табуляциями. Например, если в предыдущей программе пользователь введет строку: "Сообщение из нескольких слов", то на экране будет выведено только "Сообщение". Для ввода и вывода строк в библиотеке stdio.h содержатся специализированные функции gets и puts. Функция gets предназначена для ввода строк и имеет следующий заголовок: int puts(const char *string); Простейшая программа: ввод и вывод строки с использованием функций gets и puts будет иметь вид: char str = ""; printf("Введите строку: "); gets(str); printf("Вы ввели: "); puts(str); Помимо функций ввода и вывода в потоки в библиотеке stdio.h присутствуют функции форматированного ввода и вывода в строки. Функция форматированного ввода из строки имеет следующий заголовок: int sscanf(const char * restrict buffer,const char * restrict string, ...); Функции форматированного вывода в строку имеют следующие заголовки: int sprintf(char * restrict buffer, int snprintf(char * restrict buffer, size_t maxsize, const char * restrict format, ...); Преобразование строк В СИ для преобразования строк, содержащих числа, в численные значения в библиотеке stdlib.h предусмотрен следующий набор функций: double atof(const char *string); // преобразование строки в число типа double int atoi(const char *string); // преобразование строки в число типа int long int atol(const char *string); // преобразование строки в число типа long int long long int atoll(const char *string); // преобразование строки в число типа long long int Корректное представление вещественного числа в текстовой строке должно удовлетворять формату: [{+|-}][цифры][.цифры][{e|E}[{+|-}]цифры] После символов E, e указывается порядок числа. Корректное представление целого числа в текстовой строке
должно удовлетворять формату: Помимо приведенных выше функций в библиотеке stdlib.h доступны также следующие функции
преобразования строк в вещественные числа: Аналогичные функции присутствуют и для преобразования строк в целочисленные значения: Функции обратного преобразования (численные значения в строки) в библиотеке stdlib.h присутствуют, но они не регламентированы стандартом, и рассматриваться не будут. Для преобразования численных значений в строковые наиболее удобно использовать функции sprintf и snprintf. Обработка строк В библиотеке string.h содержаться функции для различных действий над строками. Функция вычисления длины строки: size_t strlen(const char *string); Пример: char str = "1234"; int n = strlen(str); //n == 4 Функции копирования строк: char * strcpy(char * restrict dst, const char * restrict src); char * strncpy(char * restrict dst, const char * restrict src, size_t num); Функции сравнения строк: int strcmp(const char *string1, const char *string2); int strncmp(const char *string1, const char *string2,size_t num); Функции осуществляют сравнение строк по алфавиту и возвращают: положительное значение – если string1 больше string2; отрицательное значение – если string1 меньше string2; нулевое значение – если string1 совпадает с string2; Функции объединения (конкатенации) строк: Функции поиска символа в строке: Функция поиска строки в строке: Пример: Функция поиска первого символа в строке из заданного набора символов: Функции поиска первого символа в строке не принадлежащему заданному набору символов: Функции поиска первого символа в строке из заданного набора символов: Функция поиска следующего литерала в строке: |