1 Классы и управление доступом
1.1 Понятие класса
Класс можно понимать как:
- Развитие структуры языка С.
- Представление некоторой концепции, понятия.
Преимущества классов
- Класс - определяемый пользователем тип
- Новые типы настолько же удобны в использовании, как и встроенные
- Программы легче понимать и изменять
- Проще анализировать код и обнаруживать ошибки
- Отделение деталей реализации от интерфейса
Формула класса
Существует определённая терминология, связанная с классом
- Член класса - любой элемент данных или функция, описанная в классе;
- Метод - функция, описанная в классе;
- Объект, или экземпляр класса - переменная, созданная на основе класса;
Устройство класса
Рассмотрим общий синтаксис класса
class имя_класса { private: закрытые данные закрытые методы protected: защищенные данные защищенные методы public: открытые данные открытые методы };
1.2 Класс Counter
Класс ''счетчик''
В качестве первого примера приведем класс Counter (''счетчик'')
Описание класса Counter
typedef unsigned short ushort; // псевдоним для краткости
class Counter {
private:
ushort value; // значение счётчика
public:
void set(ushort); // установить
ushort get(); // прочитать
void inc(); // увеличить на 1
void dec(); // уменьшить на 1
};
Тело класса должно заключаться в фигурные скобки, после которых стоит точка с запятой. Классы могут содержать не только прототипы (заголовки) функций, но и их полные определения. Переменные, объявленные внутри класса, принадлежат этому классу. Объявления двух классов с одинаковыми именами не допускается.
Данные и заголовки функций (методов класса) могут располагаться в нескольких разделах. Начало каждого раздела вводится одним из трех ключевых слов: private, public, protected. Раздел private начинается сразу же после открытия тела класса, поэтому его можно специально не указывать. В данный раздел помещаются члены, доступ к которым извне запрещен и может осуществляться только через методы класса. Раздел public содержит заголовки интерфейсных функций, которые можно вызывать пользователям объекта. Назначение раздела protected будет объяснено в следующей лекции.
Переменная, определенная в классе, имеет область видимости от точки объявления до конца объявления класса. Класс может иметь произвольное количество переменных - членов.
В дополнению к описанию класса, носящему названию интерфейса, прилагается описание функций, прототипы (заголовки) которых приводятся в классе
Реализация методов класса
void Counter::set(ushort val) { value=val; }
ushort Counter::get() { return value; }
void Counter::inc() { value++; }
void Counter::dec() { value--; }
Это описание носит название реализации функций класса. Через оператор :: показывается принадлежность данных или методов конкретному классу.
Данный класс демонстрирует идею счетчика, который может изменять свое состояние (текущее значение) при обращении к ему посредством вызовов методов set(), inc(), dec(). В нашей реализации значение счетчика может принимать любые значения, допустимые для диапазона ushort. Если требуется ограничить значение числом N, то требуется изменить реализацию некоторых методов:
Модификация метода set
void Counter::set(ushort val) {
if(val<=N)
count=val;
else
count=0;
}
void Counter::inc() {
if(count+1<=N) count++;
}
Другим решением явилось бы введение специальной внутренней переменной, в которую при возникновении ошибки заносилось специальное значение.
Хранение флага ошибки
class Counter {
private:
ushort value;
bool error;
public:
void set(ushort);
ushort get();
void inc();
void dec();
bool ifError();
void reset();
};
Добавление новых возможностей приводит к расширению интерфейса класса:
Обработка ошибок
bool Counter::ifError() { return error; }
void Counter::reset() { error=false; }
Метод инициализации необходим для переключения значения флага ошибки.
Использование класса
После описания класса и реализации его методов можно создавать объекты и использовать их
Пример 1. Статические переменные и массивы
Создание статических экземпляров
Counter people; // статический одиночный экземпляр
Counter animals[100]; // статический массив объектов
Здесь создается объект people. Для доступа к методам используется операция "точка"
Использование статических экземпляров
people.set(10);
people.inc();
animals[i].inc();
Пример 2. Динамические переменные и массивы
Создание статических экземпляров
Counter *cities = new Counter; // динамический объект
Counter *roads = new Counter[100];// динамический массив
Динамические объекты создаются в процессе работы программы и размещаются в динамической памяти. Для доступа к членам класса используется оператор ''стрелка''
Использование динамических экземпляров
cities->set(1);
roads[8].set(50);
cities->inc();
Организация консольной программы
Проект консольного приложения С++ обычно содержит три файла:
- заголовочный файл с интерфейсом класса;
- исходный файл с реализацией методов класса;
- исходный файл с текстом программы (содержит main).

Рассмотрим пример создания демонстрационной программы для работы с классом Counter.
Создадим три файла, которые будут размещаться в одном каталоге проекта counter_demo
- Counter.h - заголовочный файл, в который мы помещаем описание класса Counter.
- Counter.cpp - файл с реализацией функций (методов) класса Counter.
- main.cpp - файл с демонстрацией работы класса (содержит функцию main.
Файлы Counter.cpp и main.cpp содержат включение заголовочного файла Counter.h
Построение программы counter_demo происходит в несколько этапов
- Благодаря препроцессору, в исходные файлы (.cpp) помещается содержимое заголовочного файла с описанием класса.
- Каждый из исходных файлов подвергается компиляции, в результате которой образуются объектные файлы (.obj).
- Компоновщик производит сборку объектных файлов с кодом из библиотек, необходимых для работы программы.
- Полученный exe-файл готов к использованию.
1.3 Члены класса и управление доступом
Stack.h
class Stack {
public:
void create_stack(int size);
void destroy_stack();
void push(char);
char pop();
int get_max_size();
int get_current_size();
private:
int maxSize;
char* storage;
int top;
};
Date.h
class Date {
public:
void init_date(int d, int m, int y);
void next_date();
int get_day();
int get_month();
int get_year();
private:
int day, month, year;
void set_day(int);
void set_month(int);
void set_year(int);
};
Stack.cpp
void Stack::create_stack(int size)
{
maxSize=size;
storage=new char[maxSize];
top=0;
}
void Stack::push(char c)
{
if (top<maxSize)
storage[top++] = c;
}
char Stack::pop()
{
return top ? storage[--top] : 0;
}
{
push(‘a’); // !
Stack stack;
stack.create_stack(100);
stack.push(‘a’);
char c = stack.pop();
stack.maxSize = 200; // !
stack.destroy_stack();
Stack *p= new Stack;
p->create_stack(300);
p->push(‘c’);
p->top = 0; // !
}
{
Date date;
date.init_date(20, 08, 2007);
date.set_day(35); // !
Date *d = &date;
d->get_month();
d->month = 13; // !
d->set_month(13); // !
}
2 Специальные члены класса
Если вернуться к рассмотрению класса Counter, то можно обратить внимание на следующее обстоятельство: после создания экземпляра класса, значение счетчика не определено. Это опасно, так как если написать код
Counter c;
c.inc();
то произойдет сбой, поскольку значение переменнай count внутри объекта не определено. Правильно будет вызвать функцию set() для установки значения. Но пользователи класса могут об этом забыть.
Counter c;
c.set(0);
c.inc();
Для повышения защищенности кода и расширения функциональности вводят специальные члены класса. К ним относятся:
- Конструктор - это функция, автоматически вызываемая программой при создании экземпляра класса. Конструкторы используются для присвоения значений данным объекта и выделения динамической памяти под внутренние массивы объекта. Имя конструктора должно совпадать с именем класса.
- Деструктор - это функция, автоматически вызываемая программой при уничтожении экземпляра класса. Деструкторы используются чаще всего для освобождения динамической памяти. Имя деструктора состоит из символа ''тильда'' и имени класса.
- Указатель this - Каждый экземпляр класса содержит указатель на свой код. Этот механизм реализуется через использование this. Благодаря этому указателю любой объект может узнать собственный адрес в оперативной памяти.
Пример класса с конструктором и деструктором:
Класс Vector
class Vector {
int *data;
int size;
public:
Vector(int num) { data=new int[size=num]; } // конструктор
~Vector() { delete[] data; } // деструктор
};
В данном классе конструктор Vector принимает в качестве параметра число элементов и выделяет динамическую память для хранения массива. Деструктор ~Vector выполняет обратное действие: освобождение памяти, занимаемой массивом.
В классе может быть сколько угодно конструкторов (они должны отличаться списком параметров) и только один деструктор.
А вот как можно воспользоваться классом Vector:
Использование Vector
void fun()
{
Vector v1(10); // статика, размер вектора 10 элементов
Vector *v2=new Vector(20); // динамика, размер - 20.
delete v2; // здесь будет вызов деструктора для v2
} // здесь - для v1
Существуют особенные разновидности конструкторов
- Конструктор без параметров - вызывается в том случае, если экземпляр класса создается без значения
- Конструктор по-умолчанию - стандартный конструктор, вызывается в случае отсутствия описания конструкторов в классе
- Конструктор копирования - конструктор, вызываемый при создании копии объекта. В качестве параметра должен содержать ссылку на класс
Работа этих конструкторов будет проиллюстрирована в дальнейших лекциях курса.
Указатель this
Скрытый указатель на экземпляр класса
void Stack::create_stack(Stack* this, int size)
{
this->maxSize=size;
this->storage=new char[maxSize];
this->top=0;
}
void Stack::push(Stack* this, char c)
{
if (this->top < this->maxSize)
this->storage[this->top++] = c;
}
char Stack::pop(Stack* this)
{
return this->top ? this->storage[--this->top] : 0;
}
Конструкторы
Конструктор
Стандартный член класса, который автоматически вызывается при создании экземпляра (объекта)
class Stack {
public:
Stack();
Stack(unsigned int size);
//...
};
Stack default_stack;
Stack short_stack(20);
Stack long_stack(500);
class Date
{
Date (int dd=0, int mm=0, int yy=0);
//...
};
Date today(26, 5, 2004);
Date any_day;
Stack::Stack(unsigned int size):
maxSize(size),
top(0)
{
storage = new char[size];
}
Stack::Stack()
{
storage = new char[maxSize=100];
top=0;
}
Date::Date(int d, int m, int y)
{
day=d?d:current_day();
month=m?m:current_month();
year=y?y:current_year();
}
Конструкторы по-умолчанию
class Date
{
public:
void next_date();
int get_date();
...
};
class Date
{
Date ();
Date (int dd, int mm, int yy);
//...
};
Date today(26, 5, 2004);
Date any_day;
class Date
{
Date (int dd, int mm, int yy);
//...
};
Date today(26, 5, 2004);
Date any_day; // ошибка!
Деструкторы
- Освобождают ресурсы: файлы, память, блокировки
- Вызываются неявно, когда переменная выходит из области видимости, удаляется объект и т.д.
class Stack
{
public:
Stack();
~Stack();
//...
};
Stack::~Stack()
{
if storage
{
delete[] storage;
}
}
3 Разное
Константные функции-члены
class Date {
int day, month, year;
public:
//...
int get_day();
int get_month();
int get_year();
}
int Date::get_day() {
return day++;
}
class Date {
int day, month, year;
public:
//...
int get_day() const;
int get_month() const;
int get_year() const;
}
void f(Date& d, const Date& cd ) {
int i = d.get_year();
d.add_year(2);
int j = cd.get_year();
cd.add_year(3); // ошибка!
}
Статические члены
class Counter {
public:
static int incr();
int get_counter();
private:
static int count;
};
Counter count;
count.incr();
Counter::incr();
int i = count.get_counter();
};
int Counter::count = 0;
int Counter::incr()
{
return ++count;
}
int Counter::get_counter()
{
return count;
}
3.1 Управление доступом
Управление доступом к классу
Главная забота класса - скрыть как можно больше информации. Существует 4 вида пользователей класса:
- сам класс;
- обычные пользователи (другие классы);
- производные классы;
- дружественные классы;
Каждый пользователь обладает привилегиями доступа к членам класса.
Рассмотрим правила, определяющие доступ к элементам класса
- Сам класс имеет полный доступ ко всем своим элементам;
- Обычные пользователи (другие классы) имеют полный доступ только к открытому (public) разделу;
- Производные классы имеют доступ к public-разделу и к разделу protected;
- Дружественные классы и функции имеют полный доступ ко всем разделам;
Дружественный класс имеет доступ как к общедоступным, так и к закрытым членам другого класса. С ключевым словом friend может быть объявлен как целый класс, так и отдельная функция.
class MyClass1
{
friend MyClass2;
};
class MyClass2
{
};
Класс MyClass2 теперь имеет право обращаться к закрытым и защищенным членам класса MyClass1. Обратное, однако, не верно до тех пор, пока в описание класса MyClass2 мы не поместим строку friend MyClass1. Таким образом, дружественность не взаимна.
Отношение дружественности не наследуется!
Пример дружественности
class Vector {
float V[4];
//...
friend Vector operator*(const Matrix&, const Vector&);
};
class Matrix {
Vector M[4];
//...
friend Vector operator*(const Matrix&, const Vector&);
};
friend Vector operator*(const Matrix& rm, const Vector& rv)
{
Vector tmp;
//...
tmp.V[i] += rm.M[i].V[j] * rv.V[j];
}
Полное и неполное описание класса
Класс должен быть описан до использования его членов. В этом случае мы используем неполное, или предварительное объявление.
// ПРЕДВАРИТЕЛЬНОЕ ОБЪЯВЛЕНИЕ class MyClass; MyClass *mc; // для этого мы добавили неполное объявление // ПОЛНОЕ ОПИСАНИЕ class MyClass { ...... };
Неполное объявление используется для ссылки на класс, который еще не совсем определен, когда он находится в другом файле.
Нельзя создать экземпляр неполно объявленного класса.
3.2 Копирование объектов
Проблемы копирования
Проблема!
void h ()
{
Stack s1(1000);
Stack s2 = s1;
Stack s3(100000000);
s3 = s2;
}
По умолчанию – почленное копирование.
Возможные проблемы:
- Утечка памяти;
- Многократный вызов деструктора - попытка освобождения одних и тех же ресурсов;
Конструктор копирования
Конструктор копирования используется для создания копии объекта.
Пример с конструктором копирования
class Counter {
int count;
public:
Counter(const Counter& ref) {
count=ref.count;
}
};
int main() {
Counter c1;
Counter c2=c1;
return 0;
}
class Stack {
public:
//...
Stack(const Stack& rs); // копирующий конструктор
Stack& operator= (const Stack& rs);// коп. присваивание
}
Stack::Stack(const Stack& rs) :
max_size(rs.max_size),
top(rs.top)
{
storage = new char[max_size];
for(int i=0; i<top; i++) storage[i]=rs.storage[i];
}
Копирующее присваивание
Stack& Stack::operator= (const Stack& rs)
{
if (&rs != this)
{
delete[] storage;
storage = new char[max_size=rs.max_size];
top = rs.top;
for(int i=0; i<top; i++)
storage[i]=rs.storage[i];
}
return *this;
}
Итог
- Копирующий конструктор инициализирует чистую память.
- Оператор присваивания работает с созданным объектом. Стратегия:
- защита от присваивания самому себе,
- удаление старых элементов,
- инициализация и копирование новых элементов,
- возврат ссылки на себя.
3.3 Поля-объекты
Объекты в виде данных
Можно объявить член данных, который является объектом класса.
Если член данных класса должен быть инициализирован с особыми параметрами, то нужно использовать специальную запись. Конструктор члена данных нужно вызвать с аргументами путем передачи параметров непосредственно перед входом в тело конструктора включающего класса.
Класс First
class First {
public:
int val;
First() { val=0; }
First(int i) { val=i; }
int GetVal() { return val; }
};
Класс Second
class Second {
int id;
public:
First object;
Second() { id=0; }
Second(int v):object(v)) { id=v; }
int GetID() { return id; }
};
int main() {
First one;
Second two;
Second three(10);
return 0;
}
При создании экземпляра класса-контейнера Second перед выполнением его конструктора будет вызван конструктор класса First.
Инициализация
Class Schedule {
public:
Schedule(Date dt1,
Stack& s);
Schedule(Date dt1);
//...
private:
Date m_date;
Stack m_stack;
}
Schedule::Schedule(Date dt1,
Stack& s)
: m_date(dt1), m_stack(s)
{
m_stack.push(/*...*/);
}
...
Schedule::Schedule(Date dt1)
:m_date(dt1),m_stack()
{ }
...
Schedule::Schedule(Date dt1,
Stack& s)
:m_date(dt1)
{
m_stack = s;
}
Копирование
Class Schedule {
public:
Schedule& operator=(const Schedule &sch);
//...
private:
Date m_date;
Stack m_stack;
}
Schedule& Schedule::operator=(const Schedule &sch)
{
m_date = sch.m_date;
m_stack = sch.m_stack;
}
Инициализация в конструкторе
Class Schedule {
public:
Schedule(int ii,
Date& dd,
Time tt);
//…
private:
const int m_i;
Date& m_rd;
Time m_t;
};
Schedule::Schedule(int ii, Date& dd,
Time tt)
:m_i(ii), m_rd(dd), m_t(tt)
{ }
Инициализация в конструкторе обязательна для:
- членов без конструкторов по умолчанию,
- константных членов,
- членов, являющихся ссылками.
4 Класс Vector
Постановка задачи
В этом разделе мы проведем разработку полезного класса Vector для упрощения работы с динамическим массивом
Формулировка задачи
Разработать класс Vector для хранения массива целых чисел
Анализ задачи
Класс Vector должен иметь поле-указатель для хранения адреса динамического массива и переменную для хранения размера массива. В качестве функций необходимо задать:
- Конструктор с параметром (размер массива)
- Конструктор по-умолчанию
- Конструктор копирования
- Деструктор (для освобождения динамической памяти)
- Выбор элемента из массива
- Добавление элемента в массив
Описание класса
Vector.h
// Vector.h
class Vector {
int *data; // данные
int size; // размер данных
public:
Vector(int num); // конструктор с параметром
Vector(); // конструктор без параметров
Vector(Vector&); // конструктор копирования
~Vector(); // деструктор
bool getValue(int,int&); // прочитать элемент
bool setValue(int,int); // записать элемент
};
Реализация класса
Vector.cpp
// Vector.cpp
Vector::Vector(int num) {
if(num>0)
data=new int[size=num];
else
data=new int[size=10];
}
Vector::Vector() {
data=new int[size=10];
}
Vector::Vector(Vector & v) {
data=new int[size=v.size];
for(int i=0;i<size;i++)
data[i]=v.data[i];
}
Vector.cpp (продолжение)
// Vector.cpp (продолжение)
Vector::~Vector() {
delete[] data;
}
bool Vector::getValue(int index,int& value) {
if(index>=size || index<0)
return false;
else
{
value=data[index];
return true;
}
}
bool Vector::setValue(int index,int value) {
if(index>=size || index<0)
return false;
else
{
data[index]=value;
return true;
}
}
Пример использования Vector
main.cpp
// main.cpp
int main()
{
int value,i;
Vector vector(100); // массив из ста значений
Vector vector2(vector); // копия существующего вектора
for(i=0;i<100;i++)
vector.setValue(i,i*i); // заполняем квадратами чисел
bool result=vector.getValue(101,value); // Ошибка! Вернется false
return 0;
}