План лекции
  • Классы и объекты
    • Цель: скрыть детали, чтобы не заниматься слишком низкоуровневыми вещами
    • Пример: одномерный массив
    • Задачи: не забыть создать, не забыть стереть, не перепутать размер, не выйти случайно за границы
    • Шаг 1: 2 поля (данные + размер)
      • Префикс my
    • Шаг 2: конструктор, деструктор
      • Автоматический вызов деструктора
    • Шаг 3: методы для доступа
      • Проверки границ массива
    • Шаг 4: права доступа
    • Доступ с помощью лома: private в C++ vs. private в java
    • Новые проблемы вместо решенных
    • Д/з: написать то, что было на лекции


Разбор теста №2

Задача №1: Как вы думаете, что сделает вызов функции malloc(-1)? Почему он сделает именно это?
Ответ: В результате вызова malloc(-1) число -1 будет преобразовано к формальному типу аргумента, а имеено к size_t.
-1, приведенная к беззнаковому типу size_t, в представлении компьютера есть самое большое число, которое помещается в size_t
Таким образом, malloc() попытается выделить память, равную размеру оперативной памяти -1(в байтах, но этого сделать не сможет и вернет NULL.

Задача №2: Напишите возможно более эффективный код функции char *strchr(char *str, char ch).
Эта функция возвращает указатель на первое вхождение символа ch в строке str или NULL, если такого символа в строке нет.
Ответ:
char* strchr(char *str, char ch)
{
    for (;*str != '\0'; ++str)
    {
        if(*str == ch)
	  {
            return str;
        }
    }
    return NULL;
}
Задача №3: Напишите код для поиска второго вхождения символа в строку. Этот код должен использовать функцию strchr и не должен содержать циклов.
Ответ:
char *first;
char *second = NULL;
first = strchr(str, ch);
if (first != NULL)  < - если first указывает на NULL - это значит, что символ ch не был найден и искать его второе вхождение не нужно
{
    second = strchr(first+1, ch); < -  здесь мы передаем в качестве параметра указатель на элемент строки, следующий за первым вхождением символа ch
}
Задача №4: В языке C++, в отличие от C, невозможно скомпилировать вызов функции, объявление (или определение) которой недоступно в момент компиляции.
Это связано с наличием в C++ ссылок. Объясните, в чем тут дело.
Ответ: В языке С следующая строчка "foo(a,b)" интерпретируется компилятором однозначно:
положить на стек a, b, адрес возврата и выполнить функцию foo.
Что касается языка С++, то в нем это может означать,что в стек надо положить либо значения переменных a и b, либо их адреса, что определяется только в объявлении функции.

Note: в языке С тоже существует пример, когда при отсутствии объявления функции программа скомпилируется,но будет работать некорректно.
foo(int a); - объявление функции foo
foo('a'); - вызов
В этом случае компилятор сделает предположение о типе аргумента,приняв его за char.
Кроме того, в случае если функция не объявлена, компилятор предположит, что тип ее возвращаемого аргумента - int.

Задача №5: Напишите код, который выделяет в куче место для двумерного массива размера N *N, воспользовавшись при этом не более чем 2 вызовами new.
Ответ:

 int **M = new int*[N];
 int *tmp = new int[N*N];
 for (int i = 0; i < N; ++i)
 {
     M[i] = tmp + i*N;
 }
  
 
Задача №6: Напишите код, который освобождает память, выделенную в предыдущей задаче.
Ответ:
delete[] tmp;
delete[] M;
в случае если переменная tmp не видна в момент удаления,то освобождать память нужно следующим образом: delete[] M[0];
delete[] M;
В этой ситуации порядок вызовов функции delete должен быть именно такой, т.к после удаления массива M мы не можем обратиться к его нулевому элементу M[0].

Классы и объекты.

Язык С достаточно эффективен и хорош для компьютера, однако не всегда хорош для программиста. Ведь пара "незначительных" ошибок или просто опечаток в языке Си может привести к фатальным последствиям.
Язык C++ позволяет сохранить эффективность, но при этом является более благосклонным к программистам.
Цель создания "C with classes": скрыть детали, чтобы не заниматься слишком низкооуровневыми вещами и, как следствие, избежать "глупых" ошибок.

Рассмотрим возможные варианты "незначительных" ошибок на примере одномерного массива.

int *array = new int[size];
array[i] = ...
...
delete[] array;
В результате невыполнения одного из этих пунктов программа скомпилируется, но работать будет не так как ожидалось или совсем не будет

Note: пока на языке С писали специалисты, то им держать в голове все эти нюансы не составляло труда. Однако программирование зачастую является лишь инструментом,который используют ученые, инженеры и т.д. И они не дожны все время думать об этом.

Язык С++ позволяет избавиться от всех этих проблем с помощью новых специальных типов - классов.
Рассмотрим это на примере класса Array.
Пусть запись
Array a(10);
будет означать завести новую переменную a - массив длины 10.

Шаг 1:

В файле array.h заведем класс Array c 2 полями:
class Array
{
    int mySize;        < - размер массива
    int *myData;        < - массив
}; 
Note: Хорошим стилем считается в своих классах имена всех переменных начинать с "my", чтобы не возникало путаницы своих и чужих классов
Теперь, если у нас есть объект а, то мы можем узнать его размер и доступиться к полям массива следующим образом:
Array a;
 ... = a.mySize;
a.myData = ...
Таким образом мы решили задачу №3: не перепутать размер массива.

Шаг 2:

Теперь добавим в наш класс специальные методы: конструктор и деструктор.
Array( int size); < -  конструктор
~Array(); < - деструктор
Конструктор будет корректно отводить место в памяти под объект и инициализировать переменные класса. Деструктор будет вызываться автоматически при удалении объекта, например, при закрытии фигурных скобок, в которых был инициализирован объект, и правильно освобождать память.
Реализации методов опишем в файле array.cpp:
Array :: Array(int size)
{
    mySize = size;
    myData = new int[mySize];
}

Array :: ~Array()
{
    delete[] myData;
}
С спомощью конструктора и деструктора мы решили проблемы 1 и 4.

Шаг 3:

Для решения проблемы корректного обращения к элементам массива напишем методы get и set, которые будут проверять, что действия "положить элемент в массив" и "получить элемент из массива" обращаются к индексам в пределах массива.
int Array :: getValue(int index)
{
    if((index < 0) || (index >= mySize))
    {
        return -1;
    }
    return myData[index];
}

void Array :: setValue(int index, int value)
{
    if ((index < 0) || (index >= mySize))
    {
        return ;
    }
    myData[index] =  value;
}
Comments: внутри if в методах get и set подразумевается любое адекватное действие для случая некорректного обращения к массиву.
Итак, почти все "обещания" до некоторой степени выполнены.Теперь наша цель сделать так, чтобы "глупости" просто нельзя было совершить при работе с данным объектом.

Шаг 4: Права доступа.

У всех членов класса (члены класса - поля и методы) есть права доступа.По умолчанию все они private, т.е. доступны только внутри своего класса.
Еще существует тип доступа public - доступный всем. В момент объявления класса каждому члену приписываются права доступа: (Раньше в этом был небольшой обман)
class Array
{
private:
    int mySize;
    int *myData;
public:
    Array();
    ~Array();
    int getValue(int index);
    void setValue(int index ,int ch);
};
Note: Хороший стиль программирования подобных классов подразумевает объявление всех полей как private, а всех методов как public.

В последнем варианте объявления класса Array мы потеряли возможности изменять массив myData и переменную mySize напрямую, а значит застраховали себя от неправильного изменения этих полей, ведь теперь все операции с полями класса могут выполняться лишь с помощью наших методов get и set.
Однако для того,чтобы узнать длину массива,нам придется завести метод int getSize();

int Array :: getSize()
{
    return mySize;
}
Comments:единственный минус в выделении функции getSize() вместо прямого обращения - это быстродействие. Мы позже обсудим, что с этим делать.

Вывод.

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

Итоговый вариант класса Array

array.h

class Array
{
private:
    int mySize;
    int *myData;
public:
    Array();
    ~Array();
    int getValue(int index);
    void setValue(int index ,int ch);
    int getSize(); 
};

array.cpp

Array :: Array(int size)
{
    mySize = size;
    myData = new int[mySize];
}

Array :: ~Array()
{
    delete[] myData;
}

int Array :: getValue(int index)
{
    if((index < 0) || (index >= mySize))
    {
        return -1;
    }
    return myData[index];
}

void Array :: setValue(int index, int value)
{
    if ((index < 0) || (index >= mySize))
    {
        return ;
    }
    myData[index] =  value;
}

int Array :: getSize()
{
    return mySize;
}