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

Введение

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

- массив элементов заданного типа;

- указатель на объект заданного типа;

- функция, возвращающая значение заданного типа.

Язык С предусматривает два типа указателей – указатели на объекты и указатели на функции, причем указатели на объекты могут быть типизированными или без типа.

Каждая переменная в программе – это объект, имеющий имя и значение. В машине имя переменной соответствует адресу участка памяти, а значение – содержимому памяти. Чтобы получить адрес переменной в явном виде используют унарный оператор &. Оператор неприменим к выражениям, константам, битовым полям, регистровым переменным или внешним объектам (файлам).

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

Опр: Указатель – это переменная, значением которой служит либо адрес объекта некоторого типа, либо пустой адрес NULL.

Для определения и описания указателей используется унарный оператор косвенной адресации "*":

char *c;

int *i;

float *f;

Здесь c,i,f – переменные-указатели на объекты символьного, целого и вещественного типов соответственно - машинные адреса этих объектов. Операндом косвенной адресации всегда является указатель. Результат этой операции – содержимое объекта, который адресует указатель. В нашем случае *c – символьная переменная, *i – переменная целого типа, а *f- переменная вещественного типа.

3.3 Действия с указателями. Адреса указатели и массивы

Допустимы следующие основные операции над указателями:

- объявление указателя;

- инициализация указателя;

- присваивание;

- раскрытие ссылки;

- получение адреса самого указателя;

- унарные операции изменения значения указателя;

- аддитивные операции;

- оператции сравнения.

Рассмотрим эти операции подробнее на примерах.

char *c,ch='A';/

int *i,*k,ii,kk,date=1998;

float *f,ff=123.456;

i=&date; /*однотипное присваивание*/

*i=date;/* раскрытие ссылки слева*/

kk=*k;/*раскрытие ссылки справа*/

i=k;/*однотипное присваивание */

c=NULL;/*приваивание пустого адреса*/

int a=3;//Инициализация целой переменной

int* b=&a;// Инициализация указателя адресом целой переменной

int *c(&a);// Инициализация указателя адресом целой переменной

int *d=c;// Инициализация указателя другим указателем

Замечание: Инициализация указателей при объявлении является средством повышения надежности кода. Если неясно, какой адрес должен быть выбран для содержимого объявляемого указателя, то допустима инициализация указателя нулевым значением или макросом NULL:

int* j = NULL;//Допустимо стандартом 1998

int*k=0; //Допустимо стандартом 1998

Можно объявлять:

- не константые указатели на не константы:

тип * имя_указателя; - можно изменять и указатель и переменную, на которую он указывает;

- не константные указатели на константы:

const тип* имя_указателя; - можно изменять указатель, но нельзя изменять переменную, на которую он указывает;

- константные указатели на не константы:

тип* const имя_указателя; нельзя изменять указатель, но можно изменять переменную, на которую он указывает;

- константные указатели на константы:

const тип* const имя_указателя; нельзя изменять указатель, нельзя изменять переменную, на которую он указывает.

Пример:

int c=10;

const int d=20;

int *const p1 = &c;//OK

int *const p2 = &d;//NOK – здесь указатель константный, но указывать он может на переменную, а не на константу.

Иногда требуется совместить адреса объектов разных типов. В этом случае используется механизм "приведения типов":

char *c;

int *k;

c=(char *)k;/*c указывает на целое как на символ*/

int a=6;

int *pa =&a;

void* vpa = (void*)&a;//Преобразование int* в void* стиль С

void* vp= (void*)pa;//Преобразование int* в void* стиль С

void* vp= reinterpret_cast<void*>pa;//Преобразование int* в void* стиль С++

Подобно другим переменным, указатели имеют имя, адрес в памяти и значение. Поэтому выражение &ИМЯ_УКАЗАТЕЛЯ определяет, где в памяти размещен указатель:

char *c;

void *a;

a=&c;

С помощью унарных арифметических операторов -– и ++ значения указателей меняются по разному, в зависимости от типа данных указателя. Единицей в этом случае служит размер в байтах, занимаемый объектом конкретного типа. Оператор инкремента приведет к увеличению значения адреса на 1 для типа char, 2 для типа int, 4 для типа float и т.д.

Аддитивные операторы применимы к указателям с некоторыми ограничениями. Нельзя суммировать указатели, но можно прибавить целую величину. Вычисляемое значение снова зависит от типа объекта, с которым связан указатель, как и в предыдущем случае.

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

Тип разности указателей определяется по разному в зависимости от компилятора. Поэтому в заголовочном файле stddef.h определено имя ptrdiff_t с помощью которого обозначается тип разности в конкретной реализации.

Пример

#include <stdio.h>

#include <stddef.h>

void main()

{

int x[5];/*массив из 5 элементов*/

int *i,*k;/* */

ptrdiff_t j;

i=&x[0];

k=&x[4];

j=k-i;

printf("\n j=%d",(int)j);/*явное преобразование типа*/

}

Результат j=4.

Арифметические операторы и указатели

Унарные адресные операторы '*' и '&' имеют более высокий приоритет, чем арифметические операторы. Рассмотрим пример.

float a=4.0, *u, z;

u=&z;

*u=5;

a=a+*u+1;

Результат: a=10, z=5, u не изменилось.

При использовании адресной операторы '*' рекомендуется применять круглые скобки для предотвращения случайного сочетания знаков деления и разыменования:

a/*u; /* символ начала комментария*/

a/(*u); /* корректно*/

Унарные операторы '*','--','++' имеют одинаковый приоритет и при размещении рядом выполняются справа налево.

Пример.

#include <stdio.h>

#define print(y,x) printf("\n"#y" равно %d "#x" равно %u",y,x)

void main(void)

{

int x[4]={0,2,4,6},*i,y;

i=&x[0]; /*i адрес x[0]*/

y=*i; /*y=0 i=&x[0]*/

print(y,i);

y=*i++; /*y=0 i=&x[1]*/

print(y,i);

y=++*i; /*y=3 i=&x[1]*/

print(y,i);

y=*++i; /*y=4 i=&x[2]*/

print(y,i);

y=(*i)++; /*y=4 i=&x[2]*/

print(y,i);

y=++(*i); /*y=6 i=&x[2]*/

print(y,i);

}

Результаты работы для BORLAND C++ 3.1

y равно 0 i равно 65518

y равно 0 i равно 65520

y равно 3 i равно 65520

y равно 4 i равно 65522

y равно 4 i равно 65522

y равно 6 i равно 65522

Указатели и операторы сравнения

К указателям можно применять операторы сравнения и использовать указатели в отношениях. Но сравнивать указатели можно только либо с другими указателями того же типа, либо с константой NULL, либо с нулем. Допустимые действия над указателями сведем в таблицу.

Таблица 3.3.1 Таблица допустимых действий с указателями С/С++

ptr1 == ptr2 сравнение на равенство
ptr1!= ptr2 сравнение на неравенство
ptr1 < ptr2 сравнение на меньше
ptr1 <= ptr2 сравнение на меньше и равно
ptr1 > ptr2 сравнение на больше
ptr1 >= ptr2 сравнение на больше и равно
ptr1 - ptr2 вычисление числа элементов между ptr2 и ptr1
ptr1 + int_val вычисление указателя смещенного вверх
ptr1 – int_val вычисление указателя смещенного вниз

Указатели и доступ к элементам массивов

По определению указатель это либо объект со значением "адрес объекта" или "адрес функции", либо выражение, позволяющее получить адрес объекта или функции. Обычно указатели-выражения называют указателями-константами или адресами, а указатели-объекты называют указателями-переменными или просто указателями. Рассмотрим фрагмент:

int x,y;

int *p=&x;

p=&y;

Здесь p – указатель-объект, а &x,&y – указатели-выражения – константные адреса.

В соответствии с синтаксисом языка Си имя массива без индексов является адресом его первого элемента. Начальный адрес массива определяется компилятором в момент описания массива и такой адрес никогда не может быть переопределен. Следовательно, имя массива не может использоваться в левой части оператора присваивания. С другой стороны, прибавив к имени массива целую величину, можно получить адрес соответствующего элемента массива. Для оператора индексирования элемента массива можно записать:
E1[E2]->*(E1+E2). Здесь Е1 – имя массива, Е2 – целое. Для многомерного массива правила остаются теми же:

E[n][m][k]->*(E[n][m]+k)->*(*(E[n]+m)+k)->*(*(*(E+n)+m)+k).

3.4 Работа с динамической памятью в С/С++. Массивы динамической памяти

Работа с динамической памятью в С/С++

Опр. Память, выделяемая для размещения объектов программы во время ее работы, называется динамической.

В заголовочных файлах stdlib.h(STANDART) и alloc.h (BORLAND) описаны библиотечные функции С для работы с динамической памятью.

Таблица 3.4.1 Функции С работы с динамической памятью

Функция Прототип и описание
malloc void *malloc(unsigned s); Возвращает указатель на начало блока динамической памяти размером s байт. При неудачном завершении возвращает NULL.
calloc void *calloc(unsigned n, unsigned m); Возвращает указатель на начало блока обнуленной динамической памяти размером s=n*m байт. При неудачном завершении возвращает NULL.
realloc void *realloc(void *b1,unsigned ns); Изменяет размер блока ранее выделенной памяти до размера ns байт. b1 – адрес начала изменяемого блока. Если b1=NULL функция выполняется как malloc.
free void *free(void *bl); Возвращает системе ранее выделенный блок динамической памяти. b1 – адрес первого байта блока.

Функции malloc(), calloc() и realloc() выделяют программе динамическую память и возвращают адрес начала выделенного участка. Для универсальности тип возвращаемого значения каждой из этих функций есть пустой указатель void*. Тип этого указателя можно изменить операцией явного преобразования типа (тип *).

Функция free() решает обратную задачу. Заметим, что приведение указателя к типу void производится автоматически, поэтому в качестве фактического параметра в этой фукции допустим указатель любого типа.

Кроме функций C в С++ есть два оператора new и delete, которые решают аналогичную задачу.

Синтаксическими формами для этих операторов служат следующие формы:

1. pv = new type;//Выделение динамической памяти

2. delete pv;//Освобождение динамической памяти

3. pv = new type (initial_val);// Выделение динамической памяти с инициализацией значением

4. pv = new type[initial size];// Выделение динамической памяти с инициализацией размером

5. delete [] pv; //Освобождение динамической памяти одномерного массива.

Здесь pv – указатель на переменную типа type, initial_val – начальное значение переменной.–

Массивы динамической памяти в С/С++

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

Отметим ряд отличий динамических массивов от статических. Статический массив размещается в стеке функции, а динамический – в куче. Размер статического массива можно определить операцией sizeof, а размер динамического массива так определить нельзя. Пример.

#include <stdio.h>

#include <stdlib.h>

void main()

{

int m_static[10],i,j,k;

int *m_dynamic = (int *) malloc (10*sizeof(int));

printf("\nРазмер статического массива = %d",sizeof(m_static));

printf("\nРазмер динамического массива = %d", sizeof(m_dynamic));

free(m_dynamic);

}

Результат будет иметь вид:

Размер статического массива = 40

Размер динамического массива = 4

Независимо от значения параметра функции malloc() результат выражения sizeof(pointer) всегда будет равен размеру указателя.

Выделяя динамическую память для массивов важно контролировать возможность такого выделения. Это можно сделать, сравнивая возвращаемый результат функций malloc(), realloc() и calloc() c NULL.

Пример.

#include <stdio.h>

#include <stdlib.h>

#include <process.h>

#define size 100

int main(void)

{

int *data;

if ((data = (int *) malloc(size*sizeof(int))) == NULL)

{

printf("Недостаточно памяти для данных!!!\n");

exit(1); /* выход из программы*/

}…

Многомерные динамические массивы

Массивы в С/С++ бывают только одномерные, но элементами массивов могут быть сами массивы. Решим задачу, определения матрицы – произведения двух матриц:

int main()

{

int i,j,n;

cout<<”Размеры матриц A,B,R”;

cin>>n;

float**a = new float*[n];

for(j=0;j<n;j++) a[j]= new float [n];

float**b = new float*[n];

for(j=0;j<n;j++) b[j]= new float [n];

float**r = new float*[n];

for(j=0;j<n;j++) r[j]= new float [n];

for(i=0;i<n;i++)

{

for(j=0;j<n;j++)

cin >> a[i][j];

cout<<endl;

}

for(i=0;i<n;i++)

{

for(j=0;j<n;j++)

cin >> b[i][j];

cout<<endl;

}

for(i=0;i<n;i++)

for(j=0;j<n;j++)

{

float s =0; for(int k=0;k<n;k++) s+=a[i][k]*b[k][j]; r[i][j]=s;

}

for (i=0; i<n;i++) delete a[i]; delete [] a;

for (i=0; i<n;i++) delete b[i]; delete [] b;

for (i=0; i<n;i++) delete r[i]; delete [] r;

return 0;

}


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: