Язык С++ не содержит специального встроенного в язык строкового типа, как это имело место, например, в языке Pascal. Термин “строка” имеет два значения.
Во-первых, в смысле языка С строка рассматривается как массив символов произвольной длины, оканчивающийся нулевым символом (нуль-символом, нуль-терминатором, NULL-символом), ASCII код которого равен нулю. В тексте программы он обозначается как ‘\0’. Для работы с такими строками есть большое количество разнообразных самостоятельных функций, не включённых в классы (см. 6.3). Их прототипы находятся в заголовочном файле string.h. В этом параграфе рассматриваются такие строки и соответствующие методы программирования, основанные на использовании указателей при работе с массивами (см.§3). Заметим, что навыки работы с указателями для организации циклов и нужны, прежде всего, для того, чтобы можно было с успехом использовать встроенные строковые функции. В противном случае непростые задачи ещё более усложняются, если программировать то, что запрограммировано в таких функциях.
Во-вторых, строку можно рассматривать в смысле языка. С++ как объект стандартного класса String. Методы и другие средства этого класса автоматизируют работу со строками. В превосходном учебном пособии по С++ [ ] приведены несколько доводов в пользу включения в язык С++ стандартного класса String. Но в то же время здесь же подчёркивается, что ни в коем случае нельзя отказываться от обычных, оканчивающихся нулём строк как массивов символов. Они остаются наиболее эффективным способом реализации символьных строк. Использование возможностей класса String будет рассмотрено на втором курсе после подробного изучения объектно-ориентированного программирования.
Строку можно объявить и инициализировать без явного использования указателей, как статический одномерный массив символов. При этом необходимо указать размерность, достаточную для размещения текста и символа конца строки (‘\0’). Этот символ надо явно записать в конце списка символов, например:
Компилятор допускает также более простой и удобный способ, который, как правило, используется на практике
char T[11]=“математика”;.
В этом и других случаях нулевой символ в конце строковой константы записывать не надо, он добавляется компилятором автоматически. Если для строки не указана размерность, она будет определена в зависимости от её длины. Например,
сhar Fak[]=”ММФ”;
инициализирует строку из четырёх символов, так как добавляется символ конца строки.
Этот символ конца строки, как и число “нуль”, играет роль false в операторах сравнения типа
if (Fak[i]) …;
То есть, если i-й символ не является символом ‘\0’, то получается true, в противном случае — false. Поэтому вышеприведённая запись равносильна
if (Fak[i] !=’\0’)…;
Наиболее профессиональный способ объявления строки такой:
char *s;.
Объявляем переменную-указатель, которая будет содержать адрес начала строки. Перед первым использованием надо обязательно определить значение переменной указателя s. Это можно сделать по общим правилам одним из следующих способов:
a) для этого можно использовать ранее определённую статическую строку: s=Fak;
b) используем определённый предварительно адрес другой строки. Например, если определили s, то эту переменную можно использовать таким образом:
char *s2=s; или char *s2; s2=s;
c) зарезервировать память для строки и определить значение s можно с помощью операции new:
unsigned n=10; char *s =new char[n], или char *s; s =new char[n],
где n — константа или переменная, значение которой должно быть, конечно, определено.
Рассматривая строку как обычный одномерный массив символов, можно запрограммировать некоторые простые алгоритмы. Однако более профессиональным и более эффективным способом, прежде всего для программиста, является использование встроенных функций. Необходимо обратить внимание на то, что количество такого рода средств (как правило, это методы и операции стандартных классов) для работы с текстовой информацией в современных системах увеличивается.
При вводе строк c помощью cin операция >> не всегда будет работать правильно. Если в строке есть пробелы, они игнорируются и строка вводится до первого пробела. Поэтому удобнее использовать специальную функцию для ввода строк gets. Например,
char Str[20]; gets(Str);
Функция считывает символы из стандартного текстового потока stdin, связанного с клавиатурой (подробности см. в главе “Файлы”) , и помещает их в массив символов. Символы считываются до тех пор, пока не встретится новая строка. Символ перехода на новую строку не добавляется в строку, а преобразовывается в нулевой символ, завершающий строку. Другими словами, символ ‘\0’ добавляется в строку автоматически после нажатия на клавишу “Ввод”. В случае успеха возвращается строка, в противном случае функция возвращает указатель со значением NULL, то есть пустой (неопределённый) указатель. Напомним, что он играет роль false в операциях сравнения.
Для вывода строки на экран рекомендуется использовать функцию puts, например,
puts( Str);
Символ ‘\0’ в конце строки преобразуется в символ новой строки, то есть после выполнения этой функции (а не перед) осуществляется переход на новую строку экрана.
Вывести строку можно также с помощью функции printf, используя формат s:
printf(“%s”, Str);
Самостоятельно проведите компьютерный эксперимент для изучения такого метода вывода строки. Рассмотрите случай использования модификатора длины. Что получится, если вывести строку одним из операторов: 1) printf(“%30s”, Str); 2) printf(“%15s”, Str);?
Пусть строка объявлена как статический массив сивмолов и проинициализирована:
char t[30]="ABCDEF";
Строку можно вывести посимвольно:
for(int i=0; i<30; i++) putchar(t[i]); cout<<"End of string "<<endl;
В результате выведем строку ABCDEF и пробелы, что подтверждает вывод текста End of string не сразу после строки, а через пробелы. Это говорит о том, что память резервируется для указанного при объявлении количества символов, несмотря на то, что строка требует меньший объём памяти. Строку можно вывести посимвольно и так:
for (int i=0; t[i]; i++) putchar(t[i]); cout<<"End of string "<<endl;
Цикл выполняется, пока t[i] истинно, то есть пока t[i] не является символом конца строки. Это равносильно
for (int i=0; t[i]!=’\0’; i++) putchar(t[i]);
Вывод текста End of string сразу после ABCDEF без никаких пробелов означает, что в этом варианте выводим не все 30 символов строки t, а выводим символы до тех пор, пока не встретится символ ‘\0’, то есть пока не достигнем конца строки.
Посимвольный вывод используется, если строку надо вывести в оригинальном виде: по диагонали экрана, выделить некоторые символы другим цветом, вывести только часть строки и т.п.
Примеры алгоритмов работы со строками.
Пример 1. Добавить одну строку (s) к концу другой (t).
int main()
{
char t[20]="ABCDEF", s[]="uvxyz", *tp=t, *sp=s;
// Первый вариант
// Сначала находим конец строки- приёмника t.
while (*tp!='\0') tp++;
/* После выполнения этого цикла в tp будет адрес символа, следующего после последнего символа строки (буквы F), то есть адрес символа конца строки. */
while ((*tp=*sp)!='\0')
{tp++; sp++; }
puts(t);
// t и tp — адреса начала одной и той же области памяти со строкой
// "ABCDEF"
getch(); return 0;
}
В последнем цикле порядок выполнения операций следующий:
1) *sp — взять символ, на который указывает sp.
2) *tp=*sp занести этот символ в ячейку по адресу tp.
3) (*tp=*sp)!='\0') сравнить его с символом конца строки.
4) Если это был не символ конца строки, то в теле цикла увеличиваем на один байт значения обоих указателей, то есть переходим к следующим символам. Арифметические операции над указателями при работе со строками выполняются обычным образом, так как один символ занимает один байт. Поэтому tp++; увеличивает значение адреса на единицу, а не на 1*4, как это было, например, для целочисленного массива.В противном случае после копирования символа '\0' осуществляется выход из цикла.
Зачем внутренние скобки в заголовке оператора while? Без скобок
while (*tp=*sp!='\0') // ошибка
сначала выполнялась бы операция сравнения, так как она имеет более высокий приоритет, чем операция присваивания. И результат сравнения (0 или 1) запомнился бы в *tp. Но так как символьный и целый типы в языке С совместимы (см. первый семестр), то ни ошибки компиляции, ни ошибки этапа выполнения не будет, но программа будет выполняться неправильно.
Второй вариант реализации алгоритма соединения двух строк.
Если вышеприведённую программу продолжать, то сначала надо выполнить
tp=t; sp=s; .
Во-первых, получить адрес символа конца строки можно и так:
tp+=strlen(t);
где встроенная функция strlen(t) находит длину строки, то есть количество символов до символа '\0'.
Соединение двух строк можно выполнить компактнее и “красивее”:
while (*tp++=*sp++); // обязательно в конце заголовка символ “;”.
puts(t); puts(s);
Обратим внимание, что в цикле операция ++ постфиксная и она относится не к символу, а к указателю. Увеличивается на единицу не код символа, а значение указателя, то есть мы “перемещаемся” к следующему символу.
Упражнения.
1) Записать короче, более компактно первый цикл для поиска конца строки-приёмника t.
2) Наоборот, записать более простой, понятный (в стиле языка Pascal) вариант второго цикла, в котором в заголовке оператора while нельзя использовать присваивание.
3) Составить и проверить функцию для реализации аналогичного алгоритма копирования одной строки в другую.
Пример2. Составить и проверить функции для выделения подстроки и для удаления подстроки.
/* Выделение подстроки: s1 – адрес начала исходной строки, s2 – адрес выделенной подстроки, n_start – номер символа, начиная с которого выделяем подстроку, size_sub – количество выделяемых символов */
void mysub(char *s1, char *s2, int n_start, int size_sub)
{ char *p=s1;
p+=n_start;
strncpy (s2, p, size_sub);
}
/* strncpy — стандартная функция для копирования size_sub символов, начиная с адреса p, в строку по адресу s2 */
/* Из строки s, начиная с символа с номером n_start, удаляем n_del символов */
void mydelete (char *s, int n_start, int n_del)
{ char *p=s,*q;
p+=n_start;
q=p+n_del;
strcpy(p,q);
/* стандартная функция для копирования всей строки, начиная с адреса q, в строку с адресом p */
}
int main(int argc, char* argv[])
{
char S2[]="Variant: Testing of function";
char *Sub=new char[20]; mysub(S2,Sub,20,8);
/*С помощью “своей” функции из S2, начиная с символа с номером 20, выделили 8 символов, т. е. слово “ function ” */
puts(Sub); getch();
mydelete(S2,9,7);
/*С помощью “своей” функции из S2, начиная с символа с номером 9, удалили 7 символов, т. е. удалили “ Testing” */
puts(S2); getch();
return 0;
}
Пример 3.Функция считает, сколько раз подстрока sub встречается в строке s
int count_of_sub(char *s, char *sub)
{ unsigned k=0;
char *p=s, *q;
while(1)
{ q=strstr(p,sub);
/* Стандартная функция находит адрес первого появления подстроки sub. При этом поиск осуществляется, начиная с адреса p. Если подстрока не найдена, то q=NULL, что равносильно false */
if (!q) return k; // Если подстрока не найдена, выход из функции
k++;
p=q+strlen(sub);
/* В p помещаем адрес символа, следующего после найденной подстроки. Дальше поиск будет осуществляться, начиная с этого символа */
}
}
int main()
{ char S[]="Text abcdetexttextxyztext";
cout<<count_of_sub(S,"text");
getch(); return 0;
}
В функции main() нашли, сколько раз в строке S встречается подстрока “text”. Программа выведет число 3, а не 4, так как первый раз “Text” начинается с большой буквы.
Рассматривается более сложный вариант этого же примера. Ввести последовательность строк, не формируя из них массива строк. Ввод не менее трёх подряд идущих символов “@” – конец ввода. Для каждой строки найти, сколько раз подстрока “for” встречается в ней. Найти наибольшее такое количество. Заданную подстроку вывести другим цветом в разреженном виде, т. е. с пробелами между символами. После последнего for текст в строке не выводится
int main()
{ clrscr(); char t[80], *p=t, *q;
int m, n, nmax=0; // nmax – наибольшее количество повторений “for”
gets(t); // ввод первой строки
do // цикл для анализа всех строк
{ p=t; n=0; // n – количество “for” в одной строке
do { // цикл для анализа одной строки
char *u;
textcolor(2); // зелёный цвет для вывода текста
q=strstr(p,"for");
for(u=p;u<q;u++) // вывод текста, отличного от “for”
cprintf("%c",*u);
if(!q) break;
// выход из внутреннего цикла, если не нашли подстроку
n++;
p=q+3; // 3 – длина строки “for”
textcolor(11); // голубой цвет для вывода “for”
for(;q<p;q++) cprintf("%c ",*q);
// вывод “for” c пробелами между символами
}
while(*p);
cout<<" Number of \“for\”"<<n<<endl;
if(n>nmax) nmax=n;
gets(t); // ввод следующей строки
}
while(!strstr(t,"@@@"));
cout<< " MAX "<<nmax<<endl;
getch(); return 0;
}
Выход из внешнего цикла выполнится, если введём три и более подряд расположенные указанные символы “@”.
Вариант а.В случае, если введём в самом начале, или после набранной строки три и более подряд идущих символа ‘@’, например, “@@@@@”, то функция strstr(t,"@@@") найдёт адрес первого появления трёх символов в строке. Так как такие символы, а, точнее, последовательность из трёх символов ‘@’, есть в строке, то будет найден не пустой адрес. Это равносильно true, и, так как используется операция отрицания, выполняется выход из внешнего цикла.
Вариант b. Введём текст, подлежащий обработке, без указанной последовательности из трёх подряд идущих символов ‘@’. Тогда функция strstr(t,"@@@") возвращает нулевой указатель NULL, так как подстроки "@@@" нет. Это равносильно false. Поэтому с учётом операции “отрицание” выражение в скобках внешнего оператора do … while примет значение true и цикл будет продолжен. Такой же результат будет и в случае, если введём в строке "@@" или "@"
Пример 4.
Под словом будем понимать последовательность символов, отличных от специальных знаков, используемых в качестве разделителей слов. Некоторые из этих символов перечислены в тексте программы в строке pat. При тестировании будем использовать только указанные здесь символы для разделения слов. Другие разделители, не указанные в строке, будут восприниматься как символы слова. Между словами может быть любое количество таких не обязательно одинаковых символов (например, три подряд идущих точки, скобка и пробел и т.п.). В строке найти количество слов, у которых первый и последний символы одинаковые.
int main()
{ char t[80], pat[]=" ,.;:-()", *p=t, c1, c2; int k=0;
gets(t);
do
{ p+=strspn(p,pat); // в ячейке p – адрес начала слова
c1=*p; // первый символ слова
p+=strcspn(p,pat); /* адрес символа, следующего после последнего символа слова, т.е. адрес первого разделителя */
c2=*(p-1); // последний символ слова
if (c1 = = c2)k++; //сравниваем первый и последний символы слова
}
while (*p); /* пока не найден символ конца строки, который играет роль false при сравнении
или while (*p!='\0'); */
cout<<"\nResult "<<k<<endl;
getch(); return 0;
}
Функция strspn возвращает количество подряд идущих символов, которые есть в строке pat, начиная с адреса p. Поэтому оператор
p+=strspn(p,pat)
изменяет адрес на найденное количество. Другими словами, мы c помощью переменной указателя p “перемещаемся” в начало слова. Функция strcspn возвращает количество подряд идущих символов, которых нет в строке pat, начиная с адреса p. Поэтому таким образом “перемещаемся” за последний символ слова.
Заметим, что если надо проанализировать длину слова и (или) выполнить посимвольный анализ самого слова, то вместо одного оператора
p+=strcspn(p,pat);
необходимо записать два. Сначала отдельно находим длину слова, например,
Len= strcspn(p,pat);,
где Len — переменная целого типа. Затем анализируем полученную длину и (или) слово, то есть Len символов, начиная с адреса p. Выполняем требуемые действия, и изменяем значение указателя p: