Заглавная страница Избранные статьи Случайная статья Познавательные статьи Новые добавления Обратная связь FAQ Написать работу КАТЕГОРИИ: АрхеологияБиология Генетика География Информатика История Логика Маркетинг Математика Менеджмент Механика Педагогика Религия Социология Технологии Физика Философия Финансы Химия Экология ТОП 10 на сайте Приготовление дезинфицирующих растворов различной концентрацииТехника нижней прямой подачи мяча. Франко-прусская война (причины и последствия) Организация работы процедурного кабинета Смысловое и механическое запоминание, их место и роль в усвоении знаний Коммуникативные барьеры и пути их преодоления Обработка изделий медицинского назначения многократного применения Образцы текста публицистического стиля Четыре типа изменения баланса Задачи с ответами для Всероссийской олимпиады по праву Мы поможем в написании ваших работ! ЗНАЕТЕ ЛИ ВЫ?
Влияние общества на человека
Приготовление дезинфицирующих растворов различной концентрации Практические работы по географии для 6 класса Организация работы процедурного кабинета Изменения в неживой природе осенью Уборка процедурного кабинета Сольфеджио. Все правила по сольфеджио Балочные системы. Определение реакций опор и моментов защемления |
Компонентные функции структурированных объектовСодержание книги
Поиск на нашем сайте
Как мы знаем, все фундаментальные типы данных, или лучше сказать определенный с помощью этих типов объекты (переменные), характеризуются во-первых, количеством байт отводимых под объект, а во-вторых множеством операций определённых над объектом. Иногда, множество операций над объектами называют «поведением» объекта. По аналогии с фундаментальными типами данных, с помощью ключевых слов struct, class, union, строятся структурированные (определённые программистом) типы данных, которые также характеризуются этими двумя признаками: объемом памяти и поведением. Как мы знаем объем занимаемый, структурированным объектом зависит от количества компонентных данных включённых в определение структурированного типа, а вот поведение объекта реализуется с помощью так называемых компонентных функций, описание или определение которых также должно присутствовать в определении структурированного объекта. В общем случае определение структурированного объекта, который содержит и компонентные данные, и компонентные функции выглядит следующим образом struct имя_нового_типа { определение компонентного данного_1; … определение компонентного данного _N; определение или описание компонентной функции_1; … определение или описание компонентной функции_K; }; Приведём конкретный пример определения структурированного типа, например, комплексное число, в котором поведение объекта (множество операций) реализуем с помощью набора компонентных функций. По мере того как будет меняться наше представление о правильном стиле реализации структурированных объектов, мы будем переделывать определение структурированного типа комплексное число, взятого нами в качестве примера.
struct comp { // Компонентные данные double Re, Im; //Компонентные функции comp (){Re= 0; Im= 0;} //Конструктор по умолчанию comp(double r, double i) {Re = r; Im = i;} //Конструктор comp(comp &T) { Re= T.Re; Im = T.Im;} // Конструктор копии comp(double r) {Re = r; Im=0;} // Конструктор преобразования типа ~comp(){} // Деструктор void display (){ cout<< “\n Re =”<< Re<<”\t Im = “<<Im;} }; Существует два способа определения компонентных функций структурированного объекта. Первый способ – это записать определение функций непосредственно в определении структурированного типа, как и сделано в примере, в этом случае компилятор пытается сделать эти функции inline, т.е. так называемыми встраиваемыми функциями. Как мы знаем, функция может стать inline функцией, если её определение удовлетворяет достаточно жёсткому набору требований, например, определение не должно содержать операторы цикла, переключатели или операции перехода. Другими словами компонентную функцию можно определить прямо в определении структурированного типа, если она очень проста (в противном случае компилятор выдаст ошибку), если нет, если функция достаточно сложная, то существует второй способ определения компонентной функции. Он заключается в том, что в определении типа остаётся только описание функции, а её определение размещается за его пределами. Покажем это на примере конструктора по умолчанию comp () и функции display (). Кроме того добавим в класс comp статическое поле static int count и модифицируем конструкторы и деструктор так, чтобы в этом поле хранилось текущее на данный момент число объектов типа comp. Добавим также статическую функцию GetCountComp(), которая при вызове возвращает значение count. struct comp { // Компонентные данные double Re, Im; static int count; //Компонентные функции comp (); //Описание комп. функции (конструктора по умолчанию) comp(double r, double i) {Re = r; Im = i; count++;} //Конструктор comp(comp &T) { Re= T.Re; Im = T.Im; count++;} // Конструктор копии comp(double r) {Re = r; Im=0; count++;} // Конструктор преобразования типа ~comp(){count--;} // Деструктор void display(); //Описание компонентной функции display() static int GetCountComp(){return count;} }; int comp::count = 0; //Инициализация статического компонентного данного //----------------------------------------------------------------------------------------------------- // Определение компонентной функции (конструктора по умолчанию)
comp::comp(){Re= 0; Im= 0; count++;} //----------------------------------------------------------------------------------------------------- // Определение компонентной функции display void comp::display(){ cout<< “\n Re =”<< Re<<”\t Im = “<<Im;} //----------------------------------------------------------------------------------------------------------- //Определение обычной (не компонентной) функции sum() comp sum(comp A, comp& С){ comp D; D.Re = A.Re +С.Re; D.Im = A.Im +С.Im; return D; } Как видно из примера, определение компонентной функции display() ничем не отличается от определения обычной функции sum(), за исключением того, что перед именем функции стоит имя типа с двумя двоеточиями (comp::). Это «приставка» и сообщает компилятору, что функция display() является компонентной функцией класса comp. Функция sum() – это обычная (не компонентная) функция, которая принимает в качестве параметров объект и ссылку на объект типа comp и возвращает объект типа comp. Как видно из текста, эта функция в точку вызова возвращает комплексное число, равное сумме двух комплексных чисел, переданных ей в качестве формальных параметров. Способы вызова компонентных функций. Компонентные функции могут быть вызваны с помощью тех же самых операций: имя_объекта.имя_функции(фактические параметры); указатель_на_объект->имя_функции(фактические параметры); указатель_на_объект->*указатель_на_функцию (фактические параметры); (*указатель_на_объект).имя_функции(фактические параметры)); или используя полное имя имя_объекта. имя_типа::имя_функции(фактические параметры); указатель_на_объект->имя_типа::имя_функции(фактические параметры); указатель_на_объект->имя_типа::*указатель_на_функцию (фактические параметры); (*указатель_на_объект).имя_типа::имя_функции(фактические параметры));
которые мы использовали для доступа к полям данных структурированного объекта. Например:
void main (){ сomp A, B(2,3); A = sum(B, B); A.display(); B.display(); comp * pB = &B; pB->display(); // или pB->comp::display(), или (*pB).comp::display() это всё одно и тоже cout<< “\n GetCountComp() = “<<comp:: GetCountComp(); } Надо отчётливо понимать, что компонентные функции всегда вызываются с помощью конкретного объекта (или указателя на этот конкретный объект) и для этого конкретного объекта, и следовательно, функции выполняют действия над полями именно этого конкретного объекта. Так, записав А.display() мы вызываем функцию display() для печати полей объекта А, а B.display() – для печати полей объекта В. void comp::display(){ cout<< “\n Re =”<< Re<<”\t Im = “<<Im;} Встаёт вопрос, как компонентная функции, например display, во время вызова передаётся информация о том, для какого объекта она вызвана. Другими словами, откуда при вызове A.display() функция display знает, что печатать надо поля Re и Im именно объекта А. Оказывается во все компонентные функции (за исключением статических) при вызове, неявно передаётся один параметр, имеющий имя this, который является константным указателем на объект, для которого эта компонентная функция была вызвана. И компилятор, опять же неявно, с помощью этого указателя внутри компонентной функции узнаёт, поля какого объекта используются. Фактически, определение функции display() можно переписать в виде: void comp::display(){ cout<< “\n Re =”<<this->Re<<”\t Im = “<<this->Im;} и естественно оно будет работать, а компилятору ничего не надо будет неявно подставлять. Причём при вызове A.display() указатель this будет настроен на объект А, а при В.display() – на объект В. Конструкторы и деструктор. Особое место среди компонентных функций занимают так называемые конструкторы и деструктор. Конструкторы имеют следующие свойства: имя конструктора совпадает с именем типа (в нашем случае comp); конструктор не возвращает никакого значения (даже void); конструкторы вызываются (обычно неявно), чтобы создать объект соответствующего типа. Таким образом, если мы в определении структурированного типа данных видим описание или определение функций, которые не возвращают никакого значения и их имя совпадает с именем типа, то мы можем сразу сказать,что это конструкторы (см. пример выше). Конструкторов в определении типа может быть несколько, но все они должны быть уникальными, т.е. иметь разное количество или типы формальных параметров. Именно по количеству и типу фактических параметров, компилятор определяет какой из конструкторов вызвать. Другими словами в случае с конструкторами мы имеем дело с перегрузкой функций. Напомню, что перегрузкой функций называется ситуация, когда существуют несколько функций имеющих одинаковое имя, но различающихся количеством или типом формальных параметров, и компилятор по типу и количеству фактических параметров в вызове функции определяет, какую конкретно функцию ему необходимо вызвать. По количеству и типу параметров различают следующие типы конструкторов: Конструктор по умолчанию - конструктор не имеющий ни одного параметра comp(), вызывается компилятором при создании объекта структурированного типа без параметров; Конструктор копии – конструктор имеющий один параметр, который является ссылкой на объект этого же самого типа comp(comp& T), вызывается компилятором в двух случаях, при вызове функции, имеющей формальным параметром объект структурированного типа. Задача конструктора копии – при вызове функции создать на месте формального параметра функции локальный объект структурированного типа и скопировать в него содержимое фактического параметра. Вызывается компилятором во втором случае, если функция возвращает по значению объект структурированного типа. В этом случае в задачу конструктора копии входит создание в точке вызова неименованного структурированного объекта и копирование в него содержимое локального структурированного объекта, стоящего после оператора return. Конструктор преобразования типа – конструктор имеющий один параметр (comp (double r)). С его помощью (или с их помощью, так как конструкторов преобразования типа может быть несколько, например comp (int r)) осуществляется реализация возможности неявное преобразование объектов типа double к объекту типу comp в тех местах программы, в которых вместо объекта comp был использован объект double, например: comp sum(comp A, comp& С) double k = 7.1; comp B, F(3, 2); /* Для объекта В вызывается конструктор по умолчанию, так как у него нет параметров. А для создания объекта F вызывается обычный конструктор, который имеет два параметра, в нашем случае comp(double r, double i). Заметим, что фактические параметры F это целые числа (3, 2), а не вещественные, но тем не менее ошибки нет, так как они по умолчанию преобразуются к типу double. Можно сказать, что в типе double имеется конструктор преобразования типа из int в double */ B = sum (k, F); Первый параметр в соответствии с определением и описанием функции sum должен быть типа comp, но в примере использован double. В обычном случае компилятор проверяет соответствие типов формальных и фактических параметров в вызове функции, и если они не совпадают, выдаёт сообщение об ошибке, но так как мы определили в типе comp конструктор преобразования типа, то он будет неявно вызван и преобразует объект double к объекту типа comp и сообщение об ошибке выдаваться не будет. Обычные конструкторы – это конструкторы с несколькими параметрами, они естественно должны отличаться друг от друга количеством или типом формальных параметров. Вызываются такие конструкторы при создании объекта структурированного типа с параметрами. Деструктор - компонентная функция не возвращающая никакого значения (даже void) и не имеющая ни одного параметра, имя которой совпадает с именем типа перед которым стоит символ ~, в нашем случае ~comp(). Деструктор вызывается компилятором перед уничтожением объекта, что даёт возможность программисту выполнить какую-то последовательность действий перед тем как объект будет уничтожен. Обычно в деструкторе освобождают динамическую память, которая была ранее выделена под поля структурированного объекта. В нашем примере деструктор пустой, так как в объектах типа comp не используется динамическая память и нет указателей, которые бы указывали на фрагменты динамической памяти. Если конструкторов может быть много, то деструктор только один. Рассмотрим подробно, какие компонентные функции явно или неявно вызываются в нашем примере функции main, приведённой выше. Для создания объекта А вызывается конструктор по умолчанию; для создания объекта В – конструктор с двумя параметрами. При вызове функции sum(B, &B) для первого фактического параметра вызывается конструктор копии. Его задача создать локальный объект А в функции sum и скопировать в него содержимое объекта В. А вот для второго фактического параметра B конструктор копии в вызове функции sum(B, B) не вызывается, так как второй формальный параметр С является ссылкой и, следовательно, нет необходимости в создании нового локального объекта, так как ссылка является ещё одним именем для уже существующего объекта, то есть С становиться В. При выполнении операторов тела функции sum вызывается конструктор по умолчанию для создания объекта D. Далее выполняется суммирование и оператор return D. Мы с вами знаем, как выполняется оператор return при возврате по значению из функции объектов фундаментальных типов. В точке вызова создаётся неименованный объект, совпадающий с типом функции, в который копируется содержимое выражения, стоящего после оператора return. Тоже самое происходит и со структурированными объектами, в точке вызова функции sum() конструктор копии создаёт неименованный объект типа comp, в который копирует содержимое локального объекта D. Далее содержимое этого неименованного объекта с помощью операции присваивания записывается в объект А, и начинают вызываться деструкторы. После выполнения второй строки программы вызываются два деструктора, чтобы разрушить локальный объект D и неименованный объект. Далее вызывается два раза компонентная функция display() для вывода на экран полей объектов А и В.В следующей строке comp * pB = &B; определяется и инициализируется указатель pB типа comp. В этой строке никаких конструкторов не вызывается, так как pB – это указатель, а не объект типа comp. Далее с помощью указателя для объекта В вызывается функция display() После выполнения последней строки программы будут вызваны ещё два деструктора для уничтожения локальных внутри функции main() объектов A и В. Конструкторы и деструктор являются несколько специфичными, но тем не менее обычными компонентными функциями, поэтому им также неявно передаётся указатель this, и они могут быть вызваны, как и обычные компонентные функции явно с помощью имени объекта и операции точка. Другими словами первую строку сomp A, B(2,3); можно было бы переписать в виде comp A.comp(), B.comp(2,3); Статические компонентные функции. Статическими компонентными функциями называются функции, перед определением или описанием которых в определении класса стоит ключевое слово static. Перечислим основные сходства и различия между обычными и статическими компонентными функциями: • статические компонентные функции могут быть вызваны с помощью имени класса имя_класса::имя_статической_функции(фактические параметры), т.е. статическая компонентная функция может быть вызвана до того как будет создан первый объект структурированного типа имя_класса (для обычных компонентных функций это невозможно); • статические компонентные функции могут быть вызваны и как обычные компонентные функции с помощью имени объекта или указателя на объект, например имя_объекта.имя_статической_функции(фактические параметры); • в статические компонентные функции не передаётся скрытно указатель this, поэтому в теле функции непосредственно мы можем обращаться (модифицировать) только к статическим компонентным данным. Фактически, статические компонентные функции были созданы для работы со статическими компонентными данными и именно там они обычно и используются.
7.7 Расширение действия (перегрузка) стандартных операций Одной из привлекательных особенностей языка Си++ является возможность распространения действия стандартных операций на операнды, для которых эти операции первоначально в языке не предполагались. Например, определив новый тип комплексное число нам наверняка захочется иметь возможность записать сумму двух объектов класса комплексное число (comp) в виде s+g. Для того чтобы это стало возможным программист должен определить специальную функцию, называемую "операция-функция" (operator function). Формат определения операции-функции: тип_функции operator# (список формальных параметров) {операторы_тела_операции-функции} При необходимости может добавляться и прототип операции-функции с таким форматом: тип_функции operator#(список формальных параметров); где # - знак операции, например, =, +, -, и т.д.; operator# - имя функции. Как мы видим определение и описание операции-функции практически ни чем не отличается от обычной функции, но отличия все-таки имеются, и состоят они в наличии двух абсолютно эквивалентных способах вызова оператора-функции. Так для операции сложения двух комплексных чисел после определения операции-функции comp operator+(comp s, comp g){ comp d; d.Re = s.Re+g.Re; d.Im= s.Im+g.Im; return d; } два способа её вызова выглядя следующим образом: comp k(7,2), e(2,5); operator+(k, e); //первый способ вызова k+e; //второй способ вызова как видно из примера первый способ очень похож на обычный вызов обычной глобальной функции имя_функции(список фактических параметров), а второй способ более краткий позволяет добиться от типов данных, определённых программистом, такой же функциональности как и у фундаментальных типов данных, т.е. перегрузить для новых типов данных необходимый им набор, определённых в языке Си++ операций. В отличии от обычных функций при реализации операция-функция надо учитывать несколько ограничений, так количество параметров у операции-функции зависит от арности операции и от способа определения функции. Операция-функция определяет алгоритм выполнения перегруженной операции, когда эта операция применяется к объектам класса, для которого операция-функция введена. Чтобы явная связь с классом была обеспечена, операция функция должна быть либо компонентом класса, либо она должна быть определена в классе как дружественная, либо у нее должен быть хотя бы один параметр типа класс (или ссылка на класс). Начнем с последнего варианта. Если для класса Т введена операция-функция с заголовком Т operator *(T х, T у) и определены два объекта А, В класса Т, то выражение А*В интерпретируется как вызов функции operator * (А,B). Вторую возможность перегрузки бинарной операции представляют компонентные функции классов. Любая стандартная бинарная операция @ может быть перегружена с помощью нестатической операции-функции, входящей в число компонентов класса. В этом случае у нее должен быть только один параметр и заголовок может выглядеть так: Т operator @ (Т х) (здесь Т - определенный пользователем тип, т.е. класс). В этом случае выражение A@B c объектами А, В класса T в качестве операндов интерпретируется как вызов функции А.operator@(В), причем в теле операции-функции выполняется обработка компонентов объекта-параметра В и того объекта А, для которого осуществлен вызов. При необходимости принадлежность компонентов объекту А в теле операции-функции можно сделать явным с помощью указателя this. Итак, механизм классов дает возможность программисту определять новые типы данных, отображающие понятия решаемой задачи. Перегрузка стандартных операций языка Си++ позволяет сделать операции над объектами новых классов удобными и общепонятными. Но возникают два вопроса. Можно ли вводить собственные обозначения для операций, не совпадающие со стандартными операциями языка Си++? И все ли операции языка Си++ могут быть перегружены? К сожалению (или как констатация факта), вводить операции с совершенно новыми обозначениями язык Си++ не позволяет. Ответ на второй вопрос также отрицателен - существует несколько операций, не допускающих перегрузки. Вот их список: прямой выбор компонента структурированного объекта; . * обращение к компоненту через указатель на него; ?: условная операция; :: операция указания области видимости; sizeof операция вычисления размера в байтах; # препроцессорная операция; ## препроцессорная операция. Рассмотрим еще несколько важных особенностей механизма перегрузки (расширения действия) стандартных операций языка Си++. При расширении действия (при перегрузке) стандартных операций нельзя и нет возможности изменять их приоритеты. Нельзя изменить для перегруженных операций синтаксис выражений, т.е. невозможно ввести унарную операцию = или бинарную операцию ++. Нельзя вводить новые лексические обозначения операций, даже формируя их из допустимых символов. Например, возведение в степень ** из языка Фортран нельзя ввести в языке Си++. Любая бинарная операция @ определяется для объектов некоторого класса двумя существенно разными способами: либо как компонентная функция с одним параметром, либо как глобальная (возможно дружественная) функция с двумя параметрами. В первом случае х@у означает вызов х.operator@(у), во втором случае х@у означает вызов operator@(x,y). В соответствии с семантикой бинарных операций ' = ', ' [ ] ', ' ->' операции-функции с названиями operator =, operator [ ], operator -> не могут быть глобальными функциями, а должны быть нестатическими компонентными функциями. "Это гарантирует, что первыми операндами будут lvalue". Любая унарная операция '$' определяется для объектов некоторого класса также двумя способами: либо как компонентная функция без параметров, либо как глобальная (возможно дружественная) функция с одним параметром. Для префиксной операции ' $' выражение $z означает вызов компонентной функции z..operator $ () или вызов глобальной функции operator $(z). Для постфиксной операции выражение z$ означает либо вызов компонентной функции z.operator$(), либо вызов глобальной функции operator$(z). Синтаксис языка Си++ определяет некоторые встроенные операции над стандартными типами как комбинации других встроенных операций над теми же операндами. Например, для переменной long m = 0; выражение ++m означает m += 1, что в свою очередь означает выполнение выражения m = m + 1. Такие автоматические замены выражений не реализуются и не справедливы для перегруженных операций. Например, в общем случае определение operator *=() нельзя вывести из определений operator * () и operator = (). Нельзя изменить смысл выражения, если в него не входит объект класса, введенного пользователем. В частности, нельзя определить операцию-функцию, действующую только на указатели. Невозможно для операнда m типа int изменить смысл выражения 2 + m и т.п. "Операция-функция, первым параметром которой предполагается основной (стандартный) тип, не может быть компонентной функцией". Для объяснения этого ограничения предположим, что аа - объект некоторого класса и для него расширено действие операции ' + '. При разборе выражения аа + 2 компилятором выполняется вызов операции-функции аа. operator + (2) или operator +(aa,2). При разборе 2 + аа допустим вызов operator + (2,аа), но ошибочен 2.operator + (аа). Таким образом, расширение действия операции + на выражение стандартный _тип + объект_класса допустимо только с помощью глобальных операций-функций. При расширении действия операций приходится предусматривать всевозможные сочетания типов операндов. Например, определяя операцию сложения ' + ' для комплексных чисел, приходится учитывать сложение комплексного числа с вещественным и вещественного с комплексным, комплексного с целым и целого с комплексным и т.д. Если учесть, что вещественные числа представлены несколькими ти-пами (float, double, long double) и целые числа имеют разные типы (int, long, unsigned, char), то оказывается необходимым ввести большое количество операций-функций. К счастью, при вызове операций-функций действуют все соглашения о преобразованиях стандартных типов параметров, и нет необходимости учитывать сочетания всех типов. В ряде случаев для бинарной операции достаточно определить только три варианта: • стандартный_тип, класс • класс, стандартный_тип • класс, класс. Например, для рассмотренного класса comp можно ввести как дружественные такие операции-функции: comp operator +(comp x, comp у) {return(comp(x.real + y.real, x.imag + y.imag)); } comp operator + (double x, complex y) {return(comp(x + y.real, y.imag));) comp operator + (complex x, double y) { return(comp(x.real + y, x.imag));} После этого станут допустимыми выражения в следующих операторах: comp СС(1.О,2.0); comp ЕЕ; ЕЕ. = 4.0 + СС; ЕЕ = ЕЕ + 2.0; ЕЕ = СС + ЕЕ; ЕЕ = СС + 20; // По умолчанию приведение int к double СС = ЕЕ + 'е'; //По умолчанию приведение char к double Вместо использования нескольких (в нашем примере вместо трех) очень схожих операций-функций можно задачу преобразования стандартного типа в объект класса поручить конструктору. Для этого требуется только одно - необходим конструктор, формирующий объект класса по значению стандартного типа. Например, добавление в класс complex такого конструктора comp (double x) { real = x; imag =0.0; } позволяет удалить все дополнительные операции-функции, оставив только одну с прототипом: friend comp operator +(comp, comp); В этом случае целый операнд выражения 6+ЕЕ автоматически преобразуется к типу double, а затем конструктор формирует комплексное число с нулевой мнимой частью. Далее выполняется операция-функция operator +(comp(double(6),double(0)), ЕЕ) Вместо включения в класс дополнительного конструктора с одним аргументом можно в заголовке единственного конструктора ввести умалчиваемое значение второго параметра: compl(double r, double i = 0.0) { Re=r; Im = i; } Теперь каждое выражение с операцией ' + ', в которое входит, кроме объекта класса comp, операнд одного из стандартных типов, будет обрабатываться совершенно верно. Однако такое умалчивание является частным решением и не для всех классов пригодно. В отличие от всех других унарных операций операции ++ и --имеют, кроме префиксной формы еще и постфиксную. Это привело к особенностям при их перегрузке. В начальных версиях языка Си++ при перегрузках операций ++ и -- не делалось различия между постфиксной и префиксной формами. В современной версии языка Си++ принято соглашение, что перегрузка префиксных операций ++ и -- ничем не отличается от перегрузки других унарных операций, т.е. глобальные и, возможно, дружественные функции operator ++() и operator - -() с одним параметром некоторого класса определяют префиксные операции ++ и --. Компонентные операции-функции без параметров определяют те же префиксные операции. При расширении действия постфиксных операций ++ и -- операции-функции должны иметь еще один дополнительный параметр типа int. Если для перегрузки используется компонентная операция-функция, то она должна иметь один параметр типа int. Если операция-функция определена как глобальная (не компонентная), то ее первый параметр должен иметь тип класса, а второй - тип int. Когда в программе используется соответствующее постфиксное выражение, то операция-функция вызывается с нулевым целым параметром.
|
||||
Последнее изменение этой страницы: 2016-08-10; просмотров: 314; Нарушение авторского права страницы; Мы поможем в написании вашей работы! infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.135.184.124 (0.013 с.) |