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

27.06.2020

При изучении Си у начинающих часто возникают вопросы связанные с указателями, думаю вопросы у всех возникают примерно одинаковые поэтому опишу те, которые возникли у меня.

Для чего нужен указатель?

Почему всегда пишут “указатель типа” и чем указатель типа uint16_t отличается от указателя типа uint8_t ?

И кто вообще выдумал указатель?

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

Для объявления переменной как указателя необходимо перед её именем поставить * , а для получения адреса переменной используется & (унарный оператор взятия адреса).
char a = "a"; char *p = &a;
В данном случае в р содержится адрес переменной а. Но что интересно, для дальнейшей работы с указателем не надо писать звёздочку, она нужна только при объявлении .
char a = "a"; char b = "b"; char *p = &a; p = &b;
В данном случае в р содержится адрес переменной b, но если мы хотим получить значение лежащее по этому адресу, то нужно использовать оператор разыменования , та же звёздочка *.
char new_simbol = 0; char a = "a"; char *p = &a; new_simbol = *p;
Таким образом, переменная new_simbol будет содержать ascii код символа "a".

Теперь перейдём непосредственно к вопросам, для чего нужен указатель. Представьте что у нас есть массив, с которым мы хотим работать в функции. Для того чтобы передать массив в функцию его надо скопировать, то есть потратить память, которой у МК и так мало, поэтому более правильным решение будет не копировать массив, а передать адрес его первого элемента и размер.
m ={1,2,3...};
Можно это сделать так
void foo(char *m, uint8_t size) { }
или так
void foo(char m, uint8_t size) { }
Поскольку имя массива, содержит адрес его первого элемента, это есть не что иное, как указатель. Перемещаться по массиву можно с помощью простейших арифметических операций, например, для того чтобы получить значение пятого элемента массива, необходимо к адресу массива(адрес первого элемента) прибавить 4 и применить оператор разыменования.
m = *(m + 4);

И тут же возникает вопрос, для чего везде пишут тип перед указателем? Все просто, передавая адрес первого элемента массива и размер массива, мы говорим: Вот отсюда(указатель) выкопать 10 ямок(размер массива), приходим через два часа, а те, кто должны были выкопать ямки, вызвали трактор и роют котлован. Чтобы не попасть в такую ситуацию надо было указать размер ямки, в нашей аналогии тип указателя так, как тип определяет сколько байт будет занимать переменная в памяти.

Таким образом, указывая тип указателя, мы говорим компилятору, вот тебе адрес начала массива, один элемент массива занимает 2 байта, таких элементов в массиве 10 итого сколько памяти выделить под этот массив? 20 байт - отвечает компилятор. Для наглядности давайте возьмем указатель типа void, для него не определено сколько места он занимает - это просто адрес, приведём его к указателям разного типа и выполним операцию разадресации.


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

Ну и последний вопрос, кто выдумал эту бяку указатель. Для того чтобы разобраться в этом вопросе, надо обратиться к ассемблеру, например AVR, и там мы найдём инструкции
st X, r1 ;сохранить содержимое r1 в SRAM по адресу Х, где X – пара регистров r26, r27 ld r1,X ; загрузить в r1 содержимое SRAM по адресу Х, где X – пара регистров r26, r27
Становится понятно, что Х содержит указатель (адрес) и, оказывается, нет никакого злого дядьки, который придумал указатель, чтобы запудрить всем мозги, работа с указателями(адресами) поддерживается на уровне ядра МК.

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

Основные применения:

  • работа с массивами и строками;
  • прямой доступ к ОП;
  • работа с динамическими объектами, под которые выделяется ОП.

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

Тип *имя;

то есть, указатель всегда адресует определённый тип объектов ! Например,

Int *px; // указатель на целочисленные данные char *s; //указатель на тип char (строку Си)

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

1. Сложение/вычитание с числом:

Px++; //переставить указатель px на sizeof(int) байт вперед s--; //перейти к предыдущему символу строки //(на sizeof(char) байт, необязательно один)

2. Указателю можно присваивать адрес объекта унарной операцией " & ":

Int *px; int x,y; px=&x; //теперь px показывает на ячейку памяти со // значением x px=&y; //а теперь – на ячейку со значением y

3. Значение переменной, на которую показывает указатель, берется унарной операцией " * " ("взять значение"):

X=*px; //косвенно выполнили присваивание x=y (*px)++; //косвенно увеличили значение y на 1

Важно ! Из-за приоритетов и ассоциативности операций C++ действие

имеет совсем другой смысл, чем предыдущее. Оно означает "взять значение y (*px) и затем перейти к следующей ячейке памяти (++)"

Расшифруем оператор

Если px по-прежнему показывал на y , он означает "записать значение y в x и затем перейти к ячейке памяти, следующей за px ". Именно такой подход в классическом Си используется для сканирования массивов и строк.

Вот пример, с точностью до адресов памяти показывающий это важное различие. Комментарием приведены значения и адреса памяти переменных x и y , а также значение, полученное по указателю px и адрес памяти, на который он показывает. Обратите внимание, что после выполнения второго варианта кода значение, полученное по указателю, стало "мусором", так как он показывал на переменную, а не на нулевой элемент массива.

#include int main() { int x=0,y=1; int *px=&y; printf ("\nx=%d on &%p, y=%d on &%p, *px=%d on &%p",x,&x,y,&y,*px,px); x=(*px)++; //после первого запуска замените на x=*px++; printf ("\nx=%d on &%p, y=%d on &%p, *px=%d on &%p",x,&x,y,&y,*px,px); /* Действие (*px)++ x=0 on &002CFC14, y=1 on &002CFC08, *px=1 on &002CFC08 x=1 on &002CFC14, y=2 on &002CFC08, *px=2 on &002CFC08 Действие *px++ x=0 on &0021F774, y=1 on &0021F768, *px=1 on &0021F768 x=1 on &0021F774, y=1 on &0021F768, *px=-858993460 on &0021F76C */ getchar(); return 0; }

Приведём пример связывания указателя со статическим массивом:

Int a={1,2,3,4,5}; int *pa=&a; for (int i=0; i<5; i++) cout

For (int i=0; i<5; i++) cout

Эти записи абсолютно эквиваленты, потому что в Си конструкция a[b] означает не что иное, как *(a+b) , где a - объект, b – смещение от начала памяти, адресующей объект. Таким образом, обращение к элементу массива a[i] может быть записано и как *(a+i) , а присваивание указателю адреса нулевого элемента массива можно бы было записать в любом из 4 видов

Int *pa=&a; int *pa=&(*(a+0)); int *pa=&(*a); int *pa=a;

Важно ! При любом способе записи это одна и та же операция, и это - не "присваивание массива указателю", это его установка на нулевой элемент массива.

4. Сравнение указателей (вместо сравнения значений, на которые они указывают) в общем случае может быть некорректно !

Int x; int *px=&x, *py=&x; if (*px==*py) ... //корректно if (px==py) ... //некорректно!

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

Способ 1, со ссылочной переменной C++

Void swap (int &a, int &b) { int c=a; a=b; b=c; } //... int a=3,b=5; swap (a,b);

Этот способ можно назвать "передача параметров по значению, приём по ссылке".

Способ 2, с указателями Cи

Void swap (int *a, int *b) { int c=*a; *a=*b; *b=c; } //... int a=3,b=5; swap (&a,&b); int *pa=&a; swap (pa,&b);

Передача параметров по адресу, прием по значению.

Указатели и строки языка Си

Как правило, для сканирования Си-строк используются указатели.

Char *s="Hello, world";

Это установка указателя на первый байт строковой константы, а не копирование и не присваивание!

Важно !

1. Даже если размер символа равен одному байту, эта строка займёт не 12 (11 символов и пробел), а 13 байт памяти. Дополнительный байт нужен для хранения нуль-терминатора, символа с кодом 0 , записываемого как "\0" (но не "0" – это цифра 0 с кодом 48). Многие функции работы с Си-строками автоматически добавляют нуль-терминатор в конец обрабатываемой строки:

Char s; strcpy(s,"Hello, world"); //Вызвали стандартную функцию копирования строки //Ошибка! Нет места для нуль-терминатора сhar s; //А так было бы верно!

2. Длина Си-строки нигде не хранится, её можно только узнать стандартной функцией strlen(s) , где s – указатель типа char * . Для строки, записанной выше, будет возвращено значение 12, нуль-терминатор не считается. Фактически, Си-строка есть массив символов, элементов типа char .

Как выполнять другие операции со строками, заданными c помощью указателей char * ? Для этого может понадобиться сразу несколько стандартных библиотек. Как правило, в новых компиляторах C++ можно подключать и "классические" си-совместимые заголовочные файлы, и заголовки из более новых версий стандарта, которые указаны в скобках.

Файл ctype.h (cctype) содержит:

1) функции с именами is* - проверка класса символов (isalpha , isdigit , ...), все они возвращают целое число, например:

Char d; if (isdigit(d)) { //код для ситуации, когда d - цифра }

Аналогичная проверка "вручную" могла бы быть выполнена кодом вида

If (d>="0" && d<="9") {

2) функции с именами to* - преобразование регистра символов (toupper , tolower), они возвращают преобразованный символ. Могут быть бесполезны при работе с символами национальных алфавитов, а не только латиницей.

Модуль string.h (cstring) предназначен для работы со строками, заданными указателем и заканчивающимися байтом "\0" ("строками Си"). Имена большинства его функций начинаются на "str". Часть функций (memcpy , memmove , memcmp) подходит для работы с буферами (областями памяти с известным размером).

Примеры на работу со строками и указателями

1. Копирование строки

Char *s="Test string"; char s2; strcpy (s2,s); //копирование строки, s2 - буфер, а не указатель!

2. Копирование строки с указанием количества символов

Char *s="Test string"; char s2; char *t=strncpy (s2,s,strlen(s)); cout << t;

Функция strncpy копирует не более n символов (n - третий параметр), но не запишет нуль-терминатор, в результате чего в конце строки t выведется "мусор". Правильно было бы добавить после вызова strncpy следующее:

T="\0";

то есть, "ручную" установку нуль-терминатора.

3. Копирование строки в новую память

Char *s="12345"; char *s2=new char ; strcpy (s2,s);

Здесь мы безопасно скопировали строку s в новую память s2 , не забыв выделить "лишний" байт для нуль-терминатора.

4. Приведём собственную реализацию стандартной функции strcpy:

Char *strcpy_ (char *dst, char *src) { char *r=dst; while (*src!="\0") { *dst=*src; dst++; src++; } *dst="\0"; return r; }

Вызвать нашу функцию можно, например, так:

Char *src="Строка текста"; char dst; strcpy_ (&dst,&src);

Сократим текст функции strcpy_:

Char *strcpy_ (char *dst, char *src) { char *r=dst; while (*src) *dst++=*src++; *dst="\0"; return r; }

5. Сцепление строк – функция strcat

Char *s="Test string"; char *s2; char *t2=strcat (s2,strcat(s," new words"));

Так как strcat не выделяет память, поведение такого кода непредсказуемо!

А вот такое сцепление строк сработает:

Char s; strcpy (s,"Test string"); char s2; strcat (s," new words"); strcpy (s2,s); char *t2=strcat (s2,s);

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

6. Поиск символа или подстроки в строке.

Char *sym = strchr (s,"t"); if (sym==NULL) puts ("Не найдено"); else puts (sym); //выведет "t string" //для strrchr вывод был бы "tring" char *sub = strstr (s,"ring"); puts (sub); //выведет "ring"

7. Сравнение строк – функции с шаблоном имени str*cmp - "string comparing"

Char *a="abcd",*b="abce"; int r=strcmp(a,b); //r=-1, т.к. символ "d" предшествует символу "e" //Соответственно strcmp(b,a) вернет в данном случае 1 //Если строки совпадают, результат=0

8. Есть готовые функции для разбора строк - strtok , strspn , strсspn - см. пособие, пп. 8.1-8.3

9. Преобразование типов между числом и строкой - библиотека stdlib.h (cstdlib)

Char *s="qwerty"; int i=atoi(s); //i=0, исключений не генерируется!

Из числа в строку:

1) itoa , ultoa - из целых типов

Char buf; int i=-31189; char *t=itoa(i,buf,36); //В buf получили запись i в 36-ричной с.с.

2) fcvt , gcvt , ecvt - из вещественных типов

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

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

1. Описать указатель на будущий динамический объект:

Int *a; //Надёжнее int *a=NULL;

2. Оператором new или функциями malloc , calloc выделить оперативную память:

A = new int ;

#include //stdlib.h, alloc.h в разных компиляторах //... a = (int *) malloc (sizeof(int)*10);

A = (int *) calloc (10,sizeof(int));

В последнем случае мы выделили 10 элементов по sizeof(int) байт и заполнили нулями "\0" .

Важно ! Не смешивайте эти 2 способа в одном программном модуле или проекте! Предпочтительней new , кроме тех случаев, когда нужно обеспечить заполнение памяти нулевыми байтами.

3. Проверить, удалось ли выделить память - если нет, указатель равен константе константе NULL из стандартной библиотеки (в ряде компиляторов null , nullptr , 0):

If (a==NULL) { //Обработка ошибка "Не удалось выделить память" }

4. Работа с динамическим массивом или строкой ничем не отличается от случая, когда они статические.

5. Когда выделенная ОП больше не нужна, её нужно освободить:

Delete a; //Если использовали new free (a); //Пытается освободить ОП, //если использовали malloc/calloc

Важно ! Всегда старайтесь придерживаться принципа стека при распределении ОП. То есть, объект, занявший ОП последним, первым её освобождает.

Пример. Динамическая матрица размером n*m.

Const int n=5,m=4; int **a = new (int *) [n]; for (int i=0; i

После этого можно работать с элементами матрицы a[i][j] , например, присваивать им значения. Освободить память можно было бы так:

For (int i=n-1; i>-1; i--) delete a[i]; delete a;

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

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

3. Написать свои версии функций преобразования строки в число и числа в строку.

Объявление и инициализация переменной-указателя. Указатели представляют собой переменные, значениями которых являются адреса памяти. Указатель содержит адрес переменной, в которой находится конкретное значение. Переменная непосредственно ссылается на значение, а указатель косвенно ссылается на значение. Ссылка на значение через посредство указателя называется косвенной адресацией.

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

int *countPtr, count;

объявляется переменная countPtrтипаint* (указатель на целочисленное значение). Символ * в объявлении распространяется только наcountPtr. Этот символ означает, что объявляемая переменная является указателем. Можно объявлять указатели, ссылающиеся на объекты любого типа.

Указатели должны быть инициализированы либо при объявлении, либо при помощи оператора присваивания. Указатель может быть инициализирован нулем, макросом NULLили значением адреса. Указатель со значениемNULLне указывает ни на что. Инициализация указателя значением 0 эквивалента инициализации указателя константойNULL, однако использованиеNULLпредпочтительнее. Когда присваивается значение 0, то происходит его преобразование к указателю соответствующего типа. Значение 0 является единственным целым числом, которой может быть присвоено переменной-указателю непосредственно.

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

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

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

    Получение адреса указателя. Подобно любым переменным переменная типа указатель имеет адрес и значение. Операция & сообщает, где находится сам указатель.

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

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

К указателям можно применить арифметические операции, такие как: ++, --, +, +=, -, -= и можно вычислить разность двух указателей.

В качестве примера определим массив int v, первый элемент которого будет иметь адрес в памяти, равный 3000. Инициализируем указатель vPtr значением адреса v, т.е. значение vPtr равно 3000, любым из следующих операторов

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

Размер объекта в байтах зависит от типа объекта. Например, оператор

даст результат 3008 (3000+2*4), если для целого числа отводится в памяти 4 байта. Теперь vPtr будет ссылаться на элемент v.

Если бы vPtr был увеличен до значения 3016, которое соответствует адресу элемента массива v, то оператор

вернул бы vPtr к значению 3000, соответствующему началу массива. При увеличении или уменьшении указателя на единицу можно использовать операции инкремента (++) и декремента (--). Каждый из следующих операторов

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

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

x = v2Ptr - vPtr;

переменной х будет присвоено число элементов массива, расположенных начиная с адреса vPtrи доv2Ptr; в данном случае это будет значение 2.

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

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

#include

{ int x=5, y=10;

printf(“x=%d y=%d\n”, x, y);

change(&x,&y); /* передача адресов функции */

printf(“x=%d y=%d\n”, x, y); }

change (int *u, int *v)

temp=*u; /*tempприсваивается значение, на которое указываетu*/

Результат программы:

Данная функция изменяет значения переменных xиy. Путем передачи функции адресов переменных х и у мы предоставили ей возможность доступа к ним. Используя указатели и операцию (*), функция смогла извлечь величины, помещенные в соответствующие ячейки памяти, и менять их местами.

Основная ли тература: 1осн,2осн

Дополнительная литератур а: 9доп

Контрольные вопросы:

1. Назовите операции для работы с указателями?

2. На какое число увеличивается значение указателя при прибавлении из указателя целого числа?

3. Приведите пример инициализации переменной-указателя?

4. Приведите пример объявления переменной-указателя?

5. Какая операция используется для организации вызова по ссылке?

Пожалуйста, приостановите работу AdBlock на этом сайте.

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

Итак, пойдём по порядку. Объявление указателя.

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

Листинг 1.

int * p_g; // указатель на переменную типа int double * p_f; // указатель на переменную типа double

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

Листинг 2.

int n = 100; double PI = 3.1415926; int * p_k; // указатель на переменную типа int double * p_pi; // указатель на переменную типа double p_k = &n; // получаем адрес переменной n и присваиваем его указателю p_k p_pi = &PI; // получаем адрес переменной PI и присваиваем его указателю p_pi

Для вывода значения указателя на экран нужно в функции printf использовать модификатор %p. Пример:

Листинг 3.

printf ("adres peremennoi PI %p\n", p_pi);

Используя адрес переменной, который хранится в указателе, можно изменять значения этой переменной. Для этого используется операция разыменования *. Вот посмотрите на пример:

Листинг 4.

#include int main(void) { int a = 100; int * p_a = &a; // сохраняем в указатель адрес переменной a printf("a = %d\n", a); // стандартный способ получить значение переменной a printf("a = %d\n", *p_a); // получаем значение переменной a через указатель на неё // используя указатель p_a, записываем в переменную a другое значение *p_a = 50; printf("a = %d\n", *p_a); return 0; }

Рис.1 Доступ к переменной через указатель

Итого, * применительно к указателям используется в двух случаях:

  • при объявлении указателя, чтобы показать, что это указатель;
  • если мы хотим обратиться к переменной, на которую указывает указатель.

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

Листинг 5.

#include int main(void) { int a = 100; int * p_a = &a; // сохраняем в указатель адрес переменной a printf("a = %d\n", a); // стандартный способ получить значение переменной a printf("a = %d\n", *p_a); // получаем значение переменной a через указатель на неё // используя указатель p_a, записываем в переменную a другое значение *p_a = 50; printf("a = %d\n", *p_a); printf("%p\n", p_a); p_a = NULL; printf("%p\n", p_a); return 0; }

Рис.2 Обнуление указателя

На главную

Язык Си на примерах

Функции в Си

Для чего нужны функции в C?

Функции в Си применяются для выполнения определённых действий в рамках общей программы. Программист сам решает какие именно действия вывести в функции. Особенно удобно применять функции для многократно повторяющихся действий.

Простой пример функции в Cи

Пример функции в Cи:

Это очень простая программа на Си. Она просто выводит строку «Functions in C». В программе имеется единственная функция под названием main. Рассмотрим эту функцию подробно. В заголовке функции, т.е. в строке

int – это тип возвращаемого функцией значения;

main - это имя функции;

(void) - это перечень аргументов функции. Слово void указывает, что у данной функции нет аргументов;

return – это оператор, который завершает выполнение функции и возвращает результат работы функции в точку вызова этой функции;

EXIT_SUCCESS - это значение, равное нулю. Оно определено в файле stdlib.h;

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

{
puts(«Functions in C»);
return EXIT_SUCCESS;
}

называют телом функции.

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

Как из одной функции в Cи вызвать другую функцию?

Рассмотрим пример вызова функций в Си:

Запускаем на выполнение и получаем:

В этом примере создана функция sum, которая складывает два целых числа и возвращает результат. Разберём подробно устройство этой функции.

Заголовок функции sum:

int sum(int a, int b)

здесь int - это тип возвращаемого функцией значения;

sum - это имя функции;

(int a, int b) - в круглых скобках после имени функции дан перечень её аргументов: первый аргумент int a, второй аргумент int b. Имена аргументов являются формальными, т.е. при вызове функции мы не обязаны отправлять в эту функцию в качестве аргументов значения перемнных с именами a и b. В функции main мы вызываем функцию sum так: sum(d, e);. Но важно, чтоб переданные в функцию аргументы совпадали по типу с объявленными в функции.

В теле функции sum, т.е. внутри фигурных скобок после заголовка функции, мы создаем локальную переменную int c, присваиваем ей значение суммы a плюс b и возвращаем её в качестве результата работы функции опрератором return.

Теперь посмотрим как функция sum вызывается из функции main.

Вот функция main:

Сначала мы создаём две переменных типа int

int d = 1; int e = 2;

их мы передадим в функцию sum в качестве значений аргументов.

int f = sum(d, e);

её значением будет результат работы функции sum, т.е. мы вызываем функцию sum, которая возвратит значение типа int, его-то мы и присваиваем переменной f. В качестве аргументов передаём d и f. Но в заголовке функции sum

int sum(int a, int b)

аргументы называются a и b, почему тогда мы передаем d и f? Потому что в заголовке функций пишут формальные аргументы, т.е. НЕ важны названия аргументов, а важны их типы. У функции sum оба аргумента имеют тип int, значит при вызове этой функции надо передать два аргумента типа int с любыми названиями.

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

Прототип функции в Си

Рассмотрим пример функциив Си:

В этом примере функция sum определена ниже места её вызова в функции main.

В таком случае надо использовать прототип функции sum. Прототип у нас объявлен выше функции main:

int sum(int a, int b);

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

int f = sum(d, e);

а ниже функции main определяем функцию sum, которая предварительно была объявлена в прототипе:

Чем объявление функции в Си отличается от определения функции в Си?

Когда мы пишем прототип функции, например так:

int sum(int a, int b);

то мы объявляем функцию.

А когда мы реализуем функцию, т.е. записываем не только заголовок, но и тело функции, например:

то мы определяем функцию.

Оператор return

Оператор return завершает работу функции в C и возвращает результат её работы в точку вызова. Пример:

Эту функцию можно упростить:

здесь оператор return вернёт значение суммы a + b.

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

Если в примере значение аргумента a окажется больше двух, то функция вернет ноль (первый случай) и всё, что ниже комментария «// Первый случай;» выполнятся не будет.

Указатели в языке Си

Если a будет меньше двух, но b будет меньше нуля, то функция завершит свою работу и всё, что ниже комментария «// Второй случай;» выполнятся не будет.

И только если оба предыдущих условия не выполняются, то выполнение программы дойдёт до последнего оператора return и будет возвращена сумма a + b.

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

Аргументы можно передавать в функцию C по значению. Пример:

В примере, в функции main, создаём переменную int d = 10. Передаём по значению эту переменную в функцию sum(d). Внутри функции sum значение переменной увеличивается на 5. Но в функции main значение d не изменится, ведь она была передана по значению. Это означает, что было передано значение переменной, а не сама переменная. Об этом говорит и результат работы программы:

т.е. после возврата из функции sum значеие d не изменилось, тогда как внутри функции sum оно менялось.

Передача указателей функции Си

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

В этом варианте программы я перешел от передачи аргумента по значению к передаче указателя на переменную. Рассмотрим подробнее этот момент.

printf(«sum = %d\n», sum(&d));

в функцию sum передается не значение переменной d, равное 10-ти, а адрес этой переменной, вот так:

Теперь посмотрим на функцию sum:

Аргументом её является указатель на int. Мы знаем, что указатель - это переменная, значением которой является адрес какого-то объекта. Адрес переменной d отправляем в функцию sum:

Внутри sum указатель int *a разыменовывается. Это позволяет от указателя перейти к самой переменной, на которую и указывает наш указатель. А в нашем случае это переменная d, т.е. выражение

равносильно выражению

Результат: функция sum изменяет значение переменной d:

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

C/C++ в Eclipse

Все примеры для этой статьи я сделал в Eclipse. Как работать с C/C++ в Eclipse можно посмотреть здесь. Если вы работаете в другой среде, то примеры и там будут работать.

Теги: Си указатели. Указатель на указатель. Тип указателя. Арифметика указателей. Сравнение указателей.

Указатели

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

Определение

Указатель – это переменная, которая хранит адрес области памяти.

Тема 7. Указатели в Си.

Указатель, как и переменная, имеет тип. Синтаксис объявления указателей

<тип> *<имя>;

Например
Два основных оператора для работы с указателями – это оператор & взятия адреса, и оператор * разыменования. Рассмотрим простой пример.

#include #include void main() { int A = 100; int *p; //Получаем адрес переменной A p = &A; //Выводим адрес переменной A printf(«%p\n», p); //Выводим содержимое переменной A printf(«%d\n», *p); //Меняем содержимое переменной A *p = 200; printf(«%d\n», A); printf(«%d», *p); getch(); }

Рассмотрим код внимательно, ещё раз

Была объявлена переменная с именем A . Она располагается по какому-то адресу в памяти. По этому адресу хранится значение 100.

Создали указатель типа int .

Теперь переменная p хранит адрес переменной A . Используя оператор * мы получаем доступ до содержимого переменной A .
Чтобы изменить содержимое, пишем

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

#include #include void main() { int A = 100; int *a = &A; double B = 2.3; double *b = &B; printf(«%d\n», sizeof(A)); printf(«%d\n», sizeof(a)); printf(«%d\n», sizeof(B)); printf(«%d\n», sizeof(b)); getch(); }

Будет выведено
Несмотря на то, что переменные имеют разный тип и размер, указатели на них имеют один размер. Действительно, если указатели хранят адреса, то они должны быть целочисленного типа. Так и есть, указатель сам по себе хранится в переменной типа size_t (а также ptrdiff_t ), это тип, который ведёт себя как целочисленный, однако его размер зависит от разрядности системы. В большинстве случаев разницы между ними нет. Зачем тогда указателю нужен тип?

Арифметика указателей

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

Для их выполнения необходимо знать размер.
операция сдвигает указатель вперёд на байт.
Например, если указатель int *p; хранит адрес CC02, то после p += 10; он будет хранить адрес СС02 + sizeof(int)*10 = CC02 + 28 = CC2A (Все операции выполняются в шестнадцатиричном формате). Пусть мы создали указатель на начало массива. После этого мы можем «двигаться» по этому массиву, получая доступ до отдельных элементов.

#include #include void main() { int A = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p; p = A; printf(«%d\n», *p); p++; printf(«%d\n», *p); p = p + 4; printf(«%d\n», *p); getch(); }

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

Массив, по сути, сам является указателем, поэтому не нужно использовать оператор &. Мы можем переписать пример по-другому

Получить адрес первого элемента и относительно него двигаться по массиву.
Кроме операторов + и — указатели поддерживают операции сравнения. Если у нас есть два указателя a и b, то a > b, если адрес, который хранит a, больше адреса, который хранит b.

#include #include void main() { int A = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *a, *b; a = &A; b = &A; printf(«&A == %p\n», a); printf(«&A == %p\n», b); if (a < b) { printf(«a < b»); } else { printf(«b < a»); } getch(); }

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

Указатель на указатель

Указатель хранит адрес области памяти. Можно создать указатель на указатель, тогда он будет хранить адрес указателя и сможет обращаться к его содержимому. Указатель на указатель определяется как

<тип> **<имя>;

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

#include #include #define SIZE 10 void main() { int A; int B; int *p; int **pp; A = 10; B = 111; p = &A; pp = &p; printf(«A = %d\n», A); *p = 20; printf(«A = %d\n», A); *(*pp) = 30; //здесь скобки можно не писать printf(«A = %d\n», A); *pp = &B; printf(«B = %d\n», *p); **pp = 333; printf(«B = %d», B); getch(); }

Указатели и приведение типов

Так как указатель хранит адрес, можно кастовать его до другого типа.

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

#include #include #define SIZE 10 void main() { int A = 10; int *intPtr; char *charPtr; intPtr = &A; printf(«%d\n», *intPtr); printf(«———————\n»); charPtr = (char*)intPtr; printf(«%d «, *charPtr); charPtr++; printf(«%d «, *charPtr); charPtr++; printf(«%d «, *charPtr); charPtr++; printf(«%d «, *charPtr); getch(); }

В этом примере мы пользуемся тем, что размер типа int равен 4 байта, а char 1 байт. За счёт этого, получив адрес первого байта, можно пройти по остальным байтам числа и вывести их содержимое.

NULL pointer — нулевой указатель

Указатель до инициализации хранит мусор, как и любая другая переменная. Но в то же время, этот «мусор» вполне может оказаться валидным адресом. Пусть, к примеру, у нас есть указатель. Каким образом узнать, инициализирован он или нет? В общем случае никак. Для решения этой проблемы был введён макрос NULL библиотеки stdlib.
Принято при определении указателя, если он не инициализируется конкретным значением, делать его равным NULL.

int *ptr = NULL;

По стандарту гарантировано, что в этом случае указатель равен NULL , и равен нулю, и может быть использован как булево значение false . Хотя в зависимости от реализации NULL может и не быть равным 0 (в смысле, не равен нулю в побитовом представлении, как например, int или float ).
Это значит, что в данном случае

int *ptr = NULL; if (ptr == 0) { … }

вполне корректная операция, а в случае

int a = 0; if (a == NULL) { … }

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

#include #include #include void main() { int *a = NULL; unsigned length, i; printf(«Enter length of array: «); scanf(«%d», &length); if (length > 0) { //При выделении памяти возвращается указатель. //Если память не была выделена, то возвращается NULL if ((a = (int*) malloc(length * sizeof(int))) != NULL) { for (i = 0; i < length; i++) { a[i] = i * i; } } else { printf(«Error: can’t allocate memory»); } } //Если переменая была инициализирована, то очищаем её if (a != NULL) { free(a); } getch(); }

Примеры

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

#include #include void main() { int A = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int even; int evenCounter = 0; int *iter, *end; //iter хранит адрес первого элемента массива //end хранит адрес следующего за последним «элемента» массива for (iter = A, end = &A; iter < end; iter++) { if (*iter % 2 == 0) { even = *iter; } } //Выводим задом наперёд чётные числа for (—evenCounter; evenCounter >= 0; evenCounter—) { printf(«%d «, even); } getch(); }

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

#include #include #define SIZE 10 void main() { double unsorted = {1.0, 3.0, 2.0, 4.0, 5.0, 6.0, 8.0, 7.0, 9.0, 0.0}; double *p; double *tmp; char flag = 1; unsigned i; printf(«unsorted array\n»); for (i = 0; i < SIZE; i++) { printf(«%.2f «, unsorted[i]); } printf(«\n»); //Сохраняем в массив p адреса элементов for (i = 0; i < SIZE; i++) { p[i] = &unsorted[i]; } do { flag = 0; for (i = 1; i

3. Более интересный пример. Так как размер типа char всегда равен 1 байт, то с его помощью можно реализовать операцию swap – обмена местами содержимого двух переменных.

#include #include #include void main() { int length; char *p1, *p2; char tmp; float a = 5.0f; float b = 3.0f; printf(«a = %.3f\n», a); printf(«b = %.3f\n», b); p1 = (char*) &a; p2 = (char*) &b; //Узнаём сколько байт перемещать length = sizeof(float); while (length—) { //Обмениваем местами содержимое переменных побайтно tmp = *p1; *p1 = *p2; *p2 = tmp; //не забываем перемещаться вперёд p1++; p2++; } printf(«a = %.3f\n», a); printf(«b = %.3f\n», b); getch(); }

В этом примере можно поменять тип переменных a и b на double или любой другой (с соответствующим изменением вывода и вызова sizeof ), всё равно мы будет обменивать местами байты двух переменных.

4. Найдём длину строки, введённой пользователем, используя указатель #include #include void main() { char buffer; char *p; unsigned length = 0; scanf(«%127s», buffer); p = buffer; while (*p != ‘\0’) { p++; length++; } printf(«length = %d», length); getch(); }

Обратите внимание на участок кода

while (*p != ‘\0’) { p++; length++; }

его можно переписать

while (*p != 0) { p++; length++; } или while (*p) { p++; length++; }

или, убрав инкремент в условие

while (*p++) { length++; }

ru-Cyrl18-tutorialSypachev [email protected]

Указатель - это специальная переменная, которая хранит адрес другой переменной. Указатель объявляется следующим образом: тип* переменная; где тип - любой допустимый как простой, так и составной базовый тип указателя.

Например, пусть объявлена обычная переменная int t; Объявление и инициализация int* p= &t; означают следующее. В переменной p будетхраниться не обрабатываемое программой целое число (оценка студента, количество выпущенной продукции и т. п.), а адрес ячейки, в которой будет находиться информация указанного типа (целое число). Под адресом будем понимать номер первого байта выделенного для переменной участка оперативной памяти. Для переменных, не являющихся указателями, без дополнительного объявления адрес также запоминается системой, и его можно получить с помощью операции & (разадресации), например, &t. Эта унарная операция, которую иногда называют “взятие адреса”, ничего не делает со значением переменной t .

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

Один из способов показан выше и означает, что в переменную p помещается адрес ячейки t . Важно понять, что int* p= &t; равносильно int* p; p=&t ; а не *p=&t ; В этом заключается одна из трудностей начального этапа изучения указателей. Эта тема усложняется ещё и тем, что такой же символ “& ” используется при объявлении переменной ссылочного типа.

Указатели в Си.

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

Заметим, что расстановка пробелов при объявлении указателей свободная. Допустимы также следующие записи: int * p= & t; int *p= &t; Предпочтение следовало бы отдать записи в начале параграфа, из которой легче понять смысл указателя. Объявляется переменная p , а не *p , и, кроме этого, типом является int* , а не int .

Если одновременно объявляется несколько указателей, то символ “*” надо писать перед каждой переменной: float* q1, *q2;

Содержимое ячейки, адрес которой находится в p , в тексте программы обозначается с помощью операции разыменование . Для неё используется тот же символ “*”, что и при объявлении переменной-указателя. Эта унарная операция возвращает значение переменной, находящейся по указанному адресу. Поэтому *p - это обрабатываемое программой целое число, находящееся в ячейке, адрес которой - в переменной-указателе p . С учётом инициализации (p = &t ) *p и t - это одно и то же значение. Значит, если с помощью cin>>t; введём, например, число 2 и выполним *p*=5 ; или *p=*p*5; то изменится и величина t , хотя, казалось бы, не было явного её изменения. Поэтому оператор cout << t; выведет число 10 (2*5). И наоборот, изменив t (например, t++; ), этим самым мы изменим и значение *p. С помощью cout<<(*p); выведем 11.

Сказанное выше будем обозначать так:

p (или &t ) *p (или t )

В “левом прямоугольнике” (ячейке памяти) находится адрес, а в ячейке “справа” - обрабатываемое целое число.

Рассматриваемые здесь операции “&” и ”*” являются унарными и имеют более высокий приоритет по сравнению с аналогичными бинарными операциями “битовое и” и арифметическое умножение.

Для *p определены те же операции, что и для переменной указанного типа, у нас - для целых чисел. Поэтому допустимы, например, следующие операторы: а) cin>>(*p); b) int r; r=*p*2; c) if (*p%2)…; d) cout<<(*p);.

Можно выводить и значение переменной-указателя. cout<Выведет адрес в шестнадцатеричной системе счисления. При этом он не обязательно будет одинаковым при повторном выполнении одной и той же программы.

⇐ Предыдущая567891011121314Следующая ⇒

Дата публикования: 2015-02-18; Прочитано: 526 | Нарушение авторского права страницы

Studopedia.org — Студопедия.Орг — 2014-2018 год.(0.001 с)…

— указатель на Работника. Вы можете назначить один выделенный объект этому указателю или, в вашем случае, несколько (с синтаксисом массива). Таким образом, он указывает на массив сотрудников.

Вы разыменовали этот указатель.

Обозначения и предположения

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

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

в словах низкого уровня означает: возьмите указатель, добавьте раз размер указанного типа (чтобы он указывал на -ную запись) и разыменовал этот адрес, Это означает, что фактически является Employee в -ом индексе (но не в указателе).

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

Если он все еще был указателем, вам нужно будет использовать оператор стрелки, но поскольку вы использовали оператор индекса массива (), вы уже разыменовали его, то оператор точки правильный:

Последнее обновление: 27.05.2017

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

Для определения указателя надо указать тип объекта, на который указывает указатель, и символ звездочки *. Например, определим указатель на объект типа int:

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

Int x = 10; // определяем переменную int *p; // определяем указатель p = &x; // указатель получает адрес переменной

Указатель хранит адрес объекта в памяти компьютера. И для получения адреса к переменной применяется операция & . Эта операция применяется только к таким объектам, которые хранятся в памяти компьютера, то есть к переменным и элементам массива.

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

Какой именно адрес имеет переменная x? Для вывода значения указателя можно использовать специальный спецификатор %p :

#include int main(void) { int x = 10; int *p; p = &x; printf("%p \n", p); // 0060FEA8 return 0; }

В моем случае машинный адрес переменной x - 0060FEA8. Но в каждом отдельном случае адрес может быть иным. Фактически адрес представляет целочисленное значение, выраженное в шестнадцатеричном формате.

То есть в памяти компьютера есть адрес 0x0060FEA8, по которому располагается переменная x. Так как переменная x представляет тип int , то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом, переменная типа int последовательно займет ячейки памяти с адресами 0x0060FEA8, 0x0060FEA9, 0x0060FEAA, 0x0060FEAB.

И указатель p будет ссылаться на адрес, по которому располагается переменная x, то есть на адрес 0x0060FEA8.

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

#include int main(void) { int x = 10; int *p; p = &x; printf("Address = %p \n", p); printf("x = %d \n", *p); return 0; }

Консольный вывод:

Address = 0060FEA8 x = 10

Используя полученное значение в результате операции разыменования мы можем присвоить его другой переменной:

Int x = 10; int *p = &x; int y = *p; printf("x = %d \n", y); // 10

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

Int x = 10; int *p = &x; *p = 45; printf("x = %d \n", x); // 45

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

Создадим еще несколько указателей:

#include int main(void) { char c = "N"; int d = 10; short s = 2; char *pc = &c; // получаем адрес переменной с типа char int *pd = &d; // получаем адрес переменной d типа int short *ps = &s; // получаем адрес переменной s типа short printf("Variable c: address=%p \t value=%c \n", pc, *pc); printf("Variable d: address=%p \t value=%d \n", pd, *pd); printf("Variable s: address=%p \t value=%hd \n", ps, *ps); return 0; }

В моем случае я получу следующий консольный вывод:

Variable c: address=0060FEA3 value=N Variable d: address=0060FE9C value=10 Variable s: address=0060FE9A value=2

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