Некоторые функции-члены класса являются специальными в том смысле, что они влияют на создание, копирование и уничтожение объектов класса или задают способ приведения значений к значениям других типов. Часто такие специальные функции вызываются неявно.
Функция-член класса с тем же именем, что и у класса, называется конструктором. Она используется для построения объектов этого класса. Конструктор не должен возвращать никакого значения, даже void.
Если класс имеет конструктор, все объекты этого класса будут проинициализированы. Если конструктору требуются параметры, они должны быть предоставлены.
Конструкторы подчиняются тем же правилам разрешения перегрузки, что и остальные функции. На конструкторы также распространяются действие спецификаторов прав доступа.
Запись : <имя члена класса>(<значение>) [, …] называется списком инициализации (членов класса). С его помощью выполняется именно инициализация членов класса. Это может быть принципиально, например, для константных членов класса.
Переменные в списке инициализации инициализируются не в том порядке, в котором они указаны, а в том порядке, в котором объявлены в классе, поэтому следует соблюдать следующие рекомендации:
Конструктор умолчания класса Х – это конструктор класса Х, вызываемый без параметров. Конструктор умолчания обычно имеет вид Х::Х(), однако и конструктор, который может вызываться без параметров, потому что имеет параметры с умолчанием, например, Х::Х(int = 0), также считается конструктором умолчания. При отсутствии других объявленных конструкторов, конструктор умолчания генерируется компилятором.
Конструктор копирования для класса Х – это конструктор, который может быть вызван для копирования объекта класса Х, т.е. такой конструктор, который может быть вызван с одним параметром – ссылкой на объект класса Х. Например, X::X(const X&) и X::X(X&, int = 0) являются конструкторами копирования.
При отсутствии объявленных конструкторов копирования компилятор генерирует публичный конструктор копирования. Генерируемый конструктор копирования выполняет побитовое копирование объекта. Этот метод годится лишь при отсутствии в объекте каких-либо дополнительных ресурсов, например, указателей, которые хранят адреса динамически распределенной памяти. Сгенерированный конструктор скопирует адрес, а не содержимое памяти, таким образом, два разных объекта будут ссылаться на один и тот участок памяти и меняться синхронно, что не является ожидаемым поведением. В таком случае программист обязательно должен сам написать конструктор копирования, который будет, в частности, копировать содержимое динамически распределённой памяти.
Однако в тех случаях, когда копирующий конструктор по умолчанию имеет правильный смысл, лучше полагаться на это умолчание. Это короче, а читающие код должны понимать умолчания. Кроме того, компилятор знает об этом умолчании и о возможностях его оптимизации.
А написание почленного копирования классов с большим количеством членов данных вручную – занятие утомительное, и при этом можно наделать массу ошибок.
Конструктор копирования – и определяемый пользователем, и генерируемый компилятором – используется:
Семантика этих операций по определению совпадает с семантикой инициализации.
От вызовов конструктора копирования легко избавиться. С тем же успехом можно записать следующее.
В то время как цель семантики копирования состоит в том, чтобы выполнять копирование одного объекта в другой, цель семантики перемещения состоит в том, чтобы переместить владение ресурсами из одного объекта в другой (что менее затратно, чем выполнение операции копирования).
Конструктор перемещения для класса Х – это конструктор, который может быть вызван с одним параметром – правосторонней ссылкой на объект класса Х. Конструктор перемещения не получает ссылок на константы, поскольку исходный объект должен перестать владеть какими-то ресурсами, т.е. должен быть изменён.
Конструктор перемещения вызывается, когда параметром конструктора будет литерал или временный объект.
Как это следует из названия, конструктор перемещения используется для реализации семантики перемещения, т.е. для осуществления перемещения данных во время инициализации и конструирования новых объектов, что позволяет сократить издержки на копирование. Семантика перемещения даёт широкие возможности для оптимизации внутреннего кода вызовов функций. Эта оптимизация достигается отказом от копирования данных при создании временных объектов, у которых отсутствует необходимость сохранять свои внутренние ресурсы для дальнейшего использования.
Конструкторам разрешено вызывать другие конструкторы. Этот процесс называется делегированием конструкторов. Чтобы один конструктор вызывал другой, нужно просто сделать вызов этого конструктора в списке инициализации. Конструктору, который вызывает другой конструктор, не разрешается выполнять какую-либо инициализацию членов класса. Делегирующим конструктором может быть любой конструктор, например, конструктор копирования может вызвать конструктор умолчания.
Зачем используется делегирование конструкторов? Во-первых, таким образом можно избежать дублирования кода. Если один конструктор делает некоторые действия, а другой конструктор – те же самые действия и ещё что-то, то можно не переписывать одинаковые действия, а вместо этого вызвать один конструктор из другого.
Вторая причина более интересная. Объект считается созданным после завершения работы первого (вызываемого) конструктора. И если в вызывающем (делегирующем) конструкторе произойдёт какой-то сбой, то деструктор всё равно будет вызваться, т.к. объект считается созданным.
Рассмотрим пример создания класса для динамической матрицы. Если память частично будет выделена, а потом произойдёт ошибка, то выделенная память освобождена не будет.
Если же мы добавим конструктор умолчания и вызовем его из конструктора, создающего матрицу, то матрица будет считаться созданной уже после вызова конструктора умолчания, и деструктор будет вызван даже при преждевременном завершении конструктора (например, при возникновении ошибок при выделении памяти).
Нестатические неконстантные члены класса можно инициализировать следующими способами:
Действия выполняются именно в таком порядке, т.е. если использовать два способа, то следующий будет аннулировать результаты предыдущего.
Ещё один важный момент – члены класса инициализируются в том порядке, в котором они объявлены, не зависимо от порядка в списке инициализации. Поэтому лучше в списке инициализации указывать члены класса в таком же порядке, в каком они объявлены. И совсем не допустимо создавать какие-либо зависимости между инициализацией разных членов класса.
В приведённом выше примере сначала инициализируется член класса a. Инициализируется он значением члена класса b, который к этому моменту ещё не проинициализирован несмотря на то, что в списке инициализации член класса b указан ранее. Поэтому член класса a получает неопределённое значение.
Однако в теле конструктора присваивания выполняются в том порядке, в каком они записаны.
Нестатические константные члены класса могут быть проинициализированы непосредственно при объявлении или в списке инициализации. Присвоить им значение в конструкторе нельзя. Если не проинициализировать константный член класса при объявлении, то компилятор потребует определить конструктор, который будет инициализировать его в списке инициализации, иначе будет выдаваться сообщение об ошибке.
Функция-член класса Х с именем ~Х называется деструктором. Она используется для разрушения значения класса Х непосредственно перед разрушением содержащего его объекта. Деструктор не имеет параметров и возвращаемого типа, нельзя задавать даже void.
Деструкторы автоматически вызываются, когда
Деструктор в классе может быть только один (т.к. он не имеет параметров), и он должен быть публичным, иначе компилятор не сможет вызвать его при необходимости.
Деструктор может также вызываться явным образом.
Преобразования (изменения типа) объектов класса выполняются конструкторами и преобразующими функциями.
Такие преобразования, называемые пользовательскими, часто неявно применяются в дополнение к стандартным преобразованиям. Например, функция, ожидающая параметр типа Х, может вызываться не только с параметром типа Х, но и с параметром типа T, если существует преобразование из T в Х. Кроме того, пользовательские преобразования применяются для приведения инициализаторов, параметров функций, возвращаемых функциями значений, операндов в выражениях, управляющих выражений, операторах цикла и выбора и для явного приведения типов.
Пользовательские преобразования применяются только там, где они однозначны.
Конструктор с одним параметром задаёт преобразование типа своего параметра к типу своего класса.
Конструктор с одним параметром не обязательно вызывается явно.
Однако неявное преобразование может быть нежелательно в некоторых случаях.
Неявное преобразование можно подавить, объявив конструктор с модификатором explicit. Такой конструктор будет вызваться только явно.
Функция-член класса Х, имя которой имеет вид operator <имя типа>, определяет преобразование из Х в тип, заданный именем типа. Такие функции называются преобразующими функциями или функциями приведения.
Для такой функции не могут быть заданы ни параметры, ни возвращаемый тип. Но, на самом деле, возвращаемое значение у неё есть, его тип совпадает с типом, к которому осуществляется преобразование. Соответственно, функция должна иметь оператор return.
Присваивание значения типа V объекту класса X допустимо в том случае, если имеется оператор присваивания X::operator= (Z) такой, что V является Z или существует единственное преобразование V в Z. Инициализация рассматривается аналогично.
В некоторых случаях значение требуемого типа может быть создано при помощи повторного использования конструкторов или операторов преобразования. Это должно осуществляться при помощи явных преобразований – допустим только один уровень неявных преобразований, определяемых пользователем. В некоторых случаях значение требуемого типа может быть создано более чем одним способом – такое недопустимо.
Правила преобразования не являются ни самыми простыми для реализации из всех возможных, ни самыми легкими для документирования, ни настолько общими, как можно себе представить. Однако они довольно безопасны, и их применение не приводит к неприятным сюрпризам. Гораздо легче вручную разрешить неоднозначности, чем найти ошибку, вызванную преобразованием, о котором и не подозревали.
По умолчанию компилятор генерирует для класса следующий набор функций-членов класса:
Этот набор является базовым, без этих функций невозможны создание объектов класса и работа с ними.
Здесь приведён тривиальный пример, в котором все перечисленные функции-члены класса создаются по умолчанию. Однако, если какие-то функции определяются явно, то другие могут не генерироваться компилятором. Правила следующие.
Второе правило ослабляется для совместимости со старыми версиями – копирующие операции генерируются несмотря на явное объявление деструктора.
Что делать, если нужны все функции? Можно явно написать конструктор умолчания или другую нужную функцию. А можно использовать ключевое слово default, которое указывает компилятору, что нужно создать стандартный вариант той или иной функции.
Отличие между стандартным конструктором умолчания и пустым конструктором умолчания, написанным явно, проявляется при инициализации членов класса. Стандартный конструктор умолчания не инициализирует члены класса базовых типов, и при создании переменной она будет считаться неинициализированной, что приведёт к ошибке компиляции. Пустой конструктор умолчания тоже не инициализирует поля базовых типов, но компилятор считает, что программист сделал, что хотел, и ошибок не выдаёт.
При желании можно явно отказаться от использования в классе того или иного стандартного конструктора с помощью ключевого слова delete.
Если удалить конструктор копирования, то конструктор умолчания тоже будет удалён. Восстановить его можно с помощью ключевого слова default, как это сделано в примере выше.
Компилятор не случайно не генерирует все пять операций – конструктор копирования, конструктор перемещение, присваивание копированием, присваивание перемещением и деструктор – при явной реализации одной из них. Если одна из операций должна быть определена программистом, то это означает, что версия, сгенерированная компилятором, не удовлетворяет потребностям класса в одном случае и, вероятно, не будет удовлетворять и в остальных случаях.
Разработку класса необходимо начинать с интерфейса, т.е. с открытых (публичных) функций, которые будут использоваться остальной частью программы для взаимодействия с разрабатываемым классом. Для стека определены две операции: взятие элемента из стека и добавление элемента в стек. Также определим конструктор умолчания и вспомогательные функции, проверяющие пуст ли стек и была ли ошибка при работе со стеком. Таким образом, интерфейс класса следующий.
Теперь можно разрабатывать структуру данных для класса. Для хранения элементов стека будем использовать массив. Также нам понадобятся переменная, указывающая на текущий элемент стека, и переменная, хранящая признак ошибки.
Кроме этого, необходимо, естественно, определить функции, входящие в класс. Простейшие из них напишем в определении класса, чтобы они стали встраивымыми. После этого можно использовать разработанный класс.
После некоторого размышления разработчик пришел к выводу, что использование статического массива для хранения элементов стека ограничивает разработанный класс, и решил использовать динамически распределяемый массив, размер которого можно будет при необходимости увеличить. Таким образом, вместо массива мы объявляем указатель и ещё одну переменную, которая будет хранить размер динамически распределённого массива.
В результате этих изменений разработчику пришлось также изменить конструктор умолчания, в который добавлено динамическое распределение памяти, и функцию добавления элемента в стек, которая при необходимости перераспределяет выделенную память. Также необходимо явно реализовать деструктор, который будет освобождать выделенную память. При реализации деструктора, согласно ослабленному правилу, компилятор генерирует конструктор копирования и операцию присваивания копированием. Но для стека это не разумно, поэтому удалим эти функции-члены класса. Конструктор перемещения и присваивание перемещением не генерируются. Остальные функции остались без изменений.
Обратите внимание, – и это самое главное, – что основная программа осталась без изменений. Поскольку разработчик поступил правильно и начал с разработки интерфейса, изменения в самом классе не затронули остальную программу. Конечно, для стека всё просто, т.к. набор необходимых функций мал и хорошо известен. Но разработку класса всегда надо начинать с разработки интерфейса. Чем лучше будет продуман интерфейс используемых в программе классов, чем меньше будет проблем при разработке программы.
В данном примере не очень хорошо реализована обработка ошибок. Более современный и удобный способ – использование исключений. Также стек может хранить данные любого типа. Для этого он должен быть реализован как шаблон.
1. Конструкторы
class Complex
{ private:
double r, m;
public:
Complex(double r, double m) : r(r), m(m) {}
...
};
Complex c1(5, -2);
class X
{ private:
const double x;
public:
X(double _x) { x = _x; } // Ошибка
X(double _x) : x(_x) { } // Правильно
...
};
1.1. Конструктор умолчания
class Complex
{ private:
double r, m;
public:
Complex() : r(0), m(0) {}
...
};
Complex x; // Вызов конструктора умолчания
class Complex
{ private:
double r, m;
public:
Complex(double nr = 0, double nm = 0) : r(nr), m(nm) {}
...
};
Complex y1(-6, 3); // Вызов конструктора с параметрами
Complex y2; // Конструктор вызывается как конструктор умолчания
1.2. Конструктор копирования
Complex x = 2; // Создает Complex(2), затем копирует его в x
Complex y = Complex(2, 0); // Создает Complex(2, 0), затем копирует его в у
Complex x(2); // Проинициализировать x значением 2
Complex y(2, 0); // Проинициализировать у значением (2, 0)
1.3. Конструктор перемещения
class Vector
{ private:
int size;
int *vector;
public:
Vector(int s);
Vector(const Vector &v); // Конструктор копирования
Vector(Vector &&v) noexcept; // Конструктор перемещения
~Vector();
};
Vector::Vector(int s)
{ size = s;
vector = new int [size];
}
// Конструктор копирования – копируем элементы вектора
Vector::Vector(const Vector& v)
{ size = v.size;
vector = new int [size];
for (int i = 0; i < size; i++)
vector[i] = v.vector[i];
}
// Конструктор перемещения – копируем только указатель, обнуляем указатель в исходном объекте
Vector::Vector(Vector &&v) noexcept
{ size = v.size;
vector = v.vector;
v.size = 0;
v.vector = nullptr;
}
// Деструктор должен обязательно проверять значение указателя
Vector::~Vector()
{ if (vector) delete[] vector; }
// Функция f возвращает локальную переменную
Vector f()
{ Vector v = Vector(7); return v; }
// Используется конструктор перемещения, т.к. после выполнения функции локальные переменные удаляются
int main()
{ Vector v = f(); }
1.4. Делегирующий конструктор
class Matrix
{ private:
int rows, cols;
int **matrix;
public:
Matrix(int r, int c);
~Matrix();
};
Matrix::Matrix(int r, int c)
{ rows = r;
cols = c;
matrix = new int* [rows];
for (int i = 0; i < rows; i++)
matrix[i] = new int[cols];
}
Matrix::~Matrix()
{ for (int i = 0; i < rows; i++)
delete[] matrix[i];
delete[] matrix;
}
class Matrix
{ private:
int rows, cols;
int **matrix;
public:
Matrix();
Matrix(int r, int c);
~Matrix();
};
Matrix::Matrix()
{ rows = cols = 0;
matrix = nullptr;
}
Matrix::Matrix(int r, int c) : Matrix()
{ rows = r;
cols = c;
matrix = new int* [rows];
for (int i = 0; i < rows; i++)
matrix[i] = nullptr;
for (int i = 0; i < rows; i++)
matrix[i] = new int[cols];
}
Matrix::~Matrix()
{ if (!matrix) return;
for (int i = 0; i < rows; i++)
if (matrix[i]) delete[] matrix[i];
delete[] matrix;
}
1.5. Инициализация членов класса
class X
{ private:
int a = 1;
int b = int();
int c = int(2);
int d = {3};
int e {};
int f {4};
public:
X() : a(3), b(2) { } // a = 3, b = 2
X(int x) : a(3), b(2) { b = x; } // a = 3, b инициализируется параметром конструктора
};
class X
{ private:
int a, b;
public:
X() : b(5), a(b) { }
};
class X
{ private:
int a, b;
public:
X() { b = 5; a = b; }
};
class X
{ private:
const int a = 10;
};
class X
{ private:
const int a;
public
:
X() : a(10) { }
};
2. Деструкторы
class X
{ private:
int n;
public:
X();
~X();
};
X x;
x.~X(); // Явный вызов деструктора
3. Преобразования
3.1. Преобразование посредством конструктора
class X
{ private:
int x;
public:
X(int n);
...
};
X::X(int n) { x = n; }
X a = 1; // Эквивалентно X a = X(1)
class Str
{ private:
char *str;
public:
Str(int n) { str = new char [n + 1]; *str = 0; }
Str(const char *p) { str = new char [strlen(p) + 1]; strcpy(str, p); }
~Str() { if (str) delete[] str; }
};
Str s = 'a'; // Создание строки из int('a') элементов
class Str
{ private:
char *str;
public:
explicit Str(int n) { str = new char [n + 1]; *str = 0; }
Str(const char *p) { str = new char [strlen(p) + 1]; strcpy(str, p); }
~Str() { if (str) delete[] str; }
};
Str s1 = 'a'; // Ошибка – нет неявного преобразования char в Str
Str s2(10); // Правильно – создаётся строка из 10 символов
3.2. Преобразующие функции
class X
{ private:
int x;
public:
X(int n);
operator int();
...
};
X::X(int n) { x = n; }
X::operator int() { return x; }
int a;
X b(0);
a = (int)b; // Явный вызов преобразующей функции
a = b; // Неявный вызов преобразующей функции
3.3. Разрешение неоднозначности
class X { ... X(int); X(char*); ... };
class Y { ... Y(int); ... };
class Z { ... Z(X); ... };
X f(X);
Y f(Y);
Z g(Z);
int main()
{ f(1); // Неоднозначность – f(X(1)) или f(Y(1))?
f(X(1)); // Правильно
f(Y(1)); // Правильно
g("Mask"); // Ошибка – требуется применение двух преобразований, определённых пользователем
g(X("Mask")); // Правильно – g(Z(X("Mask")))
g(Z("Mask")); // Правильно – g(Z(X("Mask")))
}
class T { T(int); };
void h(double);
void h(T);
int main()
{ h(1); } // h(double(1)) или h(T(1))? Вызов h(1) означает h(double(1))
// потому что в этом случае используются только стандартные преобразования
4. Особенности создания конструкторов
class Pair
{ private:
int x, y;
};
int main()
{ Pair a; // Вызывается конструктор умолчания
Pair b(a); // Вызывается конструктор копирования
b = a; // Вызывается операция присваивание копированием
b = Pair(); // Вызывается операция присваивание перемещением
} // Деструктор вызывается неявно
class Pair
{ private:
int x, y;
public:
Pair(int _x, int _y) : x(_x), y(_y) { }
};
int main()
{ Pair a; // Ошибка – нет конструктора умолчания
}
class Pair
{ private:
int x, y;
public:
Pair(const Pair& p) : x(p.x), y(p.y) { }
};
int main()
{ Pair a; // Ошибка – нет конструктора умолчания
}
class Pair
{ private:
int x, y;
public:
Pair() = default;
Pair(int _x, int _y) : x(_x), y(_y) { }
Pair(const Pair& p) = default;
};
int main()
{ Pair a;
Pair b(a);
Pair c(2, 5);
c = a;
c = Pair(3, 9);
}
class Pair
{ private:
int x, y;
public:
Pair() = default;
Pair(int _x, int _y) : x(_x), y(_y) { }
Pair(const Pair& p) = default;
Pair& operator= (const Pair& p) { x = p.x; y = p.y; } // Операция присваивания копированием
};
int main()
{ Pair a;
Pair b(a);
Pair c(2, 5);
c = a;
c = Pair(3, 9); // Ошибка – нет операции присваивания перемещением
}
class Pair
{ private:
int x, y;
public:
Pair() = default;
Pair(int _x, int _y) : x(_x), y(_y) { }
Pair(const Pair& p) = default;
Pair& operator= (const Pair& p) { x = p.x; y = p.y; }
Pair& operator= (Pair&& p) = default; // Операция присваивания перемещением
};
int main()
{ Pair a;
Pair b(a);
Pair c(2, 5);
c = a;
c = Pair(3, 9);
}
class Pair
{ private:
int x, y;
public:
Pair() = default;
};
int main()
{ Pair a; // Ошибка
}
class Pair
{ private:
int x, y;
public:
Pair() { };
};
int main()
{ Pair a; // Нет ошибки
}
class Pair
{ private:
int x, y;
public:
Pair() = default;
};
int main()
{ Pair a; // Нет ошибки
}
class Pair
{ private:
int x, y;
public:
Pair() = default;
Pair(int _x, int _y) : x(_x), y(_y) { }
Pair(const Pair& p) = delete; // Теперь объект класса нельзя скопировать или присвоить
Pair& operator= (const Pair& p) = delete;
};
4. Пример «Разработка стека»
Первый вариант
class Stack
{ public:
Stack(); // Конструктор
int Push(int n); // Добавление элемента в стек
int Pop(); // Взятие элемента из стека
int IsEmpty() const; // Проверка, пуст ли стек
int IsError() const; // Проверка, была ли ошибка
const char* LastError() const; // Функция, возвращающая строку описания ошибки
};
#include <stdio.h>
class Stack
{ private:
// Область действия константы, определённой с помощью define, - файл.
// Чтобы локализовать область действия константы в классе, можно использовать перечислимый тип
enum { SIZE = 100 };
enum { NO_ERROR, STACK_EMPTY, STACK_FULL };
int stack[SIZE]; // Массив для хранения элементов стека
int *cur; // Указатель на текущий элемент стека
int error; // Признак ошибки
public:
Stack() { cur = stack; error = NO_ERROR; }
int Push(int n);
int Pop();
int IsEmpty() const { return cur == stack; } // Константные функции-члены класса могут применяться
int IsError() const { return error != NO_ERROR; } // как к константным, так и к неконстантным объектам
const char* LastError() const;
};
int Stack::Push(int n)
{ if (cur - stack < SIZE)
{ *cur++ = n; error = NO_ERROR; return 1; }
else
{ error = STACK_FULL; return 0; }
}
int Stack::Pop()
{ if (cur != stack)
{ error = NO_ERROR; return *--cur; }
else
{ error = STACK_EMPTY; return 0; }
}
const char* Stack::LastError() const
{ if (error == NO_ERROR)
return "There is no error";
else if (error == STACK_EMPTY)
return "Stack is empty";
else
return "Stack is full";
}
int main()
{ Stack s;
s.Push(1);
s.Push(2);
s.Push(3);
while (!s.IsEmpty())
printf("%d\n", s.Pop());
printf("%d\n", s.Pop());
printf("%s\n", s.LastError()); // Ошибка – попытка взять элемента из пустого стека
for (int i = 0; i < 110; i++)
s.Push(i);
if (s.IsError())
printf("%s\n", s.LastError()); // Ошибка – попытка положить элемент в заполненный стек
}
Второй вариант
#include <stdio.h>
#include <malloc.h>
class Stack
{ private:
enum { SIZE = 100 };
enum { NO_ERROR, STACK_EMPTY, NOT_ENOUGH_MEMORY };
int size;
int *stack;
int *cur;
int error;
public:
Stack();
~Stack();
Stack(const Stack& s) = delete; // Удаляем конструктор копирования
Stack& operator= (const Stack& s) = delete; // Удаляем операцию присваивания
int Push(int n);
int Pop();
int IsEmpty() const { return cur == stack; }
int IsError() const { return error != NO_ERROR; }
const char* LastError() const;
};
Stack::Stack()
{ size = SIZE;
stack = NULL;
if (stack = (int *)malloc(size * sizeof(int)))
{ cur = stack;
error = NO_ERROR;
}
else
{ error = NOT_ENOUGH_MEMORY;
size = 0;
}
}
Stack::~Stack()
{ if (stack) free(stack); }
int Stack::Push(int n)
{ if (!stack) return 0;
if (cur - stack < size)
{ *cur++ = n; error = NO_ERROR; return 1; }
else
if (stack = (int *)realloc(stack, (size + SIZE) * sizeof(int)))
{ cur = stack + size;
size += SIZE;
*cur++ = n;
error = NO_ERROR;
return 1;
}
else
{ error = NOT_ENOUGH_MEMORY; size = 0; return 0; }
}
int Stack::Pop()
{ if (cur != stack)
{ error = NO_ERROR; return *--cur; }
else
{ error = STACK_EMPTY; return 0; }
}
const char* Stack::LastError() const
{ if (error == NO_ERROR)
return "There is no error";
else if (error == STACK_EMPTY)
return "Stack is empty";
else
return "There is not enough memory";
}
int main()
{ Stack s;
s.Push(1);
s.Push(2);
s.Push(3);
while (!s.IsEmpty())
printf("%d\n", s.Pop());
printf("%d\n", s.Pop());
printf("%s\n", s.LastError()); // Ошибка – попытка взять элемента из пустого стека
for (int i = 0; i < 110; i++)
s.Push(i);
if (s.IsError())
printf("%s\n", s.LastError()); // Ошибка – попытка положить элемент в заполненный стек
}