Войти
Портал компьютерных советов - Hiper-ru
  • Оптимизация ОС: Программа для дефрагментации диска
  • Как включить подсветку клавиатуры на Макбуке?
  • Не включается компьютер: ПК включается и сразу выключается
  • Как добавить или убрать звуковую дорожку в программе Sony Vegas Удаление из dvd файлов ненужной звуковой дорожки
  •  не удается установить сетевое подключение Ошибка не удается установить соединение сервером
  • Как выгрузить контрагентов из 1с 8
  • Операции с указателями. Что значит в си: что такое указатель Обозначения и предположения

    Операции с указателями. Что значит в си: что такое указатель Обозначения и предположения

    Последнее обновление: 27.05.2017

    Указатели в языке Си поддерживают ряд операций: присваивание, получение адреса указателя, получение значения по указателю, некоторые арифметические операции и операции сравнения.

    Присваивание

    Указателю можно присвоить либо адрес объекта того же типа, либо значение другого указателя или константу NULL .

    Присвоение указателю адреса уже рассматривалось в прошлой теме. Для получения адреса объекта используется операция & :

    Int a = 10; int *pa = &a; // указатель pa хранит адрес переменной a

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

    Присвоение указателю другого указателя:

    #include int main(void) { int a = 10; int b = 2; int *pa = &a; int *pb = &b; printf("Variable a: address=%p \t value=%d \n", pa, *pa); printf("Variable b: address=%p \t value=%d \n", pb, *pb); pa = pb; // теперь указатель pa хранит адрес переменной b printf("Variable b: address=%p \t value=%d \n", pa, *pa); return 0; }

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

    Если мы не хотим, чтобы указатель указывал на какой-то конкретный адрес, то можно присвоить ему условное нулевое значение с помощью константы NULL , которая определена в заголовочном файле stdio.h:

    Int *pa = NULL;

    Разыменование указателя

    Операция разыменования указателя в виде *имя_указателя, позволяет получить объект по адресу, который хранится в указателе.

    #include int main(void) { int a = 10; int *pa = &a; int *pb = pa; *pa = 25; printf("Value on pointer pa: %d \n", *pa); // 25 printf("Value on pointer pb: %d \n", *pb); // 25 printf("Value of variable a: %d \n", a); // 25 return 0; }

    Через выражение *pa мы можем получить значение по адресу, который хранится в указателе pa , а через выражение типа *pa = значение вложить по этому адресу новое значение.

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

    Адрес указателя

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

    Int a = 10; int *pa = &a; printf("address of pointer=%p \n", &pa); // адрес указателя printf("address stored in pointer=%p \n", pa); // адрес, который хранится в указателе - адрес переменной a printf("value on pointer=%d \n", *pa); // значение по адресу в указателе - значение переменной a

    Операции сравнения

    К указателям могут применяться операции сравнения > , >= , < , <= ,== , != . Операции сравнения применяются только к указателям одного типа и константе NULL . Для сравнения используются номера адресов:

    Int a = 10; int b = 20; int *pa = &a; int *pb = &b; if(pa > pb) printf("pa (%p) is greater than pb (%p) \n", pa, pb); else printf("pa (%p) is less or equal pb (%p) \n", pa, pb);

    Консольный вывод в моем случае:

    Pa (0060FEA4) is greater than pb (0060FEA0)

    Приведение типов

    Иногда требуется присвоить указателю одного типа значение указателя другого типа. В этом случае следует выполнить операцию приведения типов:

    Char c = "N"; char *pc = &c; int *pd = (int *)pc; printf("pc=%p \n", pc); printf("pd=%p \n", pd);

    Теги: Си указатели. Указатель на указатель. Тип указателя. Арифметика указателей. Сравнение указателей.

    Указатели

    Э то, пожалуй, самая сложная и самая важная тема во всём курсе. Без понимания указателей дальнейшее изучении си будет бессмысленным. Указатели – очень простая концепция, очень логичная, но требующая внимания к деталям.

    Определение

    У казатель – это переменная, которая хранит адрес области памяти. Указатель, как и переменная, имеет тип. Синтаксис объявления указателей

    <тип> *<имя>;

    Например
    float *a;
    long long *b;
    Два основных оператора для работы с указателями – это оператор & взятия адреса, и оператор * разыменования. Рассмотрим простой пример.

    #include #include void main() { int A = 100; int *p; //Получаем адрес переменной A p = &A; //Выводим адрес переменной A printf("%p\n", p); //Выводим содержимое переменной A printf("%d\n", *p); //Меняем содержимое переменной A *p = 200; printf("%d\n", A); printf("%d", *p); getch(); }

    Рассмотрим код внимательно, ещё раз

    Int A = 100;

    Была объявлена переменная с именем A . Она располагается по какому-то адресу в памяти. По этому адресу хранится значение 100.

    Создали указатель типа int .

    Теперь переменная p хранит адрес переменной A . Используя оператор * мы получаем доступ до содержимого переменной A .
    Чтобы изменить содержимое, пишем

    *p = 200;

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

    #include #include void main() { int A = 100; int *a = &A; double B = 2.3; double *b = &B; printf("%d\n", sizeof(A)); printf("%d\n", sizeof(a)); printf("%d\n", sizeof(B)); printf("%d\n", sizeof(b)); getch(); }

    Будет выведено
    4
    4
    8
    4
    Несмотря на то, что переменные имеют разный тип и размер, указатели на них имеют один размер. Действительно, если указатели хранят адреса, то они должны быть целочисленного типа. Так и есть, указатель сам по себе хранится в переменной типа size_t (а также ptrdiff_t ), это тип, который ведёт себя как целочисленный, однако его размер зависит от разрядности системы. В большинстве случаев разницы между ними нет. Зачем тогда указателю нужен тип?

    Арифметика указателей

    В о-первых, указателю нужен тип для того, чтобы корректно работала операция разыменования (получения содержимого по адресу). Если указатель хранит адрес переменной, необходимо знать, сколько байт нужно взять, начиная от этого адреса, чтобы получить всю переменную.
    Во-вторых, указатели поддерживают арифметические операции. Для их выполнения необходимо знать размер.
    операция + N сдвигает указатель вперёд на N*sizeof(тип) байт.
    Например, если указатель int *p; хранит адрес CC02, то после p += 10; он будет хранить адрес СС02 + sizeof(int)*10 = CC02 + 28 = CC2A (Все операции выполняются в шестнадцатиричном формате). Пусть мы создали указатель на начало массива. После этого мы можем "двигаться" по этому массиву, получая доступ до отдельных элементов.

    #include #include void main() { int A = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p; p = A; printf("%d\n", *p); p++; printf("%d\n", *p); p = p + 4; printf("%d\n", *p); getch(); }

    Заметьте, каким образом мы получили адрес первого элемента массива

    Массив, по сути, сам является указателем, поэтому не нужно использовать оператор &. Мы можем переписать пример по-другому

    Получить адрес первого элемента и относительно него двигаться по массиву.
    Кроме операторов + и - указатели поддерживают операции сравнения. Если у нас есть два указателя a и b, то a > b, если адрес, который хранит a, больше адреса, который хранит b.

    #include #include void main() { int A = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *a, *b; a = &A; b = &A; printf("&A == %p\n", a); printf("&A == %p\n", b); if (a < b) { printf("a < b"); } else { printf("b < a"); } getch(); }

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

    Указатель на указатель

    У казатель хранит адрес области памяти. Можно создать указатель на указатель, тогда он будет хранить адрес указателя и сможет обращаться к его содержимому. Указатель на указатель определяется как

    <тип> **<имя>;

    Очевидно, ничто не мешает создать и указатель на указатель на указатель, и указатель на указатель на указатель на указатель и так далее. Это нам понадобится при работе с двумерными и многомерными массивами. А вот простой пример, как можно работать с указателем на указатель.

    #include #include #define SIZE 10 void main() { int A; int B; int *p; int **pp; A = 10; B = 111; p = &A; pp = &p; printf("A = %d\n", A); *p = 20; printf("A = %d\n", A); *(*pp) = 30; //здесь скобки можно не писать printf("A = %d\n", A); *pp = &B; printf("B = %d\n", *p); **pp = 333; printf("B = %d", B); getch(); }

    Указатели и приведение типов

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

    #include #include #define SIZE 10 void main() { int A = 10; int *intPtr; char *charPtr; intPtr = &A; printf("%d\n", *intPtr); printf("--------------------\n"); charPtr = (char*)intPtr; printf("%d ", *charPtr); charPtr++; printf("%d ", *charPtr); charPtr++; printf("%d ", *charPtr); charPtr++; printf("%d ", *charPtr); getch(); }

    В этом примере мы пользуемся тем, что размер типа int равен 4 байта, а char 1 байт. За счёт этого, получив адрес первого байта, можно пройти по остальным байтам числа и вывести их содержимое.

    NULL pointer - нулевой указатель

    У казатель до инициализации хранит мусор, как и любая другая переменная. Но в то же время, этот "мусор" вполне может оказаться валидным адресом. Пусть, к примеру, у нас есть указатель. Каким образом узнать, инициализирован он или нет? В общем случае никак. Для решения этой проблемы был введён макрос NULL библиотеки stdlib.
    Принято при определении указателя, если он не инициализируется конкретным значением, делать его равным NULL.

    Int *ptr = NULL;

    По стандарту гарантировано, что в этом случае указатель равен NULL , и равен нулю, и может быть использован как булево значение false . Хотя в зависимости от реализации NULL может и не быть равным 0 (в смысле, не равен нулю в побитовом представлении, как например, int или float ).
    Это значит, что в данном случае

    Int *ptr = NULL; if (ptr == 0) { ... }

    вполне корректная операция, а в случае

    Int a = 0; if (a == NULL) { ... }

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

    #include #include #include void main() { int *a = NULL; unsigned length, i; printf("Enter length of array: "); scanf("%d", &length); if (length > 0) { //При выделении памяти возвращается указатель. //Если память не была выделена, то возвращается NULL if ((a = (int*) malloc(length * sizeof(int))) != NULL) { for (i = 0; i < length; i++) { a[i] = i * i; } } else { printf("Error: can"t allocate memory"); } } //Если переменая была инициализирована, то очищаем её if (a != NULL) { free(a); } getch(); }

    Примеры

    Теперь несколько примеров работы с указателями
    1. Пройдём по массиву и найдём все чётные элементы.

    #include #include void main() { int A = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int even; int evenCounter = 0; int *iter, *end; //iter хранит адрес первого элемента массива //end хранит адрес следующего за последним "элемента" массива for (iter = A, end = &A; iter < end; iter++) { if (*iter % 2 == 0) { even = *iter; } } //Выводим задом наперёд чётные числа for (--evenCounter; evenCounter >= 0; evenCounter--) { printf("%d ", even); } getch(); }

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

    #include #include #define SIZE 10 void main() { double unsorted = {1.0, 3.0, 2.0, 4.0, 5.0, 6.0, 8.0, 7.0, 9.0, 0.0}; double *p; double *tmp; char flag = 1; unsigned i; printf("unsorted array\n"); for (i = 0; i < SIZE; i++) { printf("%.2f ", unsorted[i]); } printf("\n"); //Сохраняем в массив p адреса элементов for (i = 0; i < SIZE; i++) { p[i] = &unsorted[i]; } do { flag = 0; for (i = 1; i

    3. Более интересный пример. Так как размер типа char всегда равен 1 байт, то с его помощью можно реализовать операцию swap – обмена местами содержимого двух переменных.

    Указатели это чрезвычайно мощный инструмент в программировании. С помощью указателей некоторые вещи в программировании можно сделать намного проще и при этом эффективность работы вашей программы значительно повысится. Указатели даже позволяют обрабатывать неограниченное количество данных. Например, с помощью указателей можно изменять значения переменных внутри функции, при этом переменные передаются в функцию в качестве параметров. Кроме того, указатели можно использовать для динамического выделения памяти, что означает, что вы можете писать программы, которые могут обрабатывать практически неограниченные объемы данных на лету — вам не нужно знать, когда вы пишете программу, сколько памяти нужно выделить заранее. Пожалуй, это самая мощная функция указателей. Для начала давайте просто получим общее представление об указателях, научимся их объявлять и использовать.

    Что такое указатели и зачем они нужны?

    Указатели похожи на метки, которые ссылаются на места в памяти. Представьте сейф с депозитными ячейками различного размера в местном банке. Каждая ячейка имеет номер, уникальный номер, который связан только с этой ячейкой, таким образом можно быстро идентифицировать нужную ячейку. Эти цифры аналогичны адресам ячеек компьютерной памяти. К примеру, у вас есть богатый дядя, который хранит все свои ценности в своем сейфе. И чтобы обезопасить все свои сбережения, он решил завести меньший сейф, в который положит карту, на которой показано местоположение большого сейфа и указан 16-й пароль от большого сейфа, в котором и хранятся реальные драгоценности. По сути, сейф с картой будет хранить расположение другого сейфа. Вся эта организация сбережения драгоценностей эквивалентна указателям в языке Си. В компьютере, указатели просто переменные, которые хранят адреса памяти, как правило, адреса других переменных.

    Идея в том, что зная адрес переменной, вы можете пойти по этому адресу и получить данные, хранящиеся в нем. Если вам нужно передать огромный кусок данных в функцию, намного проще передать адрес в памяти, по которому хранятся эти данные, чем скопировать каждый элемент данных! Более того, если программе понадобится больше памяти, вы можете запросить больше памяти из системы. Как же это работает? Система просто возвращает адрес ячейки памяти, и мы должны сохранить этот адрес в переменной-указателе. Так мы сможем взаимодействовать с данными из указанного участка памяти.

    Синтаксис указателей

    Если у нас есть указатель, значит мы можем получить его адрес в памяти и данные на которые он ссылается, по этой причине указатели имеют несколько необычный синтаксис, отличающийся от объявления простых переменных. Более того, поскольку указатели — это не обычные переменные, то, необходимо сообщить компилятору, что переменная является указателем и сообщить компилятору тип данных, на которые ссылается указатель. Итак, указатель объявляется следующим образом:

    Data_type *pointerName;

    где, data_type — тип данных, pointerName — имя указателя.

    Например, объявим указатель, который хранит адрес ячейки памяти, в которой лежит целое число:

    Int *integerPointer;

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

    // Объявление указателя и простой переменной в одной строке int *pointer1, // это указатель variable; // это обычная переменная типа int // Объявление двух указателей в одно строке int *pointer1, // это указатель с именем pointer1 *pointer2; // это указатель с именем pointer2

    Как я и говорил, если имя переменной не предваряется символом * , то это обычная переменная, в противном случае — это указатель. Именно это и показывает пример объявления указателей, выше.

    Есть два способа использования указателя:

    1. Использовать имя указателя без символа * , таким образом можно получить фактический адрес ячейки памяти, куда ссылается указатель.
    2. Использовать имя указателя с символом * , это позволит получить значение, хранящееся в памяти. В рамках указателей, у символа * есть техническое название — операция разыименования. По сути, мы принимаем ссылку на какой-то адрес памяти, чтобы получить фактическое значение. Это может быть сложно для понимания, но в дальнейшем постараемся разобраться во всем этом.

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

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

    Int var = 5; // простое объявление переменной с предварительной инициализацией int *ptrVar; // объявили указатель, однако он пока ни на что не указывает ptrVar = &var; // теперь наш указатель ссылается на адрес в памяти, где хранится число 5

    В строке 3 использовалась операция взятия адреса, мы взяли адрес переменной var и присвоили его указателю ptrVar . Давайте рассмотрим программу, которая наглядно покажет всю мощь указателей. Итак, вот исходник:

    #include int main() { int var; // обычная целочисленная переменная int *ptrVar; // целочисленный указатель (ptrVar должен быть типа int, так как он будет ссылаться на переменную типа int) ptrVar = &var; // присвоили указателю адрес ячейки в памяти, где лежит значение переменной var scanf("%d", &var); // в переменную var положили значение, введенное с клавиатуры printf("%d\n", *ptrVar); // вывод значения через указатель getchar(); }

    Результат работы программы:

    В строке 10 , printf() выводит значение, хранящееся в переменной var . Почему так происходит? Что ж, давайте посмотрим на код. В строке 5 , мы объявили переменную var типа int . В строке 6 — указатель ptrVar на целое значение. Затем указателю ptrVar присвоили адрес переменной var , для этого мы воспользовались оператором присвоения адреса. Затем пользователь вводит номер, который сохраняется в переменную var , помните, что это то же самое место, на которое указывает ptrVar . В самом деле, так как мы используем амперсанд чтобы присвоить значение переменной var в функции scanf() , должно быть понятно, что scanf() инициализирует переменную var через адрес. На этот же адрес указывает указатель ptrVar .

    Затем, в строке 10 , выполняется операция «разыменования» — *ptrVar . Программа, через указатель ptrVar , считывает адрес, который хранится в указателе, по адресу попадает в нужную ячейку памяти, и возвращает значение, которое там хранится.

    Обратите внимание, что в приведенном выше примере, перед тем как использовать указатель, он сначала инициализируется, это нужно для того, чтобы указатель ссылался на определенный адрес памяти. Если бы мы начали использовать указатель так и не инициализировав его, он бы ссылался на какой угодно участок памяти. И это могло бы привести к крайне неприятным последствиям. Например, операционная система, вероятно, помешает вашей программе получить доступ к неизвестному участку памяти, так как ОС знает, что в вашей программе не выполняется инициализация указателя. В основном это просто приводит к краху программы.

    Если бы такие приемчики были позволены в ОС, вы могли бы получить доступ к любому участку памяти. А это значит, что для любой запущенной программы вы могли бы внести свои изменения, например, если у вас открыт документ в Word, вы могли бы изменить любой текст программно. К счастью, Windows и другие современные операционные системы остановит вас от доступа к этой памяти и преждевременно закроют вашу программу.

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

    P.S.:Если у вас нет денег на телефоне, и нет возможности его пополнить, но при этом, вам срочно нужно позвонить, вы всегда можете использовать доверительный платеж билайн . Сумма доверительного платежа может быть самой разнообразной, от 50 до 300р.

    Хотелось бы с самого начала прояснить одну вещь - я не отношу себя к категории true-кодеров, сам учусь по специальности, не связанной с разработкой ПО, и это мой первый пост. Прошу судить по всей строгости. Итак, в свое время то ли по причине того, что я спал на лекциях, то ли я не особо вникал в эту тему, но у меня возникали некоторые сложности при работе с указателями в плюсах. Теперь же ни одна моя даже самая крохотная быдлокодерская программа не обходится без указателей. В данной статье я попытаюсь рассказать базовые вещи: что такое указатели, как с ними работать и где их можно применять. Повторюсь, изложенный ниже материал предназначен для новичков.


    /* Ребят, в статье было найдено много ошибок. Спасибо тем людям, которые внесли свои замечания. В связи с этим - после прочтения статьи обязательно перечитайте комментарии */

    1. Общие сведения

    Итак, что же такое указатель? Указатель - это та же переменная, только инициализируется она не значением одного из множества типов данных в C++, а адресом, адресом некоторой переменной, которая была объявлена в коде ранее. Разберем на примере:

    Void main(){ int i_val = 7; }
    # Здесь ниже, конечно, я ребятки вам соврал. Переменная i_val - статическая, она явно будет размещена в стеке. В куче место выделяется под динамические объекты. Это важные вещи! Но в данном контексте, я, сделав сам себе замечание, позволю оставить себе все как есть, так что сильно не ругайтесь.

    Мы объявили переменную типа int и здесь же ее проинициализировали. Что же произойдет при компиляции программы? В оперативной памяти, в куче, будет выделено свободное место такого размера, что там можно будет беспрепятственно разместить значение нашей переменной i_val . Переменная займет некоторый участок памяти, разместившись в нескольких ячейках в зависимости от своего типа; учитывая, что каждая такая ячейка имеет адрес, мы можем узнать диапазон адресов, в пределах которого разместилось значение переменной. В данном случае, при работе с указателями нам нужен лишь один адрес - адрес первой ячейки, именно он и послужит значением, которым мы проинициализируем указатель. Итак:

    Void main(){ // 1 int i_val = 7; int* i_ptr = &i_val; // 2 void* v_ptr = (int *)&i_val }
    Используя унарную операцию взятия адреса & , мы извлекаем адрес переменной i_val и присваиваем ее указателю. Здесь стоит обратить внимание на следующие вещи:

    1. Тип, используемый при объявлении указателя в точности должен соответствовать типу переменной, адрес которой мы присваиваем указателю.
    2. В качестве типа, который используется при объявлении указателя, можно выбрать тип void . Но в этом случае при инициализации указателя придется приводить его к типу переменной, на которую он указывает.
    3. Не следует путать оператор взятия адреса со ссылкой на некоторое значение, которое так же визуально отображается символом & .
    Теперь, когда мы имеем указатель на переменную i_va l мы можем оперировать ее значением не только непосредственно с помощью самой переменной, но и с помощью указателя на нее. Посмотрим, как это работает на простом примере:

    #include using namespace std; void main(){ int i_val = 7; int* i_ptr = &i_val; // выведем на экран значение переменной i_val cout << i_val << endl; // C1 cout << *i_ptr << endl; // C2 }

    1. Здесь все ясно - используем саму переменную.
    2. Во втором случае - мы обращаемся к значению переменной i_val через указатель. Но, как вы заметили, мы не просто используем имя указателя - здесь используется операция разыменования: она позволяет перейти от адреса к значению.
    В предыдущем примере был организован только вывод значения переменной на экран. Можем ли мы непосредственно через указатель оперировать с значением переменной, на которую он указывает? Да, конечно, для этого они и реализованы (однако, не только для этого - но об этом чуть позже). Все, что нужно - сделать разыменование указателя:

    (*i_ptr)++; // результат эквивалентен операции инкремента самой переменной: i_val++ // т.е. в данном случае в i_val сейчас хранится значение не 7, а 8.

    2. Массивы

    Сразу перейдем к примеру - рассмотрим статичный одномерный массив определенной длинны и инициализируем его элементы:

    Void main(){ const int size = 7; // объявление int i_array; // инициализация элементов массива for (int i = 0; i != size; i++){ i_array[i] = i; } }
    А теперь будем обращаться к элементам массива, используя указатели:

    Int* arr_ptr = i_array; for (int i = 0; i != size; i++){ cout << *(arr_ptr + i) << endl; }
    Что здесь происходит: мы инициализируем указатель arr_ptr адресом начала массива i_array . Затем, в цикле мы выводим элементы, обращаясь к каждому с помощью начального адреса и смещения. То есть:

    *(arr_ptr + 0) это тот же самый нулевой элемент, смещение нулевое (i = 0),
    *(arr_ptr + 1) - первый (i = 1), и так далее.

    Однако, здесь возникает естественный вопрос - почему присваивая указателю адрес начала массива, мы не используем операцию взятия адреса? Ответ прост - использование идентификатора массива без указания квадратных скобок эквивалентно указанию адреса его первого элемента. Тот же самый пример, только в указатель «явно» занесем адрес первого элемента массива:

    Int* arr_ptr_null = &i_array; for (int i = 0; i != size; i++){ cout << *(arr_ptr_null + i) << endl; } Пройдем по элементам с конца массива:
    int* arr_ptr_end = &i_array; for (int i = 0; i != size; i++){ cout << *(arr_ptr_end - i) << endl; } Замечания:

    1. Запись array[i] эквивалентна записи *(array + i ). Никто не запрещает использовать их комбинированно: (array + i ) - в этом случае смещение идет на i , и еще на единичку. Однако, в данном случае перед выражением (array + i ) ставить * не нужно. Наличие скобок это «компенсирует.
    2. Следите за вашими „перемещениями“ по элементам массива - особенно если вам захочется использовать порнографический такой метод записи, как (array + i)[j].

    3. Динамическое выделение памяти

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

    Int size = -1; // здесь происходят какие - то // действия, которые изменяют // значение переменной size int* dyn_arr = new int;
    Что здесь происходит: мы объявляем указатель и инициализируем его началом массива, под который выделяется память оператором new на size элементов. Следует заметить, что в этом случае мы можем использовать те же приемы в работе с указателями, что и с статическим массивом. Что следует из этого извлечь - если вам нужна какая - то структура (как массив, например), но ее размер вам заранее неизвестен, то просто сделайте объявление этой структуры, а проинициализируете ее уж позже. Более полный пример приведу чуть позже, а пока что - рассмотрим двойные указатели.

    Что такое указатель на указатель? Это та же переменная, которая хранит адрес другого указателя „более низкого порядка“. Зачем он нужен? Для инициализации двумерного динамического массива, например:

    Const int size = 7; // двумерный массив размером 7x7 int** i_arr = new int*; for(int i = 0; i != size; i++){ i_arr[i] = new int; }
    А тройной указатель? Трехмерный динамический массив. Неинтересно, скажите вы, так можно продолжать до бесконечности. Ну хорошо. Тогда давайте представим себе ситуацию, когда нам нужно разместить динамические объекты какого-нибудь класса MyClass в двумерном динамическом массиве. Как это выглядит (пример иллюстрирует исключительно использование указателей, приведенный в примере класс никакой смысловой нагрузки не несет):

    Class MyClass{ public: int a; public: MyClass(int v){ this->a = v; }; ~MyClass(){}; }; void main(){ MyClass*** v = new MyClass**; for (int i = 0; i != 7; i++){ v[i] = new MyClass*; for (int j = 0; j != 3; j++){ v[i][j] = new MyClass(i*j); } } } Здесь два указателя нужны для формирования матрицы, в которой будут располагаться объекты, третий - собственно для размещения там динамических объектов (не MyClass a , а MyClass* a ). Это не единственный пример использования указателей такого рода, чуть ниже будут рассмотрены еще примеры.

    4. Указатель как аргумент функции

    Для начала создадим два динамических массива размером 4x4 и проинициализируем их элементы некоторыми значениями:

    Void f1(int**, int); void main(){ const int size = 4; // объявление и выделение памяти // под другие указатели int** a = new int*; int** b = new int*; // выделение памяти под числовые значения for (int i = 0; i != size; i++){ a[i] = new int; b[i] = new int; // собственно инициализация for (int j = 0; j != size; j++){ a[i][j] = i * j + 1; b[i][j] = i * j - 1; } } } void f1(int** a, int c){ for (int i = 0; i != c; i++){ for (int j = 0; j != c; j++){ cout.width(3); cout << a[i][j]; } cout << endl; } cout << endl; }
    Функция f1 выводит значения массивов на экран: первый ее аргумент указатель на двумерный массив, второй - его размерность (указывается одно значение, потому как мы условились для простоты работать с массивами, где количество строк совпадает с количеством столбцов).

    Задача : заменить значения элементов массива a соответствующими элементами из массива b , учитывая, что это должно произойти в некоторой функции, которая так или иначе занимается обработкой массивов. Цель: разобраться в способе передачи указателей для их дальнейшей модификации.

    1. Вариант первый. Передаем собственно указатели a и b в качестве параметров функции:

      Void f2(int** a, int** b, int c){ for (int i = 0; i != c; i++){ for (int j = 0; j != c; j++){ a[i][j] = b[i][j]; } } } После вызова данной функции в теле main - f2(a, b, 4) содержимое массивов a и b станет одинаковым.

    2. Вариант второй. Заменить значение указателя: просто присвоить значение указателя b указателю a.

      Void main(){ const int size = 4; // объявление и выделение памяти // под другие указатели int** a = new int*; int** b = new int*; // выделение памяти под числовые значения for (int i = 0; i != size; i++){ a[i] = new int; b[i] = new int; // собственно инициализация for (int j = 0; j != size; j++){ a[i][j] = i * j + 1; b[i][j] = i * j - 1; } } // Здесь это сработает a = b; }
      Однако, нам интересен случай, когда массивы обрабатываются в некоторой функции. Что первое приходит на ум? Передать указатели в качестве параметров нашей функции и там сделать то же самое: присвоить указателю a значение указателя b . То есть реализовать следующую функцию:

      Void f3(int** a, int** b){ a = b; } Сработает ли она? Если мы внутри функции f3 вызовем функцию f1(a, 4) , то увидим, что значения массива действительно поменялись. НО: если мы посмотрим содержимое массива a в main - то обнаружим обратное - ничего не изменилось. Так в чем же причина? Все предельно просто: в функции f3 мы работали не с самим указателем a , а с его локальной копией! Все изменения, которые произошли в функции f3 - затронули только локальную копию указателя, но никак не сам указатель a . Давайте посмотрим на следующий пример:

      Void false_eqv(int, int); void main(){ int a = 3, b = 5; false_eqv(a, b); // Поменялось значение a? // Конечно же, нет } false_eqv(int a, int b){ a = b; } Итак, я думаю, вы поняли, к чему я веду. Переменной a нельзя присвоить таким образом значение переменной b - ведь мы передавали их значения напрямую, а не по ссылке. То же самое и с указателями - используя их в качестве аргументов таким образом, мы заведомо лишаем их возможности изменения значения.
      Вариант третий, или работа над ошибками по второму варианту:

      Void f4(int***, int**); void main(){ const int size = 4; int** a = new int*; int** b = new int*; for (int i = 0; i != 4; i++){ a[i] = new int; b[i] = new int; for (int j = 0; j != 4; j++){ a[i][j] = i * j + 1; b[i][j] = i * j - 1; } } int*** d = &a; f4(d, b); } void f4(int*** a, int** b){ *a = b; }
      Таким образом, в main"е мы создаем указатель d на указатель a , и именно его передаем в качестве аргумента в функцию замены. Теперь, разыменовав d внутри f4 и приравняв ему значение указателя b , мы заменили значение настоящего указателя a , а не его локальной копии, на значение указателя b .

      Кстати, а чего это мы создаем динамические объекты? Ну ладно размер массива не знали, а экземпляры классов мы зачем динамическими делали? Да потому что зачастую, созданный нами объекты свое - они генерились, порождали новые данные/объекты для дальнейшей работы, а теперь пришло им время... умереть [фу, как грубо] уйти со сцены. И как мы это сделаем? Просто:

      Delete(a); delete(b); // Вот и кончились наши двумерные массивы delete(v); // Вот и нет больше двумерного массива с динамическими объектами delete(dyn_array); // Вот и удалился одномерный массив

    3. На данной ноте я хотел бы закончить свое повествование. Если найдется хотя бы пара ребят, которым понравится стиль изложения материала, то я постараюсь продолжить… ой, да кого я обманываю, мне нужен инвайт и все на этом, дайте инвайт и вашим глазам больше не придется видеть это околесицу. Шучу, конечно. Ругайте, комментируйте.