
- •Часть 1
- •Общие сведения Сведения об эумк
- •Методические рекомендации по изучению дисциплины
- •Рабочая учебная программа
- •Учреждение образования
- •«Белорусский государственный университет
- •Информатики и радиоэлектроники»
- •Часть 2 __184__
- •Содержание дисциплины
- •1. Индивидуальные практические занятия, их характеристика
- •2. Контрольные работы, их характеристика
- •3. Курсовой проект, его характеристика
- •4. Литература
- •4.1. Основная
- •4.2. Дополнительная
- •5. Перечень компьютерных программ, наглядных и других пособий, методических указаний и материалов и технических средств обучения
- •Протокол согласования учЕбной программы по изучаемой учебной дисциплине с другими дисциплинами специальности
- •Теоретический раздел Введение
- •1. Основные типы данных
- •1.1. Общие сведения
- •1.2. Данные типа int
- •1.3. Данные типа char
- •1.4. Модификаторы доступа const и volatile
- •1.5. Данные вещественного типа (с плавающей точкой)
- •1.6. Элементарный ввод-вывод
- •1.7. Структура простой программы на языке Си
- •2. Операции и выражения
- •2.1. Выражение и его интерпретация
- •2.2. Основные операции
- •2.2.1. Арифметические операции
- •2.2.2. Побитовые логические операции
- •2.2.3. Операции сдвига
- •2.2.4. Операция присваивания
- •2.2.5. Операция sizeof
- •2.2.6. Преобразование типов в выражениях
- •2.2.7. Операция преобразования типов
- •2.2.8. Приоритеты в языке Си
- •3. Операторы управления вычислительным процессом
- •3.1. Оператор if
- •3.2. Операции отношения
- •3.3. Логические операции
- •3.4. Операция запятая
- •3.5. Операция условия ?:
- •3.6. Оператор безусловного перехода goto
- •3.7. Оператор switch
- •`` ` `3.8. Операторы цикла
- •3.8.1. Оператор for
- •3.8.2. Оператор while
- •3.8.3. Оператор do...While
- •3.9. Оператор break
- •3.10. Оператор continue
- •4. Массивы и указатели
- •4.1. Одномерные массивы и их инициализация
- •4.2. Многомерные массивы и их инициализация
- •4.3. Объявление указателей
- •4.4. Операции над указателями
- •1) Взятие адреса
- •2) Косвенная адресация или разыменование указателя
- •3) Увеличение или уменьшение значения указателя на целое число
- •4) Разность указателей
- •5) Сравнение указателей
- •6) Присваивание указателей друг другу
- •4.6. Связь между указателями и массивами
- •4.7. Динамическое распределение памяти
- •4.8. Массивы указателей
- •5. Функции
- •5.1. Общие сведения
- •5.2. Область видимости переменных
- •5.2.1. Локальные переменные
- •5.2.2. Глобальные переменные
- •5.3. Передача параметров в функцию
- •5.4. Рекурсивные функции
- •5.5. Использование функций в качестве параметров функций
- •5.6. Указатели на функции
- •5.7. Структура программы на Си
- •5.8. Передача параметров в функцию main()
- •6. Строки
- •7. Классы хранения и видимость переменных
- •7.1. Общие сведения
- •7.2. Автоматический класс хранения (auto)
- •7.3. Регистровый класс хранения (register)
- •7.4. Статический класс хранения (static)
- •7.5. Внешний класс хранения (extern)
- •7.6. Заключение
- •8. Структуры, объединения и перечисления
- •8.1. Общие сведения
- •8.2. Инициализация структурных переменных
- •8.3. Вложенные структуры
- •8.4. Указатели на структуры
- •8.5. Массивы структурных переменных
- •8.6. Передача функциям структурных переменных
- •8.7. Оператор typedef
- •8.8. Поля битов в структурах
- •8.9. Объединения
- •8.10. Перечисления
- •9. Динамические структуры данных
- •9.1. Общие сведения
- •9.2. Связные списки
- •9.2.1. Односвязные списки
- •9.2.2. Двусвязные списки
- •9.2.3. Циклические списки
- •9.3. Стеки
- •9.4. Очереди
- •9.5. Деревья
- •9.5.1. Понятие графа
- •9.5.2. Бинарные деревья
- •10. Файлы
- •10.1. Общие сведения
- •10.2. Открытие и закрытие файлов
- •10.3. Функции ввода-вывода для работы с текстовыми файлами
- •10.4. Произвольный доступ к файлу
- •10.5. Функции ввода-вывода для работы с бинарными файлами
- •11. Директивы препроцессора
- •11.1. Основные понятия
- •11.2. Директива #include
- •11.3. Директивы препроцессора #define и #undef
- •11.3.1. Символические константы
- •11.3.2. Макросы с параметрами
- •11.3.3. Директива #undef
- •11.4. Условная компиляция
- •11.5. Директивы # и ##
- •12. Модульное программирование
- •13. Введение в объектно-ориентированное программирование
- •13.1. Постановка задачи
- •13.2. Решение задачи средствами Си
- •13.5. Наследование
- •13.6. Перегрузка
- •13.7. Ссылочный тип
- •Литература
- •Приложение 1. Рекомендации по оформлению текстов программ
- •Тесты к теоретическому разделу Вопросы к разделу 1. Основные типы данных
- •Вопросы к разделу 2. Операции и выражения
- •Вопросы к разделу 3. Операторы управления вычислительным процессом
- •Вопросы к разделу 4. Массивы и указатели
- •Вопросы к разделу 5. Функции
- •Вопросы к разделу 6. Строки
- •Вопросы к разделу 7. Классы хранения и видимость переменных
- •Вопросы к разделу 8. Структуры, объединения и перечисления
- •Вопросы к разделу 9. Динамические структуры данных
- •Вопросы к разделу 10. Файлы
- •Вопросы к разделу 11. Директивы препроцессора
- •Вопросы к разделу 12. Модульное программирование
- •Вопросы к разделу 13. Введение в ооп
- •Правильные ответы на вопросы тестов к теоретическому разделу
- •Вопросы к теоретическому зачету
- •Варианты индивидуальных заданий
- •Контрольная работа №2
- •Варианты индивидуальных заданий
- •Индивидуальные практические работы Указания к выбору варианта индивидуальных практических работ
- •Индивидуальная практическая работа № 1. Массивы и строки
- •Варианты индивидуальных заданий
- •Индивидуальная практическая работа № 2. Динамические структуры данных
- •Варианты индивидуальных заданий
6. Строки
В практике программирования очень часто возникает задача хранения и использования текстовых строк. При помощи таблиц кодировки группе чисел (байт) без труда можно поставить в соответствие группу символов (букв). Но сами по себе строки довольно сложны для представления в компьютерной технике ввиду того, что их длина неизбежно должна быть переменной. Существует два простых способа хранения строк в памяти, основанных на использовании статических структур: строки с упреждающей длиной и с завершающим терминальным элементом. Т.к. размер области в памяти отведенной для хранения строки должен быть постоянным, необходимо иметь способ определять её реальную длину.
В строках с упреждающей длиной в памяти перед областью, байты которой интерпретируются как символы строки, находится число, определяющее реальную длину строки. Именно такой метод используется в языке Pascal для хранения переменных типа string. Там для строковой переменной по умолчанию отводилось 256 байт в памяти. Первый байт хранил реальную длину строки, а остальные – её символы. Именно с тем, что для хранения реальной длины использовался один байт и связано ограничение на максимальную длину строки в Pascal’е.
В языке Си вообще нет встроенного строкового типа. Это, равно как и отсутствие булевых переменных, вероятнее всего, объясняется историческими причинами. Однако среди встроенных имеются функции, делающие простой и удобной работу со строками с завершающим терминальным элементом. В строках этого типа об окончании строки свидетельствует специальная (в общем случае) последовательность символов, котороя называется терминальной. Так, в Си терминальным служит символ с кодом 0 (байт 0x00).
В виду того, что специального строкового типа нет, а Си-строка является массивом элементов типа char, логично при хранении строк использовать тип char* (или char [], что, в сущности, то же самое).
Строковый литерал (в двойных кавычках), встретившийся в тексте программы, помещается компилятором в сегмент данных, в виде массива символов с завершающим нулем, имеет тип char* и равен адресу в сегменте данных, по которому расположен массив. Ввиду того, что адрес этот является для компилятора константой, известной во время компиляции, становится возможным такое выражение:
char *str = “Hello world”;
или аналогичное ему
char str[] = “Строка”;
(разница будет заключаться в том, что в первом случае переменную str можно будет изменить (как указатель), а во втором нельзя (имя массива)), однако, если написать так
char *const str = “Hello world”;
то и это отличие исчезает.
В общем случае, существует еще два способа создания строковой переменной: создание массива или динамическое выделение памяти под массив, в котором можно будет хранить строки, длина которых не больше длины массива.
Первый способ:
#include <string.h>
char str[100]; // Любые строки до 99 символов
strcpy(str, “Строка”); // Побайтое копирование строки
Второй способ:
char *str;
str = (char*) malloc(100);
strcpy(str, “Строка”);
free(str);
Использованная стандартная функция strcpy копирует строку по адресу src, в строку по адресу dest, и возвращает указатель dest:
// Прототип функции strcpy
char* strcpy(char* dest, const char* src);
char buf[15] = “BufString”;
char str* = “String”;
strcpy(buf, str);
Предположим, что области памяти, выделенные компилятором для массива buf, и строкового литерала “String” смежные. Длина массива больше длины строки, которой он инициализирован, на 5 байт – это значит, что в них может находиться что угодно. На рисунке показано, как изменится область памяти, содержащая строки, после вызова strcpy.
Приведем пример функции, которая делает в точности то же самое, что и strcpy:
char* user_strcpy(char* dest, const char* src)
{
char *result = dest;
do
{
*dest++ = *src;
}
while (*src++);
return result;
}
Копирование прекращается лишь в том случае, если в копируемой строке встречается нулевой символ. Это значит, что, если принимающая строка (буфер) имеет недостаточную длину для приёма копируемых данных, то произойдет выход за пределы массива, что может привести к непредсказуемым и трудно обнаружимым ошибкам.
Другой необходимой и часто используемой функцией работы со строками, является функция определения их длины:
size_t user_strlen(const char* src)
{
size_t result = 0;
while (*src++) result++;
return result;
}
или аналогичная ей функция из стандартной библиотеки:
size_t strlen(const char* src);
Тип size_t определен в string.h и представляет собой псевдоним целого типа без знака.
Из всего сказанного видно, что использовать операторы присваивания и сложения для копирования и конкатенации строк (как это делалось в языке Pascal) нельзя:
char *str1 = “Студент”;
char *str2 = “ БГУИР”;
char *str3;
str3 = str1 + str2; // НЕ верно!
В данном случае выражение str1 + str2 даже не будет скомпилировано, компилятор выдаст ошибку: «Неверное использование указателей». Действительно, пусть строка «Студент» находится в памяти, например, по адресу 1000, а «БГУИР» 1009. Арифметическая сумма этих чисел даст 2009. Что находится по этому адресу сказать нельзя. Присваивание же str3 адреса строки приведет к тому, что оба указателя будут указывать на одну и ту же область в памяти, и изменение строки str3 повлечет за собой соответствующее изменение str1 и str2, чего пользователь явно не ожидает. Выражение
str3 = “Студент” + “ БГУИР”;
по сути, ничем не отличается от str3 = str1 + str2: строковые литералы размещаются в памяти по адресам, которые вместо них компилятор подставит в выражение, а складывать два адреса нельзя.
С другой стороны, в стандартной библиотеке string.h, есть целый набор функций работы со строками. Рассмотрим некоторые из них:
// Прототип функции конкатенации строк
char* strcat(char* dest, const char* src);
// Пользовательская реализация
char* user_strcat(char* dest, const char* src)
{
strcpy(dest + strlen(dest), src);
return dest;
}
// Прототип функции поиска первого вхождения символа
char* strchr(char* s, int c);
// Пользовательская реализация
char* user_strchr(char* s, int c)
{
do
{
if (*s == c) return s;
}
while (*s++);
return 0;
}
// Прототип функции создания дубликата строки
char* strdup(const char* s);
// Пользовательская реализация
char* user_strdup(const char* s);
{
/* malloc – функция динамического выделения
памяти, рассматривается в главе указатели */
return strcpy((char *) malloc(strlen(s)+1), s);
}
// Использование strdup
char* some_function(const char* str)
{
char* temp = strdup(str);
// ...
free(temp); //Обязательное освобождение памяти!
}
Среди прочих, особо нужно выделить функцию strcmp. Она, в качестве параметров, получает два указателя на строки, а возвращает 0 (ноль), если строки (лексикографически) равны, отрицательное число, если первая строка меньше второй, и положительное число в противном случае:
// Прототип функции сравнения строк
int strcmp(const char* s1, const char* s2);
// Пользовательская реализация
int user_strcmp(const char* s1, const char* s2)
{
int r;
do
{
r = *s1 - *s2;
}
while (*s1++ && *s2++ && !r);
return r;
}
// Использование strcmp
char* some_function(char* str1, char* str2, char* str3)
{
printf("\n%d", strcmp(str1, str2));
printf("\n%d", strcmp(str2, str1));
printf("\n%d", strcmp(str1, str1));
}
Равно как и в случае других библиотек предпочтительно использование стандартных функций работы со строками. По сравнению с приведенными выше аналогами они намного быстрей, т.к. написаны на языке ассемблера и оптимизированы с учетом особенностей платформы применения.