Указатели в С++

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 5 мес., 2 нед. назад.

  • Автор
    Сообщения
  • #4892
    @admin

    Указатели языка Си

    Дональд Кнут считает указатели одним из главных изобретений в языке программирования Си. И это не просто так.

    Вам известно, что любая переменная — это именованная область памяти. Все пространство памяти проиндексировано. У каждого байта есть свой адрес.

    Рассмотрим пример:

    int x = 94;
    std::cout << &x; // --> 0x7ffe53575c6c

    Переменная x имеет значение 94. Но когда я вывел на консоль &x, то получил 0x7ffe53575c6c. У вас это значение, скорее всего, будет другим.

    Что это за значение ? 0x7ffe53575c6c — адрес переменной x, выведенный в 16-ной форме. Его фактическое значение и смысл для нас не интересен.

    Важно: у ЛЮБОЙ переменной есть адрес.

    Адрес переменной — значение (число). Но ведь мы умеем хранить значения? Используем для этого переменную особого типа — указатель:

    int x = 94;
    int* pX = &x; // Записываем в pX адрес переменной x
    std::cout << pX; // --> 0x7ffd09760ee4


    Важно:
    указатель — переменная, которая может хранить адрес другой переменной.

    Синтаксис int* pX означает, что pX может хранить адрес переменной типа int. Мы явно говорим, что указатель создается именно для int. Это необходимо, чтобы затем мы смогли извлечь значение, на которое указывает указатель:

    int x = 94;
    int* pX = &x;
    std::cout << *pX; // --> 94

    Если pX — адрес переменной x, то *pX — значение, которое расположено по этому адресу. Данная операция называется разыменованием указателя.

    Важно: x и *pX — абсолютно ЭКВИВАЛЕНТНЫ. Они представляют собой одно и то же значение, хранимое по одному и тому же адресу.

    Следствие: если мы меняем значение x, то меняется и значение *pX, и наоборот:

    int x = 94;
    int* pX = &x;
    std::cout << "x=" << x << "; *pX=" << *pX << std::endl; // --> x=94; *pX=94
    
    x = 18;
    std::cout << "x=" << x << "; *pX=" << *pX << std::endl; // --> x=18; *pX=18
    
    *pX = -33;
    std::cout << "x=" << x << "; *pX=" << *pX << std::endl; // --> x=-33; *pX=-33

    Двойные указатели в Си

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

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

    Дополнительное ограничение C-интерфейса заключается в том, что вы не можете создать экземпляр класса в собственном коде. Эта задача должна быть делегирована библиотеке.

    Посмотрим, как это может выглядеть:

    class MyClass; // В С-обертке мы бы создали здесь свой хэндл: typedef void HMyClass;
    
    int libFunction( int x, MyClass* result ) {
        int errorCode = 0;
        // ...
        return errorCode;
    }

    Кажется, что все неплохо. Но у нас нет определения класса MyClass (его быть и не должно). Вы не сможете создать его экземпляр. А функция libFunction() ожидает получить в качестве второго выходного параметра указатель на реальный объект:

    int main() {
        MyClass c; // Эта строка вызовет ошибку компиляции!
        int errorCode = libFunction( 94, &c );
    
        return 0;
    }

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

    MyClass x; // Переменная типа MyClass, которую мы создать, к сожалению, не можем
    MyClass* pX = &x; // Указатель на переменную типа MyClass
    MyClass** ppX = &pX; // Указатель на указатель на переменную типа MyClass
    
    *ppX;  // Если разыменуем ppX, то получим то же самое, что и просто pX
    **ppX; // А если разыменуем ppX дважды, то откатимся на исходную переменную x

    Продолжать цепочку указателей на указатели можно бесконечно. Но обычно хватает двух уровней.

    Класс указателя от определения типа не зависит. Он всего лишь хранит адрес, поэтому объявить MyClass* c мы имеем право. Воспользуемся этим:

    class MyClass;
    
    int libFunction( int x, MyClass** result ) {
        // Реализацию на самом деле мы не видим. Здесь она приводится для краткости
        int errorCode = 0;
        // ...
        return errorCode;
    }
    
    int main() {
        MyClass* c = nullptr;
        int errorCode = libFunction( 94, &c );
    
        return 0;
    }

    Теперь все лучше. Мы создаем указатель и инициализируем его значением nullptr (нулевой указатель в "никуда"). Передаем в libFunction() указатель на созданный указатель. В libFunction() уже выделяется память и создается нужный объект. Например, подобным образом:

    int libFunction( int x, MyClass** result ) {
        // ...
        *result = new MyClass;
        // ...
    }

    Но и очищать память должна будет сама библиотека. Для этого понадобится функция такого вида:

    void libFree( MyClass* object ) {
        // Реализацию мы вновь не можем видеть
        delete object;
    }
    
    int main() {
        MyClass* c = nullptr;
        int errorCode = libFunction( 94, &c );
        // ...
        libFree( c );
    
        return 0;
    }

    Отмечу, что рассмотренный подход имеет существенный недостаток: мы не можем получить доступ к функциям-членам созданного объекта! И правда. Ведь у нас отсутствует определение класса. Поэтому библиотека должна будет сама предоставлять дополнительные функции доступа, что не очень удобно. По сути это приводит к дублированию интерфейсов и лишним зависимостям. А так же явно нарушает принцип DRY. Но такова цена за хорошую переносимость.

    Дополнительные сведения и примеры

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

    С указателями используются следующие операции:

    • & — взятие адреса переменной;
    • * — разъименование указателя, т.е. получение доступа к объекту;
    • -> — разъименование члена структуры;
    • [] — индексация, доступ к элементу массива, при этом считается, что указатель содержит адрес 0 элемента.

    Операция индексации может быть реализована с помощью адресной арифметики, где применяются операции ++,—, и +, -.

    Можно создавать указатели на любые типы за исключением ссылок и битовых полей. Указатель может указывать не только на данные, но и на область кода, т.е. на функцию.

    //================================
    // Name        : ptrtest.cpp
    // Author      : your name
    // Version     :
    // Copyright   : Your copyright notice
    // Description : base using of pointers
    //=====================================
    #include <iostream>
    using namespace std;
    
    struct Mystruct
    {
    int a,b;
    int *ptr; // указатель как член структуры
    int metod(int c){return c+a;}
    int metod1(int c){return c+a;}
    
    static int smetod(int c){return c+10;}
    } mystruct,*sptr;
    
    int func(int a){return a+5;}
    
    int a=1,b=2,c=3,*iptr=&a;
    int array[10]={100,101,102,103,104,105,106,107,108,109};
    int * iptrarray[10];  // массив указателей типа int 
    int (*iarrayptr)[10]; // указатель на массив из 10 элементов типа int
    
    
    int main() {
    // пример применения указателей	
    cout<<"a= "<<a<<", "<<"b="<<b<<", c="<<c<< endl;
    cout<<"*iptr = "<<*iptr<<endl;
    cout<<"iptr[0] = "<<iptr[0]<<endl;
    cout<<"iptr[1] = "<<iptr[1]<<endl;
    cout<<"*(iptr + 1) = "<<*(iptr+1)<<endl;
    cout<<"1[iptr] = " <<1[iptr]<<endl;  // тоже возможный вариант
    *iptr= 45; // меняем значение переменной a
    cout<<"a = "<<a<<endl;
    cout<<"----------------------------------------"<<endl;
    
    // адресная арифметика 
    cout<<"iptr= "<<iptr<<endl;       // вывод адреса в памяти переменной a
    cout<<"iptr+1= "<<iptr+1<<endl;   // вывод адреса в памяти переменной b
    // разница между адресами переменных a и b в элементах
    cout<<"delements=(iptr+1)-iptr = " << (iptr+1)-iptr <<endl;
    // разница между адресами переменных a и b в байтах
    cout<<"dbytes=(long)(iptr+1) - (long)iptr = " << (long)(iptr+1) - (long)iptr <<endl;
    cout<<"dbytes=delements * sizeof(int) = "<< ((iptr+1)-iptr) * sizeof(int)<<endl;
    cout<<"----------------------------------------"<<endl;
    
    // указатель и массивы
    // имя массива является указателем на первый элемент
    iptr=array; 
    for(unsigned int i=0;i<10;i++)	{
    	cout<<"array["<<i<<"]="<<array[i]<<", *iptr="<<*iptr<<
    	", iptr[0]="<<iptr[0]<<endl;
    	iptr++; // т.е. iptr=iptr+1;
    	}
    cout<<"----------------------------------------"<<endl;
    
    // указатели и динамически выделяемая память
    iptr=new int(100); // выделяем в памяти объект типа int со значением 100
    cout<<"*iptr="<<*iptr<<endl;
    delete iptr; // освобождаем выделенную память
    cout<<"----------------------------------------"<<endl;
    
    // пустой указатель
    // обычно применяется когда заранее тип объекта неизвестен
    void* vptr=new int(100);
    cout<<"* ((int*)vptr)="<<* ((int*)vptr)<<endl;
    delete (int*)vptr;
    vptr=new double(3.14);
    cout<<"* ((double*)vptr)="<<* ((double*)vptr)<<endl;
    cout<<"----------------------------------------"<<endl;
    
    // указатели и функции
    // имя функции является и ее адресом
    typedef int (*FPTR)(int); 
    int (*fptr)(int)=func; // или FPTR fptr=func;
    cout<<"result of func(10): "<<func(10)<<endl;
    cout<<"result of fptr(10): "<<fptr(10)<<endl;
    // статические методы отличаются от обычных функций только
    // областью видимости
    fptr=Mystruct::smetod; // можно и так fptr=mystruct.smetod;
    cout<<"result of Mystruct::smetod(30): "<<Mystruct::smetod(30)<<endl;
    cout<<"result of fptr(30): "<<fptr(30)<<endl;
    // конструкция fptr=mystruct.metod; будет ошибочной
    // так как простым методам С++ не явно добавляет еще один аргумент
    // указатель на сам объект mystruct 
    cout<<"----------------------------------------"<<endl;
    
    // указатели и структуры
    mystruct.a=10;
    mystruct.b=20;
    iptr=&mystruct.b;
    cout<<"mystruct.b="<<mystruct.b<<", *iptr="<<*iptr<<endl;
    sptr=&mystruct;
    cout<<"(*sptr).a="<<(*sptr).a<<endl;
    // вместо (*sptr).a удобней пользоваться операцией ->
    cout<<"sptr->a="<<sptr->a<<", sptr->b="<<sptr->b<<endl;
    sptr->ptr=iptr;
    cout<<"*sptr->ptr="<<*sptr->ptr<<endl;
    cout<<"----------------------------------------"<<endl;
    
    
    // указатели на члены структуры, на практике редко встречается
    int Mystruct::*isptr;         // указатель на член данных структуры 
    int (Mystruct::*fsptr) (int); // указатель на метод структуры
    // при инициализации указателей на членов структуры
    // используется имя структуры
    isptr=&Mystruct::b;
    fsptr= &Mystruct::metod;
    cout<<"mystruct.*isptr="<<mystruct.*isptr<<endl;
    cout<<"(mystruct.*fsptr)(40)="<<(mystruct.*fsptr)(40)<<endl; 
    fsptr=&Mystruct::metod1;
    cout<<"now (mystruct.*fsptr)(40)="<<(mystruct.*fsptr)(40)<<endl;
    cout<<"----------------------------------------"<<endl;
    return 0;
    }

    Я не являюсь автором данного урока. Все, что тут описано справедливо для так называемых «сырых указателей» и работает в языках Си и С++. Для более углубленного изучения рекомендую посмотреть статью: «Указатели и ссылки в С++«. В языке С++ используются также «умные указатели«.

Для ответа в этой теме необходимо авторизоваться.