для класса были описаны конструкторы, то невозможно описать
переменную этого класса так, чтобы конструктор не был вызван. Если
класс имеет конструктор, не получающий параметров, то этот
конструктор будет вызываться в том случае, если в описании нет ни
одного параметра.
1.11 Вектора
Встоенное в C++ понятие вектора было разработано так, чтобы
обеспечить максимальную эффективность выполнения при минимальном
расходе памяти. Оно также (особенно когда используется совместно с
указателями) является весьма универсальным инструментом для
построения средств более высокого уровня. Вы могли бы, конечно,
возразить, что размер вектора должен задаваться как константа, что
нет проверки выхода за границы вектора и т.д. Ответ на подобные
возражения таков: "Вы можете запрограммировать это сами." Давайте
посмотрим, действительно ли оправдан такой ответ. Другими словами,
проверим средства абстракции языка C++, попытавшись реализовать эти
возможности для векторных типов, которые мы создадим сами, и
посмотрим, какие с этим связаны трудности, каких это требует
затрат, и насколько получившиеся векторные типы удобны в обращении.
- стр 37 -
class vector {
int* v;
int sz;
public:
vector(int); // конструктор
~vector(); // деструктор
int size() { return sz; }
void set_size(int);
int& operator[](int);
int& elem(int i) { return v[i]; }
};
Функция size возвращает число элементов вектора, таким образом
индексы должны лежать в диапазоне 0 ... size()-1. Функция set_size
сделана для изменения этого размера, elem обеспечивает доступ к
элементам без проверки индекса, а operator[] дает доступ с
проверкой границ.
Идея состоит в том, чтобы класс сам был структурой фиксированного
размера, управляющей доступом к фактической памяти вектора, которая
выделяется конструктором вектора с помощью распределителя свободной
памяти new:
vector::vector(int s)
{
if (s<=0) error("плохой размер вектора");
sz = s;
v = new int[s];
}
Тепрь вы можете описывать вектора типа vector почти столь же
элегантно, как и вектора, встроенные в сам язык:
vector v1(100);
vector v2(nelem*2-4);
Операцию доступа можно определить как
int& vector::operator[](int i)
{
if(i<0 || sz<=i) error("индекс выходит за границы");
return v[i];
}
Операция || (ИЛИИЛИ) - это логическая операция ИЛИ. Ее правый
операнд вычисляется только тогда, когда это необходимо, то есть
если вычисление левого операнда дало ноль. Возвращение ссылки
обеспечивает то, что запись [] может использоваться с любой стороны
операции присваивания:
v1[x] = v2[y];
Функция со странным именем ~vector - это деструктор, то есть
функция, описанная для того, чтобы она неявно вызывалась, когда
объект класса выходит из области видимости. Деструктор класса C
имеет имя ~C. Если его определить как
- стр 38 -
vector::~vector()
{
delete v;
}
то он будет, с помощью операции delete, освобождать пространство,
выделенное конструктором, поэтому когда vector выходит из области
видимости, все его пространство возвращается обратно в память для
дальнейшего использования.
1.12 Inline-подстановка
Если часто повторяется обращение к очень маленькой функции, то вы
можете начать беспокоиться о стоимости вызова функции. Обращение к
функции члену не дороже обращения к функции не члену с тем же
числом параметров (надо помнить, что функция член всегда имеет хотя
бы один параметр), и вызовы в функций в C++ примерно столь же
эффективны, сколь и в любом языке. Однако для слишком маленьких
функций может встать вопрос о накладных расходах на обращение. В
этом случае можно рассмотреть возможность спецификации функции как
inline-подставляемой. Если вы поступите таким образом, то
компилятор сгенерирует для функции соответствующий код в месте ее
вызова. Семантика вызова не изменяется. Если, например, size и elem
inline-подставляемые, то
vector s(100);
//...
i = s.size();
x = elem(i-1);
порождает код, эквивалентный
//...
i = 100;
x = s.v[i-1];
C++ компилятор обычно достаточно разумен, чтобы генерировать
настолько хороший код, насколько вы можете получить в результате
прямого макрорасширения. Разумеется, компилятор иногда вынужден
использовать временные переменные и другие уловки, чтобы сохранить
семантику.
Вы можете указать, что вы хотите, чтобы функция была inline-
подставляемой, поставив ключевое слово inline, или, для функции
члена, просто включив определение функции в описание класса, как
это сделано в предыдущем примере для size() и elem().
При хорошем использовании inline-функции резко повышают скорость
выполнения и уменьшают размер объектного кода. Однако, inline-
функции запутывают описания и могут замедлить компиляцию, поэтому,
если они не необходимы, то их желательно избегать. Чтобы inline-
функция давала существенный выигрыш по сравнению с обычной
функцией, она должна быть очень маленькой.
- стр 39 -
1.13 Производные классы
Теперь давайте определим вектор, для которого пользователь может
задавать границы изменения индекса.
class vec: public vector {
int low, high;
public:
vec(int,int);
int& elem(int);
int& operator[](int);
};
Определение vec как
:public vector
означает, в первую очередь, что vec это vector. То есть, тип vec
имеет (наследует) все свойства типа vector дополнительно к тем, что
описаны специально для него. Говорят, что класс vector является
базовым классом для vec, а о vec говорится, что он производный от
vector.
Класс vec модифицирует класс vector тем, что в нем задается
другой конструктор, который требует от пользователя указывать две
границы изменения индекса, а не длину, и имеются свои собственные
функции доступа elem(int) и operator[](int). Функция elem() класса
vec легко выражается через elem() класса vector:
int& vec::elem(int i)
{
return vector::elem(i-low);
}
Операция разрешения области видимости :: используется для того,
чтобы не было бесконечной рекурсии обращения к vec::elem() из нее
самой. с помощью унарной операции :: можно ссылаться на нелокальные
имена. Было бы разумно описать vec::elem() как inline, поскольку,
скорее всего, эффективность существенна, но необязательно,
неразумно и невозможно написать ее так, чтобы она непосредственно
использовала закрытый член v класса vector. Фунции производного
класса не имеют специального доступа к закрытым членам его базового
класса.
Конструктор можно написать так:
vec::vec(int lb, int hb) : (hb-lb+1)
{
if (hb-lb<0) hb = lb;
low = lb;
high = hb;
}
Запись : (hb-lb+1) используется для определения списка параметров
конструктора базового класса vector::vector(). Этот конструктор
вызывается перед телом vec::vec(). Вот небольшой пример, который
можно запустить, если скомпилировать его вместе с остальными
описаниями vector:
- стр 40 -
#include
void error(char* p)
{
cerr << p << "n\"; // cerr - выходной поток сообщений об
ошибках
exit(1);
}
void vector::set_size(int) { /* пустышка */ }
int& vec::operator[](int i)
{
if (i
1.14 Еще об операциях
Другое направление развития - снабдить вектора операциями:
- стр 41 -
class Vec : public vector {
public:
Vec(int s) : (s) {}
Vec(Vec&);
~Vec() {}
void operator=(Vec&);
void operator*=(Vec&);
void operator*=(int);
//...
};
Обратите внимание на способ определения конструктора производного
класса, Vec::Vec(), когда он передает свой параметр конструктору
базового класса vector::vector() и больше не делает ничего. Это
полезная парадигма. Операция присваивания перегружена, ее можно
определить так:
void Vec::operator=(Vec& a)
{
int s = size();
if (s!=a.size()) error("плохой размер вектора для =");
for (int i = 0; i
void error(char* p) {
cerr << p << "\n";
exit(1);
}
void vector::set_size(int) { /*...*/ }
int& vec::operator[](int i) { /*...*/ }
main()
{
Vec a(10);
Vec b(10);
for (int i=0; i
1.15 Друзья (friend)
Функция operator+() не воздействует непосредственно на
представление вектора. Действтельно, она не может этого делать,
поскольку не является членом. Однако иногда желательно дать
функциям не членам возможность доступа к закрытой части класса.
Например, если бы не было функции "доступа без проверки"
vector::elem(), вам пришлось бы проверять индекс i на соответствие
границам три раза за каждый проход цикла. Здесь мы избежали этой
сложности, но она довольно типична, поэтому у класса есть механизм
предоставления права доступа к своей закрытой части функциям не
членам. Просто в описание класса помещается описание функции, перед
которым стоит ключевое слово friend. Например, если имеется
class Vec; // Vec - имя класса
class vector {
friend Vec operator+(Vec, Vec);
//...
};
- стр 43 -
То вы можете написать
Vec operator+(Vec a, Vec b)
{
int s = a.size();
if (s != b.size()) error("плохой размер вектора для +");
Vec& sum = *new Vec(s);
int* sp = sum.v;
int* ap = a.v;
int* bp = b.v;
while (s--) *sp++ = *ap++ + *bp++;
return sum;
}
Одним из особенно полезных аспектов механизма friend является то,
что функция может быть другом двух и более классов. Чтобы увидеть
это, рассмотрим определение vector и matrix, а затем определение
функции умножения (см. #с.8.8).
1.16 Обобщенные Вектора
"Пока все хорошо," - можете сказать вы, - "но я хочу, чтобы один
из этих векторов был типа matrix, который я только что определил."
К сожалению, в C++ не предусмотрены средства для определения класса
векторов с типом элемента в качестве параметра. Один из способов -
продублировать описание и класса, и его функций членов. Это не
идеальный способ, но зачастую вполне приемлемый.
Вы можете воспользоваться препроцессором (#4.7), чтобы
механизировать работу. Например, класс vector - упрощенный вариант
класса, который можно найти в стандартном заголовочном файле. Вы
могли бы написать:
#include
declare(vector,int);
main()
{
vector(int) vv(10);
vv[2] = 3;
vv[10] = 4; // ошибка: выход за границы
}
Файл vector.h таким образом определяет макросы, чтобы
declare(vector,int) после расширения преврашался в описание класса