В этой статье я обобщаю измерения указателей четырьмя китайскими иероглифами: «Два своих и три чужих»! Это совсем не читается гладко и не рифмуется, что это вообще такое? Это «Два своих и три чужих» в развёрнутом виде — это: свой адрес, своё значение, чужое значение, чужой адрес и чужой тип.
Думаю, можно говорить об указателях с точки зрения этих 5 измерений. Но перед этим я написал программу, которая включает все измерения указателей из «Двух своих и трёх чужих», а затем я объясню смысл каждого измерения по очереди. Посмотрим, так ли это.
В большинстве сценариев использования указателей этих 5 измерений должно быть достаточно, чтобы помочь вам понять. Однако в некоторых особых сценариях использования указателей метод 5 измерений может не помочь.
Предупреждение: впереди длинная статья. Если вы начнёте терять терпение, можете добавить эту статью в закладки и продолжить чтение, когда у вас будет время.
1. Код программы
1.1. Код
Пример
Результат выполнения выглядит следующим образом:

В своих статьях, когда я рассказываю о вещах, которые понимаю, я люблю использовать очень простые программы для иллюстрации. Поэтому те эксперты, которые считают, что мысль обязательно нужно иллюстрировать непостижимой программой, или которые свысока смотрят на низкоуровневые программы, пожалуйста, проигнорируйте меня ^_^.
2.2. Переменная int para
В программе:
int para = 1;
printf("变量para自己的地址是: 0X%x\n", ¶);
printf("变量para自己的值是: 0X%x\n", para);
В программе определена переменная para, у которой есть собственное значение данных и собственный адрес хранения; это легко понять. Из вывода видно, что собственное значение данных переменной para — шестнадцатеричное "0X1", а её адрес — шестнадцатеричный "0X22feb4". Иными словами, в памяти область хранения, начинающаяся с адреса "0X22feb4", содержит 4 байта, в которых хранится значение данных "0X1". На моей машине переменная int занимает 4 байта; на вашей машине это может быть иначе.
Всем понятна переменная int para. Далее я нарисую схему, чтобы показать, как переменная para хранится в памяти, следующим образом:

Далее поговорим о концепции "Два своих и три чужих".
2. Два своих и три чужих
"Два своих и три чужих" в развёрнутом виде — это: собственный адрес, собственное значение, чужое значение, чужой адрес и чужой тип.
2.1. Собственный адрес
2.1.1 Понятие "собственный адрес"
"Собственный адрес" — сокращение от "собственного адреса". Как переменная, указатель pInt, подобно переменной int para, также нуждается в области хранения в памяти, и эта область хранения также имеет начальный адрес; то есть переменная-указатель pInt также имеет собственный адрес. Как упоминалось выше, адрес переменной para — "0X22feb4". Итак, каков адрес переменной-указателя pInt?
2.1.2 Получение «собственного адреса»
Все мы знаем, что «&» — это оператор взятия адреса. В программе:
printf("指针变量pInt自己的地址是: 0X%x\n", &pInt);
Она получает адрес переменной-указателя pInt через «&». Из вывода адрес переменной-указателя pInt — «0X22feb8». На моей машине переменная-указатель pInt также занимает 4 байта, поэтому переменная-указатель pInt хранится в 4-байтовой области, начиная с адреса «0X22feb8».
2.1.3 Обозначение «собственного адреса» в коде
В коде распространённый способ обозначить «собственный адрес» переменной-указателя pInt таков:
&pInt;
Теперь давайте доработаем эту схему, добавив «собственный адрес» переменной-указателя pInt, чтобы показать, как переменная-указатель pInt хранится в памяти.

«Собственный адрес» — это просто вот это. Видите, ничего особенно сложного, правда?
2.2 Собственное значение
2.2.1 Понятие «собственного значения»
«Собственное значение» — это сокращение от «значение собственных данных». Будучи переменной, указатель pInt, как и переменная para, также имеет своё собственное значение данных.
2.2.2 Получение «собственного значения»
Как упоминалось выше, собственное значение данных переменной para — «1». Тогда каково собственное значение данных переменной-указателя pInt? В программе:
pInt = ¶
printf("指针变量pInt自己的值是: 0X%x\n", pInt);
С помощью оператора «&» я присвоил значение адреса переменной para переменной-указателю pInt и использовал printf для вывода значения данных переменной-указателя pInt. Из вывода видно, что собственное значение данных переменной-указателя pInt — «0X22feb4». Отметим также, что адрес переменной para тоже «0X22feb4», поэтому,
pInt = ¶
Суть этой инструкции — передать адрес переменной para собственному значению переменной-указателя pInt, тем самым связав переменную-указатель pInt и переменную para вместе.
Как упоминалось в «собственном адресе», значение данных указателя pInt хранится в 4 байтах памяти, начиная с адреса «0X22feb8». Иными словами, 4 байта памяти, начиная с адреса «0X22feb8», используются для хранения значения данных «0X22feb4».
2.2.3 Обозначение в коде для «собственного значения»
В коде распространённые способы представления «собственного значения» переменной-указателя pInt таковы:
pInt;
Существует также такое обозначение в коде:
pInt + N; pInt - N;
Это обозначение означает прибавление числа N к «собственному значению» pInt или вычитание числа N из него; об этом будет сказано при обсуждении свойства «другого типа». Существует также такое обозначение:
pIntA - pIntB;
Это обозначение указывает, что две переменные-указатели используют свои «собственные значения» для выполнения вычитания.
2.2.4 Схематическая диаграмма
Теперь продолжим уточнять схему выше, добавив собственное значение переменной-указателя pInt.

Итак, вообще говоря, для переменной-указателя pInt «собственное значение» — это её собственное значение данных; для других переменных типа int это их адрес.
2.3 Другой адрес
2.3.1 Понятие «чужой адрес»
Понятие «чужой адрес» означает «адрес другого». Фактически, когда выше упоминалось «собственное значение», понятие «чужой адрес» уже упоминалось, хотя и не очень явно.
2.3.2 Получение «чужого адреса»
Целочисленная переменная para хранится в 4 байтах, начиная с адреса памяти «0X22feb4». В программе я использовал
pInt = ¶
чтобы передать адрес переменной para переменной-указателю pInt, тем самым связав переменную-указатель pInt и переменную para вместе. В более фундаментальном смысле это присваивает «адрес другого» «собственному значению» переменной-указателя pInt. Здесь «другого» в «адресе другого» относится к переменной para, а «адрес» в «адресе другого» относится к адресу переменной para. Обратите внимание: «чужой адрес» и «собственное значение» идентичны по значению данных. Итак, вы что-нибудь поняли?
Во многих учебниках говорится: «указатель — это переменная-адрес, которая хранит адрес другой переменной». Говоря прямо, они утверждают, что значение данных измерения «чужой адрес» равно значению данных измерения «собственное значение»; просто учебники не формулируют это так ясно.
2.3.3 Схема
Давайте снова уточним эту схему, на этот раз добавив понятие «чужой адрес».

2.4 Чужое значение
2.4.1 Понятие «чужое значение»
«Чужое значение» означает «значение данных другого».
2.4.2 Получение «чужого значения»
В программе я использовал
pInt = ¶
чтобы передать адрес переменной para в "собственное значение" переменной-указателя pInt, тем самым связав переменную-указатель pInt и переменную para вместе. В этот момент "другой" в "значении данных другого" относится к переменной para, а "значение данных" в "значении данных другого" относится к значению данных "1" переменной para. В программе я использовал
printf("指针变量pInt的他值是: 0X%x\n", *pInt);
: А это? На самом деле это *(pInt + 1).
2.4.3 Синтаксис кода для "другого значения"
Те конструкции кода, которые вы часто видите в коде, например конструкция *pInt, что они означают? На самом деле они вычисляют "другое значение" переменной-указателя pInt!
А как насчёт этих конструкций: *(pInt + 1), *pInt + 1, pInt
*(pInt + 1): Если рассматривать pInt + 1 как другой указатель, например,
int *pTemp = pInt + 1;
то *(pInt + 1) по сути вычисляет "другое значение" переменной-указателя pTemp;
*pInt + 1: Это прибавление 1 к "другому значению" pInt;
pInt
2.4.4 Схема
Продолжайте улучшать приведённую выше схему, на этот раз добавив понятие "другого значения":

2.5 Другой тип
2.5.1 Понятие «другого типа»
«Другой тип» — это сокращение от «тип другого». В программе мы видим, что при объявлении переменной-указателя pInt это записывается так:
int *pInt = NULL;
«int» перед переменной-указателем pInt не означает, что «собственное значение» pInt является значением данных типа int; скорее, оно означает, что «другое значение» pInt является значением данных типа int. Здесь «другое значение» pInt — это значение данных «0X1» переменной para, поэтому «int» перед pInt указывает, что значение данных «0X1» имеет тип «int».
Короче говоря, тип, указываемый при объявлении указателя, используется для определения «другого значения», а не «собственного значения».
Теперь посмотрите ещё раз, когда объявляется переменная para:
int para = 1;
«int» перед переменной para означает, что тип para — целочисленный тип. В этом случае «int» для para — это «собственный тип», то есть «её собственный тип». Только тип в объявлении указателя является «другим типом», типом сущности, на которую указывают.
Поскольку «другой тип» используется для определения «другого значения», какой смысл добавлять этот «другой тип» при объявлении указателя? Читайте дальше!
В программе есть следующий фрагмент кода:
int arr_int[2] = {1, 2};
pInt = arr_int;
printf("arr_int第一个元素arr_int[0]的地址是: 0X%x\n", pInt);
printf("arr_int第二个元素arr_int[1]的地址是: 0X%x\n", pInt + 1);
Я присваиваю адрес целочисленного массива arr_int переменной-указателю pInt. Тогда «собственный адрес» pInt не меняется, он по-прежнему 0X22feb8, но его «собственное значение» меняется.
Только что «собственное значение» pInt было «0X22feb4», то есть адресом переменной para. Теперь оно становится «0X22feac», то есть адресом первого элемента массива arr_int. То есть «собственный адрес» pInt не меняется, но его «собственное значение» может изменяться.
Теперь давайте посмотрим на разницу между «pInt» и «pInt + 1». Здесь выполняется арифметика с «собственным значением» pInt. По результату выполнения «собственное значение» pInt в это время равно «0X22feac», а «собственное значение» pInt + 1 равно «0X22feb0». Вы заметили? Они различаются ровно на 4 байта, и данные типа «int» тоже занимают ровно 4 байта.
Вы можете подумать, что поскольку pInt + 1 добавляет 1 к «собственному значению», должно получиться «0X22feac + 1» = «0X22fead». Почему это не так? Это трюк, который проделывает «другой тип» переменной-указателя pInt.
Значение «другого типа», выражаясь простыми словами, таково: «Эй, дружище pInt, твоё другое значение — это значение данных типа int. В дальнейшем, когда ты используешь своё значение +1, +2 или -1, -2, не добавляй по глупости 1 байт, 2 байта и не вычитай 1 байт, 2 байта. Тип int занимает 4 байта, поэтому ты должен принимать 4 байта за одну единицу и добавлять 1*4 байта, 2*4 байта или вычитать 1*4 байта, 2*4 байта, понял? Ах да, кстати, N в pInt + N может быть положительным или отрицательным.»
Конечно, если данные типа «int» на вашей машине занимают 8 байт, то pInt + 1 добавляет 8 байт к «собственному значению», pInt + 2 добавляет 8 * 2 = 16 байт к «собственному значению». Вот в чём идея.
Я привёл ещё один пример в программе, чтобы проиллюстрировать этот «другой тип». Программа выглядит так:
double *pDouble = NULL;
double arr_double[2] = {1.1, 2.2};
pDouble = arr_double;
printf("arr_double第一个元素arr_double[0]的地址是: 0X%x\n", pDouble);
printf("arr_double第二个元素arr_double[1]的地址是: 0X%x\n", pDouble + 1);
На этот раз объявите переменную-указатель pDouble. Её «другой тип» — «double», её «собственное значение» — адрес массива arr_double, а её «другое значение» — значение данных «1.1» элемента arr_double
3. Резюме
Пришло время подвести итог.
Я объявляю переменную-указатель:
type *pType = NULL;
pType имеет 5 измерений, а именно:
pType = (собственный адрес, собственное значение, другой адрес, другое значение, другой тип);
3.1 Собственный адрес: то есть «свой собственный адрес»
Как переменная, переменная-указатель pType также имеет собственный адрес. Обычный синтаксис в коде — «&pType».
Собственный адрес нечасто используется в обычных программах. Если он используется, это связано с «указателем на указатель», что является другой темой и не будет рассматриваться в этой статье;
3.2 Собственное значение: т.е. «собственное значение данных»
Как переменная, переменная-указатель pType также имеет собственное значение данных. Синтаксис кода — "pType".
С собственным значением также можно выполнять сложение и вычитание. Обычный синтаксис кода включает "pType + N", "pType - N", "pType2 - pType1" и т.д.
3.3 Чужой адрес: т.е. «адрес другого»
Собственное значение переменной-указателя pType, помимо представления её собственного значения данных, также представляет адрес переменной типа "type", привязанной к pType. Вообще говоря, «собственное значение» и «чужой адрес» переменной-указателя pType совпадают по значению данных.
Обычный способ привязать переменную типа "type" к pType: pType = &variable;
3.4 Чужое значение: т.е. «значение данных другого»
После того как переменная типа "type" привязана к pType, переменная-указатель pType может получить значение переменной типа type, т.е. «чужое значение», с помощью некоторого синтаксиса кода. Обычный синтаксис кода включает "*pType", "pType->" и т.д.
И эти шаблоны кода: "*(pType + N)", "*(pType - N)", "pType[N]" также получают «чужое значение», но это требует особого пояснения:
Можно рассматривать pType + N как:
type *pTemp = pType + N;
"*(pType + N)" фактически вычисляет «чужое значение» переменной-указателя pTemp.
Тогда вам будет легко понять "*(pType - N)";
"pType[N]" на самом деле — это "*(pType + N)". Просто запомните это.
3.5 Other-type: то есть "тип другого"
При объявлении переменной-указателя pType предшествующий "type" используется не для изменения "собственного значения" pType, а для изменения "другого значения". То есть "type" не означает, что "собственное значение" pType — это значение данных типа "type", а означает, что "другое значение" pType — это значение данных типа "type".
Роль "другого типа" в коде в основном состоит в том, что при вычислении "pType + N" или "pType - N" pType должен прибавить или вычесть (N * sizeof(type)) байт.
Указатели всегда вызывают головокружение, вероятно, потому что вы запутываетесь в одном или нескольких из этих 5 измерений. Хорошенько разберитесь в этих 5 измерениях, и указатели — всего лишь бумажный тигр.
4. Разбор упражнений
После того как мы разобрали 5 измерений, как же обойтись без практических упражнений? Ниже приведено несколько упражнений, все они связаны с указателями и способны довести новичков до головокружения и желания сдаться. Я использую эти 5 измерений, чтобы разобрать эти задачи. Проверьте, не стало ли немного легче!
4.1 Суммирование элементов массива
4.1.1 Программа
Первый пример — очень распространённая программа: нахождение суммы элементов массива. Программа выглядит так:
Пример
Программа очень проста. Сначала она выводит все элементы массива, затем вычисляет сумму всех элементов. Результат выполнения выглядит следующим образом:

4.1.2 Интерпретация с «два своих, три чужих»
4.1.2.1 Вывод элементов массива
При выводе элементов массива код выглядит следующим образом:
pArr = arr;
printf("%d ", pArr[index]);
Эта строка кода эквивалентна
pArr = arr;
printf("%d ", *( pArr + index));
Здесь «своё значение» и «другой тип» используются для сложения, а «другое значение» используется для получения элементов массива.
"Собственное значение": Сначала код присваивает значение данных имени массива arr "собственному значению" pArr. А что является значением данных имени массива arr? Это адрес элемента arr
;
int *pTemp = pArr + index;
;
"Другой тип": другим типом pArr является "int". Для pArr + index сколько байтов добавляется к собственному значению pArr? Поскольку другим типом pArr является int, то pArr + index означает собственное значение pArr плюс index * sizeof(int) байтов, верно!
pArr: можно записать как pArr + 0, то есть добавление 0 * 4 = 0 байтов. В этот момент собственным значением pTemp является адрес arr
pArr + 1: добавляем 1 * 4 = 4 байта. В этот момент собственным значением pTemp является адрес arr
pArr + 2: добавляем 2 * 4 = 8 байтов. В этот момент собственным значением pTemp является адрес arr
Таким образом, pArr + index перебирает адреса всех элементов массива.
Вы обнаружите, что собственное значение pTemp постоянно меняется; собственное значение и собственный адрес pArr остаются неизменными.
"Другое значение": Поскольку pArr + index может перебирать адреса всех элементов массива, то с помощью *(pArr + index), то есть *pTemp, можно получить другое значение pTemp и, таким образом, перебрать значения всех элементов массива!
4.1.2.2 Суммирование элементов массива
Код, используемый для суммирования элементов массива, выглядит так:
sum = sum + *(pArr + index);
Основываясь на моём только что проведённом анализе вывода элементов массива, вам всем должно быть ясно, как pArr работает в этом коде!
pArr + index по-прежнему использует собственное значение pArr для сложения, получая собственное значение временного указателя pTemp. Это собственное значение pTemp является адресом каждого элемента массива;
Затем используйте *(pArr + inedx), то есть *pTemp, чтобы получить другое значение временного указателя pTemp, которое является значением каждого элемента.
Наконец, сложите все значения pTemp, чтобы вычислить сумму элементов массива.
4.1.2.3 Итоги
Попробуйте понять распространённые выражения в коде, такие как pArr, pArr + index, *(pArr + index), pArr[index], *pArr + index, с точки зрения «двух своих и трёх чужих»!
4.2 Массив указателей
Массив указателей — это то, что объединяет указатели и массивы. Для начинающих это может быть относительно трудно для понимания. Массивы указателей — довольно большая тема. Что касается связанных концепций, обращайтесь к общим учебникам по языку C. Здесь мы только используем концепцию «двух своих и трёх чужих», чтобы объяснить связанные с указателями концепции в программе.
4.2.1 Программа
Для массивов указателей я привёл следующий пример:
Пример
Результат выполнения выглядит следующим образом:

4.2.2 Интерпретация с "двумя собственными и тремя другими"
4.2.2.1 Вывод всех строк
Сначала посмотрим на определение массива указателей:
char *arr[3] = {"abc", "def", "ghi"};
Каждый элемент этого массива выглядит как строка, но по сути это выглядит так:
char *pChar1 = "abc", *pChar2 = "def", *pChar3 = "ghi";
char *arr[3] = {pChar1, pChar2, pChar3};
Каждый элемент массива arr на самом деле является указателем, "другим типом" которого является "char".
arr
printf("%s ", arr[0]);
По сути:
printf("%s ", pChar1);
Используйте собственное значение (или другой адрес) pChar1, чтобы вывести 'b' и 'c' по одному, начиная с адреса символа 'a'.
pChar2 и pChar3 можно понимать одинаково.
4.2.2.2 Вывод каждого символа первой строки "abc"
Код выглядит следующим образом:
char *pArr = arr[0];
printf("%c ", *(pArr + index) );
Присвоить arr
pArr + index — это добавление index * sizeof(char) байт к собственному значению pArr, что даёт временную переменную-указатель pTemp:
char *pTemp = pArr + index;
Собственное значение (или другой адрес) этого указателя pTemp будет по очереди адресами символов 'a', 'b', 'c'; то есть другое значение pTemp по очереди также будет символами 'a', 'b', 'c'. Таким образом, указатель pTemp по очереди пройдёт каждый символ строки "abc".
4.3 Связный список
В связных списках указатели используются чаще всего. В таких операциях, как вставка и удаление узлов, всегда встречается код вроде следующего:
p2 = p1->next; p1->next = p3->next; ......
Что это, чёрт возьми? Аж голова кружится! Этот указатель next, тот указатель next, прыгают туда-сюда. Чёрт, у меня в голове каша. Ха-ха, а вот и метод 5-мерного анализа указателей! Однако что касается связных списков, думаю, мне лучше написать отдельную статью, чтобы их объяснить. Когда закончу объяснять связные списки, вернусь к этим указателям next. Скажу вам, суть связных списков именно такова. Как только вы в этом разберётесь, связные списки окажутся ещё большим бумажным тигром, чем указатели.
Автор: Stone Family's Fish
Источник: https://zhuanlan.zhihu.com/p/27974028