Предисловие: объяснение сложных типов

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

Ниже давайте начнём с простых типов и постепенно их разберём.

  • int p;-- Это обычная целочисленная переменная.
  • int *p;-- Сначала, начиная с p, сначала связывается с `*`.*Это указывает, что p — указатель; затем связывается с int, показывая, что тип содержимого, на которое указывает указатель, — int. Таким образом, p — указатель, возвращающий целочисленные данные.
  • int p[3]-- Сначала, начиная с p, сначала связывается с `[]`.[]Это указывает, что p — массив; затем связывается с int, показывая, что элементы в массиве — целые числа. Таким образом, p — массив, состоящий из целочисленных данных.
  • int *p[3];-- Сначала, начиная с p, сначала связывается с [], потому что приоритет [] выше, чем*`*`, поэтому p — массив; затем связывается с *, показывая, что элементы в массиве — указатели; затем связывается с int, показывая, что тип содержимого, на которое указывают указатели, — целочисленный. Таким образом, p — массив, состоящий из указателей, возвращающих целочисленные данные.
  • int (*p)[3];-- Сначала, начиная с p, сначала связывается с *, указывая, что p — указатель; затем связывается с [] (шаг с "()" можно игнорировать; он нужен только для изменения приоритета), указывая, что содержимое, на которое указывает указатель, — массив; затем связывается с int, указывая, что элементы в массиве — целые числа. Таким образом, p — указатель на массив, состоящий из целочисленных данных.
  • int **p;-- Сначала, начиная с p, сначала связывается с *, говоря, что p — указатель; затем снова связывается с *, указывая, что элемент, на который указывает указатель, — указатель; затем связывается с int, указывая, что элемент, на который указывает этот указатель, — целочисленные данные. Поскольку указатели второго уровня и выше редко используются в сложных типах, мы не будем рассматривать многоуровневые указатели в более сложных типах позже; в лучшем случае мы будем рассматривать только одноуровневые указатели.
  • int p(int);-- Начиная с p, сначала сочетается с (), что означает, что p — это функция; затем анализируем внутри (), показывая, что функция имеет параметр целочисленной переменной; затем сочетается с внешним int, показывая, что возвращаемое значение функции — целочисленные данные.
  • int (*p)(int);-- Начиная с p, сначала сочетается с оператором указателя `*`, что означает, что p — указатель; затем сочетается с (), что означает, что указатель указывает на функцию; затем сочетается с int внутри (), что означает, что функция имеет параметр типа int; затем сочетается с самым внешним int, что означает, что тип возвращаемого значения функции — целочисленный. Таким образом, p — указатель на функцию, которая имеет целочисленный параметр и тип возвращаемого значения которой — целочисленный.
  • int *(*p(int))[3];-- Этот тип пока можно пропустить; он слишком сложен. Начиная с p, сначала сочетается с (), что означает, что p — это функция; затем заходим внутрь (), сочетается с int, что означает, что функция имеет параметр целочисленной переменной; затем сочетается с внешним *, что означает, что функция возвращает указатель; затем переходим на самый внешний уровень, сначала сочетается с [], что означает, что возвращаемый указатель указывает на массив; затем сочетается с *, что означает, что элементы в массиве — указатели; затем сочетается с int, что означает, что содержимое, на которое указывают указатели, — целочисленные данные. Таким образом, p — это функция, параметр которой является целым числом и которая возвращает переменную-указатель, указывающую на массив, состоящий из переменных-указателей на целые числа.

Вот, пожалуй, и всё, что нужно сказать; наша задача состоит только в этом. Как только вы разберётесь в этих типах, остальные типы для нас — пара пустяков. Однако мы обычно не используем слишком сложные типы, так как это значительно снижает читаемость программы. Пожалуйста, используйте их с осторожностью. Нескольких типов выше нам уже достаточно.


1. Подробное объяснение указателей

Указатель — это специальная переменная, хранимое значение которой интерпретируется как адрес в памяти. Чтобы понять указатель, нужно разобраться в четырёх аспектах: тип указателя, тип, на который указывает указатель, значение указателя (или область памяти, на которую указывает указатель), и область памяти, занимаемая самим указателем. Объясним каждый отдельно.

Сначала объявим несколько указателей для примера:

Пример:

int *ptr; char *ptr; int **ptr; int (*ptr)[3]; int *(*ptr)[4];

1. Тип указателя

С точки зрения синтаксиса, нужно лишь удалить имя указателя из объявления указателя; оставшаяся часть — это тип указателя. Это тип, которым обладает сам указатель. Посмотрим на типы различных указателей в примере 1:

  • 1, int *ptr;: Тип указателя —int*
  • 2, char *ptr;: Тип указателя —char*
  • 3, int **ptr;: Тип указателя —int**
  • 4, int (*ptr)[3];: Тип указателя —int(*)[3]
  • 5, int *(*ptr)[4]; : Тип указателя —int*(*)[4]

Как вам это? Способ определения типа указателя очень прост, не так ли?

2. Тип, на который указывает указатель

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

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

  • 1, int*ptr;: тип, на который указывает указатель, —int
  • 2, char*ptr;: тип, на который указывает указатель, —char
  • 3, int**ptr;: тип, на который указывает указатель, —int*
  • 4, int(*ptr)[3];: тип, на который указывает указатель, —int()[3]
  • 5, int*(*ptr)[4]; : тип, на который указывает указатель, —int*()[4]

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

Тип указателя (то есть тип самого указателя) и тип, на который указывает указатель, — это два разных понятия. По мере более глубокого знакомства с C вы обнаружите, что разделение связанного с указателями понятия "тип" на два понятия — "тип указателя" и "тип, на который указывает указатель" — является одним из ключевых моментов для освоения указателей. Я прочитал много книг и заметил, что в некоторых плохо написанных книгах эти два понятия смешиваются, поэтому книги кажутся противоречивыми, и чем больше читаешь, тем больше запутываешься.

3. Значение указателя — или область памяти, или адрес, на который указывает указатель

Значение указателя — это числовое значение, хранящееся в самом указателе. Компилятор будет рассматривать это значение как адрес, а не как обычное числовое значение. В 32-битных программах значение указателя любого типа является 32-битным целым числом, так как в 32-битных программах все адреса памяти имеют длину 32 бита. Область памяти, на которую указывает указатель, начинается с адреса памяти, представленного значением указателя, и имеет длину sizeof(тип, на который указывает указатель). В дальнейшем, когда мы говорим, что значение указателя равно XX, это эквивалентно утверждению, что указатель указывает на область памяти с XX в качестве начального адреса; когда мы говорим, что указатель указывает на некоторую область памяти, это эквивалентно утверждению, что значение указателя является начальным адресом этой области памяти. Область памяти, на которую указывает указатель, и тип, на который указывает указатель, — это два совершенно разных понятия. В примере 1 тип, на который указывает указатель, уже существует, но так как указатель не был инициализирован, область памяти, на которую он указывает, не существует или не имеет смысла.

С этого момента каждый раз, когда вы встречаете указатель, вы должны спрашивать: Каков тип этого указателя? На какой тип он указывает? Куда указывает указатель? (Обратите особое внимание)

4. Область памяти, занимаемая самим указателем

Сколько памяти занимает сам указатель? Чтобы измерить это, достаточно воспользоваться функцией sizeof(тип указателя). На 32-битной платформе сам указатель занимает 4 байта. Понятие памяти, занимаемой самим указателем, полезно при определении того, является ли выражение с указателем (объясняется позже) lvalue.


2. Арифметические операции над указателями

К указателю можно прибавить целое число или вычесть из него целое число. Смысл этой операции для указателей отличается от обычного числового сложения и вычитания; она выполняется в единицах элементов. Например:

char a[20]; int *ptr=(int *)a; // Приведение типа не изменяет тип a ptr++;

В приведённом выше примере тип указателя ptr — int*, а тип, на который он указывает, — int. Он инициализируется так, чтобы указывать на целочисленную переменную a. В следующем операторе указатель ptr увеличивается на 1. Компилятор обрабатывает это следующим образом: он добавляет sizeof(int) к значению указателя ptr. В 32-битной программе это добавляет 4, потому что int занимает 4 байта в 32-битной программе. Поскольку адреса измеряются в байтах, адрес, на который указывает ptr, увеличился на 4 байта от адреса исходной переменной a в сторону старших адресов. Поскольку длина типа char составляет один байт, изначально ptr указывал на четыре байта, начиная с элемента 0 массива a; в этот момент он указывает на четыре байта, начиная с элемента 4 массива a. Мы можем использовать указатель и цикл для обхода массива. Посмотрите на пример:

int array[20]={0}; int *ptr=array; for(i=0;i<20;i++) { (*ptr)++; ptr++; }

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

Рассмотрим другой пример:

char a[20]="You_are_a_girl"; int *ptr=(int *)a; ptr+=5;

В этом примере ptr увеличивается на 5. Компилятор обрабатывает это так: добавляет 5 раз sizeof(int) к значению указателя ptr; в 32-битной программе это добавление 5 раз по 4 = 20. Поскольку единица адреса — байт, адрес, на который теперь указывает ptr, переместился на 20 байт в сторону старших адресов по сравнению с адресом, на который он указывал до прибавления 5.

В этом примере до прибавления 5 ptr указывает на четыре байта, начиная с элемента 0 массива a. После прибавления 5 ptr указывает за пределы допустимого диапазона массива a. Хотя на практике это может вызвать проблемы, синтаксически это допустимо. Это также отражает гибкость указателей. Если бы в приведённом выше примере ptr был уменьшен на 5, процесс был бы почти таким же, за исключением того, что значение ptr уменьшилось бы на 5 * sizeof(int), а адрес, на который указывает новый ptr, сместился бы на 20 байт в сторону младших адресов по сравнению с адресом, на который изначально указывал ptr.

Теперь позвольте привести другой пример: (заблуждение)

#include<stdio.h> int main() { char a[20]=" You_are_a_girl"; char *p=a; char **ptr=&p; //printf("p=%d\n",p); //printf("ptr=%d\n",ptr); //printf("*ptr=%d\n",*ptr); printf("**ptr=%c\n",**ptr); ptr++; //printf("ptr=%d\n",ptr); //printf("*ptr=%d\n",*ptr); printf("**ptr=%c\n",**ptr); }

Заблуждение 1. Вывод — Y и o.

Непонимание: ptr — это указатель второго уровня на char. При выполнении ptr++ указатель увеличивается на sizeof(char), поэтому вывод получается как выше. Возможно, такой результат получается лишь у немногих.

Заблуждение 2.Вывод — Y и a. Непонимание: ptr указывает на тип char *. При выполнении ptr++ указатель увеличивается на sizeof(char *) (некоторые могут подумать, что это значение равно 1, что привело бы к ответу из заблуждения 1; на самом деле это значение должно быть 4, см. предыдущий материал), то есть &p+4. Разве тогда одна операция разыменования не укажет на пятый элемент массива? Разве вывод не будет пятым элементом массива? Ответ — нет.

Правильное решение:Тип ptr — char **, а тип, на который он указывает, — char *. Адрес, на который он указывает, — это адрес p (&p). При выполнении ptr++ указатель увеличивается на sizeof(char*), то есть &p+4. Тогда куда указывает *(&p+4)? Можете спросить об этом у Бога; может быть, Он скажет вам, куда. Так что итоговый вывод будет случайным значением или, возможно, недопустимой операцией.

. Но если рассматривать array как указатель, он указывает на 0-й элемент массива. Его тип — int*, а тип, на который он указывает, — это тип элемента массива, а именно int. Поэтому совсем неудивительно, что *array равно 0. Аналогично, array+3 — это указатель на 3-й элемент массива, так что

После прибавления (или вычитания) целого числа n к указателю ptrold результатом является новый указатель ptrnew. Тип ptrnew совпадает с типом ptrold, а тип, на который указывает ptrnew, также совпадает с типом, на который указывает ptrold. Значение ptrnew будет увеличено (или уменьшено) на n * sizeof(тип, на который указывает ptrold) байт по сравнению со значением ptrold. То есть область памяти, на которую указывает ptrnew, сместится на n * sizeof(тип, на который указывает ptrold) байт в сторону более высоких (или более низких) адресов по сравнению с областью памяти, на которую указывает ptrold. Сложение и вычитание указателей: два указателя нельзя складывать; это недопустимая операция, поскольку после сложения результат указывает в неизвестном направлении и не имеет смысла. Два указателя можно вычитать, но они должны быть одного типа. Обычно это используется с массивами; я не буду вдаваться в подробности.


3. Операторы & и *

Здесь&— оператор взятия адреса,*— оператор косвенного обращения.

&aРезультатом операции является указатель. Тип указателя — это тип a плюс *, тип, на который указывает указатель, — это тип a, а адрес, на который указывает указатель, — это, ну, адрес a.

*pРезультатом операции могут быть самые разные вещи. Короче говоря, результатом *p является объект, на который указывает p. Этот объект обладает следующими характеристиками: его тип — это тип, на который указывает p, а адрес, который он занимает, — это адрес, на который указывает p.

Пример:

int a=12; int b; int *p; int **ptr; p=&a; // Результат &a — указатель, его тип — int*, а тип, на который он указывает, — // int. Адрес, на который он указывает, — это адрес a. *p=24; // Результат *p, здесь его тип — int, а адрес, который он занимает, — // адрес, на который указывает p. Очевидно, *p — это переменная a. ptr=&p; // Результат &p — указатель. Тип этого указателя — тип p плюс *, // здесь это int **. Тип, на который указывает этот указатель, — это тип p, а это // здесь это int*. Адрес, на который указывает этот указатель, — это адрес самого указателя p. *ptr=&b; // *ptr — указатель, и результат &b тоже указатель. Более того, эти два указателя // имеют одинаковый тип и одинаковый тип, на который указывают, поэтому с помощью &b можно выполнить присваивание *ptr // вполне допустимо. **ptr=34; // Результат *ptr — это то, на что указывает ptr; здесь это указатель, // применение ещё одной операции * к этому указателю даёт переменную int.

4. Выражения с указателями

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

Ниже приведены некоторые примеры выражений с указателями:

Пример:

int a,b; int array[10]; int *pa; pa=&a; // &a — это выражение с указателем. Int **ptr=&pa; // &pa также является выражением с указателем. *ptr=&b; // *ptr и &b — оба являются выражениями с указателями. pa=array; pa++; // Это тоже выражение указателя.
char *arr[20]; char **parr=arr; // Если arr рассматривать как указатель, arr также является выражением указателя. char *str; str=*parr; // *parr — это выражение указателя. str=*(parr+1); // *(parr+1) — это выражение указателя. str=*(parr+2); // *(parr+2) — это выражение указателя.

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

Итак, когда результирующий указатель выражения указателя уже явно имеет память, занимаемую самим указателем, то это выражение указателя является lvalue; в противном случае оно не является lvalue. В примере 7 &a не является lvalue, потому что он ещё не занимает явную память. *ptr является lvalue, потому что указатель *ptr уже занимает память. Фактически *ptr — это указатель pa. Поскольку pa уже имеет собственную позицию в памяти, конечно, *ptr также имеет собственную позицию.


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

Имя массива фактически можно рассматривать как указатель. Посмотрите на следующий пример:

int array[10]={0,1,2,3,4,5,6,7,8,9},value; value=array[0]; // Это также можно записать как: value=*array; value=array[3]; // Это также можно записать как: value=*(array+3); value=array[4]; // Это также можно записать как: value=*(array+4);

В приведённом выше примере обычно имя массива array представляет сам массив, с типом int*(array+3)равно 3. Остальное можно вывести по аналогии.

Пример:

char *str[3]={ "Hello,thisisasample!", "Hi,goodmorning.", "Helloworld" }; char s[80]; strcpy(s,str[0]); // Это также можно записать как strcpy(s,*str); strcpy(s,str[1]); // Это также можно записать как strcpy(s,*(str+1)); strcpy(s,str[2]); // Это также можно записать как strcpy(s,*(str+2));

В приведённом выше примере str — это массив из трёх элементов, и каждый элемент массива является указателем, причём каждый указатель указывает на строку. Если имя массива указателей str рассматривать как указатель, он указывает на 0-й элемент массива. Его тип — char **, а тип, на который он указывает, — char *.

*str — это тоже указатель. Его тип — char *, а тип, на который он указывает, — char. Адрес, на который он указывает, — это адрес первого символа строки "Hello,thisisasample!", т. е. адрес 'H'. Примечание: строка эквивалентна массиву; она хранится в памяти в виде массива. Однако строка — это константа-массив, её содержимое нельзя изменять, и она может быть только rvalue. Если рассматривать её как указатель, то она одновременно является указателем на константу и константным указателем.

str+1 — это тоже указатель. Он указывает на 1-й элемент массива. Его тип — char**, а тип, на который он указывает, — char*.

*(str+1) — это тоже указатель. Его тип — char*, а тип, на который он указывает, — char. Он указывает на первый символ 'H' строки "Hi,goodmorning.".

Теперь рассмотрим вопрос об имени массива для массива (где то, что хранится в массиве, также является массивом):

Массив объявляетсяTYPE array[n], тогда имя массива array имеет два значения:

  • Во-первых, он представляет весь массив; его тип —TYPE[n];

  • Во-вторых, это константный указатель; тип этого указателя —TYPE*, а тип, на который указывает этот указатель, —TYPE, то есть тип элемента массива. Область памяти, на которую указывает этот указатель, — это 0-й элемент массива. Сам указатель занимает отдельную область памяти; обратите внимание, что она отличается от области памяти, занимаемой 0-м элементом массива. Значение этого указателя нельзя изменять, то есть выражения вроде array++ неверны. В разных выражениях имя массива array может играть разные роли. В выражении sizeof(array) имя массива array представляет сам массив, поэтому в этом случае sizeof измеряет размер всего массива.

В выражении *array array играет роль указателя, поэтому результатом этого выражения является значение 0-го элемента массива.sizeof(*array)Измеряется размер элемента массива.

В выражении array+n (где n=0, 1, 2, ...) array действует как указатель, поэтому результатом array+n является указатель, тип которого —TYPE *, тип, на который он указывает, — TYPE, и он указывает на n-й элемент массива. Следовательно, sizeof(array+n) измеряет размер типа указателя. В 32-битной программе результат равен 4.

Пример:

int array[10]; int (*ptr)[10]; ptr=&array;:

В предыдущем примере ptr — указатель, тип которого —int(*)[10], тип, на который он указывает, — int

В этом разделе упоминалась функция sizeof(). Теперь позвольте спросить: sizeof(имя указателя) измеряет размер собственного типа указателя или размер типа, на который он указывает?

Ответ — первый. Например:

int(*ptr)[10];

Тогда в 32-битной программе имеем:

sizeof(int(*)[10])==4
sizeof(int[10])==40
sizeof(ptr)==4

На самом деле sizeof(object) всегда измеряет размер собственного типа объекта, а не размер какого-либо другого типа.


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

Можно объявить указатель на объект структурного типа. Пример:

struct MyStruct { int a; int b; int c; }; struct MyStruct ss={20,30,40}; // Объявляет объект структуры ss и инициализирует члены ss значениями 20, 30 и 40. struct MyStruct *ptr=&ss; // Объявляет указатель на объект структуры ss. Его тип — // MyStruct *, а тип, на который он указывает, — MyStruct. int *pstr=(int*)&ss; // Объявляет указатель на объект структуры ss. Но pstr и // тип, на который указывает ptr, различаются.

Как получить доступ к трём переменным-членам ss через указатель ptr?

Ответ:

ptr->a; //指向运算符,或者可以这们(*ptr).a,建议使用前者
ptr->b;
ptr->c;

А как получить доступ к трём переменным-членам ss через указатель pstr?

Ответ:

*pstr; //访问了ss 的成员a.
*(pstr+1); //访问了ss 的成员b.
*(pstr+2) //访问了ss 的成员c.

Хотя я отлаживал приведённый выше код в MSVC++6.0, вы должны знать, что использование pstr таким способом для доступа к членам структуры нестандартно. Чтобы объяснить, почему это нестандартно, давайте посмотрим, как получить доступ к каждому элементу массива через указатель: (замените структуру массивом)

int array[3]={35,56,37}; int *pa=array; // Способ доступа к трём элементам массива array через указатель pa таков: *pa; // Доступ к элементу 0 *(pa+1); // Доступ к элементу 1 *(pa+2); // Доступ к элементу 2

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

Все компиляторы C/C++, размещая элементы массива, всегда хранят отдельные элементы массива в непрерывной памяти, без промежутков между элементами. Но при хранении членов объекта структуры в некоторых средах компиляции может требоваться выравнивание по словам, двойным словам или иное выравнивание, и между соседними членами может потребоваться вставить несколько «байтов заполнения». Это приводит к возможным промежуткам в несколько байтов между членами.

Следовательно, в примере 12, даже если *pstr обращается к первой переменной-члену a объекта структуры ss, нет гарантии, что *(pstr+1) обязательно обратится к члену структуры b. Потому что между членом a и членом b может быть несколько байтов заполнения, и, возможно, *(pstr+1) как раз обращается именно к этим байтам заполнения. Это также доказывает гибкость указателей. Если ваша цель — проверить, есть ли байты заполнения между членами структуры, эй, на самом деле это хороший метод.

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


7. Связь между указателями и функциями

Можно объявить указатель как указатель на функцию.

int fun1(char *,int);
int (*pfun1)(char *,int);
pfun1=fun1;
int a=(*pfun1)("abcdefg",7); //通过函数指针调用函数。

Можно использовать указатель как формальный параметр функции. В операторе вызова функции выражение-указатель можно использовать в качестве фактического аргумента.

int fun(char *); inta; char str[]="abcdefghijklmn"; a=fun(str); int fun(char *s) { int num=0; for(int i=0;;) { num+=*s;s++; } return num; }

Функция fun в этом примере вычисляет сумму значений ASCII-кодов каждого символа в строке. Как упоминалось ранее, имя массива также является указателем. При вызове функции, когда str передаётся как фактический аргумент формальному параметру s, значение str фактически передаётся в s. Адрес, на который указывает s, становится таким же, как адрес, на который указывает str, но str и s занимают собственные области памяти. Выполнение инкремента на 1 для s внутри тела функции не означает, что str также одновременно инкрементируется на 1.


8. Преобразование типа указателя

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

float f=12.3; float *fptr=&f; int *p;

В приведённом выше примере, если мы хотим, чтобы указатель p указывал на вещественное число f, что нам делать?

Это следующий оператор?

p=&f;

Нет. Потому что тип указателя p — int *, а тип, на который он указывает, — int. Результат выражения &f — указатель; тип указателя — float *, а тип, на который он указывает, — float.

Эти два типа несовместимы, поэтому прямое присваивание не сработает. По крайней мере в моём MSVC++6.0 оператор присваивания указателя требует, чтобы типы по обе стороны оператора присваивания были одинаковыми, а также чтобы совпадали типы, на которые они указывают. Я не пробовал на других компиляторах; можете попробовать. Чтобы достичь нашей цели, нам нужно выполнить "принудительное преобразование типа":

p=(int*)&f;

Если есть указатель p и нам нужно изменить его тип и тип, на который он указывает, наTYEP *TYPE, то синтаксис таков:(TYPE *)p

Результатом такого принудительного преобразования типа является новый указатель. Тип нового указателя —TYPE *, а тип, на который он указывает, —TYPE, и адрес, на который он указывает, — это адрес, на который указывал исходный указатель. Ни одно из свойств исходного указателя p не изменяется. (Запомните это)

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

void fun(char*); int a=125,b; fun((char*)&a); void fun(char*s) { char c; c=*(s+3);*(s+3)=*(s+0);*(s+0)=c; c=*(s+2);*(s+2)=*(s+1);*(s+1)=c; }

Обратите внимание, что это 32-битная программа, поэтому тип int занимает четыре байта, а тип char занимает один байт. Функция fun изменяет порядок четырёх байтов целого числа на обратный. Вы заметили? В операторе вызова функции результат фактического аргумента &a является указателем; его тип — int *, а тип, на который он указывает, — int. Тип формального параметра-указателя — char *, а тип, на который он указывает, — char. Таким образом, при связывании фактического аргумента и формального параметра мы должны выполнить преобразование из int * в char *.

В связи с этим примером мы можем сделать так:

Представьте процесс преобразования компилятором: компилятор сначала создаёт временный указатель char *temp, затем выполняет temp=(char *)&a и наконец передаёт значение temp в s. Итак, окончательный результат: тип s — char *, тип, на который он указывает, — char, а адрес, на который он указывает, — начальный адрес a.

Мы уже знаем, что значение указателя — это адрес, на который он указывает. В 32-битной программе значение указателя фактически является 32-битным целым числом.

Можем ли мы напрямую присвоить целое число указателю в качестве его значения? Как в следующих операторах:

unsigned int a; TYPE *ptr; // TYPE — это int, char или тип структуры и т. д. a=20345686; ptr=20345686; // Наша цель — заставить указатель ptr указывать на адрес 20345686 ptr=a; // Наша цель — заставить указатель ptr указывать на адрес 20345686 // Попробуйте скомпилировать это. Вы обнаружите, что оба последних оператора неверны. Так неужели мы не можем достичь нашей цели? Нет, есть ещё один способ: unsigned int a; TYPE *ptr; // TYPE — это int, char, тип структуры и т. д. a=N // N должен представлять допустимый адрес; ptr=(TYPE*)a; // Ха-ха, так сойдёт.

Строго говоря, (TYPE *) здесь — не то же самое, что (TYPE *) при преобразовании типа указателя. Смысл (TYPE*) здесь — рассматривать значение целого числа без знака a как адрес. Выше подчёркивалось, что значение a должно представлять допустимый адрес; иначе при использовании ptr возникнет ошибка недопустимой операции. Подумайте, можно ли выполнить обратное: извлечь адрес, на который указывает указатель, то есть значение указателя, как целое число. Разумеется. Следующий пример демонстрирует извлечение значения указателя как целого числа, а затем присваивание этого целого числа указателю в качестве адреса:

int a=123,b; int *ptr=&a; char *str; b=(int)ptr; // Извлечь значение указателя ptr как целое число. str=(char*)b; // Присвоить значение этого целого числа указателю str в качестве адреса.

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


9. Проблемы безопасности указателей

Посмотрите на следующий пример:

char s='a'; int *ptr; ptr=(int *)&s; *ptr=1298;

Указатель ptr — это указатель типа int *, а тип, на который он указывает, — int. Адрес, на который он указывает, — это начальный адрес s. В 32-битной программе s занимает один байт, а тип int занимает четыре байта. Последний оператор изменяет не только один байт, занимаемый s, но и три байта, смежных с s в сторону старших адресов. Для чего используются эти три байта? Знает только компилятор, а программист вряд ли знает. Возможно, в этих трёх байтах хранятся очень важные данные, или, возможно, эти три байта случайно оказались фрагментом программного кода. Из-за неосторожного использования указателя значения этих трёх байтов были изменены! Это вызовет фатальную ошибку.

Рассмотрим другой пример:

char a; int *ptr=&a; ptr++; *ptr=115;

Этот пример, конечно, может пройти компиляцию и может быть выполнен. Но видите? После того как оператор 3 выполняет инкремент указателя ptr на 1, ptr указывает на область памяти в сторону старших адресов, смежную с целочисленной переменной a. Что находится в этой области памяти? Мы не знаем. Это могут быть очень важные данные или даже фрагмент кода.

И всё же оператор 4 фактически записывает данные в эту область памяти! Это серьёзная ошибка. Поэтому при использовании указателей программист должен очень чётко понимать: куда именно указывает мой указатель? При использовании указателей для доступа к массивам также нужно следить за тем, чтобы не выходить за нижнюю или верхнюю границу массива, так как это тоже может вызвать подобные ошибки.

При принудительном приведении типов указателей:ptr1=(TYPE *)ptr2Если sizeof(тип ptr2) больше, чем sizeof(тип ptr1), то использовать указатель ptr1 для доступа к области памяти, на которую указывает ptr2, безопасно. Если sizeof(тип ptr2) меньше, чем sizeof(тип ptr1), то использовать указатель ptr1 для доступа к области памяти, на которую указывает ptr2, небезопасно. Что касается того, почему это так, читатель должен суметь понять это, подумав об этом вместе с примером 18.