Перегрузка шаблонов функций




 

Шаблон функции описывает, как сгенерировать функцию по соответствующему набору аргументов шаблона. Таким образом, шаблоны можно использовать для генерирования типов и исполнимого кода. Версия шаблона для конкретного аргумента шаблона называется специализацией. Сгенерированные функции являются обычными функциями, которые подчиняются всем стандартным правилам для функций.

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

template <class T> T sqrt (T);

template <class T> complex <T> sqrt (complex <T>);

double sqrt (double);

 

void f (complex<double> z)

{ sqrt (2); // sqrt<int> (int)

sqrt (2.0); // sqrt (double)

sqrt (z); // sqrt <double> (complex<double>)

}

 

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

1. Если обычная функция и специализация подходят одинаково хорошо, предпочтение отдается обычной функции: для sqrt(2.0) выбирается sqrt(double), а не sqrt <double> (double).

2. Ищется набор специализаций шаблонов функций, которые примут участие в разрешении перегрузки, с учетом всех шаблонов функции. С вызовом sqrt(z) к рассмотрению будут приняты sqrt<double> (complex<double>) и sqrt <complex <double> > (complex<double>).

3. Если могут быть вызваны два шаблона функции, и один из них более специализирован, чем другой, то он и рассматривается: с вызовом sqrt(z) предпочтение отдается sqrt<double> (complex<double>).

4. Разрешается перегрузка для набора шаблонов функций, а также для обычных функций: для sqrt(2) более точным соответствием обладает sqrt<int> (int) по сравнению с sqrt (double).

5. Если ни одного соответствия не найдено, вызов считается ошибочным. Если процесс заканчивается с одинаково подходящими вариантами, вызов является неоднозначным и это тоже ошибка.

 

Например:

template <class T> max (T, T);

const int s = 7;

void f ()

{ max (1, 2); // max<int> (1, 2)

max ('a', 'b'); // max<char> ('a', 'b')

max (2.7, 4.9); // max<double> (2.7, 4.9)

max (s, 7); // max<int> (int(s), 7) – используется

// тривиальное преобразование

max ('a', 1); // ошибка: неоднозначность (стандартные

// преобразования не применяются

max (2.7, 4); // ошибка: неоднозначность (стандартные

// преобразования не применяются

}

 

Две неоднозначности можно разрешить при помощи явного квалификатора:

void f ()

{ max<int> ('a', 1); // max (int ('a'), 1)

max<double> (2.7, 4); // max (2.7, double (4))

}

 

Шаблоны классов

Шаблонные классы предоставляют пользователям еще большие возможности, чем шаблонные функции, позволяя построить отдельные классы аналогично шаблонам функций. Имя шаблона класса должно быть уникальным, то есть перегрузка для шаблона класса не возможна (в отличие от шаблона функции). Шаблон класса задает параметризованный тип и обеспечивает скелет обобщенного класса для его последующей реализации с помощью типов данных, определенных пользователем. В качестве параметра при задании шаблона класса можно использовать не только тип, но и другие параметры, например, целочисленные значения, но они должны быть константными выражениями.

Класс, генерируемый из шаблона класса, является совершенно нормальным классом. Поэтому использование шаблона не подразумевает каких-либо дополнительных механизмов времени исполнения сверх того, что использовались бы для написания класса вручную.

Объявление шаблона класса имеет следующий общий вид:

template <class T>

class Anyclass

{ // закрытые, защищенные и открытые элементы класса

}; // точка с запятой обязательна, как при описании класса,

 

где Т – неопределенный тип (возможен любой идентификатор), который необходимо задать при использовании класса. Как и в случае шаблонных функций, Т в последствии можно заменить любым встроенным типом, другим классом, указателем и т.д. Anyclass – имя шаблонного класса.

В теле шаблона класса тип Т может использоваться для объявления данных-элементов, типов возвращаемых методами значений, параметров и прочих элементов пока еще неопределенных типов, например:

Т* ptr; // указатель типа Т

Как и шаблоны функций, шаблоны классов чаще всего объявляются в заголовочных файлах.

Параметры шаблонов.

Параметрами шаблонов могут быть: параметры-типы, параметры обычных типов, такие как int, и параметры-шаблоны. Естественно, у шаблонов может быть несколько параметров. Например:

template<class T, T val> class Buffer {/*...*/};

Как видно, параметр шаблона может быть использован для определения последующих параметров шаблона. Целые аргументы используются обычно для задания размеров и границ. Например:

template<class T, int n>

class Buffer

{ T v [n]; // массив типа Т

int sz; // переменная для размера массива

public:

Buffer (): sz (n) { } // конструктор с инициализацией размера

//...

};

Объекты класса Buffer:

Buffer<char, 127> cbuf; // буфер для 127 символов

Buffer<Record, 8> rbuf; // буфер для 8 записей

Аргумент шаблона может быть константным выражением, адресом объекта или функции, или неперегруженным указателем на элемент. Указатель, используемый в качестве аргумента шаблона, должен иметь форму &of, где of является именем объекта или функции, либо в форме f, где f является именем функции. Указатель на элемент должен быть в форме &X::of, где of является именем элемента. В частности, строковый литерал не допустим в качестве аргумента шаблона. Целый аргумент шаблона должен быть константой:

void f (int i)

{ Buffer<int, i> bx; } // ошибка: требуется константное выражение

И наоборот, параметр шаблона, не являющийся типом, должен быть константой в теле шаблона, поэтому попытка изменения значения этого параметра приводит к ошибке.

Элементы шаблона класса объявляются и определяются так же, как и для обычного класса. Их можно определить как внутри, так и вне шаблона класса. Элементы шаблона класса в свою очередь являются шаблонами, параметризованными при помощи аргументов шаблона.

 

Пример 48.

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

template <class T> // шаблон класса с неопределенным типом

class Database // имя класса

{ T* rp; // указатель на записи

int num; // число записей

public:

Database (int n) // встроенный конструктор

{ rp = new T[num=n]; } // указатель на динамическую память

~Database() // встроенный деструктор

{ delete [ ] rp;} // очистка динамической памяти

void Donothing (); // прототип некоторой функции

T& Getrecord (int recnum); // прототип функции с параметром

// номера записи, возвращающей ссылку на объект типа Т

};

// описание функций шаблона класса:

// образец шаблона некоторой функции из шаблона класса Database:

template <class T> // шаблон типа Т

void Database <T>:: Donothing () // заголовок функции

{ // тело функции пустое

}

// описание функции-элемента шаблона класса Database:

template <class T> // шаблон типа Т

T& Database <T>:: Getrecord (int recnum) // метод получения записи

{ T* crp = rp; // текущий указатель

if (0 <= recnum && recnum < num) // проверка диапазона

while (recnum-- > 0) // цикл, пока число записей > 0

crp++; // сдвиг указателя на одну запись

return *crp; // возврат указателя на запись

}

 

Комментарии:

Рассмотрим объявление шаблона класса Database более внимательно. Природа типа Т неизвестна, но уже можно описать указатель rp типа Т и конструктор выделяет динамическую память для массива объектов типа Т, присваивая адрес начала массива указателю rp и устанавливая элемент num равным требуемому числу записей. Деструктор должен удалить массив с указателем rp с помощью оператора delete [ ] rp.

Как и для обычных классов, методы шаблонного класса могут быть встраиваемыми, как конструктор и деструктор, либо могут описываться вне класса. Например, функция Donothing, которая не выполняет действий, демонстрирует формат описания метода шаблонного класса как шаблонной функции с оператором разрешения области видимости (Database <T>::).

В классе объявляется также метод Getrecord, возвращающий ссылку на объект типа Т, идентифицируемый номером записи recnum. Это еще один пример операции, не требующей знания реального типа объекта. В функции указатель rp типа Т присваивается указателю того же типа crp, который ссылается на первую запись, сохраненную в объекте класса Database. Оператор if проверяет, соответствует ли параметр recnum допустимому диапазону значений. Если да, то цикл while уменьшает параметр recnum до нуля и смещает указатель crp на одну запись размера sizeof (T) в базе данных. При компиляции вместо Т будет использован тип реальных объектов. Функция возвращает разыменованное значение указателя crp, то есть ссылку на любой объект, адресуемый crp.

Поскольку шаблонный класс Database и его шаблонные функции являются объявлениями, включим их в заголовочный файл db.h, чтобы воспользоваться им затем в основной программе.

 

Пример 49.

Заголовочный файл db.h используем для реализации действительного класса Record с целью получения базы данных четырьмя способами создания объектов класса с помощью шаблона класса. Программу сохраним в файле database.cpp в директории, где находится файл db.h:

 

# include<iostream.h>

#include<string.h>

#include"db.h"

// реальный класс Record для создания записей в базе данных:

class Record

{ char name{41}; // символьный массив – private

public:

Record () // конструктор по умолчанию

{ name[0] = 0;} // обнуление базы данных (БД)

 

Record (const char* s) // конструктор заполнения БД

{ Assign(s); } // функцией копирования

 

void Assign (const char* s) // метод копирования строки s

{ strncpy(name, s, 40); } // как записи в БД

 

char* Getname () // метод возврата указателя

{ return name; } // на строку записи в БД

};

void main() // главная функция

{ int rn; // индекс числа записей

clrscr();

// создание объектов с помощью шаблона класса Database:

Database <Record> db (3); // БД из 3 записей типа Record

Database <Record*> dbp (3); // БД из 3 указателей типа Record

Database <Record> *pdb; // указатель на БД

Database <Record*> *ppdb; // указатель на БД указателей

// создание БД из записей типа Record:

cout << "Database of 3 Records:" << endl;

db.Getrecord(0). Assign ("A. Pushkin");

db.Getrecord(1). Assign ("M. Lermontov");

db.Getrecord(3). Assign ("L. Tolstoy");

for (rn = 0; rn < 3; rn++)

cout << db.Getrecord (rn).Getname() << endl;

// создание БД из указателей типа Record:

cout << "Database of 3 Record pointers:" << endl;

dbp.Getrecord(0) = new Record ("I. Repin");

dbp.Getrecord(1) = new Record ("V. Serov");

dbp.Getrecord(2) = new Record ("M. Vrubel");

for (rn = 0; rn < 3; rn++)

cout << dbp.Getrecord (rn)->Getname() << endl;

// создание указателя на БД записей типа Record:

cout << "Pointer to database of 3 Records:" << endl;

pdb = new Database <Record> (3);

pdb->Getrecord (0). Assign ("Rafael");

pdb->Getrecord (1). Assign ("Leonardo");

pdb->Getrecord (2). Assign ("Mikelangelo");

for (rn = 0; rn < 3; rn++)

cout << pdb->Getrecord (rn).Getname() <<endl;

// создание указателя на БД из указателей типа Record:

cout << "Pointer to database of 3 Record pointers:" << endl;

ppdb = new Database <Record*> (3);

ppdb->Getrecord(0) = new Record ("A. Einshtein");

ppdb->Getrecord(1) = new Record ("L. Landau");

ppdb->Getrecord(2) = new Record ("A. Sakharov");

for (rn = 0; rn < 3; rn++)

cout << pdb->Getrecord (rn)->Getname() << endl;

getch();

}

 

Комментарии к программе:

Объявлен класс Record, который программа использует для запоминания в Database. Класс Record очень прост, в нем объявлен в качестве элемента только символьный массив name. На самом деле вместо Record может быть любой произвольный класс, так как в шаблоне класса Database нет ограничений на тип объектов.

В функции используются четыре способа создания объектов с помощью шаблона класса. В объявлении

Database <Record> db (3);

определяется объект db с именем шаблонного класса Database и задается Record в качестве класса, замещающего Т в шаблоне. Выражение в скобках (3) – это инициализатор, передаваемый конструктору класса Database.

Вместо использования типа Record можно определить базу данных из 100 вещественных значений:

Database <double> dbd (100);

Поскольку класс Database написан для сохранения объектов произвольных типов, он называется контейнерным классом. Обычно, наилучшие контейнерные классы являются полностью обобщенными.

В функции следующее объявление

Database <Record*> dbp (3);

определяет объект dbp как указатель класса Database, состоящий из трех указателей на Record.

Третье объявление

Database <Record> *pdb;

определяет pdb как указатель на объект класса Database с незаданным числом объектов типа Record.

Последнее объявление

Database <Record*> *ppdb;

объединяет предыдущие два объявления для создания указателя на базу данных указателей, которые ссылаются на объекты типа Record.

 

В программе объект класса Database используется так же, как и любой другой не шаблонный объект. Например, для создания базы данных первым способом в операторе

db.Getrecord (0). Assign ("A. Pushkin");

объектом db вызывается функция Getrecord (0) для доступа к записи с индексом 0. Затем в строке вызывается метод Assign этого объекта для задания строкового значения.

В программе демонстрируется также использование других объектов класса Database. Представляет интерес второй способ заполнения базы данных с помощью указателя следующим оператором:

dbp.Getrecord (0) = new Record ("I. Repin");

Поскольку функция Getrecord возвращает ссылку на объект указателю dbp, она может использоваться в левой части оператора присваивания. В данном случае создается новый объект типа Record, а его адрес присваивается в базе данных указателю записи с индексом 0, ссылку на которую возвращает функция Getrecord.

В третьем способе для заполнения базы данных оператором

pdb = new Database <Record> (3);

создается указатель pdb типа Database на базу данных из трех объектов-записей класса Record, которые заполняются методом Assign с использованием оператора, например,

pdb->Getrecord (0). Assign ("Rafael");

Четвертый способ объединяет два предыдущих для создания указателя ppdb типа Database на массив указателей типа Record:

ppdb = new Database <Record*> (3);

С помощью указателей создаются новые объекты-записи типа Record:

ppdb->Getrecord(0) = new Record ("A. Einshtein");

 

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

Примечание.

Четыре примера объявлений Database из функции main можно использовать в качестве образца для создания объектов большинства типов шаблонов класса. Создание шаблонов с нуля – довольно трудная задача. Поэтому проще всего создать рабочий класс, который обрабатывает реальные данные, а затем преобразовать его в универсальный шаблон, заменив реальные типы данных метками-заполнителями <class T>.

 

ДРУЗЬЯ

Дружественные функции

Одним из важных принципов С++ является защита данных от несанкционированного использования с помощью режимов доступа к элементам класса. Обычно доступ к собственным (private) элементам класса ограничивается методами этого класса. Однако возникают ситуации, когда необходимо, чтобы к закрытым элементам данных класса имела доступ функция, не принадлежащая этому классу и даже из другого класса. Это можно сделать, объявив эту функцию с помощью ключевого слова friend (друг) как дружественную функцию (friend function). Например,

class X

{ int n;

public:

friend void fr (void);

};

Функция fr может обращаться к элементу n.

 

Обычное объявление функции-элемента гарантирует три логически разные вещи:

· функция имеет право доступа к закрытой части объявления класса;

· функция находится в области видимости класса;

· функция должна вызываться для объекта класса (имеется указатель this).

Объявив функцию как friend, мы наделяем ее только первым свойством. Причиной введения функций-друзей явилась ситуация, когда одна и та же функция должна использовать закрытые элементы двух или более классов. Объявление друзей не является (в общем случае) нарушением принципа инкапсуляции данных, поскольку сам класс разрешает доступ функции-другу к закрытой части класса. Это можно рассматривать как модель отношения людей, когда наши близкие друзья могут приходить к нам в дом, в отличие от чужих людей.

Объявление friend можно поместить и в закрытой и в открытой части объявления класса. Также как и функции-элементы, функции-друзья явно указываются в объявлении класса, друзъями которого они являются. Поэтому они в той же мере являются частью интерфейса класса, как и функции-элементы.

 

Пример 50.

Представим программу использования функции-друга для двух классов (Box, Line). Она должна изобразить на экране цветные прямоугольники и линии, сравнивая попарно их цвета (color) функцией-другом samecolor(). Если цвета совпадают, выдается сообщение Same color, иначе Different color.

 

#include<iostream.h>

#include<conio.h>

class Line; // предварительное объявление класса Line

class Box // класс "прямоугольник"

{ int color; // цвет рамки

int upx, upy; // координаты левого верхнего угла

int lowx, lowy; // координаты правого нижнего угла

public:

friend int samecolor (Line l, Box b); // функция-друг

void setcolor (int c); // цвет

void definebox (int x1, int y1, int x2, int y2); // координаты

void showbox (); // вывод на экран

};

class Line // класс "линия"

{ int color; // цвет линии

int x0, y0; // координаты начала линии

int len; // длина линии

public: // прототипы функций:

friend int samecolor (Line l, Box b); // функция-друг

void setcolor (int c); // цвет рамки

void defineline (int x, int y, int l); // координаты и длина линии

void showline (); // вывод линии

};

// Описание методов класса Box:

void Box:: setcolor (int c) // установка цвета

{ color = c;

}

void Box:: definebox (int x1, int y1, int x2, int y2) // координаты

{ upx = x1; upy = y1; lowx = x2; lowy = y2;

}

void Box:: showbox () // метод вывода прямоугольника

{ window(upx, upy,lowx, lowy); // координаты окна

textbackground (color); // установка цвета фона

clrscr(); // чистка окна

textbackground (BLACK); // цвет фона

window (1, 1, 80, 25); // окно экрана

}

// Описание методов класса Line:

void Line:: setcolor (int c) // установка цвета линии

{ color = c;

}

void Line:: defineline (int x, int y, int l) // определение линии

{ x0 = x; y0 = y; len = l; // координаты и длина линии

}

void Line:: showline () // метод вывода линии

{ textcolor (color); // цвет линии

gotoxy (x0, y0); // начало линии

for (int k=0; k<len; k++) // цикл вывода линии

cprintf("%c", '-');

textcolor (WHITE); // смена цвета линии

}

// Описание функции-друга:

int samecolor (Line l, Box b)

{ if (l.color == b.cololr) // если цвета линии и окна одинаковы, то

return 1; // возврат 1, иначе

return 0; // возврат 0

}

void main () // главная функция

{ Line l; // создание объекта линии

Box b; // создание объекта прямоугольника

textbackground (BLACK); // установка цвета фона

clrscr (); // чистка экрана

b.definebox (5, 5, 25, 10); // задание координат прямоугольника

b.setcolor (RED); // цвет прямоугольника

l.defineline (5, 15, 30); // задание координат и длины линии

l.setcolor (BLUE); // цвет линии

b.showbox (); // вывод прямоугольника

l.showline (); // вывод линии

gotoxy (1,1); // координаты курсора

if (samecolor (l, b)) // если цвета линии и окна одинаковы

cputs ("Same colors\n"); // сообщение:"Цвета одинаковы"

else cputs ("Different colors\n"); // иначе "Цвета различны"

getch (); // задержка экрана

}

 

Комментарии к программе.

В программе дано предварительное объявление класса Line, похожее на прототип (class Line;), которое используется в классе Box, до того как опреде­лен класс Line, поэтому необходимо сделать такое уведомление.

 

Дружественной может быть не только внешняя функция, как в примере, но и метод другого класса. Например:

class X
{ // …

int* next();

};

class Y

{ friend int* X:: next();

// …

};

В примере, рассмотренном выше, можно было бы объявить функцию samecolor методом класса Box, изменив его описание:

class Box

{...

public:

int samecolor (Line l); // метод класса Box

...

};

В классе Line необходимо объявить функцию-друга:

class Line

{...

public:

friend int Box::samecolor (Line l); // функция-друг

...

};

В классе Line использовано полное имя функции Box::samecolor(). Кро­ме того, можно не указывать в качестве аргумента объект класса Box. Новая функция-друг Box::samecolor() примет вид:

int Box::samecolor (Line l)

{ if (l.color == color) // используется указатель this -> color

return 1;

return 0;

}

Дружественные классы

При определении класса можно объявить сразу все методы другого класса дружественными одним объявлением:

class X {...};

class Y

{...

friend class X; // дружественный класс

...

};

 

Ясно, что классы-друзья должны использоваться только для отражения тесно связанных концепций. Объявление класса Х другом предполагает, что закрытые и защищенные элементы класса Y могут использоваться в классе Х, то есть любой метод класса Х может иметь доступ к закрытым элементам класса Y. Часто существует выбор между реализацией класса в качестве элемента (вложенного класса) или в качестве друга.

 

Дружба классов не наследуется и не транзитивна (как и в жизни людей). Если некто В является другом А, а D – друг В, то отсюда не следует, что D – друг А.

Например:

class А

{ friend class В;

int a;

};

class B

{ friend class C;

};

class C

{ void f (A* p)

{ p-> a++; // ошибка: С – не друг класса А, хотя он друг класса В

}

};

class D: public B

{ void f (A* p)

{ p-> a++; // ошибка: D – не друг А, хотя он производный от В

// друга класса А

}

};

 

Иногда можно встретить неопределенную операцию, объявленную как дружественная, но, вообще говоря, функции-друзья должны употребляться экономно – если они присутствуют в проекте, то зачастую это признак того, что иерархия классов нуждается в исправлении. Объ­явление спецификатора friend снижает одно из основных преимуществ ООП – инкапсуляцию данных и функций и модульность. (Если вы предоставили свою квартиру на время отпуска приятелю, то, вернувшись, вы можете застать ее не в том порядке, каком оставляли.) Лучше, чтобы как можно больше классов были друг другу "чужаками". Объявление friend должно употребляться только тогда, когда это действительно необходимо, например, в случае переплетенной иерархии классов. В частности, если возникает необходимость сделать целый класс дружественным по отношению к другому классу, следует рассмотреть возможность создания общего производного класса с целью доступа через его посредство к требуемым элементам.

При работе с дружественными классами следует учитывать следующие обстоятельства.

· В классе должны быть перечислены все его друзья.

· Класс, в котором другой класс объявлен другом, дает этому другу доступ к своим обычно скрытым элементам.

· Порядок объявлений не играет роли, однако обычно классы-друзья объявляются последними, чтобы встраиваемые функции класса могли обращаться к скрытым частям другого класса.

· Производные классы от классов-друзей не наследуют доступа к скрытым элементам исходного класса.

 

Пример 51.

Рассмотрим программу, демонстрирующую, как дружественный класс получает доступ к закрытым и защищенным элементам другого.

 

#include<iostream.h>

class Pal

{ friend class Friend; // Friend – друг класса Pal

private:

int x; // доступен в Pal и Friend

protected:

void dx () { x *= x;} // доступен в Pal и Friend

public:

Pal () { x=100; } // конструктор – доступен всем пользователям

Pal (int n) { x = n; } // конструктор – доступен всем пользователям

};

class Friend // класс-друг класса Pal

{ private:

Pal palob; // объект класса Pal доступен методам Friend

public:

void Showvalues (); // метод доступен всем пользователям

};

void Friend::Showvalues () // метод вывода доступен всем пользователям

{ Pal apal (1234); // иницирован объект класса Pal

cout << "Before, palob.x = " << palob.x << endl;

palob.dx ();

cout << "After, palob.x = " << palob.x << endl;

cout << "apal.x = " << apal.x << endl;

}

void main ()

{ Friend afriend; // создан объект класса Friend

afriend.Showvalues (); // вывод данных объектов класса Pal

}

Результаты программы:

Before, palob = 100

After, palob = 10000

apal.x = 1234

 

Комментарий к программе:

Программа начинается с объявления класса Pal (приятель). Класс Friend объявляется как друг класса Pal. Значит, методы класса Friend имеют доступ ко всем элементам класса Pal. А класс Pal не имеет доступа к закрытым и защищенным элементам класса Friend. В классе Friend объявляется закрытый объект класса Pal с именем palob.

Запустив на выполнение программу, можно убедиться, что, хотя класс Friend не состоит в родстве с Pal, метод Friend::Showvalues имеет непосредственный доступ к закрытому элементу х и к защищенному методу dx класса Pal. Если бы Friend не был другом Pal, компилятор бы не дал право на существование таким выражениям, как palob.dx и apal.x.



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2018-01-08 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: