Основы C++
Пространства имен
Пространство имен нужно для организации классов, функций, данных и типов в виде идентифицируемой и именованной части программы без определения нового типа. Допустим, мы хотим создать графическую библиотеку
В дальнейшем, чтобы пользоваться классами библиотеки мы можем использовать следующую конструкцию
Также можем сами создать свой класс Color
и коллизии имен не будет.
Область видимости
Существует несколько разновидностей областей видимости , которые можно использовать для управления используемыми именами:
Глобальная область видимости (global scope): область исходного текста, не входящая ни в одну другую область видимости.
Пространство имен (namespace scope): именованная область видимости. вложенная в глобальную область видимости или в другое пространство имен.
Область видимости класса (class scope): часть исходного текста. находящаяся в классе.
Локальная область видимости (local scope): между фигурными скобками
{ . . . }
блока или в списке аргументов функции.Область видимости инструкции: например. в цикле
for
.
Основное предназначение области видимости - сохранить локальность имен. чтобы они не пересекались с именами. объявленными в другом месте.
Объекты
Объект - это место в памяти, имеющее тип, который определяет нформацию которую можно записать туда и значение - непосредственно информация. Например, строки символов вводятся в переменные типа string, а целые числа - в переменные типа int. Именованный объект называется переменной. Для доступа к объекту нужно знать его имя.
Объект - уаток памяти, в котором хранится значение определенного типа.
Тип определяет набор возможных значений и операций, выполняемых над объектом.
Значение - набор битов в памяти интерпритируемый в соответствии с типом.
Переменная - именованный объект
Объявление - инструкция, приписывающая объекту определеное имя
Определение - объявление, выделяющее память для объекта
Инициализация - объявление и присваивание объекту первичное значение.
Объект можно интерпретировать как "коробку", в которую можно поместить значение, имеющее тип объекта.
Типы
Простые типы
char bool int double unsigned
Составные типы
Структуры(struct)
Структура - способ хранения нескольких типов данных
Можно комбинировать определение формы структуры с созданием структурных переменных.
Можно создать одну структурную переменную, то есть мы не сможем создавать другие переменные этого типа
Можно указывать размер члена структуры(в битах). Тип поля должен быть целочисленным или перечислимым.
Объединения(union)
Объединение - это формат данных, который может хранить в пределах одной области памяти разные типы данных, но в каждый момент времени только один из них.
Переменную one можно использовать для хранения int, long или double, если только делать это не одновременно
Зачем использовать?
экономия памяти
элемент данных может использовать два или более форматов, но никогда - одновременно
Например, предположи м, что вы ведете реестр каких-то предметов, из которых одни имеют целочисленный идентификато р, а другие - строковый.
Анонимное обидинение не имеет имени; в сущности , его члены становятся переменными, расположенными по одному и тому же адресу в памяти . Разумеется , только одна из них может быть текущей в единицу времени:
Перечисления(enum, enum class)
enum
Способ создания символических констант(как const).
объявили имя нового типа spectrum; spectrum называется перечислением
компилятор установит значения перечислителей 0-7(первый 0 и к каждому последующему +1). Если 5 перечислителю присоить значение 0, то 6 значение будет равно 1 и т.д.
Результат не определен, т.к. мы вышли за диапазон. Для нахождения верхнего предела выбирается перечислитель с максимальным значением. Затем ищется наименьшее число, являющееся степенью двойки , которое больше этого максимального значения, и из него вычитается единица.
Например: enum bigstep {first = -6, second = 100, third};
максимальное число - 101, минимальное число представляющее степень двойки, которое больше 101 - 128, . Верхний предел - 127. Для нахождения минимального предела выбирается минимальное значение перечислителя. Если оно раво 0 или больше, то нижним пределом диапазона будет 0. Если число отрицательное, используется такой же подходит, как при вычислении верхнего предела, но со знаком минус. В нашем случае . Нижний предел -7.
Это нужно, чтобы компилятор мог выяснить, сколько места необходимо для хранения перечисления(1 байт или менее для перечисления с небольшим диапазоном - 4 байт для перечислений с типом long).
Если не нужно создавать переменные перечислимого типа, а нужно только использовать константы можно не указывать идентфикатор.
enum classs
enum - очень простой пользовательский тип, который задает множество значений(элементов перечисления) в виде символических констант:
Ключевой слово class означает, что перечисления находятся в области видимости перечисления, т.е. чтобы обратиться к элементу jan, нужно использовать конструкцию Month::jan
. Каждому элементу можно присоить значение. По умолчания первое значение 0, последующие +1.
Мonth представляет собой отдельный тип, отличный от "базового" типа int. Каждое значение типа Мonth имеет эквивалентное целочисленное значение, но большинство значений типа int не имеют эквивалентного значения типа Мonth.
Если вы настаиваете на использовании записи Мonth ( 9999) , то компилятор с вами согласится.
Заметим, что вы не можете использовать запись Мonth { 9 9 9 9 } , поскольку такая запись допускает только те значения, которые могут использоваться в инициализации Month; значения int к таковым не относятся.
К сожалению, мы не можем определить конструктор для перечисления. который проверял бы значения инициализаторов, но написать простую функцию для проверки не составляет труда.
перечисления полезны, когда нам нужно м ножество связанных друг с другом именованных целочисленных констант. Как правило, с помощью перечислений представляют нaбopы aльтepнaтив (up, down; yes , no, maybe; on, off; n, ne, e, se, s , sw, w, nw) или отличительных признаков (red, Ыuе. green, yellow, maroon, crimson, black)
массивы
Массив - это структура данных, состоящая из однотипных объектов, расположенных в смежных ячейках памяти. Элементы массива нумеруются в порядке возрастания начиная с нуля.
Количество элементов именованного массива должно быть известно на этапе компиляции, поэтому нужен const. Если мы хотим, чтобы количество элементов массива было переменным, то должны разместить его в динамической памяти и обращаться к нему через указатель.
Указатели
Безопасность типов
Безопасные преобразования
Чтобы преобразование было безопасным, надо преобразовать в тип содержащий больше либо столько же битов, чтобы не происходила потеря информации.
bool -> char, int, double
char -> int, double
int -> double
Не безопасно -
Вычисления
Выражения
length - lvalue переменной lenght, 20 - rvalue переменной length
Константные выражения
Константа - именнованый объект, который после инициализации нвозможно изменить.
constexpr
должно быть известно во время компиляции
const
не должно быть известно во время компиляции, после инициализации const
переменной ее не изменить
Операторы
Имя
Комментарий
Вызов функции
Передача а в качестве аргумента в функцию f
Префиксный инкремент
Увеличить на единицу и использовать увеличенное значение
Префиксный декремент
Уменьшить на единицу и использовать уменьшенное значение
Не
Результат имеет тип bool
Унарный минус
Умножение
Деление
Остаток от деления
Только для целочисленных типов
Сложение
Вычитание
Запись Ь в поток out
Здесь out - поток ostream
Считать ь из потока in
Здесь in - поток istream
Меньше
Результат имеет тип bool
Меньше или равно
Результат имеет тип bool
Больше
Результат имеет тип bool
Больше или равно
Результат имеет тип bool
Равно
Не путать с оператором =
Не равно
Результат имеет тип bool
а && Ь
Логическое И
Результат имеет тип bool
a ll b
Логические ИЛИ
Результат имеет тип bool
Присваивание
Не путать с оператором ==
Составное присваивание
lval=lval *а; используется также с операторами /, %, + и -
Выражение а<b<с означает ( а<b)<с, а значение выражения а<b имеет тип Ьооl , т.е. оно может быть либо true, либо false. Итак, выражение а<b<с эквивалентно тому, что выполняется либо неравенство . Либо неравенство . В частности , выражение не означает "Лежит ли значение b между значениями а и с?", как многие наивно (и совершенно неправильно) думают. Таким образом, выражение в принципе является бесполезным.
Преобразования
Если оператор имеет операнд типа douЬle, то используется арифметика чисел с плавающей точкой и результат имеет тип douЬle; в противном случае используется целочисленная арифметика, и результат имеет тип int.
'a'int{'a'}
Записи type (value)
и type { value }
означают "преобразовать value
в тип type
. Как если бы вы инициализировали переменную типа type значением value". Другими словами, при необходимости компилятор преобразовывает ("повышает") операнд типа int в операнд типа douЬle, а операнд типа char - в операнд типа int. Вычислив результат. компилятор может преобразовать его снова для использования в качестве инициализатора или в правой части оператора присваивания, например:
Инструкции
if
switch-case
Значение i сравнивается со значениями кейсов, можно убрать
break
, чтобы при первом удачном сравнение программа дальше искала одинаковые значения. Если не найдено совпадений, выполняется блокdefault
.Значения кейсов должны быть целочислеными или char или перечисление
Значения кейсов должны быть константными
У кейсов не могут быть одинаковые значения
while
for
Функции
int
- тип, square
- идентификатор, в скобках находится список параметров.
Ошибки
Введение
Ошибки времени компиляции(compile-time errors). Это ошибки, обнаруженные компилятором. Их можно подразделить на категории в зависимости от того, какие правила языка он нарушают:
синтаксические ошибки;
ошибки, связанные с типами.
Ошибки времени редактирования связей(link-time errors). Это ошибки , обнаруженные редактором связей при попытке объединить объектные файлы в выполнимый модуль.
Ошибки времени выполнения(run-time errors). Это ошибки, обнаруженные проверками в работающей программе. Их можно подразделить на следующие категории:
ошибки, обнаруженные компьютером (аппаратным обеспечением и/или операционной системой) ;
ошибки, обнаруженные библиотекой (например, стандартной библиотекой С++);
ошибки, обнаруженные кодом пользователя.
Логические ошибки(logic errors). Это ошибки , найденные программистом в поисках причины неправильных результатов.
Источники ошибок
Плохая спецификация. Если мы слабо представляем себе, что должна делать программа, то вряд ли сможем адекватно проверить все ее "темные углы" и убедиться, что все варианты обрабатываются правильно (т. е . что при любом входном наборе данных мы получим либо правильный ответ, либо осмысленное сообщение об ошибке).
Неполные программы. В ходе разработки неизбежно возникают варианты, которые мы не предусмотрели. Наша цель - убедиться. что все варианты обработаны правильно.
Непредусмотренные аргументы. Функции принимают аргументы . Если функция принимает аргумент, который не был предусмотрен, то возникнет проблема, как, например, при вызове стандартной библиотечной функции извлечения корня из - 1 . 2: sqrt ( -1 . 2) . Поскольку функция sqrt ( ) вычисляет квадратный корень от значения типа douЫe и возвращает результат типа douЫe. в этом случае она не сможет вернуть правильный результат. Такие проблемы обсуждаются в разделе 5 . 5 .3.
Непредусмотренные входны.е данные. Обычно программы считывают данные (с клавиаrуры, из файлов, средствами графического пользовательского интерфейса, из сетевых соединений и т.д.). Как правило. программы предъявляют к входным данным много требований, например, чтобы было введено целочисленное значение. Но что если пользователь введет не ожидаемое целочисленное значение, а строку "Отвали! "? Этот вид проблем обсуждается в разделах 5.6.3 и 1 0 . 6 .
Непредусмотренное состояние. Большинство программ хранит большое количество данных ("состояний"), предназначенных для использования разными частями системы. К их числу относятся списки адресов, каталоги телефонов или записанные в vector данные о температуре. Что если эти данные окажутся неполными или неправильными? В этом случае разные части программы должны сохранять управляемость. Эти проблемы обсуждаются в разделе 26.3.5.
Логические ошибки. Это ошибки. когда программа просто делает не то, что от нее ожидается; мы должны найти и исправить эти ошибки. Примеры поиска таких ошибок приводятся в разделе 6 . 6 и 6.9.
Ошибки времени компиляции
Синтаксические ошибки
При нахождение ошибки, комплитор может указать на строку которая не содержит ошибок, стоит проверить предшествующие строки программы.
Компилятор не знает, что именно вы пытаетесь сделать, потому что формулирует сообщения об ошибках с учетом того, что вы на самом деле сделали, а не ваших намерений. Например обнаружив ошибочное объявление переменной a3
, компилятор вряд ли напишет что-то вроде:
"Вы неправильно написали слово int: не следует употреблять прописную букву i."
Скорее всего он выразится так:
Сисинтаксическая ошибка: пропущена ' ; ' перед идентификатором 'sЗ'"
"У переменной 'sЗ' пропущен идентификатор класса или типа"
"Неправильный идентификатор класса или типа
Int
"
Все эти сообщения можно перевести так:
"Перед переменной aЗ имеется синтаксическая ошибка, и надо что-то сделать либо с типом
Int
, либо с переменной aЗ."
Ошибки, связанные с типами
Не ошибки
Ошибки времени редактирования связей
Любая программа состоит из нескольких отдельно компилируемых частей, которые называют единицами трансляции(translation units).
Каждая функция в программе должна быть объявлена с одним и тем же типом во всех единицах трансляции, в которых она используется. Для этого используются заголовочные файлы.
Каждая функция должно быть определена в программе 1 раз.
Если хотя бы одно из этих правил нарушено, редактор связей сообщит об ошибке.
Ошибки времени выполнения программы
Приведеные вызовы функций возвращат отрицательные числа, присвоенные переменным
area1
,area2
Деление на ноль в последнем вычисление
Обработка ошибок в вызывающем коде
Если нет необходимости сообщать об ошибках в каждом из аргументов, код можно упростить:
Для того, чтобы полностью защитить функцию area() от неправильных аргументов, необходимо исправить вызовы функции framed_area():
Если изменится тело функции framed_area(), то наш код будет непригоден, его называют хрупким. Можем ввести переменную, чтобы этого избежать:
Код стал большим и сложным.
Обработка ошибок в вызываемом коде
Это решение выглядит неплохо и нам не нужно будет писать проверку для каждого вызова функций framed_area()
Почему проверка аргументов функции пишется не всегда?
Мы не можем модифицировать определение функции. Функция находится в библиотеке, которую по тем или иным причинам невозможно изменить. Возможно, она будет использована другими людьми, не разделяющими вашего подхода к обработке ошибок. Возможно, она принадлежит кому-то еще, и вы не имеете доступа к ее исходному коду. Возможно, она включена в постоянно обновляющуюся библиотеку, так что, изменив эту функцию, вы будете вынуждены изменять ее в каждой новой версии.
Вызываемая функция не знает, что делать при выявлении ошибки. Эта ситуация типична для библиотечных функций. Автор библиотеки может выявить ошибку, но только вы знаете, что в таком случае следует делать.
Вызываемая функция не знает, откуда ее вызвали. Получив сообщение об ошибке, вы понимаете, что произошло нечто непредвиденное, но не можете знать, как именно выполняемая программа оказалась в данной точке. И ногда необходимо. чтобы сообщение было более конкретным.
Производительность. Дпя небольшой функции стоимость проверки может перевесить стоимость вычисления самого результата. Например, в случае с функцией area ( ) проверка вдвое увеличивает ее размер (т.е. удваивает количество машинных инструкций, которые необходимо выполнить, а не просто длину исходного кода) . В некоторых программах этот факт может оказаться критически важным, особенно если одна и та же информация проверяется постоянно функциями, вызывающими одна другую, и передающими при этом информацию более или менее неизменной.
Что в итоге делать? Проверять аргументы функции, если у вас нет веских причин поступать иначе.
Сообщения об ошибках
Исключения
Основная идея исключений состоит в отделении выявления ошибки(что может сделать в вызываемой функции) от ее обработки(что можно сделать в вызывающей функции), чтобы гарантировать, что ни одна выявленная ошибка не останется необработанной. Если функция обнаруживает ошибку, которую не может обработать она не выполняет оператор return
как обычно, а генерирует исключение с помощью инструкции throw
, показывая, что произошло нечто неправильное. Любая функция, прямо или косвенно вызывающая данную функцию, может перехватить созданное исключение с помощью конструкции catch, т.е. указать, что следует делать, если вызываемый код вызвал и нструкцию throw
. Функция выражает свою заинтересованность в перехвате исключений с помощью блока try
, перечисляя виды исключений, которые она планирует обрабатывать в своих разделах catch
блока try
. Если ни одна из вызывающих функций не перехватила исключение, то программа прекращает работу. ЗАКОНСПЕКТИРОВАТЬ YANDEX C++ БЕЛЫЙ ПОЯС, МОЖЕТ ЧТО-ТО ЕЩЕ
Логические ошибки
Программа делает ровно то, что вы написали. Если вы ожидали другой результат и все предыдущие ошибки устранены, то нужно искать ошибку в логике программы.
Функции
Функция - это именованная последовательность инструкций.
Функции нужны, чтобы:
избавиться от дублирования кода
выделить логические участки программы
упростить отладку
Объявления и определения
Объявление (declaration) - это инструкция, которая вводит имя в область видимости.
устанавливая тип именованной сущности (например, переменной или функции) и
(необязательно) выполняя инициализацию (например. указывая начальное значение переменной или тело функции).
Имя должно быть объявлено до использования в программе.
Объявлением считается только объявление, не являющееся определением. Определение устанавливает, на что именно ссылается имя . В частности, определение переменной выделяет память для этой переменной. Следовательно, ни одну сущность невозможно определить дважды.
Объявление, которое не является одновременно определением, просто сообщает, как можно использовать имя; оно представляет собой интерфейс, но не выделяет памяти и не описывает тело функции.
Объявление переменной указывает ее тип, но лишь определение создает реальный объект (выделяет память) . Объявление функции также устанавливает ее тип (типы аргументов и тип возвращаемого значения), но лишь определение создает тело функции (выполняемые инструкции).
Обратите внимание на то, что тело функции хранится в памяти как часть программы, поэтому вполне корректным будет сказать, что определения функций и переменных выделяют память, а объявления - нет.
Разница между объявлением и определением позволяет разделить программу на части и компилировать их по отдельности. Объявления обеспечивают связь между разными частями программы; при этом не нужно беспокоиться об определениях. Поскольку все объявления должны быть согласованы друг с другом (включая единственное определение), использование имен во всей программе должно быть непротиворечивым.
Аргументы функций
Передача параметров по значению
Передается копия значения.
Передача параметров по константной ссылке
Чтобы не копировать значение, в функцию нужно передавать адресс. Если мы не хотим изменять значение то стоит передавать по константной ссылке.
Амперсант означает ссылку. Теперь операции будут производиться не над копией, а над самим вектором.
Вызов функции и возврат значения
Объявление аргументов и тип возвращаемого значения
Передача параметров по значению
Передается копия значения.
Передача параметров по константной ссылке
Чтобы не копировать значение, в функцию нужно передавать адресс. Если мы не хотим изменять значение то стоит передавать по константной ссылке.
Амперсант означает ссылку. Теперь операции будут производиться не над копией, а над самим вектором.
Передача параметров по ссылке
int&
- ссылка на переменную int.
Если мы хотим изменять значение нужно передавать не по константной ссылке.
Проверка аргументов и преобразование типов
Реализация вызова функции
При вызове этих функций реализация языка программирования создает структуру данных содержащую копии всех ее параметров и локальных переменных.
Такую структуру данных называют зап11сью активац11и Финкции(functton acttvation record) или просто записью активации. Каждая функция имеет собственную запись активации .
Теперь функция expression()
вызывает функцию term()
, так что компилятор создает запись активации для вызова функции term()
.
Функция term()
имеет дополнительную переменную d
, которую необходимо хранить в памяти. поэтому при вызове мы резервируем для нее место, даже если в коде она нигде не используется. Для разумных функций затраты на создание записей активации не зависят от их размера. Локальная переменная d будет инициализирована только в том случае , если будет выполнен раздел case '/'
. Теперь функция term()
вызывает функцию primary()
Теперь функция primary()
вызывает функцию expression()
.
Этот вызов функции expression()
также имеет собственную запись активации, отличающуюся от записи активации первого вызова функции expression()
. Хорошо это или плохо, но теперь мы попадаем в очень запутанную ситуацию, поскольку переменные left и t при двух разных вызовах будут разными. Функция, которая прямо или (как в данном случае) косвенно вызывает себя, называется рекурс11вной (recursive). Как можно видеть, рекурсивные функции являются естественным следствием метода реализации, который мы используем для вызовов функций и возврата из них (и наоборот) .
Итак, каждый раз, когда мы вызываем функцию, стек зшшсей активации(stack of actlvatlon records), который часто называют просто стеком(stack), увеличивается на одну запись. И наоборот, когда функция возвращает управление, ее запись активации больше не используется. Например, когда при последнем вызове функции expression()
управление возвращается функции primary()
, стек возвращается в предыдущее состояние.
Когда функция primary()
возвращает управление функции term()
, стек возвращается в состояние, показанное ниже.
И так далее. Этот стек. который часто называют стеком вызовов(call stack), - структура данных, которая увеличивается и уменьшается с одного конца в соответствии с правилом "последним вошел - первым вышел".
соnstехрr-функции
constexpr функция вычисляет выражение на этапе компиляции, аргументы должны бать константны.
Функция, объявленная как constexpr. должна быть настолько простой, чтобы компилятор мог ее вычислить. В С++ 11 это означает, что функция, объявленная как constexpr
, должна иметь тело, состоящее из одной инструкции return. в С++ 14 мы также можем написать простой цикл. соnstехрr-функция не может иметь побочные эффекты, т.е. она не может изменять значения переменных вне собственного тела, за исключением тех, которые она присваивает или использует для инициализации.
Если компилятор не в состоянии определить, что соns tехрr-функция является "достаточно простой" (в соответствии с подробными правилами стандарта), такая функция рассматривается как ошибка.
Потоки ввода и вывода
Ввод и вывод
Большинство современных операционных систем поручают управление устройствами ввода-вывода специализированным драйверам, а затем программы обращаются к ним с помощью средств библиотеки ввода-вывода. обеспечивающих максимально единообразную связь с разными источниками и адресатами данных. В общем случае драйверы устройств глубоко внедрены в операционную систему и недоступны для большинства пользователей, а библиотечные средства ввода-вывода обеспечивают абстракцию ввода-вывода. так что программист не должен думать об устройствах и их драйверах.
При использовании такой модели вся входная и выходная информация может рассматриваться как потоки байтов (символов), обрабатываемые средствами библиотеки ввода-вывода.
Наша работа как программистов, создающих приложения, сводится к следующему.
Настроить потоки ввода-вывода для получения данных из устройств ввода и вывода данных на устройства вывода.
Читать и записывать потоки.
Модель потока ввода-вывода
Стандартная библиотека языка С++ содержит определение типа istream для потоков ввода и типа ostream - для потоков вывода.
Поток ostream
делает следующее:
Превращает значения разных типов в последовательности символов.
Посылает эти символы "куда-то" (например. на консоль, в файл, основную память или на другой компьютер) .
Буфер - это структура данных, которую поток ostream использует внутренне для хранения в ходе взаимодействия с операционной системой информации, полученной от вас. Задержка между записью в поток ostream и появлением символов в пункте назначения обычно объясняется тем, что эти символы находятся в буфере. Буферизация важна для производительности программы, а производительность программы важна при обработке больших объемов данных.
Поток istream
делает следующее:
Превращает последовательности символов в значения разных типов.
Получает эти символы "откуда-то" (например. с консоли, из файла, из основной памяти или от другого компьютера).
Поток iostream
делает то же что и ostream
и istream
istream
как и поток ostream
для взаимодействия с операционной системой поток использует буфер. При этом буферизация может оказаться визуально заметной для пользователя. Когда вы используете поток istream, связанный с клавиатурой, все, что вы введете, останется в буфере. пока вы не нажмете клавишу Enter
(возврат каретки/переход на новую строку). так что если вы передумали. то. пока не нажали клавишу Enter
. можете стереть символы с помощью клавиши Backspace
.
Файлы
При работе с файлами поток ostream преобразует объекты. хранящиеся в основной памяти, в потоки байтов и записывает их на диск. Поток istream
действует наоборот: иначе говоря. он считывает поток байтов с диска и составляет из них объект.
Открытие файла
Поток ifstream
- это поток istream
для чтения из файла, поток ofstream
- это поток ostream
для записи в файл, а поток fstream
представляет собой поток iostream
, который можно использовать как для чтения, так и для записи. Перед использованием файловый поток должен быть связан с файлом
Чтение из поток
Создадим файл .txt
и напишем программу, которая выводит консоль содержание файла:
Чтение из поток до разделителя
Допустим, считать нужно дату из следующего текстового файла date.txt
:
Оператор чтения из потока
Решим ту же самую задачу с помощью оператора чтения из потока ().
Оператор записи в поток. Дозапись в файл.
При каждом запустке программы файл записывается заново, то есть его содержимое удалялось и запись начиналась заново. Для того, чтобы открыть файл в режиме дозаписи, нужно передать специальный флажок ios::app
Форматирование вывода. Файловые манипуляторы.
fixed
- указывает, что числа далее нужно выводить на экран с фиксированной точностью.
setprecision
- задает количество знаков после запятой.
setw
(set width) - указывает ширину поля, которое резервируется для вывода переменной.
setfill
- указывает, каким символом заполнять расстояние между колонками.
left
- выравнивание по левому краю поля.
ООП
Классы
Класс является абстрактным, если его можно использовать только в качестве базового класса. Как сделать класс абстрактным?
Сделать конструкторы protected
Добавить в него чисто виртуальные функции
Класс, который можно использовать для создания объектов, в противоположность абстрактному классу называется конкретным (concrete).
Порождение - это способ построения одного класса из другого так, чтобы новый класс можно было использовать вместо исходного
Типы, определенные пользователем
Тип называется встроенным, если компилятор знает, как представить объекты такого типа и какие операторы к нему можно применять без уточнений в виде объявлений которые создает программист в исходного коде. Типы не относящиеся к строенным называются пользовательскими(типы определенные пользователем, UTD - user-defined types)
Класс - это пользовательский тип, определяющий представление объектов этого класса, их создание, использование и кничтожение.
Классы и члены класса
Для доступа к членам используют конструкцию имя_экземпляра_объекта.имя_члена
Тип члена определяет, какие операции с ним можно выполнять.
Разработка класса
Структура и функции
Конструктор - функция-член имя которой совпадает с именем класса.
Сокрытие деталей
Модификаторы доступа нужны, чтобы ограничить доступ к членам.
Значение объекта называют состоянием(state) объекта. Состояние - это данные-члена класса. В примере выше это y, m, d. Кореектное значение - корректное состояние объекта. За корректное состояние отвечает конструктор.
Правило, определяющее смысл корректного значения, называют инвариантом. Инвариант для класса Date
(Объект класса Date должен представлять дату в прошлом. настоящем или будущем") необычайно трудно сформулировать точно: вспомните о високосных годах, переходе с юлианского на григорианский календарь, часовых поясах и т.п. Однако для простых и реалистичных ситуаций написать класс Date - вполне доступная задача. Например. если мы инициализируем журнальные записи, нас не должны беспокоить ни григорианский, ниюлианский календари. ни даже календарь племени майя. Если мы не можем придумать хороший инвариант. то, вероятно, имеют место простые данные.
Определение функций-членов
Для определения членов вне класса используется конструкция имя_класса::имя_члена
```Class Date{ public: Date(int, int, int); private: int y, d, m; }
Date::Date(int yy, int mm, int dd) :y(yy), m(mm), d(dd) { ... }
Функция-член класса получают неявный аргумент, позволяющий идентифицировать объект для которого они называются.
Перегрузка операторов
Для класса или перечисления можно определить практически все операторы, существующие в языке С++. Этот процесс называют перегрузкой операторов (operator overloadlпg). Он применяется, когда требуется сохранить привычные обозначения для разрабатываемого нами типа
Перегруженный оператор должен иметь хотя бы один операнд с пользовательским типом.
Интерфейсы классов
Интерфейс должен быть полным
Интерфейс должен быть минимальным
Класс должен иметь конструкторы
Класс должен поддерживать копирование(или явно запрещать его)
Следует предусмотреть тщательную проверку типов аргументов
Деструктор должен освобождать все ресурсы
Константные функции члены
const ознчает, что данный метод не будет модифицировать объект
Конструктор
Основные операции
Какие конструкторы должен иметь класс?
Конструкторы с одним или несколькими аргументами.
Конструктор по умолчанию.
Копирующий конструктор (копирование объектов одинаковых типов).
Копирующее присваивание (копирование объектов одинаковых типов).
Перемещающий конструктор (перемещение объектов одинаковых типов).
Перемещающее присваивание (перемещение объектов одинаковых типов).
Деструктор.
(не из книги) Конструктор с списком инициализации(initializer_list)
Явные конструкторы
Конструктор , имеющий один арrумент, определяет преобразование типа этого арrумента в свой класс. Это может оказаться очень полезным.
Однако неявные преобразования еле.дует применять редко и осторожно, поскольку они могут вызвать неожиданные и нежелательные эффекты . Например, наш класс vector, определенный выше, имеет конструктор , принимающий аргумент типа int, откуда еле.дует, что он определяет преобразование типа int в класс vector:
Когда создается объект класса х, вызывается один из его конструкторов.
Когда уничтожается объект типа х, вызывается его деструктор.
Деструктор вызывается всегда, когда уничтожается объект класса; это происходит, когда объект выходит из области видимости, программа завершает работу или к указателю на объект применяется оператор delete. Некоторый подходящий конструктор вызывается каждый раз. когда создается объект класса; это происходит при и нициализации переменной, при создании объекта с помощью оператора new (за исключением встроенных типов), а также при копировании объекта.
Векторы и динамически выделяемая память
Указатель - это адрес ячейки памяти.
Динамически распределяемая память и указатели
Когда начинается выполнение программы, написанной на языке С++, компилятор компилирует память под код (иногда эту память называют сегментом кода) и глобальные переменные (эту память называют сегментом данных).
Кроме того, выделяется память, которая будет использоваться при вызове функций для хранения их аргументов и локальных переменных (эта память называется стеком) . Остальная память компьютера может использоваться для других целей; она называется динамически распределяемой или просто динамической.
Язык С++ делает эту динамическую память (которую также называют кучей (heap)) доступной с помощью оператора new:
Указанная выше инструкция просит систему поддержки выполнения программ разместить четыре числа типа douЫe в динамической памяти и вернуть указатель на первое из них. Этот указатель используется для инициализации переменной р.
Оператор new возвращает указатель на объект. который он создал . Если оператор new создал несколько объектов (массив), то он возвращает указатель на первый из этих объектов. Если этот объект имеет тип х. то указатель, возвращаемый оператором new, имеет тип Х*:
Данный оператор new возвращает указатель н а переменную типа douЫe, но тип douЫe отличается от типа char, поэтому мы не должны (и не можем) присвоить указатель на переменную типа douЫe указателю на переменную типа char.
Размещение в динамической памяти
Оператор new выполняет выделение (allocation) динамической памяти (free store).
Оператор new возвращает указатель на выделенную память.
Значением указателя является адрес первого байта выделенной памяти.
Указатель указывает на объект определенного типа.
Указатель не знает на какое количество элементов он указывает
Оператор new может выделять память как для отдельных элементов, так и для последовательности элементов:
Количество объектов может задаваться пеменной. Это важно, поскольку позволяет нам выбирать для какого количества объектов можно выделять память во время выполнения программы. Если
Указатели на объекты разных типов имеют разные типы.
Почему нельзя? Мы же можем присвоить переменную типа int переменной типа double и наоборот. Причина в операторе []
. ДЛя того чтобы найти нужный элемент, он использует информацию о размере его типа. Например, элемент qi[2]
находится на расстоянии, равном двум размерам типа int
от элемента qi[0]
, а элемент qd[2]
находится на расстоянии, равном двум размерам типа douЫe от элемента qd[0]
. Если размер типа int отличается от размера типа douЬle, как во многих компьютерах, то, разрешив указателю qi указывать на память, выделенную для адресации указателем qd, можем получить довольно странные результаты.
Это объяснение с практической точки зрения. С теоретической точки зрения ответ таков: присваивание друг другу указателей на разные типы сделало бы возможными ошибки типа (type errors).
Доступ с помощью указателей
Кроме оператора разыменования *. к указателю можно применять оператор индексирования []
:
Когда оператор [ ] применяется к указателю р. он интерпретирует память как последовательность объектов (имеющих тип, указанный в объявлении указателя) , на первый из которых указывает указатель р.
Диапазоны
Основная проблема, связанная с указателями, заключается в том, что указатель не знает, на какое количество элементов он указывает.
И меется ли третий элемент там, куда указывает указатель pd? Можно ли обращаться к пятому элементу pd [ 4 ] ? Если мы посмотрим на определение указателя pd, то ответим "да" и "нет" соответственно. Однако компилятор об этом не знает; он не отслеживает значения указателей. Наш код просто обращается к памяти так, будто она распределена правильно. Компилятор даже не возразит против выражения pd [ -3 ] . как будто можно разместить три числа типа douЫe перед элементом, на который указывает указатель pd.
Нам не известно, что собой представляют ячейки памяти, на которые указывают выражения pd [ -3 ] и pd [ 4 ] . Однако м ы знаем. что они не могут использоваться как часть нашего массива, в котором хранятся три числа типа douЬle, на которые указывает указатель pd. Вероятнее всего, они являются частью других объектов, и мы просто заблудились. Это плохо . Это катастрофически плохо. Здесь слово "катастрофически" означает либо "моя программа почему-то завершилась аварийно", либо "моя программа выдает неправильные ответы". Выход за пределы допустимого диапазона представляет собой особенно ужасную ошибку, поскольку очевидно, что при этом опасности подвергаются данные, не имеющие отношения к нашей программе. Считывая содержимое ячейки памяти, находящейся за пределами допустимого диапазона, мы получаем случайное число, которое может быть результатом совершенно других вычислений. Выполняя запись в ячейку памяти, находящуюся за пределами допустимого диапазона, можем перевести какой-то объект в "невозможное" состояние или просто получить совершенно неожиданное и неправильное значение. Такие действия, как правило. остаются незамеченными достаточно долго, поэтому их особенно трудно выявить. Что еще хуже: дважды выполняя программу, в которой происходит выход за пределы допустимого диапазона, с немного разными входными данными, мы можем прийти к совершенно разным результатам . Ошибки такого рода ("неустойчивые ошибки") выявить труднее всего.
Инициализация
Очевидно, что объявление указателя рО без инициализации может вызвать проблемы . Рассмотрим пример.
Эта инструкция записывает число 7.0 в некую ячейку памяти. Мы не знаем, в какой части памяти расположена эта ячейка. Это может быть безопасно, но рассчитывать на это нельзя. Рано или поздно мы получим тот же результат, что и при выходе за пределы допустимого диапазона: программа аварийно завершит работу или выдаст неправильные результаты.
Мы можем указать список инициализации для массивов объектов, память для которых выделяется оператором new, например:
Когда мы используем собственные типы. мы получаем больший контроль над инициализацией. Если класс Х имеет конструктор по умолчанию, то мы получим следующее:
Если тип У имеет конструктор, но не конструктор по умолчанию, мы должны выполнить явную инициализацию:
Нулевой указатель
Если в вашем распоряжении нет другого указателя, которым можно было бы инициализировать ваш указатель, используйте нулевой указатель nullptr
.
При присваивании указателю нулевого значения последнее носит название нулевого указателя (null pointer). и зачастую корректность указателя (т.е. то, что он указывает на что-то) проверяется с помощью сравнения его значения с nullptr:
Освобождение памяти
Оператор new выделяет участок динамической памяти. Поскольку память компьютера ограничена, неплохо было бы возвращать память обратно, когда она станет больше ненужной. В этом случае освобожденную память можно было бы использовать для хранения других объектов. Для больших и долго работающих программ такое освобождение памяти играет важную роль.
При работе такой программы каждый вызов функции calc () будет "терять" память, выделяемую под массив элементов douЬle, адрес которого присваивается указателю р. Например, вызов calc ( 100 , 1000) сделает недоступным для остальной части программы участок памяти, на котором может разместиться тысяча переменных типа douЫe.
Оператор, освобождающий память, называется delete. Для того чтобы освободить память для дальнейшего использования, оператор delete следует применить к указателю, который был возвращен оператором new:
Оператор delete имеет две разновидности:
delete р освобождает память, выделенную с помощью оператора new для отдельного объекта;
delete [ ] р освобождает память, выделенную с помощью оператора new для массива объектов.
Двойное удаление объекта - очень грубая ошибка. Рассмотрим пример.
Вторая инструкция delete р порождает две проблемы.
Вы больше не владеете объектом, поэтому диспетчер динамической памяти может изменить внутреннюю структуру данных так, что выполнить инструкцию delete р правильно во второй раз будет невозможно.
Диспетчер динамической памяти может повторно использовать память, на которую указывал указатель р, так что теперь указатель р указывает на другой объект: удаление этого объекта (принадлежащего другой части программы) приведет к ошибке.
Удаление нулевого указателя не приводит ни к каким последствиям (так как нулевой указатель не указывает н и на один объект), поэтому эта операция безвредна:
Почему следует избегать утечки памяти? Программа, которая должна работать "бесконечно" , не должна допускать никаких утечек памяти . Примерами таких программ являются операционная система, а также большинство встроенных систем
Деструкторы
Вызываются при кничтожение объектов, удобно удалять ресурсы, если класс имеет вирутальную функцию, то но должн иметь виртуальный деструктор, поскольку он является базовым.
Указатели на объекты класса
Когда мы удаляем объект класса vector с помощью оператора delete, вызывается деструктор класса vector.
При создании объекта класса vector в динамической памяти оператор new выполняет следующие действия:
сначала выделяет память для объекта класса vector;
затем вызывает конструктор класса vector, чтобы инициализировать объект; этот конструктор выделяет память для элементов объекта класса vector и инициализирует их.
Удаляя объект класса vector, оператор delete выполняет следующие действия:
сначала вызывает деструктор класса vector; этот деструктор, в свою очередь, вызывает деструкторы элементов (если таковые имеются), а затем освобождает память, занимаемую элементами вектора;
затем освобождает память, занятую объектом класса vector.
Все классы поддерживают оператор -> (стрелка) для доступа к своим членам с помощью указателя на объект:
Путанца с типами: void* и операторы приведения типов
Тип void* означает "указатель на ячейку памяти, тип которой компилятору неизвестен". Он используется тогда, когда необходимо передать адрес из одной части программы в другую. причем каждая из них ничего не знает о типе объекта, с которым работает другая часть. Примерами являются адреса, служащие аргументами функций обратного вызова, а также распределители памяти самого нижнего уровня (такие, как реализация оператора new).
Указателю типа void* можно присвоить указатель на любой объект, например:
Поскольку компилятор не знает что такое void*, мы должен ему сказать
Оператор static_cast позволяет явно преобразовать указатели связанных типов один в другой, например такие, как void или douЫe. Имя static_cast - это сознательно выбранное отвратительное имя для отвратительного (и опасного) оператора, который следует использовать только в случае крайней необходимости. Его редко можно встретить в программах (если он вообще используется) . Операции, такие как static_cast, называют явным преобразованием типа (explicit type conversion) или просто приведением (cast).
В языке С++ предусмотрены два оператора приведения типов, которые потенциально еще хуже оператора static_cast.
Оператор reinterpret_cast может преобразовать тип в совершенно другой, никак не связанный с ним, например int в douЫe*.
Оператор const cast позволяет отбросить квалификатор const.
Лучше избегать всяких кастов
Указатели и ссылки
Ссылку (reference) можно интерпретировать как автоматически разыменовываемый постоянный указатель или альтернативное имя объекта. Указатели и ссылки отличаются следующими особенностями.
П рисвоение чего-либо указателю изменяет значение указателя, а не объекта, на который он указывает.
Для того чтобы получить указатель, как правило, необходимо использовать оператор new или &.
Для доступа к объекту, на который указывает указатель, используются операторы * и [ ] .
Присвоение ссылке нового значения изменяет значение объекта, на который она ссылается, а не саму ссылку.
После инициализации ссылку невозможно перенаправить на другой объект.
Присваивание ссылок выполняет глубокое копирование (новое значение присваивается объекту. на который указывает ссылка); присваивание указателей не использует глубокое копирование (новое значение присваивается указателю, а не объекту) .
Нулевые указатели представляют опасность.
Обратите внимание на последний пример; это значит не только то, что эта конструкция не работает, - после инициализации невозможно связать ссылку с другим объектом . Если вам нужно указать на другой объект, используйте указатель.
Как ссылка, так и указатель основаны на адресации памяти . Они просто используют адреса по-разному, чтобы предоставить программисту несколько разные возможности .
Указатели и ссылка как параметры функций
Если вы хотите изменить значение переменной значением, вычисленным функцией, можно использовать три варианта:
Какой выбор сделать? Нам кажется, что возврат значения чаще приводит к более очевидному (а значит, менее подверженному ошибкам) коду:
Этот стиль предпочтительнее для небольших объектов, таких как тип int.
Для маленьких объектов предпочтительнее передача по значению.
Для функций , допускающих в качестве своего аргумента "отсутствующий объект" (представленный значением nullptr), следует использовать указатели (и не забывать о проверке nullptr!) .
В противном случае в качестве параметра следует использовать ссылку.
Прочее
указатели, массивы, ссылки
— указатели на определенный тип vs указатели на void.
— арифметика указателей. Сакральное (на самом деле нет) знание компилятора о размере объекта. Сравнение указателей.
— умные указатели и RAII.
— type punning (каламбур типизации) или веселые вещи, которых вы не ожидали от оптимизатора.
— указатели на функции-члены класса.
— указатели на подобъекты внутри объектов со сложным наследованием.
— ссылки, константные ссылки
память отводится не побайтно, а кусками определенного минимального размера (т.е. сто объектов по 1 байту займут в памяти гораздо больше, чем 100 байт). И что при удалении система сама разберется, какой размер был у куска памяти, когда его отводили.
between reference and pointers - Hi Asterix - fair enough, a quick One Lone Coder lesson. The difference between pointers and references is - nothing! As I elude to at the start of the video, the syntax is not helpful here as they use the same symbols with multiple purposes. I assume you are referring to functions along the lines of SomeFunction(SomeObject &o). The ampersand '&' can always be read as "get the address of" so in this case we pass a pointer to the object as the argument to the function, instead of a copy of the object itself. This means any changes the function makes to the object, it makes to the original object. You could interpret this as the function having both read and write access to the original object, whereas without the ampersand it would only have read access (as writing would only affect the copy). The reason you may want to pass by reference could also be performance. Instead of having to copy the object (which may not be trivial), you just copy its pointer. In C++ you have to explicitly say you wish to "pass by reference" and it assumes "pass by value" is the default. Java on the other hand almost does everything as "pass by reference".
Статическая и динамическая память
— что такое динамическая память и откуда она берется в системе.
— чем динамическая память отличается от других типов памяти в модели С++.
— как отводить и освобождать динамическую память (new/delete, new[]/delete[], malloc/calloc/free). Грабли (утечки памяти, двойное освобождение, невызовы деструкторов).
— кратенько (или длинненько) о встроенном аллокаторе, его достоинствах и недостатках. Фрагментация памяти, требования к выравниванию, минимальный кусок выделяемой памяти.
— замена аллокатора на свой. Placement new.
— создание сложных объектов (наследование, виртуальные функции, виртуальное наследование, аллоцирующие память члены класса). Где в памяти могут оказаться куски казалось бы единого объекта.
— многопоточность и аллокация памяти.
стандарты и их различия, область видимости и время жизни, виды памяти, таблица виртуальных методов, исключения, rtti, семантика перемещения, идиомы c++ классика многопоточности osi tcp udp ооп. функ прог cmake, git теор вер, дискрет
Last updated
Was this helpful?