Смекни!
smekni.com

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

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

переменную этого класса так, чтобы конструктор не был вызван. Если

класс имеет конструктор, не получающий параметров, то этот

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

одного параметра.

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&bsol;"; // 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 << "&bsol;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) после расширения преврашался в описание класса