Основы C++

Пространства имен

Пространство имен нужно для организации классов, функций, данных и типов в виде идентифицируемой и именованной части программы без определения нового типа. Допустим, мы хотим создать графическую библиотеку

namespace Graph_lib {
    class Color {}
    class Shape {}
    class Line : Shape {}
    ...
}

В дальнейшем, чтобы пользоваться классами библиотеки мы можем использовать следующую конструкцию

Graph_lib::Color

Также можем сами создать свой класс Color и коллизии имен не будет.

Область видимости

void f() { g(); } //g() находится вне области видимости
void g(){ f(); } // OK
void h()
{
    int x = y; // y находится вне области видимости
    int y = x; // x находится в области видимсоти
}

Существует несколько разновидностей областей видимости , которые можно использовать для управления используемыми именами:

  • Глобальная область видимости (global scope): область исходного текста, не входящая ни в одну другую область видимости.

  • Пространство имен (namespace scope): именованная область видимости. вложенная в глобальную область видимости или в другое пространство имен.

  • Область видимости класса (class scope): часть исходного текста. находящаяся в классе.

  • Локальная область видимости (local scope): между фигурными скобками { . . . } блока или в списке аргументов функции.

  • Область видимости инструкции: например. в цикле for.

Основное предназначение области видимости - сохранить локальность имен. чтобы они не пересекались с именами. объявленными в другом месте.

    void f ( int х)   // Функция f глобальная;
    {                  // переменная х локальная в функции f
        int z = х+7 ; // Переменная z локальная
    }
    int q ( int х)    // Функция g глобальная;
    {                  // переменная х локальная в функции g
        int f = х+2 ; // Переменная f локальная
        return 2 *f ;
    }
// Фнукция f() не видит переменную x функции q()
// Функция g() не видит переменную x функции g()
{
    int x = 7;
    int y = 5;
    {
        int x = y; // x = 5
        int y = 6;
        x = y; // x = 6
        ++x; // x = 7
    }
    ++x; // x = 8
}

Объекты

Объект - это место в памяти, имеющее тип, который определяет нформацию которую можно записать туда и значение - непосредственно информация. Например, строки символов вводятся в переменные типа string, а целые числа - в переменные типа int. Именованный объект называется переменной. Для доступа к объекту нужно знать его имя.

  • Объект - уаток памяти, в котором хранится значение определенного типа.

  • Тип определяет набор возможных значений и операций, выполняемых над объектом.

  • Значение - набор битов в памяти интерпритируемый в соответствии с типом.

  • Переменная - именованный объект

  • Объявление - инструкция, приписывающая объекту определеное имя

  • Определение - объявление, выделяющее память для объекта

      int a;//определение переменной
      string s= "hello";//определение переменной и ее инициализация
  • Инициализация - объявление и присваивание объекту первичное значение.

      int emus{7}; // set emus to 5
      int rheas = {12}; // set rheas to 12
      int rocs = {}; // set rocs to 0
      int psychics{}; // set psychics to 0
  • Объект можно интерпретировать как "коробку", в которую можно поместить значение, имеющее тип объекта.

Типы

Простые типы

char bool int double unsigned

Составные типы

Структуры(struct)

Структура - способ хранения нескольких типов данных

struct inflatable //объявление структуры. inflatable - идентификатор или дескриптор
{
    char name[20];
    float volume;
    double price;
};
inflatable goose; // переменная типа inflatable
goose.price = 5.0;
inflatable guest {"Gloria", 1.99, 29.99};
inflatable guest1 {}; //volume = price = 0; байты name = 0

Можно комбинировать определение формы структуры с созданием структурных переменных.

struct perks
{
    int key_number;
    char car[12];
} mr_smith, ms_jones; // two perks variables

struct new_perks
{
    int key_number;
    char car[12];
} mr_giltz =
{
    7,
    "Packed"
};

Можно создать одну структурную переменную, то есть мы не сможем создавать другие переменные этого типа

struct // no tag
{
    int x; // 2 members
    int y;
} position; // a structure variable
position.x = 2;

Можно указывать размер члена структуры(в битах). Тип поля должен быть целочисленным или перечислимым.

struct torgle_register
{
    unsigned int SN : 4; // 4 бита для значения SN
    unsigned int     : 4; // 4 бита не используются
    bool goodIn        : 1; // Допустимый ввод(1 бит)
    bool goodTorgle    : 1; // Признак успешности
};
torgle_register tr = {14, true, false};

Объединения(union)

Объединение - это формат данных, который может хранить в пределах одной области памяти разные типы данных, но в каждый момент времени только один из них.

union one
{
    int     int_val;
    long       long_val;
    double     double_val;
};

Переменную one можно использовать для хранения int, long или double, если только делать это не одновременно

one pail;
pail.int_val     = 15; // сохраняем int
pail.double_val = 15.0; //удаляем int, сохраняем double

Зачем использовать?

  • экономия памяти

  • элемент данных может использовать два или более форматов, но никогда - одновременно

Например, предположи м, что вы ведете реестр каких-то предметов, из которых одни имеют целочисленный идентификато р, а другие - строковый.

struct widget
{
    char brand[20];
    int type;
    union id // формат зависит от типа предмета
    {
        long id_num; // предмет первого типа
        char id_char[20]; // другие предметы
    } id_val;
};
...
widget prize;
...
if (prize.type == 1)
    cin >> prize.id_val.id_num;
else
    cin >> prize.id_val.id_char;

Анонимное обидинение не имеет имени; в сущности , его члены становятся переменными, расположенными по одному и тому же адресу в памяти . Разумеется , только одна из них может быть текущей в единицу времени:

struct widget
{
    char brand[20];
    int type;
    union // anonymous union
    {
        long id_num; // type 1 widgets
        char id_char[20]; // other widgets
    };
};
...
widget prize;
...
if (prize.type == 1)
    cin >> prize.id_num;
else
    cin >> prize.id_char;

Перечисления(enum, enum class)

enum

Способ создания символических констант(как const).

enum spectrum {red, orange, yellow, green, blue, violet, indigo, ultraviolet};
  • объявили имя нового типа spectrum; spectrum называется перечислением

  • компилятор установит значения перечислителей 0-7(первый 0 и к каждому последующему +1). Если 5 перечислителю присоить значение 0, то 6 значение будет равно 1 и т.д.

spectrum band;             // band - переменная типа spectrum
band = blue;             // blue - перечислитель
band = 2000;             // ошибка - 2000 - не перечислитель
int color = blue;         //тип spectrum приводится к int
color = 3 + red;          // red преобразуется в int
band = spectrum(3);        // приведение 3 к типу spectrum
band = spectrum(4003);     // результат не определен

Результат не определен, т.к. мы вышли за диапазон. Для нахождения верхнего предела выбирается перечислитель с максимальным значением. Затем ищется наименьшее число, являющееся степенью двойки , которое больше этого максимального значения, и из него вычитается единица.

Например: enum bigstep {first = -6, second = 100, third}; максимальное число - 101, минимальное число представляющее степень двойки, которое больше 101 - 128, 1281127128-1-127. Верхний предел - 127. Для нахождения минимального предела выбирается минимальное значение перечислителя. Если оно раво 0 или больше, то нижним пределом диапазона будет 0. Если число отрицательное, используется такой же подходит, как при вычислении верхнего предела, но со знаком минус. В нашем случае 8+1=7-8+1=-7. Нижний предел -7.

Это нужно, чтобы компилятор мог выяснить, сколько места необходимо для хранения перечисления(1 байт или менее для перечисления с небольшим диапазоном - 4 байт для перечислений с типом long).

Если не нужно создавать переменные перечислимого типа, а нужно только использовать константы можно не указывать идентфикатор.

enum { red, orange, yellow, green, blue, violet, indigo, ultraviolet };

enum classs

enum - очень простой пользовательский тип, который задает множество значений(элементов перечисления) в виде символических констант:

enum class Month{
    jan = 1, feb, mar, apr, ..., dec
};

Ключевой слово class означает, что перечисления находятся в области видимости перечисления, т.е. чтобы обратиться к элементу jan, нужно использовать конструкцию Month::jan. Каждому элементу можно присоить значение. По умолчания первое значение 0, последующие +1.

enum class Month{
    jan = 1,
    feb,
    mar = 5,
    apr, // 6
    ...
};
Month m = Month::feb;
Month m2 = feb;         // ошибка: feb не в области видимости
m = 7;                     // ошибка: нельзя присовить значение int переменной Month
Month mm = Month(7);      // конвертирование int в Month(непроверяемое)

Мonth представляет собой отдельный тип, отличный от "базового" типа int. Каждое значение типа Мonth имеет эквивалентное целочисленное значение, но большинство значений типа int не имеют эквивалентного значения типа Мonth.

Month bad = 9999; // Ошибка: целое число невозможно преобразовать в объект типа Month

Если вы настаиваете на использовании записи Мonth ( 9999) , то компилятор с вами согласится.

Заметим, что вы не можете использовать запись Мonth { 9 9 9 9 } , поскольку такая запись допускает только те значения, которые могут использоваться в инициализации Month; значения int к таковым не относятся.

К сожалению, мы не можем определить конструктор для перечисления. который проверял бы значения инициализаторов, но написать простую функцию для проверки не составляет труда.

Month int_to_month(int x)
{
    if (x < int(Month::jan) || int(Month::dec) < x)
        error("invalid month");
    return Month(x);
}
...
Month mm = int_to_month(m);

перечисления полезны, когда нам нужно м ножество связанных друг с другом именованных целочисленных констант. Как правило, с помощью перечислений представляют н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 int mах = 100;
int qai[max] ; // Глобальный ма ссив (из 100 чисел типа int) ; "живет вечно "
void f (int n)
{
    char lac[20];     // локальный массив; "живет" до выхода из области видимости
    double lad[n];  // Ошибка: размер массива не является константой
    lac[1] = 'x';
    *lac = 'c';      // lac[0] = 'c';
    int cards[4] = {3, 6, 8, 10};
    int hand[4];
    hand = cards;
    int num[5] {2, 5}; //{2, 5, 0, 0, 0};
    int num1[10] {}; //{0, 0, 0, ..., 0};
    int num2[] {2, 4, 6}; //num2.size = 3(необязательно писать размер, если мы инициализируем массив, компилятор сам считает размер)
    char plif[] {'h', 'i', 112421031, '\0'}; //ошибка - сужение
}

Количество элементов именованного массива должно быть известно на этапе компиляции, поэтому нужен const. Если мы хотим, чтобы количество элементов массива было переменным, то должны разместить его в динамической памяти и обращаться к нему через указатель.

Указатели

Безопасность типов

Безопасные преобразования

Чтобы преобразование было безопасным, надо преобразовать в тип содержащий больше либо столько же битов, чтобы не происходила потеря информации.

  • bool -> char, int, double

  • char -> int, double

  • int -> double

Не безопасно -

    double d = 2.5;
    int y = d; //y = 2
    int a = 5;
    double b = a; //b = 5.0
    int a = 120;
    char c = a; // в соответсвие с таблицей ASCII в переменной c лежит значение x
    int a  = 20000;
    char c = a;// Попытка втиснуть большое значение типа int  в маленькую переменную типа char
    double f = 2.7
    int e {f}; //компилятор выдаст ошибку, если использовать инициализатор, т.к. он запрещает сужение
    char c1 {130} //ошибка

Вычисления

Выражения

    int lenght = 20;

length - lvalue переменной lenght, 20 - rvalue переменной length

Константные выражения

Константа - именнованый объект, который после инициализации нвозможно изменить.

    constexpr double pi = 3.14159;
    pi = 7; //ошибка
    int v = 2*pi/x;

constexpr должно быть известно во время компиляции

constexpr int max = 100;
void use(int n)
{
    constexpr int c1 = max + 7;
    constexpr int c2 = n + 7; //ошибка
}

const не должно быть известно во время компиляции, после инициализации const переменной ее не изменить

constexpr int max = 100;
void use(int n)
{
    constexpr int c1 = max + 7;
    const int c2 = n + 7;
    c2 = 7; //ошибка
}

Операторы

Имя

Комментарий

f(а)f (а)

Вызов функции

Передача а в качестве аргумента в функцию f

++++lval

Префиксный инкремент

Увеличить на единицу и использовать увеличенное значение

--lval

Префиксный декремент

Уменьшить на единицу и использовать уменьшенное значение

!а! а

Не

Результат имеет тип bool

а

Унарный минус

аЬа*Ь

Умножение

а/Ьа/Ь

Деление

аа%Ь

Остаток от деления

Только для целочисленных типов

а+Ьа+Ь

Сложение

аЬа-Ь

Вычитание

out<<out<<

Запись Ь в поток out

Здесь out - поток ostream

in>>bin>>b

Считать ь из потока in

Здесь in - поток istream

а<Ьа<Ь

Меньше

Результат имеет тип bool

а<=Ьа<=Ь

Меньше или равно

Результат имеет тип bool

а>Ьа>Ь

Больше

Результат имеет тип bool

а>=Ьа>=Ь

Больше или равно

Результат имеет тип bool

а==Ьа==Ь

Равно

Не путать с оператором =

а!=Ьа ! =Ь

Не равно

Результат имеет тип bool

а && Ь

Логическое И

Результат имеет тип bool

a ll b

Логические ИЛИ

Результат имеет тип bool

lval=a=a

Присваивание

Не путать с оператором ==

lval=a*=a

Составное присваивание

lval=lval *а; используется также с операторами /, %, + и -

Выражение а<b<с означает ( а<b)<с, а значение выражения а<b имеет тип Ьооl , т.е. оно может быть либо true, либо false. Итак, выражение а<b<с эквивалентно тому, что выполняется либо неравенство true<ctrue<c. Либо неравенство false<cfalse<c. В частности , выражение а<b<са<b<с не означает "Лежит ли значение b между значениями а и с?", как многие наивно (и совершенно неправильно) думают. Таким образом, выражение а<b<са<b<с в принципе является бесполезным.

Преобразования

Если оператор имеет операнд типа douЬle, то используется арифметика чисел с плавающей точкой и результат имеет тип douЬle; в противном случае используется целочисленная арифметика, и результат имеет тип int.

52=2\frac{5}{2}=2

2.52=1.25\frac{2.5}{2}=1.25

'a'+1=+1=int{'a'}+1+1

Записи type (value) и type { value } означают "преобразовать value в тип type. Как если бы вы инициализировали переменную типа type значением value". Другими словами, при необходимости компилятор преобразовывает ("повышает") операнд типа int в операнд типа douЬle, а операнд типа char - в операнд типа int. Вычислив результат. компилятор может преобразовать его снова для использования в качестве инициализатора или в правой части оператора присваивания, например:

    double d = 2.5;
    int i = 2;
    double d2 = d/i; //d2==1.25
    int i2 = d/i; //i2 == 1
    int i3 {d/i}; //ошибка - преобразование double -> int
    d2 = d/i; //d2 == 1.25
    i2 = d/i; //i2 == 1

Инструкции

if

    int a = 0;
    int b = 0;
    cout << "Please enter two integers\n";
    cin >> a >> b;

    if (a<b) // condition
    {        // 1st alternative (taken if condition is true):
        cout << "max(" << a << "," << b <<") is " << b <<"\n";
    }
    else if (a > b)
    {        // 2nd alternative (taken if condition is false):
        cout << "max(" << a << "," << b <<") is " << a << "\n";
    }
    else (a == b) return 0;

switch-case

switch(i)
{
    case 0: case 3: case 5: 
        break;
    case 1: case 4: case 6:
        break;
    case 2: 
        break;
    default: break;
}
  • Значение i сравнивается со значениями кейсов, можно убрать break, чтобы при первом удачном сравнение программа дальше искала одинаковые значения. Если не найдено совпадений, выполняется блок default.

  • Значения кейсов должны быть целочислеными или char или перечисление

  • Значения кейсов должны быть константными

  • У кейсов не могут быть одинаковые значения

while

    int i = 0; // start from 0
    while (i<100) {
        cout << i << '\t' << square(i) << '\n';
        ++i; // increment i (that is, i becomes i+1)
    }

for

    for (int i = 0; i<100; ++i)
        cout << i << '\t' << square(i) << '\n';

Функции

int - тип, square - идентификатор, в скобках находится список параметров.

int square(int); //объявление функции
int square(int x) // определение функции
{
    reutnr x*x;
}

Ошибки

Введение

  • Ошибки времени компиляции(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.

Ошибки времени компиляции

Синтаксические ошибки

int area(int length, int width);

int a1 = area(7; //Ошибка - пропущена скобка )
int a2 = area(7) //Ошибка - пропущена точка с запятой ;

При нахождение ошибки, комплитор может указать на строку которая не содержит ошибок, стоит проверить предшествующие строки программы.

Компилятор не знает, что именно вы пытаетесь сделать, потому что формулирует сообщения об ошибках с учетом того, что вы на самом деле сделали, а не ваших намерений. Например обнаружив ошибочное объявление переменной a3, компилятор вряд ли напишет что-то вроде:

  • "Вы неправильно написали слово int: не следует употреблять прописную букву i."

Скорее всего он выразится так:

  • Сисинтаксическая ошибка: пропущена ' ; ' перед идентификатором 'sЗ'"

  • "У переменной 'sЗ' пропущен идентификатор класса или типа"

  • "Неправильный идентификатор класса или типа Int"

Все эти сообщения можно перевести так:

  • "Перед переменной aЗ имеется синтаксическая ошибка, и надо что-то сделать либо с типом Int, либо с переменной aЗ."

Ошибки, связанные с типами

int area(int length, int width);

int a1 = arean(7); //Ошибка - необъявленная функция
int a2 = area(7); //Ошибка - неверное количество аргументов
int a3 = area("seve", 2); //Ошибка - первый аргумент имеет неверный тип

Не ошибки

int area(int length, int width);

int a1 = area(7, -5); // ОК - но почему у фигуры отрицательная сторона
int a2 = area(2.3, 4.4) // ОК - но будет вызвана area(2,4)
char a3 = area(100, 9999) // ОК - но результат будет усечен

Ошибки времени редактирования связей

  • Любая программа состоит из нескольких отдельно компилируемых частей, которые называют единицами трансляции(translation units).

  • Каждая функция в программе должна быть объявлена с одним и тем же типом во всех единицах трансляции, в которых она используется. Для этого используются заголовочные файлы.

  • Каждая функция должно быть определена в программе 1 раз.

Если хотя бы одно из этих правил нарушено, редактор связей сообщит об ошибке.

int area(int length, int width);

int main()
{
    int x = area(2,3); //Если мы подключим файл, где есть определение функции area(), тогда все хорошо, иначе ошибка
    // Кроме того в другом файле должна быть определена точно такая же функция
    // НЕ double area(int lenght, int width)
    // НЕ int area(int length, int width, int less)
}

Ошибки времени выполнения программы

int area(int length, int width) // вычисление площади прямоугольника
{
    return length*width;
}
int framed_area(int x, int y) // вычисление площади в пределах рамки
{
    return area(x–2,y–2);
}
int main()
{
    int x =1;
    int y = 2;
    int z = 4;
    // . . .
    int area1 = area(x,y);
    int area2 = framed_area(1,z);
    int area3 = framed_area(y,z);
    double ratio = double(area1)/area3; // преобразуем в тип double
                                        // чтобы выполнить деление с плавующей точкой
}
  • Приведеные вызовы функций возвращат отрицательные числа, присвоенные переменным area1, area2

  • Деление на ноль в последнем вычисление

Обработка ошибок в вызывающем коде

    if (x<=0) error("Неположительное x");
    if (y<=0) error("Неположительное y");
    int area1 = area(x,y);
    // функция error() останавливает программу, выдавая сообщение об ошибке

Если нет необходимости сообщать об ошибках в каждом из аргументов, код можно упростить:

    if (x<=0 || y<=0) error("non-positive area() argument");
    int area1 = area(x,y);

Для того, чтобы полностью защитить функцию area() от неправильных аргументов, необходимо исправить вызовы функции framed_area():

    if (z<=2)
        error("Неположительный второй аргумент функции  area() при вызове из функции framed_area()");
    int area2 = framed_area(1,z);
    if (y<=2 || z<=2)
        error("Неположительный аргумент функции area() при вызове из функции framed_area()");
    int area3 = framed_area(y,z);

Если изменится тело функции framed_area(), то наш код будет непригоден, его называют хрупким. Можем ввести переменную, чтобы этого избежать:

constexpr int frame_width = 2;
int framed_area(int x, int y)
{
    return area(x–frame_width,y–frame_width);
}
...
    if (1– frame_width<=0 || z–frame_width<=0)
        error("Неположительный аргумент функции area() при вызове из функции framed_area()");
    int area2 = framed_area(1,z);
    if (y– frame_width<=0 || z–frame_width<=0)
        error("Неположительный аргумент функции area() при вызове из функции framed_area()");
    int area3 = framed_area(y,z);

Код стал большим и сложным.

Обработка ошибок в вызываемом коде

int framed_area(int x, int y)
{
    constexpr int frame_width = 2;
    if (x–frame_width<=0 || y–frame_width<=0)
        error("non-positive area() argument called by framed_area()");
    return area(x–frame_width,y–frame_width);
}

Это решение выглядит неплохо и нам не нужно будет писать проверку для каждого вызова функций framed_area()

int area(int length, int width)
{
    if (length<=0 || width <=0) 
        error("non-positive area() argument");
    return length*width;
}

Почему проверка аргументов функции пишется не всегда?

  • Мы не можем модифицировать определение функции. Функция находится в библиотеке, которую по тем или иным причинам невозможно изменить. Возможно, она будет использована другими людьми, не разделяющими вашего подхода к обработке ошибок. Возможно, она принадлежит кому-то еще, и вы не имеете доступа к ее исходному коду. Возможно, она включена в постоянно обновляющуюся библиотеку, так что, изменив эту функцию, вы будете вынуждены изменять ее в каждой новой версии.

  • Вызываемая функция не знает, что делать при выявлении ошибки. Эта ситуация типична для библиотечных функций. Автор библиотеки может выявить ошибку, но только вы знаете, что в таком случае следует делать.

  • Вызываемая функция не знает, откуда ее вызвали. Получив сообщение об ошибке, вы понимаете, что произошло нечто непредвиденное, но не можете знать, как именно выполняемая программа оказалась в данной точке. И ногда необходимо. чтобы сообщение было более конкретным.

  • Производительность. Дпя небольшой функции стоимость проверки может перевесить стоимость вычисления самого результата. Например, в случае с функцией area ( ) проверка вдвое увеличивает ее размер (т.е. удваивает количество машинных инструкций, которые необходимо выполнить, а не просто длину исходного кода) . В некоторых программах этот факт может оказаться критически важным, особенно если одна и та же информация проверяется постоянно функциями, вызывающими одна другую, и передающими при этом информацию более или менее неизменной.

Что в итоге делать? Проверять аргументы функции, если у вас нет веских причин поступать иначе.

Сообщения об ошибках

Исключения

Основная идея исключений состоит в отделении выявления ошибки(что может сделать в вызываемой функции) от ее обработки(что можно сделать в вызывающей функции), чтобы гарантировать, что ни одна выявленная ошибка не останется необработанной. Если функция обнаруживает ошибку, которую не может обработать она не выполняет оператор return как обычно, а генерирует исключение с помощью инструкции throw, показывая, что произошло нечто неправильное. Любая функция, прямо или косвенно вызывающая данную функцию, может перехватить созданное исключение с помощью конструкции catch, т.е. указать, что следует делать, если вызываемый код вызвал и нструкцию throw. Функция выражает свою заинтересованность в перехвате исключений с помощью блока try, перечисляя виды исключений, которые она планирует обрабатывать в своих разделах catch блока try. Если ни одна из вызывающих функций не перехватила исключение, то программа прекращает работу. ЗАКОНСПЕКТИРОВАТЬ YANDEX C++ БЕЛЫЙ ПОЯС, МОЖЕТ ЧТО-ТО ЕЩЕ

Логические ошибки

Программа делает ровно то, что вы написали. Если вы ожидали другой результат и все предыдущие ошибки устранены, то нужно искать ошибку в логике программы.

Функции

Функция - это именованная последовательность инструкций.

int square(int x)
{
    return x*x; //тело функции
}

Функции нужны, чтобы:

  • избавиться от дублирования кода

  • выделить логические участки программы

  • упростить отладку

Объявления и определения

Объявление (declaration) - это инструкция, которая вводит имя в область видимости.

  • устанавливая тип именованной сущности (например, переменной или функции) и

  • (необязательно) выполняя инициализацию (например. указывая начальное значение переменной или тело функции).

    int a = 7; // Переменная типа int
    const double cd = 8.7; // Константа с плавающей точкой двойной точности
    double sqrt(double); // Функция с аргументом типа double, возвращающая тип double
    vector<Token> v; // Переменная вектор - объектов Token

Имя должно быть объявлено до использования в программе.

Объявлением считается только объявление, не являющееся определением. Определение устанавливает, на что именно ссылается имя . В частности, определение переменной выделяет память для этой переменной. Следовательно, ни одну сущность невозможно определить дважды.

double sqrt(double d) { /* . . . */ } // Определение
double sqrt(double d) { /* . . . */ } // ошибка - повторное определение
int a; // определение
int a; // ошибка - повторное определение

Объявление, которое не является одновременно определением, просто сообщает, как можно использовать имя; оно представляет собой интерфейс, но не выделяет памяти и не описывает тело функции.

Объявление переменной указывает ее тип, но лишь определение создает реальный объект (выделяет память) . Объявление функции также устанавливает ее тип (типы аргументов и тип возвращаемого значения), но лишь определение создает тело функции (выполняемые инструкции).

Обратите внимание на то, что тело функции хранится в памяти как часть программы, поэтому вполне корректным будет сказать, что определения функций и переменных выделяют память, а объявления - нет.

Разница между объявлением и определением позволяет разделить программу на части и компилировать их по отдельности. Объявления обеспечивают связь между разными частями программы; при этом не нужно беспокоиться об определениях. Поскольку все объявления должны быть согласованы друг с другом (включая единственное определение), использование имен во всей программе должно быть непротиворечивым.

Аргументы функций

Передача параметров по значению

Передается копия значения.

int f(int x)
{
    x = x+1; // Присваивание значения локальной переменной x
    return x;
}
int main()
{
    int xx = 0;
    cout << f(xx) << '\n'; // 1
    cout << xx << '\n'; // 0; f() не изменяет переменную
}

Передача параметров по константной ссылке

Чтобы не копировать значение, в функцию нужно передавать адресс. Если мы не хотим изменять значение то стоит передавать по константной ссылке.

void print(const vector<double>& v)
{
    cout << "{ ";
    for (int i = 0; i<v.size(); ++i) {
        cout << v[i];
        if (i!=v.size()–1) cout << ", ";
    }
    cout << " }\n";
}

Амперсант означает ссылку. Теперь операции будут производиться не над копией, а над самим вектором.

Вызов функции и возврат значения

Объявление аргументов и тип возвращаемого значения

int g(int double);//объявление
int g(int x, double y){ return x*y; } //определение

Передача параметров по значению

Передается копия значения.

int f(int x)
{
    x = x+1; // Присваивание значения локальной переменной x
    return x;
}
int main()
{
    int xx = 0;
    cout << f(xx) << '\n'; // 1
    cout << xx << '\n'; // 0; f() не изменяет переменную
}

Передача параметров по константной ссылке

Чтобы не копировать значение, в функцию нужно передавать адресс. Если мы не хотим изменять значение то стоит передавать по константной ссылке.

void print(const vector<double>& v)
{
    cout << "{ ";
    for (int i = 0; i<v.size(); ++i) {
        cout << v[i];
        if (i!=v.size()–1) cout << ", ";
    }
    cout << " }\n";
}

Амперсант означает ссылку. Теперь операции будут производиться не над копией, а над самим вектором.

Передача параметров по ссылке

int& - ссылка на переменную int.

int i = 7;
int& r = i; //r - ссылка на переменную i
r = 10; // i = 10
i = 12; // r = 12, i = 12

Если мы хотим изменять значение нужно передавать не по константной ссылке.

Проверка аргументов и преобразование типов

void f(int x);
void g(double x) 
{ f(x);} //произойдет сужающее преобразование
void h(double x);
void j(int x) { static_cast<double>(x);} //если мыы действительно хотим преобразовать переменную, лучше сделатьэто явно

Реализация вызова функции

douЬle expression ( Token_stream& ts)
{
    double left = term (ts);
    Token t = ts.get();
    // ...
}
double term(Token_stream& ts)
{
    double left = primary(ts);
    Token t = ts.get();
    // . . .
        case '/':
        {
            double d = primary(ts);
        // . . .
        }
    // . . .
}
double primary(Token_stream& ts)
{
    Token t = ts.get();
    switch (t.kind) {
    case '(':
        { 
            double d = expression(ts);
            // . . .
        }
        // . . .
    }
}

При вызове этих функций реализация языка программирования создает структуру данных содержащую копии всех ее параметров и локальных переменных.

Такую структуру данных называют зап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 double xscale = 10;
constexpr double yscale = 0.8;
constexpr Point scale(Point p) { return {xscale*p.x,yscale*p.y}; };

Функция, объявленная как constexpr. должна быть настолько простой, чтобы компилятор мог ее вычислить. В С++ 11 это означает, что функция, объявленная как constexpr, должна иметь тело, состоящее из одной инструкции return. в С++ 14 мы также можем написать простой цикл. соnstехрr-функция не может иметь побочные эффекты, т.е. она не может изменять значения переменных вне собственного тела, за исключением тех, которые она присваивает или использует для инициализации.

int gob = 9;
constexpr void bad(int & arg) // ошибка - нет возвращаемого значения
{
    ++arg; // ошибка - модифицируется значение переменной
    glob = 7; // ошибка - модифицируется нелокальная параменная
}

Если компилятор не в состоянии определить, что соns tехрr-функция является "достаточно простой" (в соответствии с подробными правилами стандарта), такая функция рассматривается как ошибка.

Потоки ввода и вывода

Ввод и вывод

Большинство современных операционных систем поручают управление устройствами ввода-вывода специализированным драйверам, а затем программы обращаются к ним с помощью средств библиотеки ввода-вывода. обеспечивающих максимально единообразную связь с разными источниками и адресатами данных. В общем случае драйверы устройств глубоко внедрены в операционную систему и недоступны для большинства пользователей, а библиотечные средства ввода-вывода обеспечивают абстракцию ввода-вывода. так что программист не должен думать об устройствах и их драйверах.

При использовании такой модели вся входная и выходная информация может рассматриваться как потоки байтов (символов), обрабатываемые средствами библиотеки ввода-вывода.

Наша работа как программистов, создающих приложения, сводится к следующему.

  1. Настроить потоки ввода-вывода для получения данных из устройств ввода и вывода данных на устройства вывода.

  2. Читать и записывать потоки.

Модель потока ввода-вывода

Стандартная библиотека языка С++ содержит определение типа istream для потоков ввода и типа ostream - для потоков вывода.

Поток ostream делает следующее:

  • Превращает значения разных типов в последовательности символов.

  • Посылает эти символы "куда-то" (например. на консоль, в файл, основную память или на другой компьютер) .

Буфер - это структура данных, которую поток ostream использует внутренне для хранения в ходе взаимодействия с операционной системой информации, полученной от вас. Задержка между записью в поток ostream и появлением символов в пункте назначения обычно объясняется тем, что эти символы находятся в буфере. Буферизация важна для производительности программы, а производительность программы важна при обработке больших объемов данных.

Поток istream делает следующее:

  • Превращает последовательности символов в значения разных типов.

  • Получает эти символы "откуда-то" (например. с консоли, из файла, из основной памяти или от другого компьютера).

Поток iostream делает то же что и ostream и istream

istream как и поток ostream для взаимодействия с операционной системой поток использует буфер. При этом буферизация может оказаться визуально заметной для пользователя. Когда вы используете поток istream, связанный с клавиатурой, все, что вы введете, останется в буфере. пока вы не нажмете клавишу Enter (возврат каретки/переход на новую строку). так что если вы передумали. то. пока не нажали клавишу Enter. можете стереть символы с помощью клавиши Backspace.

Файлы

При работе с файлами поток ostream преобразует объекты. хранящиеся в основной памяти, в потоки байтов и записывает их на диск. Поток istream действует наоборот: иначе говоря. он считывает поток байтов с диска и составляет из них объект.

Открытие файла

Поток ifstream - это поток istream для чтения из файла, поток ofstream - это поток ostream для записи в файл, а поток fstream представляет собой поток iostream, который можно использовать как для чтения, так и для записи. Перед использованием файловый поток должен быть связан с файлом

Чтение из поток

Создадим файл .txt и напишем программу, которая выводит консоль содержание файла:

hello world!
second line
ifstream input("hello.txt");
string line;
getline(input, line);
cout << line << endl;
getline(input, line);
cout << line << endl;
getline(input, line);
cout << line << endl;//в третий раз выведется второе значение тк достигнут конец файла. 
//Тк getline возвращает ссылку на поток, из которого берет данные. Поток можно привести
//к типу bool, причем false будет в случае, когда с потоком уже можно
//дальше не работать

//.............
//если файла hello.txt нет, то код будет работать, ошибок никаких не будет
//желательно чтобы программа должна говорить, что такого файла не существует
//метод is_open() возвращает true если файловый поток открыт и готов работать
if (input.is_open()){
    while (getline(input, line)) {
        cout << line << endl;
    }
    cout << "done!" << endl;
} else {
    cout << "error!" << endl;
}

//....................
//Поток можно приводить к типу bool причем значение true соответствует тому, 
//что с потоком можно работать в данный момент
//Поэтому код можно переписать
ifstream input("helol.txt");
string line;
if (input){
    while (getline(input, line)) {
        cout << line << endl;
    }
    cout << "done!" << endl;
} else {
    cout << "error!" << endl;
}

Чтение из поток до разделителя

Допустим, считать нужно дату из следующего текстового файла date.txt:

2017−01−25
ifstream input("date.txt");
string year;
string month;
string day;
if (input) {
    getline(input, year, '-');
    getline(input, month, '-');
    getline(input, day, '-');
}
cout << year << ' ' << month << ' ' << day << endl;

Оператор чтения из потока

Решим ту же самую задачу с помощью оператора чтения из потока ().

ifstream input("date.txt");
int year = 0;
int month = 0;
int day = 0;
if (input) {
    input >> year;
    input.ignore(1);//метода ignore принимает целое число — сколько символов нужно пропустить.
    input >> month;
    input.ignore(1);
    input >> day;
    input.ignore(1);
}
cout << year << ' ' << month << ' ' << day << endl;

Оператор записи в поток. Дозапись в файл.

const string path = "output.txt";
ofstream output(path);
output << "hello" << endl;

ifstream input(path);//проверим, что записалось в файл
if (input) {
    string line;
    while (getline(input, line)) {
        cout << line << endl;
    }
}

При каждом запустке программы файл записывается заново, то есть его содержимое удалялось и запись начиналась заново. Для того, чтобы открыть файл в режиме дозаписи, нужно передать специальный флажок ios::app

ofstream output(path, ios::app);
output << " world!" << endl;

Форматирование вывода. Файловые манипуляторы.

fixed - указывает, что числа далее нужно выводить на экран с фиксированной точностью.

cout << fixed;

setprecision - задает количество знаков после запятой.

cout << fixed << setprecision(2);

setw(set width) - указывает ширину поля, которое резервируется для вывода переменной.

cout << fixed << setprecision(2);
cout << setw(10);

setfill - указывает, каким символом заполнять расстояние между колонками.

cout << setfill('.');

left - выравнивание по левому краю поля.

cout << left;
void Print(const vector<string>& names,
           const vector<double>& values, 
           int width) 
{
    for (const auto& n : names) {
        cout << setw(width) << n << ' ';
    }
    cout << endl;
    cout << fixed << setprecision(2);
    for (const auto& v : values) {
        cout << setw(width) << v << ' ';
    }
    cout << endl;
}
int main()
{
    vector<string> names = {"a", "b", "c"};
    vector<double> values = {5, 0.01, 0.000005};
    cout << setfill('.');
    cout << left;
    Print(names, values, 10);
}
a......... b......... c.........
5.00...... 0.01...... 0.00......

ООП

Классы

Класс является абстрактным, если его можно использовать только в качестве базового класса. Как сделать класс абстрактным?

  1. Сделать конструкторы protected

     protected:
         Shape();
         Shape(initializer_list<Point> lst);
    
     Shape ss; //ошибка
  2. Добавить в него чисто виртуальные функции

Класс, который можно использовать для создания объектов, в противоположность абстрактному классу называется конкретным (concrete).

Порождение - это способ построения одного класса из другого так, чтобы новый класс можно было использовать вместо исходного

Типы, определенные пользователем

Тип называется встроенным, если компилятор знает, как представить объекты такого типа и какие операторы к нему можно применять без уточнений в виде объявлений которые создает программист в исходного коде. Типы не относящиеся к строенным называются пользовательскими(типы определенные пользователем, UTD - user-defined types)

Класс - это пользовательский тип, определяющий представление объектов этого класса, их создание, использование и кничтожение.

Классы и члены класса

Class X{
    public:
        int m;         //Член-данные
        int f(){}   // Функция-член
};

Для доступа к членам используют конструкцию имя_экземпляра_объекта.имя_члена

X x;
x.m = 5;
x.f();

Тип члена определяет, какие операции с ним можно выполнять.

Разработка класса

Структура и функции

struct Date{
    int y;
    int m;
    int d;
}

Date today;
today.y = 2005;
today.m = 12;
today.d = 24;

Конструктор - функция-член имя которой совпадает с именем класса.

struct Date{
    int y, m, d;
    Date(int y, int m, int d);
}

Date today{2007, 12, 24};
Date new_today = Date{2007, 12, 24};

Сокрытие деталей

class Date{
    public:
        Date(int y, int m, int d);
        int month() { return month; }
        int day()     { return day;   }
        int year()     { return year;     }
    private:
        int y, m, d;
};
Date date{2000, 12, 2};
date.y=5//ошибка

Модификаторы доступа нужны, чтобы ограничить доступ к членам.

Значение объекта называют состоянием(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) { ... }

При использовании списка инициализации, объект создается с переданными в конструктор значениями. Если инициализировать члены в теле конструктора, то они проинициализируются после создания. Если у нас члены `const` то с помощью списка инициализации мы сможем их проинициализровать собственными значениями.

Определение функции-члена в классе приводит к следующим последствиям.

* функция становится `inline`.
* При изменении тела встраиваемой функции-члена класса придется перекомпилировать заново все модули, в которых он используется. Если функция-член определена вне класса, то потребуется перекомпилировать только само определение класса
* Определение класса становится больше по размеру

То есть, если не нужна эффективность, нужно определять функции-члены вне объявления класса.

### Ссылка на текущий объект
```cpp
class Date {
    public:
        Date() {}
        int month() { return m; }

        private:
            int y, m, d;
};

void f(Date date1, Date d2)
{
    cout << date1.month() << date2.month();
}

Функция-член класса получают неявный аргумент, позволяющий идентифицировать объект для которого они называются.

Перегрузка операторов

Для класса или перечисления можно определить практически все операторы, существующие в языке С++. Этот процесс называют перегрузкой операторов (operator overloadlпg). Он применяется, когда требуется сохранить привычные обозначения для разрабатываемого нами типа

enum class Month {
    Jan=1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
};
Month operator++(Month& m) // prefix increment operator
{
    m = (m==Dec) ? Jan : Month(int(m)+1); // Циклические переход
    return m;
}

Month m = Sep;
++m; // Oct
++m; // Nov

Перегруженный оператор должен иметь хотя бы один операнд с пользовательским типом.

int operator(int ,int); // error
Vector operator+(const Vector&, const Vector&);

Интерфейсы классов

  • Интерфейс должен быть полным

  • Интерфейс должен быть минимальным

  • Класс должен иметь конструкторы

  • Класс должен поддерживать копирование(или явно запрещать его)

  • Следует предусмотреть тщательную проверку типов аргументов

  • Деструктор должен освобождать все ресурсы

Константные функции члены

const ознчает, что данный метод не будет модифицировать объект

int Date::day() const
{
    ++d; //ошибка
    return d;
}

Конструктор

Основные операции

Какие конструкторы должен иметь класс?

  • Конструкторы с одним или несколькими аргументами.

  • Конструктор по умолчанию.

  • Копирующий конструктор (копирование объектов одинаковых типов).

  • Копирующее присваивание (копирование объектов одинаковых типов).

  • Перемещающий конструктор (перемещение объектов одинаковых типов).

  • Перемещающее присваивание (перемещение объектов одинаковых типов).

  • Деструктор.

  • (не из книги) Конструктор с списком инициализации(initializer_list)

Явные конструкторы

Конструктор , имеющий один арrумент, определяет преобразование типа этого арrумента в свой класс. Это может оказаться очень полезным.

class complex {
    public:
        complex(double); // Преобразование double в complex
        complex(double,double);
        // . . .
};
complex z1 = 3.14; // OK: преобразует 3.14 в (3.14,0)
complex z2 = complex{1.2, 3.4};

Однако неявные преобразования еле.дует применять редко и осторожно, поскольку они могут вызвать неожиданные и нежелательные эффекты . Например, наш класс vector, определенный выше, имеет конструктор , принимающий аргумент типа int, откуда еле.дует, что он определяет преобразование типа int в класс vector:

class vector {
    // . . .
    vector(int);
    // . . .
};
vector v = 10; // Создаем вектор из 10 double
v = 20; // Присваиваем вектору v новый вектор из 20 double

void f(const vector&);
f(10); // eh? Calls f with a new vector of 10 doubles
  • Когда создается объект класса х, вызывается один из его конструкторов.

  • Когда уничтожается объект типа х, вызывается его деструктор.

Деструктор вызывается всегда, когда уничтожается объект класса; это происходит, когда объект выходит из области видимости, программа завершает работу или к указателю на объект применяется оператор delete. Некоторый подходящий конструктор вызывается каждый раз. когда создается объект класса; это происходит при и нициализации переменной, при создании объекта с помощью оператора new (за исключением встроенных типов), а также при копировании объекта.

Векторы и динамически выделяемая память

Указатель - это адрес ячейки памяти.

int x = 17;        //резервируем участок памяти размером типа int, для хранения переменной x и записываем туда число 17
int *ptr = &x;    //указатель ptr хранит адрес переменной x
*ptr = 27;        //операция разыменования, присваиваем 27 переменной типа int, на которую указывает ptr
sizeof(x);        //4
sizeof(int);    //4

Динамически распределяемая память и указатели

Когда начинается выполнение программы, написанной на языке С++, компилятор компилирует память под код (иногда эту память называют сегментом кода) и глобальные переменные (эту память называют сегментом данных).

Кроме того, выделяется память, которая будет использоваться при вызове функций для хранения их аргументов и локальных переменных (эта память называется стеком) . Остальная память компьютера может использоваться для других целей; она называется динамически распределяемой или просто динамической.

Язык С++ делает эту динамическую память (которую также называют кучей (heap)) доступной с помощью оператора new:

double *p = new double[4];//Размеща ем 4 числа douЫe в динамической памяти

Указанная выше инструкция просит систему поддержки выполнения программ разместить четыре числа типа douЫe в динамической памяти и вернуть указатель на первое из них. Этот указатель используется для инициализации переменной р.

Оператор new возвращает указатель на объект. который он создал . Если оператор new создал несколько объектов (массив), то он возвращает указатель на первый из этих объектов. Если этот объект имеет тип х. то указатель, возвращаемый оператором new, имеет тип Х*:

char *q = new double[4];//ошибка - указатель double присваивается переменной char*

Данный оператор new возвращает указатель н а переменную типа douЫe, но тип douЫe отличается от типа char, поэтому мы не должны (и не можем) присвоить указатель на переменную типа douЫe указателю на переменную типа char.

Размещение в динамической памяти

Оператор new выполняет выделение (allocation) динамической памяти (free store).

  • Оператор new возвращает указатель на выделенную память.

  • Значением указателя является адрес первого байта выделенной памяти.

  • Указатель указывает на объект определенного типа.

  • Указатель не знает на какое количество элементов он указывает

Оператор new может выделять память как для отдельных элементов, так и для последовательности элементов:

int *pi = new int;//Выделяем память для одной переменной int
int *qi = new int[n];//Выделяем память для n переменных int(массив)
double pd = new double;//Выделяем память для одной переменной double

Количество объектов может задаваться пеменной. Это важно, поскольку позволяет нам выбирать для какого количества объектов можно выделять память во время выполнения программы. Если n=2n=2

Указатели на объекты разных типов имеют разные типы.

pi = pd;//ошибка - нельзя присвоить тип doule* указателю int*
pd = pi;//ошибка - нельзя присвоить тип int* указателю douЬle*

Почему нельзя? Мы же можем присвоить переменную типа int переменной типа double и наоборот. Причина в операторе []. ДЛя того чтобы найти нужный элемент, он использует информацию о размере его типа. Например, элемент qi[2] находится на расстоянии, равном двум размерам типа int от элемента qi[0] , а элемент qd[2] находится на расстоянии, равном двум размерам типа douЫe от элемента qd[0]. Если размер типа int отличается от размера типа douЬle, как во многих компьютерах, то, разрешив указателю qi указывать на память, выделенную для адресации указателем qd, можем получить довольно странные результаты.

Это объяснение с практической точки зрения. С теоретической точки зрения ответ таков: присваивание друг другу указателей на разные типы сделало бы возможными ошибки типа (type errors).

Доступ с помощью указателей

Кроме оператора разыменования *. к указателю можно применять оператор индексирования []:

double *p = new double[4];//выделение памяти для 4 обхектов типа double
double x = *p;//присваиваем первый объект массива с помощью указателя p
double y = p[2];//присваиваем третий объект с помощью указателя p
*p = 7.7;//записываем в первый объект число типа double
p[2] = 9.9;//записываем в третий объект число типа double

Когда оператор [ ] применяется к указателю р. он интерпретирует память как последовательность объектов (имеющих тип, указанный в объявлении указателя) , на первый из которых указывает указатель р.

Диапазоны

Основная проблема, связанная с указателями, заключается в том, что указатель не знает, на какое количество элементов он указывает.

double *pd = new double[3];
pd[2] = 2.2;
pd[4] = 4.4;
pd[-3] = -3.3;

И меется ли третий элемент там, куда указывает указатель pd? Можно ли обращаться к пятому элементу pd [ 4 ] ? Если мы посмотрим на определение указателя pd, то ответим "да" и "нет" соответственно. Однако компилятор об этом не знает; он не отслеживает значения указателей. Наш код просто обращается к памяти так, будто она распределена правильно. Компилятор даже не возразит против выражения pd [ -3 ] . как будто можно разместить три числа типа douЫe перед элементом, на который указывает указатель pd.

Нам не известно, что собой представляют ячейки памяти, на которые указывают выражения pd [ -3 ] и pd [ 4 ] . Однако м ы знаем. что они не могут использоваться как часть нашего массива, в котором хранятся три числа типа douЬle, на которые указывает указатель pd. Вероятнее всего, они являются частью других объектов, и мы просто заблудились. Это плохо . Это катастрофически плохо. Здесь слово "катастрофически" означает либо "моя программа почему-то завершилась аварийно", либо "моя программа выдает неправильные ответы". Выход за пределы допустимого диапазона представляет собой особенно ужасную ошибку, поскольку очевидно, что при этом опасности подвергаются данные, не имеющие отношения к нашей программе. Считывая содержимое ячейки памяти, находящейся за пределами допустимого диапазона, мы получаем случайное число, которое может быть результатом совершенно других вычислений. Выполняя запись в ячейку памяти, находящуюся за пределами допустимого диапазона, можем перевести какой-то объект в "невозможное" состояние или просто получить совершенно неожиданное и неправильное значение. Такие действия, как правило. остаются незамеченными достаточно долго, поэтому их особенно трудно выявить. Что еще хуже: дважды выполняя программу, в которой происходит выход за пределы допустимого диапазона, с немного разными входными данными, мы можем прийти к совершенно разным результатам . Ошибки такого рода ("неустойчивые ошибки") выявить труднее всего.

Инициализация

double* р0 ; // Объявление без инициализации: возможны проблемы
double* p1 new double ; // Выделение памяти для переменной типа double без инициализа ции
double* р2 new double (5.5) ; // Инициализируем переменную типа double числом 5 . 5
double* р3 = new double [5] ; // Выделение памяти для массива из пяти douЫ e без инициализа ции

Очевидно, что объявление указателя рО без инициализации может вызвать проблемы . Рассмотрим пример.

*р0 = 7.0 ;

Эта инструкция записывает число 7.0 в некую ячейку памяти. Мы не знаем, в какой части памяти расположена эта ячейка. Это может быть безопасно, но рассчитывать на это нельзя. Рано или поздно мы получим тот же результат, что и при выходе за пределы допустимого диапазона: программа аварийно завершит работу или выдаст неправильные результаты.

Мы можем указать список инициализации для массивов объектов, память для которых выделяется оператором new, например:

double* p4 = new double[5] {0,1,2,3,4};
double* p5 = new double[] {0,1,2,3,4};

Когда мы используем собственные типы. мы получаем больший контроль над инициализацией. Если класс Х имеет конструктор по умолчанию, то мы получим следующее:

X *p1 = new X; //один объект класса X, инициализированный по умолчанию
X* p2 = new X[17]; //17 объектов класса X, инициализированных по умолчанию

Если тип У имеет конструктор, но не конструктор по умолчанию, мы должны выполнить явную инициализацию:

Y* py1 = new Y; // error: no default constructor
Y* py2 = new Y{13}; // OK: initialized to Y{13}
Y* py3 = new Y[17]; // error: no default constructor
Y* py4 = new Y[17] {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};

Нулевой указатель

Если в вашем распоряжении нет другого указателя, которым можно было бы инициализировать ваш указатель, используйте нулевой указатель nullptr.

double *p0 = nullptr;

При присваивании указателю нулевого значения последнее носит название нулевого указателя (null pointer). и зачастую корректность указателя (т.е. то, что он указывает на что-то) проверяется с помощью сравнения его значения с nullptr:

if (р0 ! = nullptr) // Проверка корректности указателя p0
if(p0) //эквивалентно конструкции выше

Освобождение памяти

Оператор new выделяет участок динамической памяти. Поскольку память компьютера ограничена, неплохо было бы возвращать память обратно, когда она станет больше ненужной. В этом случае освобожденную память можно было бы использовать для хранения других объектов. Для больших и долго работающих программ такое освобождение памяти играет важную роль.

double *calc(int res_size, int max)
{
    double *p = new double[max];
    double *res = new double[res_size];
    return res;
}
double *r = calc(100, 1000);

При работе такой программы каждый вызов функции calc () будет "терять" память, выделяемую под массив элементов douЬle, адрес которого присваивается указателю р. Например, вызов calc ( 100 , 1000) сделает недоступным для остальной части программы участок памяти, на котором может разместиться тысяча переменных типа douЫe.

Оператор, освобождающий память, называется delete. Для того чтобы освободить память для дальнейшего использования, оператор delete следует применить к указателю, который был возвращен оператором new:

double *calc(int res_size, int max)
{
    double *p = new double[max];
    double *res = new double[res_size];
    delete[] p;
    return res;
}
double *r = calc(100, 1000);
delete[] r;

Оператор delete имеет две разновидности:

  • delete р освобождает память, выделенную с помощью оператора new для отдельного объекта;

  • delete [ ] р освобождает память, выделенную с помощью оператора new для массива объектов.

Двойное удаление объекта - очень грубая ошибка. Рассмотрим пример.

int* р = new int ( S ) ;
delete р ; // р указывает на объект, созданный опера тором new
// Указа тель здесь больше не используется
delete р ; // Ошибка : р ука зывает на память , принадлежащую
// диспетчеру динамической памяти

Вторая инструкция delete р порождает две проблемы.

  • Вы больше не владеете объектом, поэтому диспетчер динамической памяти может изменить внутреннюю структуру данных так, что выполнить инструкцию delete р правильно во второй раз будет невозможно.

  • Диспетчер динамической памяти может повторно использовать память, на которую указывал указатель р, так что теперь указатель р указывает на другой объект: удаление этого объекта (принадлежащего другой части программы) приведет к ошибке.

Удаление нулевого указателя не приводит ни к каким последствиям (так как нулевой указатель не указывает н и на один объект), поэтому эта операция безвредна:

int* р = О ;
delete р ; // Отлично : никаких действий не нужно
delete р ; // То же самое - по-прежнему ника ких действий

Почему следует избегать утечки памяти? Программа, которая должна работать "бесконечно" , не должна допускать никаких утечек памяти . Примерами таких программ являются операционная система, а также большинство встроенных систем

Деструкторы

Вызываются при кничтожение объектов, удобно удалять ресурсы, если класс имеет вирутальную функцию, то но должн иметь виртуальный деструктор, поскольку он является базовым.

Указатели на объекты класса

vector* f(int s)
{
    vector* p = new vector(s); // Выделяем динамическую память для вектора
    // заполняем *p
    delete p;
    return p;
}
void ff()
{
    vector* q = f(4);
    // используем *q
    delete q; // освобождаем динамическую память
}

Когда мы удаляем объект класса vector с помощью оператора delete, вызывается деструктор класса vector.

При создании объекта класса vector в динамической памяти оператор new выполняет следующие действия:

  • сначала выделяет память для объекта класса vector;

  • затем вызывает конструктор класса vector, чтобы инициализировать объект; этот конструктор выделяет память для элементов объекта класса vector и инициализирует их.

Удаляя объект класса vector, оператор delete выполняет следующие действия:

  • сначала вызывает деструктор класса vector; этот деструктор, в свою очередь, вызывает деструкторы элементов (если таковые имеются), а затем освобождает память, занимаемую элементами вектора;

  • затем освобождает память, занятую объектом класса vector.

Все классы поддерживают оператор -> (стрелка) для доступа к своим членам с помощью указателя на объект:

Путанца с типами: void* и операторы приведения типов

Тип void* означает "указатель на ячейку памяти, тип которой компилятору неизвестен". Он используется тогда, когда необходимо передать адрес из одной части программы в другую. причем каждая из них ничего не знает о типе объекта, с которым работает другая часть. Примерами являются адреса, служащие аргументами функций обратного вызова, а также распределители памяти самого нижнего уровня (такие, как реализация оператора new).

Указателю типа void* можно присвоить указатель на любой объект, например:

void* pvl = new int ; //int* превращается в void*
void* pv2 new douЬle [10]; //douЬle* превращается в void*

Поскольку компилятор не знает что такое void*, мы должен ему сказать

void f (void* pv)
{
    void* pv2 = pv ; // Правильно (тип void* для этого и предназна чен)
    double* pd = pv ; // Ошибка : невозможно привести тип void* к double *
    *pv = 7 ; // Ошибка: невозможно разыменовать void * (тип объекта , на который указывает pv, неизвестен)
    pv[2] = 9 ; // Ошибка : void* нельзя индексировать
    int* pi = static_cast<int*>(pv) ; // ОК: явное преобразование
    //...

Оператор static_cast позволяет явно преобразовать указатели связанных типов один в другой, например такие, как void или douЫe. Имя static_cast - это сознательно выбранное отвратительное имя для отвратительного (и опасного) оператора, который следует использовать только в случае крайней необходимости. Его редко можно встретить в программах (если он вообще используется) . Операции, такие как static_cast, называют явным преобразованием типа (explicit type conversion) или просто приведением (cast).

В языке С++ предусмотрены два оператора приведения типов, которые потенциально еще хуже оператора static_cast.

  • Оператор reinterpret_cast может преобразовать тип в совершенно другой, никак не связанный с ним, например int в douЫe*.

  • Оператор const cast позволяет отбросить квалификатор const.

Reqister* in = reinterpret_cast<Reqister*>(Oxff);
//сообщаем компилятору. что определенная
//часть памяти (участок. начинающийся с ячейки OxFF) рассматривается
//как объект класса Reg i s ter (возможно , со специальной
//семантикой) . Такой код необходим, например. при разработке драйверов
//устройств.
void f (const Buffer* р)
{
    Buffer* Ь = const_cast<Buffer*>(p);
    //анулируется квалификатор const
// ...

Лучше избегать всяких кастов

Указатели и ссылки

Ссылку (reference) можно интерпретировать как автоматически разыменовываемый постоянный указатель или альтернативное имя объекта. Указатели и ссылки отличаются следующими особенностями.

  • П рисвоение чего-либо указателю изменяет значение указателя, а не объекта, на который он указывает.

  • Для того чтобы получить указатель, как правило, необходимо использовать оператор new или &.

  • Для доступа к объекту, на который указывает указатель, используются операторы * и [ ] .

  • Присвоение ссылке нового значения изменяет значение объекта, на который она ссылается, а не саму ссылку.

  • После инициализации ссылку невозможно перенаправить на другой объект.

  • Присваивание ссылок выполняет глубокое копирование (новое значение присваивается объекту. на который указывает ссылка); присваивание указателей не использует глубокое копирование (новое значение присваивается указателю, а не объекту) .

  • Нулевые указатели представляют опасность.

int x = 10;
int* p = &x;     // you need & to get a pointer
*p = 7;          // use * to assign to x through p
int x2 = *p;     // read x through p
int* p2 = &x2;  // get a pointer to another int
p2 = p;         // p2 and p both point to x
p = &x2;        // make p point to another object
...
int y = 10;
int& r = y;     // the & is in the type, not in the initializer
r = 7;             // assign to y through r (no * needed)
int y2 = r;     // read y through r (no * needed)
int& r2 = y2;     // get a reference to another int
r2 = r;          // the value of y is assigned to y2
r = &y2;         // error: you can’t change the value of a reference
                 // (no assignment of an int* to an int&)

Обратите внимание на последний пример; это значит не только то, что эта конструкция не работает, - после инициализации невозможно связать ссылку с другим объектом . Если вам нужно указать на другой объект, используйте указатель.

Как ссылка, так и указатель основаны на адресации памяти . Они просто используют адреса по-разному, чтобы предоставить программисту несколько разные возможности .

Указатели и ссылка как параметры функций

Если вы хотите изменить значение переменной значением, вычисленным функцией, можно использовать три варианта:

int incr_v(int x) { return x+1; }     // compute a new value and return it
void incr_p(int* p) { ++*p; }         // pass a pointer
                                    // (dereference it and increment the result)
void incr_r(int& r) { ++r; }         // pass a reference

Какой выбор сделать? Нам кажется, что возврат значения чаще приводит к более очевидному (а значит, менее подверженному ошибкам) коду:

int x = 2;
x = incr_v(x); // copy x to incr_v(); then copy the result out and assign it

Этот стиль предпочтительнее для небольших объектов, таких как тип 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