Указатели на объекты
В языке С можно получить доступ к структуре непосредственно или с использованием указателей на эту структуру. Аналогичным образом в С++ можно ссылаться на объект непосредственно, как это имело место во всех предыдущих примерах, или используя указатель на этот объект. Указатели на объекты являются одним из важнейших понятий С++.
Для доступа к членам объекта через сам объект используется оператор «точка» (.). Если же используется указатель на объект, тогда необходимо использовать оператор «стрелка» (—>). Использование операторов «точка» и «стрелка» аналогично их использованию для структур и объединений.
Указатель на объект объявляется с использованием того же синтаксиса, что и указатели на данные других типов. В следующей программе создается простой класс с именем P_example и определяется объект этого класса ob, а также указатель р на объект P_example. Ниже проиллюстрировано, как получить доступ к объекту ob непосредственно и опосредованно с использованием указателя:
// простой пример использования указателя на объект
#include
class P_example int num;
public:
void set_num(int val)
void show_num();
>;
void P_example::show_num()
cout >
int main()
P_example ob, *p; // объявление объекта и указателя на него
ob.set_num(1); // прямой доступ к ob
ob.show_num();
р = &ob; // присвоение р адреса ob
p->show_num(); // доступ к ob с помощью указателя
return 0;
>
Обратим внимание, что адрес объекта ob получен с использованием оператора взятия адреса & точно так же, как берется адрес переменной любого типа.
Инкремент или декремент указателя изменяет его таким образом, что он всегда указывает на следующий элемент базового типа. То же самое справедливо и для объектов. Следующий пример модифицирует предыдущую программу, в результате чего ob становится массивом из двух элементов типа P_example. Обратим внимание на инкремент и декремент указателя р, с помощью которого осуществляется доступ к элементам массива:
// увеличение указателя на объект
#include
class P_example int num;
public:
void set_num(int val)
void show_num();
>;
void P_example::show_num()
cout >
int main()
P_example ob[2], *p;
ob[0].set_num(10); // прямой доступ к объекту
ob[1].set_num(20);
p = &ob[0]; // получение указателя на первый элемент
p->show_num(); // вывод значения ob[0] с помощью указателя
р++; // переход к следующему объекту
p->show_num(); // вывод значения ob[1] с помощью указателя
р—; // переход к предыдущему объекту
p->show_num(); // вывод значения оb [0]
return 0;
>
Программа выводит на экран числа 10, 20, 10.
Как сделать указатель на объект класса с
Рассмотрим простой пример.
#include class A < >; class AB: public A < >; class AC: public A < >; void main ()
Это очень простой пример. Пустые классы, простое наследование… Единственно, что важно в объявлении этих классов — спецификаторы доступа в описании баз производных классов. Базовый класс (его будущие члены) должен быть абсолютно доступен в производном классе. Первый оператор функции main() — объявление указателя на объект класса A. Затем следует определение объекта-представителя класса A, следом — настройка указателя на этот объект. Естественно, при этом используется операция взятия адреса. Всё это давно известно и очень просто. Следующие две строки являются определениями пары объектов, которые являются представителями двух разных производных классов…
За объявлениями объектов в программе располагаются строки, которые позволяют настроить указатель на базовый класс на объект производного класса. Для настройки указателя на объект производного класса нам не потребовалось никаких дополнительных преобразований. Здесь важно только одно обстоятельство. Между классами должно существовать отношение наследования. Таким образом, проявляется важное свойство объектно-ориентированного программирования: УКАЗАТЕЛЬ НА БАЗОВЫЙ КЛАСС МОЖЕТ ССЫЛАТЬСЯ НА ОБЪЕКТЫ — ПРОИЗВОДНЫХ КЛАССОВ. Подобное, на первый взгляд, странное обстоятельство имеет своё объяснение.
Рассмотрим схемы объектов MyA, MyAB, MyAC:
MyA::= A MyAB::= A AB MyAC::= A AC
Все три объекта имеют общий элемент (объекты производных классов — фрагмент) — представитель базового класса A. Исключительно благодаря этому общему элементу указатель на объект класса A можно настроить на объекты производных классов. Указателю просто присваивается адрес базового фрагмента объекта производного типа. В этом и состоит секрет подобной настройки. Как мы увидим, для указателя pObj, настроенного на объект производного класса, вообще не существует фрагмента объекта, представленного производным классом.
pObj A AC
Ниже пунктирной линии — пустота. Для того чтобы убедиться в этом, мы усложним структуру класса A, определив в нём функцию Fun1. Конечно же, эта функция ничего не будет делать. Но у неё будет спецификация возвращаемого значения и непустой список параметров. Нам от неё большего и не требуется. Лишь бы сообщала о собственном вызове…
class A < public: int Fun1(int); >; int A::Fun1(int key)
Аналогичной модификации подвергнем производные классы AB и AC (здесь предполагаются вызовы функций-членов непосредственно из функции main(), а потому надо помнить о спецификаторе public), а затем продолжим опыты.
class AB: public A < public: int Fun1(int key); >; int AB::Fun1(int key) < cout class AC: public A < public: int Fun1(int key); int Fun2(int key);// В этом классе мы объявим вторую функцию. >; int AC::Fun1(int key) < cout int AC::Fun2(int key)
Теперь мы займёмся функцией main(). Первая пара операторов последовательно из объекта запускает функцию-член производного класса, а затем — подобную функцию базового класса. С этой целью используется квалифицированное имя функции-члена.
MyAC.Fun2(2); //Вызвана AC::Fun2()… MyAC.Fun1(2); //Вызвана AC::Fun1()… MyAC.A::Fun1(2); //Вызвана A::Fun1()…
Следующие строки посвящены попытке вызова функций-членов по указателю на объект базового типа. Предполагается, что в данный момент он настроен на объект MyAC.
pObj->Fun1(2); //Вызвана A::Fun1()…
И это всё, что можно способен указатель на объект базового типа, если его настроить на объект производного типа. Ничего нового. Тип указателя на объект — базовый класс. В базовом классе существует единственная функция-член, она известна транслятору, а про структуру производного класса в базовом классе никто ничего не знает. Так что следующие операторы представляют пример того, что не следует делать с указателем на объекты базового класса, даже настроенного на объект производного класса.
//pObj->Fun2(2); //pObj->AC::Fun1(2);
То ли дело указатель на объект производного типа! И опять здесь нет ничего нового и неожиданного. С «нижнего этажа бункера» видны все «этажи»!
AC* pObjAC = &MyAC; pObjAC->Fun1(2); pObjAC->Fun2(2); pObjAC->AC::Fun1(2); pObjAC->Fun1(2); pObjAC->A::Fun1(2);
И, разумеется, указатель на объект производного класса не настраивается на объект базового.
//pObjAC = &MyA;
Основной итог этого раздела заключается в следующем: указатель на объект базового класса можно настроить на объект производного типа. Через этот указатель можно «увидеть» лишь фрагмент объекта производного класса — его «базовую» часть — то, что объект получает в наследство от своих предков. Решение о том, какая функция должна быть вызвана, принимается транслятором. В момент выполнения программы всё уже давно решено. Какая функция будет вызвана из объекта производного типа — зависит от типа указателя, настроенного на данный объект. В этом случае мы наблюдаем классический пример статического связывания.
Как сделать указатель на объект класса с
Кроме указателей на простые типы можно использовать указатели на структуры. А для доступа к полям структуры, на которую указывает указатель, используется операция ->:
unsafe < Point point = new Point(0, 0); Console.WriteLine(point); // X: 0 Y: 0 Point* p = &point; p->X = 30; Console.WriteLine(p->X); // 30 // разыменовывание указателя (*p).Y = 180; Console.WriteLine((*p).Y); // 180 Console.WriteLine(point); // X: 30 Y: 180 > struct Point < public int X < get; set; >public int Y < get; set; >public Point(int x, int y) < X = x; Y = y; >public override string ToString() => $"X: Y: "; >
Обращаясь к указателю p->X = 30; мы можем получить или установить значение свойства структуры, на которую указывает указатель. Обратите внимание, что просто написать p.X=30 мы не можем, так как p — это не структура Point, а указатель на структуру.
Альтернативой служит операция разыменования: (*p).X = 30;
Стоит отметить, что указатель может указывать только на те структуры, которые не имеют полей ссылочных типов (в том числе полей, которые генерируются компилятором автоматически для автосвойств).
Указатели на массивы и stackalloc
С помощью ключевого слова stackalloc можно выделить память под массив в стеке. Смысл выделения памяти в стеке в повышении быстродействия кода. Посмотрим на примере вычисления квадратов чисел:
unsafe < const int size = 7; int* square = stackalloc int[size]; // выделяем память в стеке под семь объектов int int* p = square; // вычисляем квадраты чисел от 1 до 7 включая for (int i = 1; i for (int i = 0; i < size; i++) < Console.WriteLine(square[i]); >>
Оператор stackalloc принимает после себя массив, на который будет указывать указатель. int* square = stackalloc int[size]; .
Для манипуляций с массивом создаем указатель p: int* p = square; , который указывает на первый элемент массива, в котором всего 7 элементов. То есть с помощью указателя p мы сможем перемещаться по массиву square.
Далее в цикле происходит подсчет квадратов чисел от 1 до 7. В цикле для установки значения (квадрата числа — i * i) по адресу, который хранит указатель, выполняется выражение:
*p = i * i;
Затем происходит инкремент указателя p++ , и указатель p смещается вперед на следующий элемент в массиве square.
Чуть более сложный пример — вычисление факториала:
unsafe < const int size = 7; int* factorial = stackalloc int[size]; // выделяем память в стеке под семь объектов int int* p = factorial; *(p++) = 1; // присваиваем первой ячейке значение 1 и // увеличиваем указатель на 1 for (int i = 2; i for (int i = 0; i < size; i++) < Console.WriteLine(factorial[i]); >>
Также с помощью оператора stackalloc выделяется память для 7 элементов массива. И также для манипуляций с массивом создаем указатель p: int* p = factorial; , который указывает на первый элемент массива, в котором всего 7 элементов
Далее начинаются уже сами операции с указателем и подсчет факториала. Так как факториал 1 равен 1, то присваиваем первому элементу, на который указывает указатель p, единицу с помощью операции разыменования: *(p++)= 1;
Для установки некоторого значения по адресу указателя применяется выражение: *p=1 . Но кроме этого тут происходит также инкремент указателя p++ . То есть сначала первому элементу массива присваивается единица, потом указатель p смещается и начинает указывать уже на второй элемент. Мы могли бы написать это так:
*p= 1; p++;
Чтобы получить предыдущий элемент и сместиться назад, можно использовать операцию декремента: Console.WriteLine(*(—p)); . Обратите внимание, что операции *(—p) и *(p—) различаются, так как в первом случае сначала идет смещение указателя, а затем его разыменовывание. А во втором случае — наоборот.
Затем вычисляем факториал всех остальных шести чисел: *p = p[-1] *i; . Обращение к указателям как к массивам представляет альтернативу операции разыменовывания для получения значения. В данном случае мы получаем значение предыдущего элемента.
И в заключении, используя указатель factorial, выводим факториалы всех семи чисел.
Оператор fixed и закрепление указателей
Ранее мы посмотрели, как создавать указатели на типы значений, например, int или структуры. Однако кроме структур в C# есть еще и классы, которые в отличие от типов значений, помещают все связанные значения в куче. И в работу данных классов может в любой момент вмешаться сборщик мусора, периодически очищающий кучу. Чтобы фиксировать на все время работы указатели на объекты классов используется оператор fixed .
Допустим, у нас есть класс Point:
class Point < public int x; public int y; public override string ToString() =>$"x: y: "; >
Зафиксируем указатель с помощью оператора fixed:
unsafe < Point point = new Point(); // блок фиксации указателя fixed (int* pX = &point.x) < *pX = 30; >fixed (int* pY = &point.y) < *pY = 150; >// можно совместить оба блока /*fixed (int* pX = &point.x, pY = &point.y) < *pX = 30; *pY = 150; >*/ Console.WriteLine(point); // x: 30 y: 150 >
Оператор fixed создает блок, в котором фиксируется указатель на поле объекта person. После завершения блока fixed закрепление с переменных снимается, и они могут быть подвержены сборке мусора.
Кроме адреса переменной можно также инициализировать указатель, используя массив, строку или буфер фиксированного размера:
unsafe < int[] nums = < 0, 1, 2, 3, 7, 88 >; string str = "Привет мир"; fixed(int* p = nums) < int third = *(p+2); // получим третий элемент Console.WriteLine(third); // 2 >fixed(char* p = str) < char forth = *(p + 3); // получим четвертый элемент Console.WriteLine(forth); // в >>
При инициализации указателей на строку следует учитывать, что указатель должен иметь тип char* .
Указатели на члены
Объявления указателей на члены — это особый случай объявлений указателей. Они объявляются с помощью следующей последовательности:
описатель класса storage-class-opt cv-qualifiers opttype-specifierms-modifier optqualified-name ::* cv-qualifiersoptidentifier pm-initializer opt ;
- Спецификатор объявления:
- Необязательный спецификатор класса хранения.
- Необязательные const и volatile описатели.
- Спецификатор типа: имя типа. Это тип элемента, на который нужно указать, а не класс.
- Декларатор:
- Необязательный модификатор, используемый в системах Microsoft. Дополнительные сведения см. в разделе «Модификаторы для конкретного майкрософт».
- Полное имя класса, содержащего члены, на которые должен указывать указатель.
- Оператор :: .
- Оператор * .
- Необязательные const и volatile описатели.
- Идентификатор, задающий имя указателя на член.
- Необязательный инициализатор указателя на член:
- Оператор = .
- Оператор & .
- Полное имя класса.
- Оператор :: .
- Имя нестатитического члена класса соответствующего типа.
Как обычно, в одном объявлении допускается несколько деклараторов (и любые связанные инициализаторы). Указатель на член может не указывать на статический элемент класса, член ссылочного типа или void .
Указатель на член класса отличается от обычного указателя: он содержит сведения о типе элемента и для класса, к которому принадлежит член. Обычный указатель идентифицирует только один объект в памяти (содержит адрес этого объекта). Указатель на член класса идентифицирует этот член в любом экземпляре класса. В следующем примере объявляется класс Window и несколько указателей на данные-член.
// pointers_to_members1.cpp class Window < public: Window(); // Default constructor. Window( int x1, int y1, // Constructor specifying int x2, int y2 ); // Window size. bool SetCaption( const char *szTitle ); // Set window caption. const char *GetCaption(); // Get window caption. char *szWinCaption; // Window caption. >; // Declare a pointer to the data member szWinCaption. char * Window::* pwCaption = &Window::szWinCaption; int main()
В предыдущем примере pwCaption указатель на любой член класса Window , который имеет тип char* . pwCaption имеет тип char * Window::* . В следующем фрагменте кода объявляются указатели на функции-члены SetCaption и GetCaption .
const char * (Window::* pfnwGC)() = &Window::GetCaption; bool (Window::* pfnwSC)( const char * ) = &Window::SetCaption;
Указатели pfnwGC и pfnwSC указывают на функции GetCaption и SetCaption класса Window соответственно. Код копирует информацию непосредственно в заголовок окна с помощью указателя на член pwCaption :
Window wMainWindow; Window *pwChildWindow = new Window; char *szUntitled = "Untitled - "; int cUntitledLen = strlen( szUntitled ); strcpy_s( wMainWindow.*pwCaption, cUntitledLen, szUntitled ); (wMainWindow.*pwCaption)[cUntitledLen - 1] = '1'; // same as // wMainWindow.SzWinCaption [cUntitledLen - 1] = '1'; strcpy_s( pwChildWindow->*pwCaption, cUntitledLen, szUntitled ); (pwChildWindow->*pwCaption)[cUntitledLen - 1] = '2'; // same as // pwChildWindow->szWinCaption[cUntitledLen - 1] = '2';
Разница между .* операторами и ->* операторами (операторами указателя на члены) заключается в том, что .* оператор выбирает членов, заданных объектом или ссылкой на объект, а ->* оператор выбирает элементы через указатель. Дополнительные сведения об этих операторах см. в разделе «Выражения» с операторами указателя на члены.
Результатом операторов указателя на член является тип элемента. В этом случае он выглядит так: char * .
В следующем фрагменте кода функции-члены GetCaption и SetCaption вызываются с использованием указателей на члены.
// Allocate a buffer. enum < sizeOfBuffer = 100 >; char szCaptionBase[sizeOfBuffer]; // Copy the main window caption into the buffer // and append " [View 1]". strcpy_s( szCaptionBase, sizeOfBuffer, (wMainWindow.*pfnwGC)() ); strcat_s( szCaptionBase, sizeOfBuffer, " [View 1]" ); // Set the child window's caption. (pwChildWindow->*pfnwSC)( szCaptionBase );
Ограничения указателей на члены
Адрес статического элемента не является указателем на элемент. Это обычный указатель на один экземпляр статического элемента. Для всех объектов данного класса существует только один экземпляр статического элемента. Это означает, что вы можете использовать обычные операторы адреса (&) и деreference (*).
Указатели на члены и виртуальные функции
Вызов виртуальной функции через функцию указателя на член работает так, как если бы функция была вызвана напрямую. Правильная функция ищется в таблице v-table и вызывается.
Ключ для виртуальных функций, работающих как обычно, вызывает их через указатель на базовый класс. (Дополнительные сведения о виртуальных функциях см. в разделе Виртуальные функции.)
В следующем коде показан вызов виртуальной функции через функцию указателя на член.
// virtual_functions.cpp // compile with: /EHsc #include using namespace std; class Base < public: virtual void Print(); >; void (Base::* bfnPrint)() = &Base::Print; void Base::Print() < cout class Derived : public Base < public: void Print(); // Print is still a virtual function. >; void Derived::Print() < cout int main() < Base *bPtr; Base bObject; Derived dObject; bPtr = &bObject; // Set pointer to address of bObject. (bPtr->*bfnPrint)(); bPtr = &dObject; // Set pointer to address of dObject. (bPtr->*bfnPrint)(); > // Output: // Print function for class Base // Print function for class Derived