Указатели
Указатели являются одной из сильных сторон С++. Грамотное их применение позволяет повысить скорость выполнения программы и более эффективно использовать память. Например, во многих случаях в качестве аргументов функции лучше использовать указатели на объект, чем каждый раз создавать их копии.
С указателями используются следующие операции:
- & - взятие адреса переменной;
- * - разъименование указателя, т.е. получение доступа к объекту;
- -> - разъименование члена структуры;
- [] - индексация, доступ к элементу массива, при этом считается, что указатель содержит адрес 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;
}
Как видно из этого примера, работа с указателями требует большой внимательности. Легко затереть не те данные, или обратиться по указателю к уже удаленному объекту или забыть удалить объект. Также указатели позволяют обойти некоторые ограничения языка (например обратиться к защищенному члену класса) и даже повлиять на систему в целом (например, в DOS программе зная адрес можно обратиться к системным переменным DOS или BIOS). Поэтому в некоторых языках как java полностью откзались от указателей ради безопасности.
Для устранения проблемы удаления объекта существуют готовые классы указатели. Некоторые из них рассмотрены в пункте динамическая память.
Адреса
Имя переменной является ее своеобразным (буквенным) адресом. Однако у любой переменной есть также и обычный (цифровой или физический ) адрес: номер ячейки, выделенной под эту переменную.
При страничной организации памяти адреса являются составными и состоят из номера сегмента памяти и смещения ячейки относительно начала этого сегмента.
Лучшая иллюстрация страничной организации памяти компьютера - это страничная организация любой печатной книги. Для того чтобы найти нужную строчку, нет необходимости задавать ее номер, считая от начала текста. Вместо этого можно задать сначала номер страницы ( = сегмент ) и только затем номер строки, считая от начала этой страницы ( =смещение ).
Для обращения к статически заданной переменной можно использовать как ее имя, объявленное в разделе var, так и ее физический адрес.
Например, " адрес " одной и той же географической точки можно записать по-разному: "49°47' северной широты и 86°36' восточной долготы" или просто "вершина пика Белуха Восточная1) ".
Указатели
Для того чтобы хранить (цифровые) адреса, нужны особые переменные. Их называют указателями и относят к специальному типу данных.
Описание указателей
При описании типизированного указателя необходимо сообщить компилятору, адреса переменных какого типа он может хранить:
var <имя_указателя>: ^<тип_адресуемой_переменной>;
Например:
var p: ^integer;
q: ^real;
s: ^array[1..10] of byte;
Кроме того, существуют универсальные нетипизированные указатели, которые могут хранить адрес переменной любого типа:
var <имя_указателя>: pointer;
Операции с указателями
Определение адреса
Физический адрес любой переменной можно узнать при помощи стандартной функции addr(<имя_переменной>):<указатель> или унарной операции@<имя_переменной>.
В зависимости от значения директивы компилятора {$T}, результатом операции @ будет либо типизированный указатель (если установлено {$T+} ), тип которого будет определен в соответствии с типом использованной переменной, либо нетипизированный указатель pointer (если установлено {$T-} ).
Результат функции addr() совместим с указателями любых типов:
p:= addr(x); {x: real; p: ^byte)
Разыменование
Для того чтобы воспользоваться значением, хранящимся по некоторому адресу, необходимо его оттуда "извлечь". Унарная операция ^ называетсяразыменованием и записывается по следующему шаблону:
<имя_указателя>^
Результатом операции ^ является значение, хранящееся по указанному адресу. Тип этого значения будет определяться типом ( типизированного ) указателя. Кнетипизированным указателям операцию разыменования применять нельзя.
Из-за вольностей, допускаемых процедурой addr(), при разыменовании порой могут возникнуть забавные ситуации. Например, в результате выполнения такой вот программы:
const a: array[1..3] of char ='ААА'; {код(А)=128 или 01000000}
var p: ^word;
begin p:= addr(a);
writeln(p^)
end
на экран будет выведено 32896, что в двоичной системе счисления выглядит как 01000000.01000000 (точкой помечена граница двух байтов). Иными словами, коды двух первых букв оказались слитыми в значение типа word.
Замечание: Операции @ и ^ являются взаимно обратными, то есть для любой переменной a и для любого типизированного указателя p верны следующие равенства:
@(p^)= p и (@a)^ = a