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

  • Форматирование при вводе/выводе

  • Ввод/вывод в языке С++ средствами стандартной библиотеки

  • Методы управления флагами класса ios

  • В классе ios определены методы форматирования: … которые используют следующие защищенные атрибуты целого типа

  • Методы обмена с потоками

  • Курс ООП в С презентация. ООП в с++(полный курс). Объекты и классы


    Скачать 1.76 Mb.
    НазваниеОбъекты и классы
    АнкорКурс ООП в С презентация
    Дата21.02.2022
    Размер1.76 Mb.
    Формат файлаppt
    Имя файлаООП в с++(полный курс).ppt
    ТипДокументы
    #368655
    страница18 из 26
    1   ...   14   15   16   17   18   19   20   21   ...   26

    Потоковые классы


    Потоки и типы, определяемые пользователем
    Для ввода и вывода в потоках используются перегруженные для всех стандартных типов операции чтения и извлечения << и >>. При этом выбор конкретной операции определяется типом фактических параметров.
    Иногда разработчику требуется единый способ определения состояния объектов. Под этим подразумевается ввод и вывод объектов с помощью соответствующих потоков.
    Для организации подобного интерфейса с пользователем необходимо определить операции чтения и извлечения в том классе, объекты которого будут передаваться потокам.

    Потоковые классы


    class person
    {
    public:
    ...
    friend istream& operator>>(istream& i, person& rhs);
    friend ostream& operator<<(ostream& o, const person& rhs);
    ...
    private:
    string name;
    string sirname;
    int year;
    int weight;
    };
    istream& operator>>(istream& i, person& rhs)
    {
    return i >> rhs.name >> rhs.sirname;
    }
    ostream &operator<<(ostream& o, const person& rhs)
    {
    return o << rhs.name << " " << rhs.sirname << endl;
    }


    Форматирование при вводе/выводе
    В потоковых классах форматирование выполняется тремя способами:
      с помощью флагов;
      манипуляторов;
      форматирующих методов.

      Флаги представляют собой отдельные биты, объединенные в поле x_flags класса ios.

    Ввод/вывод в языке С++ средствами стандартной библиотеки


    Таблица 3. Флаги форматирования потоковых классов


    флаг


    описание действия при установленном бите


    skipws


    при извлечении пробельные символы игнорируются


    left


    выравнивание по левому краю поля


    right


    выравнивание по правому краю поля


    internal


    знак числа выводится по левому краю, число – по правому; промежуток заполняется символами x_fill


    dec


    десятичная система счисления


    oct


    восьмеричная система счисления


    hex


    шестнадцатеричная система счисления


    showbase


    выводится основание системы счисления
    (0х для шестнадцатеричных чисел и 0 для восьмеричных)


    showpoint


    при выводе вещественных чисел печатать десятичную точку и дробную часть


    uppercase


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


    showpos


    печатать знак при выводе положительных чисел


    scientific


    печатать вещественные числа в форме мантиссы с порядком


    fixed


    печатать вещественные числа в форме с фиксированной точкой


    Флаги [left, right и internal], [dec, oct и hex], а также [scientific и fixed] взаимно исключают друг друга, т.е. в каждый момент может быть установлен только один флаг из каждой группы.
    Методы управления флагами класса ios:
    Все функции возвращают прежние флаги потока


    long flags()


    возвращает текущие флаги потока;


    long flags(long)


    присваивает флагам значение параметра;


    long setf(long)


    устанавливает флаги, биты которых установлены в параметре;


    long setf(long,long)


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


    В классе ios определены методы форматирования:
    … которые используют следующие защищенные атрибуты целого типа:


    int width()


    возвращает значение ширины поля вывода;


    int width(int)


    устанавливает ширину поля вывода в соответствии со значением параметра;


    int precision()


    возвращает значение точности представления при выводе вещественных чисел;


    int precision(int)


    устанавливает значение точности представления при выводе вещественных чисел, возвращает старое значение точности;


    char fill()


    возвращает текущий символ заполнения;


    char fill(char)


    устанавливает значение текущего символа заполнения, возвращает старое значение символа.


    x_width


    минимальная ширина поля вывода;


    x_precision


    количество цифр в дробной части при выводе вещественных чисел


    x_fill


    символ заполнения поля вывода


    Манипуляторы – это функции, которые можно включать в цепочку операций помещения и извлечения для форматирования данных, т. е. их можно включать прямо в поток. Манипуляторы делятся на простые, не требующие указания аргументов, и параметризованные.
    Простые манипуляторы:


    dec


    устанавливает при вводе и выводе флаг десятичной системы счисления;


    oct


    устанавливает при вводе и выводе флаг восьмеричной системы счисления;


    hex


    устанавливает при вводе и выводе флаг шестнадцатеричной системы счисления;


    ws


    устанавливает при вводе извлечение пробельных символов;


    endl


    при выводе включает в поток символ новой строки и выгружает буфер;


    ends


    при выводе включает в поток нуль-терминатор;


    flush


    при выводе выгружает буфер.


    Параметризированные манипуляторы требуют указания аргумента. Для их использования к программе надо подключить заголовочный файл .


    setbase(int n)


    задает основание системы счисления
    (n=10|8|16|0, 0 – по умолчанию, десятичное число, кроме случаев, когда вводятся восьмеричные или шестнадцатеричные числа)


    resetiosflags(long)


    сбрасывает флаги состояния потока, биты которых установлены в параметре;


    setiosflags(long)


    устанавливает флаги состояния потока, биты которых в параметре равны 1.


    setfill(int)


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


    setprecision(int)


    устанавливает максимальное количество цифр в дробной части для вещественных чисел;


    setw(int)


    устанавливает минимальную ширину поля вывода.


    В качестве ещё одного варианта реализации интерфейса с человеком приведём такой элемент диалога как таблица.
    class person_util
    {
    public:
    static int start_x;
    static void make_title();
    static void make_table(person&);
    };
    Метод make_table(), кроме возможностей библиотеки "windows.h", иллюстрирует механизмы использования потоков и их манипуляторов. Задача этого метода заключается в последовательном выводе колонок с информацией о значении атрибутов объекта типа person. Для этого из структуры CSBI извлекаются текущие координаты курсора, и в соответствующей горизонтальной позиции выводится строка необходимой ширины, имеющая нужный цвет и выравнивание. Атрибут цвета текста (attribute) определяется побитовым сложением числовых констант: FOREGROUND_RED | FOREGROUND_GREEN| FOREGROUND_INTENSITY| BACKGROUND_BLUE, что, в итоге, представляется десятичным числом 30 и желтыми символами на синем фоне.


    void person_util::make_table(person& arg)
    {
    HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_SCREEN_BUFFER_INFO CSBI; COORD c;
    GetConsoleScreenBufferInfo(hStdout, &CSBI);
    c = CSBI.dwCursorPosition; c.X = start_x;
    WORD attribute = 30;
    SetConsoleCursorPosition(hStdout, c);
    SetConsoleTextAttribute(hStdout, attribute);
    cout.setf(ios::left);
    cout.width(20); cout << arg.get_name();
    SetConsoleTextAttribute(hStdout, 7);
    cout << " ";
    SetConsoleTextAttribute(hStdout, attribute);
    cout << setw(20) << arg.get_sirname();
    SetConsoleTextAttribute(hStdout, 7);
    cout << endl;
    }
    Создание заголовка таблицы методом make_title() мы не приводим, поскольку он представляет собой упрощенный вариант рассмотренного выше make_table(). Напомним лишь, что нашему статическому атрибуту надо присвоить начальное значение:
    int person_util::start_x = 0;


    ставим флаг
    "выравнивание налево":


    используем метод форматирования ширины поля вывода:


    функция-манипулятор тоже определяет ширину:


    Пользователь может создать свой собственный манипулятор и определить в нем правила, по которым будет осуществляться вывод информации в поток
    typedef ostream&(*point)(ostream&, int, char);//point - тип-указатель
    //на функцию-манипулятор:
    class mnp //класс для манипулятора
    {
    int width; //ширина поля вывода
    char fill; //символ-заполнитель
    point fun; //функция, определяющая ширину и заполнитель
    public:
    mnp(int w, char c, point f) : width(w), fill(c), fun(f) {}
    //перегруженная функция-операция для вывода объекта в поток:
    friend ostream& operator<<(ostream&, mnp);
    };
    ostream& operator<<(ostream& out, mnp m)
    {
    return m.fun(out, m.width, m.fill);//возвращаем поток как результат
    //работы функции-манипулятора:
    }


    ostream& format(ostream& out, int w, char f)
    { //выполняем форматирование для выводимых данных:
    out.width(w); //ширина
    out.fill(f); //заполнитель
    //правое выравнивание для всех данных,
    //вывод целых - в шестнадцатиричной СС и верхнем регистре:
    out.flags(ios::right|ios::uppercase|ios::hex);
    return out;
    }
    void main()
    { //выводим A на 15 позициях, с заполнением пустоты точками:
    int A = 987; cout << mnp(15, '.', format) << A << endl;
    }
    В данном случае для объекта класса mnp вызывается перегруженный оператор
    <<, который обращается к функции format() через атрибут-указатель fun
    объекта m, передавая ей основные параметры формата вывода. С тем же успехом, вместо конструктора класса, можно было бы вызвать функцию вида:
    mnp set_stream(int w, char f)
    {
    return mnp(w, f, format);
    }
    …подменяющую явное обращение к объекту класса mnp. В любом случае, пользователь увидел бы что-нибудь наподобие "............3DB".


    Методы обмена с потоками
    Для работы с объектами потоковых классов наряду с операциями (>> и <<)
    определены ещё и методы неформатированного чтения и записи в поток.
    Преобразование данных при этом не выполняется.


    get()


    возвращает код извлеченного из потока символа или EOF, если достигнут конец файла;


    get(c)


    присваивает код извлеченного из потока символа аргументу с;


    get(buf, num, lim='\n')


    читает из потока символы, пока не встретится символ lim
    (по умолчанию '\n') или пока не будет прочитано num-1 символов, прочитанные символы помещаются в массив buf и к ним добавляется '\0'. Символ lim остается в потоке.


    getline(buf, num, lim='\n')


    аналогично предыдущему методу, но lim удаляется из потока (в массив buf он все равно не записывается);


    peek()


    возвращает код следующего символа в потоке, но не извлекает его из потока;


    ignore(num= 1, lim = EOF)


    считывает и пропускает символы до тех пор, пока не будет прочитано num символов или не встретится разделитель, заданный параметром lim. Возвращает ссылку на текущий поток;


    read(buf, num)


    считывает из потока num символов в массив buf.


    gcount()


    возвращает количество символов, прочитанных последним вызовом функции неформатированного ввода;


    rdbuf()


    возвращает указатель на буфер типа streambuf, связанный с данным потоком.


    put(c)


    выводит в поток символ с;


    write(buf,num)


    выводит в поток num символов из массива buf.


    seekg(pos)


    устанавливает текущую позицию чтения в значение pos;


    seekg(offs, org)


    перемещает текущую позицию чтения на offs байтов, считая от одной из трех позиций, определяемых параметром org: ios::beg (от начала файла), ios::cur (от текущей позиции) или ios::end (от конца файла);


    tellg()


    возвращает текущую позицию чтения потока;


    flush()


    записывает содержимое потока вывода на физическое устройство;


    seekp(pos)


    устанавливает текущую позицию записи в значение pos;


    seekp (offs, org)


    перемещает текущую позицию записи на offs байтов, считая от одной из трех позиций, определяемых параметром org: ios::beg (от начала файла), ios::cur (от текущей позиции) или ios::end (от конца файла);


    tellp()


    возвращает текущую позицию записи потока;


    При организации диалогов с пользователем программы при помощи потоков необходимо учитывать буферизацию. Например, при выводе приглашения к вводу мы не можем гарантировать, что оно появится раньше, чем будут считаны данные из входного потока, поскольку приглашение появится на экране только при заполнении буфера вывода.
    Для решения этой проблемы определена функция tie(), которая связывает потоки istream и ostream с помощью вызова вида cin.tie(&cout). После этого вывод очищается (т. е. выполняется функция cout.flush()) каждый раз, когда требуется новый символ из потока ввода.
    Приведем пример использования некоторых методов:
    cin.ignore(7, ',');
    streambuf SB = *cin.rdbuf();
    cout << &SB;
    Очистив поток ввода, мы получаем от пользователя символы, которые заносятся в поток при определенных условиях: когда пропущено 7 символов, или после того, как была встречена запятая. Далее инициализируем собственный буфер буфером потока cin, и передаем его потоку cout. Таким образом, для строки "123,4567890" получим результат "4567890", а для "12345678,90" уже "8,90"!


    Ошибки потоков
    Наиболее распространенная ошибка при использовании потоков – не заметить, что введено не то, что предполагалось, поскольку на входе был не тот формат. Нужно или проверять состояние потока ввода перед использованием предположительно считанных данных, или задействовать исключения.
    Для отслеживания ошибок потоков в базовом классе ios определено поле state, отдельные биты которого отображают состояние потока.


    ios::goodbit


    нет ошибок;


    ios::eofbit


    конец файла;


    ios::failbit


    ошибка форматирования или преобразования;


    ios::badbit


    серьезная ошибка, пользоваться потоком нельзя;


    ios::hardfail


    неисправность оборудования.


    Получить текущее состояние потока можно с помощью метода int rdstate(), но существуют и другие более удобные методы.
    Если произошла ошибка ввода, в результате которой установлен флаг только failbit, то работу с потоком можно продолжить, но сначала надо сбросить флаги ошибок с помощью метода clear(). Этот же метод используется и для установки флагов, если передать ему не нулевой аргумент.


    int eof()


    возвращает ненулевое значение, если установлен флаг eofbit;


    int fail()


    возвращает ненулевое значение, если установлен один из флагов failbit, badbit или hardfail;


    int bad()


    возвращает ненулевое значение, если установлен флаг badbit;


    int good()


    возвращает ненулевое значение, если сброшены все флаги.


    void clear
    (int = 0)


    параметр принимается в качестве состояния ошибки, при отсутствии параметра состояние ошибки устанавливается 0;


    operator void*()


    возвращает нулевой указатель, если установлен хотя бы один бит ошибки, эта операция вызывается каждый раз, когда поток сравнивается с 0;


    operator ! ()


    возвращает ненулевой указатель, если установлен хотя бы один бит ошибки.


    Stream.clear(); //сбросить все флаги
    Stream.clear(rdstate() | ios::eofbit);// установить флаг конец файла
    Stream.clear(ios::eofbit); // установить флаг конец файла.
    //и сбросить все остальные
    while(Stream)
    {
    //ввод или вывод …
    };


    Файловые потоки
    Файл – именованная информация на внешнем носителе, т. е. логически файл – это конечное множество последовательных байтов, следовательно, устройства, такие как принтер, клавиатура и монитор, являются частным случаем файлов.
    По способу доступа файлы можно разделить на
      последовательные, чтение и запись в которые производится с начала байт за байтом;
      файлы с произвольным доступом, допускающие чтение и запись в указанную позицию.

      Стандартная библиотека содержит три класса для работы с файлами:

      ifstream – класс входных файловых потоков;
      ofstream – класс выходных файловых потоков;
      fstream – класс двунаправленных файловых потоков.

      Эти классы являются производными от классов istream, ostream и iostream соответственно, поэтому они наследуют перегруженные операции << и >>, флаги форматирования, манипуляторы, методы, состояние потоков и т. д.
      Использование файлов в программе предполагает следующие операции:

      создание потока;
      открытие потока и связывание его с файлом;
      обмен (ввод/вывод);
      уничтожение потока;
      закрытие файла.


    Конструкторы без параметров создают объект соответствующего класса, не связывая его с файлом. Конструкторы с параметрами создают объект соответствующего класса, открывают файл с указанным именем и связывают файл с объектом:
    ifstream(const char *name, int mode = ios::in);
    ofstream(const char *name, int mode = ios::out | ios::trunc);
    fstream(const char *name, int mode = ios::in | ios::out);
    Вторым параметром является режим открытия файла. Вместо значения по умолчанию можно указать одно из следующих значений, определенных в классе ios:
    По умолчанию файлы открываются в текстовом режиме. Это означает, что на вводе последовательность возврата каретки/перевода строки преобразуется в символ '\n'. На выводе символ '\n' преобразуется в последовательность возврат каретки/перевод строки. В двоичном режиме такие преобразования не выполняются.
    Функция компонента ofstream::open объявляется следующим образом:
    void open(char * name, int=ios::out, int prot=filуbuf::openprot);
    Аналогично, объявление ifstream::open имеет вид:
    void open(char * name, int=ios::in, int prot=filуbuf::openprot);


    Второй аргумент, называемый режимом открытия, имеет показанные умолчания. Аргумент режима открытия (возможно, связанный операцией ИЛИ с несколькими битами режима) можно явно задать в следующей форме:
      ios::in – открыть файл для чтения;
      ios::out – открыть файл для записи;
      ios::ate – установить указатель на конец файла, читать нельзя, можно только записывать данные в конец файла;
      ios::app – открыть файл для добавления;
      ios::trunc – если файл существует, то создать новый;
      ios::binary – открыть в двоичном режиме;
      ios::nocreate – если файл не существует, выдать ошибку, новый файл не открывать;
      ios::noreplace – если файл существует, выдать ошибку, существующий файл не открывать;

      Открыть файл в программе можно с использованием либо конструкторов, либо метода open, имеющего такие же параметры, как и в соответствующем конструкторе.
      fstream stream; //конструктор без параметров
      //открывается файл, который связывается через fbuf с потоком
      stream.open("..\\f.dat",ios::in);
      //конструктор с параметрами, создает и открывает для чтения файл
      fstream stream("..\\f.dat",ios::in);

    После того как файловый поток открыт, работать с ним можно также как и со стандартными потоками cin и cout. При чтении данных из входного файла надо контролировать был ли достигнут конец файла после очередной операции вывода. Это можно делать с помощью метода eof().
    Если в процессе работы возникнет ошибочная ситуация, то потоковый объект принимает значение равное 0.
    Когда программа покидает область видимости потокового объекта, то он уничтожается, при этом перестает существовать связь между потоковым объектом и физическим файлом, а сам файл закрывается. Если файл требуется закрыть раньше, то используется метод close().


    //вспомогательная функция
    int GetInt(istream&in) //ввод целого числа из потока in
    {
    int value;
    while(1) //бесконечный цикл
    {
    in>>value; //чтение целого числа
    if(in.peek=='\n')//если в буфере типа streambuf, связанном с //потоком in находится изображение целого числа, заканчивающегося //'\n', то после извлечения этого числа в буфере останется только //'\n'. Выполняется проверка этого условия.
    {
    in.get(); //буфер очищается
    break; //выход, т. к. число введено верно
    }
    else
    {
    cout<<"\nError in data; Repeat, please:"< in.clear();//сбрасываются флаги ошибок потока in
    while(in.get()!='\n'{}; //из буфера извлекаются все
    } //символы до символа '\n'
    }
    return value;
    }


    //функция чтения объекта из потока
    istream& operator >>(istream& in, person& S)
    {
    in.getline(S.name,20); // читает из потока символы, пока не //встретится символ '\n' или пока не будет прочитано 19 символов, //прочитанные символы помещаются в массив S.name и к ним //добавляется '\0'.
    S.age=GetInt(in); //вызов функции для ввода целого числа
    return in;
    }


    Одним из типовых средств работы пользователя с данными программы является меню, под которым, кроме экранной формы, подразумевается и некоторый объект системы. Класс для него должен описывать характеристики экранного объекта и методы для работы с ним.
    class menu
    {
    public:
    void add_string(string s); //добавить строку в меню
    int show(); //показать меню
    menu(int, int, int, int); //конструктор с параметрами
    void redraw(); //перерисовка экранной формы
    private:
    int pos; //начальная позиция отображения
    int curr_pos; //текущая позиция маркера меню
    int start_y; //начальная координата Y
    int start_x; //начальная координата X
    unsigned int height; //высота в строках
    unsigned int width; //ширина в символах
    vector Strings; //массив строк меню
    HANDLE hStdout;
    };


    Исходя из назначения данного класса, единственным конструктором, имеющим смысл, является конструктор с параметрами, задающий значения экранных координат, ширину и высоту меню, а также определяющий контекст. menu::menu(int sx, int sy, int w, int h)
    {
    start_x = sx; start_y = sy; width = w; height = h;
    pos = 0; curr_pos = 0;
    hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
    }
    Поскольку пользователь осуществляет взаимодействие с меню на основе символьной информации, меню должно содержать массив составляющих его строк, что реализуется посредством включения в класс меню атрибута Strings контейнерного типа vector. Добавление строк туда должно учитывать правила работы с экранным объектом, для чего имеется метод add_string(). Он определяет разницу между шириной меню в символах и длиной строки, после чего либо дополняет её пробелами, либо отсекает лишние символы, и только после этого добавляет измененную строку в контейнер Strings.
    void menu::add_string(string s)
    {
    int count = width - s.size();
    if (count) s.append(count, ' ');
    else s = s.substr(0, width);
    Strings.push_back(s);
    }


    Метод redraw()предназначен для перерисовки меню при изменении его состояния, которое определяется номером pos первой отображаемой строки и позицией curr_pos маркера внутри строк, выводимых на экран. Эти значения непосредственно зависят от действий пользователя, обрабатываемых методом show().
    void menu::redraw()
    {
    COORD c; c.X = start_x; c.Y = start_y; int y = 0;
    WORD attribute;
    vector::iterator si = Strings.begin();
    for(int i = curr_pos; i < curr_pos+height &&
    si != Strings.end(); i++, si++, y++)
    {
    c.Y = start_y + y; SetConsoleCursorPosition(hStdout, c);
    if(i == pos)
    {
    attribute = FOREGROUND_RED|FOREGROUND_GREEN |
    FOREGROUND_INTENSITY|BACKGROUND_BLUE|BACKGROUND_INTENSITY;
    }
    else
    {


    attribute = FOREGROUND_RED|FOREGROUND_GREEN|
    FOREGROUND_INTENSITY|BACKGROUND_BLUE;
    };
    SetConsoleTextAttribute(hStdout,attribute);cout << Strings[i];
    SetConsoleTextAttribute(hStdout, 0); cout << endl;
    }
    }


    Метод show() непосредственно реализует интерфейс с пользователем, изменяя состояние меню в зависимости от нажатия клавиш человеком, и возвращая по нажатию клавиши Enter номер выбранной строки.
    int menu::show()
    {
    int z;
    if (Strings.begin() != Strings.end())
    {
    redraw();
    do
    {
    z = _getch(); //получаем код нажатой клавиши
    switch (z)
    {
    case 72: //нажата клавиша "стрелка вверх"
    {
    SetConsoleTextAttribute(hStdout, 0);
    if (pos == curr_pos && pos > 0)
    --curr_pos;
    if (pos > curr_pos) --pos;
    break;
    }


    case 80: //нажата клавиша "стрелка вниз"
    {
    SetConsoleTextAttribute(hStdout, 120);
    if (pos < Strings.size()-1) ++pos;
    if (pos == curr_pos+height &&
    pos < Strings.size()) ++curr_pos;
    break;
    }
    }
    redraw();
    }
    while (z != 13); //выполняем, пока не нажали Enter
    SetConsoleTextAttribute(hStdout, 7);
    return pos;
    }
    else
    {
    SetConsoleTextAttribute(hStdout, 7);
    return -1;
    }
    }


    Нижеследующий пример показывает создание меню на основе массива объектов класса person, имена и фамилии которых передаются объекту-меню M в качестве строк.
    void main()
    {
    person x; string s; int index;
    vector
    Persons;
    vector
    ::iterator pi;
    for (int i=0; i < 2; i++)
    {
    cout << "Enter name: "; cin >> s;
    x.set_name(s);
    cout << "Enter sirname: "; cin >> s;
    x.set_sirname(s);
    Persons.push_back(x);
    }
    menu M(10, 10, 15, 4);
    for (pi = Persons.begin(); pi != Persons.end(); pi++)
    {
    M.add_string(pi->get_name()+" "+pi->get_sirname());
    }
    index = M.show();
    cout << Persons[index].get_sirname() << endl;
    cout << "Address of object is " << &Persons[index] << endl;
    }


    Поскольку метод show() возвращает индекс элемента меню, можно узнать, и какой объект был выбран, и какой у этого объекта адрес, о чём и говорят две последние строки кода. Теперь мы можем связать объект или указатель на него с любым другим объектом, передавая их как параметры соответствующим методам этих объектов. Отметим, что использование меню, естественно, не ограничивается выбором единственного объекта, и может быть продолжено путём очередных обращений к методу show().


    Контейнерные классы

    1   ...   14   15   16   17   18   19   20   21   ...   26


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