Главная страница
Навигация по странице:

  • 1.6. Реализация преобразования типов

  • 1.7. Обработка исключений

  • 1.8. Статические члены класса

  • 1.9. Члены класса создаваемые автоматически

  • 1.10. Семантика перемещения: move-конструктор и move-operator=

  • Пособие по ооп. С++ ООП УчебноеПособие. Объектноориентированное


    Скачать 1.49 Mb.
    НазваниеОбъектноориентированное
    АнкорПособие по ооп
    Дата23.11.2021
    Размер1.49 Mb.
    Формат файлаpdf
    Имя файлаС++ ООП УчебноеПособие.pdf
    ТипУчебное пособие
    #279608
    страница3 из 7
    1   2   3   4   5   6   7

    1.5.
    Перегрузка префиксной и постфиксной операций
    инкремента
    При перегрузке операций инкремента и декремента нужно учитывать, что они имеют две формы префиксную и постфиксную.
    Пример 5. Реализовать класс – Time («Время»). Для него перегру- зить операцию инкремента в двух формах (увеличение времени на одну се- кунду). Требования к классу представлены на UML диаграмме (рис.1.2).
    Рис. 1.2. UML диаграмма класса Time
    //classTime.h
    #ifndef CLASSTIME_H
    #define CLASSTIME_H class Time { private: int sec,min,hour; void increment(); // Функция-утилита public:
    Time(int _hour=0,int _min=0,int _sec=0);
    // Конструктор с параметрами по умолчанию int getHour() const;
    Time operator++();
    //Префиксная форма инкремента
    Time operator++(int);
    //Постфиксная форма инкремента
    };
    #endif
    Реализация функций класса Time в файле classTime.cpp:

    37
    //classTime.cpp
    #include "classTime.h"
    Time::Time(int _hour, int _min, int _sec) :hour(_hour), min(_min), sec(_sec){} int Time::getHour() const{ return hour;
    }
    Time Time::operator++() { increment(); return *this;
    }
    // фиктивный целый параметр не имеет имени
    Time Time::operator++(int) {
    Time temp(*this); increment(); return temp;
    }
    // Функция-утилита увеличения времени на 1 секунду void Time::increment() { sec++; if (sec == 60) { sec = 0; min++;
    } if (min == 60) { min = 0; hour++;
    }
    }
    В данном классе используется конструктор с параметрами по умолчанию.

    Нельзя в классе объявлять одновременно конструктор по умолчанию и конструктор со всеми параметрами по умолчанию. Это приведет к ошиб- ке. Компилятор не сможет определить, какой из конструкторов нужно вызвать.
    Реализация конструктора использует инициализаторы элементов.
    В связи с этим тело конструктора пусто.
    Time::Time(int _hour,int _min,int _sec): hour(_hour),min(_min),sec(_sec){}

    38
    В общем случае такой способ не является обязательным, но в некото- рых ситуациях без него не обойтись. Например, константные элементы класса должны получать начальные значения с помощью инициализаторов элементов. Присваивания в теле конструктора в этом случае недопустимы.
    Член-функцию класса можно объявить константной, если она не должна изменять значение полей объекта. Например, int Time::getHour() const{ return hour;
    }

    Рекомендуется все функции для доступа к полям на чтение (getXX- функции) объявлять константными.
    Это становится важным, если мы хотим объявить объект-константу для данного класса. Такой объект может использовать только константные член-функции.
    Например, можно объявить объект-константу для некоторого эталона времени const Time X(12,0,0);
    Для объекта X можно использовать только функцию getHour().

    Уберите квалификатор const из функции getHour()
    и попробуйте использовать его для константного объекта.
    При перегрузке операции инкремента (декремента) для получения возможности использования и префиксной, и постфиксной форм, каждая из этих двух перегруженных функций-операций должна иметь разную сигна- туру. Это даст возможность компилятору определить, какая версия инкре- мента имеется в виду в каждом конкретном случае.
    Допустим, мы объявили объект t типа Time:
    Time T;

    39
    По соглашению, принятому в С++, когда компилятор встречает вы- ражение с префиксным инкрементом ++t, генерируется вызов функции- элемента t.operator++(), объявление которой должно иметь вид:
    Time operator++();
    Когда компилятор встречает выражение постфиксной формы инкре- мента t++, он генерирует вызов функции t.operator++(0), объявлени- ем которой является:
    Time operator++(int);
    Нуль (0) в генерируемом вызове функции является чисто формаль- ным значением, введенным для того, чтобы сделать список аргументов функции operator++, используемой для постфиксной формы инкремен- та, отличным от списка аргументов функции operator++, используемой для префиксной формы инкремента. Заметьте, что формальный параметр является фиктивным, и поэтому не имеет имени.
    Обе формы перегрузки операции ++ используют private функцию increment. Это связано с нетривиальным алгоритмом увеличения време- ни на одну секунду.
    // Функция-утилита увеличения времени на 1 секунду void Time:: increment() { sec++; if (sec==60) { sec=0; min++;
    } if (min==60) { min=0; hour++;
    }
    }
    Перегруженная операция префиксного инкремента возвращает копию текущего объекта с измененным временем. Это происходит потому, что те- кущий объект *this возвращается как объект класса Time, что активизи- рует конструктор копии.

    40
    Time Time::operator++() { increment(); return *this;
    }
    Чтобы эмулировать действие постфиксного инкремента, необходимо вернуть неизмененную копию объекта Time. При входе в operator++ текущий объект *this сохраняется во временном объекте temp. Затем вызывается increment(), чтобы инкрементировать объект. В итоге, воз- вращается неизмененная копия объекта temp.
    // фиктивный целый параметр не имеет имени
    Time Time::operator++(int) {
    Time temp(*this); increment(); return temp;
    }
    Отметим, что эта функция не может вернуть ссылку на объект класса
    Time
    , потому что значение, которое надо вернуть, сохраняется в локальной переменной в определении функции. Локальные переменные уничтожают- ся, когда функция, в которой они объявлены, завершена. Таким образом, объявление типа, возвращаемого функцией, как Time&, привело бы к ссылке на объект, который после возвращения больше не существует.

    Возвращение ссылки на локальную переменную является типичной ошибкой, которую трудно найти.
    Пример функции main для класса Time:
    #include
    #include "classTime.h" using namespace std; void main () {
    Time t1; Time t2(11,59,59); cout<}

    41
    1.6.
    Реализация преобразования типов
    В языке С++ определены операции преобразования между встроен- ными типами данных. А преобразования между встроенными типами и ти- пами, определенными пользователями, или только между типами, опреде- ленными пользователями, требуют определения. Для этого используются или конструкторы преобразований, или операции преобразований. Выбор механизма реализации преобразования зависит от типов данных, между ко- торыми производится преобразование.
    Конструктор преобразования (конструктор с единственным аргу- ментом) может быть использован для преобразования объектов разных ти- пов (включая встроенные типы) в объекты данного класса.
    Операция преобразования (называемая также операцией приведения) может быть использована для преобразования объекта одного класса в объ- ект другого класса или в объект встроенного типа. Такая операция преобра- зования должна быть нестатической функцией-членом. Операция преобра- зования этого вида не может быть дружественной функцией.
    Пример 6. Реализовать класс «Строка». Предусмотреть конструктор для преобразования строк в стиле С char* в объекты класса «Строка» и операцию приведения для преобразования класса «Строка» в строку в стиле С char*. Требования к классу представлены на UML диаграмме
    (рис. 1.3).
    //STR.h
    #ifndef STR_H
    #define STR_H
    #include using namespace std; class Str { friend ostream & operator << (ostream &, const Str &); friend istream & operator >> (istream &, Str &); public:
    Str(const char * = "");
    //конструктор преобразования

    42
    Str(const Str &);
    //конструктор копии

    Str();
    //деструктор
    Str & operator= (const Str &); //присваивание
    Str & operator+= (const Str &); //сцепление (конкатенация) bool operator!() const;
    //проверка строки на пустоту bool operator== (const Str &) const;//проверка на равенство char & operator[] (int);
    //получение ссылки на символ char operator[] (int) const; //получение символа по номеру
    Str operator() (int, int); //получение подстроки int getLength() const;
    //определение длины строки operator char*() const;
    //операция приведения к char* private: char * sPtr;
    //указатель на начало строки int length;
    //длина строки
    };
    #endif
    Рис.1.3. UML диаграмма класса Str
    //STR.cpp
    // Определение некоторых функций элементов класса Str
    #include
    #include
    #include
    #include
    #include "str.h"

    43
    // конструктор преобразования: преобразовывает char* в Str
    Str::Str(const char* s) { length=(int)strlen(s); sPtr=new char[length + 1]; assert(sPtr != 0);
    //завершение, если память не выделена strcpy(sPtr,s);
    }
    // конструктор копии
    Str::Str(const Str & copy) { length=copy.length; sPtr=new char[length + 1]; assert(sPtr != 0);
    //завершение, если память не выделена strcpy(sPtr,copy.sPtr);
    }
    // деструктор
    Str::Str() { delete [] sPtr; }
    //операция присваивания
    Str & Str::operator =(const Str & right) { if (&right != this) { delete [] sPtr; length = right.length; sPtr = new char[length + 1]; assert(sPtr != 0); strcpy(sPtr, right.sPtr);
    } return *this;
    }
    //сцепление (конкатенация)
    Str & Str::operator +=(const Str & right) { char * tempPtr = sPtr; length += right.length; sPtr = new char [length+1]; assert(sPtr!=0); strcpy(sPtr, tempPtr); strcat(sPtr, right.sPtr); delete [] tempPtr; return *this;
    }
    //проверка строки на пустоту bool Str::operator !() const { return length ==0;
    }

    44
    //проверка двух строк на равенство bool Str::operator == (const Str & right) const { if (length!=right.length) return 0; return strcmp(sPtr, right.sPtr) == 0;
    }
    //получение ссылки на символ char & Str::operator[] (int ind) {
    //проверка, не находится ли индекс вне диапазона assert(ind >= 0 && ind < length); return sPtr[ind];
    //создание L-величины
    }
    //получение символа char Str::operator[] (int ind) const{
    //проверка, не находится ли индекс вне диапазона assert(ind >= 0 && ind < length); return sPtr[ind];
    //создание R-величины
    }
    //получение подстроки заданной длины, начинающейся с заданного
    //индекса
    Str Str::operator() (int ind, int sublength) {
    //проверка, что индекс в диапазоне и длина подстроки >=0 assert(ind >= 0 && ind < length && sublength>=0);
    Str sub;
    //определение длины подстроки if ((sublength==0) || (ind+sublength>length)) sub.length=length-ind+1; else sub.length=sublength+1;
    //выделение памяти для подстроки sub.sPtr=new char[sub.length]; assert(sub.sPtr!=0);
    //копирование подстроки в новый Str strncpy(sub.sPtr,&sPtr[ind],sub.length); sub.sPtr[sub.length]='\0'; //завершение новой строки sPtr return sub;
    }
    //определение длины строки int Str::getLength() const { return length;
    }
    //операция приведения к char*
    Str::operator char*() const { char *c=new char[length];

    45 assert(sPtr != 0); strcpy(c,sPtr); return c;
    }
    //перегруженная операция вывода данных ostream &operator << (ostream & output, const Str & s) { output< //возврат ссылки на поток для возможности многократного
    //последовательного использования операции << return output;
    }
    //перегруженная операция ввода данных istream &operator >> (istream & input, Str & s) { char temp[100]; //буфер для хранения входных данных; input>>setw(100)>>temp; s=temp; return input;
    }
    // String.cpp
    #include "Str.h" int main() { char *s=new char[100]; cin>>s;
    Str s1("Hello"),s2(s), s3; cin>>s3; cout<>s; return 0;
    }
    Объявление перегруженной функции-операции приведения типа из определенного пользователем типа Str в тип char*: operator char*() const;
    Перегруженная функция-операция приведения не указывает тип воз- вращаемой величины, потому что им является тип, к которому преобразо- ван объект.

    46
    Если s – объект класса Str, то когда компилятор встречает выраже- ние (char*)s, он порождает вызов s.operator char*(). Для этого вызова операнд s – это объект класса Str, для которого была активизиро- вана функция-элемент operator char*.
    Конструктор преобразования получает аргумент char* и создает объект Str
    Str(const char * = "");
    Конструктор с одним параметром char* преобразует соответствую- щую строку в объект Str, который затем присваивается создаваемому объ- екту Str.

    Любой конструктор с единственным аргументом можно рассматривать как конструктор преобразования.
    Одной из особенностей операций приведения и конструкторов пре- образований является то, что при необходимости компилятор может вызы- вать эти функции автоматически для создания временных объектов.
    Если в программе в том месте, где ожидается char*, появился объ- ект s определенного пользователем типа Str, то в этом случае компилятор для преобразования объекта в char* вызывает перегруженную функцию- операцию приведения operator char* и использует в выражении ре- зультирующий char*. Например: cout << s;
    Поэтому операция приведения для класса Str позволяет не перегру- жать операцию << (поместить в поток), предназначенную для вывода Str с использованием cout.

    Уберите из примера 5 перегрузку операции вывода в поток и про- верьте работоспособность программы.

    47
    Наличие конструктора преобразования означает, что нет необходи- мости применять перегруженную операцию специально для присваивания строк символов объектам Str. Компилятор автоматически активизирует конструктор преобразования для создания временного объекта Str, содер- жащего строку символов. Затем активизируется перегруженная операция присваивания, чтобы присвоить временный объект другому объекту Str.

    Некоторые преобразования типов могут быть источниками ошибок.
    Например, при преобразовании от вещественного типа к целому может быть получен некорректный результат, хотя преобразование из целого типа в вещественный всегда гарантирует правильный результат. Поэтому преоб- разование из целого типа в вещественный является безопасным, а обратное
    — небезопасным. Для выполнения небезопасных преобразований типов используется операция явного преобразования [2, раздел 4.3].
    Поскольку любой конструктор с одним параметром будет использо- ваться всегда по умолчанию для неявного преобразования типа, то для за- прета неявного преобразования необходимо дополнить такой конструктор модификатором explicit.
    В классе Str конструктор с параметром char * использовался спе- циально для демонстрации возможности неявного преобразования. Поэто- му он был описан без модификатора explicit.
    Чтобы пояснить ситуацию, когда необходимо использовать модифи- катор explicit, рассмотрим следующий пример. Допустим, необходимо к строке, представляемой классом Str, дописать константную строку, со- держащую число, например, номер курса. Сделаем это, используя перегру- женную операцию +=.
    Str s1("Hello"); s1+="2"; // s1 содержит Hello2

    48
    Предположим, при наборе кода программы сделана ошибка — оказа- лись пропущены кавычки. s1+=2; // сообщение об ошибке компиляции
    В нашем примере произойдёт ошибка компиляции, т.к. компилятор не найдет перегруженной операции += с правым операндом целого типа и не определено приведение целого типа к типу Str.
    Если бы в классе Str был определён конструктор преобразования из типа int, такая операция была бы разрешена и не ошибка компиляции не возникла бы. Предположим, что конструктор с одним параметром типа int существует и предназначен для формирования строки с заданным ко- личеством пробелов.
    Str(int n);
    //конструктор неявного преобразования
    Str::Str(int n) { length = n; sPtr = new char[length + 1]; assert(sPtr != 0);
    //завершение, если память не выделена for (int i = 0; i < length; ++i) sPtr[i] = ' '; sPtr[length] = 0;
    }
    По правилам языка С++ он является конструктором преобразования типа, но его назначением является формирование пробельной строки за- данной длины. В этом случае результат операции с пропущенными кавыч- ками окажется неожиданным s1+=2; // s1 содержит Hello и еще два пробела
    Именно поэтому следовало указать компилятору, что мы не хотим рассматривать такой конструктор как конструктор неявного преобразова- ния. В этом случае конструктор нужно объявить с модификатором explicit. explicit Str(int n);
    //конструктор преобразования, вызываемого явно
    Str::Str(int n) { length = n;

    49 sPtr = new char[length + 1]; assert(sPtr != 0);
    //завершение, если память не выделена for (int i = 0; i < length; ++i) sPtr[i] = ' '; sPtr[length] = 0;
    }
    При использовании explicit конструктора будут получены сле- дующие результаты: s1+=2; // сообщение об ошибке компиляции s1+=Str(2); //s1 содержит Hello и еще два пробела

    В C++11 ключевое слово explicit применимо и к операторам преоб- разования. По аналогии с конструкторами, оно защищает от непредвиден- ных неявных преобразований.
    1.7.
    Обработка исключений
    Из-за большого количества недиагностируемых ошибок времени вы- полнения при работе со строками char* в примере 6 используется макрос assert. void assert(int выражение)
    Он позволяет включить в программу диагностические сообщения о таких ошибках. Например, assert(sPtr != 0); //завершение, если память не выделена или assert(ind >= 0 && ind < length && sublength>=0);
    //проверка, что индекс находится в диапазоне и длина подстроки >=0
    Если значение выражения есть нуль, то assert(выражение) напечатает сообщение следующего вида:
    Assertion failed: выражение, file имя файла, line nnn
    После чего будет вызвана функция abort, которая завершит вычисления.

    50
    Основное применение макроса assert — диагностика ошибок на этапе отладки приложения. В частности, такой механизм широко применя- ется при создании тестов, в том числе и unit-тестов.
    Но в некоторых случаях ошибки времени выполнения могут возни- кать и в отлаженной программе. Например, ошибки при выделении дина- мической памяти или несоответствия типов входных данных. Для обработ- ки таких ситуаций используется механизм исключений.
    Когда во время выполнения программы происходит ошибка, генери- руется так называемое исключение (исключительная ситуация), которое можно перехватить и обработать. Если исключение не обработать, то программа завершится с ошибкой.
    Если в программе возникла исключительная ситуация и в текущем контексте не хватает информации для принятия решения о том, как дейст- вовать дальше, информацию об ошибке можно передать во внешний, более общий контекст. Для этого в программе создается объект с информацией об исключении, который затем «запускается» из текущего контекста (гово- рят, что в программе запускается исключение).
    Для работы с исключениями можно использовать как встроенные ти- пы [2, раздел 1.15], так и создаваемые пользователями классы исключений.
    В простейшем случае класс исключения может быть пустым. Этого достаточно, если нам нужно только просигнализировать о возникновении ситуации и никакие данные передавать обработчику не нужно.
    Например, создадим класс, описывающий ошибку, возникающую при невозможности выделить динамическую память. class OutOfMemoryException {};
    Чтобы воспользоваться классом исключения нужно assert заме- нить запуском исключения.

    51
    //assert(sPtr!=0); if (sPtr==0) throw OutOfMemoryException();
    В этом случае определение перегруженной операции конкатенации будет выглядеть следующим образом:
    //сцепление (конкатенация)
    Str & Str::operator +=(const Str & right) { char * tempPtr = sPtr; length += right.length; sPtr = new char [length+1]; if (sPtr==0) throw OutOfMemoryException(); strcpy(sPtr, tempPtr); strcat(sPtr, right.sPtr); delete [] tempPtr; return *this;
    }
    Оператор генерации исключения throw производит целый ряд дей- ствий. Сначала создаётся копия запускаемого объекта, которая возвращает- ся из функции, содержащей throw, даже если тип объекта исключения не соответствует типу, который положено возвращать этой функции. При этом управление передается в специальную часть программы, называемую
    обработчиком исключения; она может находиться далеко от того места, где было запущено исключение. Также уничтожаются все локальные объекты, созданные к моменту запуска исключения. Автоматическое уничтожение локальных объектов называется «раскруткой стека».

    Создайте класс исключения BadIndexException для контроля вы- хода индекса за границы. Замените все использования assert генерацией исключений соответствующих типов.
    Поскольку ситуация невозможности выделить память встречается крайне редко, рассмотрим ошибку, связанную с индексацией. После прове- дённых изменений в программе неправильное значение индекса и в случае использования макроса assert, и в случае оператора throw приводит к

    52 аварийному завершению программы. Различаются только сообщения, опи- сывающие причину завершения программы. Но в отличие от использова- ния assert сгенерированные исключения можно перехватывать и обраба- тывать в программе без обязательного аварийного завершения.
    В том случае, когда команда throw не должна приводить к аварий- ному завершению, следует создать специальный блок, который должен реагировать на исключение. Этот блок, называемый блоком try , пред- ставляет область видимости, перед которой ставится ключевое слово try: try {
    // Программный код, который может генерировать исключения
    }
    При использовании обработки исключений выполняемый код поме- щается в блок try, а обработка исключений производится после блока try
    . Это существенно упрощает написание и чтение программы, посколь- ку основной код не смешивается с кодом обработки ошибок. Часто не сам код, который может генерировать исключения, а вызов функции, содержа- щей этот код, помещают в блок try.
    Программа должна где-то среагировать на запущенное исключение.
    Это место называется обработчиком исключения. В программу необходимо включить обработчик исключения для каждого типа перехватываемого ис- ключения. Обработчик может перехватывать как определенный тип ис- ключения, так и исключения классов, производных от этого типа.
    Обработчики исключений следуют сразу же за блоком try и обозна- чаются ключевым словом catch: try {
    // Программный код, который может генерировать исключения
    } catch (type1 id1){
    // Обработка исключений типа type1
    }

    53 catch (typeN idN){
    // Обработка исключений типа typeN
    }
    //Здесь продолжается нормальное выполнение программы...
    Если id1, ..., idN не нужны, их можно опускать.
    Если в программе запускается исключение, механизм обработки ис- ключений начинает искать первый обработчик с аргументом, соответст- вующим типу исключения. Управление передается в найденную секцию catch
    , и исключение считается обработанным (т. е. дальнейший поиск об- работчиков прекращается). Выполняется только нужная секция catch, а работа программы продолжается, начиная с позиции, следующей за по- следним обработчиком для данного блока try.
    Иногда требуется написать обработчик для перехвата любых типов исключений. Для этой цели используется специальный список аргументов в виде многоточия (...): catch (...){ cout<<"an exception was thrown"<}
    Поскольку такой обработчик перехватывает все исключения, он раз- мещается в конце списка обработчиков.
    Рассмотрим использование в операции индексирования исключения
    BadIndexException для контроля выхода индекса за границы строки.
    //получение ссылки на символ char & Str::operator[] (int ind) {
    //проверка, не находится ли индекс вне диапазона if (ind < 0 || ind >= length) throw BadIndexException(); return sPtr[ind];
    //создание L-величины
    }
    Тип исключения обычно предоставляет достаточно информации для определения характера ошибки, например:
    Str s="New string for tests"; for (int i=0; i<10; ++i){ cout<<"Input number of symbol"<

    54 cin>> i; try { cout< } catch (BadIndexException){
    //обработка исключения
    .. cout<<"illegal index"< }
    }
    Рассмотренный цикл при любых входных данных будет выполнен 10 раз, но в случаях выхода за границы вместо символа строки будет выведено сообщение: illegal index.
    Иногда кроме типа исключения обработчику желательно передавать дополнительную информацию, например, значение индекса, вызвавшего исключение. Тогда для исключений нужно использовать классы, имеющие конструктор с параметрами, поля и методы.
    Расширим класс BadIndexException, чтобы можно было переда- вать значение индекса в обработчик. class BadIndexException { private: int ind; public:
    BadIndexException(int i): ind(i) {} int getInd() const { return ind;
    }
    };
    Теперь при выбрасывании исключения нужно указать значение не- корректного индекса. throw BadIndexException(ind);
    Тогда обработчик будет выглядеть следующим образом: catch (BadIndexException e){
    //обработка исключения
    .. cout<<"illegal index"< }
    Механизм исключений в первую очередь предназначен для разделе- ния места, где возникает ошибка, и места, где она обрабатывается. Поэтому

    55 чаще всего исключения, выбрасываемые в функциях, обрабатываются там, где эти функции вызываются. При этом такие функции могут входить в со- став библиотек, поставляемых сторонними разработчиками. При этом мо- гут быть известны только заголовки функций. Чтобы сообщить, какие ис- ключения может выбрасывать функция, в её объявление можно добавить список возможных исключений. char & operator[] (int ind) throw (BadIndexException);
    В объявлении функции после списка аргументов указана необяза- тельная спецификация исключений. Спецификация исключений состоит из ключевого слова throw, за которым в круглых скобках перечисляются ти- пы всех потенциальных исключений, которые могут запускаться данной функцией.

    Вообще говоря, вы не обязаны сообщать пользователям вашей функции, какие исключения она может запускать. Однако такое поведение считается нецивилизованным – оно означает, что пользователи не будут знать, как написать код перехвата потенциальных исключений.
    1.8.
    Статические члены класса
    Классы могут содержать статические поля и статические функции.
    Если данные-члены объявлены с квалификатором static, то для всех объектов класса поддерживается только одна копия таких данных. Стати- ческий член используется совместно всеми объектами данного класса. Для того чтобы существовал статический член, не обязательно, чтобы сущест- вовали объекты такого класса.
    Пример 7. Реализовать класс, позволяющий подсчитывать количест- во существующих объектов данного класса. class Counter { private:

    56 static int count; public: static int getCount() { return count;
    }
    Counter() {
    ++count;
    }
    Counter() {
    --count;
    }
    }; int Counter::count = 0; int main() { cout << "before " << Counter::getCount() << endl;
    Counter first; cout << "after first " << first.getCount() << endl;
    Counter * second = new Counter(); cout << "after second " << second->getCount() << endl; delete second; cout << "after delete second " << Counter::getCount() << endl; return 0;
    }
    Внутри класса возможно только объявление статического члена, но не его определение. private: static int count;
    Статические данные можно использовать только после определения.
    Это делается путем нового объявления статической переменной, причем используется оператор разрешения области видимости для того, чтобы идентифицировать тот класс, к которому принадлежит переменная. int Counter::count = 0;
    Статические члены-данные подчиняются правилам доступа к членам класса. Однако определение статических членов-данных выполняется вне зависимости от спецификатора доступа.

    57

    Определение статических членов-данных класса рекомендуется распо- лагать вместе с определением самого класса в заголовочном файле.
    К статическим членам класса можно обращать как через класс, так и через объект или указатель/ссылку на объект. cout << "before " << Counter::getCount() << endl;
    Counter first; cout << "after first " << first.getCount() << endl;
    Counter * second = new Counter(); cout << "after second " << second->getCount() << endl;
    Статические функции-члены не могут обращаться к нестатическим данным или вызывать нестатические функции этого же класса. Это связано с тем, что в статические функции не передается указатель this на объект, для которого вызвана функция.
    Реализация статической функции записывается так же, как и реализа- ция любой другой функции-члена класса. При этом если реализация вне класса, то ключевое слово static не указывается.
    Например, определение статической функции getCount вне класса могло бы выглядеть следующим образом int Counter::getCount() { return count;
    }
    1.9.
    Члены класса создаваемые автоматически
    Поскольку некоторые члены класса создаются компилятором автома- тически, следует понимать принципы их создания и знать ситуации, в ко- торых они создаются. Это позволит в одних случаях избежать ошибок, а в других – эффективно использовать действия по умолчанию.
    Рассмотрим следующий «не очень полезный» класс. class Empty {};
    На самом деле, такое описание класса эквивалентно следующему:

    58 class Empty{ public:
    Empty() {}
    Empty(Empty const &) {}
    Empty & operator = (Empty const &) {}
    Empty() {}
    };
    Здесь присутствуют 4 функции:
    Empty() ― конструктор без параметров,
    Empty(Empty const &) ― конструктор копий,
    Empty & operator =(Empty const &) ― операция копирующего присваивания,
    Empty() ― деструктор.
    Если класс не имеет членов-данных, то эти функции не выполняют никаких действий.
    Напомним, что класс для описания исключения часто можно оста- вить пустым, используя принцип определения по умолчанию. И тогда его описание будет выглядеть именно таким образом.
    Добавим член класса value в определение «не очень полезного» пустого класса. class NotQuiteEmpty{ public: int value;
    };
    У классов, имеющих члены-данные, конструктор без параметров и деструктор, создаваемые по умолчанию, остаются пустыми. Конструктор копии, создаваемый по умолчанию, выполняет побитовое копирование членов-данных. Операция присваивания, создаваемая по умолчанию, вы- полняет побитовое присваивание.
    Принцип создания функций-членов класса по умолчанию состоит в следующем: если любая из перечисленных функций, кроме конструктора без параметров, явно не задана, она создаётся неявно по умолчанию. Прин-

    59 цип создания функций-членов класса по умолчанию для конструктора без параметров имеет особенность. Такой конструктор создается неявно, толь- ко если не задан явно ни один конструктор. Если в классе будет явно опи- сан хотя бы один конструктор, неявного создания конструктора без пара- метров не произойдёт. А это в дальнейшем при использовании класса мо- жет привести к ошибкам.
    На практике, иногда, создаваемые неявно функции могут привнести нежелательную функциональность, от которой нужно избавиться. Напри- мер, нужно создать класс, для которого должен существовать только один экземпляр. Такие классы называются синглетонами. Для них рекомендует- ся запретить операцию присваивания и конструктор копирования. С другой стороны, рекомендуется наряду с другими конструкторами всегда иметь конструктор без параметров, даже если его тело является пустым.
    В C++ предусмотрен механизм управления генерацией стандартных конструкторов и функций. Рассмотрим его на примере класса A: class A { private: int x; public:
    A(int i) {…}
    // Данная запись указывает на необходимость сгенерировать
    // конструктор по умолчанию
    A() = default;
    // Запретить генерацию конструктора копии по умолчанию
    A(const A&) = delete;
    // А так можно запретить генерацию operator=
    A& operator = (const A&) = delete;
    }

    60
    1.10.
    Семантика перемещения: move-конструктор и
    move-operator=
    Семантика перемещения появилась в стандарте С++11. Она нацелена на уменьшение количества создаваемых копий объектов при выполнении конструктора копии и операции присваивания, которые вызываются для rvalue выражений. Любое выражение в C++ является или левосторонним
    (lvalue), или правосторонним (rvalue). Выражение lvalue — это объект, ко- торый имеет имя. Все переменные являются lvalue. А выражение rvalue — это временный безымянный объект, не существующий за пределами того выражения, которое его создало. Более подробно rvalue и lvalue выражения в С++ рассматривались в [2, раздел 1.13].
    Для иллюстрации проблемы лишних копий рассмотрим упрощённую реализацию класса для динамического массива.
    Пример 8. Реализовать move-семантику на примере упрощённого класса для динамического массива.
    Для этого достаточно описать конструктор с параметрами по умолча- нию и конструктор копии, деструктор и перегрузить операцию сложения двух массивов одинакового размера и операцию присваивания. Чтобы от- слеживать процесс создания и удаления объектов, в классе добавлены два поля:
     поле name — имя, отражающее способ создания объекта;
     статическое поле f для подсчёта существующих в текущий момент объектов. Значение поля f увеличивается каждый раз при выполнении операции new и уменьшается при выполнении операции delete.
    #include
    #include using namespace std; class myvector { private:

    61 static int f; string name; int size; int * vect; public: myvector(int s = 1, string nm ="noname"): size(s), name(nm){ f++; cout << "constructor > " << name<<" "< } myvector(const myvector & v): size(v.size){ f++; name = "Copy ( "+v.name+" )"; cout << "copy > " << name << " " << f << endl; vect = new int[v.size]; for (int i = 0; i }
    myvector(){ f--; cout << "destructor > " << name << " " << f << endl; delete[] vect;
    } myvector& operator= (const myvector &v){ cout << name <<" operator= > " << v.name <<" "<< f << endl; if (&v != this) { delete[] vect; vect = new int[v.size]; for (int i = 0; i } return *this;
    } myvector operator+(const myvector& v){ if (size == v.size) { myvector v1(size, name + " + "+v.name); for (int i = 0; i < size; ++i) v1.vect[i] = vect[i] + v.vect[i]; return v1;
    } return *this;//лучше использовать исключение
    }
    };
    В следующей программе выражениями rvalue являются a + b внут- ри вызова конструктора копии для объекта сс и a + D в операторе при-

    62 сваивания. Именно для них будут создаваться дополнительные временные объекты, которые после использования в конструкторе копии и операции присваивания тут же будут удалены.
    #include "vect.h" int myvector::f = 0; int main() { myvector a(3,"A"), b(3,"B"),D(a); myvector cc (a + b); a = b; b = a + D; return 0;
    }
    В окне вывода можно проследить последовательность вызовов кон- структоров и деструкторов для данной программы (рис. 1.4).
    Рис.1.4. Последовательность вызовов конструкторов и деструкторов
    Каждая строка содержит информацию о вызываемой функции (кон- структор, деструктор, операция присваивания), имени объекта и количестве объектов, существующих в текущий момент. При этом можно заметить создание и уничтожение временных объектов, а также накладные расходы на копирование этих объектов.

    63
    Например, при создании объекта myvector cc (a + b) сначала создаётся временный объект при вычислении значения a + b. Затем вы- зывается конструктор копии myvector(const myvector & v), в ко- торый в качестве значения передаётся, созданный ранее, временный объект с именем A + B. После копирования временный объект удаляется. В ре- зультате происходит лишнее выделение памяти и копирование данных од- ного и того же объекта.

    Во многих современных компиляторах встроен механизм Return Value
    Optimization (RVO), решающий, в том числе, и эту проблему. Однако авто- матическая оптимизация не всегда бывает эффективной, поэтому в стан- дарте C++11 Бьярн Страуструп предложил вынести решение на уровень языка. Для этого были введены move-конструктор и move-operator=
    Идея move-семантики состоит в том, чтобы не удалять временный объект и не выделять память для полей в новом объекте, а инициализиро- вать поля в создаваемом объекте ссылками на поля временного объекта.
    Деструкторы для временных переменных вызываются в тот момент, когда эти переменные уже не используются для вычислений. Однако при использовании move-семантики деструктор не должен освобождать память временного объекта, поскольку ссылкой на нее инициализируется поле в другом объекте. Для этого в move-конструкторе и move-операции присваи- вания поле указатель временного объекта меняет значение на nullptr, а в деструкторе выделенная память освобождается только, если поле указатель не равен nullptr.

    Конструктор копирования выделяет новую область памяти для хранения данных, вызывая оператор new, а перемещающий конструктор — забирает данные у переданного ему временного объекта.

    64
    Добавим в класс move-конструктор, move-операцию присваивания и внесём изменения в деструктор.
    //move-конструктор myvector(myvector&& v){ name = "Move_Copy ( " + v.name+" )"; cout << "move > " << name << " " << f << endl; size = v.size; vect= v.vect;
    // Не позволит сразу удалить временный объект v.vect = nullptr;
    }
    //измененный деструктор
    myvector(){ if (vect != nullptr) { f--; cout << "destructor > " << name << " " << f << endl; delete[] vect;
    }
    }
    //move-операция присваивания myvector& operator=(myvector&& v){ cout << name <<" operator-move= > "<< v.name <<" "<< f <}
    Чтобы отличать функции с перемещающей семантикой в стандарте
    С++11 введены rvalue ссылки — myvector && v. При этом компилятор будет использовать функции с перемещающей семантикой только в случае, если параметром является rvalue выражение (временный объект).
    Теперь, при выполнении той же самой программы, для строки myvector cc(a+b); компилятор выберет move-конструктор вместо конструктора копии. А для строки b = a + D; компилятор выберет move- операцию присваивания. Результат выполнения приведен на рисунке 1.5.

    65
    Рис.1.5. Последовательность вызовов для move-семантики

    Вследствие наличия большого количества стандартных классов исполь- зовать move-конструкторы и move-операции присваивания приходится редко, однако знание такого механизма необходимо.

    66
    1   2   3   4   5   6   7


    написать администратору сайта