воскресенье, 31 января 2010 г.

Ссылки и указатели C++ как входные параметры

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

void do_some_calc(int arg)
{
    int count = arg;
    int prev = 1, prev_prev =0;
    for(int i = 0; i < count; i++)
    {
        if(i < 1)
        {
            prev_prev = prev;
            prev = arg;
        }
        arg = prev + prev_prev;
    }
}

Эта программка должна вычислять arg-е число Фибоначчи и записывать его в тот же arg (да, способ не самый адекватный, проехали это).

Допустим, мы создали переменную int number = 3 и отправили её в процедуру do_some_calc(number). Но по выходу из процедуры переменная number не сможет подтвердить, что мы действительно что-то вычисляли, потому что значение её не изменится.

А всё потому, что передавалась не сама переменная, а её копия. И вычисленное значение, сохранённое в копии переменной number с именем arg, по выходу из процедуры просто удалилось вместе со всем, что использовалось в её теле.

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

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

void allocate_memory_for_ptr(int* ptr)
{
    ptr = new int[1000];
}


Да, передаётся указатель int* tricky_pointer. Да, выделяется память для него allocate_memory_for_ptr(tricky_pointer). Но по выходу из процедуры доступ к tricky_pointer[0]..tricky_pointer[whatever] получить не удастся. Более того, в процедуре только что нашими руками была создана утечка памяти.
Как ни странно, а в allocate_memory_for_ptr(...) была передана копия указателя tricky_pointer, которой повезло так же, как и переменной arg из прошлого примера. Можно заключить, что всё (абсолютно всё, включая указатели) передаваемое параметром в функцию, просто копируется. И этого эффекта будет незаметно, если в процедуре меняется только содержимое указателя или не меняется вообще ничего, как в примере с суммой элементов массива (для этого, в общем-то, придумана константность, но о ней как-нибудь попозже). Когда же меняется сам адрес, лучше поступить вот так:

void allocate_memory_for_ptr(int*& ptr)
{
    ptr = new int[1000];
}

А если говорить непосредственно о ссылках, то вот самая типичная процедура:

void change_ref(int& ref)
{
    int p = 5;
    ref = p;
}

При таком её использовании:

int i = 0;
int& r = i;
change_ref(r);

с самой ссылкой ничего не станет, изменится лишь значение i. Ссылки предоставляют меньше свободы в работе с адресом, и вся процедура change_ref(...) по сути может быть  расписана как получение указателя на i в качестве параметра (по ссылке, по значению - без разницы, мы ведь меняем лишь значение) и изменения содержимого указателя. А вот поиграться непосредственно с адресом ссылки, как мы это проделывали в примере allocate_memory_for_ptr(...) не получится из-за ограничений на операции со ссылками.

Следовательно, ссылки, в общем-то, удобнее в работе, нежели указатели, но все различия между ними не заканчиваются на операции разыменовывания. И, как стало понятно, параметр, передаваемый в функцию, не может быть в неё "засунут" извне - он обязательно копируется. Другое дело уже в том, что иногда эта копия бывает нежелательна.

четверг, 14 января 2010 г.

Ссылки и указатели C++. Арифметика

Вот так вот пишет программист программы уже который отчётный период, а какая-нибудь загвоздка о двух-трёх строках полностью отправляет его в долгий дрифт. Одна из таких загвоздок в C++ - ссылки и указатели. Кто-то использует только указатели (скорее всего, люди старой закалки, расцвет мастерства которых пришёлся на C без плюсов) и прекрасно с ними живёт, кто-то использует ссылки и предпочитает амперсанд "звёздочке", а кто-то не использует ни то, ни другое (что ж, в модных фреймворках и с этим можно жить довольно неплохо).

Для начала надо разобраться, что такое указатель.
Указатель - это тип данных, предназначенный для "указывания" на некую сущность (переменную, объект класса, функцию) в памяти. По сути, всё, за что отвечает указатель, - это хранение адреса и доступ к значению по этому адресу. Обычно размер указателя составляет 4 байта.
Указатель также поддерживает арифметические операции (сложение, вычитание, присваивание значения). Чтобы понять основную суть указателей, есть идея рассмотреть несложную программу, реализующую подсчёт суммы элементов массива. Реализует это она самым "указательным" образом.

int sum(int* inLeft, int* inRight)
{
    if(inLeft == inRight)
        return *inLeft;
    else
        return *inRight + sum(inLeft, --inRight);
}

Для девятиэлементного массива mas вызов функции будет иметь такой вид:
int massum = sum(&mas[0], &mas[8]);


А теперь с конца: если у имеется некая переменная, получить её адрес можно применением к ней оператора взятия адреса &.
Именно результатом взятия адреса и может быть инициализирован указатель (инициализировать его значением переменной не получится, потому что это означало бы, что мы хотим получить доступ к произвольной области памяти, что есть unsafe. Формально компилятором это выражается в невозможности привести тип A к типу "указатель на А"). А дальше всё довольно прямолинейно: если есть желание работать с адресом, предоставляемым указателем, можно использовать его (указатель) безо всяких дополнительных символов (причём, разумеется, применение оператора взятия адреса к указателю будет расцениваться как адрес его самого и интерпретироваться компилятором как "указатель на указатель на А"); если же необходимо получить доступ к значению, на которое он всеми силами указывает, нужно применить оператор доступа по значению ("звёздочка"; если говорить правильно, разыменовывание). При этом к "звёздочке" можно применять арифметические операторы и совершать изменение значения самой переменной. Для  оператора взятия адреса такой возможности нет. Сущность, которая подвергается взятию адреса, должна быть rvalue, то есть стоять справа от знака "равно" (такой способ, как
int** new_pointer = ++&old_pointer дело тоже не спасает).

Что ж, в теме поста с легкой руки были затронуты и ссылки. Поэтому теперь можно придумать что-нибудь и про них.
Ссылка - тип данных, который появился уже в языке C++ и который выполняет те же функции, что и указатель, но в этаком "локальном" контексте.
Ссылка также содержит адрес объекта и занимает те же самые 4 байта (только убедиться в этом чуть сложнее:) ). только разыменовывание у неё выведено на передний план, и взятие значения теперь осуществляется без дополнительных звёздочек и амперсандов, а адрес получается только соответствующим оператором &.
Ещё одна немаловажная особенность ссылки в том, что она не может быть неициализирована. Если оставить в коде строку int* i; , то компилятор максимум, что из себя выжмет, так это предупреждение. Если же подобным образом поступить со ссылкой: int& i;, то тут у него появится шанс схватить программиста за рукав. Такая особенность языка C++: ссылки и соответствующие им переменные - это одно целое.

Теперь настала пора переписать уже набивший оскомину код вычисления суммы массива, только уже в более модном стиле, на ссылках:

int sum(int& inLeft, int& inRight)
{
    if(&inLeft == &inRight)
        return inLeft;
    else
    {
        int* right_temp = &inRight;
        right_temp--;
        int& new_right = *right_temp;
        return inRight + sum(inLeft, new_right);
    }
}
Сразу стало заметно, насколько больше "мусора" в этой реализации. А всё потому, что ссылки не поддерживают арифметические операции. Ссылка прикрепляется к переменной "намертво", без возможности "перепрыгивания" на соседний или совсем отдалённый объект. И именно поэтому для процедуры обновления адресов пришлось прибегать к указателям.

Однако, наверное, основное назначение ссылки - передача параметров в функцию. В C++ дело вот в чём: существует 2 типа передачи, по значению и по адресу. В передаче параметра по значению (выглядит примерно так: int func(int par)) сама функция получает не что иное, как копии передаваемых параметров. В большинстве случаев именно это и требуется, однако для объектов, хранящих своё состояние (тут прямо почувствовалось, как разговор перешёл в тему объектно-ориентированного программирования), типа матриц, копия должна быть обязательно сконструирована, а это бывает (абсолютно всегда) затруднительно. При передаче же по адресу (выглядит так: int func(int& par) либо так: int func(int* par)) передаётся не сама сущность, а её адрес. И ссылка, безусловно, облегчает работу с такими параметрами: в отличие от указателя, разыменовывается она автоматически.

Подводя итог, можно скопировать всё написанное в окошко редактирования ещё раз (ведь так он всегда подводится) и описать всё то, что очень сподручно знать про саму суть указателей и ссылкок:

указатель /* */ ссылка
Объявление
int* pointer /* */ int& ref
без инициализации /* */ только с инициализацией
Взятие адреса
pointer /* */ &ref

Разыменовывание
*pointer /* */ ref

Арифметические операции с адресом
(--pointer++) += 3; // сложение и вычитание /* */ не предусмотрено

Инициализация
pointer1 = pointer2; /* */ ref1 = ref2;
pointer = &ref; /* */ ref = *pointer;
pointer = &val;/* */ ref1 = val;

P.S. Как всегда, нашлись ещё ценные источники. К примеру, тонкости оператора & как взятия адреса и части объявления ссылки описаны здесь: http://novikovmaxim.livejournal.com/120178.html