Как описать указатель на начало массива
В C++ указатели и массивы тесно связаны. Обычно компилятор преобразует массив в указатели. С помощью указателей можно манипулировать элементами массива, как и с помощью индексов.
Имя массива по сути является адресом его первого элемента. Соответственно через операцию разыменования мы можем получить значение по этому адресу:
#include int main() < int nums[] ; std::cout
Так, в моем случае я получу следующий консольный вывод:
nums[0] address: 0x1f1ebffe60 nums[0] value: 1
Прибавляя к адресу первого элемента некоторое число, мы можем получить определенный элемент массива.
#include int main() < int nums[] ; int num2 = *(nums + 1); // второй элемент int num3 = *(nums + 2); // третий элемент std::cout int, размер которого, как правило, составляет 4 байта, поэтому прибавление единицы к адресу означает увеличение адреса на 4. Прибавляя к адресу 2, мы увеличиваем значение адреса на 4 * 2 = 8. И так далее.Например, в цикле пробежимся по всем элементам:
#include int main() < int nums[] ; for(unsigned i<>; i < std::size(nums); i++) < std::cout nums[0]: address=0xd95adffc30 value=1 nums[1]: address=0xd95adffc34 value=2 nums[2]: address=0xd95adffc38 value=3 nums[3]: address=0xd95adffc3c value=4 nums[4]: address=0xd95adffc40 value=5Но при этом имя массива это не стандартный указатель, и мы не можем изменить его адрес, например, так:
int nums[] ; nums++; // так сделать нельзя int b ; nums = &b; // так тоже сделать нельзяУказатели на массивы
Имя массива всегда хранит адрес самого первого элемента. И нередко для перемещения по элементам массива используются отдельные указатели:
int nums[] ; int *ptr ; int num3 = *(ptr+2); std::coutЗдесь указатель ptr изначально указывает на первый элемент массива. Увеличив указатель на 2, мы пропустим 2 элемента в массиве и перейдем к элементу nums[2] .
Можно сразу присвоить указателю адрес конкретного элемента массива:
int nums[] ; int *ptr ; // адрес третьего элемента std::cout #include int main() < const int n = 5; int nums[n]; for(int *ptr; ptr <=&nums[n-1]; ptr++) < std::cout << "address=" << ptr << "\tvalue brush:cpp;">#include int main() < int nums[3][4] < , , >; unsigned int n < sizeof(nums)/sizeof(nums[0]) >; // число строк unsigned int m < sizeof(nums[0])/sizeof(nums[0][0]) >; // число столбцов int *end ; // указатель на самый последний элемент 0 + 3 * 4 - 1 = 11 int *ptr ; // указатель на первый элемент for( unsigned i; ptr > >Поскольку в данном случае мы имеем дело с двухмерным массивом, то адресом первого элемента будет выражение a[0] . Соответственно указатель указывает на этот элемент. С каждой итерацией указатель увеличивается на единицу, пока его значение не станет равным адресу последнего элемента, который хранится в указателе end.
Мы также могли бы обойтись и без указателя на последний элемент, проверяя значение счетчика:
#include int main() < const unsigned n ; // число строк const unsigned m ; // число столбцов int nums[n][m] < , , >; const unsigned count ; // общее количество элементов int *ptr; // указатель на первый элемент первого массива for(unsigned i; i > >Но в обоих случаях программа вывела бы следующий результат:
1 2 3 4 5 6 7 8 9 10 11 12Указатель на строки и массивы символов
Поскольку массив символов может интерпретироваться как строка, то указатель на значения типа char тоже может интерпретироваться как строка:
#include int main() < char hello[] ; char *phello ; std::coutПри выводе на консоль значения указателя фактически будет выводиться строка.
Также можно применять операцию разыменовывания для получения отдельных символов, например, выведем первый символ:
std::coutЕсли же необходимо вывести на консоль адрес указателя, то его надо преобразовать к типу void*:
std::coutВ остальном работа с указателем на массив символов производится также, как и с указателями на массивы других типов.
Также поскольку указатель типа char тоже может интерпретироваться как строка, то теоретически мы можем написать следующим образом:
char *phello ;Однако следует учитывать, что строковые литералы в С++ рассматриваются как константы. Поэтому предыдущее определение указателя может при компиляции вызвать как минимум предупреждение, а попытка изменить элементы строки через указатель - к ошибке компиляции. Поэтому при определении указателя на строку, следует определять указатель как указатель на константу:
#include int main() < const char *phello ; // указатель на константу std::coutМассивы указателей
Также можно определять массивы указателей. В некотором смысле массив указателей будет похож на массив, который содержит другие массивы. Однако массив указателей имеет преимущества.
Например, возьмем обычный двухмерный символьный массив - массив, который хранит строки:
#include int main() < char langs[][20] < "C++", "Python", "JavaScript">; std::coutДля определения двухмерного массива мы должны указать как минимум размер вложенных массивов, который будет достаточным, чтобы вместить каждую строку. В данном случае размер каждого вложенного массива - 20 символов. Однако зачем для первой строки - "C++", которая содержит 4 символа (включая концевой нулевой байт) выделять аж 20 байтов? Это - ограничение подобных массивов. Массивы указателей же позволяют обойти подобное ограничение:
#include int main() < const char *langs[] < "C++", "Python", "JavaScript">; // перебор массива for(unsigned i<>; i < std::size(langs); i++) < std::cout >В данном случае элементами массива langs являются указатели: 3 указателя, каждый из которых занимает 4 или 8 байт в зависимости от архитекутуры (размер адреса). Каждый из этих указателей указывает на адрес в памяти, где расположены соответствующие строки: "C++", "Python", "JavaScript". Однако каждая из этих строк будет занимать именно то пространство, которое ей непосредственно необходимо. То есть строка "С++" будет занимать 4 байта. С одной стороны, мы здесь сталкиваемся с дополнительными издержками: дополнительно выделяется память для хранения адресов в указателях. С другой стороны, когда строки в массиве сильно различаются по длине , то мы можем получить общий выигрыш в количестве потребляемой памяти.
Как описать указатель на начало массива
В Си существует связь между указателями и массивами, и связь эта настолько тесная, что эти средства лучше рассматривать вместе. Любой доступ к элементу массива, осуществляемый операцией индексирования, может быть выполнен с помощью указателя. Вариант с указателями в общем случае работает быстрее, но разобраться в нем, особенно непосвященному, довольно трудно.
int a[10];определяет массив a , состоящий из десяти последовательных объектов с именами a[0] , a[1] , . a[9] .
Запись a[i] отсылает нас к i-му элементу массива. Если pa есть указатель на int , т.е. объявлен как
int *pa;то в результате присваивания
pa будет указывать на нулевой элемент a , иначе говоря, pa будет содержать адрес элемента a[0] .
x = *pa;будет копировать содержимое a[0] в x .
Если pa указывает на некоторый элемент массива, то pa+1 по определению указывает на следующий элемент, pa+i — на i-й элемент после pa , а pa-i — на i-й элемент перед pa . Таким образом, если pa указывает на a[0] , то
есть содержимое a[1] , pa+i — адрес a[i] , а *(pa+i) — содержимое a[i] .
Сделанные замечания верны независимо от типа и размера элементов массива a . Смысл слов «добавить 1 к указателю», как и смысл любой арифметики с указателями, состоит в том, чтобы pa+1 указывал на следующий объект, а pa+i — на i-й после pa .
Между индексированием и арифметикой с указателями существует очень тесная связь. По определению значение переменной или выражения типа массив есть адрес нулевого элемента массива. После присваивания
pa и a имеют одно и то же значение. Поскольку имя массива является синонимом адреса его начального элемента, присваивание pa = &a[0] можно также записать в следующем виде:
pa = a;Еще более удивительно (по крайней мере на первый взгляд) то, что a[i] можно записать как *(a+i) . Вычисляя a[i] , Си сразу преобразует его в *(a+i) ; указанные две формы записи эквивалентны. Из этого следует,что записи &a[i] и a+i также будут эквивалентными, т.е. и в том и в другом случае это адрес i-го элемента массива a . С другой стороны, если pa — указатель, то его можно использовать с индексом, т.е. запись pa[i] эквивалентна записи *(pa+i) . Короче говоря, элемент массива можно изображать как в виде указателя со смещением, так и в виде имени массива с индексом.
Между именем массива и указателем, выступающим в роли имени массива, существует одно различие. Указатель — это переменная, поэтому можно написать pa = a или pa++ . Но имя массива не является переменной, и записи вроде a = pa или a++ не допускаются.
Если имя массива передается функции, то последняя получает в качестве аргумента адрес его начального элемента. Внутри вызываемой функции этот аргумент является локальной переменной, содержащей адрес. Мы можем воспользоваться отмеченным фактом и написать еще одну версию функции strlen , вычисляющей длину строки.
/* strlen: возвращает длину строки */ int strlen(char *s)
Так как переменная s — указатель, к ней применима операция ++ ; s++ не оказывает никакого влияния на строку символов в функции, которая обратилась к strlen . Просто увеличивается на 1 некоторая копия указателя,находящаяся в личном пользовании функции strlen . Это значит,что все вызовы, такие как:
strlen("Здравствуй, мир"); /* строковая константа */ strlen(array); /* char array[100]; */ strlen(ptr); /* char *ptr; */char s[];char *s;в определении функции эквивалентны. Мы отдаем предпочтение последнему варианту, поскольку он более явно сообщает,что s есть указатель. Если функции в качестве аргумента передается имя массива, то она может рассматривать его так, как ей удобно — либо как имя массива,либо как указатель, и поступать с ним соответственно. Она может даже использовать оба вида записи, если это покажется уместным и понятным.
Функции можно передать часть массива, для этого аргумент должен указывать на начало подмассива. Например, если a — массив, то в записях
функции f передается адрес подмассива, начинающегося с элемента a[2] . Внутри функции f описание параметров может выглядеть как
f(int arr[])
f(int *arr)Следовательно, для f тот факт, что параметр указывает на часть массива, а не на весь массив, не имеет значения.
Если есть уверенность, что элементы массива существуют, то возможно индексирование и в «обратную» сторону по отношению к нулевому элементу; выражения p[-1] , p[-2] и т.д. не противоречат синтаксису языка и обращаются к элементам, стоящим непосредственно перед p[0] . Разумеется, нельзя «выходить» за границы массива и тем самым обращаться к несуществующим объектам.
Указатель на массив C++
И имеется следующая конструкция, указывающая на массив из 20 элементов типа short:
short (*pas)[20] = &tellПолучившимся типом данных переменной pas является тип: short (*)[20] . Собственно вопрос, как создать переменную данного типа, и выделить ей память при помощи операции new?
Пример взят из книги: Стивен Прата - Язык программирования C++ (6 издание). Стр. 182.
Отслеживать
81k 9 9 золотых знаков 78 78 серебряных знаков 135 135 бронзовых знаков
задан 26 окт 2016 в 15:46
133 1 1 золотой знак 2 2 серебряных знака 9 9 бронзовых знаков2 ответа 2
Сортировка: Сброс на вариант по умолчанию
Если у вас есть объявление массива вида
T a[N];где T - это некоторый тип, а N - это число элементов в массиве, то указатель на первый элемент массива будет иметь тип T * . Например
T *p = a;После этого определения указатель p указывает на первый элемент массива a .
Чтобы выделить динамически память для массива, аналогичного массиву, определенному выше, вы можете записать
T *p = new T[N];Здесь элемент массива имеет тип T , а p как и выше показывает на первый элемент динамически выделенного безыменного массива..
Теперь представим, что T это алиас для типа short[20] , например
typedef short T[20];Тогда ранее показанные объявления для указателя могут быть записаны как
T *p = new T[1];Если снова вернуться к исходному типу short[20] , то получим
short( *p )[20] = &a;short( *p )[20] = new short[1][20];Последнее предложение означает, что выделяется массив из одного элемента (вы можете выделять массив из произвольного числа элементов в соответствии с вашей задачей), элементами которого в свою очередь являются массивы из 20 элементов типа short .
Имейте в виду, что когда используется так называемая арифметика указателей, то значение указателя меняется на значение кратное sizeof( T )
Поэтому если вы, например, объявите указатель как
short( *p )[20] = &a;где T эквивалентно short[20] , то после применения, например, инкремента к этому указателю
указатель будет содержать адрес сразу же после последнего элемента массива a .
Ну, и напоследок пример работы с таким указателем.
#include #include #include #include #include const size_t N = 20; short ( * create_2D_array( size_t n ) )[N] < short ( *p )[N] = new short[n][N]; return p; >int main() < short a[N]; std::iota( std::begin( a ), std::end( a ), 1 ); for ( short x : a ) std::cout ( *p + N ), std::reverse_iterator( *p ), 1 ); for ( short x : *p ) std::coutВывод программы на консоль
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20Как описать указатель на начало массива
Массив представляет собой агрегат из нескольких переменных одного и того же типа. Массив с именем a из LENGTH элементов типа TYPE объявляется так:
TYPE a[LENGTH];Это соответствует тому, что объявляются переменные типа TYPE со специальными именами a[0], a[1], . a[LENGTH-1]. Каждый элемент массива имеет свой номер - индекс. Доступ к x-ому элементу массива осуществляется при помощи операции индексации:
int x = . ; /* целочисленный индекс */ TYPE value = a[x]; /* чтение x-ого элемента */ a[x] = value; /* запись в x-тый элемент */В качестве индекса может использоваться любое выражение, выдающее значение целого типа: char, short, int, long. Индексы элементов массива в Си начинаются с 0 (а не с 1), и индекс последнего элемента массива из LENGTH элементов - это LENGTH-1 (а не LENGTH). Поэтому цикл по всем элементам массива - это
TYPE a[LENGTH]; int indx; for(indx=0; indx LENGTH; indx++) . a[indx]. ;Статические массивы можно объявлять с инициализацией, перечисляя значения их элементов в <> через запятую. Если задано меньше элементов, чем длина массива остальные элементы считаются нулями:
int a10[10] = < 1, 2, 3, 4 >; /* и 6 нулей */Если при описании массива с инициализацией не указать его размер, он будет подсчитан компилятором:
int a3[] = < 1, 2, 3 >; /* как бы a3[3] */В большинстве современных компьютеров (с фон-Неймановской архитектурой) память представляет собой массив байт. Когда мы описываем некоторую переменную или массив, в памяти выделяется непрерывная область для хранения этой переменной. Все байты памяти компьютера пронумерованы. Номер байта, с которого начинается в памяти наша переменная, называется адресом этой переменной (адрес может иметь и более сложную структуру, чем просто целое число - например состоять из номера сегмента памяти и номера байта в этом сегменте). В Си адрес переменной можно получить с помощью операции взятия адреса &. Пусть у нас есть переменная var, тогда &var - ее адрес. Адрес нельзя присваивать целой переменной; для хранения адресов используются указатели (смотри ниже).
Данное может занимать несколько подряд идущих байт. Размер в байтах участка памяти, требуемого для хранения значения типа TYPE, можно узнать при помощи операции sizeof(TYPE), а размер переменной - при помощи sizeof(var). Всегда выполняется sizeof(char)==1. В некоторых машинах адреса переменных (а также агрегатов данных массивов и структур) кратны sizeof(int) или sizeof(double) - это так называемое "выравнивание (alignment) данных на границу типа int". Это позволяет делать доступ к данным более быстрым (аппаратура работает эффективнее).
Язык Си предоставляет нам средство для работы с адресами данных - указатели (pointer)| -. Указатель физически - это адрес некоторой переменной ("указуемой" переменной). Отличие указателей от машинных адресов состоит в том, что указатель может содержать адреса данных только определенного типа. Указатель ptr, который может указывать на данные типа TYPE, описывается так:
TYPE var; /* переменная */ TYPE *ptr; /* объявление ук-ля */ ptr = & var;В данном случае мы занесли в указательную переменную ptr адрес переменной var. Будем говорить, что указатель ptr указывает на переменную var (или, что ptr установлен на var). Пусть TYPE равно int, и у нас есть массив и указатели:
int array[LENGTH], value; int *ptr, *ptr1;Установим указатель на x-ый элемент массива
ptr = & array[x];Указателю можно присвоить значение другого указателя на такой же тип. В результате оба указателя будут указывать на одно и то же место в памяти: ptr1 = ptr;
Мы можем изменять указуемую переменную при помощи операции *
*ptr = 128; /* занести 128 в указуемую перем. */ value = *ptr; /* прочесть указуемую переменную */В данном случае мы заносим и затем читаем значение переменной array[x], на которую поставлен указатель, то есть
*ptr означает сейчас array[x]Таким образом, операция * (значение по адресу) оказывается обратной к операции & (взятие адреса):
& (*ptr) == ptr и * (&value) == valueОперация * объясняет смысл описания TYPE *ptr; оно означает, что значение выражения *ptr будет иметь тип TYPE. Название же типа самого указателя - это (TYPE *). В частности, TYPE может сам быть указательным типом - можно объявить указатель на указатель, вроде char **ptrptr;
Имя массива - это константа, представляющая собой указатель на 0-ой элемент массива. Этот указатель отличается от обычных тем, что его нельзя изменить (установить на другую переменную), поскольку он сам хранится не в переменной, а является просто некоторым постоянным адресом.
массив указатель ____________ _____ array: | array[0] | ptr:| * | | array[1] | | | array[2] |Следствием такой интерпретации имен массивов является то, что для того чтобы поставить указатель на начало массива, надо писать
ptr = array; или ptr = &array[0]; но не ptr = &array;Операция & перед одиноким именем массива не нужна и недопустима!
Такое родство указателей и массивов позволяет нам применять операцию * к имени массива: value = *array; означает то же самое, что и value = array[0];
Указатели - не целые числа! Хотя физически это и номера байтов, адресная арифметика отличается от обычной. Так, если дан указатель TYPE *ptr; и номер байта (адрес), на который указывает ptr, равен byteaddr, то
ptr = ptr + n; /* n - целое, может быть и < 0 */заставит ptr указывать не на байт номер byteaddr + n, а на байт номер
byteaddr + (n * sizeof(TYPE))то есть прибавление единицы к указателю продвигает адрес не на 1 байт, а на размер указываемого указателем типа данных! Пусть указатель ptr указывает на x-ый элемент массива array. Тогда после
TYPE *ptr2 = array + L; /* L - целое */ TYPE *ptr1 = ptr + N; /* N - целое */ ptr += M; /* M - целое */указатели указывают на
ptr1 == &array[x+N] и ptr == &array[x+M] ptr2 == &array[L]Если мы теперь рассмотрим цепочку равенств
*ptr2 = *(array + L) = *(&array[L]) = array[L]то получим ОСНОВНОЕ ПРАВИЛО: пусть ptr - указатель или имя массива. Тогда операции индексации, взятия значения по адресу, взятия адреса и прибавления целого к указателю связаны соотношениями:
ptr[x] тождественно *(ptr+x) &ptr[x] тождественно ptr+x(тождества верны в обе стороны), в том числе при x==0 и x < 0. Так что, например,
ptr[-1] означает *(ptr-1) ptr[0] означает *ptrУказатели можно индексировать подобно массивам. Рассмотрим пример:
/* индекс: 0 1 2 3 4 */ double numbers[5] = < 0.0, 1.0, 2.0, 3.0, 4.0 >; double *dptr = &numbers[2]; double number = dptr[2]; /* равно 4.0 */ numbers: [0] [1] [2] [3] [4] | [-2] [-1] [0] [1] [2] dptrесли dptr = &numbers[x] = numbers + x то dptr[i] = *(dptr + i) = = *(numbers + x + i) = numbers[x + i]Указатель на один тип можно преобразовать в указатель на другой тип: такое преобразование не вызывает генерации каких-либо машинных команд, но заставляет компилятор изменить параметры адресной арифметики, а также операции выборки данного по указателю (собственно, разница в указателях на данные разных типов состоит только в размерах указуемых типов; а также в генерации команд `->' для выборки полей структур, если указатель - на структурный тип).
Целые (int или long) числа иногда можно преобразовывать в указатели. Этим пользуются при написании драйверов устройств для доступа к регистрам по физическим адресам, например:
unsigned short *KISA5 = (unsigned short *) 0172352;
- Как уже было сказано, адреса данных часто выравниваются на границу некоторого типа. Мы же можем задать невыровненное целое значение. Такой адрес будет некорректен.
- Структура адреса, поддерживаемая процессором, может не соответствовать формату целых (или длинных целых) чисел. Так обстоит дело с IBM PC 8086/80286, где адрес состоит из пары short int чисел, хранящихся в памяти подряд. Однако весь адрес (если рассматривать эти два числа как одно длинное целое) не является обычным long-числом, а вычисляется более сложным способом: адресная пара SEGMENT:OFFSET преобразуется так
unsigned short SEGMENT, OFFSET; /*16 бит: [0..65535]*/ unsigned long ADDRESS = (SEGMENT OFFSET; получается 20-и битный физический адрес ADDRESSВ Си принято соглашение, что указатель (TYPE *)0 означает "указатель ни на что". Он является просто признаком, используемым для обозначения несуществующего адреса или конца цепочки указателей, и имеет специальное обозначение NULL. Обращение (выборка или запись данных) по этому указателю считается некорректным (кроме случая, когда вы пишете машинно-зависимую программу и работаете с физическими адресами).
Отметим, что указатель можно направить в неправильное место - на участок памяти, содержащий данные не того типа, который задан в описании указателя; либо вообще содержащий неизвестно что:
int i = 2, *iptr = &i; double x = 12.76; iptr += 7; /* куда же он указал ?! */ iptr = (int *) &x; i = *iptr;Само присваивание указателю некорректного значения еще не является ошибкой. Ошибка возникнет лишь при обращении к данным по этому указателю (такие ошибки довольно тяжело искать!).
При передаче имени массива в качестве параметра функции, как аргумент передается не копия САМОГО МАССИВА (это заняло бы слишком много места), а копия АДРЕСА 0-ого элемента этого массива (т.е. указатель на начало массива).
f(int x )< x++; > g(int xa[])< xa[0]++; > int a[2] = < 1, 1 >; /* объявление с инициализацией */ main()< f(a[0]); printf("%d\n",a[0]); /* a[0] осталось равно 1*/ g(a ); printf("%d\n",a[0]); /* a[0] стало равно 2 */ >В f() в качестве аргумента передается копия элемента a[0] (и изменение этой копии не приводит к изменению самого массива - аргумент x является локальной переменной в f()), а в g() таким локалом является АДРЕС массива a - но не сам массив, поэтому xa[0]++ изменяет сам массив a (зато, например, xa++ внутри g() изменило бы лишь локальную указательную переменную xa, но не адрес массива a).
Заметьте, что поскольку массив передается как указатель на его начало, то размер массива в объявлении аргумента можно не указывать. Это позволяет одной функцией обрабатывать массивы разной длины:
вместо Fun(int xa[5]) < . >можно Fun(int xa[] ) < . >или даже Fun(int *xa )
Если функция должна знать длину массива - передавайте ее как дополнительный аргумент:int sum( int a[], int len ) < int s=0, i; for(i=0; i < len; i++) s += a[i]; return( s ); >. int arr[10] = < . >; . int sum10 = sum(arr, 10); .Количество элементов в массиве TYPE arr[N]; можно вычислить специальным образом, как
#define LENGTH (sizeof(arr) / sizeof(arr[0]))#define LENGTH (sizeof(arr) / sizeof(TYPE))Оба способа выдадут число, равное N. Эти конструкции обычно употребляются для вычисления длины массивов, задаваемых в виде
TYPE arr[] = < . >;без явного указания размера. sizeof(arr) выдает размер всего массива в байтах.
sizeof(arr[0]) выдает размер одного элемента. И все это не зависит от типа элемента (просто потому, что все элементы массивов имеют одинаковый размер).
Строка в Си - это последовательность байт (букв, символов, литер, character), завершающаяся в конце специальным признаком - байтом '\0'. Этот признак добавляется компилятором автоматически, когда мы задаем строку в виде "строка". Длина строки (т.е. число литер, предшествующих '\0') нигде явно не хранится. Длина строки ограничена лишь размером массива, в котором сохранена строка, и может изменяться в процессе работы программы в пределах от 0 до длины массива-1. При передаче строки в качестве аргумента в функцию, функции не требуется знать длину строки, т.к. передается указатель на начало массива, а наличие ограничителя '\0' позволяет обнаружить конец строки при ее просмотре.
С массивами байт можно использовать следующую конструкцию, задающую массивы (строки) одинакового размера:
char stringA [ITSSIZE]; char stringB [sizeof stringA];В данном разделе мы в основном будем рассматривать строки и указатели на символы.
2.1.
Операции взятия адреса объекта и разыменования указателя - взаимно обратны.
TYPE objx; TYPE *ptrx = &objx; /* инициализируем адресом objx */ *(&objx) = objx; &(*ptrx) = ptrx;Вот пример того, как можно заменить условный оператор условным выражением (это удастся не всегда):
if(c) a = 1; else b = 1;Предупреждение: такой стиль не способствует понятности программы и даже компактности ее кода.
#include int main(int ac, char *av[]) < int a, b, c; a = b = c = 0; if(av[1]) c = atoi(av[1]); *(c ? &a : &b) = 1; /* . */ printf("cond=%d a=%d b=%d\n", c, a, b); return 0; >2.2.
Каким образом инициализируются по умолчанию внешние и статические массивы? Инициализируются ли по умолчанию автоматические массивы? Каким образом можно присваивать значения элементам массива, относящегося к любому классу памяти?
2.3.
Пусть задан массив int arr[10]; что тогда означают выражения:
arr[0] *arr *arr + 2 arr[2] *(arr + 2) arr &arr[2] arr+22.4.
Правильно ли написано увеличение величины, на которую указывает указатель a, на единицу?
*a++;Ответ: нет, надо:
(*a)++; или *a += 1;2.5.
Дан фрагмент текста:
char a[] = "xyz"; char *b = a + 1;Чему равны
b[-1] b[2] "abcd"[3](Ответ: 'x', '\0', 'd' )
Можно ли написать a++ ? То же про b++ ? Можно ли написать b=a ? a=b ? (нет, да, да, нет)
2.6.
Ниже приведена программа, вычисляющая среднее значение элементов массива
int arr [] = ; main () < int i; long sum; for ( i = 0, sum = 0L; i < (sizeof(arr)/sizeof(int)); i++ ) sum += arr[i]; printf ("Среднее значение = %ld\n", sum/8) >Перепишите указанную программу с применением указателей.
2.7.
Что напечатается в результате работы программы?
char arr[] = ; main ()Почему массив arr[] описан вне функции main()? Как внести его в функцию main() ?
Ответ: написать внутри main
static char arr[]=.2.8.
Можно ли писать на Си так:
f( n, m )Ответ: к сожалению нельзя (Си - это не Algol). При отведении памяти для массива в качестве размера должна быть указана константа или выражение, которое может быть еще во время компиляции вычислено до целочисленной константы, т.е. массивы имеют фиксированную длину.
2.9.
Предположим, что у нас есть описание массива
static int mas[30][100];
- выразите адрес mas[22][56] иначе
- выразите адрес mas[22][0] двумя способами
- выразите адрес mas[0][0] тремя способами
2.10.
Составьте программу инициализации двумерного массива a[10][10], выборки элементов с a[5][5] до a[9][9] и их распечатки. Используйте доступ к элементам по указателю.
2.11.
Составьте функцию вычисления скалярного произведения двух векторов. Длина векторов задается в качестве одного из аргументов.
2.12.
Составьте функцию умножения двумерных матриц a[][] * b[][].
2.13.
Составьте функцию умножения трехмерных матриц a[][][] * b[][][].
2.14.
Для тех, кто программировал на языке Pascal: какая допущена ошибка?
char a[10][20]; char c; int x,y; . c = a[x,y];Ответ: многомерные массивы в Си надо индексировать так:
c = a[x][y];В написанном же примере мы имеем в качестве индекса выражение x,y (оператор "запятая") со значением y, т.е.
c = a[y];Синтаксической ошибки нет, но смысл совершенно изменился!
2.15.
Двумерные массивы в памяти представляются как одномерные. Например, если
int a[N][M];то конструкция a[y][x] превращается при компиляции в одномерную конструкцию, подобную такой:
int a[N * M]; /* массив развернут построчно */ #define a_yx(y, x) a[(x) + (y) * M]a[y][x] есть *(&a[0][0] + y * M + x)Следствием этого является то, что компилятор для генерации индексации двумерных (и более) массовов должен знать M - размер массива по 2-ому измерению (а также 3-ему, 4-ому, и.т.д.). В частности, при передаче многомерного массива в функцию
f(arr) int arr[N][M]; < . >/* годится */ f(arr) int arr[] [M]; < . >/* годится */ f(arr) int arr[] []; < . >/* не годится */ f(arr) int (*arr)[M]; < . >/* годится */ f(arr) int *arr [M]; < . >/* не годится: это уже не двумерный массив, а одномерный массив указателей */А также при описании внешних массивов:
extern int a[N][M]; /* годится */ extern int a[ ][M]; /* годится */ extern int a[ ][ ]; /* не годится: компилятор не сможет сгенерить операцию индексации */Вот как, к примеру, должна выглядеть работа с двумерным массивом arr[ROWS][COLS], отведенным при помощи malloc();
void f(int array[][COLS]) < int x, y; for(y=0; y < ROWS; y++) for(x=0; x < COLS; x++) array[y][x] = 1; >void main()
2.16.
Как описывать ссылки (указатели) на двумерные массивы? Рассмотрим такую программу:
#include #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Указателем здесь является ptr. Отметим, что у него задана размерность по второму измерению: Second, именно для того, чтобы компилятор мог правильно вычислить двумерные индексы.
Попробуйте сами объявить
char (*ptr)[4]; char (*ptr)[6]; char **ptr;и увидеть, к каким невеселым эффектам это приведет (компилятор, кстати, будет ругаться; но есть вероятность, что он все же странслирует это для вас. Но работать оно будет плачевно). Попробуйте также использовать ptr[x][y].
Обратите также внимание на инициализацию строк в нашем примере. Строка "ABC." равносильна объявлению
2.17.
Массив s моделирует двумерный массив char s[H][W]; Перепишите пример при помощи указателей, избавьтесь от операции умножения. Прямоугольник (x0,y0,width,height) лежит целиком внутри (0,0,W,H).
char s[W*H]; int x,y; int x0,y0,width,height; for(x=0; x < W*H; x++) s[x] = '.'; . for(y=y0; y < y0+height; y++) for(x=x0; x < x0+width; x++) s[x + W*y] = '*';char s[W*H]; int i,j; int x0,y0,width,height; char *curs; . for(curs = s + x0 + W*y0, i=0; i < height; i++, curs += W-width) for(j=0; j < width; j++) *curs++ = '*';Такая оптимизация возможна в некоторых функциях из главы "Работа с видеопамятью".
2.18.
Что означают описания?
int i; // целое. int *pi; // указатель на целое. int *api[3]; // массив из 3х ук-лей на целые. int (*pai)[3]; // указатель на массив из 3х целых. // можно описать как int **pai; int fi(); // функция, возвращающая целое. int *fpi(); // ф-ция, возвр. ук-ль на целое. int (*pfi)(); // ук-ль на ф-цию, возвращающую целое. int *(*pfpi)(); // ук-ль на ф-цию, возвр. ук-ль на int. int (*pfpfi())(); // ф-ция, возвращающая указатель на // "функцию, возвращающую целое". int (*fai())[3]; // ф-ция, возвр. ук-ль на массив // из 3х целых. иначе ее // можно описать как int **fai(); int (*apfi[3])(); // массив из 3х ук-лей на функции, // возвращающие целые.Переменные в Си описываются в формате их использования. Так описание
int (*f)();означает, что f можно использовать в виде
int value; value = (*f)(1, 2, 3 /* список аргументов */);Однако из такого способа описания тип самой описываемой переменной и его смысл довольно неочевидны. Приведем прием (позаимствованный из журнала "Communications of the ACM"), позволяющий прояснить смысл описания. Описание на Си переводится в описание в стиле языка Algol-68. Далее
ref ТИП означает "указатель на ТИП" proc() ТИП "функция, возвращающая ТИП" array of ТИП "массив из элементов ТИПа" x: ТИП "x имеет тип ТИП"Приведем несколько примеров, из которых ясен и способ преобразования:
int (*f())(); означает (*f())() : int *f() : proc() int f() : ref proc() int f : proc() ref proc() intто есть f - функция, возвращающая указатель на функцию, возвращающую целое.
int (*f[3])(); означает (*f[])() : int *f[] : proc() int f[] : ref proc() int f : array of ref proc() intf - массив указателей на функции, возвращающие целые. Обратно: опишем
g как указатель на функцию, возвращающую указатель на массив из 5и указателей на функции, возвращающие указатели на целые.g : ref p() ref array of ref p() ref int *g : p() ref array of ref p() ref int (*g)() : ref array of ref p() ref int *(*g)() : array of ref p() ref int (*(*g)())[5] : ref p() ref int *(*(*g)())[5] : p() ref int (*(*(*g)())[5])(): ref int *(*(*(*g)())[5])(): int int *(*(*(*g)())[5])();В Си невозможны функции, возвращающие массив:
proc() array of . а только proc() ref array of .Само название типа (например, для использования в операции приведения типа) получается вычеркиванием имени переменной (а также можно опустить размер массива):
g = ( int *(*(*(*)())[])() ) 0;2.19.
Напишите функцию strcat(d,s), приписывающую строку s к концу строки d.
char *strcat(d,s) register char *d, *s; < while( *d ) d++; /* ищем конец строки d */ while( *d++ = *s++ ); /* strcpy(d, s) */ return (d-1); /* конец строки */ >Цикл, помеченный "strcpy" - это наиболее краткая запись операторов
do < char c; c = (*d = *s); s++; d++; >while(c != '\0');На самом деле strcat должен по стандарту возвращать свой первый аргумент, как и функция strcpy:
char *strcat(d,s) register char *d, *s;Эти два варианта демонстрируют, что функция может быть реализована разными способами. Кроме того видно, что вместо стандартной библиотечной функции мы можем определить свою одноименную функцию, несколько отличающуюся поведением от стандартной (как возвращаемое значение в 1-ом варианте).
© Copyright А. Богатырев, 1992-95
Си в UNIX