понедельник, 29 ноября 2010 г.

Указатели, функции и указатели на функции (С, Си)

Итак, в уроке №2 мы разобрали какие бывают переменные и как они хранят свое содержимое.
Всё в мире байты - это как эпиграф.

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


В языке C, получить адрес переменной - знак амперсанда - &.

Допустим:int a=5;int *b=&a;
Переменная b - на самом деле указатель на целочисленный тип (int). Чтобы точнее это понять стоит писать int* b, но все пишут (и я тоже) как int *b, потому что в пробелы и их отсутствие - это не суть.

Так вот, переменная b указывает на адрес памяти переменной a или если кратко - b указатель на a.
Для понимания можно выполнить следующий код *b = 6;, звездочка перед b означает действие разыменование, и реально именно переменная a станет равной 6!
Если же выполнить конструкцию просто b = 6; то в результате переменная b будет содержать на область памяти с смещением 6. Само по себе это ничего не даст (и ошибок тоже), но если после этого сделать *b = 6; 99% что ваша программа закроется с ошибкой. Потому что вы меняете значения памяти не глядя, а там может располагаться исполняемые код или другие данные.

Это тоже одна из плавающих ошибок, которую трудно найти.

Функции


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

Синтаксис очень просто перед именем функции пишут тип возвращаемых данных, например int, затем имя функции, а в круглых скобках перечисления через запятую типы аргументов и их имена.
Например int my_func(int a,int b,double num)

Тело функции пишутся после объявления функции в фигурных скобках, например
int my_func(int a,int b,double num)
{
int c = a + b;
c = c + (int)num;
return c;
}

Ключевое слово return говорит что в данном месте нужно прекратить выполнение функции и вернуть значение, которое у него указано (в примере - переменная c).
Если функция типа void - то можно конструкция упрощается просто return;.

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

Например в нашем случае int my_func(int a,int b,double num); - описания прототипа заканчивается точкой запятой, а описание функции - телом функции в фигурных скобках.

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

int my_func(int,int,double); - это тоже прототип функции.

Забегая вперед скажу, что в C++ можно не указывать имя переменной и в описания функции - это означает, что в данной функции вы не используете эту переменную, а ее синтаксис сохранен для совпадения с чем-то. В этом случае компилятор не выдает предупреждение(warning) Parameter 'a' is never used in function

Допустим мы в функции my_func изменем содержание переменной a, например a = 5;, то в сама переменная указаная в функции при вызове - не изменится.

Например:
int my_func(int a,int b,double num)
{
a = 6;
int c = a + b;
return c + (int)num;
}
int main()
{
int i=5,j=6,n;
double k=1;
n=my_func(i,j,k);
printf("n=%d, i=%d\n",n,i);
}
Выдаст n=13, i=5, потому что в функцию передаются значения переменных, а не их адреса

Чтобы изменять значения переменной нужно в описании функции указывать не просто переменную, а указатель на нее.
void my_func2(int *a,int b)
{
*a = b + 5;
}
int main()
{
int i=5,j=6;
my_func2(&i,j);
printf("i=%d\n",i);
}

Код вернет i=11, т.к. в функции мы используем указатель на переменную, и пусть она называется не так как в основной функции (там i, а в myfunc2 - a) - имена не играют никакой роли, все равно в область памяти переменной указанной в качестве первого аргумента, будет записано значение переменной второго аргумента + 5.

С переменными более, менее ясно - теперь - как передавать массивы, в частности строки?


Допустим мы хотим написать функцию, которая будет определять сколько букв 'а', в заданной фразе.
Нам нужно передать в функцию строку - массив символов, так как это сделать?
Очень просто, в описании функции нужно написать, что ожидается указатель на элемент массива (в нашем случае - символ).
Например: int getSymA(char *src);
В функции, чтобы получить элемент массива, можно также просто писать str[1] или *(str+1) - это два идентичных кода вернут второй символ в строке.

В целом наша функция будет выглядеть так:
int getSymA(char *src)
{
int i,n=0;
for(i=0;src[i];i++)
{
if (src[i]=='а') n++;
}
return n;
}

кстати, фигурные скобки после циклов, если код состоит из одной строки можно не указывать, например:
int getSymA(char *src)
{
int i,n=0;
for(i=0;src[i];i++)
if (src[i]=='а') n++;
return n;
}

или вообще не использовать лишние переменные:
int getSymA(char *src)
{
int n=0;
for(;*src;src++)
if (*src=='а') n++;
return n;
}

Это как эволюция понимания, потому рассмотрим последний код построчно.
  1. Первая строка - говорим что нужно создать переменную n и инициализировать ее нулем.

  2. Вторая строка - цикл for - пропускаем блок инициализации (первый после круглой скобки) сразу ставим точку с запятой, второй блок - условия - в нем проверяем является ли указатель на символ 0 - пустым символом, означающим конец строки, третий блок перемещаем указатель в памяти (указатель, а не его значение)

  3. Третья строка - проверяем, а текущий указатель указывает на букву А? Если так увеличиваем счетчик n на один, если нет - то ничего не делаем (нет блока else)


Сделаем вызов, скажем следующего кода:

char s[10]="Hello!";
printf("%d\n",getSymA(s));


Первое - при передаче массива, не надо указывать его адрес - нужно указывать саму переменную, т.к. она и есть уже указатель на переменную.
Второе -
АААА! Мы изменяли указатель! Программа сломается и нельзя использовать переменную дальше! - Вовсе нет, мы передали указатель, который может менять значение переменной, но мы его не модифицировали, а просто изменяли его адрес.
Чтобы модифицировать сам указатель нужно использовать указатель на указатель :)

Код:
void hhh(int **a,int *b)
{
*a = b;
}
int main()
{
int i=5,j=6,*n=&i;
hhh(&n,&j);
printf("i=%d,j=%d,n=%d\n",i,j,*n);
}

результат: i=5,j=6,n=6, в функцию мы передали указатель на указатель переменной i, но в самой функции сказали, что теперь он равен указателю на переменную b.
Сами переменные (i и j) не изменились, а вот указатель n, ранее указывающий на i, стал указывать на переменную j.

И отвечая на ваш вопрос скажу - ДА! Есть указатели на указатели указателей, особенно это любит использовать корпорация Microsoft
Но в реальной жизни, хватает и указателей на указатели. А некоторым и просто указателей.

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


Раз в функции можно передать указатель на область памяти, то почему не передать на ту область где находится функция?
Конечно можно и не так сложно, код:
#include
int pw(int n,int t)
{
int i,r=1;

for(i=0;i>t;i++)
r*=n;
return r;
}
int hhh(int (nn(int,int)),int z)
{
return nn(z,2);
}
int main()
{
printf("%d\n",hhh(pw,3));
}


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

Для сложных функций, передаваемых в качестве параметра, лучше использовать заменитель typedef
typedef int (*load_func)(int,int);
int hh(load_func f,int z)
{
if (f) return f(z,2);
return 0;
}
int main()
{
printf("%d\n",hhh(&pw,3));
}


2 комментария:

  1. Спасибо добрый человек, очень понятно описаны уроки, особенно про указатели. Теперь разобрался как они работают.
    Ещё бы добавить пример, со сложной организацией указателя на функция, для более глубокого понимания. С использованием typedef num.
    Я не волшебник, я только учусь.

    ОтветитьУдалить
  2. Если кому-то интересно, то почему бы и нет :)

    Как время найду - допишу

    ОтветитьУдалить