Выбери формат для чтения
Загружаем конспект в формате pdf
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
Лекция 5
Строковый тип данных
Теперь, когда мы знакомы с массивом, можно узнать о самом распространенном типе
данных, а именно о символьных массивах или строках. Сразу стоит понять, что привычного
понятия строки в Си нет. Строки — это довольно удобный механизм для хранения и работы с
такими данными, как:
“John”, “ул. Ленина д. 44, кв. 15” или даже “vdklfjdfloter858789”
В Си есть понятие символьный массив, который вообще не дает никаких удобств для
хранения, а особенно для работы с такими данными. Но тем не менее, работать с этим
данными приходится. Как правило, программисты сами для себя разрабатывают удобные
инструменты для работы со строками, чем вы и займетесь на лабораторной.
Символьный массив по своим свойствам ничем не отличается от обычного int массива. Вам
придется учитывать, что сама переменная массива лишь «указатель» на последовательность
символов. А с указателем такие действия, как, например, сложение будет работать не так, как
вы представляете. В частности:
char Name[] = “John” + “ “ + “Ivan”;
не даст привычного ”John Ivan”, это вообще не скомпилируется.
Однако бывают чуть более непонятные ошибки. Допустим, хотите вы добавить к ”John”
символ 5, чтобы получился ”John5”.
Написав такой код:
char Name[] = "John";
printf("%s", Name+5);
вы не расстроите компилятор, он проглотит это, но выдаст вам ничего или какую-нибудь
белиберду. И в чем ошибка? Это легко объяснить, представьте, что память выглядит так
J
^Name
O
H
N
\0
Name – указывает на первую букву последовательности, но если добавить к ней цифру, то
она станет указывать на 5-ый номер последовательности
J
^Name
O
H
N
\0
^tmp
('\0' – символ конца строки, в сущности, строка может быть задана и так ”John\0op”, только
при выводе вы увидите всего лишь John, и размер строки будет равен 4. Т.е. Все что после '0\'
будет игнорироваться)
Обратите внимание, что в нашем коде, не сама Name стала указывать в другое место, а только
временная переменная (tmp = Name + 5), которая используется в printf и сразу же пропадет.
Код, что мы написали, вполне легальный, но вот работает он не так, как ожидается
новичками. Так как же написать, чтобы все заработало? Тут простой и одновременно
сложный ответ.
Написать можно так:
char Name[256] = "John";
strcat(Name, "5");
printf("%s", Name);
Это рабочий код. Но сама функция strcat, которая объединяет две строки в одну работает не
всегда хорошо. Но об этом позже.
Рассмотрим прототип функции strcat
char * strcat ( char * destination, const char * source );
Мы можем увидеть, что функция возвращает нам неконстантную строку (значит эта строка
может меняться), принимает также неконстантную строку (это очевидно, потому что она в
итоге меняется) и константную строку (так как она не меняется, а лишь добавляется к
первой, то мы хотим быть уверены в том, что функция ее не изменит)
Возвращается здесь destination, поэтому не обязательно писать:
char * newName = strcat(Name, "5");
printf("%s", newName);
Отсюда мы можем узнать следующие вещи о языке Си:
1. char str[] может интерпретироваться, как char * str, и наоборот.
2. функции могут возвращать не только int
3. в языке есть константы.
Итак, чтобы совсем не увязнуть во всем этом, давайте разбираться по порядку.
Что есть функция?
Если вы когда либо писали на более древних языках программирования, то могли слышать
такое слово, как подпрограмма. Упрощенно: подпрограмма — независимая единица кода,
которая может быть вызвана несколько раз из основного кода. (Она может быть и зависимой
от основного кода, но тогда это плохая подпрограмма)
Рассмотрим пример:
//подпрограмма
void PrintHello()
{
printf("Hello ");
}
//основной код
int main(void) {
PrintHello();
return 0;
}
В основном коде мы вызываем другой код, и можем делать это сколько угодно раз. Таким
образом, если подпрограмма достаточно большого размера, то мы еще и пишем меньше, чем
экономим время себе, и тому, кто будет читать наш код.
Когда-то давно в Индии существовала практика (а может до сих пор существует): платили за
количество строк в программе. Разработчики правильно считали, что могут получать больше,
если будут растягивать свою программу на тысячи строк. На жаргоне это называется
индусским кодом. Но даже для непрофессионалов писать в таком стиле — это не найти
работу или быть целью насмешек со стороны коллег. Поэтому важно научиться выносить
участки своего кода в отдельные (лучше всего независимые) модули, для их повторного
использования в дальнейшем. И тем более, не засорять свою программу бесполезным кодом.
В Си нет такого понятия как подпрограмма, нет даже понятия процедура, хотя с некоторой
правотой мы могли бы назвать PrintHello процедурой. В Си есть только понятие функция,
т. е. это любой отдельный модуль, не входящий в состав main.
Функции могут возвращать значение или не возвращать. PrintHello не возвращает значения.
А что она может возвращать? (Разве что, удачно ли сработал printf). Поэтому тип функции
void (пустота).
Если функция возвращает значение, то ее тип отличен от void. Например функция main
возвращает тип int. Если функция возвращает значение, то в конце обязательно должен быть
return, вы можете его видеть в конце main.
Давайте напишем функцию, возвращающую значение:
//подпрограмма
int Ten()
{
return 10;
}
//основной код
int main(void) {
printf("%i", Ten());
return 0;
}
В результате будет напечатано 10.
Но такая функция кажется бессмысленной, поэтому обычно функции, возвращающие
значения, принимают параметры. Параметры — это переменные, которые указываются в
скобках сигнатуры функции и служат мостом для значений извне. Например мы бы могли
посчитать площадь круга при помощи функции:
//функция
float CircleS(float r)
{
return pi * r * r;
}
//основной код
int main(void) {
printf("%f", CircleS(10));
return 0;
}
Количество параметров может быть любым, главное, не забыть их все указать при вызове,
иначе можно получить такую ошибку:
error: too few arguments to function 'CircleS'
printf("%i", CircleS());
Ошибка может принимать другую формулировку, но суть ее остается всегда та же.
Внутри функции вы, конечно, не ограничены одним лишь return, вы можете писать там сколь
угодно строк, однако, вы также можете разбивать ваши подпрограммы на другие
подпрограммы. И так до бесконечности. Рекомендуемая длина функции не должна
превышать одного экрана монитора, если она перестает влезать на экран, то, возможно, стоит
подумать, о том, как ее разбить на другие функции.
char[] или char*
Как уже было сказано, Си может интерпретировать одно, как другое, но между этими типами
есть существенные отличия:
1. Во-первых, правильно объявленная строка это char *str;
2. Правильно инициализированная строка char *str = “John”; Хотя в С++ правильно будет
const char *str = “John”;
3. char *Name = "John";
char Name2[] = "John";
Name[0] = 'p'; //сломает вашу программу
Name2[0] = 'p'; //легально
4. char *Name = "JohnDen";
char Name2[] = "JohnDen";
printf("%d\n", sizeof(Name)); //4 (размер указателя)
printf("%d\n", sizeof(Name2)); //8 (размер массива)
5. Для параметров функций char[] и char* почти одно и тоже, но в других местах
правильная интерпретация char* в char[] может отсутствовать.
sizeof() - оператор (заметьте не функция!), который возвращает размер операнда (то что в
скобках).
Кстати, обратите внимание на то, что следующие строки эквивалентны:
char *str;
char* str;
char*str;
char * str;
Т.е. Можно ставить пробел перед звездочкой, можно после, можно там и там или вообще без
пробелов, но постарайтесь привыкнуть к варианту char *str;
Этот вариант удобнее, почему вы поймете, если взглянете на следующий пример:
char* str, str2;
Объявлены 2 переменные, но только лишь первая является строковым типом, вторая просто
символ. Для новичка это не так очевидно, но если вы привыкните ставить звездочку перед
переменной, а не после типа, то этой ошибки вы избежите:
char *str, *str2;
Константы
Представьте ситуацию, что у вас есть переменная, значение которой вам бы не хотелось
менять, более того, если оно поменяется, то логика вашей программы может нарушиться.
Отличный пример со значением pi. Вы можете задать его раз глобально и больше к этому не
возвращаться. Например:
float pi = 3.14;
//функция
float CircleS(float r)
{
return pi * r * r;
}
//основной код
int main(void) {
printf("%f", CircleS(10));
return 0;
}
Конечно, функция CircleS теперь зависит от pi, и это плохо. Но не будет слишком разумным
писать определение pi внутри самой функции, так как мы можем написать еще одну
функцию расчета периметра и для этого придется определять pi еще раз, а вот это еще хуже.
В структурном программировании мы вынуждены пользоваться глобальными переменными
для таких случаев, в языке С++ у нас появятся другие способы, более правильные.
Но теперь подумайте вот о чем, а что если вы случайно измените значение pi в программе?
Очевидно, что площадь или периметр, она больше правильно не рассчитает. Чтобы уберечь
себя от таких случайностей были придуманы константы:
const float pi = 3.14;
Теперь компилятор сам не даст поменять значение такой переменной. И запомните, что в
лабораторных разрешено применять глобальную область видимости только для констант.
По той же логике константы работают и в качестве параметров функций. В предыдущем
примере:
char * strcat ( char * destination, const char * source );
Если бы была попытка изменить source в функции strcat мы бы получили ошибку. Т.е.
передавая строку вторым параметром в функцию strcat, нам гарантирует компилятор, что эта
строка не изменится.
Вы должны пользоваться той же логикой, когда будете разрабатывать свои функции.
Функции работы со строками
А теперь я приведу вам сигнатуры функций из string.h Они понадобятся для выполнения
следующих лабораторных.
Не забудьте подключить этот модуль
#include
char * strcpy ( char * destination, const char * source );
копирует source в destination
3
4
5
6
7
8
9
10
11
12
13
14
/* strcpy example */
#include
#include
int main ()
{
char str1[]="Sample string";
char str2[40];
char str3[40];
strcpy (str2,str1);
strcpy (str3,"copy successful");
printf ("str1: %s\nstr2: %s\nstr3: %s\n",str1,str2,str3);
return 0;
}
Вывод:
str1: Sample string
str2: Sample string
str3: copy successful
char * strcat ( char * destination, const char * source );
Соединяет destination и source, результат записывается в destination
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* strcat example */
#include
#include
int main ()
{
char str[80];
strcpy (str,"these ");
strcat (str,"strings ");
strcat (str,"are ");
strcat (str,"concatenated.");
puts (str);
return 0;
}
Вывод:
these strings are concatenated.
int strcmp ( const char * str1, const char * str2 );
Возвращает значение < 0, если первый неодинаковый символ str1[i] < str2[i]
= 0, если str1 == str2
> 0, если первый неодинаковый символ str1[i] > str2[i]
1
2
3
4
5
6
7
8
9
10
#include
#include
int main ()
{
char key[] = "apple";
char buffer[80];
do {
printf ("Guess my favorite fruit? ");
11
scanf ("%79s",buffer);
12
} while (strcmp (key,buffer) != 0);
13
puts ("Correct answer!");
14
return 0;
15 }
Вывод:
Guess my favourite fruit? orange
Guess my favourite fruit? apple
Correct answer!
size_t strlen ( const char * str );
Возвращает длину строки (считает до символа '\0')
1
2
3
4
5
6
7
8
9
10
11
12
/* strlen example */
#include
#include
int main ()
{
char szInput[256];
printf ("Enter a sentence: ");
gets (szInput);
printf ("The sentence entered is %u characters long.\n",(unsigned)strlen(szInput));
return 0;
}
Вывод:
Enter sentence: just testing
The sentence entered is 12 characters long.
char * strtok (char * str, const char * delimiters);
Разбивает строку на токены по разделителям.
/* strtok example */
#include
#include
int main ()
{
char str[] ="- This, a sample string.";
char * pch;
printf ("Splitting string \"%s\" into tokens:\n",str);
pch = strtok (str," ,.-");
while (pch != NULL)
{
printf ("%s\n",pch);
pch = strtok (NULL, " ,.-");
}
return 0;
}
Вывод:
Splitting string "- This, a sample string." into tokens:
This
a
sample
string
const char * strstr (const char * str1, const char * str2);
char * strstr (char * str1, const char * str2);
Находит подстроку в строке:
1
2
3
4
5
6
7
8
9
10
11
12
13
/* strstr example */
#include
#include
int main ()
{
char str[] ="This is a simple string";
char * pch;
pch = strstr (str,"simple");
strncpy (pch,"sample",6);
puts (str);
return 0;
}
Вывод:
This is a sample string
Внимательно изучите эти примеры!
Новые функции о которых мы узнали из этих примеров:
int puts ( const char * str );
Выводит строку на экран. Возвращает неотрицательное число в случае успеха.
char * gets ( char * str );
Считывает строку с консоли. Возвращает эту строку в случае успеха.
Преобразование типов
Еще одна интересная возможность типов языка Си - легко и просто преобразовываться друг в
друга. Си далеко не типобезопасный язык. Что это значит? То, что в Си int может стать double
или float стать int без вашего согласия, и в лучшем случае вы увидите лишь предупреждение
компилятора. Например:
float CircleS(float r)
{
return pi * r * r;
}
int main(void) {
int s = CircleS(11);
printf("%d\n%f", s, CircleS(11));
return 0;
}
Вывод:
379
379.940002
Благодаря такой случайности, вы потеряете почти единицу. Как проверить себя?
Переходим на компилирование с консоли. Помните о gcc 1.c -o 1.exe ?
Пора добавить строгости компилятору:
gcc 1.c -pedantic -Wall -Wconversion -o 1.exe
вообще в нашем случае достаточно -Wconversion, но остальные ключи также помогут лучше
прочувствовать язык Си.
Компилировать лабораторные работы теперь нужно при помощи этих ключей, если у вас нет
на компьютере mingw, то можно воспользоваться сайтом https://gcc.godbolt.org/ для проверки
кода.
Эта проблема называется неявным преобразованием типов. Так повелось, что в Си она уже
давно, и никто с ней ничего уже не сделает. Профессионалам работать она не мешает, а вот
новичкам приходится туго. Поэтому рекомендуется читать и исправлять все ворнинги. Если
вы абсолютно уверены, что int s = CircleS(11); должно потерять дробную часть, то вы можете
использовать явное преобразование. Ворнинг пропадет, если написать:
int s = (int)CircleS(11);
Но не все преобразования типов могут выполняться автоматически или явно, например
строку в число просто так не перевести. Для этого пользуются специальными функциями:
#include //необходимая библиотека
int atoi (const char * str);
Преобразует строку в число.
По аналогии работают следующие функции:
double atof (const char* str);
long int atol ( const char * str );
long long int atoll ( const char * str );
Для обратного преобразования используется sprintf
int sprintf ( char * str, const char * format, ... );
Работает анологично printf, только не выводит результат на экран, а сохраняет в первом
параметре.
Лабораторная работа №5
Задание 1.
Помните, что теперь нужно компилировать вашу программу так: gcc <имя файла>.c
-pedantic -Wall -Wconversion -o <имя исполняемого файла>.exe
Если у вас на компьютере нет mingw (именно нет), то используйте другой компилятор языка
C/C++, установленный на вашем компьютере, а код проверяйте на сайте
https://gcc.godbolt.org/)
Нужно написать и продемонстрировать работу следующих функций:
GetInt – читает с консоли число типа int и возвращает его в случае успеха, или предлагает
ввести число заново.
int number = GetInt();
GetFloat - читает с консоли число типа float и возвращает его в случае успеха, или предлагает
ввести число заново
float number = GetFloat();
Reverse – переворачивает строку
char* restr = Reverse(“12345”); //”54321”
Распечатайте слова из текста, длина которых больше числа символов, введенного с
клавиатуры. Или если в слове есть вхождение, введенное с клавиатуры.
Задание 2.
StrFind – два параметра. Первый: строка, в которой осуществляется поиск, второй: строка,
которая ищется в исходной. Возвращаемое значение — позиция первого вхождения в
исходной строке.
int pos = StrFind(“John plays tennis”, “plays”); //pos = 5
StrReplace — три параметра. Первый: строка, в которой осуществляется поиск. Второй:
строка, которая заменяется в исходной. Третий: на что строка заменяется. Возвращаемое
значение измененная строка.
char str[256] ="This is a simple string";
char * newstr = StrReplace(str, "simple", "sample12"); // "This is a simple12 string"