конспект. Конспект. Курс лекций по дисциплине процедурное программирование
Скачать 1.95 Mb.
|
Тема 5. Массивы в C++ Сегодня мы с поговорим о массивах. Вы уже знаете, что переменная — это ячейка в памяти компьютера, где может храниться одно единственное значение. Массив — это область памяти, где могут последовательно храниться несколько значений. Возьмем группу студентов из десяти человек. У каждого из них есть фамилия. Создавать отдельную переменную для каждого студента — не рационально. Создадим массив, в котором будут храниться фамилии всех студентов. Пример инициализации массива string students[10] = { "Иванов", "Петров", "Сидоров", "Ахмедов", "Ерошкин", "Выхин", "Андеев", "Вин Дизель", "Картошкин", "Чубайс" }; Описание синтаксиса Массив создается почти так же, как и обычная переменная. Для хранения десяти фамилий нам нужен массив, состоящий из 10 элементов. Количество элементов массива задается при его объявлении и заключается в квадратные скобки. Чтобы описать элементы массива сразу при его создании, можно использовать фигурные скобки. В фигурных скобках значения элементов массива перечисляются через запятую. В конце закрывающей фигурной скобки ставится точка с запятой. Попробуем вывести наш массив на экран с помощью оператора cout #include #include int main() { std::string students[10] = { "Иванов", "Петров", "Сидоров", "Ахмедов", "Ерошкин", "Выхин", "Андеев", "Вин Дизель", "Картошкин", "Чубайс" }; std::cout << students << std::endl; // Пытаемся вывести весь массив непосредственно return 0; } Скомпилируйте этот код и посмотрите, на результат работы программы. Готово? А теперь запустите программу еще раз и сравните с предыдущим результатом. В моей операционной системе вывод был следующим: Первый вывод: 0x7ffff8b85820 Второй вывод: 0x7fff7a335f90 Третий вывод: 0x7ffff847eb40 Мы видим, что выводится адрес этого массива в оперативной памяти, а никакие не «Иванов» и «Петров». Дело в том, что при создании переменной, ей выделяется определенное место в памяти. Если мы объявляем переменную типа int , то на машинном уровне она описывается двумя параметрами — ее адресом и размером хранимых данных. Массивы в памяти хранятся таким же образом. Массив типа int из 10 элементов описывается с помощью адреса его первого элемента и количества байт, которое может вместить этот массив. Если для хранения одного целого числа выделяется 4 байта, то для массива из десяти целых чисел будет выделено 40 байт. Так почему же, при повторном запуске программы, адреса различаются? Это сделано для защиты от атак переполнения буфера. Такая технология называется рандомизацией адресного пространства и реализована в большинстве популярных ОС. Попробуем вывести первый элемент массива — фамилию студента Иванова. #include #include int main() { std::string students[10] = { "Иванов", "Петров", "Сидоров", "Ахмедов", "Ерошкин", "Выхин", "Андеев", "Вин Дизель", "Картошкин", "Чубайс" }; std::cout << students[0] << std::endl; return 0; } Смотрим, компилируем, запускаем. Убедились, что вывелся именно «Иванов». Заметьте, что нумерация элементов массива в C++ начинается с нуля. Следовательно, фамилия первого студента находится в students[0] , а фамилия последнего — в students[9] В большинстве языков программирования нумерация элементов массива также начинается с нуля. Попробуем вывести список всех студентов. Но сначала подумаем, а что если бы вместо группы из десяти студентов, была бы кафедра их ста, факультет из тысячи, или даже весь университет? Ну не будем же мы писать десятки тысяч строк с cout ? Конечно же нет! Мы будем использовать циклы. Вывод элементов массива через цикл #include #include int main() { std::string students[10] = { "Иванов", "Петров", "Сидоров", "Ахмедов", "Ерошкин", "Выхин", "Андеев", "Вин Дизель", "Картошкин", "Чубайс" }; for (int i = 0; i < 10; i++) { std::cout << students[i] << std::endl; } return 0; } Если бы нам пришлось выводить массив из нескольких тысяч фамилий, то мы бы просто увеличили конечное значение счетчика цикла — строку for (...; i < 10; ...) заменили на for (...; i < 10000; ...) Заметьте что счетчик нашего цикла начинается с нуля, а заканчивается девяткой. Если вместо оператора строгого неравенства — i < 10 использовать оператор «меньше, либо равно» — i <= 10 , то на последней итерации программа обратится к несуществующему элементу массива — students[10] . Это может привести к ошибкам сегментации и аварийному завершению программы. Будьте внимательны — подобные ошибки бывает сложно отловить. Массив, как и любую переменную можно не заполнять значениями при объявлении. Объявление массива без инициализации string students[10]; // или string teachers[5]; Элементы такого массива обычно содержат в себе «мусор» из выделенной, но еще не инициализированной, памяти. Некоторые компиляторы, такие как GCC, заполняют все элементы массива нулями при его создании. При создании статического массива, для указания его размера может использоваться только константа. Размер выделяемой памяти определяется на этапе компиляции и не может изменяться в процессе выполнения. int n; cin >> n; string students[n]; /* Неверно */ Выделение памяти в процессе выполнения возможно при работе с динамическими массивами. Но о них немного позже. Заполним с клавиатуры пустой массив из 10 элементов. Заполнение массива с клавиатуры #include #include using std::cout; using std::cin; using std::endl; int main() { int arr[10]; // Заполняем массив с клавиатуры for (int i = 0; i < 10; i++) { cout << "[" << i + 1 << "]" << ": "; cin >> arr[i]; } // И выводим заполненный массив. cout << "\nВаш массив: "; for (int i = 0; i < 10; ++i) { cout << arr[i] << " "; } cout << endl; return 0; } Скомпилируем эту программу и проверим ее работу. Если у вас возникают проблемы при компиляции исходников из уроков — внимательно прочитайте ошибку компилятора, попробуйте проанализировать и исправить ее. Массивы — очень важная вещь в программировании. СоветуюТема 6. вам хорошо попрактиковаться в работе с ними. Тема 6. Функции в C Очень часто в программировании необходимо выполнять одни и те же действия. Например, мы хотим выводить пользователю сообщения об ошибке в разных местах программы, если он ввел неверное значение. без функций это выглядело бы так: #include #include using namespace std; int main() { string valid_pass = "qwerty123"; string user_pass; cout << "Введите пароль: "; getline(cin, user_pass); if (user_pass == valid_pass) { cout << "Доступ разрешен." << endl; } else { cout << "Неверный пароль!" << endl; } return 0; } А вот аналогичный пример с функцией: #include #include using namespace std; void check_pass (string password) { string valid_pass = "qwerty123"; if (password == valid_pass) { cout << "Доступ разрешен." << endl; } else { cout << "Неверный пароль!" << endl; } } int main() { string user_pass; cout << "Введите пароль: "; getline (cin, user_pass); check_pass (user_pass); return 0; } По сути, после компиляции не будет никакой разницы для процессора, как для первого кода, так и для второго. Но ведь такую проверку пароля мы можем делать в нашей программе довольно много раз. И тогда получается копи-паст и код становится нечитаемым. Функции — один из самых важных компонентов языка C++. Любая функция имеет тип, также, как и любая переменная. Функция может возвращать значение, тип которого в большинстве случаев аналогично типу самой функции. Если функция не возвращает никакого значения, то она должна иметь тип void (такие функции иногда называют процедурами) При объявлении функции, после ее типа должно находиться имя функции и две круглые скобки — открывающая и закрывающая, внутри которых могут находиться один или несколько аргументов функции, которых также может не быть вообще. после списка аргументов функции ставится открывающая фигурная скобка, после которой находится само тело функции. В конце тела функции обязательно ставится закрывающая фигурная скобка. Пример построения функции #include using namespace std; void function_name () { cout << "Hello, world" << endl; } int main() { function_name(); // Вызов функции return 0; } Перед вами тривиальная программа, Hello, world, только реализованная с использованием функций. Если мы хотим вывести «Hello, world» где-то еще, нам просто нужно вызвать соответствующую функцию. В данном случае это делается так: function_name(); . Вызов функции имеет вид имени функции с последующими круглыми скобками. Эти скобки могут быть пустыми, если функция не имеет аргументов. Если же аргументы в самой функции есть, их необходимо указать в круглых скобках. Также существует такое понятие, как параметры функции по умолчанию. Такие параметры можно не указывать при вызове функции, т.к. они примут значение по умолчанию, указанно после знака присваивания после данного параметра и списке всех параметров функции. В предыдущих примерах мы использовали функции типа void , которые не возвращают никакого значения. Как многие уже догадались, оператор return используется для возвращения вычисляемого функцией значения. Рассмотрим пример функции, возвращающей значение на примере проверки пароля. #include #include using namespace std; string check_pass (string password) { string valid_pass = "qwerty123"; string error_message; if (password == valid_pass) { error_message = "Доступ разрешен."; } else { error_message = "Неверный пароль!"; } return error_message; } int main() { string user_pass; cout << "Введите пароль: "; getline (cin, user_pass); string error_msg = check_pass (user_pass); cout << error_msg << endl; return 0; } В данном случае функция check_pass имеет тип string, следовательно она будет возвращать только значение типа string, иными словами говоря строку. Давайте рассмотрим алгоритм работы этой программы. Самой первой выполняется функция main(), которая должна присутствовать в каждой программе. Теперь мы объявляем переменную user_pass типа string, затем выводим пользователю сообщение «Введите пароль», который после ввода попадает в строку user_pass. А вот дальше начинает работать наша собственная функция check_pass() В качестве аргумента этой функции передается строка, введенная пользователем. Аргумент функции — это, если сказать простым языком переменные или константы вызывающей функции, которые будет использовать вызываемая функция. При объявлении функций создается формальный параметр, имя которого может отличаться от параметра, передаваемого при вызове этой функции. Но типы формальных параметров и передаваемых функии аргументов в большинстве случаев должны быть аналогичны. После того, как произошел вызов функции check_pass() , начинает работать данная функция. Если функцию нигде не вызвать, то этот код будет проигнорирован программой. Итак, мы передали в качестве аргумента строку, которую ввел пользователь. Теперь эта строка в полном распоряжении функции (хочу обратить Ваше внимание на то, что переменные и константы, объявленные в разных функциях независимы друг от друга, они даже могут иметь одинаковые имена. В следующих уроках я расскажу о том, что такое область видимости, локальные и глобальные переменные). Теперь мы проверяем, правильный ли пароль ввел пользователь или нет. если пользователь ввел правильный пароль, присваиваем переменной error_message соответствующее значение. если нет, то сообщение об ошибке. После этой проверки мы возвращаем переменную error_message . На этом работа нашей функции закончена. А теперь, в функции main(), то значение, которое возвратила наша функция мы присваиваем переменной error_msg и выводим это значение (строку) на экран терминала. Также, можно организовать повторный ввод пароля с помощью рекурсии (о ней мы еще поговорим). Если объяснять вкратце, рекурсия — это когда функция вызывает сама себя. Смотрите еще один пример: #include #include using namespace std; bool password_is_valid (string password) { string valid_pass = "qwerty123"; if (valid_pass == password) return true; else return false; } void get_pass () { string user_pass; cout << "Введите пароль: "; getline(cin, user_pass); if (!password_is_valid(user_pass)) { cout << "Неверный пароль!" << endl; get_pass (); // Здесь делаем рекурсию } else { cout << "Доступ разрешен." << endl; } } int main() { get_pass (); return 0; } Функции очень сильно облегчают работу программисту и намного повышают читаемость и понятность кода, в том числе и для самого разработчика (не удивляйтесь этому, т. к. если вы откроете код, написанный вами полгода назад,не сразу поймете соль, поверьте на слово). Не расстраивайтесь, если не сразу поймете все аспекты функций в C++, т. к. это довольно сложная тема и мы еще будем разбирать примеры с функциями в следующих уроках. Совет: не бойтесь экспериментировать, это очень хорошая практика, а после прочтения данной статьи порешайте элементарные задачи, но с использованием функций. Это будет очень полезно для вас. Если Вы найдете какие-либо ошибки в моем коде, обязательно напишите об этом в комментариях. здесь же можно задавать все вопросы. Тема 7. Указатели в C++ При выполнении любой программы все необходимые для ее работы данные должны быть загружены в оперативную память компьютера. Для обращения к переменным, находящимся в памяти, используются специальные адреса, которые записываются в шестнадцатеричном виде. ПАМЯТЬ. АДРЕСА. УКАЗАТЕЛИ Практически в любой программе используются переменные, которые располагаются в оперативной памяти. Упрощенно память можно представить в виде массива байтов, каждый из которых имеет адрес, начиная с нуля. В зависимости от типа, переменные занимают различный объем памяти. Организация памяти. Хранение переменных в памяти Условно можно разделить всю память на два вида: статическую и динамическую. С физической точки зрения разницы между этими типами памяти нет. Поэтому, когда будем говорить о видах памяти, будем иметь в виду способы организации работы с ней. Статическая память выделяется до начала работы программы, а статические переменные создаются и инициализируются до входа в функцию main. Например, при объявлении переменной int а[10]; автоматически выделяется 10 ячеек памяти, каждая из которых предназначена для хранения целого значения. Динамическую память часто называют «кучей». Программа может захватывать участки динамической памяти требуемого размера. Необходимо помнить, что после использования захваченную память следует освободить. Указатели. Объявление. Инициализация Если переменных в памяти потребуется слишком большое количество, которое не сможет вместить в себя сама аппаратная часть, произойдет перегрузка системы или её зависание. Если мы объявляем переменные статично, так как мы делали в предыдущих уроках, они остаются в памяти до того момента, как программа завершит свою работу, а после чего уничтожаются. Такой подход может быть приемлем в простых примерах и несложных программах, которые не требуют большого количества ресурсов. Если же наш проект является огромным программным комплексом с высоким функционалом, объявлять таким образом переменные, естественно, было бы довольно глупо. Можете себе представить, если бы какая-нибудь RPG использовала такой метод работы с данными? В таком случае, самым заядлым геймерам пришлось бы перезагружать свои высоконагруженные системы кнопкой reset после нескольких секунд работы игры. Дело в том, что, играя в RPG, геймер в каждый новый момент времени видит различные объекты на экране монитора, например, сейчас я стреляю во врага, а через долю секунды он уже падает убитым, создавая вокруг себя множество спецэффектов, таких как пыль, тени, и т.п. Естественно, все это занимает какое-то место в оперативной памяти компьютера. Если не уничтожать неиспользуемые объекты, очень скоро они заполнят весь объем ресурсов ПК. По этим причинам, в большинстве языков, в том числе и C/C++, имеется понятие указателя. Указатель — это переменная, хранящая в себе адрес ячейки оперативной памяти. Мы можем обращаться, например, к массиву данных через указатель, который будет содержать адрес начала диапазона ячеек памяти, хранящих этот массив. После того, как этот массив станет не нужен для выполнения остальной части программы, мы просто освободим память по адресу этого указателя, и она вновь станет доступна для других переменных. Указатели. Когда компилятор обрабатывает оператор определения статической переменной, например, int j=10; он выделяет память в соответствии с типом (int) и инициализирует ее указанным значением (10). Все обращения в программе к переменной по ее имени (j) заменяются компилятором на ее адрес области памяти, в которой хранится значение переменной. Адресом участка оперативной памяти является номер байта, с которого начинается этот участок. Программист может определить собственные переменные для хранения адресов областей памяти. Такие переменные называются указателями. Указатель — это переменная, которая содержит адрес другой переменной или функции. Описание указателя: тип_ данных *имя_указателя; Звездочка относится непосредственно к имени_указателя, поэтому, чтобы объявить несколько указателей, требуется ставить ее перед каждым именем. Примеры объявления указателей. int *pi, а, b, *рк; /*объявляются две целые переменные а и b и два указателя pi и рк на неименованные области памяти типа int */ double *ра, b; /*объявление переменной b типа double и указателя ра на тип double*/ char *ptext; //объявление указателя ptext на переменную типа char Инициализация указателей. При объявлении указателя желательно выполнить его инициализацию. Если значение указателя заранее неизвестно, то можно его инициализировать нулевым адресом (NULL). Рассмотрим некоторые способы инициализации указателей. 1. Инициализация указателя адресом существующего объекта: • с помощью операции получения адреса: int а=5; //объявление целой переменной int *р=&а; //в указатель записывается адрес а • с помощью значения другого инициализированного указателя: int а=5; int *р=&а; int *г=р; 2. Инициализация пустым значением: float *r=NULL; или float *r=0; Далее приведен конкретный пример обращения к переменным через указатель и напрямую. Пример использования статических переменных #include using namespace std; int main() { int a; // Объявление статической переменной int b = 5; // Инициализация статической переменной b a = 10; b = a + b; cout << "b = " << b << endl; return 0; } Пример использования динамических переменных #include using namespace std; int main() { int *a = new int; // Объявление указателя для переменной типа int int *b = new int(5); // Инициализация указателя *a = 10; *b = *a + *b; cout << "b = " << *b << endl; delete b; delete a; return 0; } Синтаксис первого примера вам уже должен быть знаком. Мы объявляем/инициализируем статичные переменные a и b , после чего выполняем различные операции напрямую с ними. Во втором примере мы оперируем динамическими переменными посредством указателей. Рассмотрим общий синтаксис указателей в C++. Выделение памяти осуществляется с помощью оператора new и имеет вид: тип данных *имя_указателя = new тип_данных; Например, int *a = new int; После удачного выполнения такой операции в оперативной памяти компьютера происходит выделение диапазона ячеек, необходимого для хранения переменной типа int Логично предположить, что для разных типов данных выделяется разное количество памяти. Следует быть особенно осторожным при работе с памятью, потому что именно ошибки программы, вызванные утечкой памяти, являются одними из самых трудно находимых. На отладку программы в поисках одной ничтожной ошибки, может уйти час, день, неделя, в зависимости от упорности разработчика и объема кода. Инициализация значения, находящегося по адресу указателя, выполняется схожим образом, только в конце ставятся круглые скобки с нужным значением: тип данных *имя_указателя = new тип_данных(значение) В нашем примере это int *b = new int(5) Для того, чтобы получить адрес в памяти, на который ссылается указатель, используется имя переменной-указателя с префиксом & перед ним (не путать со знаком ссылки в C++). Например, чтобы вывести на экран адрес ячейки памяти, на который ссылается указатель b во втором примере, мы пишем cout << "Address of b = " << &b << endl; В моей системе получено значение 0x1aba030 . У вас оно может быть другим, потому что адреса в оперативной памяти распределяются таким образом, чтобы максимально уменьшить фрагментацию. Поскольку, в любой системе список запущенных процессов, а также объем и разрядность памяти могут отличаться, система сама распределяет данные для обеспечения минимальной фрагментации. Для того, чтобы получить значение, которое находится по адресу, на который ссылается указатель, используется префикс * . Данная операция называется разыменованием указателя. Во втором примере мы выводим на экран значение, которое находится в ячейке памяти (у нас это 0x1aba030 ): cout << "b is " << *b << endl; В этом случае необходимо использовать знак * Чтобы изменить значение, находящееся по адресу, на который ссылается указатель, нужно также использовать звездочку, например, как во втором примере — *b = *a + *b; Когда мы оперируем данными, то используем знак * Когда мы оперируем адресами, то используем знак & Для того, чтобы освободить память, выделенную оператором new, используется оператор delete. Пример освобождения памяти #include using namespace std; int main() { // Выделение памяти int *a = new int; int *b = new int; float *c = new float; // ... Любые действия программы // Освобождение выделенной памяти delete c; delete b; delete a; return 0; } При использовании оператора delete для указателя, знак * не используется. Подведем итог. int x; int *y = &x; // От любой переменной можно взять адрес при помощи операции взятия адреса "&". Эта операция возвращает указатель, то есть является операцией взятия адреса int z = *y; // Указатель можно разыменовать при помощи операции разыменовывания "*". Это операция возвращает тот объект, на который указывает указатель Неплохо бы иметь в виду следующее. char — это всегда ровно один байт и во всех стандартах C и C++ sizeof (char) == 1 (но при этом стандарты не гарантируют, что в байте содержится именно 8 бит, как это ни смешно! Далее, если прибавить к указателю на какой-нибудь тип T число, то реальное численное значение этого указателя увеличится на это число, умноженное на sizeof (T). То есть если p имеет тип T *TYPE, то p + 3 эквивалентно (T *)((char *)p + 3 * sizeof (T)). Аналогичные соображения относятся и к вычитанию. Ссылки. Ссылки — это то же самое, что и указатели, но с другим синтаксисом и некоторыми другими важными отличиями. Следующий код ничем не отличается от предыдущего, за исключением того, что в нём фигурируют ссылки вместо указателей: int x; int &y = x; int z = y; Если слева от знака присваивания стоит ссылка, то нет никакого способа понять, хотим мы присвоить то, что стоит после знака равенства, самой ссылке или объекту, на который она ссылается. Поэтому такое присваивание всегда присваивает объекту, а не ссылке. Но это не относится к инициализации ссылки: инициализируется, разумеется, сама ссылка. Поэтому после инициализации ссылки нет никакого способа изменить её саму, то есть ссылка всегда постоянна (но не её объект). |