От: | LaptevVV | ||
Дата: | 16.02.09 10:07 | ||
Оценка: |
Поля-массивы в классе
В С++ нет запрета на объявление в классе поля-массива. Естественно, размер класса с полем-массивом увеличивается на размер массива. Однако использование массива в классе недостаточно хорошо отражено в стандарте. Это вызывает многочисленные вопросы, особенно связанные с инициализацией. Мы начнем разбираться с этими вопросами на самом простом примере, в котором объявляются и инициализируются несколько полей-массивов (листинг 4.1).
Листинг 4.1. Поля-массивы в классе
class Arrays { int m0[10]; static const unsigned int k = 10; enum { n = 10 }; int m1[k]; int m2[n]; public: Arrays() // конструктор { for (int i = 0; i < k; ++i) m0[i]=m1[i]=m2[i]=0; } };
В классе Arrays объявлено три поля-массива: m0, m1 и m2. Как видим, задать количество элементов массива можно либо явной константой, либо константным выражением со статическими и (или) перечислимыми константами, причем константы должны быть определены раньше массива. Задавать количество элементов поля-массива обязательно.
Нельзя задать количество элементов как значение другого поля, заполняемого конструктором, даже если это поле константное. Например, пусть в классе объявлены следующие поля:
const int k; int m[k];
В этом случае система Visual C++.NET 2003 выдает ошибку компиляции C2327, которая сигнализирует о том, что k в данном случае не является ни статической константой, ни константой перечислимого типа.
Конструктор без аргументов Array() обнуляет массивы, выполняя цикл в теле. Такой способ инициализации поля-массива — в теле конструктора — является наиболее простым и позволяет присваивать элементам массива любые значения. Однако наиболее часто выполняется обнуление, поэтому для массивов разрешается применять инициализацию нулем. Тогда наш конструктор выглядит значительно проще:
Arrays(): m0(), m1(), m2() {}
Для массива объектов некоторого типа такая запись означает вызов конструктора по умолчанию (без аргументов), и не забудьте, что этот вызов выполняется для каждого элемента массива.
К сожалению, применение списков инициализации для полей-массивов этим и ограничивается — в скобках нельзя ничего указывать. Например, при написании следующей конструкции система Visual C++.NET 2003 выдаст сообщение об ошибке С2536, сигнализируя о том, что явная инициализация массивов запрещена:
Arrays(): m0(1,2,3,4,5), m1(), m2() {}
Попытки написать в скобках некоторое одиночное выражение (например, вызов функции, заполняющей массив) компилятор отвергает по той же причине.
Явная инициализация не проходит даже для символьных массивов. Например, объявим в классе Arrays символьный массив:
char s[4];
После этого попытаемся в списке инициализации конструктора присвоить этому массиву символьную константу:
Arrays(): m0(), m1(), m2(), s("abc") {}
Тогда в системе Visual C++.NET 2003 мы получим сообщение об ошибке:
error C2536: 'Arrays::Arrays::s' :
cannot specify explicit initializer for arrays
Это сообщение говорит о том, что нельзя определять явный инициализатор для массивов.
Тем не менее, можно задать в классе указатель на символ:
char *s;
Затем можно инициализировать указатель символьной константой в списке инициализации конструктора, как было показано ранее.
Как это ни странно, но большие проблемы возникают при попытках объявить в классе константный массив встроенного типа! Как мы уже выяснили, константы нельзя инициализировать в теле конструктора, значения им присваиваются только в списке инициализации конструктора. Однако для константного массива встроенного типа не работает даже инициализация нулем.
ПРИМЕЧАНИЕ
Этот вопрос практически не отражен в стандарте, поэтому компиляторы ведут себя по-разному. В системе Visual C++.NET 2003 выдается ошибка компиляции C2439, а Borland C++ Builder 6 выдает только предупреждение W8038 о том, что массив не инициализируется.
Не проходит и отмена константности. Например, зададим массив m0 как константный, а в теле конструктора определим инициализацию в цикле:
for (int i = 0; i < 10; ++i) const_cast<int>(m0[i]) = 0;
Однако и Visual C++.NET 2003, и Borland C++ Builder 6 отказываются компилировать такой цикл.
Но для константного массива из объектов не встроенного типа задавать инициализацию нулем разрешается. Для этого в классе должен быть определен конструктор без аргументов, который вызывается для инициализации каждого элемента константного поля-массива. Например, вполне можно инициализировать константный массив денег:
const TMoney ss[10];
Для этого достаточно задать в списке инициализации конструктора инициализацию нулем ss(). Как реально инициализируется такой массив, конечно, зависит от реализации конструктора по умолчанию.
Статические поля-массивы
Методы перевода перевода регистра в классе, реализующем строки, должны обеспечивать перевод и русских, и английских букв. Простейшая реализация методов преобразования в верхний и нижний регистры может быть такой, как в листинге 4.19. Поскольку перевод регистра — разовое действие, то пусть обе операции возвращают значение.
Листинг 4.19. Простейшая реализация методов изменения регистра строки
TString TString::operator++() // toUpper { const char lower[59] = "abcdefghijklmnopqrstuvwxyzабвгдежзийклмнопрстуфхцчшщъыьэюя"; const char upper[59] = "ABCDEFGHIJKLMNOPQRSTUVWXYZАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ"; for(int i = 0; i<this->Length(); i++) { for (int j = 0; j < 59; j++) if (s[i] == lower[j]) s[i]=upper[j]; } return *this; } TString TString::operator--() // toLower { const char lower[59] = "abcdefghijklmnopqrstuvwxyzабвгдежзийклмнопрстуфхцчшщъыьэюя"; const char upper[59] = "ABCDEFGHIJKLMNOPQRSTUVWXYZАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ"; for(int i = 0; i<this->Length(); i++) { for (int j = 0; j < 59; j++) if (s[i] == upper[j]) s[i]=lower[j]; } return *this; }
Методы прекрасно работают, однако таком виде они нас, конечно, не устраивают — любой программист тут же скажет, что использование локальных массивов очень неэффективно. Массивы заново создаются при каждом входе в функцию — на это тратится и время и место при выполнении программы.
Первое побуждение — вынести массивы из методов и объявить два поля-массива. Но тут нас ожидают некоторые сложности. Во-первых, поля нельзя объявить константными, хотя по сути своей они таковыми и являются. Дело в том, что тогда мы не сможем эти поля инициализировать — константные массивы встроенного типа никаким образом не инициализируются . Но если массивы не константные, то их придется заполнять в каждом конструкторе, написав для этого приватную функцию.
Во-вторых, и это гораздо важнее, неконстантные поля-массивы существенно увеличивают размер класса — на 118 байт. Пока объектов немного, это не доставляет беспокойства. Но представьте, что нам надо объявить массив размером в 1000 элементов типа TString. Ситуация отнюдь не надуманная, так как подобный массив, например, в качестве буфера может использовать простой текстовый редактор. Но такой массив имеет уже 118 000 лишних байтов, которые еще и заполняются во время создания — ведь конструктор вызывается для каждого элемента массива. Ситуация совершенно неприемлемая как с точки зрения расхода памяти, так и с точки зрения эффективного выполнения программы.
Так как массивы у нас символьные, то можно было бы обойтись указателями на символ — тем более, что их можно инициализировать символьной константой. Ситуация с памятью несколько улучшается: два указателя занимают в классе всего 8 байт. Однако указатели тоже будут инициализироваться для каждого создаваемого объекта.
Массивы нужно все-таки иметь в единственном экземпляре; они, очевидно, должны быть объявлены константными и проинициализированы один раз при объявлении. Мы можем это сделать, если объявим их глобальными. Однако такое решение совершенно неприемлемо с точки зрения инкапсуляции — глобальные переменные излишне доступны и их использование способствует появлению трудноуловимых ошибок. Массивы по сути своей являются (и должны быть) частью класса TString.
В языке С++ есть средство, позволяющее сделать именно то, что нам требуется: объявить массивы в единственном экземпляре, сделать их константными, проинициализировать при объявлении и, тем не менее, локализовать их в классе. Такие константные поля-массивы надо объявить в классе статическими (см. п.п. 9.4.2 в [1]):
static const char lower[59]; // маленикие буквы static const char upper[59]; // БОЛЬШИЕ БУКВЫ
Инициализация (определение) статических полей выполняется вне класса:
const char TString::lower[59] = "abcdefghijklmnopqrstuvwxyzабвгдежзийклмнопрстуфхцчшщъыьэюя"; const char TString::upper[59] = "ABCDEFGHIJKLMNOPQRSTUVWXYZАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ";
Этот оператор присваивания является определением статического члена класса (а в классе — только объявление). Такой оператор присваивания — единственная форма инициализации константных статических массивов (и любых других константных статических полей встроенного нецелочисленного типа). Обратите внимание, что слово static в операторе инициализации отсутствует — его там писать нельзя. Статические массивы, как и статические константы (и вообще любые статические поля), являются частью класса, создаются в единственном экземпляре при запуске программы (до создания любых объектов класса), поэтому их нельзя инициализировать в конструкторе.
ПРИМЕЧАНИЕ
Помимо статических полей, в классе можно объявлять и статические методы. Такие методы являются методами класса и могут вызываться до создания первого объекта этого класса.
Cтатические константы места в классе не занимают. Точно так же не увеличивают размер класса и любые статические поля — они хранятся вне класса. Но принцип инкапсуляции выполняется — доступ к таким полям имеют только методы и «друзья» класса. Наши методы перевода регистра упрощаются (листинг 4.20).
Листинг 4.20. Реализация методов перевода регистра
TString TString::operator++() // toUpper { for(int i = 0; i < this->Length(); i++) { for (int j = 0; j < 59; j++) if (s[i] == lower[j]) s[i]=upper[j]; } return *this; } TString TString::operator--() // toLower { for(int i = 0; i < this->Length(); i++) { for (int j = 0; j < 59; j++) if (s[i] == upper[j]) s[i]=lower[j]; } return *this; }
Проверим работу методов.
TString t ("Мама мыла Милу"); std::cout << t << std::endl; ++t; std::cout << t << std::endl; --t; std::cout << t << std::endl; TString b = "abCDefGH"; std::cout << b << std::endl; ++b; std::cout << b << std::endl; --b; std::cout << b << std::endl;
При выполнении этого фрагмента на экране появятся следующие строки:
Мама мыла Милу МАМА МЫЛА МИЛУ мама мыла милу abCDefGH ABCDEFGH abcdefgh
Как видим, все работает правильно.