Инструменты пользователя

Инструменты сайта


pointer

Различия

Показаны различия между двумя версиями страницы.

Ссылка на это сравнение

pointer [2019/01/17 23:38]
pointer [2020/06/13 13:46] (текущий)
Строка 1: Строка 1:
 +====== Указатели в C++ ======
 +{{htmlmetatags>
 +metatag-description=(Указатели в языке программирования C++, получение адреса переменной, создание указателей, извлечение значения по указателю.)
 +}}
 +
 +  * [[Операторы динамического распределения памяти]]: new, delete.
 +**Указатель** (пойнтер, англ. pointer) - это переменная, содержащая адрес другой переменной. Тип данных pointer равен 4 байта. Указатели очень широко используются в языке C. Это происходит отчасти потому, что иногда они дают единственную возможность выразить нужное действие, а отчасти потому, что они обычно ведут к более компактным и эффективным программам, чем те, которые могут быть получены другими способами.
 +<note important>Следует четко понимать, что компилятору абсолютно безразлично, как написано объявление int *p или int* p.  Программист может выбрать свой стиль. Однако символы & и * лучше связывать с переменными, а не типом. Так как в соответствии с правилами языка C++ символ * (как и символ &) связывается с отдельной переменной, а не ее типом.</note>
 +<note tip>Адрес переменной - это адрес первого байта переменной.</note><note important>Указатель должен быть равен NULL или указывать на адрес переменной.</note>
 +
 +Так как указатель содержит адрес объекта, это дает возможность "косвенного" доступа к этому объекту через указатель.
 +  * **Объявление указателя**. <code>int * pi  = NULL;</code>
 +  * **Знак &** используется для получения адреса переменной. <code>
 +        pi = &i;
 + cout<<&i<<endl;//записи эквивалентны
 + cout<<pi<<endl;//записи эквивалентны</code>
 +  * **Разыменование** - изменение значения переменной на который указывает указатель.<code>*pi=48;</code>
 +
 +<file>
 +#include <iostream>
 +using namespace std;
 +
 + /* возвращает длину строки  s */
 +  int strlen(char *s)
 +  {
 +      int n; 
 +      for (n = 0; *s != '\0'; s++)
 +              n++;
 +      return(n);
 +  }
 +
 +int main()
 +{
 + int i;
 + int *pi = NULL;
 + pi = &i;
 + cout<<&i<<endl;
 + cout<<pi<<endl;
 + *pi=48; //разыменование - изменение значения переменной на который указывает указатель.
 + cout<<i<<endl;
 + cout<<strlen("ER345")<<endl;
 +}
 +</file>
 +
 +====== Указатели и массивы ======
 +В языке C существует сильная взаимосвязь между указателями и массивами , настолько сильная, что указатели и массивы действительно следует рассматривать одновременно. Любую операцию, которую можно выполнить с помощью индексов массива, можно сделать и с помощью указателей. Вариант с указателями обычно оказывается более быстрым, но и несколько более трудным для непосредственного понимания. Описание <file>
 +int a[10];
 +</file>
 + 
 +определяет массив размера 10, т.е. набор из 10 последовательных объектов, называемых a[0], a[1], ..., a[9]. Запись a[i] соответствует элементу массива через i позиций от начала. Если pa - указатель целого, описанный как <file>
 +int *pa;
 +</file>
 +
 + 
 +то присваивание <file>
 +pa = &a[0];
 +</file>
 + 
 +приводит к тому, что pa указывает на нулевой элемент массива a. Это означает, что pa содержит адрес элемента a[0]. Теперь присваивание <file>
 +x = *pa</file>
 + 
 +будет копировать содержимое a[0] в x. 
 +
 +Если ра указывает на некоторый определенный элемент массива a, то по определению pa+1 указывает на следующий элемент, и вообще pa-i указывает на элемент, стоящий на i позиций до элемента, указываемого pa, а pa+i на элемент, стоящий на i позиций после. Таким образом, если pa указывает на a[0], то *(pa+1)
 + 
 +ссылается на содержимое a[1], pa+i - адрес a[i], а *(pa+i) - содержимое a[i]. 
 +
 +Эти замечания справедливы независимо от типа переменных в массиве a. Суть определения "добавления 1 к указателю", а также его распространения на всю арифметику указателей, состоит в том, что приращение масштабируется размером памяти, занимаемой объектом, на который указывает указатель. Таким образом, i в pa+i перед прибавлением умножается на размер объектов, на которые указывает pa.
 +
 +Очевидно существует очень тесное соответствие между индексацией и арифметикой указателей. В действительности компилятор преобразует ссылку на массив в указатель на начало массива. В результате этого имя массива является указательным выражением. Отсюда вытекает несколько весьма полезных следствий. Так как имя массива является синонимом местоположения его нулевого элемента, то присваивание pa = &a[0]
 + 
 +можно записать как pa = a.
 +
 +Еще более удивительным, по крайней мере на первый взгляд, кажется тот факт, что ссылку на a[i] можно записать в виде *(a+i). При анализировании выражения a[i] в языке C оно немедленно преобразуется к виду *(a+i); эти две формы совершенно эквивалентны. Если применить операцию & к обеим частям такого соотношения эквивалентности, то мы получим, что &a[i] и a+i тоже идентичны: a+i - адрес i-го элемента от начала a. С другой стороны, если pa является указателем, то в выражениях его можно использовать с индексом: pa[i] идентично *(pa+i). Короче, любое выражение, включающее массивы и индексы, может быть записано через указатели и смещения и наоборот, причем даже в одном и том же утверждении.
 +
 +<note warning>Имеется одно различие между именем массива и указателем, которое необходимо иметь в виду. Указатель является переменной, так что операции pa=a и pa++ имеют смысл. Но имя массива является константой, а не переменной: конструкции типа a=pa или a++,или p=&a будут незаконными.</note>
 +
 +Когда имя массива передается функции, то на самом деле ей передается местоположение начала этого массива. Внутри вызванной функции такой аргумент является точно такой же переменной, как и любая другая, так что имя массива в качестве аргумента действительно является указателем, т.е. переменной, содержащей адрес. Мы можем использовать это обстоятельство для написания нового варианта функции strlen, вычисляющей длину строки:  /* возвращает длину строки  s */
 +<file>
 +  int strlen(char *s)
 +  {
 +      int n; 
 +      for (n = 0; *s != '\0'; s++)
 +              n++;
 +      return(n);
 +  }
 +</file>
 +
 + 
 +Операция увеличения s совершенно законна, поскольку эта переменная является указателем, s++ никак не влияет на символьную строку в обратившейся к strlen функции, а только увеличивает локальную для функции strlen копию адреса. 
 +
 +Описания формальных параметров в определении функции в виде char s[];
 +
 + 
 +и char *s;
 +
 + 
 +совершенно эквивалентны; какой вид описания следует предпочесть, определяется в значительной степени тем, какие выражения будут использованы при написании функции. Если функции передается имя массива, то в зависимости от того, что удобнее, можно полагать, что функция оперирует либо с массивом, либо с указателем, и действовать далее соответвующим образом. Можно даже использовать оба вида операций, если это кажется уместным и ясным. 
 +
 +  * Источник: [[http://www.opennet.ru/docs/RUS/bogatyrev/gl_2.html|Хрестоматия по программированию на Си в Unix]]
 +
 +Имя массива - это константа, представляющая собой указатель на 0-ой элемент массива. Этот указатель отличается от обычных тем, что его нельзя изменить (установить на другую переменную), поскольку он сам хранится не в переменной, а является просто некоторым постоянным адресом.
 +<file>
 +
 +        массив           указатель
 +           ____________       _____
 +    array: | array[0] |   ptr:| * |
 +           | array[1] |         |
 +           | array[2] |<--------- сейчас равен &array[2]
 +            ...     |
 +</file>
 +Следствием такой интерпретации имен массивов является то, что для того чтобы поставить указатель на начало массива, надо писать
 +    ptr = array;  или  ptr = &array[0];
 +            но не
 +    ptr = &array;
 +Операция & перед одиноким именем массива не нужна и недопустима!
 +Такое родство указателей и массивов позволяет нам применять операцию * к имени массива: value = *array; означает то же самое, что и value = array[0];
 +
 +<note important>Указатели - не целые числа! Хотя физически это и номера байтов, адресная арифметика отличается от обычной.</note>
 +====== Как описывать ссылки (указатели) на двумерные массивы? ======
 +Как описывать ссылки (указатели) на двумерные массивы? Рассмотрим такую программу:
 +<file>
 +    #include <stdio.h>
 +    #define First  3
 +    #define Second 5
 +    char arr[First][Second] = {
 +            "ABC.",
 +            { 'D', 'E', 'F', '?', '\0' },
 +            { 'G', 'H', 'Z', '!', '\0' }
 +    };
 +    char (*ptr)[Second];
 +    main(){
 +            int i;
 +            ptr = arr;      /* arr и ptr теперь взаимозаменимы */
 +            for(i=0; i < First; i++)
 +                    printf("%s\t%s\t%c\n", arr[i], ptr[i], ptr[i][2]);
 +    }
 +</file>
 +Указателем здесь является ptr. Отметим, что у него задана размерность по второму измерению: Second, именно для того, чтобы компилятор мог правильно вычислить двумерные индексы.
 +
 +Попробуйте сами объявить
 +<file>
 +    char (*ptr)[4];
 +    char (*ptr)[6];
 +    char **ptr;
 +</file>
 +и увидеть, к каким невеселым эффектам это приведет (компилятор, кстати, будет ругаться; но есть вероятность, что он все же странслирует это для вас. Но работать оно будет плачевно). Попробуйте также использовать ptr[x][y].
 +
 +Обратите также внимание на инициализацию строк в нашем примере. Строка "ABC." равносильна объявлению
 +
 +            { 'A', 'B', 'C', '.', '\0' },
 +
 +===== Указатели на функции =====
 +**Например, используется в функции: [[Быстрая сортировка]] (англ. quicksort), часто называемая qsort. В функции qsort указатель на функцию применяется для указания способа сортировки.**
 +
 +Прежде чем вводить указатель на функцию, напомним, что каждая функция характеризуется типом возвращаемого значения, именем и сигнатурой. Напомним, что сигнатура определяется количеством, порядком следования и типами параметров. Иногда говорят, что сигнатурой функции называется список типов ее параметров.
 +
 +А теперь путём последовательности утверждений придем к обсуждению темы данного раздела урока.
 +
 +1. При использовании имени функции без последующих скобок и параметров имя функции выступает в качестве указателя на эту функцию, и его значением служит адрес размещения функции в памяти.
 +
 +2. Это значение адреса может быть присвоено некоторому указателю, и затем уже этот новый указатель можно применять для вызова функции.
 +
 +3. В определении нового указателя должен быть тот же тип, что и возвращаемое функцией значение, и та же сигнатура.
 +
 +4. Указатель на функцию определяется следующим образом:
 +
 +
 + тип_функции   (*имя_указателя)(спецификация_параметров);
 +
 + 
 +
 +Например: int (*func1Ptr) (char); - определение указателя func1Ptr на функцию с параметром типа char, возвращающую значение типа int.
 +
 +Примечание: Будьте внимательны!!! Если приведенную синтаксическую конструкцию записать без первых круглых скобок, т.е. в виде int *fun (char); то компилятор воспримет ее как прототип некой функции с именем fun и параметром типа char, возвращающей значение указателя типа int *. 
 +Второй пример: char * (*func2Ptr) (char * ,int); - определение указателя func2Ptr на функцию с параметрами типа указатель на char и типа int, возвращающую значение типа указатель на char. 
 +
 +Иллюстрируем на практике.
 +В определении указателя на функцию тип возвращаемого значения и сигнатура (типы, количество и последовательность параметров) должны совпадать с соответствующими типами и сигнатурами тех функций, адреса которых предполагается присваивать вводимому указателю при инициализации или с помощью оператора присваивания. В качестве простейшей иллюстрации сказанного приведем программу с указателем на функцию:
 +
 +<file>
 +#include <iostream>
 +using namespace std;
 +void f1(void)        // Определение f1.
 +
 + cout << "Load f1()\n";
 + }
 +void f2(void)        // Определение f2.
 +{
 + cout << "Load f1()\n";
 +}
 +void main()
 +
 + void (*ptr)(void);  // ptr - указатель на функцию.
 + ptr = f2; // Присваивается адрес f2().
 + (*ptr)(); // Вызов f2() по ее адресу.
 + ptr = f1; // Присваивается адрес f1().
 + (*ptr)(); // Вызов f1() по ее адресу.
 + ptr(); // Вызов эквивалентен (*ptr)();
 +}
 +</file>
 +Результат выполнения программы: 
 +
 + Load f2()
 + Load f1()
 + Load f1()
 + Press any key to continue
 +
 +
 + 
 +
 +Здесь значением имени_указателя служит адрес функции, а с помощью операции разыменования * обеспечивается обращение по адресу к этой функции. Однако будет ошибкой записать вызов функции без скобок в виде *ptr();. Дело в том, что операция () имеет более высокий приоритет, нежели операция обращения по адресу *. Следовательно, в соответствии с синтаксисом будет вначале сделана попытка обратиться к функции ptr(). И уже к результату будет отнесена операция разыменования, что будет воспринято как синтаксическая ошибка. 
 +
 +При определении указатель на функцию может быть инициализирован. В качестве инициализирующего значения должен использоваться адрес функции, тип и сигнатура которой соответствуют определяемому указателю.
 +
 +При присваивании указателей на функции также необходимо соблюдать соответствие типов возвращаемых значений функций и сигнатур для указателей правой и левой частей оператора присваивания. То же справедливо и при последующем вызове функций с помощью указателей, т.е. типы и количество фактических параметров, используемых при обращении к функции по адресу, должны соответствовать формальным параметрам вызываемой функции. Например, только некоторые из следующих операторов будут допустимы:
 +
 +<file>
 +char f1(char) {...}       // Определение функции.
 +char f2(int) {...}        // Определение функции.
 +void f3(float) {...}      // Определение функции.
 +int* f4(char *){...}     // Определение функции.
 +char (*pt1)(int);        // Указатель на функцию.
 +char (*pt2)(int);         // Указатель на функцию.
 +void (*ptr3)(float) = f3; // Инициализированный указатель.
 +void main()
 +
 +    pt1 = f1;  // Ошибка - несоответствие сигнатур.
 +    pt2 = f3;  // Ошибка - несоответствие типов (значений и сигнатур).
 +    pt1 = f4;  // Ошибка - несоответствие типов.
 +    pt1 = f2;  // Правильно.
 +    pt2 = pt1; // Правильно.
 +    char с = (*pt1)(44); // Правильно.
 +    с = (*pt2)('\t');    // Ошибка - несоответствие сигнатур.
 +}
 +</file>
 +Следующая программа отражает гибкость механизма вызовов функций с помощью указателей.
 +<file>
 +#include <iostream>
 +using namespace std;
 +// Функции одного типа с одинаковыми сигнатурами:
 +int add(int n, int m) { return n + m; }
 +int division(int n, int m) { return n/m; }
 +int mult(int n, int m) { return n * m; }
 +int subt(int n, int m) { return n - m; }
 +void main()
 +
 + int (*par)(int, int); // Указатель на функцию.
 + int a =  6, b = 2; 
 + char c = '+';
 + while   (c != ' ')
 + {
 + cout << "\n Arguments: a = " << a <<", b = " << b;
 + cout << ". Result for c = \'" << c << "\':";
 +
 + switch (c)
 + {
 + case '+': 
 + par = add;
 + c = '/';
 + break;
 + case '-': 
 + par = subt; 
 + c = ' '; 
 + break;
 + case '*': 
 + par = mult; 
 + c = '-'; 
 + break;
 + case '/': 
 + par = division;
 + c = '*'; 
 + break;
 + }
 + cout << (a = (*par)(a,b))<<"\n"; //Вызов  по  адресу.
 + }
 +}
 +</file>
 +Результаты выполнения программы: 
 +<file>
 + Arguments: a = 6, b = 2. Result for c = '+':8
 +
 + Arguments: a = 8, b = 2. Result for c = '/':4
 +
 +  Arguments: a = 4, b = 2. Result for c = '*':8
 +
 +  Arguments: a = 8, b = 2. Result for c = '-':6
 + Press any key to continue
 +</file>
 +
 +Цикл продолжается, пока значением переменной c не станет пробел. В каждой итерации указатель par получает адрес одной из функций, и изменяется значение c. По результатам программы легко проследить порядок выполнения ее операторов.
 +
 +Массивы указателей на функции.
 +Указатели на функции могут быть объединены в массивы. Например, float (*ptrArray[4]) (char) ; - описание массива с именем ptrArray из четырех указателей на функции, каждая из которых имеет параметр типа char и возвращает значение типа float. Чтобы обратиться, например, к третьей из этих функций, потребуется такой оператор:
 +<file>
 +float а = (*ptrArray[2])('f');
 +</file>
 +Как обычно, индексация массива начинается с 0, и поэтому третий элемент массива имеет индекс 2.
 +
 +Массивы указателей на функции удобно использовать при разработке всевозможных меню, точнее программ, управление которыми выполняется с помощью меню. Для этого действия, предлагаемые на выбор будущему пользователю программы, оформляются в виде функций, адреса которых помещаются в массив указателей на функции. Пользователю предлагается выбрать из меню нужный ему пункт (в простейшем случае он вводит номер выбираемого пункта) и по номеру пункта, как по индексу, из массива выбирается соответствующий адрес функции. Обращение к функции по этому адресу обеспечивает выполнение требуемых действий. Самую общую схему реализации такого подхода иллюстрирует следующая программа для "обработки файлов": 
 +
 +<file>
 +#include <iostream> 
 +using namespace std;
 +
 +/* Определение функций для обработки меню
 +(функции фиктивны т. е. реальных действий не выполняют):*/
 +
 +void act1 (char* name)
 +{
 + cout <<"Create file..." << name; 
 +
 +void act2 (char* name)
 +
 + cout << "Delete file... " << name; 
 +}
 +void act3 (char* name)
 +
 + cout << "Read file... " << name; 
 +
 +void act4 (char* name)
 +
 + cout << "Mode file... " << name; 
 +
 +void act5 (char* name) 
 +
 + cout << "Close file... " << name;
 +}
 + 
 +void main()
 +
 + // Создание и инициализация массива указателей 
 + void (*MenuAct[5])(char*)  = {act1, act2, act3, act4, act5};
 +
 + int number;  // Номер выбранного пункта меню.
 + char FileName[30];  // Строка для имени файла.
 +
 + // Реализация меню
 + cout << "\n 1 - Create";
 + cout << "\n 2 - Delete";
 + cout << "\n 3 - Read";
 + cout << "\n 4 - Mode";
 + cout << "\n 5 - Close";
 +
 + while (1)  // Бесконечный цикл.
 +
 + while (1)
 + { // Цикл продолжается до ввода правильного номера. 
 + cout << "\n\nSelect action: "; 
 + cin >> number;
 + if (number>>= 1 && number <= 5) break; 
 +
 + cout << "\nError number!"; 
 +
 + if (number != 5)
 +
 + cout << "Enter file name: ";
 + cin >> FileName; // Читать имя файла. 
 + }
 + else break;
 + // Вызов функции по указателю на нее:
 + (*MenuAct[number-1])(FileName);
 + } // Конец бесконечного цикла.
 +}
 +</file>
 +Пункты меню повторяются, пока не будет введен номер 5 - закрытие.
 +