Законченный пример класса




Программирование без упрятывания данных (в расчете на структуры)требует меньшего предварительного обдумывания задачи, чемпрограммирование с упрятыванием данных (в расчете на классы).Структуру можно определить не очень задумываясь о том, как еебудут использовать. Когда определяется класс, внимание концентрируетсяна том, чтобы обеспечить для нового типа полный набор операций.Это важное смещение акцента в проектировании программ. Обычновремя, затраченное на разработку нового типа, многократно окупаетсяв процессе отладки и развития программы. Вот пример законченного определения типа intset, представляющегопонятие "множество целых": class intset { int cursize, maxsize; int *x; public: intset(int m, int n); // не более m целых из 1..n ~intset(); int member(int t) const; // является ли t членом? void insert(int t); // добавить к множеству t void start(int& i) const { i = 0; } void ok(int& i) const { return i<cursize; } void next(int& i) const { return x[i++]; } }; Для проверки этого класса вначале создадим, а затем распечатаеммножество случайных целых чисел. Это простое множество целыхможно использовать для проверки, есть ли повторения в ихпоследовательности. Но для большинства задач нужен, конечно,более развитый тип множества. Как всегда возможны ошибки, поэтомунужна функция: #include <iostream.h> void error(const char *s) { cerr << "set: " << s << '\n'; exit(1); } Класс intset используется в функции main(), для которой должнобыть задано два параметра: первый определяет число создаваемыхслучайных чисел, а второй - диапазон их значений: int main(int argc, char* argv[]) { if (argc!= 3) error("нужно задавать два параметра"); int count = 0; int m = atoi(argv[1]); // число элементов множества int n = atoi(argv[2]); // из диапазона 1..n intset s(m,n); while (count<m) { int t = randint(n); if (s.member(t)==0) { s.insert(t); count++; } } print_in_order(&s); } Значение счетчика параметров программы argc равно 3, хотя программа имеет только два параметра. Дело в том, что в argv[0] всегда передается дополнительный параметр, содержащий имя программы. Функция extern "C" int atoi(const char*) является стандартной библиотечной функцией, преобразующей целое из строкового представления во внутреннюю двоичную форму. Как обычно, если вы не хотите иметь такое описание в своей программе, то вам надо включить в нее соответствующий заголовочный файл, содержащий описания стандартных библиотечных функций. Случайные числа генерируются с помощью стандартной функции rand: extern "C" int rand(); // будьте осторожны: // числа не совсем случайные int randint(int u) // диапазон 1..u { int r = rand(); if (r < 0) r = -r; return 1 + r%u; } Подробности реализации класса мало интересны для пользователя, но в любом случае будут использоваться функции-члены. Конструктор размещает массив целых с размером, равным заданному максимальному размеру множества, а деструктор удаляет этот массив: intset::intset(int m, int n) // не более m целых в 1..n { if (m<1 || n<m) error("недопустимый размер intset"); cursize = 0; maxsize = m; x = new int[maxsize]; } intset::~intset() { delete x; } Целые добавляются таким образом, что они хранятся во множестве в возрастающем порядке: void intset::insert(int t) { if (++cursize > maxsize) error("слишком много элементов"); int i = cursize-1; x[i] = t; while (i>0 && x[i-1]>x[i]) { int t = x[i]; // поменять местами x[i] и x[i-1] x[i] = x[i-1]; x[i-1] = t; i--; } } Чтобы найти элемент, используется простой двоичный поиск: int intset::member(int t) const // двоичный поиск { int l = 0; int u = cursize-1; while (l <= u) { int m = (l+u)/2; if (t < x[m]) u = m-1; else if (t > x[m]) l = m+1; else return 1; // найден } return 0; // не найден } Наконец, нужно предоставить пользователю набор операций, с помощью которых он мог бы организовать итерацию по множеству в некотором порядке (ведь порядок, используемый в представлении intset, от него скрыт). Множество по своей сути не является внутренне упорядоченным, и нельзя позволить просто выбирать элементы массива (а вдруг завтра intset будет реализовано в виде связанного списка?). Пользователь получает три функции: start() - для инициализации итерации, ok() - для проверки, есть ли следующий элемент, и next() - для получения следующего элемента: class intset { //... void start(int& i) const { i = 0; } int ok(int& i) const { return i<cursize; } int next(int& i) const { return x[i++]; } }; Чтобы обеспечить совместную работу этих трех операций, надо запоминать тот элемент, на котором остановилась итерация. Для этого пользователь должен задавать целый параметр. Поскольку наше представление множества упорядоченное, реализация этих операций тривиальна. Теперь можно определить функцию print_in_order: void print_in_order(intset* set) { int var; set->sart(var); while (set->ok(var)) cout << set->next(var) << '\n'; } Другой способ построения итератора по множеству приведен в $$7.8.

Еще о классах

В этом разделе описаны дополнительные свойства класса. Описан способ обеспечить доступ к частным членам в функциях, не являющихся членами ($$5.4.1). Описано, как разрешить коллизии имен членов ($$5.4.2) и как сделать описания классов вложенными ($$5.4.3), но при этом избежать нежелательной вложенности ($$5.4.4). Вводится понятие статических членов (static), которые используются для представления операций и данных, относящихся к самому классу, а не к отдельным его объектам ($$5.4.5). Раздел завершается примером, показывающим, как можно построить дискриминирующее (надежное) объединение ($$5.4.6).

Друзья

Пусть определены два класса: vector (вектор) и matrix (матрица). Каждый из них скрывает свое представление, но дает полный набор операций для работы с объектами его типа. Допустим, надо определить функцию, умножающую матрицу на вектор. Для простоты предположим, что вектор имеет четыре элемента с индексами от 0 до 3, а в матрице четыре вектора тоже с индексами от 0 до 3. Доступ к элементам вектора обеспечивается функцией elem(), и аналогичная функция есть для матрицы. Можно определить глобальную функцию multiply (умножить) следующим образом: vector multiply(const matrix& m, const vector& v); { vector r; for (int i = 0; i<3; i++) { // r[i] = m[i] * v; r.elem(i) = 0; for (int j = 0; j<3; j++) r.elem(i) +=m.elem(i,j) * v.elem(j); } return r; } Это вполне естественное решение, но оно может оказаться очень неэффективным. При каждом вызове multiply() функция elem() будет вызываться 4*(1+4*3) раз. Если в elem() проводится настоящий контроль границ массива, то на такой контроль будет потрачено значительно больше времени, чем на выполнение самой функции, и в результате она окажется непригодной для пользователей. С другой стороны, если elem() есть некий специальный вариант доступа без контроля, то тем самым мы засоряем интерфейс с вектором и матрицей особой функцией доступа, которая нужна только для обхода контроля. Если можно было бы сделать multiply членом обоих классов vector и matrix, мы могли бы обойтись без контроля индекса при обращении к элементу матрицы, но в то же время не вводить специальной функции elem(). Однако, функция не может быть членом двух классов. Надо иметь в языке возможность предоставлять функции, не являющейся членом, право доступа к частным членам класса. Функция - не член класса, - имеющая доступ к его закрытой части, называется другом этого класса. Функция может стать другом класса, если в его описании она описана как friend (друг). Например: class matrix; class vector { float v[4]; //... friend vector multiply(const matrix&, const vector&); }; class matrix { vector v[4]; //... friend vector multiply(const matrix&, const vector&); }; Функция-друг не имеет никаких особенностей, за исключением права доступа к закрытой части класса. В частности, в такой функции нельзя использовать указатель this, если только она действительно не является членом класса. Описание friend является настоящим описанием. Оно вводит имя функции в область видимости класса, в котором она была описана, и при этом происходят обычные проверки на наличие других описаний такого же имени в этой области видимости. Описание friend может находится как в общей, так и в частной частях класса, это не имеет значения. Теперь можно написать функцию multiply, используя элементы вектора и матрицы непосредственно: vector multiply(const matrix& m, const vector& v) { vector r; for (int i = 0; i<3; i++) { // r[i] = m[i] * v; r.v[i] = 0; for (int j = 0; j<3; j++) r.v[i] +=m.v[i][j] * v.v[j]; } return r; } Отметим, что подобно функции-члену дружественная функция явно описывается в описании класса, с которым дружит. Поэтому она является неотъемлемой частью интерфейса класса наравне с функцией-членом. Функция-член одного класса может быть другом другого класса: class x { //... void f(); }; class y { //... friend void x::f(); }; Вполне возможно, что все функции одного класса являются друзьями другого класса. Для этого есть краткая форма записи: class x { friend class y; //... }; В результате такого описания все функции-члены y становятся друзьями класса x.

Уточнение имени члена

Иногда полезно делать явное различие между именами членов классов и прочими именами. Для этого используется операция:: (разрешения области видимости): class X { int m; public: int readm() const { return m; } void setm(int m) { X::m = m; } }; В функции X::setm() параметр m скрывает член m, поэтому к члену можно обращаться, только используя уточненное имя X::m. Правый операнд операции:: должен быть именем класса. Начинающееся с:: имя должно быть глобальным именем. Это особенно полезно при использовании таких распространенных имен как read, put, open, которыми можно обозначать функции-члены, не теряя возможности обозначать ими же функции, не являющиеся членами. Например: class my_file { //... public: int open(const char*, const char*); }; int my_file::jpen(const char* name, const char* spec) { //... if (::open(name,flag)) { // используется open() из UNIX(2) //... } //... }

Вложенные классы

Описание класса может быть вложенным. Например: class set { struct setmem { int mem; setmem* next; setmem(int m, setmem* n) { mem=m; next=n; } }; setmem* first; public: set() { first=0; } insert(int m) { first = new setmem(m,first); } //... }; Доступность вложенного класса ограничивается областью видимости лексически объемлющего класса: setmem m1(1,0); // ошибка: setmem не находится // в глобальной области видимости Если только описание вложенного класса не является совсем простым, то лучше описывать этот класс отдельно, поскольку вложенные описания могут стать очень запутанными: class setmem { friend class set; // доступно только для членов set int mem; setmem* next; setmem(int m, setmem* n) { mem=m; next=n; } // много других полезных членов }; class set { setmem* first; public: set() { first=0; } insert(int m) { first = new setmem(m,first); } //... }; Полезное свойство вложенности - это сокращение числа глобальных имен, а недостаток его в том, что оно нарушает свободу использования вложенных типов (см. $$12.3). Имя класса-члена (вложенного класса) можно использовать вне описания объемлющего его класса так же, как имя любого другого члена: class X { struct M1 { int m; }; public: struct M2 { int m; }; M1 f(M2); }; void f() { M1 a; // ошибка: имя `M1' вне области видимости M2 b; // ошибка: имя `M1' вне области видимости X::M1 c; // ошибка: X::M1 частный член X::M2 d; // нормально } Отметим, что контроль доступа происходит и для имен вложенных классов. В функции-члене область видимости класса начинается после уточнения X:: и простирается до конца описания функции. Например: M1 X::f(M2 a) // ошибка: имя `M1' вне области видимости { /*... */ } X::M1 X::f(M2 a) // нормально { /*... */ } X::M1 X::f(X::M2 a) // нормально, но третье уточнение X:: излишне { /*... */ }

Статические члены

Класс - это тип, а не некоторое данное, и для каждого объекта класса создается своя копия членов, представляющих данные. Однако, наиболее удачная реализация некоторых типов требует, чтобы все объекты этого типа имели некоторые общие данные. Лучше, если эти данные можно описать как часть класса. Например, в операционных системах или при моделировании управления задачами часто нужен список задач: class task { //... static task* chain; //... }; Описав член chain как статический, мы получаем гарантию, что он будет создан в единственном числе, т.е. не будет создаваться для каждого объекта task. Но он находится в области видимости класса task, и может быть доступен вне этой области, если только описан в общей части. В этом случае имя члена должно уточняться именем класса: if (task::chain == 0) // какие-то операторы В функции-члене его можно обозначать просто chain. Использование статических членов класса может заметно сократить потребность в глобальных переменных. Описывая член как статический, мы ограничиваем его область видимости и делаем его независимым от отдельных объектов его класса. Это свойство полезно как для функций-членов, так и для членов, представляющих данные: class task { //... static task* task_chain; static void shedule(int); //... }; Но описание статического члена - это только описание, и где-то в программе должно быть единственное определение для описываемого объекта или функции, например, такое: task* task::task_chain = 0; void task::shedule(int p) { /*... */ } Естественно, что и частные члены могут определяться подобным образом. Отметим, что служебное слово static не нужно и даже нельзя использовать в определении статического члена класса. Если бы оно присутствовало, возникла бы неоднозначность: указывает ли оно на то, что член класса является статическим, или используется для описания глобального объекта или функции? Слово static одно из самых перегруженных служебных слов в С и С++. К статическому члену, представляющему данные, относятся оба основных его значения: "статически размещаемый", т.е. противоположный объектам, размещаемым в стеке или свободной памяти, и "статический" в смысле с ограниченной областью видимости, т.е. противоположный объектам, подлежащим внешнему связыванию. К функциям-членам относится только второе значение static.

Указатели на члены

Можно брать адрес члена класса. Операция взятия адреса функции-члена часто оказывается полезной, поскольку цели и способы применения указателей на функции, о которых мы говорили в $$4.6.9, в равной степени относятся и к таким функциям. Указатель на член можно получить, применив операцию взятия адреса & к полностью уточненному имени члена класса, например, &class_name::member_name. Чтобы описать переменную типа "указатель на член класса X", надо использовать описатель вида X::*. Например: #include <iostream.h> struct cl { char* val; void print(int x) { cout << val << x << '\n'; } cl(char* v) { val = v; } }; Указатель на член можно описать и использовать так: typedef void (cl::*PMFI)(int); int main() { cl z1("z1 "); cl z2("z2 "); cl* p = &z2; PMFI pf = &cl::print; z1.print(1); (z1.*pf)(2); z2.print(3); (p->*pf)(4); } Использование typedef для замены трудно воспринимаемого описателя в С достаточно типичный случай. Операции.* и ->* настраивают указатель на конкретный объект, выдавая в результате функцию, которую можно вызывать. Приоритет операции () выше, чем у операций.* и ->*, поэтому нужны скобки. Во многих случаях виртуальные функции ($$6.2.5) успешно заменяют указатели на функции.

Структуры и объединения

По определению структура - это класс, все члены которого общие, т.е. описание struct s {... это просто краткая форма описания class s { public:... Поименованное объединение определяется как структура, все члены которой имеют один и тот же адрес ($$R.9.5). Если известно, что в каждый момент времени используется значение только одного члена структуры, то объявив ее объединением, можно сэкономить память. Например, можно использовать объединение для хранения лексем транслятора С: union tok_val { char* p; // строка char v[8]; // идентификатор (не более 8 символов) long i; // значения целых double d; // значения чисел с плавающей точкой }; Проблема с объединениями в том, что транслятор в общем случае не знает, какой член используется в данный момент, и поэтому контроль типа невозможен. Например: void strange(int i) { tok_val x; if (i) x.p = "2"; else x.d = 2; sqrt(x.d); // ошибка, если i!= 0 } Кроме того, определенное таким образом объединение нельзя инициализировать таким кажущимся вполне естественным способом: tok_val val1 = 12; // ошибка: int присваивается tok_val tok_val val2 = "12"; // ошибка: char* присваивается tok_val Для правильной инициализации надо использовать конструкторы: union tok_val { char* p; // строка char v[8]; // идентификатор (не более 8 символов) long i; // значения целых double d; // значения чисел с плавающей точкой tok_val(const char*); // нужно выбирать между p и v tok_val(int ii) { i = ii; } tok_val(double dd) { d = dd; } }; Эти описания позволяют разрешить с помощью типа членов неоднозначность при перегрузке имени функции (см. $$4.6.6 и $$7.3). Например: void f() { tok_val a = 10; // a.i = 10 tok_val b = 10.0; // b.d = 10.0 } Если это невозможно (например, для типов char* и char[8] или int и char и т.д.), то определить, какой член инициализируется, можно, изучив инициализатор при выполнении программы, или введя дополнительный параметр. Например: tok_val::tok_val(const char* pp) { if (strlen(pp) <= 8) strncpy(v,pp,8); // короткая строка else p = pp; // длинная строка } Но лучше подобной неоднозначности избегать. Стандартная функция strncpy() подобно strcpy() копирует строки, но у нее есть дополнительный параметр, задающий максимальное число копируемых символов. То, что для инициализации объединения используются конструкторы, еще не гарантирует от случайных ошибок при работе с объединением, когда присваивается значение одного типа, а выбирается значение другого типа. Такую гарантию можно получить, если заключить объединение в класс, в котором будет отслеживаться тип заносимого значения: class tok_val { public: enum Tag { I, D, S, N }; private: union { const char* p; char v[8]; long i; double d; }; Tag tag; void check(Tag t) { if (tag!= t) error(); } public: Tag get_tag() { return tag; } tok_val(const char* pp); tok_val(long ii) { i = ii; tag = I; } tok_val(double dd) { d = dd; tag = D; } long& ival() { check(I); return i; } double& fval() { check(D); return d; } const char*& sval() { check(S); return p; } char* id() { check(N); return v; } }; tok_val::tok_val(const char* pp) { if (strlen(pp) <= 8) { // короткая строка tag = N; strncpy(v,pp,8); } else { // длинная строка tag = S; p = pp; // записывается только указатель } } Использовать класс tok_val можно так: void f() { tok_val t1("короткая"); // присваивается v tok_val t2("длинная строка"); // присваивается p char s[8]; strncpy(s,t1.id(),8); // нормально strncpy(s,t2.id(),8); // check() выдаст ошибку } Описав тип Tag и функцию get_tag() в общей части, мы гарантируем, что тип tok_val можно использовать как тип параметра. Таким образом, появляется надежная в смысле типов альтернатива описанию параметров с эллипсисом. Вот, например, описание функции обработки ошибок, которая может иметь один, два, или три параметра с типами char*, int или double: extern tok_val no_arg; void error(const char* format, tok_val a1 = no_arg, tok_val a2 = no_arg, tok_val a3 = no_arg);


Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2016-02-16 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: