Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Programmirovanie_-_1_kurs / Методические указания к лабораторным работам 3-4

.pdf
Скачиваний:
58
Добавлен:
09.06.2015
Размер:
700.69 Кб
Скачать

В случае со структурой test использовалось выравнивание целого по четырехбайтной (словной) границе. В результате такого выравнивания и образовались "пустоты" в памяти.

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

struct test

{

char h; int b; double f;

};

test str;

int a1 = sizeof(str);

int a2 = sizeof(char) + sizeof(int) + sizeof(double);

cout << a1 << " " << a2; // "16 13"

Если для оператора sizeof указано имя массива, то оператор вернет размер всего массива, а не отдельного элемента.

int x[1000];

cout << sizeof(x); // 4000

3. УКАЗАТЕЛИ

3.1. Понятие адреса переменной

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

адресом.

Чтобы узнать адрес конкретной переменной, используется унарная операция взятия адреса. Для

20

применения данной операции к объекту следует указать знак амперсанда перед его именем (&).

В следующем примере мы выведем на экран значения и адреса переменных a и b.

unsigned int a = 40000; unsigned int b = 300;

cout << "a (value): " << a << "\n"; cout << "a (address): " << &a << "\n"; cout << "b (value): " << b << "\n"; cout << "b (address): " << &b << "\n";

Возможный результат:

a (value): 40000

a (address): 0012FEAC b (value): 300

b (address): 0012FEA8

Как мы видим, адрес отображается в шестнадцатеричном виде и представляет собой некоторое четырехбайтное значение. Адрес показывает, где именно в памяти программы (с какого по счету байта памяти) размещается значение переменной.

Рисунок 5. Представление переменных a и b в памяти

3.2. Понятие указателя

Язык С/С++ позволяет осуществлять доступ к памяти при помощи специальных переменных, называемых

указателями.

21

Указатель – переменная, содержащая адрес некоторой ячейки памяти. Указатель определяет не только адрес, но и тип данных объекта, размещающегося в памяти по заданному адресу.

При объявлении указателя используется следующий синтаксис:

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

От объявления обычной переменной данная конструкция отличается наличием символа «звездочка» (*), определяющего, что новый идентификатор будет являться указателем.

Под указатель, как и под любую другую переменную, отводится место в памяти, причем размер указателя не зависит от типа данных значения, на которое он ссылается. Компилятор выделяет под указатель либо два, либо четыре байта, в зависимости от используемой модели памяти.

Для записи определенного адреса в переменнуюуказатель используется оператор присваивания. Указатель, как и любая другая переменная, может быть проинициализирован сразу при своем объявлении.

//Объявляем символьную переменную Symbol char Symbol = 'Y';

//Объявляем указатель на переменную Symbol char* pSymbol = &Symbol;

//Объявляем целочисленную переменную Value

int Value = 1234;

//Объявляем указатель для ссылки на целочисленные переменные ...

int *pValue;

//... и присваиваем ему адрес переменной Value pValue = &Value;

Указатель может быть объявлен с типом данных void. Указатели void* используют в случае, когда тип данных объекта, на который будет ссылаться указатель, заранее

22

неизвестен. В дальнейшем, такой указатель должен быть явно приведен к какому-либо типу данных.

int a; double b; cin >> a;

cin >> b;

// ptr - указатель на нетипизированную

//область памяти void* ptr;

//В зависимости от результата проверки условия

//указатель ptr может ссылаться на значения

//различных типов данных

if (a>b) ptr = &a;

else

ptr = &b;

3.3. Операции с указателями

3.3.1. Разыменование указателя

К указателям может быть применена специальная

операция разыменования. Операция разыменования

позволяет обратиться к значению, размещающемуся в памяти по заданному адресу.

Для обозначения данной операции используется символ «звездочка» (*). «Звездочка» ставится перед указателем, через который выполняется обращение к значению в памяти.

Операция разыменования возвращает значение в соответствии с типом данных указателя. Следует отметить, что операцию разыменования нельзя применять к нетипизированным указателям void* без их приведения к определенному типу.

Операция разыменования может использоваться не только для чтения, но и для изменения данных, размещающихся в памяти.

23

Пример 1:

// Объявляем целую переменную Value1: int Value1 = 10;

//Объявляем указатель pValue1

//и присваиваем ему адрес переменной Value1: int* pValue1 = &Value1;

//Объявляем целую переменную Value2

//и присваиваем ей значение, хранящееся по

//адресу pValue1 (значение переменной Value1):

int Value2 = *pValue1;

cout << Value2; // 10

Пример 2:

double value = 0.01;

cout << *(&value); // 0.01

Пример 3:

//Используем указатель pX

//для изменения значения переменной X

int X = 10; int *pX = &X; *pX += 10;

cout << X; // 20

Пример 4:

//Указатель ptr типа данных void* используется

//для работы с разнотипными значениями

int

a = 1;

double

b = 1.5;

void*

ptr;

ptr = &a;

//

1

cout << *( (int*)ptr );

ptr = &b;

 

 

*( (double*)ptr ) /= 5;

//

0.3

cout << b;

24

3.3.2. Арифметические операции и операции сравнения

К указателям могут применяться некоторые арифметические операции и операции сравнения (таблица 1). Кроме того, к указателям допускается применять операторы инкремента и декремента.

Таблица 1. Операции с указателями

Операция

Выражение

 

Результат

Описание

 

Равно (==)

указатель1 ==

true/

Сравнивает два указателя на

 

 

указатель2

 

false

равенство адресов

 

Не

равно

указатель1 !=

true/

Сравнивает два указателя на

(!=)

 

указатель2

 

false

неравенство адресов

 

Меньше

указатель1

<

true/

Возвращает

 

истину,

если

(<)

 

указатель2

 

false

адрес указателя 1 меньше

 

 

 

 

 

адреса указателя 2

 

Меньше

указатель1 <=

true/

Возвращает

 

истину,

если

или

равно

указатель2

 

false

адрес указателя 1 меньше

(<=)

 

 

 

 

или равен адресу указателя

 

 

 

 

2

 

 

 

Больше (>)

указатель1

>

true/

Возвращает

 

истину,

если

 

 

указатель2

 

false

адрес указателя 1 больше

 

 

 

 

 

адреса указателя 2

 

Больше

указатель1 >=

true/

Возвращает

 

истину,

если

или

равно

указатель2

 

false

адрес указателя 1 больше

(>=)

 

 

 

 

или равен адресу указателя

 

 

 

 

2

 

 

 

Вычитание

указатель1

целое

Вычисляет

 

количество

()

 

указатель2

 

число

элементов

заданного

типа

 

 

 

 

 

между указателями

 

Вычитание

указатель

указатель

Вычисляет

 

указатель,

()

 

целое_число

 

 

отстоящий

от

заданного на

Сложение

указатель

+

указатель

определенное

количество

элементов (в соответствии с

(+)

 

целое_число

 

 

 

 

 

типом указателя)

 

Инкремент

указатель++

 

указатель

Вычисляет

 

указатель,

(++)

 

++указатель

 

 

отстоящий от заданного на 1

Декремент

указатель--

 

указатель

элемент

 

 

 

(--)

 

--указатель

 

 

 

 

 

 

25

Операции сравнения позволяют проверить равенство/неравенство адресов, содержащихся в указателях, а также определить взаимное расположение значений в памяти.

Пример 1:

//Убедимся, что указатели ptr1 и ptr2

//ссылаются на одну и ту же область памяти

int

a

=

1;

int*

ptr1 =

&a;

int* ptr2 =

&a;

if (ptr1 ==

ptr2)

cout << "Указатели ссылаются на одну переменную";

Пример 2:

//Убедимся, что указатели ptr1 и ptr2

//ссылаются на разные области памяти

int

a

=

1;

int

b

=

1;

int*

ptr1 =

&a;

int* ptr2 =

&b;

if (ptr1 !=

ptr2)

cout << "Указатели ссылаются на разные переменные";

Пример 3:

//Определим, в каком порядке

//в памяти размещаются переменные a и b

int

a

=

1;

int

b

=

1;

int*

ptr1 =

&a;

int*

ptr2 =

&b;

cout

<< ptr1 << "\n";

cout << ptr2 << "\n"; if (ptr1 < ptr2)

cout << "ptr1 < ptr2";

26

В результате сложения указателя с целым числом N получается новый адрес, опережающий исходный на M

байт, причем: M = N*размер_типа_данных_указателя.

Например, при добавлении к указателю типа double* целого числа 2, мы сместимся в памяти на 16 байт (16 = 2*8, 8 – размер типа данных double). Аналогичным образом работает операция вычитания (целого числа) и операторы инкремента/декремента.

Использование арифметических операций, а также операторов инкремента/декремента позволяет перемещаться в памяти от одного элемента массива к другому, например:

double X[]={1.1, 2.5, 3.7, 4, 5}; double* ptr = &(X[0]);

cout << ptr << " " << *ptr << "\n"; ptr++;

cout << ptr << " " << *ptr << "\n"; ptr = ptr+2;

cout << ptr << " " << *ptr << "\n"; ptr = ptr-3;

cout << ptr << " " << *ptr << "\n";

Результаты:

002EF794 1.1 002EF79C 2.5 002EF7AC 4 002EF794 1.1

Операция вычитания указателя из указателя может применяться в том случае, если оба указателя имеют одинаковый тип данных. Результатом вычитания указателя из указателя является целое число N, определяющее количество элементов заданного типа, размещающихся в памяти между двумя адресами. Число N определяется как отношение количества байт, разделяющих в памяти два адреса, к размеру типа данных указателя:

27

int Y[10];

int* ptr1 = &(Y[2]); int* ptr2 = &(Y[5]); cout << ptr1 << endl; cout << ptr2 << endl; int x = ptr2 - ptr1; cout << x;

Результаты:

0012FE90

0012FE9C

3

3.4. Особенности применения указателей

3.4.1. Значение NULL

Указатель может содержать специальное значение NULL. Значение NULL соответствует нулевому адресу и говорит о том, что указатель не ссылается на какую-либо ячейку памяти (другими словами, ни на что не указывает).

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

int* A = NULL;

cout << *A; // !!! Ошибка (Access violation) // Разыменовывать адрес NULL нельзя

3.4.2. Указатели и массивы

Имя объявляемого массива всегда ассоциируется компилятором с адресом его первого элемента. Таким образом, идентификатор, обозначающий имя массива, одновременно является и указателем.

Пример:

int A[6] = {0,1,2,3,4,5}; cout << A;

Возможный результат:

0012FE98

Разыменовывая идентификатор массива, мы получаем доступ к элементу с индексом 0:

28

int A[5] = {10,20,30,40,50}; cout << *A; // cout << A[0];

Прибавляя к указателю целые числа, можно перемещаться по объектам массива:

int A[5] = {10,20,30,40,50}; cout << *(A+2); // cout << A[2];

В следующем примере проинициализируем массив квадратами индексов его элементов, используя адрес первого элемента, а затем выведем все элементы на экран:

const int N=10; int A[N];

for (int i=0; i<N; i++)

{

int* ptr = A+i; *ptr = i*i;

}

for (int i=0; i<N; i++) cout << A[i] << " ";

Результат:

0 1 4 9 16 25 36 49 64 81

3.4.3. Применение к указателям оператора sizeof

Как и к любой другой переменной, к указателям можно применять оператор sizeof. Для явно объявленных указателей оператор sizeof вернет их размер (2 или 4 байта, в зависимости от принятой модели памяти).

Следует отметить, что для идентификатора, объявленного как массив, оператор sizeof вернет размер массива, а не указателя:

int X[] = {1, 2, 3};

cout << sizeof(X) << "\n"; // 12 int* ptr = X;

cout << sizeof(ptr) << "\n"; // 4

3.4.4. Указатели на указатели

Указатель может ссылаться на переменную, которая, в свою очередь, также является указателем. В таком случае,

29