При определении класса имеется возможность задать для объекта начальное значение. Специальный метод класса, называемый
Для класса String имеет смысл в качестве начального значения использовать пустую строку:
class String
{
public:
String(); // объявление конструктора
};
// определение конструктора
String::String()
{
str = 0;
length = 0;
}
Определив такой
Конструктор без аргументов называется стандартным
class String
{
public:
String(); // стандартный конструктор
String(const char* p);
// дополнительный конструктор
};
// определение второго конструктора
String::String(const char* p)
{
length = strlen(p);
str = new char[length + 1];
if (str == 0) {
// обработка ошибок
}
strcpy(str, p); // копирование строки
}
Теперь можно, создавая переменные типа String, инициализировать их тем или иным образом:
char* cp;
// выполняется стандартный конструктор
String s1;
// выполняется второй конструктор
String s2("Начальное значение");
// выполняется стандартный конструктор
String* sptr = new String;
// выполняется второй конструктор
String* ssptr = new String(cp);
String он может выглядеть следующим образом:
class String
{
public:
String(const String s);
};
String::String(const String s)
{
length = s.length;
str = new char[length + 1];
strcpy(str, s.str);
}
Очевидно, что новый объект будет копией своего аргумента. При этом новый объект независим от первоначального в том смысле, что изменение значения одного не изменяет значения другого.
// первый объект с начальным значением
// "Astring"
String a("Astring");
// новый объект – копия первого,
// т.е. со значением "Astring"
String b(a);
// изменение значения b на "AstringAstring",
// значение объекта a не изменяется
b.Concat(a);
Столь логичное поведение объектов класса String на самом деле обусловлено наличием
String::String(const String s)
{
length = s.length;
str = s.str;
}
При вызове метода Concat для объекта b произошло бы следующее: объект b перераспределил бы память под строку str, выделив новый участок памяти и удалив предыдущий (см. определение метода выше). Однако указатель str объекта a по-прежнему указывает на первоначальный участок памяти, только что b. Соответственно, значение объекта a испорчено.
Для класса Complex, который мы рассматривали ранее, кроме стандартного
class Complex
{
public:
Complex();
Complex(int rl, int im = 0);
Complex(const Complex c);
// прибавить комплексное число
Complex operator+(const Complex x) const;
private:
int real; // вещественная часть
int imaginary; // мнимая часть
};
//
// Стандартный конструктор создает число (0,0)
//
Complex::Complex() : real(0), imaginary(0)
{}
//
// Создать комплексное число из действительной
// и мнимой частей. У второго аргумента есть
// значение по умолчанию — мнимая часть равна
// нулю
Complex::Complex(int rl, int im=0) :
real(rl), imaginary(im)
{}
//
// Скопировать значение комплексного числа
//
Complex::Complex(const Complex c) :
real(c.real), imaginary(c.imaginary)
{}
Теперь при создании комплексных чисел происходит их
Complex x1; // начальное значение – ноль Complex x2(3); // мнимая часть по умолчанию равна 0 // создается действительное число 3 Complex x3(0, 1); // мнимая единица Complex y(x3); // мнимая единица
Concat несколько иначе:
Concat(String s);
вместо
Concat(const String s);
т.е. использовали бы
b.Concat(a)
компилятор создал бы временную переменную типа String – копию объекта a, и передал бы ее в качестве аргумента. При выходе из метода String эта переменная была бы уничтожена. Представляете, насколько снизилось бы быстродействие метода!
Второй пример вызова
b.Concat("LITERAL");
хотя сам метод определен только для аргумента – объекта типа String. Поскольку в классе String есть String с начальным значением "LITERAL", ссылка на нее будет передана в качестве аргумента метода String, а по завершении Concat временная переменная будет уничтожена.
Чтобы избежать подобного неэффективного преобразования, можно определить отдельный метод для работы с указателями:
class String
{
public:
void Concat(const String s);
void Concat(const char* s);
};
void
String::Concat(const char* s)
{
length += strlen(s);
char* tmp = new char[length + 1];
if (tmp == 0) {
// обработка ошибки
}
strcpy(tmp, str);
strcat(tmp, s);
delete [] str;
str = tmp;
}
Аналогично тому, что при создании объекта выполняется
У класса может быть только один ~ ’. Для объектов класса String
class String
{
~String();
};
String::~String()
{
if (str)
delete str;
}
Если
Деструктор всегда вызывается перед тем, как String был создан с помощью операции new, то при вызове
delete str;
выполняется ~String(), а затем String:
int funct(void)
{
String str;
. . .
return 0;
}
При выходе из функции funct по оператору return переменная str будет уничтожена: выполнится
В особых случаях
sptr->~String();
Такие вызовы встречаются довольно редко; соответствующие примеры будут рассматриваться позже, при описании new и delete.
Рассмотрим более подробно, как создаются объекты. Предположим, формируется объект типа Book.
Во-первых, под объект выделяется необходимое количество памяти: либо динамически, если объект создается с помощью операции
Класс Book – производный от класса Item, поэтому вначале вызывается Item.
У объекта класса Book имеются атрибуты – объекты других классов, в частности, String. После завершения
И только теперь очередь дошла до вызова Book.
В самом конце, после завершения Book, создаются структуры, необходимые для работы виртуального механизма (отсюда следует, что в
Вызов
Item::Item() : taken(false), invNumber(0)
{}
В данном случае атрибутам объекта присваиваются начальные значения. Для класса Book
Book::Book() : Item(), title("<None>"),
author("<None>"), publisher("<None>"),
year(-1)
{}
Вначале выполняется стандартный Item, а затем создаются атрибуты объекта с некими начальными значениями. Теперь предположим, что у классов Item и Book есть не только стандартные Item
class Item
{
public:
Item(long in) { invNumber = in; };
. . .
};
class Book
{
public:
Book(long in, const String a,
const String t);
. . .
};
Тогда Book имеет смысл записать так:
Book::Book(long in, const String a,
const String t) :
Item(in), author(a), title(t)
{}
Такого же результата можно добиться и при другой записи:
Book::Book(long in, const String a,
const String t) :
Item(in)
{
author = a;
title = t;
}
Однако предыдущий вариант лучше. Во втором случае вначале для атрибутов author и title объекта типа Book вызываются стандартные String, вы можете убедиться, насколько эффективнее первый вариант Book.
Встречается еще один случай, когда без
class A
{
public:
A(const String x);
private:
String str_ref;
};
A::A(const String x) : str_ref(x)
{}
Создавая объект класса A, мы задаем строку, на которую он будет ссылаться. Ссылка A будет ссылаться на одну и ту же строку. Выбор ссылки в качестве атрибута класса обычно как раз и определяется тем, что ссылка
Рассмотрим еще один пример использования ссылки в качестве атрибута класса. Предположим, что в нашей библиотечной системе книги, журналы, альбомы и т.д. могут храниться в разных хранилищах. Хранилище описывается объектом класса Repository. У каждого элемента хранения есть атрибут, указывающий на его хранилище. Здесь может быть два варианта. Первый вариант – элемент хранения хранится всегда в одном и том же месте, переместить книгу из одного хранилища в другое нельзя. В данном случае использование ссылки полностью оправдано:
class Repository
{
. . .
};
class Item
{
public:
Item(Repository rep) :
myRepository(rep) {};
. . .
private:
Repository myRepository;
};
При создании объекта необходимо указать, где он хранится. Изменить хранилище нельзя, пока данный объект не уничтожен. Атрибут myRepository всегда ссылается на один и тот же объект.
Второй вариант заключается в том, что книги можно перемещать из одного хранилища в другое. Тогда в качестве атрибута класса Item лучше использовать указатель на Repository:
class Item
{
public:
Item() : myRepository(0) {};
Item(Repository* rep) :
myRepository(rep) {};
void MoveItem(Repository* newRep);
. . .
private:
Repository* myRepository;
};
Создавая объект Item, можно указать, где он хранится, а можно и не указывать. Впоследствии можно изменить хранилище, например с помощью метода MoveItem.
При уничтожении объекта вызов
В создании и уничтожении объектов имеется одно существенное отличие. Создавая объект, мы всегда точно знаем, какому классу он принадлежит. При уничтожении это не всегда известно.
Item* itptr;
if (type == "book")
itptr = new Book();
else
itptr = new Magazin();
. . .
delete itptr;
Во время компиляции неизвестно, каким будет значение переменной type и, соответственно, объект какого класса удаляется операцией delete. Поэтому компилятор может вставить вызов только
Для того чтобы все необходимые virtual.
class Item
{
virtual ~Item();
};
class Book
{
public:
virtual ~Book();
};
Возникает вопрос – почему бы всегда не объявлять
Выделение памяти под объекты некоего класса производится либо при создании переменных типа этого класса, либо с помощью операции
Прежде всего, рассмотрим модификацию операции new, которая уже определена в самом языке. (Точнее, она определена в стандартной библиотеке языка Си++.) Эта операция не выделяет память, а лишь создает объект на заранее выделенном участке памяти. Форма операции следующая:
new (адрес) имя_класса
(аргументы_конструктора)
Перед именем класса в круглых скобках указывается адрес, по которому должен располагаться создаваемый объект. Фактически, такая операция new не выделяет памяти, а лишь создает объект по указанному адресу, выполняя его
char memory_chunk[4096]; Book* bp = new (memory_chunk) Book; . . . bp->~Book(); Magazin* mp = new (memory_chunk) Magazin; . . . mp->~Magazin();
В этом примере никакой memory_chunk. Операции new создают объекты в начале этого массива (разумеется, мы предполагаем, что 4096 байтов для объектов достаточно). Когда объект становится ненужным, явно вызывается его
Любой класс может использовать два вида операций new и delete – глобальную и определенную для класса. Если класс и ни один из его базовых классов, как прямых, так и косвенных, не определяет операцию new, то используется глобальная операция new. Глобальная операция new всегда используется для выделения памяти под встроенные типы и под массивы (независимо от того, объекты какого класса составляют массив).
Если класс определит операцию new, то для всех экземпляров этого класса и любых классов, производных от него, глобальная операция будет переопределена, и будет использоваться new данного класса. Если нужно использовать именно глобальную операцию, можно перед new поставить два двоеточия ::new.
Вид стандартной операции new следующий:
class A
{
void* operator new(size_t size);
};
Аргумент size задает размер необходимой памяти в байтах. size_t – это тип целого, подходящий для установления размера объектов в данной реализации языка, определенный через typedef. Чаще всего это тип long. Аргумент операции new явно при ее вызове не задается. Компилятор сам его подставляет, исходя из размера создаваемого объекта.
Реализация операции new, которая совпадает со стандартной, выглядит просто:
void*
A::operator new(size_t size)
{
return ::new char[size];
}
В классе может быть определено несколько операций new с различными дополнительными аргументами. При вызове new эти аргументы указываются сразу после ключевого слова new в скобках до имени типа. Компилятор добавляет от себя еще один аргумент – размер памяти, и затем вызывает соответствующую операцию. Описанная выше модификация new, помещающая объект по определенному адресу, имеет вид:
void* operator new(void* addr, size_t size);
Предположим, мы хотим определить такую операцию, которая будет инициализировать каждый байт выделенной памяти каким-либо числом.
class A
{
void* operator new(char init, size_t size);
};
void*
A::operator new(char init, size_t size)
{
char* result = ::new char[size];
if (result) {
for (size_t i = 0; i < size; i++)
result[i] = init;
}
return result;
}
Вызов такой операции имеет вид:
A* aptr = new (32) A;
Память под объект класса A будет инициализирована числом 32 (что, кстати, является кодом пробела).
Отметим, что если класс определяет хотя бы одну форму операции new, глобальная операция будет переопределена. Например, если бы в классе A была определена только операция new с
A* ptr = new A;
привел бы к ошибке компиляции, поскольку подобная форма new в классе не определена. Поэтому, если вы определяете new, определяйте все ее формы, включая стандартную (быть может, просто вызывая глобальную операцию).
В отличие от операции new, для которой можно определить разные модификации в зависимости от числа и типов аргументов, операция delete существует только в единственном варианте:
void operator delete (void* addr);
В качестве аргумента ей передается адрес, который в свое время возвратила операция new для данного объекта. Соответственно, для класса можно определить только одну операцию delete. Напомним, что операция delete ответственна только за delete, которая будет вызывать стандартную форму, выглядит следующим образом:
void
A::operator delete(void* addr)
{
::delete [] (char*)addr;
}
При определении класса имеется возможность задать для объекта начальное значение. Специальный метод класса, называемый
Для класса String имеет смысл в качестве начального значения использовать пустую строку:
class String
{
public:
String(); // объявление конструктора
};
// определение конструктора
String::String()
{
str = 0;
length = 0;
}
Определив такой
Конструктор без аргументов называется стандартным
class String
{
public:
String(); // стандартный конструктор
String(const char* p);
// дополнительный конструктор
};
// определение второго конструктора
String::String(const char* p)
{
length = strlen(p);
str = new char[length + 1];
if (str == 0) {
// обработка ошибок
}
strcpy(str, p); // копирование строки
}
Теперь можно, создавая переменные типа String, инициализировать их тем или иным образом:
char* cp;
// выполняется стандартный конструктор
String s1;
// выполняется второй конструктор
String s2("Начальное значение");
// выполняется стандартный конструктор
String* sptr = new String;
// выполняется второй конструктор
String* ssptr = new String(cp);
String он может выглядеть следующим образом:
class String
{
public:
String(const String s);
};
String::String(const String s)
{
length = s.length;
str = new char[length + 1];
strcpy(str, s.str);
}
Очевидно, что новый объект будет копией своего аргумента. При этом новый объект независим от первоначального в том смысле, что изменение значения одного не изменяет значения другого.
// первый объект с начальным значением
// "Astring"
String a("Astring");
// новый объект – копия первого,
// т.е. со значением "Astring"
String b(a);
// изменение значения b на "AstringAstring",
// значение объекта a не изменяется
b.Concat(a);
Столь логичное поведение объектов класса String на самом деле обусловлено наличием
String::String(const String s)
{
length = s.length;
str = s.str;
}
При вызове метода Concat для объекта b произошло бы следующее: объект b перераспределил бы память под строку str, выделив новый участок памяти и удалив предыдущий (см. определение метода выше). Однако указатель str объекта a по-прежнему указывает на первоначальный участок памяти, только что b. Соответственно, значение объекта a испорчено.
Для класса Complex, который мы рассматривали ранее, кроме стандартного
class Complex
{
public:
Complex();
Complex(int rl, int im = 0);
Complex(const Complex c);
// прибавить комплексное число
Complex operator+(const Complex x) const;
private:
int real; // вещественная часть
int imaginary; // мнимая часть
};
//
// Стандартный конструктор создает число (0,0)
//
Complex::Complex() : real(0), imaginary(0)
{}
//
// Создать комплексное число из действительной
// и мнимой частей. У второго аргумента есть
// значение по умолчанию — мнимая часть равна
// нулю
Complex::Complex(int rl, int im=0) :
real(rl), imaginary(im)
{}
//
// Скопировать значение комплексного числа
//
Complex::Complex(const Complex c) :
real(c.real), imaginary(c.imaginary)
{}
Теперь при создании комплексных чисел происходит их
Complex x1; // начальное значение – ноль Complex x2(3); // мнимая часть по умолчанию равна 0 // создается действительное число 3 Complex x3(0, 1); // мнимая единица Complex y(x3); // мнимая единица
Concat несколько иначе:
Concat(String s);
вместо
Concat(const String s);
т.е. использовали бы
b.Concat(a)
компилятор создал бы временную переменную типа String – копию объекта a, и передал бы ее в качестве аргумента. При выходе из метода String эта переменная была бы уничтожена. Представляете, насколько снизилось бы быстродействие метода!
Второй пример вызова
b.Concat("LITERAL");
хотя сам метод определен только для аргумента – объекта типа String. Поскольку в классе String есть String с начальным значением "LITERAL", ссылка на нее будет передана в качестве аргумента метода String, а по завершении Concat временная переменная будет уничтожена.
Чтобы избежать подобного неэффективного преобразования, можно определить отдельный метод для работы с указателями:
class String
{
public:
void Concat(const String s);
void Concat(const char* s);
};
void
String::Concat(const char* s)
{
length += strlen(s);
char* tmp = new char[length + 1];
if (tmp == 0) {
// обработка ошибки
}
strcpy(tmp, str);
strcat(tmp, s);
delete [] str;
str = tmp;
}
Аналогично тому, что при создании объекта выполняется
У класса может быть только один ~ ’. Для объектов класса String
class String
{
~String();
};
String::~String()
{
if (str)
delete str;
}
Если
Деструктор всегда вызывается перед тем, как String был создан с помощью операции new, то при вызове
delete str;
выполняется ~String(), а затем String:
int funct(void)
{
String str;
. . .
return 0;
}
При выходе из функции funct по оператору return переменная str будет уничтожена: выполнится
В особых случаях
sptr->~String();
Такие вызовы встречаются довольно редко; соответствующие примеры будут рассматриваться позже, при описании new и delete.
Рассмотрим более подробно, как создаются объекты. Предположим, формируется объект типа Book.
Во-первых, под объект выделяется необходимое количество памяти: либо динамически, если объект создается с помощью операции
Класс Book – производный от класса Item, поэтому вначале вызывается Item.
У объекта класса Book имеются атрибуты – объекты других классов, в частности, String. После завершения
И только теперь очередь дошла до вызова Book.
В самом конце, после завершения Book, создаются структуры, необходимые для работы виртуального механизма (отсюда следует, что в
Вызов
Item::Item() : taken(false), invNumber(0)
{}
В данном случае атрибутам объекта присваиваются начальные значения. Для класса Book
Book::Book() : Item(), title("<None>"),
author("<None>"), publisher("<None>"),
year(-1)
{}
Вначале выполняется стандартный Item, а затем создаются атрибуты объекта с некими начальными значениями. Теперь предположим, что у классов Item и Book есть не только стандартные Item
class Item
{
public:
Item(long in) { invNumber = in; };
. . .
};
class Book
{
public:
Book(long in, const String a,
const String t);
. . .
};
Тогда Book имеет смысл записать так:
Book::Book(long in, const String a,
const String t) :
Item(in), author(a), title(t)
{}
Такого же результата можно добиться и при другой записи:
Book::Book(long in, const String a,
const String t) :
Item(in)
{
author = a;
title = t;
}
Однако предыдущий вариант лучше. Во втором случае вначале для атрибутов author и title объекта типа Book вызываются стандартные String, вы можете убедиться, насколько эффективнее первый вариант Book.
Встречается еще один случай, когда без
class A
{
public:
A(const String x);
private:
String str_ref;
};
A::A(const String x) : str_ref(x)
{}
Создавая объект класса A, мы задаем строку, на которую он будет ссылаться. Ссылка A будет ссылаться на одну и ту же строку. Выбор ссылки в качестве атрибута класса обычно как раз и определяется тем, что ссылка
Рассмотрим еще один пример использования ссылки в качестве атрибута класса. Предположим, что в нашей библиотечной системе книги, журналы, альбомы и т.д. могут храниться в разных хранилищах. Хранилище описывается объектом класса Repository. У каждого элемента хранения есть атрибут, указывающий на его хранилище. Здесь может быть два варианта. Первый вариант – элемент хранения хранится всегда в одном и том же месте, переместить книгу из одного хранилища в другое нельзя. В данном случае использование ссылки полностью оправдано:
class Repository
{
. . .
};
class Item
{
public:
Item(Repository rep) :
myRepository(rep) {};
. . .
private:
Repository myRepository;
};
При создании объекта необходимо указать, где он хранится. Изменить хранилище нельзя, пока данный объект не уничтожен. Атрибут myRepository всегда ссылается на один и тот же объект.
Второй вариант заключается в том, что книги можно перемещать из одного хранилища в другое. Тогда в качестве атрибута класса Item лучше использовать указатель на Repository:
class Item
{
public:
Item() : myRepository(0) {};
Item(Repository* rep) :
myRepository(rep) {};
void MoveItem(Repository* newRep);
. . .
private:
Repository* myRepository;
};
Создавая объект Item, можно указать, где он хранится, а можно и не указывать. Впоследствии можно изменить хранилище, например с помощью метода MoveItem.
При уничтожении объекта вызов
В создании и уничтожении объектов имеется одно существенное отличие. Создавая объект, мы всегда точно знаем, какому классу он принадлежит. При уничтожении это не всегда известно.
Item* itptr;
if (type == "book")
itptr = new Book();
else
itptr = new Magazin();
. . .
delete itptr;
Во время компиляции неизвестно, каким будет значение переменной type и, соответственно, объект какого класса удаляется операцией delete. Поэтому компилятор может вставить вызов только
Для того чтобы все необходимые virtual.
class Item
{
virtual ~Item();
};
class Book
{
public:
virtual ~Book();
};
Возникает вопрос – почему бы всегда не объявлять
Выделение памяти под объекты некоего класса производится либо при создании переменных типа этого класса, либо с помощью операции
Прежде всего, рассмотрим модификацию операции new, которая уже определена в самом языке. (Точнее, она определена в стандартной библиотеке языка Си++.) Эта операция не выделяет память, а лишь создает объект на заранее выделенном участке памяти. Форма операции следующая:
new (адрес) имя_класса
(аргументы_конструктора)
Перед именем класса в круглых скобках указывается адрес, по которому должен располагаться создаваемый объект. Фактически, такая операция new не выделяет памяти, а лишь создает объект по указанному адресу, выполняя его
char memory_chunk[4096]; Book* bp = new (memory_chunk) Book; . . . bp->~Book(); Magazin* mp = new (memory_chunk) Magazin; . . . mp->~Magazin();
В этом примере никакой memory_chunk. Операции new создают объекты в начале этого массива (разумеется, мы предполагаем, что 4096 байтов для объектов достаточно). Когда объект становится ненужным, явно вызывается его
Любой класс может использовать два вида операций new и delete – глобальную и определенную для класса. Если класс и ни один из его базовых классов, как прямых, так и косвенных, не определяет операцию new, то используется глобальная операция new. Глобальная операция new всегда используется для выделения памяти под встроенные типы и под массивы (независимо от того, объекты какого класса составляют массив).
Если класс определит операцию new, то для всех экземпляров этого класса и любых классов, производных от него, глобальная операция будет переопределена, и будет использоваться new данного класса. Если нужно использовать именно глобальную операцию, можно перед new поставить два двоеточия ::new.
Вид стандартной операции new следующий:
class A
{
void* operator new(size_t size);
};
Аргумент size задает размер необходимой памяти в байтах. size_t – это тип целого, подходящий для установления размера объектов в данной реализации языка, определенный через typedef. Чаще всего это тип long. Аргумент операции new явно при ее вызове не задается. Компилятор сам его подставляет, исходя из размера создаваемого объекта.
Реализация операции new, которая совпадает со стандартной, выглядит просто:
void*
A::operator new(size_t size)
{
return ::new char[size];
}
В классе может быть определено несколько операций new с различными дополнительными аргументами. При вызове new эти аргументы указываются сразу после ключевого слова new в скобках до имени типа. Компилятор добавляет от себя еще один аргумент – размер памяти, и затем вызывает соответствующую операцию. Описанная выше модификация new, помещающая объект по определенному адресу, имеет вид:
void* operator new(void* addr, size_t size);
Предположим, мы хотим определить такую операцию, которая будет инициализировать каждый байт выделенной памяти каким-либо числом.
class A
{
void* operator new(char init, size_t size);
};
void*
A::operator new(char init, size_t size)
{
char* result = ::new char[size];
if (result) {
for (size_t i = 0; i < size; i++)
result[i] = init;
}
return result;
}
Вызов такой операции имеет вид:
A* aptr = new (32) A;
Память под объект класса A будет инициализирована числом 32 (что, кстати, является кодом пробела).
Отметим, что если класс определяет хотя бы одну форму операции new, глобальная операция будет переопределена. Например, если бы в классе A была определена только операция new с
A* ptr = new A;
привел бы к ошибке компиляции, поскольку подобная форма new в классе не определена. Поэтому, если вы определяете new, определяйте все ее формы, включая стандартную (быть может, просто вызывая глобальную операцию).
В отличие от операции new, для которой можно определить разные модификации в зависимости от числа и типов аргументов, операция delete существует только в единственном варианте:
void operator delete (void* addr);
В качестве аргумента ей передается адрес, который в свое время возвратила операция new для данного объекта. Соответственно, для класса можно определить только одну операцию delete. Напомним, что операция delete ответственна только за delete, которая будет вызывать стандартную форму, выглядит следующим образом:
void
A::operator delete(void* addr)
{
::delete [] (char*)addr;
}
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.