План лекции
|
- Классы и объекты
- Цель: скрыть детали, чтобы не заниматься слишком низкоуровневыми вещами
- Пример: одномерный массив
- Задачи: не забыть создать, не забыть стереть, не перепутать размер, не выйти случайно за границы
- Шаг 1: 2 поля (данные + размер)
- Шаг 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;
}