Классы. первое знакомство
Простейшее определение класса без наследования имеет вид:
class имя_класса {
// по умолчанию раздел private – частные члены класса
public: //открытые функции и переменные класса
};
Определение класса соответствует введению нового типа данных, а понятие переменной данного типа – понятию объекта (экземпляра) класса. Список членов класса включает определение данных и функций, те из них, чьи объявления находятся в описании класса, называются функциями-членами класса. В ООП для таких функций используется термин “методы класса”. Классы могут также содержать определения функций, тело которых помещается в определение класса (inline-функции). Для инициализации/уничтожения объектов используются специальные функции-конструкторы с тем же именем, что и класс, и деструкторы, с именем класса, перед которым стоит символ “~”.
Переменные, объявленные в разделе класса по умолчанию как частные (private), имеют область видимости в пределах класса. Их можно сделать видимыми вне класса, если поставить перед началом объявления слово public.
Обычно переменные в классе объявляются private-переменными, а функции видимыми (public). Открытые функции-члены класса имеют доступ ко всем закрытым данным класса, через них возможен доступ к этим данным.
Классами в С++ являются также структуры (struct) и объединения (union).Отличием структуры и объединения от класса является то, что их члены по умолчанию открытые (public), а не закрытые, как у класса. Это обеспечивает преемственность с языком C. Кроме того, структуры и объединения не могут наследоваться и наследовать.
В следующей программе определяется простейший класс Strtype, членами которого являются массив strтипа char и функции set(),show(), get().
#include <iostream.h> //пример 10
#include <string.h>
#include <conio.h>
class Strtype{
char str[80]; //private
public:
void set (char *);//задать str
void show(); //вывести str
char* get(); //вернуть str
}; //конец определения класса
void Strtype::set(char *s) {// определение метода set()
strcpy(str,s);}//копирование s в str
void Strtype::show() {// определение метода show()
cout<<str<<endl;}
char * Strtype::get() {// определение метода get()
return str;}
int main() {
Strtype obstr; //объявление объекта
obstr.set("bel.university ");//вызов метода set()
obstr.show();//вызов метода show()
cout<<obstr.get()<<endl;
while(!kbhit());//задержка выхода до нажатия клавиши
return 0;
}
Вывод: bel.university
bel.university
Массив член-класса str – является частным (private), доступ к нему возможен только через функции-члены класса. Такое объединение в классе сокрытых данных и открытых функций и есть инкапсуляция. Здесь obstr – объект данного класса. Вызов функции осуществляется из объекта добавлением к имени объекта имени функции, через точку или –>, если используется указатель на объект.
Приведем примеры нескольких объявлений объектов:
Strtype a; //объект a
Strtype x[100]; //массив объектов
Strtype *p; //указатель на объект
p=new Strtype; // создание динамического объекта
a.set("строка"); // вызов функций
x[i].set("строка");
p->set("строка");
Оператор расширения области видимости “::”указываетдоступ к элементам класса. Например, Strtype::set(char *s) означает принадлежность функции set(char *s) области видимости класса Strtype. Кроме этого оператор “::”используется для доступа к данным класса (оператор вида Strtype::count), для указания внешней или глобальной области видимости переменной, скрытой локальным контекстом (оператор вида ::globalName).
Следующая программа демонстрирует создание класса Stack на основе динамического массива. Для инициализации объекта класса используется метод Stack() –конструктор класса.
#include <iostream.h> // пример 11
#include <conio.h>
#define SIZE 10
// объявление класса Stack для символов
class Stack {
char *stck; // содержит стек
int tos; // индекс вершины стека
public:
Stack(); //конструктор
~Stack(){delete [] stck;}//деструктор
void push(char ch); // помещает в стек символ
char pop(); // выталкивает из стека символ
};
// инициализация стека
Stack::Stack(){
stck=new char[SIZE];//динамический массив
tos=0;
cout << "работа конструктора … \n";
}
// помещение символа в стек
void Stack::push(char ch){
if (tos==SIZE) {cout << "стек полон";return; }
stck[tos]=ch;
tos++;
}
// выталкивание символа из стека
char Stack::pop(){
if (tos==0) {cout << "стек пуст";return 0;}
tos--;
return stck[tos];
}
int main(){
/*образование двух автоматически инициализируемых стеков */
Stack s1, s2; //вызов конструктора для s1 и s2
int i;
s1.push('a');s2.push('x');
s1.push('b');s2.push('y');
s1.push('c');s2.push('z');
for(i=0;i<3;i++) cout<<"символ из s1:"<<s1.pop() << "\n";
for(i=0;i<3;i++) cout<<"символ из s2:"<<s2.pop() << "\n";
while(!kbhit());//задержка
return 0;
}
Вывод:
Работа конструктора …
Работа конструктора …
Символ из s1:c
Символ из s1:b
Символ из s1:a
Символ из s2:z
Символ из s1:y
Символ из s1:x
Конструктор Stack() вызывается автоматически при создании объектов класса s1,s2и выполняет инициализацию объектов, состоящую из выделения памяти для динамического массива и установки указателя на вершину стека в нуль. Конструктор может иметь аргументы, но не имеет возвращаемого значения и может быть перегружаемым.
Деструктор ~Stack() выполняет действия необходимые для корректного завершения работы с объектом, а именно, в деструкторе может освобождаться динамически выделенная память, закрываться соединения с файлами, и др. Он не имеет аргументов. Именем деструктора является имя класса, перед которым стоит знак “~” – тильда.
Рассмотрим еще один пример класса, реализующего динамический односвязный список:
#include <iostream.h>// пример 12
#include <conio.h>
struct Node{// объявление класса Node
int info; //информационное поле
Node* next; // указатель на следующий элемент
};
class List {// объявление класса List
Node* top; // указатель на начало списка
public:
List(){ // конструктор
top=0;
cout<<"\nkonstructor:\n";};
~List(){// деструктор
release();
cout<<"\ndestructor:\n";}
void push(int);// добавление элемента
void del() { // удаление элемента
Node* temp = top;
top = top->next;
delete temp; }
void show();
void release();// удаление всех элементов
};
void List::push(int i){
Node* temp = new Node;
temp->info = i;
temp->next =top;
top=temp; }
void List::show() {
Node* temp=top;
while (temp!=0) {
cout<<temp->info<<"->";
temp=temp->next;}
cout<<endl;}
void List::release() {
while (top!=0)del();}
int main(){
List *p;//объявление указателя на LIst
List st;//объявление объекта класса List
int n = 0;
cout << "Input an integer until 999: ";
do {//добавление в список, пока не введено 999
cin >> n; st.push(n);
} while(n != 999);
st.show();
st.del();
p=&st;
p->show();
while(!kbhit());
return 0;
}
Необходимо отметить, что некоторый класс ClassA может использоваться до его объявления. В этом случае перед испльзованием класса ClassA нужно поместить его неполное объявление:
class ClassA;
Важнейшим принципом ООП является наследование. Класс, который наследуется, называется базовым, а наследуемый – производным. В следующем примере класс Derived наследует компоненты класса Base, точнее компоненты раздела public, которые остаются открытыми, и компоненты раздела protected(защищенный), которые остаются закрытыми. Компоненты раздела private, также наследуются, но являются не доступными напрямую для производного класса. Производному классу доступны все данные и методы базового класса, наследуемые из разделов public и protected. Для переопределенных методов в производном классе действует принцип полиморфизма, который будет рассмотрен ниже. Объекту базового класса можно присвоить объект производного, указателю на базовый класс – значение указателя на производный класс. В этом случае через указатель на базовый класс можно получить доступ только к полям и функциям базового класса. Для доступа к полям и функциям порожденного класса следует привести (преобразовать) ссылку на базовый класс к ссылке на порожденный класс.
#include <iostream.h> // пример 12
#include <conio.h>
class Base { // определение базового класса
int i; //private по умолчанию
protected:
int k;
public:
Base(){i=0; k=1;}
void set_i(int n); // установка i
int get_i(){ // возврат i
return i;}
void show(){
cout<<i<<" "<<k<<endl;}
}; //конец Base
class Derived : public Base { // производный класс
int j;
public:
void set_j(int n);
int mul(); //умножение i на к базового класса и на j производного
}; //конец Derived
//установка значения i в базовом классе
void Base::set_i(int n){
i = n; }
//установка значения j в производном классе
void Derived::set_j(int n){
j = n;}
//возврат i*k из Base умноженного на j из Derived
int Derived::mul(){
/*производный класс наследует функции-члены базового класса*/
return j * get_i()*k;//вызов get_i() базового класса
}
int main(){
Derived ob;
ob.set_i(10); //загрузка i в Base
ob.set_j(4); // загрузка j в Derived
cout << ob.mul()<<endl; //вывод числа 40
ob.show(); //вывод i и k, 10 1
Base bob=ob;//присваивание объекта ссылке на базовой тип
cout<<bob.get_i(); // вывод i
while (!kbhit());
return 0;
}
Переменная i недоступна в производном классе, переменная k доступна, поскольку находится в разделе protected. В производном классе наследуются также функции get_i(), set_i() и show() классаBaseиз раздела public. Функция show() позволяет получить доступ из производного класса к закрытой переменной i производного класса. В результате выводится 40 10 1 10.
Принцип полиморфизма, состоящий в перегрузке методов, объявленных в различных классах с одним и тем же именем и списком параметров, будет рассмотрен ниже.
2. КЛАССЫ И ФУНКЦИИ
При реализации класса используются функции-члены класса (методы класса), функции-“друзья” класса, конструкторы, деструкторы, функции-операторы. Функции-члены класса объявляются внутри класса. Определение функции обычно помещается вне класса. При этом перед именем функции помещается операция доступа к области видимости имя_класса::. Таким образом, определение функции-члена класса имеет вид:
тип имя_класса:: имя_функции (список аргументов) { }
Определение функции может находиться и внутри класса вслед за его объявлением. Такие функции называются inline-функциями. Рассмотрим пример:
#include <iostream.h> // пример 20
#include <string.h>
#include <stdlib.h>
#include <conio.h>
#define SIZE 255
class Strtype {
char *p;
int len;
public:
Strtype(){// инициализация объекта, inline конструктор
p=new char [SIZE];
if(!p) {cout << "ошибка выделения памяти\n";
exit(1);}
*p='\0';
len=0;
}
~Strtype(); //деструктор
void set(char *ptr);
char *get(){return p;}
void show();
};
// освобождение памяти при удалении объекта строка
inline Strtype::~Strtype(){ //inline-деструктор
cout << "освобождение p\n";
delrete [] p;
}
void Strtype::set(char *ptr) {
if(strlen(ptr) >= SIZE) {
cout << "строка слишком велика \n";
return;
}
strcpy(p, ptr);
len = strlen(p);
}
void Strtype::show(){
cout << p << " длина : " << len<< "\n";
}
int main(){
{Strtype s1,s2;
s1.set("This is a test");
s2.set("I love C++");
s1.show();
s2.show();
cout<<s2.get()<<endl; }
while (!kbhit());
return 0;
}
В результате будет выведено:
This is a test длина: 14
I love C++ длина: 10
I love C++
Освобождение р
Освобождение р
Конструктор Strtype() и функция get() являются inline-функциями. Деструктор ~Strtype() также является inline – функцией, хотя и определяется вне класса. Ключевое слово inlineсодержит указание компилятору создать код функции, подставляемый в точку вызова. При этом время вызова сокращается, хотя код может увеличиться.
Вызов функций-членов класса осуществляется одним из двух способов:
имя_объекта.имя_функции(аргументы);
указатель_на_объект -> имя_функции(аргументы);
Еще раз обращаем внимание на то, что при определении функции- члена класса перед именем функции стоит имя класса, а при вызове –имя объекта класса.