С++ для начинающих

       

Указатели


Указатели и динамическое выделение памяти были вкратце представлены в разделе 2.2. Указатель – это объект, содержащий адрес другого объекта и позволяющий косвенно манипулировать этим объектом. Обычно указатели используются для работы с динамически созданными объектами, для построения связанных структур данных, таких, как связанные списки и иерархические деревья, и для передачи в функции больших объектов – массивов и объектов классов – в качестве параметров.

Каждый указатель ассоциируется с некоторым типом данных, причем их внутреннее представление не зависит от внутреннего типа: и размер памяти, занимаемый объектом типа указатель, и диапазон значений у них одинаков[5]. Разница состоит в том, как компилятор воспринимает адресуемый объект. Указатели на разные типы могут иметь одно и то же значение, но область памяти, где размещаются соответствующие типы, может быть различной:

  • указатель на int, содержащий значение адреса 1000, направлен на область памяти 1000-1003 (в 32-битной системе);
  • указатель на double, содержащий значение адреса 1000, направлен на область памяти 1000-1007 (в 32-битной системе).
  • Вот несколько примеров:

    int             *ip1, *ip2;

    complex<double> *cp;

    string          *pstring;

    vector<int>     *pvec;

    double          *dp;

    Указатель обозначается звездочкой перед именем. В определении переменных списком звездочка должна стоять перед каждым указателем (см. выше: ip1 и ip2). В примере ниже lp – указатель на объект типа long, а lp2 – объект типа long:

    long *lp, lp2;

    В следующем случае fp интерпретируется как объект типа float, а fp2 – указатель на него:

    float fp, *fp2;



    Оператор разыменования (*) может отделяться пробелами от имени и даже непосредственно примыкать к ключевому слову типа. Поэтому приведенные определения синтаксически правильны и совершенно эквивалентны:

    string *ps;

    string* ps;

    Однако рекомендуется использовать первый вариант написания: второй способен ввести в заблуждение, если добавить к нему определение еще одной переменной через запятую:




    //внимание: ps2 не указатель на строку!
    string* ps, ps2;

    Можно предположить, что и ps, и ps2 являются указателями, хотя указатель – только первый из них.

    Если значение указателя равно 0, значит, он не содержит никакого адреса объекта.

    Пусть задана переменная типа int:

    int ival = 1024;

    Ниже приводятся примеры определения и использования указателей на int pi и pi2:

    //pi инициализирован нулевым адресом

    int *pi = 0;

    // pi2 инициализирован адресом ival

    int *pi2 = &ival;

    // правильно: pi и pi2 содержат адрес ival

    pi = pi2;

    // pi2 содержит нулевой адрес

    pi2 = 0;

    Указателю не может быть присвоена величина, не являющаяся адресом:

    // ошибка: pi не может принимать значение int

    pi = ival

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

    double dval;

    double *ps = &dval;

    то оба выражения присваивания, приведенные ниже, вызовут ошибку компиляции:

    // ошибки компиляции

    // недопустимое присваивание типов данных: int* <== double*

    pi = pd

    pi = &dval;

    Дело не в том, что переменная pi не может содержать адреса объекта dval – адреса объектов разных типов имеют одну и ту же длину. Такие операции смешения адресов запрещены сознательно, потому что интерпретация объектов компилятором зависит от типа указателя на них.

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

    // правильно: void* может содержать

    // адреса любого типа

    void *pv = pi;

    pv = pd;

    Тип объекта, на который указывает void*, неизвестен, и мы не можем манипулировать этим объектом. Все, что мы можем сделать с таким указателем, – присвоить его значение другому указателю или сравнить с какой-либо адресной величиной. (Более подробно мы расскажем об указателе типа void в разделе 4.14.)



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

    или косвенную адресацию, обозначаемую звездочкой (*). Имея следующие определения переменных:

    int ival = 1024;, ival2 = 2048;

    int *pi = &ival;

    мы можем читать и сохранять значение ival, применяя операцию разыменования к указателю pi:

    // косвенное присваивание переменной ival значения ival2

    *pi = ival2;

    // косвенное использование переменной ival как rvalue и lvalue

    *pi = abs(*pi); // ival = abs(ival);

    *pi = *pi + 1;  // ival = ival + 1;

    Когда мы применяем операцию взятия адреса (&) к объекту типа int, то получаем результат типа int*

    int *pi = &ival;

    Если ту же операцию применить к объекту типа int* (указатель на int), мы получим указатель на указатель на int, т.е. int**. int** – это адрес объекта, который содержит адрес объекта типа int. Разыменовывая ppi, мы получаем объект типа int*, содержащий адрес ival. Чтобы получить сам объект ival, операцию разыменования к ppi необходимо применить дважды.

    int **ppi = &pi;

    int *pi2 = *ppi;

    cout << "Значение ival\n"

         << "явное значение: " << ival << "\n"

         << "косвенная адресация: " << *pi << "\n"

         << "дважды косвенная адресация: " << **ppi << "\n"

         << endl;

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

    int i, j, k;

    int *pi = &i;

    // i = i + 2

    *pi = *pi + 2;

    // увеличение адреса, содержащегося в pi, на 2

    pi = pi + 2;

    К указателю можно прибавлять целое значение, можно также вычитать из него. Прибавление к указателю 1 увеличивает содержащееся в нем значение на размер области памяти, отводимой объекту соответствующего типа. Если тип char занимает 1 байт, int – 4 и double – 8, то прибавление 2 к указателям на char, int и double увеличит их значение соответственно на 2, 8 и 16. Как это можно интерпретировать? Если объекты одного типа расположены в памяти друг за другом, то увеличение указателя на 1 приведет к тому, что он будет указывать на следующий объект. Поэтому арифметические действия с указателями чаще всего применяются при обработке массивов; в любых других случаях они вряд ли оправданы.



    Вот как выглядит типичный пример использования адресной арифметики при переборе элементов массива с помощью итератора:

    int ia[10];

    int *iter = &ia[0];

    int *iter_end = &ia[10];

    while (iter != iter_end) {

      do_something_with_value (*iter);

      ++iter;

    }

    Упражнение 3.8

    Даны определения переменных:

    int ival = 1024, ival2 = 2048;

    int *pi1 = &ival, *pi2 = &ival2, **pi3 = 0;

    Что происходит при выполнении нижеследующих операций присваивания? Допущены ли в данных примерах ошибки?

    (a) ival = *pi3;   (e) pi1 = *pi3;

    (b) *pi2 = *pi3;   (f) ival = *pi1;

    (c) ival = pi2;    (g) pi1 = ival;

    (d) pi2 = *pi1;    (h) pi3 = &pi2;

    Упражнение 3.9

    Работа с указателями – один из важнейших аспектов С и С++, однако в ней легко допустить ошибку. Например, код

    pi = &ival;

    pi = pi + 1024;

    почти наверняка приведет к тому, что pi будет указывать на случайную область памяти. Что делает этот оператор присваивания и в каком случае он не приведет к ошибке?

    Упражнение 3.10

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

    int foobar(int *pi) {

      *pi = 1024;

      return *pi;

    }

    int main() {

      int *pi2 = 0;

      int ival = foobar(pi2);

      return 0;

    }

    В чем состоит ошибка? Как можно ее исправить?

    Упражнение 3.11

    Ошибки из предыдущих двух упражнений проявляются и приводят к фатальным последствиям из-за отсутствия в С++ проверки правильности значений указателей во время работы программы. Как вы думаете, почему такая проверка не была реализована? Можете ли вы предложить некоторые общие рекомендации для того, чтобы работа с указателями была более безопасной?


    Содержание раздела