Реализация практических классов
Обыкновенно для реализации класса используют два файла: заголовочный файл и файл с исходным кодом. В заголовочном файле класс описывается, а в файле исходного кода реализуются методы. Такой способ организации позволяет повысить производительность при сборке программы. Операторы #ifndef MYSTRING_H и #define MYSTRING_H нужны для ограничения включения файла один раз в программу. Множественное включение приводит к ошибкам множественного определения и объявления. #endif указывает на окончание блока включения.
Приведем пример полной реализации класса строки.
#ifndef MYSTRING_H #define MYSTRING_H
#include <iostream>
class MyString { public:
MyString(); MyString(const char* s);
MyString(const MyString& s); ~MyString();
MyString& operator =(const MyString& v); MyString operator +(const MyString& v) const; bool operator ==(const MyString& v) const; char operator [] (unsigned int i) const;
int length() const; private:
friend std::istream& operator >>(std::istream& i, MyString& s); friend std::ostream& operator <<(std::ostream& o, MyString& s); char *ptr;
};
std::istream& operator >>(std::istream& i, MyString& s); std::ostream& operator <<(std::ostream& o, MyString& s);
#endif // MYSTRING_H
Обратите внимание на const и &. В конце объявления метода const означает то, что метод может использоваться для константного экземпляра. При указании аргументов const гарантирует, что данные аргумента не будут изменены. А & позволяет передавать не все данные класса, а только адрес их расположения. Тип MyString& похож на тип const MyString*. Но только похож! Для прояснения различий рекомендую обратиться к [1][2].
Файл с кодом будет выглядеть так:
#include <cstring> #include "mystring.h"
using namespace std;
MyString::MyString() {
*(ptr = new char[1])='\0';
}
MyString::MyString(const char* s){ ptr = new char[1]; strcpy(ptr, s);
}
MyString::MyString(const MyString& s) { ptr = new char[1];
strcpy(ptr, s.ptr);
}
MyString::~MyString() { delete[] ptr;
}
MyString &MyString::operator =(const MyString &v) { strcpy(ptr, v.ptr);
return *this;
}
MyString MyString::operator +(const MyString &v) const { MyString t(*this);
strcat(t.ptr, v.ptr); return t;
}
bool MyString::operator ==(const MyString &v) const { return !strcmp(ptr, v.ptr);
}
char MyString::operator [](unsigned int i) const { return ptr[i];
}
int MyString::length() const { return strlen(ptr);
}
istream& operator >>(std::istream& i, MyString &s) { return i >> s.ptr;
}
ostream& operator <<(std::ostream& o, MyString &s){ return o << s.ptr;
}
Теперь мы имеем класс строки, который имеет возможность для практического применения в программе.
Расширение и использование
Теперь рассмотрим вопрос расширения возможностей класса. Первый способ, который активно применяют начинающие – это дописать класс. В результате получается новый класс с нужными функциями. Иногда такой способ применяют на практике. Например, когда передаётся объект по сети или между программами. Так работают технологии OLE, ActiveX, CORBA. Но в большинстве случаев, такой подход больше походит на применение КамАЗов в качестве такси.1
В случае обмена объектами между программами, мы сталкиваемся с необходимостью передачи дополнительных данных об используемых объектах. Собственно, вполне вероятна ситуация, когда нам понадобиться передать описание более половины объектов, применяющихся в программе. Поэтому, проще включить всю нужную информацию в объект. Такой подход к построению объектов называется компетентно-ориентированным.
1В действительности, использование этого подхода в ООП нарушает правило единственности абстракции.
В нашем случае более разумными и эффективным будут наследование и агрегирование. Сначала рассмотрим наследование.
Суть подхода заключается в создании производного класса от нашей строки. Для этого надо данные строки переместить в секцию protected. А в производном классе мы будем их ис-
пользовать напрямую. Например:
class AnotherString: public MyString { AnotherString();
AnotherString(const AnotherString &v); ~AnotherString();
void foo(char x){
for(int i=0;ptr[i]!='\0';++i) ptr[i]^=x;
}
};
Также, требуется создать конструкторы и деструкторы в новом классе.
Этот способ часто используется, когда требуется расширить абстракцию. В принципе, наследование позволяет значительно сэкономить ресурсы и повысить возможности расширения программы. В целом, при написании программы, важно построить правильную иерархию классов.
При использовании агрегирования, мы создадим новый класс и включим в него строку как поле. То есть используем переменную типа строка в новом классе. Это весьма удобно, поскольку вся работа с памятью возложена на класс строки.
Такой способ более подходит для последнего варианта класса, так как он имеет достаточный набор методов для эффективного использования.
class NewString { NewString();
NewString(const NewString& v); ~NewString();
void foo(char x){
for(int i=0;i<String.length();++i) String[i]^=x;
}
private:
MyString String;
};
Преимущество агрегирования в рассматриваемом нами случае состоит в том, что можно смело изменить внутреннюю структуру строки, сохранив внешний интерфейс. И это никак не скажется на внутренней структуре новой строки. В тоже время, при наследовании нам придётся изменить класс так, чтобы он соответствовал новой структуре класса строки.
Библиография
1:Р. Лафоре, Объектно-ориентированное программирование в С++
2:Дж. Коплиен, Программирование на C++