IT Notes

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

Вернуть результат из функции можно с помощью return. Такой подход мы рассматривали, когда говорили об области видимости переменных в C++. Но предположим, что мы решили идти другим путем. Построим функцию таким образом, что основной результат возвращается через аргумент. При этом 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. Но такова цена за хорошую переносимость.

Похожие публикации

Комментарии

побольше бы таких сайтов. Все подробно и доступно описано

Интересно!