КИТ и Э КБГУ Понедельник, 06.05.2024, 22:06
Приветствую Вас Гость | RSS
Меню сайта

Наш опрос
Оцените мой сайт
Всего ответов: 118

Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0

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

  1. Указатели и адреса. Указатели и массивы.
  2. Адресная арифметика.
  3. Символьные указатели функций. Указатели на функции.
  4. Массивы указателей. Указатели на указатели.
  5. Динамические переменные.

Практическая работа 14. Обработка одномерных массивов.

Практическая работа 15 Обработка многомерных массивов.

Практическая работа 16. Работа с динамическими структурами. Программирование операций адресной арифметики.

Практическая работа 17. Обработка массивов указателей.

Практическая работа 18. Работа с указателями на функции.

 

1. Указатели и адреса. Указатели и массивы

Указатели широко применяются в Си — отчасти потому, что в некоторых случаях без них просто не обойтись, а отчасти потому, что программы с ними обычно короче и эффективнее.

Указатель — это переменная, содержащая адрес переменной.

1. Указатели и адреса

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

один байт может хранить значение типа char,

двухбайтовые ячейки могут рассматриваться как целое типа short, а четырехбайтовые — как целые типа long.

Указатель — это группа ячеек (как правило, две или четыре), в которых может храниться адрес.

Так, если с имеет тип char, а р — указатель на с, то унарный оператор & выдает адрес объекта, так что инструкция

р = &с;

присваивает переменной p адрес ячейки c (говорят, что р указывает на с). Оператор & применяется только к объектам, расположенным в памяти: к переменным и элементам массивов.

Его операндом не может быть ни выражение, ни константа, ни регистровая переменная.

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

Пример1. Предположим, что x и y имеют тип int, a ip — указатель на int. Следующие несколько строк придуманы специально для того, чтобы показать, каким образом объявляются указатели и как используются операторы & и *.

int х = 1, у = 2, z[10];

int *ip;                              // ip - указатель на int

ip = &x;                            // теперь ip указывает на х

y = *ip;                             // у теперь равен 1

*ip =0;                              // х теперь равен 0

ip = &z[0];                       // ip теперь указывает на z[0]

Указанный принцип применим и в объявлениях функций.

Пример2.

1. Запись

double *dp, atof (char *);

означает, что выражения *dp и atof(s) имеют тип double, а аргумент функции atof есть указатель на char.

2. Если ip указывает на x целочисленного типа, то *ip можно использовать в любом месте, где допустимо применение х; например,

*ip = *ip+ 10;

увеличивает *ip на 10.

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

у = *ip + 1

берет то, на что указывает ip, и добавляет к нему 1, а результат присваивает переменной y. Аналогично

*ip += 1

увеличивает на единицу то, на что указывает ip; те же действия выполняют

++*ip и (*ip)++

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

Это обусловлено тем, что унарные операторы * и ++ имеют одинаковый приоритет и порядок выполнения — справа налево.

Так как указатели сами являются переменными, в тексте они могут встречаться и без оператора косвенного доступа. Например, если iq есть другой указатель на int, то

iq = ip

копирует содержимое ip в iq, чтобы ip и iq указывали на один и тот же объект.

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

В Си существует связь между указателями и массивами, и связь эта настолько тесная, что эти средства лучше рассматривать вместе.

Любой доступ к элементу массива, осуществляемый операцией индексирования, может быть выполнен с помощью указателя.

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

Объявление int a[10];

определяет массив а размера 10, т. е. блок из 10 последовательных объектов с именами а[0], а[1],...,а[9].

Запись а[i] отсылает нас к i-му элементу массива. Если pa есть указатель на int, т. е. объявлен как

int *pa;

то в результате присваивания

ра = &а[0];

pa будет указывать на нулевой элемент а, иначе говоря, pa будет содержать адрес элемента а[0].

Теперь присваивание

х = *ра;

будет копировать содержимое а[0] в х.

Если ра указывает на некоторый элемент массива, то ра+1 по определению указывает на следующий элемент, pa+i — на i-й элемент после ра, a pa-i — на i-й элемент перед ра.

Таким образом, если ра указывает на а[0], то

*(pa+1)

есть содержимое а[1], а+i -адрес a[i], а *(pa+i) — содержимое a[i].

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

Смысл слов "добавить 1 к указателю", как и смысл любой арифметики с указателями, состоит в том, чтобы ра+1 указывал на следующий объект, а ра+1 — на 1-й после ра.

Между индексированием и арифметикой с указателями существует очень тесная связь. По определению значение переменной или выражения типа массив есть адрес

нулевого элемента массива.

После присваивания

ра = &а[0];

ра и а имеют одно и то же значение.

Поскольку имя массива является синонимом расположения его начального элемента, присваивание ра=&а[0] можно также записать в следующем виде:

pa = a

Еще более удивительно то, что а[i] можно записать как *(а+i).

Вычисляя а[i], Си сразу преобразует его в *(a+i); указанные две формы записи эквивалентны. Из этого следует, что полученные в результате применения оператора & записи &а[i] и a+i также будут эквивалентными, т. е. и в том и в другом случае это адрес 1-го элемента после а.

С другой стороны, если ра — указатель, то его можно использовать с индексом, т. е. запись pa[i] эквивалентна записи *(pa+i).

Короче говоря, элемент массива можно изображать как в виде указателя со смещением, так и в виде имени массива с индексом.

Между именем массива и указателем, выступающим в роли имени массива, существует одно различие.

Указатель — это переменная, поэтому можно написать ра=а или ра++. Но имя массива не является переменной, и записи вроде а=ра или а++ не допускаются.

2. Адресная арифметика

Под адресной арифметикой понимаются действия над указателями, связанные с использованием адресов памяти. Рассмотрим операции, которые можно применять к указателям.

1.Присваивание. Указателю можно присвоить значение адреса. Любое число, присвоенное указателю, трактуется как адрес памяти.

Пример 1.

int *u,*adr;

int N;

u=&N;                              // указателю присвоен адрес переменной N

adr=0х00FD;                   // указателю присвоен 16-ти ричный адрес

2. Взятие адреса. Так как указатель является переменной, то для получения адреса памяти, где расположен указатель, можно использовать операцию взятия адреса &

Пример 2.

int *a,*b;

a=&b;                               // указателю a присвоен адрес указателя b

3. Косвенная адресация. Для того, чтобы получить значение, хранящееся по адресу, на который ссылается указатель, или послать данное по адресу,

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

Пример 3.

int *uk;

int n;

int m=5;

uk=&m;                            // uk присвоен адрес переменной m

n=*uk;                              // переменная n примет значение 5

*uk=13;                            // переменная m примет значение 13

4.Преобразование типа. Указатель на объект одного типа может быть преобразован в указатель на другой тип.

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

Операция преобразования типа указателя применяется в виде (_тип_ *)_указатель_

Пример 4.

int i, *ptr;

i=0x8e41;

ptr=&i;

printf("%d\n", *ptr);                                            // печатается значение int:-29119

printf("%d\n", *((char *)ptr));                             // ptr преобразован к типу сhar, извлекается 1 байт и его двоичный код печатается в виде десятичного числа, - печатается: 65

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

через этот указатель.

5. Определение размера. Для определения размера указателя можно использовать операцию размер в виде sizeof(_указатель_).

Размер памяти, отводимой компилятором под указатель, зависит от модели памяти. Для близких указателей операция sizeof дает значение 2, а для дальних 4.

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

7. Индексация. Указатель может индексироваться применением к нему операции индексации, обозначаемой в Си квадратными скобками [].

Индексация указателя имеет вид:

_указатель_[_индекс_], где _индекс_ записывается целочисленным выражением.

Операция индексирования является бинарной и применяется к адресу и индексу. Возвращаемым значением операции индексации является данное, находящееся по адресу,

смещенному в большую или меньшую сторону относительно адреса, содержащегося в указателе в момент применения операции. Этот адрес определяется так:

(адрес в указателе) + (значение _индекс_) * sizeof(_тип_),

где _тип_ - это тип указателя.

Из этого адреса извлекается или в этот адрес посылается, в зависимости от контекста применения операции, данное, тип которого интерпретируется в соответствии с типом указателя.

Пример 5.

int *uk1;

int b,k;

uk1=&b;                                                         // в uk1 адрес переменной b

k=3;b=uk1[k];                                                 // переменной b присваивается значение int, взятое из адреса на 6 большего, чем адрес переменной b; в uk1 адрес не изменился

uk1[k]=-14;                                                    // в адрес на 6 больший, чем адрес переменной b, посылается -14

 Операция индексации не изменяет значение указателя, к которому она применялась.

8. Увеличение/уменьшение. Если к указателю применяется операция увеличения ++ или уменьшения --, то значение указателя увеличивается или уменьшается на размер объекта,

который он адресует

Пример 6.

 long b;                             // b - переменная типа int длиной 4 байта

long *ptr;                          // ptr - указатель на объект int длиной 4 байта

ptr=&b;                            // в ptr адрес переменной b

ptr++;                              // в ptr адрес увеличился на 4

ptr--;                                // в ptr адрес уменьшился на 4

9. Сложение. Одним из операндов операции сложения может быть указатель, а другим операндом обязательно должно быть выражение целого типа.

 Операция сложения вырабатывает адрес, который определяется следующим образом: 

(адресв указателе) + (значение int_выражения)*sizeof(_тип_),

где _тип_ тип данных, на которые ссылается указатель.

 Пример 7.

double d;

int n;

double *uk;

uk=&d;                             // в uk адрес переменной d

n=3;

uk=uk+n;                           // в результате выполнения операции сложения, а затем операции присваивания в uk новый адрес на 24 больше, чем предыдущий

uk=n+uk;                           // в uk адрес увеличился еще на 24

10. Вычитание. Левым операндом операции вычитания должен быть указатель, а правым должно быть выражение целого типа.

Операция вычитания вырабатывает адрес, который определяется так:

(адрес в указателе) - (значение int_выражения)*sizeof(_тип_).

К указателям можно применять только описанные операции и операции, которые выражаются через них, например, разрешается к указателю применить операцию uk+=n;

так как ее можно выразить через uk=uk+n.

Операции, недопустимые с указателями:

 - сложение двух указателей;

- вычитание двух указателей на различные объекты;

- сложение указателей с числом с плавающей точкой;

- вычитание из указателей числа с плавающей точкой;

- умножение указателей;

- деление указателей;

- поразрядные операции и операции сдвига.

3. Символьные указатели функций. Указатели на функции

3.1. Символьные указатели функций

Строковая константа, написанная в виде "Я строка" есть массив символов. Во внутреннем представлении этот массив заканчивается нулевым символом '\0',

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

Чаще всего строковые константы используются в качестве аргументов функций, как, например, в

printf("здравствуй, мир\n");

Когда такая символьная строка появляется в программе, доступ к ней осуществляется через символьный  указатель; printf получает указатель на начало массива символов.

Точнее, доступ к строковой константе осуществляется через указатель на ее первый элемент.

Строковые константы нужны не только в качестве аргументов функций. Если, например, переменную pmessage объявить как

char *pmessage

то присваивание

pmessage = "now is the time";

поместит в нее указатель на символьный массив, при этом сама строка не копируется, копируется лишь указатель на нее.

Операции для работы со строкой как с единым целым в Си не предусмотрены.

Существует важное различие между следующими определениями:

char amessage[] = "now is the time";                  // массив

char *pmessage = "now is the time";                   // указатель

amessage — это массив, имеющий такой объем, что в нем как раз помещается указанная последовательность символов и '\0'.

Отдельные символы внутри массива могут изменяться, но amessage всегда указывает на одно и то же место памяти.

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

Пример 1. Дополнительные моменты, связанные с указателями и массивами, проиллюстрируем на несколько видоизмененных вариантах двух полезных программ, взятых нами из стандартной библиотеки. Первая из них, функция strcpy(s, t), копирует строку t в строку s. Хотелось бы написать прямо s=t, но такой оператор копирует указатель, а не символы.

Чтобы копировать символы, нам нужно организовать цикл.

Первый вариант strcpy, с использованием массива, имеет следующий вид:

/* strcpy: копирует t в s; вариант с индексируемым массивом*/

void strcpy(char *s, char *t)

{ int i;

i = 0;

while ((s[i] = t[i]) != '\0' )

i++; }

/* strcpy: копирует t в s: версия 1 (с указателями) */

void strcpy(char *s, char *t)

{ while ((*s = *t) != '\0' )

{ s++;

   t++;} }

На практике strcpy так не пишут. Опытный программист предпочтет более короткую запись:

/* strcpy: копирует t в s; версия 2 (с указателями) */

void strcpy(char *s, char *t)

{ while ((*s++ = *t++) != '\0'); }

Приращение s и t здесь осуществляется в управляющей части цикла. Значением *t++ является символ, на который указывает переменная t перед тем, как ее значение будет увеличено; постфиксный оператор ++ не изменяет указатель t, пока не будет взят символ, на который он указывает. То же в отношении s: сначала символ запомнится в позиции, на которую указывает старое значение s, и лишь после этого значение переменной s увеличится. Пересылаемый символ является одновременно и значением, которое сравнивается с '\0' . В итоге копируются все символы, включая и заключительный символ '\0'.

Замечание: сравнение с '\0' здесь лишнее (поскольку в Си ненулевое значение выражения в условиитрактуется и как его истинность), мы можем сделать еще одно и последнее сокращение текста программы:

/* strcpy: копирует t в s; версия 3 (с указателями) */

void strcpy(char *s, char *t)

{ while (*s++ = *t++); }

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

3.2. Указатели и аргументы функций

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

В программе сортировки нам понадобилась функция swap, меняющая местами два неупорядоченных элемента.

Однако недостаточно написать

swap(a, b);

где функция swap определена следующим образом:

void swap(int x, int y) /* НЕВЕРНО */

{

int temp;

temp = x;

x = y;

y = temp;

}

Поскольку swap получает лишь копии переменных a и b, она не может повлиять на переменные а и b той программы, которая к ней обратилась.

Чтобы получить желаемый эффект, вызывающей программе надо передать указатели на те значения, которые должны быть изменены:

swap(&a, &b);

Так как оператор & получает адрес переменной, &a есть указатель на a. В самой же функции swap параметры должны быть объявлены как указатели, при этом доступ к значениям параметров будет осуществляться косвенно.

void swap(int *px, int *py) /* перестановка *рх и *рy */

{

int temp;

temp = *рх;

*рх = *py;

*рy = temp;

}

Аргументы-указатели позволяют функции осуществлять доступ к объектам вызвавшей ее программы и дают возможность изменить эти объекты.

3.3. Указатели на функции

Функция располагается в памяти по определенному адресу, который можно присвоить указателю в качестве его значения. Адресом функции является ее точка входа. Именно этот адрес используется при вызове функции. Так как указатель хранит адрес функции, то она может быть вызвана с помощью этого указателя. Он позволяет также передавать ее другим функциям в качестве аргумента.

В программе на С адресом функции служит ее имя без скобок и аргументов (это похоже на адрес массива, который равен имени массива без индексов).

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

Пример 2. 

 void error(char* p) { /* ... */ }

 void (*efct)(char*);                     // указатель на функцию

void f()

{ efct = &error;                            // efct настроен на функцию error

  (*efct)("error");                          // вызов error через указатель efct

  }

Для вызова функции с помощью указателя (efct в нашем примере) надо вначале применить операцию косвенности к указателю - *efct. Поскольку приоритет операции вызова () выше, чем приоритет косвенности *, нельзя писать просто *efct("error"). Это будет означать *(efct("error")), что является ошибкой. По той же причине скобки нужны и при описании указателя на функцию. Однако, писать просто efct("error") можно, т.к. транслятор понимает, что efct является указателем на функцию, и создает команды, делающие вызов нужной функции.

4. Массивы указателей. Указатели на указатели

4.1. Массивы указателей

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

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

double  *p[20];

 Переменную p следует понимать как массив (операция []), каждым элементом которого является указатель на переменную типа double (операция *). Переменная p является массивом указателей как тип данных, но не является таковой как структура данных. Чтобы превратиться в структуру данных, она быть дополнена указуемыми переменными и указателями (связями).

Многообразие вариантов реализации массивов указателей возникает по нескольким причинам:

  • cам массив указателей, указуемые переменные и ссылки (указатели) могут быть заданы статически (в тексте программы), либо динамически созданы во время ее выполнения;
  • двоякая интерпретация указателя как указателя на отдельную переменную и на массив переменных (строку), позволяет создавать одномерные СД – массивы указателей на переменные и двумерные – массивы указателей на массивы (строки) таких переменных;
  • указуемые переменные могут быть «собственностью» структуры данных, однако массив указателей может ссылаться и на переменные (объекты), являющиеся составными частями других структур данных.

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

4.2. Типы данных, используемые при работе с массивами указателей

Один тип данных уже был нами упомянут – это массив указателей, переменная вида int *p[]. 

Кроме нее используется еще одни тип вида int **pp, который можно определить в общем виде как указатель на указатель.

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

Первый вариант – указатель на указатель на отдельную переменную, имеет довольно специфическое применение. Обычно от используется для передачи адреса заголовкакакой-либо структуры данных при необходимости его изменения. В формальных параметрах для этой цели предпочтительнее использование ссылки на указатель.

Пример 1.

int a=5, b=10;

int *p=&a;

int **pp=&p;

(**pp)++;  *pp=&b; **pp=0;

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

Пример 2.

int a[10]=5, b[10]=10;

int *p=a; int **pp=&p;

for (int i=0;i<10;i++) (*pp)[i]=0;

*pp=b;

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

{

(*pp)++;

**pp=i;

 }

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

Классическая интерпретация - указатель на массив указателей на отдельные объекты использует естественный порядок операций *p[i] для доступа к указуемым объектам.

Пример 3.

int a=5, b=10, с=15;

int *p[]={&a,&b,&c,NULL};

 int **pp=p;

for (int s=0,i=0;i<10;i++)  s=s+*pp[i];

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

Пример 4.

 

int a[]={5,6,7,8}, b[]={1,2,3,4}, с[]={5,2,4,8};

int *p[]={a,b,c};

int **pp=p;

for (int s=0,i=0;i<3;i++) 

for (int j=0;j<4;j++)

s=s+pp[i][j];

4.3. Статические и динамические массивы указателей

Статический массив указателей формируется при трансляции: переменные (сам массив указателей и указуемые переменные) определяются статически, как обычные именованные переменные, а указатели инициализируются. Структура данных включена непосредственно в программный код и «готова к работе».

int a1,a2,a3, *pd[] = { &a1, &a2, &a3, NULL};

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

Пример 5. Статический массив указателей программно заполняется адресами элементов статического же массива (динамически формируются только сами указатели).

int d[19], *pd[20];

for (i=0; i<19; i++) pd[i] = &d[i];

pd[i] = NULL;

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

int *p, *pd[20];

for (i=0; i<19; i++){ p = new int; *p = i;  pd[i] = p; }

pd[i] = NULL;

Наконец, если массив указателей формируется как динамическая структура данных, то динамический массив указателей создается в процессе работы программы. Операция new в качестве результата возвращает указатель на область памяти, содержащую указатели, т.е. тип int**, который запоминается в переменной того же типа.

 

int **pp, *p;

pp = new int *[20];                      // память под массив указателей из 20 указателей типа int*

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

{

p = new int;

*p = i;

pp[i] = p;                                      // можно pp[i]=new int; *pp[i]=i;

}

pp[i] = NULL;

4.4. Массив указателей. Физический и логический порядок

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

  1. pp[i] -i-й указатель в массиве;
  2. *pp[i] -значение i-ой указуемой переменной.

Алгоритмы работы с массивом указателей и обычным массивом внешне очень похожи. Разница же состоит в том, что размещение данных в обычном массиве соответствует их физическому порядку следования в памяти, а массив указателей позволяет сформировать логический порядок следования элементов в соответствии с размещением указателей на них.  Тогда изменение порядка следования (включение, исключение, упорядочение, перестановка), которое в обычном массиве заключается в перемещении самих элементов, в массиве указателей должно сопровождаться операциями над указателями на них. Очевидные преимущества возникают, когда сами указуемые переменные являются достаточно большими, либо перемещение их невозможно по каким-либо причинам (например, на них ссылаются другие части программы).

Пример 6. Для сравнения приведем функции сортировки массива и массива указателей.

//  Сортировка массива

void sort1 (double d[],int sz)

{

int i,k;

do {

for ( k=0, i=0; i<sz-1; i++)

if (d[i] > d[i+1])

{

 double c;

 c = d[i];

 d[i] = d[i+1];

 d[i+1] = c;

k=1;

}

} while (k);

}

//  Сортировка массива указателей

 void sort2 (double *pd[])

{

int i,k;

do {

for ( k=0, i=0; pd[i+1]!=NULL;i++)

if (*pd[i] > *pd[i+1])                          // Сравнение указуемых переменных

{

double *c;                                            // Перестановка указателей

c = pd[i];

 pd[i] = pd[i+1];

pd[i+1] = c;

k = 1;

}

} while (k);

}

5 Динамические переменные

5.1. Создание динамических переменных

Для создания динамических переменных служат функции malloc и calloc. Они описаны в библиотеке alloc.h.

Вид обращения к функциям:

  1. malloc(size), где size (типа unsigned) - объем памяти, который необходимо выделить;
  2. calloc(nelem,elsize), где nelem (типа unsigned) - число элементов, для которых надо выделить память; elsize (типа unsigned) - объем памяти, который необходимо выделить для каждого элемента.

Обе функции выделяют основную память и возвращают указатель на тип void, содержащий адрес выделенной области памяти. Если памяти недостаточно, функции возвращают NULL.

Функция calloc, кроме отведения памяти, присваивает каждому элементу значение 0, что очень удобно для автоматического создания массивов.

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

Но это справедливо лишь в Турбо-СИ, поэтому для переносимости программного обеспечения лучше всегда использовать явное преобразование типа.

Для определения необходимого объема памяти можно использовать унарную операцию sizeof (размер). Эта операция используется в двух формах:

1) sizeof(<выражение>)

2) sizeof(<тип>)

Частный случай выражение - константа или переменная. Результат выполнения операции - количество байтов, занимаемых операндом, то есть объем памяти в байтах, необходимый для хранения:

1) значения выражения;

2) значения заданного типа.

Пример 1. Пример иллюстрирует результат выполнения операции sizeof

#include <stdio.h>

main ()

{int b; float d[500];

printf("\nРазмер памяти под целое %d",sizeof(int));                       /* Результат: 2 */

printf("\nРазмер памяти под переменную b %d",sizeof(b));           /* Результат: 2 */

printf("\nРазмер памяти под d %d", sizeof d);                                 /* Результат: 2000 */

}

Пример 2. Пример динамического распределения памяти

/* Динамическое создание одной переменной типа int */

#include <stdio.h>

#include <alloc.h>

main()

{int *iptr;                                  /* iptr - указатель на целое */

iptr=(int *) malloc(sizeof(int));

*iptr=421;          /* *iptr - это имя динамической переменной. Ее значение равно 421.*/

printf("Содержимое iptr: %p\n",iptr);             /* Вывод: Содержимое iptr: <адрес> */

printf("Адресуемое значение: %d\n",*iptr); /* Вывод: Адресуемое значение: 421 */

}

Оператор "iptr=(int*) malloc(sizeof(int));" выполняет следующие действия:

1) выражение sizeof(int) возвращает количество байтов, требуемых для хранения переменной типа int (для компилятора Турбо-Си на IBM PC это значение равно 2);

2) функция malloc(n) резервирует n последовательных байтов доступной (свободной) памяти в компьютере, и возвращает начальный адрес размещения в памяти этой последовательности байтов;

3) выражение (int *) указывает, что этот начальный адрес есть указатель на данные типа int. Это выражение явного приведения типа. Для Турбо-Си это приведение необязательно, но для других компиляторов Си является обязательным. Из соображения переносимости программного обеспечения лучше всегда предусматривать явное приведение типов в своих программах;

4) адрес, полученный с помощью функции malloc , запоминается в iptr. Таким образом, получена динамически созданная целая переменная, к которой можно обращаться при помощи имени "*iptr".

Краткая формулировка действий: "выделить в памяти компьютера некоторый участок для переменной int, затем присвоить начальный адрес этого участка переменной iptr, являющейся указателем на переменную типа int".

Делать присваивание "*iptr=421" без предварительного присвоения адреса в iptr нельзя, так как нет гарантии, что iptr указывает на свободный участок памяти. Вообще, правило использования указателей: указатель всегда должен иметь адрес до своего использования в программе.

Пример 3. Динамическое распределение памяти. Адресная арифметика

/* Динамическое создание трех переменных типа int */

#include <stdio.h>

#include <alloc.h>

main()

{

#define n 3

int *list, i;

list=(int *) calloc(n,sizeof(int));

*list=421;

*(list+1)=53;

*(list+2)=1806;

printf("\nСписок адресов :");

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

printf("%4p ",(list+i));

printf("\nСписок значений :");

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

printf("%4d", *(list+i));

printf("\n");

}

/* list указывает на участок памяти размером 3*2=6 байтов, достаточный для хранения 3-х элементов типа int. */

/* list+i представляет адрес памяти, определяемый выражением list+(i*sizeof(int)). */

Пример 4. В примере указатель инициализируется в момент описания.

#include <alloc.h>

main()

{ int *y=(int *)malloc(sizeof(int));

/* Указатель y инициализируется в момент описания; то есть здесь y оп

Вход на сайт

Поиск

Календарь
«  Май 2024  »
ПнВтСрЧтПтСбВс
  12345
6789101112
13141516171819
20212223242526
2728293031

Архив записей

Друзья сайта
  • Официальный блог
  • Сообщество uCoz
  • FAQ по системе
  • Инструкции для uCoz

  • Copyright Fatima_Zh © 2024Бесплатный хостинг uCoz