| есть |
| 12 |
|_________|
Таким образом, УКАЗАТЕЛЬ - это "стрелка, указывающая на некий ящик-переменную".
Начало этой стрелки можно (в свою очередь) хранить в какой-нибудь переменной.
При этом, если стрелка указывает на переменную типа int,
то тип переменной, хранящей начало стрелки, есть int *
Если типа char, то тип - char *
АДРЕС (указатель на) можно взять только от переменной или элемента массива,
но не от выражения.
int x;
int arr[5];
Законно &x стрелка на ящик "x"
& arr[3] стрелка на ящик "arr[3]"
Незаконно &(2+2) тут нет именованного "ящика",
на который указывает стрелка,
да и вообще ящика нет.
ИСПОЛЬЗОВАНИЕ УКАЗАТЕЛЕЙ
Указатели несколько различно ведут себя СЛЕВА и СПРАВА
от оператора присваивания.
Нас интересует новая операция, применяемая только к указателям:
*pointer
---------------------------------------------------------------------------
СПРАВА от присваиваний и в формулах
===================================
*pointer означает
"взять значение переменной (лежащее в ящике),
на которую указывает указатель,
хранящийся в переменной pointer".
В нашем примере - это число 12.
То есть *pointer означает "пройти по стрелке и взять указываемое ею ЗНАЧЕНИЕ".
printf("%d\n", *pointer);
Печатает 12;
z = *pointer; /* равноценно z = 12; */
z = *pointer + 66; /* равноценно z = 12 + 66; */
Заставим теперь указатель указывать на другую переменную
(иначе говоря, "присвоим указателю адрес другой переменной")
pointer = &var2;
________
/pointer/
_/_______/_
| |
| &var2 |
| |
|_______|_|
|
|&var2
|
V
________
/ var2 /
_/_______/_
| |
| 43 |
| |
|_________|
После этого z = *pointer;
означает z = 43;
---------------------------------------------------------------------------
Таким образом, конструкция
z = *pointer;
означает
z = *(&var2);
означает
z = var2;
То есть * и & взаимно СТИРАЮТСЯ.
СЛЕВА от присваивания...
*pointer = 123;
Означает "положить значение правой части (т.е. 123)
в переменную (ящик), на который указывает указатель,
хранящийся в переменной pointer".
Пройти по стрелке и положить значение в указываемую переменную.
В данном случае *pointer обозначает
не ЗНАЧЕНИЕ указываемой переменной,
а САМУ указываемую переменную.
________
/pointer/
_/_______/_
| |
| &var2 |
| |
|_______|_|
|
|Положить туда 123
|
V
________
/ var2 /
_/_______/_
| |
| 123 |
| |
|_________|
pointer = &var2;
*pointer = 123;
означает
*(&var2) = 123;
означает
var2 = 123;
То есть снова * и & взаимно СТИРАЮТ друг друга.
---------------------------------------------------------------------------
ЕщЛ пример:
*pointer = *pointer + 66;
или
*pointer += 66;
---------------------------------------------------------------------------
Вернемся к примеру с функцией (@). Как он работает?
В строке /* #1 */
Мы вызываем функцию f(), передавая в нее
УКАЗАТЕЛЬ на переменную y ("адрес переменной y").
В строке /* #2 */
Отводится локальная переменная с именем ptr,
которая в качестве начального значения
получает значение первого аргумента функции в точке вызова -
то есть УКАЗАТЕЛЬ на y.
В строке /* #3 */
Мы видим
*ptr = 7;
что следует рассматривать как
*(&y) = 7; точнее *(&main::y)=7;
то есть как
y = 7; точнее main::y=7;
Что и хотелось.
При этом отметим, что само имя "y" этой переменной
внутри функции f() НЕВИДИМО и НЕИЗВЕСТНО!
---------------------------------------------------------------------------
ПРИМЕР: обмен значений двух переменных.
void main(){
int x, y;
int temporary; /* вспомогательная переменная */
x=1; y=2;
temporary=x; x=y; y=temporary;
printf("x=%d y=%d\n", x, y); /* Печатает x=2 y=1 */
}
---------------------------------------------------------------------------
Теперь то же с использованием адресов и указателей:
void swap(int *a, int *b){
int tmp;
tmp = *a; *a = *b; *b = tmp;
}
void main(){
int x, y;
x = 1; y = 2;
swap(&x, &y);
printf("x=%d y=%d\n", x, y);
}
---------------------------------------------------------------------------
ЕщЛ пример:
int x;
int *ptr1, *ptr2;
ptr1 = &x; ptr2 = &x;
*ptr1 = 77;
printf("%d\n", *ptr2); /* Печатает 77 */
То есть на одну переменную МОГУТ указывать несколько указателей.
---------------------------------------------------------------------------
ЕщЛ пример:
int x;
int *ptr1; /* Не инициализирована */
x = *ptr1;
В ptr1 нет указателя ни на что, там есть мусор.
Указатель указывает "в никуда" (пальцем в небо).
Скорее всего произойдЛт сбой в работе программы.
Мораль: ВСЕГДА инициализируй переменные, указатели в том числе.
МАССИВЫ
Язык Си работает с именами массивов специальным образом.
Имя массива "a" для
int a[5];
является на самом деле указателем на его нулевой элемент.
То есть у нас есть переменные (ящики)
с именами
a[0] a[1] ... a[4].
При этом само имя a при его использовании в программе означает &a[0]
a
|
|
|
V
a[0] a[1] a[2] a[3] a[4]
_________________________________________
| | | | | |
| | | | | |
| | | | | |
-----------------------------------------
Поэтому
int a[5];
/* ПередаЛтся не КОПИЯ самого массива, а копия УКАЗАТЕЛЯ на его начало */
void f(int *a){ /* или f(int a[]), что есть равноценная запись */
printf("%d\n", a[1]);
a[2] = 7;
}
main (){
a[1] = 777;
f(a); /* аргумент - массив */
printf("%d\n", a[2]);
}
Вызов f(a); сделает именно ожидаемые вещи.
В этом примере мы видим два правила:
ПРАВИЛО_1:
При передаче в функцию имени массива
в аргумент функции копируется не весь массив (жирновато будет),
а указатель на его 0-ой элемент.
ПРАВИЛО_2:
Указатель на начало массива
МОЖНО индексировать как сам массив.
Это вторая операция, помимо *pointer,
применимая к указателям: pointer[n].
Второе правило влечет за собой ряд следствий.
int a[5]; /* массив */
int *ptr; /* указательная переменная */
ptr = a; /* законно, означает ptr = &a[0]; */
Теперь
ptr[0] = 3; /* означает a[0] = 3; */
ptr[1] = 5; /* означает a[1] = 5; */
Более того. Возьмем теперь
ptr = &a[2];
a[0] a[1] a[2] a[3] a[4]
_________________________________________
| | | | | |
a: | | | | | |
| | | | | |
----------------------------------------------
| | | | ...
ptr: | | | |
-----------------------------
-2 -1 ptr[0] ptr[1] ptr[2]
Мы как бы "приложили" к массиву a[] массив ptr[].
В котором
ptr[0] есть a[2]
ptr[1] есть a[3]
ptr[2] есть a[4]
ptr[3] находится за концом массива a[], МУСОР
Более того, допустимы отрицательные индексы!
ptr[-1] есть a[1]
ptr[-2] есть a[0]
ptr[-3] находится перед началом массива a[], МУСОР
Итак: индексировать можно И массивы И указатели.
Кстати, для имени массива a[]
*a означает то же самое, что и a[0].
Это обратное следствие из схожести массивов и указателей.
19.c
/* Задача: написать функцию инвертирования порядка символов
в массиве char.
A B C D ---> D C B A
В решении можно использовать рекурсию.
*/
/* Мы приведем рекурсивное и нерекурсивное решения (два варианта) */
#include
/* Сначала - несколько служебных функций. */
/* ФУНКЦИЯ ПОДСЧЕТА ДЛИНЫ СТРОКИ.
Как уже объяснялось, строка текста - это массив char,
в конце которого помещен символ '\0'.
Сам символ \0 не считается.
*/
int strlen(char s[]){ /* функция от массива букв */
int counter = 0; /* счетчик и одновременно индекс */
while(s[counter] != '\0') /* пока не встретился признак конца текста */
counter++; /* посчитать символ */
return counter; /* сколько символов, отличных от '\0' */
}
/* ФУНКЦИЯ ПЕЧАТИ СТРОКИ.
Печатаем каждый элемент массива как символ при помощи putchar(c).
Как только встречаем элемент массива, равный '\0' - останавливаемся.
Заметьте, что при наличии завершающего символа нам НЕ НАДО
передавать в функцию размер массива, он нам неинтересен.
В конце эта функция переводит строку.
*/
int putstr(char s[]){
int i = 0; /* индекс */
while(s[i] != '\0'){
putchar(s[i]);
i++;
}
putchar('\n');
return i;
}
/* ТЕПЕРЬ МЫ ЗАНИМАЕМСЯ ФУНКЦИЕЙ ИНВЕРТИРОВАНИЯ.
Для этого нам нужна вспомогательная функция:
сдвиг элементов массива на 1 влево.
Исходный массив: A B C D E F
<----------
Результат: B C D E F F
-
Последний элемент удваивается.
n - размер массива.
Функция работает так:
Исходный массив: A B C D E F n=6
После i=1 B B C D E F
После i=2 B C C D E F
После i=3 B C D D E F
После i=4 B C D E E F
После i=5 B C D E F F
i=6 ==> остановка.
*/
void shiftLeft(char s[], int n){
int i;
for(i=1; i < n; i++)
s[i-1] = s[i];
}
/* Функция инвертирования.
Идея такова:
- если длина массива меньше или равна 1, то инвертировать нечего,
ибо массив состоит из 1 или 0 элементов.
- если длина массива > 1, то
a) Спасти нулевой элемент массива.
A B C D E F
|
|
V
tmp
b) Сдвинуть массив влево
B C D E F F
c) В последний элемент массива поместить спасенный нулевой элемент.
tmp
|
V
B C D E F A
d) Инвертировать начало массива длиной n-1.
{B C D E F}A
Получится:
F E D C B A
Что и требовалось.
s[] - массив,
n - его длина.
*/
void reverse(char s[], int n){