Чи існує обмеження максимальної довжини масиву в C ++?


183

Чи існує максимальна довжина для масиву в C ++?

Це обмеження C ++ чи це залежить від моєї машини? Це підлаштовується? Це залежить від типу, з якого складається масив?

Чи можу я якось подолати цю межу чи потрібно шукати кращий спосіб зберігання інформації? А яким повинен бути найпростіший спосіб?

Що мені потрібно зробити - це зберігати довгий довгий int на масиві, я працюю в середовищі Linux. Моє запитання: що мені робити, якщо мені потрібно зберегти масив з N довгих цілих чисел з N> 10 цифр?

Мені це потрібно, тому що я пишу криптографічний алгоритм (як, наприклад, p-Pollard) для школи, і потрапляю на цю стіну цілих чисел та довжини представлення масивів.

Відповіді:


163

Є два обмеження, обидва не виконуються C ++, а скоріше апаратними засобами.

Перша межа (ніколи не повинна бути досягнута) встановлюється обмеженнями типу розміру, що використовується для опису індексу в масиві (та його розміру). Він задається максимальним значенням, яке std::size_tможе прийняти система. Цей тип даних досить великий, щоб містити розмір у байтах будь-якого об’єкта

Інший межа - обмеження фізичної пам'яті. Чим більше об’єктів у масиві, тим швидше буде досягнуто це обмеження, оскільки пам'ять заповнена. Наприклад, a vector<int>заданого розміру n зазвичай займає в кілька разів більше пам'яті, ніж масив типу vector<char>(мінус невелике постійне значення), оскільки intзазвичай більший за char. Тому vector<char>може містити більше елементів, ніж vector<int>раніше, ніж пам’ять заповнена. Те саме враховується для необроблених масивів у стилі С, як int[]і char[].

Крім того, на цю верхню межу може впливати тип, який allocatorвикористовується для побудови, vectorоскільки allocatorвільний керувати пам'яттю будь-яким способом. Дуже дивний, але, тим не менш, мислимий розподільник може об'єднати пам'ять таким чином, що однакові екземпляри об'єкта діляться ресурсами. Таким чином, ви можете вставити в контейнер безліч однакових об'єктів, які б інакше використовували всю наявну пам'ять.

Крім цього, C ++ не застосовує жодних обмежень.


20
Крім того, ви можете легко змінювати обмеження розміру стека, особливо якщо ви використовуєте теми, які знову-таки є специфічними для впровадження (але можуть бути змінені).
Аларік

@Alaric: Правда. Я не хотів надто глибоко вникати в специфіку системи, тому що вони дуже відрізняються, і я не знаю жодної з них.
Конрад Рудольф

@Konrad, цікавий момент про типи алокаторів і не те, про що я знав. Дякуємо за інформацію.
SmacL

11
std :: size_t зазвичай (завжди?) - це розмір вказівника, а не розмір найбільшого цілого числа, яке має вбудовану технічну підтримку в цілочисельній математичній одиниці. У будь-якій ОС x86, яку я використовував, розмір_t - 32-бітний для 32-бітної ОС та 64-розрядний для 64-бітної ОС.
Містер Фооз

2
Я розумію, що максимальна межа з масиву максимального значення процесора слова . Це пов’язано з оператором індексації. Наприклад, машина може мати розмір слова 16 біт, але регістр адрес 32 біт. Частка пам'яті обмежена за розміром параметром, переданим до newабо malloc. Шматок пам'яті, більший за масив, можна отримати через вказівник.
Томас Меттьюз

171

Ніхто не згадав про обмеження розміру кадру стека .

Можна виділити два місця пам'яті:

  • На купі (динамічно виділена пам'ять).
    Обмеження розміру тут - це комбінація наявного обладнання та можливості ОС імітувати простір за допомогою інших пристроїв для тимчасового зберігання невикористаних даних ( тобто переміщення сторінок на жорсткий диск).
  • На стеку (Локально оголошені змінні).
    Тут обмежений розмір визначено компілятором (з можливими обмеженнями на апаратні засоби). Якщо ви читаєте документацію компілятора, ви можете часто змінити цей розмір.

Таким чином, якщо ви розподіляєте масив динамічно (ліміт великий і детально описаний іншими повідомленнями).

int* a1 = new int[SIZE];  // SIZE limited only by OS/Hardware

Як варіант, якщо масив виділяється на стек, то ви обмежені розміром кадру стека. NB вектори та інші контейнери мають невелику присутність у стеці, але зазвичай основна частина даних буде знаходитись у купі.

int a2[SIZE]; // SIZE limited by COMPILER to the size of the stack frame

4
Переважне розміщення великих масивів не відбувається на стеку чи глобально визначеному, а скоріше за допомогою динамічного розподілу (через newабо malloc).
Томас Меттьюз

1
@Thomas Matthews: Не в моєму світі. Динамічно виділені об'єкти потребують управління. Якщо потрібно динамічно виділити, я використовував би об'єкт стека, який представляє динамічно виділену пам ятку, як std :: vector.
Мартін Йорк

2
Не вистачає однієї справи з куточками: Global Arraysхоча вони не є красунею і найкраще уникати, вони не підпадають під обмеження stack, і вам не потрібно malloc/ freeпрацювати з ними.
тед

1
@ted, чому слід «найкраще уникати глобальних масивів»? Якщо бути точнішим, я думаю, ви маєте на увазі статично розподілені масиви. Їх масштаби не повинні бути глобальними. Я можу стверджувати, що вони кращі, ніж динамічні масиви, оскільки ви можете використовувати абсолютну адресацію з ними (принаймні, в Linux), чого не можна робити з динамічно розподіленими масивами.
Z boson

2
Дуже важливий момент. Нещодавно я натрапив на проект з відкритим вихідним кодом "якості виробництва", який надав налаштований максимальний розмір буфера. Всі буфери були виділені на стеку, тому конфігурування достатньо великого значення призведе до того, що програма відразу ж запуститься на сегмент за замовчуванням.
aroth

13

Дивлячись на це з практичної, а не теоретичної точки зору, для 32-бітної системи Windows максимальний загальний об'єм пам'яті, доступний для одного процесу, становить 2 Гб. Ви можете зламати ліміт, перейшовши на 64-бітну операційну систему з набагато більшою фізичною пам'яттю, але чи потрібно це робити чи шукати альтернативи, дуже залежить від призначених користувачів та їх бюджетів. Ви також можете дещо розширити його за допомогою PAE .

Тип масиву дуже важливий, оскільки вирівнювання структури за замовчуванням для багатьох компіляторів становить 8 байт, що дуже марно, якщо використання пам'яті є проблемою. Якщо ви використовуєте Visual C ++ для націлювання на Windows, ознайомтеся з директивою #pragma pack як спосіб подолання цього.

Інша річ, що потрібно зробити, це подивитися на те, що в методах стиснення пам’яті можуть допомогти вам, як-от рідкісні матриці, при стисненні льоту та інше ... Знову це дуже залежить від програми. Якщо ви відредагуєте свою публікацію, щоб дати більше інформації про те, що є насправді у ваших масивах, ви можете отримати більше корисних відповідей.

Редагувати: Враховуючи трохи більше інформації про ваші точні вимоги, схоже, що ваш об'єм пам’яті має бути від 7,6 ГБ до 76 ГБ, нестиснений, що потребує досить дорогого 64-бітного вікна для зберігання у масиві пам’яті на C ++. Виникає питання, чому ви хочете зберігати дані в пам'яті, де передбачається швидкість доступу, і дозволити випадковий доступ. Найкращий спосіб зберігати ці дані поза масивом - це майже на основі того, як ви хочете отримати доступ до них. Якщо вам потрібно отримати доступ до членів масиву випадковим чином, для більшості додатків, як правило, існують способи групування згустків даних, які мають доступ одночасно. Наприклад, у великих ГІС та просторових базах даних дані часто поповнюються географічною територією. У термінах програмування на C ++ ви можете замінити оператор масиву [], щоб отримати необхідні частини даних із зовнішнього сховища.


1
Є системні виклики, які дозволяють виділяти пам'ять поза програмним простором; але це залежить від ОС і не є портативним. Ми використовували їх у вбудованих системах.
Томас Меттьюз

4

Я погодився б із вищезгаданим, що якщо ви задумуєте свій масив

 int myArray[SIZE] 

то SIZE обмежується величиною цілого числа. Але ви завжди можете розмістити шматок пам'яті і мати вказівник на нього, такий великий, як ви хочете, поки malloc не поверне NULL.


Я не впевнений, чи це неправильно, чи я вас неправильно зрозумів, чи щось інше. Наприклад, цьому заважає компілятор MSVC17: int oops[INT_MAX]{0};Він генерує,C2148 - total size of array must not exceed 0x7fffffff bytes
kayleeFrye_onDeck

З 16 Гб DDR4 і про 66%пам'ять, що використовується в даний час до запуску мого додатка як налагодження в Windows 10 з VS2017, у мене є невизначений ліміт щодо величини внутрішнього масиву, з яким я можу ініціалізуватися 0. Іноді я можу це зробити з елементами ~ 257k, іноді отримую переповнення стека. Якщо я додаю що- небудь до свого додатка, крім основного та масиву, це число знижується (очевидно). Мені довелося експериментувати, щоб визначити це число, тому я не бачу, як на цю метрику можна покластися поза знанням ваших теоретичних меж у вакуумі.
kayleeFrye_onDeck

4

Щоб узагальнити відповіді, розгорніть їх та відповіді на ваше запитання безпосередньо:

Ні, C ++ не встановлює жодних обмежень для розмірів масиву.

Але оскільки масив повинен зберігатися десь у пам'яті, так застосовуються обмеження щодо пам'яті, накладені іншими частинами комп'ютерної системи. Зауважте, що ці обмеження безпосередньо не стосуються розмірів (= кількість елементів) масиву, а скоріше його розміру (= обсяг зайнятої пам'яті). Розміри ( D ) і в оперативній пам'яті розмір ( S ) з масиву не те ж саме, як вони пов'язані пам'яті , прийнятим одним елементом ( Е ): S = Д * Е .

ТеперE залежить від:

  • тип елементів масиву (елементи можуть бути меншими або більшими)
  • вирівнювання пам'яті (для підвищення продуктивності елементи розміщуються за адресами, множими деяке значення, що вводить
    "витрачений простір" між елементами
  • розмір статичних частин об'єктів (в об'єктно-орієнтованому програмуванні статичні компоненти об'єктів одного типу зберігаються лише один раз, незалежно від кількості таких однотипних об'єктів)

Також зауважте, що ви, як правило, отримуєте різні обмеження, пов'язані з пам'яттю, розподіляючи масиви даних на стеці (як автоматична змінна int t[N]:), або на купі (динамічне розміщення з malloc()/ newабо з використанням механізмів STL), або в статичній частині пам'яті процесу (як статична змінна:) static int t[N]. Навіть при розподілі по купі, вам все одно потрібний невеликий об'єм пам’яті на стеці, щоб зберігати посилання на виділені купою блоки пам'яті (але це, як правило, незначно).

Розмір size_tтипу не впливає на програміста (я припускаю, що програміст використовує size_tтип для індексації, оскільки він призначений для нього), оскільки постачальник компілятора має typedefйого до цілого типу, достатньо великого, щоб вирішити максимальний об'єм пам'яті, можливий для даної платформи архітектури.

Витікають джерела обмежень щодо розміру пам'яті

  • об'єм пам'яті, доступної для процесу (який обмежений 2 ^ 32 байтами для 32-бітних програм, навіть на 64-бітних ядрах ОС),
  • поділ оперативної пам’яті (наприклад, обсяг пам’яті процесора, розроблений для стека або купи),
  • фрагментація фізичної пам'яті (багато розпорошених невеликих фрагментів вільної пам'яті не застосовуються для зберігання однієї монолітної структури),
  • кількість фізичної пам'яті,
  • і кількість віртуальної пам'яті.

Їх не можна "переробити" на рівні програми, але ви можете використовувати інший компілятор (щоб змінити обмеження розміру стека), або перенести додаток на 64-бітні або перенести його на іншу ОС, або змінити фізичну / конфігурація віртуальної пам'яті (віртуальної? фізичної?) машини.

Не рідкість (і навіть доцільно) трактувати всі перераховані вище фактори як зовнішні порушення і, таким чином, як можливі джерела помилок виконання, а також ретельно перевіряти та реагувати на помилки, пов’язані з розподілом пам'яті, у вашому програмному коді.

Отже, нарешті: хоча C ++ не встановлює жодних обмежень, вам все одно доведеться перевірити наявність несприятливих умов, пов’язаних з пам’яттю, під час запуску коду ... :-)


3

Як зазначається безліч відмінних відповідей, існує маса обмежень, які залежать від вашої версії компілятора C ++, характеристик операційної системи та комп'ютера. Однак я пропоную наступний скрипт на Python, який перевіряє ліміт на вашій машині.

Він використовує двійковий пошук і на кожній ітерації перевіряє, чи можливий середній розмір, створюючи код, який намагається створити масив розміру. Сценарій намагається скомпілювати його (вибачте, ця частина працює лише на Linux) і налаштувати двійковий пошук залежно від успіху. Перевір:

import os

cpp_source = 'int a[{}]; int main() {{ return 0; }}'

def check_if_array_size_compiles(size):
        #  Write to file 1.cpp
        f = open(name='1.cpp', mode='w')
        f.write(cpp_source.format(m))
        f.close()
        #  Attempt to compile
        os.system('g++ 1.cpp 2> errors')
        #  Read the errors files
        errors = open('errors', 'r').read()
        #  Return if there is no errors
        return len(errors) == 0

#  Make a binary search. Try to create array with size m and
#  adjust the r and l border depending on wheather we succeeded
#  or not
l = 0
r = 10 ** 50
while r - l > 1:
        m = (r + l) // 2
        if check_if_array_size_compiles(m):
                l = m
        else:
                r = m

answer = l + check_if_array_size_compiles(r)
print '{} is the maximum avaliable length'.format(answer)

Ви можете зберегти його на своїй машині та запустити, і він надрукує максимальний розмір, який ви можете створити. Для моєї машини це 2305843009213693951.


2

Я не думаю, що було сказано в попередніх відповідях.

Я завжди відчуваю "неприємний запах" у сенсі рефакторингу, коли люди використовують такі речі у своєму дизайні.

Це величезний масив і, можливо, не найкращий спосіб представити свої дані як з точки зору ефективності, так і з точки зору продуктивності.

ура,

Роб


Чи є у вас якісь пропозиції щодо того, що я повинен використовувати?
луїс

Якщо ви можете сказати нам, які дані ви зберігаєте, можливо, ми можемо. (-:
Роб Уеллс

Вибачте Луїс, моя перша відповідь була дуже легковажною. Це буде залежати від характеру ваших даних. Відношення ваших даних буде визначати модель, яку ви використовуєте для представлення даних. Тоді колекція має бути зрозумілою з цього. Якщо ні, то я б переймався моделлю даних.
Роб Веллс

не так легковажно для мене: як щодо кешованої бази даних з такою іграшкою? tweaktown.com/news/22066/…

2

Якщо вам доведеться мати великі дані, вам потрібно розділити їх на керовані фрагменти. Це не все впишеться в пам'ять на будь-якому невеликому комп’ютері. Ви, ймовірно, можете завантажувати частину даних з диска (що розумно підходить), виконувати свої обчислення та зміни на ньому, зберігати їх на диску, а потім повторювати до завершення.


Дивіться також для сортування об'єднань на прикладі алгоритму для обробки даних, занадто великих, щоб вміститись у пам'яті.
Томас Меттьюз

2

Настільки прикро неспецифічні, як і всі нинішні відповіді, вони в основному вірні, але з багатьма застереженнями, які не завжди згадуються. Суть у тому, що у вас є дві верхні межі, і лише одна з них - це щось фактично визначене, тому YMMV :

1. Складіть часові межі

В основному, що дозволить ваш компілятор. Для Visual C ++ 2017 у вікні x64 для Windows 10 це максимальний ліміт на час компіляції до встановлення обмеження 2 Гб,

unsigned __int64 max_ints[255999996]{0};

Якщо я зробив це замість цього,

unsigned __int64 max_ints[255999997]{0};

Я отримаю:

Error C1126 automatic allocation exceeds 2G

Я не впевнений, як 2G корелює з 255999996/ 7. Я погуглив обидві цифри, і єдине, що я міг знайти, що, можливо, було пов'язано, це це * nix Q&A щодо питання точностіdc . Так чи інакше, не здається, який тип масиву int ви намагаєтеся заповнити, скільки саме елементів можна виділити.

2. Межі часу виконання

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

int main()
{
    int max_ints[257400]{ 0 };
    return 0;
}

Але якщо я його тріщиную лише трохи ...

int main()
{
    int max_ints[257500]{ 0 };
    return 0;
}

Бам! Переповнення стека!

Exception thrown at 0x00007FF7DC6B1B38 in memchk.exe: 0xC00000FD: Stack overflow (parameters: 0x0000000000000001, 0x000000AA8DE03000). Unhandled exception at 0x00007FF7DC6B1B38 in memchk.exe: 0xC00000FD: Stack overflow (parameters: 0x0000000000000001, 0x000000AA8DE03000).

І лише для того, щоб детально ознайомитись з усією наповнюваністю вашого додатка, це було добре:

int main()
{
    int maxish_ints[257000]{ 0 };
    int more_ints[400]{ 0 };
    return 0;
}  

Але це спричинило переповнення стека:

int main()
{
    int maxish_ints[257000]{ 0 };
    int more_ints[500]{ 0 };
    return 0;
}  

1

Я здивований, що функція члена max_size () std :: vector тут не згадується.

"Повертає максимальну кількість елементів, які вміє вмістити контейнер через обмеження щодо впровадження системи або бібліотеки, тобто std :: відстань (для початку (), кінця ()) для найбільшого контейнера."

Ми знаємо, що std::vectorреалізується як динамічний масив під кришкою, тому max_size()слід дати дуже близьке наближення максимальної довжини динамічного масиву на вашій машині.

Наступна програма будує таблицю приблизної максимальної довжини масиву для різних типів даних.

#include <iostream>
#include <vector>
#include <string>
#include <limits>

template <typename T>
std::string mx(T e) {
    std::vector<T> v;
    return std::to_string(v.max_size());
}

std::size_t maxColWidth(std::vector<std::string> v) {
    std::size_t maxWidth = 0;

    for (const auto &s: v)
        if (s.length() > maxWidth)
            maxWidth = s.length();

    // Add 2 for space on each side
    return maxWidth + 2;
}

constexpr long double maxStdSize_t = std::numeric_limits<std::size_t>::max();

// cs stands for compared to std::size_t
template <typename T>
std::string cs(T e) {
    std::vector<T> v;
    long double maxSize = v.max_size();
    long double quotient = maxStdSize_t / maxSize;
    return std::to_string(quotient);
}

int main() {
    bool v0 = 0;
    char v1 = 0;

    int8_t v2 = 0;
    int16_t v3 = 0;
    int32_t v4 = 0;
    int64_t v5 = 0;

    uint8_t v6 = 0;
    uint16_t v7 = 0;
    uint32_t v8 = 0;
    uint64_t v9 = 0;

    std::size_t v10 = 0;
    double v11 = 0;
    long double v12 = 0;

    std::vector<std::string> types = {"data types", "bool", "char", "int8_t", "int16_t",
                                      "int32_t", "int64_t", "uint8_t", "uint16_t",
                                      "uint32_t", "uint64_t", "size_t", "double",
                                      "long double"};

    std::vector<std::string> sizes = {"approx max array length", mx(v0), mx(v1), mx(v2),
                                      mx(v3), mx(v4), mx(v5), mx(v6), mx(v7), mx(v8),
                                      mx(v9), mx(v10), mx(v11), mx(v12)};

    std::vector<std::string> quotients = {"max std::size_t / max array size", cs(v0),
                                          cs(v1), cs(v2), cs(v3), cs(v4), cs(v5), cs(v6),
                                          cs(v7), cs(v8), cs(v9), cs(v10), cs(v11), cs(v12)};

    std::size_t max1 = maxColWidth(types);
    std::size_t max2 = maxColWidth(sizes);
    std::size_t max3 = maxColWidth(quotients);

    for (std::size_t i = 0; i < types.size(); ++i) {
        while (types[i].length() < (max1 - 1)) {
            types[i] = " " + types[i];
        }

        types[i] += " ";

        for  (int j = 0; sizes[i].length() < max2; ++j)
            sizes[i] = (j % 2 == 0) ? " " + sizes[i] : sizes[i] + " ";

        for  (int j = 0; quotients[i].length() < max3; ++j)
            quotients[i] = (j % 2 == 0) ? " " + quotients[i] : quotients[i] + " ";

        std::cout << "|" << types[i] << "|" << sizes[i] << "|" << quotients[i] << "|\n";
    }

    std::cout << std::endl;

    std::cout << "N.B. max std::size_t is: " <<
        std::numeric_limits<std::size_t>::max() << std::endl;

    return 0;
}

У моєму macOS (clang версія 5.0.1) я отримую наступне:

|  data types | approx max array length | max std::size_t / max array size |
|        bool |   9223372036854775807   |             2.000000             |
|        char |   9223372036854775807   |             2.000000             |
|      int8_t |   9223372036854775807   |             2.000000             |
|     int16_t |   9223372036854775807   |             2.000000             |
|     int32_t |   4611686018427387903   |             4.000000             |
|     int64_t |   2305843009213693951   |             8.000000             |
|     uint8_t |   9223372036854775807   |             2.000000             |
|    uint16_t |   9223372036854775807   |             2.000000             |
|    uint32_t |   4611686018427387903   |             4.000000             |
|    uint64_t |   2305843009213693951   |             8.000000             |
|      size_t |   2305843009213693951   |             8.000000             |
|      double |   2305843009213693951   |             8.000000             |
| long double |   1152921504606846975   |             16.000000            |

N.B. max std::size_t is: 18446744073709551615

На ideone gcc 8.3 я отримую:

|  data types | approx max array length | max std::size_t / max array size |
|        bool |   9223372036854775744   |             2.000000             |
|        char |   18446744073709551615  |             1.000000             |
|      int8_t |   18446744073709551615  |             1.000000             |
|     int16_t |   9223372036854775807   |             2.000000             |
|     int32_t |   4611686018427387903   |             4.000000             |
|     int64_t |   2305843009213693951   |             8.000000             |
|     uint8_t |   18446744073709551615  |             1.000000             |
|    uint16_t |   9223372036854775807   |             2.000000             |
|    uint32_t |   4611686018427387903   |             4.000000             |
|    uint64_t |   2305843009213693951   |             8.000000             |
|      size_t |   2305843009213693951   |             8.000000             |
|      double |   2305843009213693951   |             8.000000             |
| long double |   1152921504606846975   |             16.000000            |

N.B. max std::size_t is: 18446744073709551615

Слід зазначити, що це теоретична межа і що на більшості комп’ютерів у вас не вистачить пам’яті набагато раніше, ніж ви досягнете цієї межі. Наприклад, ми бачимо, що для типу charon gccмаксимальна кількість елементів дорівнює max std::size_t. Спробувавши це , ми отримаємо помилку:

prog.cpp: In function int main()’:
prog.cpp:5:61: error: size of array is too large
  char* a1 = new char[std::numeric_limits<std::size_t>::max()];

Нарешті, як зазначає @MartinYork, для статичних масивів максимальний розмір обмежений розміром вашого стека.


0

Як уже зазначалося, розмір масиву обмежений вашим обладнанням та вашою ОС (man ulimit). Хоча ваше програмне забезпечення може бути обмежене лише вашою творчістю. Наприклад, чи можете ви зберегти "масив" на диску? Вам справді потрібні довгі довгі вставки? Вам справді потрібен щільний масив? Вам взагалі потрібен масив?

Одним з простих рішень було б використання 64-бітного Linux. Навіть якщо фізично у вас недостатньо оперативної пам’яті для вашого масиву, ОС дозволить вам розподіляти пам'ять так, як якщо б ви це зробили, оскільки наявна у вашому процесі віртуальна пам'ять, швидше за все, значно більша, ніж фізична пам'ять. Якщо вам дійсно потрібно отримати доступ до всього масиву, це означає його зберігання на диску. Залежно від вашої схеми доступу, можуть бути більш ефективні способи зробити це (наприклад, використовувати mmap () або просто зберігати дані послідовно у файлі (у цьому випадку вистачить 32-бітного Linux)).


2
Хм, диски, масиви, ... ніхто не чує віртуальної пам'яті . ОС, які підтримують віртуальну пам'ять , почнуть використовувати зовнішній пристрій для пам’яті, наприклад, жорсткий диск, і поміняти фрагменти внутрішньою пам’яттю.
Томас Меттьюз

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.