Перейти к содержимому

Как сделать указатель на объект класса с

  • автор:

Как сделать указатель на объект класса с

Кроме указателей на простые типы можно использовать указатели на структуры. А для доступа к полям структуры, на которую указывает указатель, используется операция ->:

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* .

Указатели на объекты

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

Для доступа к членам объекта через сам объект используется оператор «точка» (.). Если же используется указатель на объект, тогда необходимо использовать оператор «стрелка» (—>). Ис­пользование операторов «точка» и «стрелка» аналогично их использованию для структур и объе­динений.

Указатель на объект объявляется с использованием того же синтаксиса, что и указатели на данные других типов. В следующей программе создается простой класс с именем 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.

Указатели

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

Определение указателя

Для определения указателя надо указать тип объекта, на который указывает указатель, и символ звездочки *:

тип_данных* название_указателя;

Сначала идет тип данных, на который указывает указатель, и символ звездочки *. Затем имя указателя.

Например, определим указатель на объект типа int:

int* p;

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

#include int main()

Например, в моем случае консоль вывела «0x8» — некоторый адрес в шестнадцатеричном формате (обычно для представления адресов в памяти применяется шестнадцатеричная форма). Но также можно инициализировать указатель некоторым значением:

int* p<>;

Поскольку конкрентное значение не указано, указатель в качестве значения получает число 0. Это значение представляет специальный адрес, который не указывает не на что. Также можно явным образом инициализировать нулем, например, используя специальную константу nullptr :

int* p;

Хотя никто не запрещает не инициализировать указатели. Однако в общем случае рекомендуется все таки инициализировать, либо каким-то конкретным значением, либо нулем, как выше. Так, к примеру, нулевое значение в будущем позволит определить, что указатель не указывает ни на какой объект.

Cтоит отметить что положение звездочки не влияет на определение указателя: ее можно помещать ближе к типу данных, либо к имени переменной — оба определения будут равноценны:

int* p1<>; int *p2<>;

Также стоит отметить, что размер значения указателя (хранимый адрес) не зависит от типа указателя. Он зависит от конкретной платформы. На 32-разрядных платформах размер адресов равен 4 байтам, а на 64-разрядных — 8 байтам. Например:

#include int main() < int *pint<>; double *pdouble<>; std::cout

В данном случае определены два указателя на разные типы — int и double. Переменные этих типов имеют разные размеры — 4 и 8 байт соответственно. Но размеры значений указателей будут одинаковы. В моем случае на 64-разрядной платформе размер обоих указателей равен 8 байтам.

Получение адреса и оператор &

С помощью операция & можно получить адрес некоторого объекта, например, адрес переменной. Затем этот адрес можно присвоить указателю::

int number ; int *pnumber ; // указатель pnumber хранит адрес переменной number

Выражение &number возвращает адрес переменной number . Поэтому переменная pnumber будет хранить адрес переменной number . Что важно, переменная number имеет тип int, и указатель, который указывает на ее адрес, тоже имеет тип int. То есть должно быть соответствие по типу. Однако также можно использовать ключевое слово auto :

int number ; auto *pnumber ; // указатель pnumber хранит адрес переменной number

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

#include int main() < int number ; int *pnumber ; // указатель pnumber хранит адрес переменной number std::cout

Консольный вывод программы в моем случае:

number addr: 0x1543bffc74

В каждом отдельном случае адрес может отличаться и при разных запусках программы может меняться. К примеру, в моем случае машинный адрес переменной number — 0x1543bffc74 . То есть в памяти компьютера есть адрес 0x1543bffc74, по которому располагается переменная number. Так как переменная x представляет тип int , то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом, переменная типа int последовательно займет ячейки памяти с адресами 0x1543bffc74, 0x1543bffc75, 0x1543bffc76, 0x1543bffc77.

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

И указатель pnumber будет ссылаться на адрес, по которому располагается переменная number, то есть на адрес 0x1543bffc74.

Итак, указатель pnumber хранит адрес переменной number, а где хранится сам указатель pnumber? Чтобы узнать это, мы также можем применить к переменной pnumber операцию &:

#include int main() < int number ; int *pnumber ; // указатель pnumber хранит адрес переменной number std::cout

Консольный вывод программы в моем случае:

number addr: 0xe1f99ff7cc pnumber addr: 0xe1f99ff7c0

Здесь мы видим, что переменная number располагается по адресу 0xe1f99ff7cc , а указатель, который хранит этот адрес, — по адресу 0xe1f99ff7c0 . Из вывода видно, что обе переменные хранятся совсем рядом в памяти

Получение значения по адресу

Но так как указатель хранит адрес, то мы можем по этому адресу получить хранящееся там значение, то есть значение переменной number. Для этого применяется операция * или операция разыменования («indirection operator» / «dereference operator»). Результатом этой операции всегда является объект, на который указывает указатель. Применим данную операцию и получим значение переменной number:

#include int main() < int number ; int *pnumber ; std::cout Address = 0x44305ffd4c Value = 25

Значение, которое получено в результате операции разыменования, можно присвоить другой переменной:

int n1 ; int *pn1 ; // указатель pn1 хранит адрес переменной n1 int n2 < *pn1>; // n2 получает значение, которое хранится по адресу в pn1 std::cout int x = 10; int *px = &x; *px = 45; std::cout

Указатели на объекты

При первом знакомстве с указателями в C++ (см. Указатели в C++. Часть 1 ) может сложиться упрощённое представление, что указатели могут указывать только на отдельные переменные встроенных (скалярных) типов C++, и что это просто ещё одна, альтернативная форма доступа к таким переменным. В таком применении указатели были бы приятным дополнением языка, но с весьма ограниченными возможностями.

При более внимательном изучении указателей C++ мы обнаруживаем, что указатель может быть адресом размещения (указывать на) любого допустимого объекта в программе: структуры, объекта класса, массива, функции, или опять же указателя на некоторый объект, или указателя на указатель и так далее… Это делает указатели чуть ли не самым мощным инструментом программиста на C++ … но и самым опасным в смысле возможных скрытых ошибок.

Рассмотрим такие варианты подробнее. Самым простым вариантом будет использование указателей на составные объекты (объекты классов и структуры). Но уже такое использование указателей открывает широкие перспективы в C++, как мы сейчас увидим.

Итак, мы уже знаем, что описание класса не создаёт никаких новых объектов данных в программе — это только описание некоторого шаблона, нового типа данных, согласно которому будут создаваться реальные объекты данных.

При создании (объявлении) новых объектов данных мы можем вычислять адрес этих объектов и присваивать их указателям на объекты этого класса. Напишем простейшую программу, оперирующую с указателями на объекты некоторого класса (файл ex1.cc):

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *