Присваивание указателей друг другу 1 страница

p1 = p; // содержимое p копируется в p1, т.е. p1 теперь

// указывает на тот же элемент массива либо

// переменную, что и p

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

4.5. Тип void

Ключевое слово void говорит об отсутствии информации о размере элемента данных в памяти.

Практическое значение имеет только понятие указателя на тип данных void. Все остальные применения этого ключевого слова носят скорее синтаксическую нагрузку.

Объявление указателя типа void:

void *p;

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

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

Например:

unsigned a = 0x1234;

void *p = &a;

char b;

unsigned c;

b = *(char *)p; // Приводит указатель типа void к типу

// char и присваивает b значение

// переменной, на которую он указывает

printf("%.2x\n", b); // Выводит 34

c = *(unsigned *)p;

printf("%.4x\n", c); // Выводит 1234

Строка формата "%.2x\n" означает, что число будет выведено в его шестнадцатиричном представлении с двумя знаками. В случае, если число представимо меньшим количеством цифр, оно будет дополнено слева нулями до заданной длины. Аналогично "%.4x\n" означает вывод шестнадцатиричного числа с 4 знаками. Более подробную информацию о строке формата, например "%.4x\n", можно получить в справочной системе среды программирования.

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

4.6. Связь между указателями и массивами

В Си массивы и указатели тесно связаны между собой и являются практически взаимозаменяемыми.

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

Объявим переменные:

int a[5];

int *p;

Можно присвоить указателю адрес первого элемента массива двумя способами:

p = a;

p = &a[0];

Есть несколько способов доступа к элементам массива.

1. Обычный способ, через указание имени массива и индекса, например, a[3]

2. Способ указатель/смещение: например, *(p + 3). Константа 3 называется смещением и показывает, на какой элемент массива производится ссылка, т.е. значение смещения равно значению индекса массива. В выражении используются скобки, так как приоритет операции * выше, чем операции +. В случае без скобок в выражении *p+3 к значению *p прибавляется число 3 (в данном случае к значению элемента a[0] будет прибавлено 3).

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

*(p++)

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

*(--p)

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

3. Имя массива (т.к. является указателем) можно использовать в арифметике указателей. Например, выражение *(a+3) будет ссылаться на элемент массива a[3].

4. Указатели, в свою очередь, могут быть использованы вместо имен массива. Если, p=&a[0], то p[1] и a[1] обращаются к одной и той же ячейке памяти.

Выражение a+=3 является недопустимым, так как имя массива – это указатель-константа, которая всегда указывает на первый элемент массива. А здесь делается попытка изменить значение начального адреса массива.

Объявим двумерный массив:

int a[3][3];

Выражения наподобие а[0], a[1], a[2] при компиляции заменяются адресами первых элементов соответственно первой, второй и третьей строк массива, a

a[0]-> a[0][0] a[0][1] a[0][2]

a[1]-> a[1][0] a[1][1] a[1][2]

a[2]-> a[2][0] a[2][1] a[2][2]

Очевидны следующие равенства:

a[i]+j == &a[i][j]; // адрес j -го элемента i -строки массива

*(a[i]+j) == a[i][j]; // содержимое j -го элемента i -ой строки массива

*(*(a+i)+j) == a[i][j]; // содержимое j -го элемента i -ой строки массива

4.7. Динамическое распределение памяти

Для динамического распределения памяти применяются функции malloc() и free(). Их прототипы находятся в файле <stdlib.h>.

Прототип функции malloc():

void *malloc(unsigned s);

Функция возвращает указатель типа void на блок выделенной памяти длиной s байт. Если необходимого количества памяти нет в наличии – возвращает NULL.

Например:

char *p;

p = (char *) malloc(1000); // выделяет 1000 байт

Функция free() освобождает память, то есть память возвращается системе и в дальнейшем её можно выделить снова. Для высвобождения памяти, динамически выделенной ранее, используется функция free(). Её прототип:

void free(void *p);

То есть, чтобы освободить память, выделенную в предыдущем примере, нужно написать

free(p);

При динамическом распределении памяти для массивов следует описать указатель соответствующего типа и присвоить ему значение при помощи функции calloc. Вот прототип этой функции:

void *calloc(unsigned n, unsigned m);

Функция принимает два аргумента: число элементов n и размер каждого элемента m, инициализирует выделенный блок памяти нулями и возвращает указатель на его начало Если по каким-либо причинам (например, в случае нехватки физической памяти) память не может быть выделена, возвращается NULL.

Одномерный массив a[10] из элементов типа float можно создать следующим образом

float *a;

a = (float *) calloc(10,sizeof(float));

Для динамического создания двумерного массива вначале нужно распределить память для массива указателей на одномерные массивы, а затем распределять память для одномерных массивов. Пусть, например, требуется создать массив a[m][n]. Это можно сделать при помощи следующего фрагмента программы:

#include <stdlib.h>

void main()

{

double **a;

int n,m,i;

scanf("%d %d",&m,&n);

a = (double **) calloc(m,sizeof(double *));

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

a[i] = (double *) calloc(n, sizeof (double));

}

Функция realloc изменяет размер объекта, выделенного в результате предыдущего вызова malloc, calloc или realloc. Содержимое первоначального объекта сохраняется при условии, что размер вновь выделяемой памяти больше, чем первоначальный. В противном случае содержимое остается неизменным лишь в пределах размера нового объекта. Прототип функции realloc выглядит следующим образом:

void *realloc(void *ptr, unsigned size);

Функция realloc принимает два аргумета: указатель на первоначальный объект (ptr) и новый размер объекта (size). Если ptr равен NULL, realloc работает аналогично malloc. Если size равен 0, а ptr не NULL, память, выделенная для объекта, освобождается. В противном случае, если ptr не NULL и размер больше нуля, realloc пытается выделить для объекта новый блок памяти. Функция realloc возвращает либо указатель на вновь выделенную память, либо NULL.

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

Массивы могут состоять из указателей. Обычный случай такого массива – это массив строк. Элементом такого массива является строка. В качестве примера рассмотрим массив name, который может быть использован для хранения имен.

char *name[4] = {“Nata”,”Lena”,”Masha”,”Katya”};

Выражение name[4] в объявлении означает массив из четырех элементов. Элементами данного массива будут являться указатели на тип char. Каждый указатель инициализируется адресом строки, указанной в фигурных скобках.

Каждое из значений переменной-указателя (“Nata”,”Lena” и т.д.) хранится в памяти как строка символов с конечным нулевым символом.

В name[0] содержится адрес “Nata”.

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

Следующие объявления переменных

int a[] = {10,11,12,13,14,}; // массив целых чисел

int *p[] = {a, a+1, a+2, a+2, a+3, a+4}; // массив указателей

// на целые числа

int **pp = p; //указатель на указатель

порождают программные объекты, представленные на схеме рис.4.5.

Рис.4.5. Схема размещения переменных при объявлении

При выполнении операции pp-p получим нулевое значение, так как ссылки pp и p равны и указывают на начальный элемент массива указателей, связанного с указателем p (на элемент p[0]).

После выполнения операции pp+=2 схема изменится и примет вид, изображенный на рис.4.6.

Рис.4.6. Схема размещения переменных после выполнения операции pp+=2

Результатом выполнения вычитания pp-p будет 2, так как значение pp есть адрес третьего элемента массива p. Ссылка *pp-a тоже дает значение 2, так как обращение *pp есть адрес третьего элемента массива a, а обращение a есть адрес начального элемента массива a. При обращении с помощью ссылки **pp получим 12 - это значение третьего элемента массива a. Ссылка *pp++ даст значение четвертого элемента массива p т.е. адрес четвертого элемента массива a.

Если считать, что pp=p, то обращение *++pp это значение второго элемента массива a (т.е. значение 11), операция ++*pp изменит содержимое указателя p[0], таким образом, что он станет равным значению адреса элемента a[1].

Сложные обращения раскрываются изнутри. Например, обращение *(++(*pp)) можно разбить на следующие действия: *pp дает значение начального элемента массива p[0], далее это значение инкременируется ++(*p), в результате чего указатель p[0] станет равен значению адреса элемента a[1], и последнее действие это выборка значения по полученному адресу, т.е. значение 11.

Объявление переменных

int a[3][3] = { { 11,12,13 },

{ 21,22,23 },

{ 31,32,33 } };

int *pa[3] = { a, a[1], a[2] };

int *p = a[0];

порождает в программе объекты, представленные на схеме рис.4.7.

Рис.4.7. Схема размещения указателей на двумерный массив

Согласно этой схеме доступ к элементу a[0][0] можно получить по указателям a, path, pa при помощи следующих ссылок: a[0][0], *a, **a[0], *p, **pa, *p[0].

5. Функции

5.1. Общие сведения

Функция по своей сути – это подпрограмма, которая может манипулировать данными и возвращать некоторое значение. Каждая программа имеет, по крайней мере, одну функцию main(), которая при запуске программы вызывается автоматически. Функция main() может вызывать другие функции, те в свою очередь могут вызывать следующие и т.д.

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

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

Функции могут возвращать значения. После обращения к функции она может выполнить некоторые действия, а затем в качестве результата своей работы вернуть некоторое значение (возвращаемое значение), тип которого задаётся явно или неявно. Если тип функции не указан, то неявно полагается, что функция возвращает данные типа int.

Таким образом, запись:

int myFunction();

означает, что функция myFunction возвращает целочисленное значение.

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

int myFunction(int x, float y);

Это объявление означает, что функция myFunctionне только возвращает целое число, но и принимает два значения в качестве параметров: целочисленное и вещественное. Параметр описывает тип значения, которое будет передано функции при её вызове. Фактические значения, передаваемые в функцию, называются параметрами функции. Параметры функции могут быть разного типа.

int a = myFunction(3, 9.9);

Здесь переменная целого типа а инициализируется значением, возвращаемым функцией myFunction, а в качестве параметров этой функции передаются значения 3 и 9.9.

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

Использование функций в программе требует, чтобы функция сначала была объявлена, а затем определена. Посредством объявления функции компилятору сообщается её имя, тип возвращаемого значения и параметры. Ни одну функцию нельзя вызвать в программе, если выше по тексту программы для неё не было записано ни прототипа, ни реализации. Объявление функции называется прототипом.

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

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

Рис.5.1. Составные части прототипа функции

В прототипе и в определении функции тип возвращаемого значения, имя и список формальных параметров должны соответствовать друг другу. Если такого соответствия нет, компилятор покажет сообщение об ошибке. Однако прототип функции не обязан содержать имена параметров, можно ограничиться только указанием их типов. Например:

long area(int, int);

Этот прототип объявляет функцию с именем area(), которая возвращает значение типа long и принимает два целочисленных параметра.

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

Определение функции состоит из заголовка функции и её тела. Заголовок подобен прототипу функции, с тем различием, что параметры поименованные и в конце заголовка отсутствует точка с запятой.

int area(int length, int width)

{

return (length * width);

}

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

5.2. Область видимости переменных

5.2.1. Локальные переменные

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

Локальные переменные определяются подобно другим переменным. Параметры, переданные функции, тоже считаются локальными переменными и их можно использовать как определённые внутри тела функции. Изменения, внесённые в параметры во время выполнения функции, не влияют на значения, которые передаются в функцию. То есть локальная копия каждого параметра создаётся в самой функции. Такие локальные копии внешних переменных обрабатываются так же, как и любые другие. Ниже представлен пример использования параметров функции и переменных, локально определённых внутри функции.

#include <stdio.h>

float convert(float); //Прототип функции

void main()

{

float TempFer,TempCel; //Объявление переменных типа float

printf(“Ведите температуру по Фаренгейту: ”);

scanf (“%f”,&TempFer);

TempCel=Convert(TempFer); //Вызывается функция и значение,

//возвращаемое ей, присваивается

//переменной TempCel

printf(“/nЭто температура по Цельсию: ”);

printf(“%f”,TempCel);

}

float convert(float fer) //Определение функции

{

float cel;

cel = ((fer – 32) * 5) / 9;

return cel;

}

5.2.2. Глобальные переменные

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

#include <stdio.h>

void myFunc(); //прототип функции

int x = 5, y = 7; //объявление глобальных переменных

void main()

{

printf(“x from main: %d”,x);

printf(“\n y from main: %d”,y);

myFunc();

printf(“\n Return from myFunc \n”);

printf(“x from main: %d”,x);

printf(“\n y from main: %d”,y);

}

void myFunc()

{

int y = 10, x = 15;

printf(“\n x from myFunc: %d”,x);

printf(“\n y from myFunc: %d”,y);

}

Результат:

x from main 5

y from main 7

x from myFunc 15

y from myFunc 10

Return from myFunc

x from main 5

y from main 7

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

5.3. Передача параметров в функцию

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

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

#include <stdio.h>

#include <conio.h>

#include <iostream.h>

void perestan(int x, int y);

void perestanP(int *x, int *y);

void perestanM(int b[]);

void main()

{

clrscr();

int a[2] = {4,5}, x = 4, y = 5;

perestan(x,y);

printf("После использования perestan() значения х и у: %d %d", x, y);

perestanP(&x,&y);

printf("\n После использования perestanP значения х и у: %d %d", x, y);

perestanM(a);

printf("\n После использования perestanM значения a[0] и a[1]: %d %d",

a[0], a[1]);

getche();

}

void perestan(int x, int y)

{

int temp;

temp = x;

x = y;

y = temp;

}

void perestanP(int *x, int *y)

{

int temp;

temp = *x;

*x = *y;

*y = temp;

}

void perestanM(int b[]);

{

int temp;

temp = b[0];

b[0] = b[1];

b[1] = temp;

}

Результат:

После использования Perestan значения х и у: 4 5 – значения x и y не поменялись, т.к. просто копировалось значение параметра в функцию.

После использования PerestanP значения х и у: 5 4 – значения поменялись, т.к. в функцию уже передавались адреса этих параметров

После использования PerestanM значения a[0] и a[1]: 5 4 - В данном случае пересылается копия адреса массива а, массива b не существует. Запись в int b[] создаёт не массив, а указатель на массив.

5.4. Рекурсивные функции

Функция может вызывать саму себя. Это называется рекурсией, которая может быть прямой и косвенной. Если функция вызывает саму себя – это прямая рекурсия, если же она вызывает другую функцию, которая в свою очередь вызывает первую, то это косвенная рекурсия. Рекурсия полезна в тех случаях, когда выполняется процедура над данными и потом эту же процедуру надо выполнить над полученным результатом.

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

Когда функция вызывает саму себя, передаётся новая копия данных этой функции, что приводит иногда к большим затратам памяти. Локальные переменные во второй версии независимы от локальных переменных первой и не могут непосредственно влиять друг на друга.

Пример использования рекурсии рассмотрим на программе нахождения определённого члена ряда Фибоначчи:

1,1,2,3,5,8,13,21,34…

Каждое число ряда представляет собой сумму двух предыдущих чисел. В общем случае n-e число равно сумме (n-2)-го и (n-1)-го чисел.

Для рекурсивных функций необходимо условие прекращения рекурсии, в ряду Фибоначчи условием прекращения будет n<3.

#include <stdio.h>

int fib (int);

void main()

{

int n, answer;

printf(“Введите номер для нахождения: ”);

scanf(“%d”, &n);

answer = fib(n);

printf(“\n answer is: %d”, answer);

}

int fib(int n)

{

if (n<3)

return 1;

else

return (fib(n-2) + fib(n-1));

}

После ввода параметра, программа проверяет, не меньше ли он числа 3, и, если это так, то функция fib() возвращает значение 1. В противном случае выводится сумма значений, возвращаемых при вызове функции fib() с параметрами n-2 и n-1. Таким образом, эту программу можно представить как циклический вызов функции fib(). Единственными вызовами, которые немедленно возвращают 1, являются вызовы функций fib(1) и fib(2). Рекурсивное использование функции fib() показано на рис 5.3.

Рис.5.3. Использование рекурсии


5.5. Использование функций в качестве параметров функций

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

Имеются некоторые функции: double() – удвоение числа, triple() – утроение числа, square() – возведение в квадрат, cube() – возведение в куб, возвращающие некоторые целые значения. Можно записать:

answer = (doubleValue(tripleValue(square(cube(x)))));

Это выражение принимает переменную х, и передаёт её в качестве параметра функции cube(), возвращаемое значение которой (куб числа) передаётся как параметр функции square(). После этого возвращаемое значение функции (квадрат числа) передаётся в качестве параметра функции tripleValue(). Возвращаемое значение данной функции (утроенное число) передаётся как параметр функции doubleValue(). Наконец, значение возврата функции doubleValue() (удвоенное число) присваивается переменной answer.

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

unsigned long x = 2;

unsigned long cub = cube(x); // 2 в кубе = 8

unsigned long squar = square(cub); // 8 в квадрате = 64

unsigned long tripl = tripleValue(squar); // 64 * 3 = 192

unsigned long answer = doubleValue(tripl); // 192 * 2 = 384

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

Для каждого параметра, объявляемого в прототипе и определении функции, должно быть передано соответствующее значение в вызове функции. Передаваемое значение должно иметь объявленный тип. Из этого правила существует исключение. Если в прототипе функции для параметра объявляется стандартное значение, то это значение используется, если при вызове функции для этого параметра не установлено никакого значения. Например, прототип функции:

long myFunc(int x = 50);

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

long myFunc(int = 50);

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

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

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

В объявлении

long (*p) (int);

создаётся указатель p на функцию, которая принимает целочисленный параметр и возвращает значение типа long. Круглые скобки вокруг (*p) обязательны. Если убрать эти скобки, то это выражение будет объявлять функцию p, принимающую целочисленный параметр и возвращающую указатель на значение типа long. Чтобы связать указатель на функцию с определённой функцией, нужно просто записать для него операцию присваивания, указав в правой части имя функции без каких-либо скобок.При вызове функции через указатель следует задать все параметры, установленные для данной функции.

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

#include <stdio.h>

void mult(int, int);

void main()

{

int x, y;

void (*p)(int, int); //Объявление указателя на функцию

p = mult; //Связь указателя с функцией

scanf(“%d %d”, &x, &y);

p(x,y);

}

void mult(int x, int y)

{

printf(“%d”, x * y);

}

5.7. Структура программы на Си

Все программы содержат одну или более функций. Функция main() должна всегда присутствовать в программе, это первая функция, которая получает управление. Вид программы на Си показан ниже. Функции от func1() до funcN() – функции пользователя.

команды препроцессора

объявление глобальных типов

объявление глобальных переменных

прототипы функций

тип_возврата main(параметры)

{

операторы

}

тип_возврата func1(параметры)

{

операторы

}

...

тип_возврата funcN(параметры)

{

операторы

}

5.8. Передача параметров в функцию main()

Во многих операционных системах, в частности, в DOS и UNIX, предусмотрена возможность передачи аргументов в функцию main() из командной строки. Аргументы командной строки – это текст, записанный после имени запускаемого на исполнение файла. При использовании командной строки функция main() выглядит так:


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



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