Пришла мне в голову интересная задачка: реализовать свой массив на c++.

Массивы — это одна из базовых структур, трудно себе представить сколько-нибудь сложную программу без них. Но что если попробовать реализовать массив самому? В голову сразу приходит список: будем в каждом элементе массива хранить указатель на следующий и хранить эти элементы в динамически выделяемой памяти на куче.

Слишком просто. Давайте обойдемся без кучи. Никаких malloc и new. Можно ли тогда сделать массив?

На одних глобальных переменных мы далеко не уедем. Для того чтобы реализовать идею со списком, нам все-таки нужно выделять память динамически, и если знать, как работают внутренности C на низком уровне, то решение становится понятным.

Где происходит динамическое выделение памяти кроме кучи? На стеке вызовов. Каждая вызванная функция хранит там свои локальные переменные. Вызовем функцию рекурсивно и много выделенного место на стеке. Правда, оно будет размазано по разным вызовам, но можно связать все вместе указателями, которым плевать на государственные границы структуру стека. Заметим, что все действия с массивом (списком) нужно произвести внутри рекурсивного вызова, пока стек не свернулся.

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

Gist

#include <iostream>

struct Array {
    int val;
    Array* previous;

    Array(int val, Array* previous) : val(val), previous(previous) {}

    int& operator[](size_t index) {
        if (index == 0) {
            return val;
        }
        return (*previous)[index - 1];
    }
};

void generator(size_t n, void(*payload)(Array), Array* previous = 0) {
    Array element(0, previous);
    if (n == 1) {
        payload(element);
    } else {
        generator(n - 1, payload, &element);
    }
}

size_t N;

void test(Array array) {
    for (size_t i = 0; i < N; ++i) {
        std::cin >> array[i];
    }
    for(size_t i = 0 ; i < N - 1; ++i) { 
        for(size_t j = 0 ; j < N - i - 1 ; ++j) {  
            if(array[j] > array[j+1]) {           
                int tmp = array[j];
                array[j] = array[j+1];
                array[j+1] = tmp; 
            }
        }
    }
    for (size_t i = 0; i < N; ++i) {
        std::cout << array[i] << ' ';
    }
}

int main() {
    std::cin >> N;
    generator(N, test);
}

Комментарии (27)


  1. Chuvi
    17.09.2021 19:19
    +4

    Что мы получили кроме дикого оверхеда?


  1. dmandreev
    17.09.2021 19:48
    +3

    Что это за бред вообще? Проба GPT-3? Надмозг?


    1. DistortNeo
      17.09.2021 20:47

      Меня больше интересует другое: аккаунт создан 3 октября 2012 г., последняя активность 04.06.2021, ни одной публикации или комментария. И тут опа: и публикация. Такое вообще бывает?


      1. Chuvi
        17.09.2021 23:42

        Вопрос в том, как долго статья была в песочнице.

        Наткнулся кто-то на эту статью и решил пригласить.


    1. alexprey
      17.09.2021 21:42

      Учитывая что подпись автора говорит, что он разработчик DL и NLP, то вполне вероятно так и есть)


  1. lorc
    17.09.2021 20:06
    +2

    если знать, как работают внутренности C на низком уровне

    ... то можно воспользоваться mmap(MAP_ANONYMOUS) или там VirtualAlloc(). malloc же берет память где-то?


    1. Alex_ME
      17.09.2021 20:53
      +1

      Еще есть sbrk


      1. lorc
        17.09.2021 21:27

        Есть, но им лучше не пользоваться.


      1. oleshii
        18.09.2021 11:02

        Если память не изменяет, сам sbrk ничего не выделяет, а сдвигает границу адресного пространства кучи, доступной процессу. Причём, в теории, может сдвинуть её как вверх, так и вниз. Отсюда и расшифровка первого символа акронима sbrk, [s] - shift.


        1. godzie
          18.09.2021 11:30
          +1

          сам sbrk ничего не выделяет, а сдвигает границу адресного пространства кучи, доступной процессусам sbrk ничего не выделяет, а сдвигает границу адресного пространства кучи, доступной процессу

          ... что впринципе и является выделением памяти, ведь мы получаем больше виртуальных адресов для использования.

          В линуксе вниз граница не двигается, в этом смысле предпочтительнее ммап- выделенную им память можно отдать обратно ос.


          1. oleshii
            18.09.2021 11:38

            впринципе и является выделением памяти

            Скорее резервирование памяти под кучу. Выделение сложный процесс аллокациии из кучи под текущие нужды.


            1. DistortNeo
              18.09.2021 12:05

              Можно рассмотреть вырожденный случай кучи, когда память только выделяется, но не освобождается.


              1. oleshii
                18.09.2021 12:13

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

                В чём практический смысл такой кучи ? В исчерпывании памяти и вызове process killer ?


                1. fougasse
                  18.09.2021 13:22

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


                  1. oleshii
                    18.09.2021 18:19

                    Это разработка своего аллокатора, которая в строне от темы беседы


                1. DistortNeo
                  18.09.2021 14:11

                  Так речь же о том, что без malloc обойтись. А остальные детали в данном случае не интересны.


                  Ну то есть вместо выделения куска памяти в куче с помощью malloc (или его собственной реализации) просто инкрементируем указатель на память.


                  1. oleshii
                    18.09.2021 18:26

                    инкрементируем указатель на память

                    Простого инкремента не буде стандартным ли alloc пользоваться, или своими реализациями. В любом итоге это: списки кусков по размеру, деревья аллоцированных кусков, мьютексы.


    1. thatsme
      18.09.2021 09:19

      Ну аффтар же сказал "без кучи". Без кучи единственный вариант рекурсия. Так себе троллинг впрочем ...


      1. godzie
        18.09.2021 15:13
        +3

        alloca есть как минимум


      1. lorc
        18.09.2021 16:47

        Выделение памяти через mmap() не имеет прямого отношения к куче. То есть, конечно, можно в выделенной памяти потом завести heap allocator, но ведь можно и не заводить.


  1. fougasse
    17.09.2021 20:26
    +8

    Так С или плюсы?

    Ничего, что под массивом в мире С/С++ понимается непрерывный участок памяти, а список - это другое? И одно второе не заменяет.


  1. anonymous
    00.00.0000 00:00


  1. sidorovmax
    17.09.2021 22:05
    +1

    Чтобы идея стала законченной, можно все тоже сделать, но без циклов?


  1. mynameco
    17.09.2021 23:01
    +2

    Не хватает коментария от автора: извините, меня взломали.


  1. gwg605
    18.09.2021 00:15

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

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


  1. agmt
    18.09.2021 13:27
    +2

    1. Прочитайте, что такое массив. // Тут не выполняется даже доступа по индексу за O(1).
    2. Прочитайте, что происходит со стеком после завершения функции. // Скорее всего, данные там останутся, но вызов функции перетрёт значения.


  1. oleg-m1973
    18.09.2021 14:45
    +1

    В целом пост, конечно, безграмотный, как и было сказано в предыдущих комментариях. Но есть в нём одна разумная мысль:

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

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