Указатели на объекты
В языке С можно получить доступ к структуре непосредственно или с использованием указателей на эту структуру. Аналогичным образом в С++ можно ссылаться на объект непосредственно, как это имело место во всех предыдущих примерах, или используя указатель на этот объект. Указатели на объекты являются одним из важнейших понятий С++.
Для доступа к членам объекта через сам объект используется оператор «точка» (.). Если же используется указатель на объект, тогда необходимо использовать оператор «стрелка» (—>). Использование операторов «точка» и «стрелка» аналогично их использованию для структур и объединений.
Указатель на объект объявляется с использованием того же синтаксиса, что и указатели на данные других типов. В следующей программе создается простой класс с именем 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.
Указатели на методы классов в C++
Решил написать статью об указателях на методы классов. Недавно мне пришлось столкнуться с тем, как они работают изнутри, когда писал некоторые вещи ориентированные под компилятор. Эти указатели работают не совсем как обычные указатели, не имеют возможности быть приведенными в void, и часто имеют размер больше 8 байт. Информации на эту тему в интернете я нашел относительно немного, потому решил разобраться сам.
Особенно пугают такие страшилки, которые мало что объясняют о том как происходит на самом деле и почему, а лишь пытаются приучить программиста слепо следовать требованиям.
Давайте разберемся что и почему происходит.
Все манипуляции будут произведены для архитектуры x86-64.
Взглянем на код.
#include int main()
Размер указателя на метод больше 8 байт. В некоторых компиляторах это не так, например компилятор Microsoft ужимает до 8 байт указатель на метод в некоторых случаях. В последних версиях компиляторов clang и gcc для Linux принимал размер 16 байт.
Как мне кажется, разработчики компиляторов не могли без особой причины заменить обычный указатель на нечто другое. Давайте же разберемся почему они так сделали.
Посмотрим такой код на C++. Это базовый пример вызова метода из указателя на метод.
struct A; typedef void (A::*Ptr) (); Ptr ptr; void call(A *a) < (a->*ptr)(); >
Скомпилировав код такой командой:
clang++ code.cpp -c -emit-llvm -S -O3 -fno-discard-value-names
Получаем вывод LLVM IR:
@ptr = dso_local local_unnamed_addr global < i64, i64 >zeroinitializer, align 8 ; Function Attrs: uwtable define dso_local void @_Z4callP1A(%struct.A* %a) local_unnamed_addr #0 < entry: %.unpack = load i64, i64* getelementptr inbounds (< i64, i64 >, < i64, i64 >* @ptr, i64 0, i32 0), align 8, !tbaa !2 %.unpack1 = load i64, i64* getelementptr inbounds (< i64, i64 >, < i64, i64 >* @ptr, i64 0, i32 1), align 8, !tbaa !2 %0 = bitcast %struct.A* %a to i8* %1 = getelementptr inbounds i8, i8* %0, i64 %.unpack1 %this.adjusted = bitcast i8* %1 to %struct.A* %2 = and i64 %.unpack, 1 %memptr.isvirtual.not = icmp eq i64 %2, 0 br i1 %memptr.isvirtual.not, label %memptr.nonvirtual, label %memptr.virtual memptr.virtual: ; preds = %entry %3 = bitcast %struct.A* %this.adjusted to i8** %vtable = load i8*, i8** %3, align 1, !tbaa !5 %4 = add i64 %.unpack, -1 %5 = getelementptr i8, i8* %vtable, i64 %4, !nosanitize !7 %6 = bitcast i8* %5 to void (%struct.A*)**, !nosanitize !7 %memptr.virtualfn = load void (%struct.A*)*, void (%struct.A*)** %6, align 8, !nosanitize !7 br label %memptr.end memptr.nonvirtual: ; preds = %entry %memptr.nonvirtualfn = inttoptr i64 %.unpack to void (%struct.A*)* br label %memptr.end memptr.end: ; preds = %memptr.nonvirtual, %memptr.virtual %7 = phi void (%struct.A*)* [ %memptr.virtualfn, %memptr.virtual ], [ %memptr.nonvirtualfn, %memptr.nonvirtual ] tail call void %7(%struct.A* %this.adjusted) ret void >
LLVM IR является промежуточным представлением между машинным кодом и C++ в компиляторе Clang. Он позволяет компилятору производить оптимизации не зависящие от конкретной архитектуры процессора, а нам он дает понять что происходит на тех или иных стадиях компиляции, и является более читаемым чем язык ассемблера.
Подробнее про LLVM IR можно узнать в Википедии, официальном сайте LLVM и Clang.
- Взглянув на первую строчку, видно что указатель на метод является структурой `< i64, i64 >`, а не обычным указателем. Эта структура содержит два i64 элемента, которые могут уместить в себя 2 обычных указателя. Видно почему мы не можем приводить указатели на методы в обычные. Мы не можем без потерь преобразовать 16 байт в 8 байт в общем случае.
- В блоке `entry`, начинающимся с 5 строки, видно что происходит корректирование указателя `this`. Это значит, что компилятор прибавляет к указателю на `this` значение второго элемента этой структуры, и позже в блоке `memptr.end` передает его в вызов метода.
- Нечто странное происходит происходит в блоке `entry` на 14 строке с первым элементом структуры. Компилятор вычисляет выражение аналогичное следующему: `bool isvirtual = val & 1`. Компилятор считает указатель на метод виртуальным, если число в нем нечетное, в противном случае невиртуальным.
- Если указатель на метод указывает на невиртуальный метод, то значение первого элемента считается обычным указателем на функцию, который позже вызывается. Эти предположения происходят в блоке `memptr.nonvirtual`.
- Если указатель на метод указывает на виртуальный метод, то тут сложнее. Вначале вычитается единица из первого элемента структуры, и вычисленное значение является отступом для виртуальной таблицы, указатель на которую берется из значения указателя на `this`. Это происходит в блоке `memptr.virtual`.
- Информацию является ли он виртуальным
- Указатель на адрес метода (если не виртуальный)
- Смещение в vtable (если виртуальный)
- корректирование `this`
Метод класса имеет невидимый первый параметр — указатель на `this`, который передается компилятором при вызове метода. Остальные аргументы передаются после в том же порядке, что и были.
Если бы мы писали этот код на C++, то он выглядел бы примерно так:
A *a; a->method_name(1, 2, 3); method_name(a, 1, 2, 3);
Чтобы разобраться с значением корректирования, рассмотрим следующий пример:
struct A < char a[123]; >; struct B < char a[0]; void foo(); static void bar(B *arg); >; struct C : A, B <>; void (C::*a)() = &C::foo; void (C::*b)() = &B::foo; void (B::*c)() = &B::foo; void (*a1)(C*) = &C::bar; // error void (*b1)(C*) = &B::bar; // error void (*c1)(B*) = &B::bar; // ok
Как мы видим, тут представлены примеры указателей на методы и аналогичные функции, которые принимают указатель на класс как указатель на `this`. Однако компилятор не может преобразовать указатели a1 и b1 в связи с тем, что мы не можем бесплатно преобразовывать указатели дочернего типа в указатели родительского типа. Компилятору необходимо запомнить отступ (значение корректирования) внутри дочернего класса для родительского класса и сохранить его где-то.
Посмотрим такой код:
struct A < char a[123]; >; struct B < char a[0]; void foo(); static void bar(B *arg); >; struct C : A, B <>; void (C::*a)() = &C::foo; void (C::*b)() = &B::foo; void (B::*c)() = &B::foo;
Скомпилируем код командой:
clang++ code.cpp -c -emit-llvm -S -O3 -fno-discard-value-names
@a = dso_local global < i64, i64 >< i64 ptrtoint (void (%struct.B*)* @_ZN1B3fooEv to i64), i64 123 >, align 8 @b = dso_local global < i64, i64 >< i64 ptrtoint (void (%struct.B*)* @_ZN1B3fooEv to i64), i64 123 >, align 8 @c = dso_local global < i64, i64 >< i64 ptrtoint (void (%struct.B*)* @_ZN1B3fooEv to i64), i64 0 >, align 8
Видно, что указатель на метод указывает на одну и ту же функцию. Однако значение корректирования разное из-за того что класс B расположен по сути внутри класса C.
Компилятору C++ необходимо знать отступ от базового класса для того чтобы передать `this` в метод класса.
Что плохого в этой реализации:
- Размер указателя относительно большой, даже если корректирование отсутствует каким либо образом в gcc и clang
- Каждый раз идет проверка виртуальности метода, даже если мы знаем что он не виртуальный
- Использовать статический метод, принимающий экземпляр класса
- Забыть про существование указателей на методы, и решить проблему как-то иначе в прочих случаях
- В интернете есть советы использовать std::bind, std::function и подобные библиотечные функции. Проверив их поведение, я не обнаружил существования каких либо оптимизаций для указателей на методы.
- У меня нет технической возможности проверить что происходит в компиляторах Microsoft, поэтому не особо про них рассказал. Однако протестировав онлайн компиляторы, я заметил что MSVC умеет анализировать структуру классов и удалять поле значения корректирования, если оно не требуется.
#include #include struct A; extern A* a; extern void(A::*func)(); template T assume_not_virual(T input) < struct Ptr < uint64_t a, b; >; static_assert(sizeof(T) == sizeof(Ptr), ""); Ptr ptr; memcpy(&ptr, &input, sizeof(input)); __builtin_assume(!(ptr.a & 1)); return input; > void call() < (a->*assume_not_virual(func))(); >
В данном примере компилятор не будет проверять метод на виртуальность, и сразу вызовет его как невиртуальный. Это лишь пример необычной оптимизации, не стоит использовать его в реальности.
Также я написал маленькую программку, что выводит данные об указателях на методы и помогает понять их внутренности, пока писал эту статью. Работает в clang и gcc под Linux. Код выложен тут.
Проведя это маленькое расследование, я понял как работают указатели на методы и как с ними жить. Надеюсь, это оказалось полезно для кого-то.
Указатели на классы
Классы и указатели
Добрый день, у меня такой вопрос. Вот есть две идентичные записи. Client * NewClient = new Client;.
Классы и указатели
Пишет:"Отсутствуют экземпляры конструктора "Pet::Pet", соответствующие списку аргументов типы.
Классы, указатели и функции?
Помогите пожалуйста разобраться в программе. Нужно из конструктора класса передать адреса в.
2924 / 1274 / 114
Регистрация: 27.05.2008
Сообщений: 3,465
ob1 — указатель на объект класса A. ob2 — указатель на объект класса B.
Регистрация: 13.01.2013
Сообщений: 71
Сообщение от CheshireCat
ob1 — указатель на объект класса A. ob2 — указатель на объект класса B.
ну если ob1 указатель на А то почему он new B?
Ушел с форума
16470 / 7433 / 1187
Регистрация: 02.05.2013
Сообщений: 11,617
Записей в блоге: 1
ob1 — это указатель на подобъект A, являющийся частью объекта класса B.
ob2 — это указатель на объект класса B.
Так что оба указателя указывают на объект класса B, просто ob2 делает это «бесхитростно», а
ob1 — через указатель на A.
229 / 76 / 9
Регистрация: 03.02.2013
Сообщений: 311
Если в классе B будут дополнительные поля, которых нет в классе А, то через указатель ob1 они будут недоступны.
Регистрация: 13.01.2013
Сообщений: 71
Сообщение от Пёс
Если в классе B будут дополнительные поля, которых нет в классе А, то через указатель ob1 они будут недоступны.
ну так гораздо понятней стало.
а тогда обратная схема допустима:
B *ob3 = new A ?
2924 / 1274 / 114
Регистрация: 27.05.2008
Сообщений: 3,465
Нет. Преобразование «указатель на потомка => указатель на предка» (или «потомок => предок») выполняется компилятором автоматически — так как потомок одновременно «является» и предком, т.е. содержит все поля и методы предка, тут работает LSP. Наоборот — нельзя (предок ничего не «знает» о тех полях и методах, которые появились у потомка).
Ушел с форума
16470 / 7433 / 1187
Регистрация: 02.05.2013
Сообщений: 11,617
Записей в блоге: 1
Есть класс A.
И есть класс B, который унаследован от A.
Это значит, что B содержит в себе подобъект класса A.
Теперь с объектом класса B можно обращаться через указатель или
ссылку на класс A или B. В первом случае будут доступны только
поля, объявленные в классе A, во втором случае — также все остальные,
которые появились в классе B.
Если в A есть виртуальный метод, переопределенный в классе B, то при
вызова этого метода на объекте класса B через указатель или ссылку на
класс A, будет вызываться метод B, а не A. Это поддержка языком
такой вещи, как полиморфизм. То есть, за указателем на базовый класс A
можно «замаскировать» объекты других, унаследованных от него типов, и
работать с ними через единый интерфейс, задекларированный в классе A.
229 / 76 / 9
Регистрация: 03.02.2013
Сообщений: 311
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
class A { int field_A; void method_A() { //что-то там. } }; class B : public A { public: int field_B; void method_B() { //что-то другое. } }; int main() { A *ob1=new B; B *ob2=new B; ob1->field_A = 1; //допустимо, обращаемся к полю, определённому в А, объекта класса B через указатель на А ob1->method_A(); //допустимо, обращаемся к методу, определённому в А, объекта класса B через указатель на А ob1->field_B = 1; //недопустимо, указатель на A ничего не знает о полях, определённых в B. ob1->method_B(); //недопустимо, указатель на A ничего не знает о методах, определённых в B. ob2->field_A = 1; //допустимо, указатель на B знает о полях определённых в A. ob2->method_B(); //допустимо, указатель на B знает о методах определённых в А ob2->field_B = 1; //допустимо, указатель на B знает о полях определённых в своём классе. ob2->method_B(); //допустимо, указатель на B знает о методах определённых в своём классе. B* ob3 = new A; //НЕДОПУСТИМО, нельзя помещать объект родителя, в указатель на потомка. }
Регистрация: 13.01.2013
Сообщений: 71
а в чем соль тогда записи A* ob1=new B если можно писать просто A *ob1=new A ?
859 / 448 / 112
Регистрация: 06.07.2013
Сообщений: 1,491
Сообщение от rizr
а в чем соль тогда записи A* ob1=new B если можно писать просто A *ob1=new A ?
в том что в A нет того что есть в B
2924 / 1274 / 114
Регистрация: 27.05.2008
Сообщений: 3,465
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
#include using namespace std; class A { public: virtual void func() { cout "A::func()" endl; }; }; class B: public A { public: void func() { cout "B::func()" endl; }; }; class C: public B { public: void func() { cout "C::func()" endl; }; }; int main(void) { const int SIZE = 5; A* arr[SIZE]; // можно объявить массив указателей на базовый класс! // а запихать в него разные классы. arr[0] = new A; arr[1] = new B; arr[2] = new A; arr[3] = new C; arr[4] = new C; // и все отработает корректно! for(int i = 0; i SIZE; ++i) arr[i]->func(); }
229 / 76 / 9
Регистрация: 03.02.2013
Сообщений: 311
rizr, соль в том, что когда нужно обработать кучу объектов, имеющих сходные свойства, но тем не менее имеющие различные поля и методы (которые нам собственно и не нужны) можно передавать их все, в виде указателей и ссылок на их родительский класс.
Например, нам нужно написать функцию, которая что-нибудь делает с полем field_A:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
class A { int field_A; void method_A() { //что-то там. } }; class B : public A { public: int field_B; void method_B() { //что-то другое. } }; bool function_1(const A* ptr) { return ptr->field_A > 0; //возвращает true, когда field_a переданного объекта положительное. } int main() { A *ob1=new B; B *ob2=new B; function_1(ob1); function_1(ob2); }
PROFIT: Нам не пришлось создавать 2 версии функции function_1, потому что она не обращается к специфическим полям и методам класса B.
Регистрация: 13.06.2019
Сообщений: 66
Чтоб не создавать лишнюю тему задам вопрос здесь.
Дано 3 разных файла:
int m_size;
Person *m_persons;
1 2 3 4 5 6 7 8 9 10
int main() { char names[][15] = { "A", "B", "C", "D", "F" }; Group group(5); После идёт Group::Group(int size) : m_size(size), m_persons(new Person[size]) { } И за ним следует Person::Person() : m_id(0), m_name(NULL), m_gender(false), m_age(0.0) { }
Я правильно понимаю что Person *m_persons; является указателем на класс Person ?
8737 / 4315 / 960
Регистрация: 15.11.2014
Сообщений: 9,760
Сообщение от Triglav86
Я правильно понимаю что Person *m_persons; является указателем на класс Person ?
Комп_Оратор)
8927 / 4684 / 626
Регистрация: 04.12.2011
Сообщений: 13,941
Записей в блоге: 16
Сообщение от Triglav86
Я правильно понимаю что Person *m_persons; является указателем на класс Person ?
Указатель на класс, это мерзость имеющая место быть в головах и на языках. Но она полностью отсутствует в природе. Нет указателей на классы. Есть указатели на объекты. В вашем случае указатель на объект класса Person обладает дополнительным свойством, которое находится в голове программиста и в недрах системы. Внешне он не отличим от простого указателя на объект. Оно (свойство) ни как не присутствует в самом указателе. А именно, — этот указатель создан как указатель на первый элемент цепочки последовательно размещённых в свободной памяти объектов. Цепочка имеет длину size объектов или size*sizeof(Person) байт. Освобождение этой памяти можно корректно сотворить лишь используя оператор делит с квадратными челюстями delete [] ptr_to_miracle . То есть, ваш указатель похож на указатель на объект класса Person и работает как таковой почти везде. Но это указатель на первый объект цепи. Вот такой вот сайд сайз эффект.
Triglav86, вы видели дату создания темы? Зачем вошли в дом, где ваш вопрос не соответствует теме?
Сообщение от Triglav86
Чтоб не создавать лишнюю тему задам вопрос здесь.
Похоже на «чтобы не ходить в туалет покакаю здесь». Думаю, модераторы разделять топик так как оно должно быть. Его грамотно провели участники достойные всяческого уважения.
87844 / 49110 / 22898
Регистрация: 17.06.2006
Сообщений: 92,604
Помогаю со студенческими работами здесь
Перечисление,указатели, классы
Здравствуйте, можете скинуть или написать простые задачи на тему "Перечисление,указатели, классы"
Ссылки. Указатели. Классы
Здравствуйте. Пишу базу данных, и наткнулся на проблему с ссылкой. Код ниже, как ее исправить. int.
Указатели на производные классы
Подскажите, как создать массив типа "указатель на базовый класс", который будет содержать указатель.
Задача на классы и статические указатели
Условие задачи: в любой момент времени можно получить последнего из могикан (объект класса), без.
Возвращение исходного массива (классы, указатели)
нужно сделать так чтоб после каждого действия, при указатели на вызов функции output возвращался.
Урок №121. Скрытый указатель *this
Один из частых вопросов, которые новички задают по поводу классов: «При вызове метода класса, как C++ отслеживает то, какой объект его вызвал?». Ответ заключается в том, что C++ для этих целей использует скрытый указатель *this!
Оглавление:
- Скрытый указатель *this
- Указатель *this всегда указывает на текущий объект
- Явное указание указателя *this
- Цепочки методов класса
- Заключение
Скрытый указатель *this
Ниже приведен простой класс, который содержит целочисленное значение и имеет конструктор и функции доступа. Обратите внимание, деструктор здесь не нужен, так как язык C++ может очистить память после переменной-члена самостоятельно:
class Another
int m_number ;
Another ( int number )
setNumber ( number ) ;
void setNumber ( int number ) < m_number = number ; >
int getNumber ( ) < return m_number ; >
Another another ( 3 ) ;
another . setNumber ( 4 ) ;
std :: cout << another . getNumber ( ) << '\n' ;
Результат выполнения программы:
При вызове another.setNumber(4); C++ понимает, что функция setNumber() работает с объектом another , а m_number — это фактически another.m_number . Рассмотрим детально, как это всё работает.
Возьмем, к примеру, следующую строку:
another . setNumber ( 4 ) ;
Хотя на первый взгляд кажется, что у нас здесь только один аргумент, но на самом деле у нас их два! Во время компиляции строка another.setNumber(4); конвертируется компилятором в следующее:
setNumber ( &another , 4 ) ; // объект another конвертировался из объекта, который находился перед точкой, в аргумент функции!
Теперь это всего лишь стандартный вызов функции, а объект another (который ранее был отдельным объектом и находился перед точкой) теперь передается по адресу в качестве аргумента функции.
Но это только половина дела. Поскольку в вызове функции теперь есть два аргумента, то и метод нужно изменить соответствующим образом (чтобы он принимал два аргумента). Следовательно, следующий метод:
void setNumber ( int number ) < m_number = number ; >
Конвертируется компилятором в:
void setNumber ( Another * const this , int number ) < this ->m_number = number ; >
При компиляции обычного метода, компилятор неявно добавляет к нему параметр *this. Указатель *this — это скрытый константный указатель, содержащий адрес объекта, который вызывает метод класса.
Есть еще одна деталь. Внутри метода также необходимо обновить все члены класса (функции и переменные), чтобы они ссылались на объект, который вызывает этот метод. Это легко сделать, добавив префикс this-> к каждому из них. Таким образом, в теле функции setNumber(), m_number (переменная-член класса) будет конвертирована в this->m_number . И когда *this указывает на адрес another , то this->m_number будет указывать на another.m_number .
Соединяем всё вместе:
При вызове another.setNumber(4) компилятор фактически вызывает setNumber(&another, 4) .
Внутри setNumber() указатель *this содержит адрес объекта another .
К любым переменным-членам внутри setNumber() добавляется префикс this-> . Поэтому, когда мы говорим m_number = number , компилятор фактически выполняет this->m_number = number , который, в этом случае, обновляет another.m_number на number .
Хорошей новостью является то, что это всё происходит скрыто от нас (программистов), и не имеет значения, помните ли вы, как это работает или нет. Всё, что вам нужно запомнить — все обычные методы класса имеют указатель *this, который указывает на объект, связанный с вызовом метода класса.
Указатель *this всегда указывает на текущий объект
Начинающие программисты иногда путают, сколько указателей *this существует. Каждый метод имеет в качестве параметра указатель *this, который указывает на адрес объекта, с которым в данный момент выполняется операция, например: