Чому x [0]! = X [0] [0]! = X [0] [0] [0]?


149

Я вивчаю трохи С ++ і борюся з покажчиками. Я розумію, що я можу мати 3 рівня покажчиків, оголосивши:

int *(*x)[5];

так що *xце вказівник на масив з 5 елементів, на які вказують int. Також я це знаю x[0] = *(x+0);, x[1] = *(x+1)і так далі ...

Отже, з огляду на вищезазначену декларацію, чому так x[0] != x[0][0] != x[0][0][0]?


58
x[0], x[0][0]і x[0][0][0]мають різні типи. Їх не можна порівняти. Що ви маєте на увазі під !=?
болов

4
@celticminstrel вони не однакові: int **x[5]це масив з 5 елементів. Елемент - це вказівник на покажчик на int`
bolov

5
@celticminstrel int** x[5]буде масивом з п'яти покажчиків, які вказують на покажчики, які вказують на int. int *(*x)[5]- вказівник на масив з п'яти покажчиків, які вказують на int.
emlai


5
@ Leo91: По-перше, у вас тут два рівні вказівники, а не три. По-друге, що x[0] != x[0][0] != x[0][0][0]означає? Це неправдиве порівняння в C ++. Навіть якщо ви розбити його на x[0] != x[0][0]і x[0][0] != x[0][0][0]він до цих пір не діє. Отже, що означає ваше запитання?
ANT

Відповіді:


261

x- вказівник на масив з 5 покажчиків на int.
x[0]це масив з 5 покажчиків на int.
x[0][0]- покажчик на int.
x[0][0][0]є int.

                       x[0]
   Pointer to array  +------+                                 x[0][0][0]         
x -----------------> |      |         Pointer to int           +-------+
               0x500 | 0x100| x[0][0]---------------->   0x100 |  10   |
x is a pointer to    |      |                                  +-------+
an array of 5        +------+                        
pointers to int      |      |         Pointer to int                             
               0x504 | 0x222| x[0][1]---------------->   0x222                    
                     |      |                                             
                     +------+                                             
                     |      |         Pointer to int                              
               0x508 | 0x001| x[0][2]---------------->   0x001                    
                     |      |                                             
                     +------+                                             
                     |      |         Pointer to int                              
               0x50C | 0x123| x[0][3]---------------->   0x123                    
                     |      |                                             
                     +------+                                             
                     |      |         Pointer to int                              
               0x510 | 0x000| x[0][4]---------------->   0x000                    
                     |      |                                             
                     +------+                                             

Ви можете це бачити

  • x[0]є масивом і перетворюється на вказівник на його перший елемент при використанні у виразі (за деякими винятками). Тому x[0]дамо адресу свого першого елемента, x[0][0]який є 0x500.
  • x[0][0]містить адресу, intяка є 0x100.
  • x[0][0][0]містить intзначення 10.

Так, x[0]так само &x[0][0]і , отже, &x[0][0] != x[0][0].
Отже, x[0] != x[0][0] != x[0][0][0].


Ця діаграма мене трохи заплутує: 0x100повинна з’являтися негайно зліва від поля, що містить 10, так само, 0x500як і зліва від поля. Замість того, щоб бути далеко зліва та внизу.
ММ

@MattMcNabb; Я не думаю, що це має бути заплутаним, але змінюється відповідно до вашої пропозиції для більшої ясності.
хакі

4
@haccks - Моє задоволення :) Причина, чому ця діаграма чудова, полягає в тому, що вам навіть не потрібно пояснення, яке ви дали, що випливає з цього. Сама діаграма сама по собі пояснює, що вона вже відповідає на питання. Наступний текст - це просто бонус.
rayryeng

1
Ви також можете використовувати програмне забезпечення для діаграми yed. Це мені дуже допомагає в організації міркувань
rpax

@GrijeshChauhan Я використовую asciiflow для коментарів до коду, yeD для презентацій :)
rpax

133
x[0] != x[0][0] != x[0][0][0]

відповідно до вашої власної публікації,

*(x+0) != *(*(x+0)+0) != *(*(*(x+0)+0)+0)`  

що спрощено

*x != **x != ***x

Чому він повинен бути рівним?
Перший - це адреса якогось вказівника.
Другий - адреса іншого вказівника.
І третя - якась intцінність.


Я не можу зрозуміти ... якщо x [0], x [0] [0], x [0] [0] [0] еквівалентно * (x + 0), * (x + 0 + 0) , * (x + 0 + 0 + 0), чому вони повинні мати різні адреси?
Лео91

41
@ Leo91 x[0][0]є (x[0])[0], тобто *((*(x+0))+0), ні *(x+0+0). Відхилення відбувається до другого [0].
emlai

4
@ Leo91 x[0][0] != *(x+0+0)просто подобається x[2][3] != x[3][2].
özg

@ Leo91 Другий коментар, який ви "отримали зараз", видалено. Ви чогось не розумієте (що можна було б пояснити у відповіді), чи це не ваша справа? (дехто любить видаляти коментарі без особливого інформаційного вмісту)
deviantfan

@deviantfan вибачте, я не можу зрозуміти, що ви маєте на увазі. Я розумію відповіді, а також багато коментарів, які допомогли методу роз'яснити концепцію.
Лео91

50

Ось макет пам'яті вашого вказівника:

   +------------------+
x: | address of array |
   +------------------+
            |
            V
            +-----------+-----------+-----------+-----------+-----------+
            | pointer 0 | pointer 1 | pointer 2 | pointer 3 | pointer 4 |
            +-----------+-----------+-----------+-----------+-----------+
                  |
                  V
                  +--------------+
                  | some integer |
                  +--------------+

x[0]дає "адресу масиву",
x[0][0]дає "покажчик 0",
x[0][0][0]дає "деяке ціле число".

Я вважаю, зараз має бути очевидним, чому вони всі різні.


Вищезазначене досить близько для базового розуміння, саме тому я написав його так, як написав. Однак, як справедливо вказує хак, перший рядок не є на 100% точним. Отож, ось усі деталі:

З визначення мови С значенням x[0]є весь масив цілих чисел покажчиків. Однак масиви - це те, з чим насправді нічого не можна зробити. Ви завжди маніпулюєте або їх адресою, або їх елементами, ніколи не всім масивом у цілому:

  1. Ви можете перейти x[0]до sizeofоператора. Але це насправді не використання значення, його результат залежить лише від типу.

  2. Ви можете взяти його адресу, яка дає значення x, тобто "адресу масиву" з типом int*(*)[5]. Іншими словами:&x[0] <=> &*(x + 0) <=> (x + 0) <=> x

  3. У всіх інших контекстах значення x[0]vol перетворюється на вказівник на перший елемент масиву. Тобто вказівник зі значенням "адреса масиву" та тип int**. Ефект такий самий, як якщо б ви прикинули xвказівник типу int**.

Через розпад масиву-вказівника у випадку 3. всі використання в x[0]кінцевому рахунку призводять до вказівника, який вказує на початок масиву вказівника; виклик printf("%p", x[0])буде надрукувати вміст комірок пам'яті, позначених як "адреса масиву".


1
x[0]не є адресою масиву.
хакі

1
@haccks Так, слідуючи букві стандарту, x[0]це не адреса масиву, це сам масив. Я додав поглиблене пояснення цього і того, чому я написав, що x[0]це "адреса масиву". Сподіваюся що вам це подобається.
cmaster - відновити моніку

дивовижні графіки, які це прекрасно пояснюють!
МК

"Однак масиви - це те, з чим насправді нічого не можна зробити в C." -> Приклад лічильника: printf("%zu\n", sizeof x[0]);повідомляє розмір масиву, а не розмір вказівника.
chux

@ chux-ReinstateMonica І я продовжував говорити: "Ви завжди маніпулюєте або їхньою адресою, або їх елементами, ніколи не всім масивом у цілому", а за цим пунктом 1 перерахунку, де я розповідаю про ефект sizeof x[0]...
cmaster - відновити моніку

18
  • x[0]відмежування найбільш зовнішнього вказівника ( вказівник на масив розміром 5 вказівника на int) і призводить до масиву розміром 5 вказівника на int;
  • x[0][0]відміняє найвіддаленіший вказівник та індексує масив, в результаті чого вказує на int;
  • x[0][0][0] відміняє все, що призводить до конкретної цінності.

До речі, якщо ви коли-небудь відчуваєте плутанину щодо того, що означають подібні декларації, використовуйте cdecl .


11

Нехай розгляне крок за кроком виразами x[0], x[0][0]і x[0][0][0].

Як xвизначено наступним чином

int *(*x)[5];

тоді вираз x[0]- це масив типу int *[5]. Враховуйте, що вираз x[0]еквівалентний виразу *x. Тобто дереферентування вказівника на масив ми отримуємо сам масив. Нехай позначають це як y, тобто у нас є декларація

int * y[5];

Вираз x[0][0]еквівалентний y[0]і має тип int *. Нехай позначимо це як z, тобто у нас є декларація

int *z;

вираз x[0][0][0]еквівалентний виразу, y[0][0]який у свою чергу еквівалентний виразу z[0]і має тип int.

Так ми маємо

x[0] має тип int *[5]

x[0][0] має тип int *

x[0][0][0] має тип int

Тож вони є об'єктами різного типу і до речі різної величини.

Запустити, наприклад

std::cout << sizeof( x[0] ) << std::endl;
std::cout << sizeof( x[0][0] ) << std::endl;
std::cout << sizeof( x[0][0][0] ) << std::endl;

10

Перше, що я маю це сказати

x [0] = * (x + 0) = * x;

x [0] [0] = * (* (x + 0) + 0) = * * x;

x [0] [0] [0] = * (* (* (x + 0) + 0)) = * * * x;

Отже * x ≠ * * x ≠ * * * x

З наступної картини все зрозуміло.

  x[0][0][0]= 2000

  x[0][0]   = 1001

  x[0]      = 10

введіть тут опис зображення

Це лише приклад, де значення x [0] [0] [0] = 10

і адреса x [0] [0] [0] дорівнює 1001

ця адреса зберігається у x [0] [0] = 1001

та адреса x [0] [0] - 2000

і ця адреса зберігається у x [0] = 2000

Отже x [0] [0] [0] x [0] [0] x [0]

.

РЕДАКЦІЯ

Програма 1:

{
int ***x;
x=(int***)malloc(sizeof(int***));
*x=(int**)malloc(sizeof(int**));
**x=(int*)malloc(sizeof(int*));
***x=10;
printf("%d   %d   %d   %d\n",x,*x,**x,***x);
printf("%d   %d   %d   %d   %d",x[0][0][0],x[0][0],x[0],x,&x);
}

Вихідні дані

142041096 142041112 142041128 10
10 142041128 142041112 142041096 -1076392836

Програма 2:

{
int x[1][1][1]={10};
printf("%d   %d   %d   %d \n ",x[0][0][0],x[0][0],x[0],&x);
}

Вихідні дані

10   -1074058436   -1074058436   -1074058436 

3
Ваша відповідь вводить в оману. x[0]не містить мурашиної адреси. Його масив. Він занепаде, щоб вказувати на свій перший елемент.
хакі

Уммм ... що це означає? Ваша редакція - це як вишня на торті на вашу неправильну відповідь. Немає сенсу
хакі

@haccks Якщо він використовує лише вказівники, ця відповідь буде правильною. У розділі адреси відбудуться деякі зміни при використанні Array
apm

7

Якщо ви бачите масиви з реальної точки зору, це виглядає так:

x[0]- вантажний контейнер, наповнений ящиками.
x[0][0]являє собою єдиний ящик, повний ящиків для взуття, всередині вантажного контейнера.
x[0][0][0]являє собою єдину коробку для взуття всередині ящика, всередині вантажного контейнера.

Навіть якби це була єдина коробка для взуття в єдиній ящику в вантажному контейнері, це все-таки коробка взуття, а не вантажний контейнер


1
не x[0][0]було б жодного ящика, наповненого шматками паперу, на яких написані місця розташування вставки для взуття?
wchargin

4

В C ++ існує принцип, так що: оголошення змінної вказує саме спосіб використання змінної. Розглянемо свою декларацію:

int *(*x)[5];

що можна переписати як (для зрозумілішого):

int *((*x)[5]);

Завдяки принципу, ми маємо:

*((*x)[i]) is treated as an int value (i = 0..4)
 (*x)[i] is treated as an int* pointer (i = 0..4)
 *x is treated as an int** pointer
 x is treated as an int*** pointer

Тому:

x[0] is an int** pointer
 x[0][0] = (x[0]) [0] is an int* pointer
 x[0][0][0] = (x[0][0]) [0] is an int value

Так ви зможете з’ясувати різницю.


1
x[0]це масив з 5 ints, а не покажчик. (він може занепадати до вказівника у більшості контекстів, але тут важлива відмінність).
ММ

Гаразд, але ви повинні сказати: x [0] - це масив з 5 покажчиків int *
Nghia Bui

Щоб дати правильне виведення для @MattMcNabb: *(*x)[5]є an int, так (*x)[5]є an int *, так *xє an (int *)[5], так xє an *((int *)[5]). Тобто є xвказівником на 5-ти масив покажчиків на int.
wchargin

2

Ви намагаєтеся порівняти різні типи за значенням

Якщо ви візьмете адреси, ви можете отримати більше того, що очікуєте

Майте на увазі, що ваша декларація має значення

 int y [5][5][5];

дозволяло порівняння ви хочете, так як y, y[0], y[0][0], y[0][0][0]матиме різні значення і типів , але один і той же адресу

int **x[5];

не займає суміжного простору.

xі x [0]мають один і той же адресу, але x[0][0]і x[0][0][0]кожен за різними адресами


2
int *(*x)[5]відрізняється відint **x[5]
MM

2

Будучи pпокажчиком: ви укладаєте посилання p[0][0], що еквівалентно *((*(p+0))+0).

У позначенні C (&) та dereference (*):

p == &p[0] == &(&p[0])[0] == &(&(&p[0])[0])[0])

Еквівалентний:

p == &*(p+0) == &*(&*(p+0))+0 == &*(&*(&*(p+0))+0)+0

Подивіться, що & * можна відновити, просто видаливши його:

p == p+0 == p+0+0 == p+0+0+0 == (((((p+0)+0)+0)+0)+0)

Що ви намагаєтесь показати усім після свого першого речення? У вас просто багато варіацій p == p . &(&p[0])[0]відрізняється відp[0][0]
ММ

Хлопець запитав, чому 'x [0]! = X [0] [0]! = X [0] [0] [0]', коли x - покажчик, правда? Я намагався показати йому, що він може бути захоплений нотацією С відпуску (*), коли він укладає [0]. Отже, це спробувати показати йому правильну позначку, щоб х було рівним х [0], посилаючись на х [0] знову з &, і так далі.
Лучано

1

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

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

Це досить легко побудувати, але зрозуміло трохи складніше. У програмі нижче ми побачимо, як ми можемо змусити всі три значення бути однаковими.

ПРИМІТКА . Поведінка в цій програмі не визначено, але я публікую її тут як цікаву демонстрацію того, що вказівники можуть зробити, але не повинні .

#include <stdio.h>

int main () {
  int *(*x)[5];

  x = (int *(*)[5]) &x;

  printf("%p\n", x[0]);
  printf("%p\n", x[0][0]);
  printf("%p\n", x[0][0][0]);
}

Це компілюється без попереджень як у C89, так і у C99, а вихідний такий:

$ ./ptrs
0xbfd9198c
0xbfd9198c
0xbfd9198c

Цікаво, що всі три значення однакові. Але це не повинно бути сюрпризом! Спочатку давайте розбимо програму.

Ми оголосимо xяк вказівник на масив з 5 елементів, де кожен елемент має тип вказівника на int. Ця декларація виділяє 4 байти на стеку виконання (або більше залежно від вашої реалізації; на моїй машині покажчики - 4 байти), тому xце стосується фактичного місця в пам'яті. У сімействі мов C вміст - xце лише сміття, щось, що залишилося від попереднього використання локації, тому xсам по собі нікуди не вказує - звичайно, не на виділений простір.

Тож, природно, ми можемо взяти адресу змінної xі помістити її кудись, тож саме це і робимо. Але ми підемо вперед і вкладемо його в х себе. Оскільки &xмає інший тип, ніж xнам, нам потрібно зробити акторський склад, щоб ми не отримували попереджень.

Модель пам'яті виглядала б приблизно так:

0xbfd9198c
+------------+
| 0xbfd9198c |
+------------+

Отже, 4-байтовий блок пам'яті за адресою 0xbfd9198cмістить бітовий малюнок, відповідний шістнадцятковим значенням 0xbfd9198c. Досить просто.

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

Ми можемо бачити, що значення однакові, але лише в сенсі дуже низького рівня ... їх бітові структури однакові, але дані типу, пов'язані з кожним виразом, означають, що інтерпретовані значення різні. Наприклад, якщо ми роздрукували за x[0][0][0]допомогою рядка формату %d, ми отримали б величезне від’ємне число, тому «значення» на практиці різні, але бітовий малюнок той самий.

Це насправді дуже просто ... на діаграмах стрілки просто вказують на одну і ту ж адресу пам'яті, а не на різні. Однак, хоча нам вдалося витіснити очікуваний результат із невизначеної поведінки, це просто так - невизначено. Це не виробничий код, а просто демонстрація заради повноти.

У розумній ситуації ви будете використовувати mallocдля створення масиву 5 вказівних покажчиків і знову для створення ints, на які вказується цей масив. mallocзавжди повертає унікальну адресу (якщо у вас немає пам’яті; в такому випадку вона повертає NULL або 0), тому вам ніколи не доведеться турбуватися про подібні вказівники.

Сподіваємось, це повна відповідь, яку ви шукаєте. Ви не повинні сподіватися x[0], x[0][0]і x[0][0][0]бути рівними, але вони можуть бути, якщо їх змусити. Якщо щось перейшло тобі над головою, дай мені знати, щоб я міг уточнити!


Я б сказав, що ще одне дивне використання покажчиків я не бачив.
хакі

@haccks Так, це досить дивно, але коли ви розбиваєте його, це так само базово, як і інші приклади. Це просто випадок, коли бітові шаблони однакові.
Пураг

Ваш код викликає не визначену поведінку. x[0]насправді не є дійсним об’єктом правильного типу
MM

@MattMcNabb Це не визначено, і я дуже ясно про це насправді. Я не згоден щодо типу. xє вказівником на масив, тому ми можемо використовувати []оператор, щоб вказати зміщення від цього вказівника та відкинути його. Що там дивного? Результат x[0]- це масив, і C не скаржиться, якщо ви друкуєте це, використовуючи, %pоскільки так воно і реалізовано внизу.
Пураг

І компіляція цього -pedanticпрапора не дає жодних попереджень, тому C добре з типами ...
Purag

0

Тип int *(*x)[5]- int* (*)[5]це вказівник на масив з 5 вказівників на входи.

  • xце адреса першого масиву з 5 покажчиків на ints (адреса з типом int* (*)[5])
  • x[0]адреса першого масиву з 5 покажчиків на ints (однакова адреса з типом int* [5]) (зміщення адреси x на 0*sizeof(int* [5])тобто індекс * size-of-type-being-point-to and dereference)
  • x[0][0]є першим вказівником на int в масиві (однакова адреса з типом int*) (зміщення адреси x на 0*sizeof(int* [5])і перенаправлення, а потім на 0*sizeof(int*)і перенаправлення)
  • x[0][0][0]- це перший int, на який вказують вказівник на int (зсув адреси x на 0*sizeof(int* [5])і перенаправлення та зміщення цієї адреси на, 0*sizeof(int*)і перенаправлення, і зміщення цієї адреси за допомогою 0*sizeof(int)і перенаправлення)

Тип int *(*y)[5][5][5]- int* (*)[5][5][5]це вказівник на 3d-масив 5x5x5 покажчиків на ints

  • x це адреса першого 3d-масиву 5x5x5 покажчиків до ints з типом int*(*)[5][5][5]
  • x[0]- адреса першого 3d-масиву 5x5x5 покажчиків до ints (зміщення адреси x на 0*sizeof(int* [5][5][5])і перенаправлення)
  • x[0][0]- адреса першого 2d масиву 5x5 покажчиків на ints (зсув адреси x на 0*sizeof(int* [5][5][5])і відміни, а потім зміщення цієї адреси на 0*sizeof(int* [5][5]))
  • x[0][0][0]- це адреса першого масиву з 5 покажчиків на входи (зсув адреси x на 0*sizeof(int* [5][5][5])і перенаправлення та зміщення цієї адреси на 0*sizeof(int* [5][5])та зміщення цієї адреси на 0*sizeof(int* [5]))
  • x[0][0][0][0]є першим вказівником на int в масиві (зсув адреси x на 0*sizeof(int* [5][5][5])і перенавантаження та зсув цієї адреси на 0*sizeof(int* [5][5])та зсув цієї адреси на 0*sizeof(int* [5])та зміщення цієї адреси за допомогою 0*sizeof(int*)і перенаправлення)
  • x[0][0][0][0][0]- це перший int, на який вказують вказівник на int (зсув адреси x за допомогою 0*sizeof(int* [5][5][5])і перенаправлення та зміщення цієї адреси за допомогою 0*sizeof(int* [5][5])та зміщення цієї адреси за допомогою 0*sizeof(int* [5])та зсув цієї адреси за допомогою 0*sizeof(int*)і перенаправлення та зміщення цієї адреси за допомогою 0*sizeof(int)і перенаправлення)

Що стосується розпаду масиву:

void function (int* x[5][5][5]){
  printf("%p",&x[0][0][0][0]); //get the address of the first int pointed to by the 3d array
}

Це еквівалентно переходу int* x[][5][5]або int* (*x)[5][5]тобто вони все - розпад останнього. Ось чому ви не отримаєте попередження про компілятор для використання x[6][0][0]у цій функції, але вам доведеться, x[0][6][0]тому що інформація про розмір зберігається

void function (int* (*x)[5][5][5]){
  printf("%p",&x[0][0][0][0][0]); //get the address of the first int pointed to by the 3d array
}
  • x[0] - адреса першого 3d-масиву 5x5x5 покажчиків до ints
  • x[0][0] - адреса першого 2d масиву 5x5 покажчиків до ints
  • x[0][0][0] це адреса першого масиву з 5 покажчиків до ints
  • x[0][0][0][0] є першим вказівником на int в масиві
  • x[0][0][0][0][0] - це перший int, на який вказують вказівник на int

В останньому прикладі використовувати семантично набагато зрозуміліше *(*x)[0][0][0], ніж x[0][0][0][0][0]це, тому що перший і останній [0]тут інтерпретуються як затримка вказівника, а не індекс у багатовимірному масиві через тип. Однак вони однакові, оскільки (*x) == x[0]незалежно від семантики. Ви також можете використати *****x, що виглядатиме так, що ви перенаправляєте покажчик 5 разів, але це насправді трактується точно так само: зміщення, перенаправлення, перенавантаження, 2 зміщення в масив і перенапряження, виключно через тип ви застосовуєте операцію до.

За суті , коли ви [0]або до не тип масиву, це зміщення і разименованія з - за порядку старшинства .***(a + 0)

Коли ви [0]або до типу масиву , то це зміщення , то ідемпотентів разименованія (The разименовать дозволений компілятором , щоб отримати ту саму адресу - це операція ідемпотентна).**

Якщо ви [0]або *тип із типом масиву 1d, це зсув, а потім дереференція

Якщо ви [0]або **тип 2d масиву, то це лише зміщення, тобто зсув, а потім ідентифікація відхилення.

Якщо ви [0][0][0]чи ***тип 3d-масиву, то це зсув + ідемпотентна перенаправлення, то зміщення + ідемпотентна відмінність, а потім зміщення + ідемпотентна відмінність, а потім дереференція. Справжня дереференція виникає лише тоді, коли тип масиву повністю викреслений.

Для прикладу int* (*x)[1][2][3]типу розкручується по порядку.

  • x має тип int* (*)[1][2][3]
  • *xмає тип int* [1][2][3](зсув 0 + ідемпотентний відвід)
  • **xмає тип int* [2][3](зсув 0 + ідемпотентний відвід)
  • ***xмає тип int* [3](зсув 0 + ідемпотентний відвід)
  • ****xмає тип int*(зміщення 0 + відхилення)
  • *****xмає тип int(зміщення 0 + відхилення)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.