Итак, краткий курс sh по указателям в C.
Прежде всего, Почему мы используем указатели в C? По сути, у нас есть для использования указателей по следующим причинам:
- Чтобы функции могли изменять свои параметры
- Для отслеживания динамически выделяемой памяти
Указатели пригодятся и другими способами, поскольку они предлагают форму косвенного обращения . Они позволяют нам манипулировать другими объектами без необходимости знать имена других объектов.
Indirection - мощный инструмент программирования, который вы уже видели, когда имели дело с массивами. Вместо создания 100 уникальных целочисленных переменных мы можем создать массив целых чисел и использовать подписку для ссылки на конкретный объект c. Таким образом, вместо записи
int var0 = 0;
int var1 = 1;
int var2 = 2;
...
int var99 = 99;
мы можем написать
int var[100];
for ( int i = 0; i < 100; i++ )
var[i] = i;
Запись с индексом массива позволяет нам ссылаться на объект косвенно , а не на уникальный имя. Он предоставляет ярлык для управления большим количеством объектов, ссылаясь на них одним выражением. Указатели служат практически той же цели. Предположим, у нас есть несколько целочисленных переменных с именами x
, y
и z
. Мы можем создать указатель p
для обращения к каждому из них по очереди:
int x = 10;
int y = 20;
int z = 30;
int *p;
Давайте начнем с игры с x
. Мы устанавливаем p
для указания на x
, используя унарный оператор &
address-of:
p = &x; // int * = int *
Тип переменной p
равен int *
. Тип выражения &x
равен int *
, а значением выражения является адрес x
. Затем мы можем изменить значение от x
до p
, используя унарный оператор перенаправления *
:
*p = 15; // int = int
Так как тип переменной p
равен int *
тип выражения *p
равен int
, и это выражение обозначает тот же объект , что и выражение x
. Таким образом, в строке выше мы меняем значение, хранящееся в x
косвенно до p
. Мы можем сделать то же самое с y
и z
:
p = &y;
*p = 25;
p = &z;
*p = 35;
Хорошо, круто, но почему бы просто не назначить x
, y
и z
напрямую ? Почему go через боль присвоения их адресов p
и присвоения значений через *p
?
Обычно мы бы так не поступали, но есть случай, когда этого нельзя избежать. Предположим, мы хотим написать функцию, которая изменяет значение одного или нескольких ее параметров, например:
void foo( int x )
{
x = 2 * x;
}
и вызывать ее так:
int main( void )
{
int val = 2;
printf( "before foo: %d\n", val );
foo( val );
printf( "after foo: %d\n", val );
return 0;
}
Что мы хотим видеть это
before foo: 2
after foo: 4
но то, что мы получаем это
before foo: 2
after foo: 2
Это не работает, потому что C использует параметр- Соглашение о передаче, называемое «передачей по значению» - короче говоря, формальный параметр x
в определении функции обозначает отдельный объект в памяти, чем фактический параметр val
. Запись нового значения в x
не влияет на val
. Чтобы foo
изменил фактический параметр val
, мы должны передать указатель в val
:
void foo( int *x ) // x == &val
{ // *x == val
*x = *x * 2;
}
int main( void )
{
int val = 2;
printf( "before foo: %d\n", val );
foo( &val );
printf( "after foo: %d\n", val );
return 0;
}
Сейчас мы получаем ожидаемый результат - val
изменяется на foo
. Выражение *x
относится к тому же объекту, что и val
. И теперь мы можем написать что-то вроде
foo( &y ); // x == &y, *x == y
foo( &z ); // x == &z, *x == z
Это наш первый вариант использования - возможность функции изменять свои параметры.
Бывают случаи, когда во время выполнения программы вам нужно выделить немного дополнительной памяти. Поскольку это распределение происходит во время выполнения, невозможно указать имя для этой дополнительной памяти так же, как для обычной переменной. IOW, нет способа написать
int x = new_memory();
, потому что имена переменных не существуют во время выполнения (они не сохраняются в сгенерированном машинном коде). Опять же, мы должны обратиться к этой динамически выделенной памяти косвенно через указатель:
int *p = malloc( sizeof *p ); sizeof *p == sizeof (int)
Это выделяет достаточно места для одного объекта int
и присваивает адрес этого нового пространства p
. Вы можете выделить блоки произвольного размера:
int *arr = malloc( sizeof *arr * 100 );
выделяет достаточно места для 100 int
объектов и устанавливает arr
для указания на первый из них.
Это наш второй вариант использования - отслеживание динамически выделяемой памяти.
Краткое примечание о синтаксисе указателя. С операциями с указателями связаны два оператора. Унарный оператор
&
используется для получения адреса объекта, тогда как унарный
*
разыменовывает указатель. Предположим, у нас есть
int
объект с именем
x
и указатель на
int
с именем
p
:
p = &x; // assign the address of x to p
*p = 10; // assign a new value to whatever p points to
В объявлении унарный *
указывает, что объявленная вещь имеет тип указателя:
int *p; // p has type "pointer to int"
Когда вы инициализируете указатель в объявлении, например
int *p = &x;
p
, это , а не с разыменовкой. Это то же самое, что и запись
int *p;
p = &x;
Оператор *
связывается с объявленной вещью, не со спецификатором типа. Вы можете написать то же объявление, что и
int* p;
int * p;
int*p;
, и оно всегда будет анализироваться как int (*p);
.
Для любого типа T
выполняются следующие условия:
T *p; // p has type "pointer to T"
T *p[N]; // p has type "array of pointers to T"
T (*p)[N]; // p has type "pointer to array of T"
T *p(); // p has type "function returning pointer to T"
T (*p)(); // p has type "pointer to function returning T"
Сложные объявления указателя могут стать трудными для чтения, поскольку унарный *
является префиксным оператором и имеет более низкий приоритет чем операторы []
и ()
. Например:
T *(*(*foo)())[N];
foo
- указатель на функцию, возвращающую указатель на массив N-элементных указателей на T
.
С вашим кодом мы имеем дело с первым случаем - мы хотим, чтобы функция readstudent
изменяла содержимое существующего экземпляра struct student
. И readstudent
делает это, вызывая scanf
для чтения значений в каждом отдельном элементе:
scanf("%s", pstu->name);
scanf("%f", &pstu->grade1);
scanf("%f", &pstu->grade2);
Помните, что scanf
ожидает, что его аргументы будут указателями - опять же, мы пытаемся изменить содержимое объекта, поэтому мы должны передать указатель на этот объект в качестве параметра.
&pstu->grade1
вычисляется по адресу grade1
члена объекта, на который pstu
указывает . &pstu->grade2
соответствует адресу grade2
члена объекта, на который указывает pstu
.
Так что, черт возьми, происходит с pstu->name
?
Массивы особые в C. Если это не операнд операторов sizeof
или унарных &
или строковый литерал, используемый для инициализации массива в объявлении символов, например
char foo[] = "test";
* выражение из Тип "N-элемент массива T
" будет преобразован ("распад") в выражение типа "указатель на T
", а значением выражения будет адрес первого элемента массива.
Мы не будем go вникать в это, но это было преднамеренное проектное решение Ритча ie, когда он впервые создавал язык C, и оно действительно служит цели. Это также означает, что при большинстве обстоятельств массивы теряют свое «массивность», и в действительности вы имеете дело с указателем на первый элемент, а не с самим массивом. В случае вызова scanf
мы передаем эквивалент из &pstu->name[0]
.