Смекни!
smekni.com

Правила правой руки 17 Замечания для программистов на c 17 Глава 1 (стр. 27 из 43)

date(); // дата по умолчанию: сегодня

};

Конструкторы подчиняются тем же правилам относительно типов

параметров, что и перегруженные функции (#4.6.7). Если контрукторы

существенно различаются по типам своих парметров, то компилятор при

каждом использовании может выбрать правильный:

date today(4);

date july4("Июль 4, 1983");

date guy("5 Ноя");

date now; // инициализируется по умолчанию

Заметьте, что функции члены могут быть перегружены без явного

использования ключевого слова overload. Поскольку полный список

функций членов находится в описании класса и как правило короткий,

то нет никакой серьезной причины требовать использования слова

overload для предотвращения случайного повторнго использования

имени.

Размножение конструкторов в примере с date типично. При

разработке класса всегда есть соблазн обеспечить "все", поскольку

кажется проще обеспечить какое-нибудь средство просто на случай,

что оно кому-то понадобится или потому, что оно изящно выглядит,

чем решить, что же нужно на самом деле. Последнее требует больших

размышлений, но обычно приводит к программам, которые меньше по

размеру и более понятны. Один из способов сократить число

родственных функций - использовать параметры по умолчанию. В случае

date для каждого параметра можно задать значение по умолчанию,

интерпретируемое как "по умолчанию принимать: today" (сегодня).

- стр 147 -

class date {

int month, day, year;

public:

// ...

date(int d =0, int m =0, int y =0);

date(char*); // дата в строковом представлении

};

date::date(int d, int m, int y)

{

day = d ? d : today.day;

month = m ? m : today.month;

year = y ? y : today.year;

// проверка, что дата допустимая

// ...

}

Когда используется значение параметра, указывающее "брать по

умолчанию", выбранное значение должно лежать вне множества

возможных значений параметра. Для дня day и месяца mounth ясно, что

это так, но для года year выбор нуля неочевиден. К счастью, в

европейском календаре нет нулевого года . Сразу после 1 г. до н.э.

(year==-1) идет 1 г. н.э. (year==1), но для реальной программы это

может оказаться слишком тонко.

Объект класса без конструкторов можно инициализировать путем

присваивания ему другого объекта этого класса. Это можно делать и

тогда, когда конструкторы описаны. Например:

date d = today; // инициализация посредством присваивания

По существу, имеется конструктор по умолчанию, определенный как

побитовая копия объекта того же класса. Если для класса X такой

конструктор по умолчанию нежелателен, его можно переопределить

конструктором с именем X(X&). Это будет обсуждаться в #6.6.

5.2.5 Очистка

Определяемый пользователем тип чаще имеет, чем не имеет,

конструктор, который обеспечивает надлежащую инициализацию. Для

многих типов также требуется обратное действие, деструктор, чтобы

обеспечить соответствующую очистку объектов этого типа. Имя

деструктора для класса X есть ~X() ("дополнение конструктора"). В

частности, многие типы используют некоторый объем памяти из

свободной памяти (см. #3.2.6), который выделяется конструктором и

освобождается деструктором. Вот, например, традиционный стековый

тип, из которого для краткости полностью выброшена обработка

ошибок:

- стр 148 -

class char_stack {

int size;

char* top;

char* s;

public:

char_stack(int sz) { top=s=new char[size=sz]; }

~char_stack() { delete s; } // деструктор

void push(char c) { *top++ = c; }

char pop() { return *--top;}

}

Когда char_stack выходит из области видимости, вызывается

деструктор:

void f()

{

char_stack s1(100);

char_stack s2(200);

s1.push('a');

s2.push(s1.pop());

char ch = s2.pop();

cout << chr(ch) << "&bsol;n";

}

Когда вызывается f(), конструктор char_stack вызывается для s1,

чтобы выделить вектор из 100 символов, и для s2, чтобы выделить

вектор из 200 символов. При возврате из f() эти два вектора будут

освобождены.

5.2.6 Inliпе

При программировании с использованием классов очень часто

используется много маленьких функций. По сути, везде, где в

программе традиционной структуры стояло бы просто какое-нибудь

обычное использование структуры данных, дается функция. То, что

было соглашением, стало стандартом, который распознает компилятор.

Это может страшно понизить эффективность, потому что стоимость

вызова функции (хотя и вовсе не высокая по сравнению с другими

языками) все равно намного выше, чем пара ссылок по памяти,

необходимая для тела функции.

Чтобы справиться с этой проблемой, был разработан аппарат inline-

функций. Функция член, определенная (а не просто описанная) в

описании класса, считается inline. Это значит, например, что в

функциях, которые используют приведенные выше char_stack, нет

никаких вызовов функций кроме тех, которые используются для

реализации операций вывода! Другими словами, нет никаких затрат

времени выполнения, которые стоит принимать во внимание при

разработке класса. Любое, даже самое маленькое действие, можно

задать эффективно. Это увтерждение снимает аргумент, который чаще

всего приводят чаще всего в пользу открытых членов данных.

Функцию член можно также описать как inline вне описания класса.

Например:

- стр 149 -

char char_stack {

int size;

char* top;

char* s;

public:

char pop();

// ...

};

inline char char_stack::pop()

{

return *--top;

}

5.3 Интерфейсы и Реализации

Что представляет собой хороший класс? Нечто, имеющее небольшое и

хорошо определенное множество действий. Нечто, что можно

рассматривать как "черный ящик", которым манипулируют только

посредством этого множества действий. Нечто, чье фактическое

представление можно любым мыслимым способом изменить, не повлияв на

способ использования множества действий. Нечто, чего можно хотеть

иметь больше одного.

Для всех видов контейнеров существуют очевидные примеры: таблицы,

множества, списки, вектора, словари и т.д. Такой класс имеет

операцию "вставить", обычно он также имеет операции для проверки

того, был ли вставлен данный элемент. В нем могут быть действия для

осуществления проверки всех элементов в определенном порядке, и

кроме всего прочего, в нем может иметься операция для удаления

элемента. Обычно контейнерные (то есть, вмещающие) классы имеют

конструкторы и деструкторы.

Скрытие данных и продуманный интерфейс может дать концепция

модуля (см. например #4.4: файлы как модули). Класс, однако,

является типом. Чтобы использовать его, необходимо создать объекты

этого класса, и таких объектов можно создавать столько, сколько

нужно. Модуль же сам является объектом. Чтобы использовать его, его

надо только инициализировать, и таких объектов ровно один.

5.3.1 Альтернативные Реализации

Пока описание открытой части класса и описание функций членов

остаются неизменными, реализацию класса можно модифицировать не

влияя на ее пользователей. Как пример этого рассмотрим таблицу

имен, которая использовалась в настольном калькуляторе в Главе 3.

Это таблица имен:

struct name {

char* string;

char* next;

double value;

};

Вот вариант класса table:

- стр 150 -

// файл table.h

class table {

name* tbl;

public:

table() { tbl = 0; }

name* look(char*, int = 0);

name* insert(char* s) { return look(s,1); }

};

Эта таблица отличается от той, которая определена в Главе 3 тем,

что это настоящий тип. Можно описать более чем одну table, можно

иметь указатель на table и т.д. Например:

#include "table.h"

table globals;

table keywords;

table* locals;

main() {

locals = new table;

// ...

}

Вот реализация table::look(), которая использует линейный поиск в

связанном списке имен name в таблице:

#include

name* table::look(char* p, int ins)

{

for (name* n = tbl; n; n=n->next)

if (strcmp(p,n->string) == 0) return n;

if (ins == 0) error("имя не найдено");

name* nn = new name;

nn->string = new char[strlen(p)+1];

strcpy(nn->string,p);

nn->value = 1;

nn->next = tbl;

tbl = nn;

return nn;

}

Теперь рассмотрим класс table, усовершенствованный таким образом,

чтобы использовать хэшированный просмотр, как это делалось в

примере с настольным калькулятором. Сделать это труднее из-за того

ограничения, что уже написанные программы, в которых использовалась

только что определенная версия класса table, должны оставаться

верными без изменений:

- стр 151 -

class table {

name** tbl;

int size;

public:

table(int sz = 15);

~table();

name* look(char*, int = 0);

name* insert(char* s) { return look(s,1); }

};

В структуру данных и конструктор внесены изменения, отражающие

необходимость того, что при использовании хэширования таблица

должна иметь определенный размер. Задание конструктора с параметром

по умолчанию обеспечивает, что старая программа, в которой не

указывался размер таблицы, останется правильной. Параметры по

умолчанию очень полезны в ситуации, когда нужно изменить класс не

повлияв на старые программы. Теперь конструктор и деструктор

создают и уничтожают хэш-таблицы:

table::table(int sz)

{

if (sz < 0) error("отрицательный размер таблицы");

tbl = new name*[size=sz];

for (int i = 0; inext) {

delete n->string;

delete n;

}

delete tbl;

}

Описав деструктор для класса name можно получить более простой и

ясный вариант table::~table(). Функция просмотра практически

идентична той, которая использовалась в примере настольного

калькулятора (#3.1.3):

- стр 152 -

#include

name* table::look(char* p, int ins)

{

int ii = 0;

char* pp = p;

while (*pp) ii = ii<<1 ^ *pp++;

if (ii < 0) ii = -ii;

ii %= size;

for (name* n=tbl[ii]; n; n=n->next)

if (strcmp(p,n->string) == 0) return n;

if (ins == 0) error("имя не найдено");

name* nn = new name;

nn->string = new char[strlen(p)+1];

strcpy(nn->string,p);

nn->value = 1;

nn->next = tbl[ii];

tbl[ii] = nn;

return nn;

}

Очевидно, что функции члены класса должны заново компилироваться

всегда, когда вносится какое-либо изменение в описание класса. В

идеале такое изменение никак не должно отражаться на пользователях

класса. К сожалению, это не так. Для размещения переменной

классового типа компилятор должен знать размер объекта класса. Если