Как передать ссылку на массив в функцию c
На этом шаге мы рассмотрим способы передачи массивов в функцию и их возврата.
При решении задач часто требуется использовать массив в качестве параметра функции, кроме того, функции могут возвращать указатель на массив в качестве результата. Рассмотрим реализацию этих возможностей.
При передаче массивов через механизм параметров возникает задача определения в теле функции количества элементов массива, использованного в качестве фактического параметра. При работе со строками, то есть с массивами типа char[] , последний элемент каждого из которых имеет значение ‘\0’, анализируется каждый элемент, пока не встретится символ ‘\0’, и это считается концом строки-массива.
Пример 1. Проиллюстрируем передачу строк, описанных по-разному, в функцию.
#include int len(char e[]) //Функция вычисления < //длины строки. int m=0; while (e[m++]); return m-1; > void main()
Текст этой программы можно взять здесь.
В функции len() строка-параметр представлена как массив, и обращение к его элементам выполняется с помощью явного индексирования.
Если массив-параметр функции не есть символьная строка, то нужно либо использовать только массивы фиксированного, заранее определенного размера, либо передавать значение размера массива в функцию явным образом. Часто это делается с помощью дополнительного параметра.
Пример 2. Составить массив, каждый элемент которого равен максимальному из соответствующих значений двух других массивов.
#include void max_vect(int n, int *x, int *y, int *z) < for (int i=0;i
Текст этой программы можно взять здесь.
Заголовок функции max_vect() можно записать по-другому, учитывая тот факт, что имя массива — это указатель на начальный элемент массива: void max_vect(int n, int x[], int y[], int z[]) .
В качестве результата работы функции может быть указатель на массив, создаваемый в этой функции. Изменим предыдущий пример: количество элементов массивов будем расчитывать программно; функция возвратит указатель на созданный массив.
Пример 3. Составить массив, каждый элемент которого равен максимальному из соответствующих значений двух других массивов. Функция должна вернуть указатель на созданный массив.
#include int *max_vect(int n, int *x, int *y) < int *z = new int[n]; //Выделение памяти для //элементов массива. for (int i=0;i//Расчет элементов массива. z[i]=x[i]>y[i]?x[i]:y[i]; return z; > //--------------------------// void main() < int a[]=; int b[]=; int kc=sizeof(a)/sizeof(a[0]); //Количество элементов //массива. int *c; //Указатель на результирующий массив. c=max_vect(kc,a,b); //Создание массива. for (int i=0;i //Вывод результата. cout//Возврат памяти в "кучу". >
Текст этой программы можно взять здесь.
Особенность использования массивов в C++ заключается в том, что по имени массива нельзя определить его размерность и размеры по каждому измерению. По определению, многомерный массив не существует, он рассматривается как одномерный массив, каждый элемент которого, в свою очередь, представляет собой массив. При необходимости передать в функцию многомерный массив нужно указать значение каждой размерности, что «отвергается» компилятором.
Указанные ограничения на возможность применения многомерных массивов в качестве параметров можно обойти нескольким путями. Первый путь — подмена многомерного массива одномерным и имитация внутри функции доступа к многомерному массиву. Второй путь — использование вспомогательных массивов указателей на массивы . Одномерные массивы служат в этом случае для представления строк матриц. Так как вспомогательный массив указателей и массивы-строки матрицы являются одномерными, то их размеры могут быть опущены в ее соответствующих спецификациях формальных параметров. Тем самым появляется возможность обработки в теле функции двухмерных, а в более общем случае и многомерных массивов с изменяющимися размерами. Конкретные значения размеров должны передаваться функции либо с помощью дополнительных параметров, либо с пользованием глобальных (внешних) переменных. Третий путь предусматривает применение классов представления многомерных массивов .
Приведем пример программы, иллюстрирующей первый из перечисленных способов. В функции будем передавать только размерность и указатель на массив, а внутри функций будем рассматривать этот указатель как указатель на квадратную матрицу заданной размерности.
Пример 4. Составить программу транспонирования квадратной матрицы.
#include #include void main() < void print (int,int*); //Прототип функции печати матрицы. void trans (int,int*); //Прототип функции транспонирования. int n; cout > n; int *a= new int[n*n]; //Резервируем место под элементы таблицы. cout //Используем датчик for (int i=0;i//псевдослучайных чисел. for (int j=0;j //Печать исходной таблицы. trans(n,a); //Транспонирование матрицы. cout //Печать результирующей таблицы. delete []a; //Возвращение памяти в "кучу". > void print (int x, int *b) //Функция печати таблицы. < for (int i=0;i for (int j=0;j > void trans (int x, int *b) //Функция транспонирования матрицы. < int c; for (int i=0;i for (int j=i+1;j >
Текст этой программы можно взять здесь.
Многомерный массив с переменными размерами, сформированный в функции, непосредственно невозможно вернуть в вызывающую программу как результат выполнения функции. Однако возвращаемым функцией значением может быть указатель на одномерный массив указателей на одномерные массивы с элементами известной размерности и заданного типа. В следующей программе функция single_matr() возвращает именно такой указатель, так как имеет тип int ** . В тексте функции формируется набор одномерных массивов с элементами типа int и создается массив указателей на эти одномерные массивы. Количество одномерных массивов и их длины определяются значением параметра функции, описанного как int n . Совокупность создаваемых динамических массивов представляет квадратную матрицу порядка n . Диагональным элементам матрицы присваиваются единичные значения, остальным — нулевые, то есть создается единичная матрица. Локализованный в функции single_matr() указатель int** p «настраивается» на создаваемый динамический массив указателей и используется в операторе возврата из функции как возвращаемое значение. В основной программе вводится с клавиатуры желаемое значение порядка матрицы ( int n ), а после ее формирования печатается результат.
#include #include //Для exit(). int **single_matr(int n) < //Вспомогательный указатель на матрицу. int **p; //Массив указателей на строки - одномерные массивы. p= new int *[n]; if (p==NULL) < cout for (int i=0;i//Формирование строки элементов типа int. p[i]=new int [n]; if (p[i]==NULL) < cout //Заполнение текущей строки. for (int j=0;j if (j!=i) p[i][j]=0; else p[i][j]=1; > return p; > void main() < int n; cout > n; int **matr; //Указатель для формируемой матрицы. matr = single_matr(n); for (int i=0;i for (int j=0;j for (i=0;i //Очистка памяти. delete matr[i]; delete [] matr; >
Текст этой программы можно взять здесь.
Со следующего шага мы начнем знакомиться со строками.
Передача массива в функцию, подсчет длины массива по указателю
-
В С++ массивы можно инициализировать следующим образом:
int arr[] = ; // длина массива определяется после инициализации
Как следствие, его можно передать в функцию таким же способом:
void func(int arr[])< //your code >
void func(int* arr) < //your code >int arr[5] = ;
Мы привыкли обращаться к элементам массива по индексам, но попробуйте скомпилировать и запустить следующие строки:
int main()< int arr[5] = ; cout
void func(int* arr, int length)< //your code >
Но в ряде задач длина входного массива может быть неизвестной. На этот случай тоже есть решение, мы можем анализировать данные, которые поступили в функцию по указателю, например:
int len(int* arr)
Крайне важно понять критерий по которому мы будем оценивать содержимое. В противном случае, можно получить не совсем то, что ожидалось.
Указатели и ссылки


Указатели – одна из самых сложных тем в программировании, постараюсь рассказать её попроще и ближе к практике. Начнём с представления данных в памяти микроконтроллера: в прошлом уроке про битовые операции мы обсуждали, что минимальным адресуемым блоком памяти является байт, то есть мы можем обратиться к любому байту в памяти микроконтроллера. Работая с переменными, мы не задумываемся об адресах и расположении данных в памяти, мы просто используем их имена для чтения/записи, передаём имена в качестве аргументов в функции и совершаем прочие действия с данными. Владение адресами блоков данных позволяет делать многие вещи более быстро и эффективно с точки зрения памяти. Несколько примеров возможностей, которые дают указатели:
- При помощи указателей можно разбивать любые данные (переменные всех типов, структуры) на биты для последующей манипуляции с ними (передача/запись/чтение).
- Можно передавать адреса блоков данных в качестве аргументов в функции, таким образом при вызове функции не создаются копии переменных и код работает быстрее. Другими словами – дать возможность функции менять передаваемый аргумент.
- Работа с динамической памятью “напрямую”, выделение памяти под переменные и объекты. Читай урок про динамическую память.
Что же такое указатель? Это переменная, которая содержит адрес области данных (переменной/структуры/объекта/функции и т.п.) в памяти микроконтроллера, точнее – его самого первого блока, байта. Зная адрес первого байта в блоке данных, можно получить контроль над данными по этому адресу, но нужно знать размер этого блока. Поэтому при создании указателя мы указываем, на какой тип данных он указывает, это может быть любой тип данных.
Данный урок является максимально коротким и “справочным”, более подробно об указателях, ссылках и их особенностях рекомендую почитать в справочнике по C++, в статье на Хабре про указатели и массивы и большой и сложной статье на тему указателей со сложными примерами.
“Адресные” операторы
Операторов у нас немного, но они являются одной из самых мощных “фишек” языка С++:
- & – возвращает адрес в памяти
- * – обращение (чтение, запись) по указанному адресу
- -> – оператор косвенного обращения к членам и методам (для указателей на структуры и классы). Является короткой записью конструкции через указатель: a->b равносильно (*a).b
Мы можем создать указатель на нужный тип данных вот таким образом:
тип_данных* имя_указателя; тип_данных * имя_указателя; тип_данных *имя_указателя;
Да, можно перепутать с умножением, но компилятор не перепутает. Все три варианта записи равносильны, в чужом коде или статье можно встретить любой из них.
Указатели на “обычные” переменные
Работа с указателем позволяет читать/менять значение переменной через её адрес. Смотрим пример с комментариями:
byte b; // просто переменная типа byte b = 10; // b теперь 10 byte* ptr; // ptr – переменная "указатель на объект типа byte" ptr = &b; // указатель ptr хранит адрес переменной b *ptr = 24; // b теперь равна 24 (записываем по адресу &b) byte s; // переменная s s = *ptr; // s теперь тоже равна 24 (читаем по адресу &b)
Вроде бы ничего сложного: создали указатель ptr на byte ( byte* ptr ) и записали в него адрес переменной b: ptr = &b . Теперь мы имеем власть над переменной b через указатель ptr , можем менять её значение как *ptr = значение; или читать как *ptr; .
Давайте попробуем передать адрес в функцию и изменить значение переменной по её адресу внутри функции. Пусть у нас будет функция, которая принимает адрес переменной типа int и возводит эту переменную в квадрат.
void square(int* val)
Вот так будем это использовать:
int value = 7; // создали переменную square(&value); // передали её адрес в функцию // тут value уже == 49
Чем хорош этот подход? Мы не создавали копию переменной, как это делалось в уроке про функции, а просто передали адрес и меняли значения напрямую.
Указатели на массивы
Про массивы у нас был отдельный урок, и там я не стал вас грузить и рассказывать “откуда берутся массивы”, потому что массивы – это на самом деле указатель и его друзья. Имя массива является указателем на первый элемент этого массива, а тип элемента мы задаём при объявлении массива. То есть myArray[0] == *myArray или myArray == &myArray[0] . Для упрощения написания и чтения кода введены квадратные скобки, а на самом деле это работает так: a[b] == *(a + b) !
Массив – это область памяти, заполненная “переменными” указанного типа, и мы можем обращаться к ним по адресу. Пара примеров по работе с массивом без использования квадратных скобок:
void setup() < Serial.begin(9600); // работаем без [] скобок byte myArray[] = ; // выведет 1 2 3 4 5 for (byte i = 0; i < 5; i++) < Serial.print(*(myArray + i)); Serial.print(' '); >Serial.println(); // работаем с отдельным указателем int myArray2[] = ; int* ptr2 = myArray2; // указатель на массив // выведет 10 20 30 40 50 for (byte i = 0; i < 5; i++) < Serial.print(*ptr2); ptr2++; Serial.print(' '); >>
Обратите внимание на второй пример: в цикле идёт увеличение указателя на единицу, ptr2++ , таким образом и осуществляется “переключение” на следующий элемент массива. Такое устройство массивов позволяет без лишних проблем передавать их в качестве аргументов в функции. Пример: функция, возвращающая сумму всех элементов в массиве:
void setup() < Serial.begin(9600); int myArray[] = ; Serial.println(sumArray(myArray)); > int sumArray(int* arrayPtr) < int sum = 0; // суммируем массив for (byte i = 0; i < 6; i++) sum += arrayPtr[i]; return sum; // возвращаем >
Важный момент: массив “не знает”, какого он размера, это всего лишь выделенная область памяти. Для универсальности такого подхода нужно обязательно знать размер массива заранее или передавать его в качестве аргумента:
void setup() < Serial.begin(9600); int myArray[] = ; // выводим сумму массива Serial.println( sumArray(myArray, sizeof(myArray)/sizeof(int)) ); // передали массив и его размер в количестве элементов > long sumArray(int* arrayPtr, int arrSize) < long sum = 0; for (int i = 0; i < arrSize; i++) sum += arrayPtr[i]; return sum; // возвращаем >
Важный момент: функция должна знать, какой тип массива в неё передаётся (в нашем случае int ).
Указатель на функцию
Функция тоже имеет свой адрес в памяти, по которому к ней можно обратиться. По указателю функцию можно просто вызвать, а можно передать её в качестве аргумента в другую функцию. Указатель на функцию объявляется так:
возвращаемый_тип_данных (*имя)(аргументы)
Затем указателю можно присвоить адрес любой функции просто как имя (оператор взятия адреса, как и в случае с массивами, не нужен). Для вызова функции через указатель оператор * также не нужен:
void setup() < Serial.begin(9600); void (*ptrF)(byte a); // указатель на функцию (она объявлена ниже) ptrF = printByte; // даём адрес функции printByte ptrF(125); // вызов printByte через указатель (выведет 125) int (*ptrFunc)(byte a, byte b); // сделаем ещё указатель ptrFunc = sumFunc; // на функцию sumFunc // вызовем printByte, которой передадим результат sumFunc // через указатель ptrFunc ptrF(ptrFunc(10, 30)); // выведет 40 >void printByte(byte b) < Serial.println(b); >int sumFunc(byte a, byte b) < return (a + b); >void loop() <>
Таким способом можно реализовать в библиотеке фишку в стиле “attachInterrupt” – хранить в классе адрес функции и вызывать её из класса.
Указатель на структуры/классы
Структуры и классы являются составными типами данных, механизм взаимодействия с ними через указатели чуть отличается. Давайте создадим структуру, указатель на неё, и обратимся к структуре через него:
struct myStruct < byte myByte; int myInt; >; // создадим структуру someStruct myStruct someStruct; // указатель типа myStruct* на структуру someStruct myStruct* p = &someStruct; // пишем по адресу в someStruct.myInt p -> myInt = -666; //(*p).myInt = -666; // или так, см. начало урока
Таким образом можно передавать из функции в функцию большие структуры, не создавая их копии в памяти – программа будет работать быстрее! С классом будет всё абсолютно то же самое.
Указатель на void
Во всех предыдущих примерах мы создавали указатель на заранее известный тип данных. А что делать, если хочется передать адрес на неизвестный тип данных? Можно сделать void* ptr – указатель на void , любой тип. В дальнейшей работе нужно будет преобразовать указатель к нужному типу данных:
float Fval = 0.254; // переменная float void* ptrV = &Fval; // указатель на любой тип (дали ему float, он не против) // создали Fptr - указатель на float // и преобразовали неизвестный ptrV во float float* Fptr = (float*)ptrV; // теперь *Fptr равен 0.254
Здесь мы преобразовали ptrV , который был void* (указатель на void ) в указатель на float при помощи (float*) . Иногда это может быть удобно, например при передаче данных разного формата при помощи одной “универсальной функции”. Также можно встретить преобразование типа указателя через cast :
float* Fptr = static_cast(ptrV);
Разбивка на байты
Иногда бывает нужно передать какой-нибудь кусок данных, а потом принять его с другой стороны. Либо записать эти данные на какой-нибудь внешний носитель (EEPROM, карта памяти и подобное), а потом считать его обратно. Нужен универсальный инструмент, который запишет любую дату, а потом корректно её прочитает. Для решения этой задачи можно использовать указатели, делается это следующим образом: создаём указатель на тип byte , присваиваем ему адрес блока данных любого типа, выполнив преобразование (byte*) . Получим просто указатель на первый байт данных. Зная длину (размер в байтах) нашего куска данных, можем считать его побайтно, просто прибавляя к адресу единицу!
Рассмотрим простой пример с разбиванием числа unsigned long на 4 байта при помощи указателей:
// большое число uint32_t bigVal = 123456789; // указатель ptrB на адрес &bigVal // приведённый к (byte*) byte* ptrB = (byte*)&bigVal; // разбиваем на байты byte bigVal_1 = *ptrB; byte bigVal_2 = *(ptrB + 1); byte bigVal_3 = *(ptrB + 2); byte bigVal_4 = *(ptrB + 3); // попробуем собрать обратно // понадобится новая переменная // такого же типа, что и первая (uint32_t) uint32_t newBig; byte* ptrN = (byte*)&newBig; // и собираем 4 байта обратно! *ptrN = bigVal_1; *(ptrN + 1) = bigVal_2; *(ptrN + 2) = bigVal_3; *(ptrN + 3) = bigVal_4; // в этом месте newBig равна 123456789
Таким образом можно “разобрать” и “собрать” любой набор данных (массив любого типа, структуру), зная его размер. Задачу можно решить более красиво, используя массив байтов для чтения и записи. Рассмотрим примеры с приведением типа указателя через (byte*) , через void* и через template :
Пример через (byte*)
// буфер byte buffer[20]; // структура для теста struct myStruct < byte val1; int val2; long val3; float val4; >; void setup() < // === тест с переменными === long a = 123456789; long b = 0; // разбиваем блок данных a на байты // и сохраняем в buffer writeData((byte*)&a, sizeof(a)); // собираем блок данных b // из буфера buffer readData((byte*)&b, sizeof(b)); // здесь b == 123456789 // === тест со структурами === // создаём структуру myStruct transmit; // присваиваем значение члену val4 transmit.val4 = 3.1415; // "приёмная" структура myStruct recieve; // разбиваем блок данных transmit на байты // и сохраняем в buffer writeData((byte*)&transmit, sizeof(transmit)); // собираем блок данных recieve // из буфера buffer readData((byte*)&recieve, sizeof(recieve)); // здесь recieve.val4 == 3.1415 >void writeData(byte* data, int length) < int i = 0; while (length--) < buffer[i] = *(data + i); i++; >> void readData(byte* data, int length) < int i = 0; while (length--) < *(data + i) = buffer[i]; i++; >> void loop() <>
Пример через void*
// буфер byte buffer[20]; // структура для теста struct myStruct < byte val1; int val2; long val3; float val4; >; void setup() < // === тест с переменными === long a = 123456789; long b = 0; // разбиваем блок данных a на байты // и сохраняем в buffer writeData(&a, sizeof(a)); // собираем блок данных b // из буфера buffer readData(&b, sizeof(b)); // здесь b == 123456789 // === тест со структурами === // создаём структуру myStruct transmit; // присваиваем значение члену val4 transmit.val4 = 3.1415; // "приёмная" структура myStruct recieve; // разбиваем блок данных transmit на байты // и сохраняем в buffer writeData(&transmit, sizeof(transmit)); // собираем блок данных recieve // из буфера buffer readData(&recieve, sizeof(recieve)); // здесь recieve.val4 == 3.1415 >void writeData(void* data, int length) < uint8_t* dataByte = (uint8_t*)data; int i = 0; while (length--) < buffer[i] = *(dataByte + i); i++; >> void readData(void* data, int length) < uint8_t* dataByte = (uint8_t*)data; int i = 0; while (length--) < *(dataByte + i) = buffer[i]; i++; >> void loop() <>
Пример через template
// буфер byte buffer[20]; // структура для теста struct myStruct < byte val1; int val2; long val3; float val4; >; void setup() < // === тест с переменными === long a = 123456789; long b = 0; // разбиваем блок данных a на байты // и сохраняем в buffer writeData(a); // собираем блок данных b // из буфера buffer readData(b); // здесь b == 123456789 // === тест со структурами === // создаём структуру myStruct transmit; // присваиваем значение члену val4 transmit.val4 = 3.1415; // "приёмная" структура myStruct recieve; // разбиваем блок данных transmit на байты // и сохраняем в buffer writeData(transmit); // собираем блок данных recieve // из буфера buffer readData(recieve); // здесь recieve.val4 == 3.1415 >template void writeData(T &data) < const uint8_t *ptr = (const uint8_t*) &data; for (uint16_t i = 0; i < sizeof(T); i++) < buffer[i] = *ptr++; >> template void readData(T &data) < uint8_t *ptr = (uint8_t*) &data; for (uint16_t i = 0; i < sizeof(T); i++) < *ptr++ = buffer[i]; >> void loop() <>
Данные примеры отличаются только вариантом передачи аргумента-адреса и его обработки:
- В первом случае мы приводим указатель к (byte*) при передаче аргумента в функцию. Также передаём размер блока данных при помощи sizeof()
- Во втором случае у нас void* и ему всё равно, какой тип данных ему передадут. Дальше мы переводим указатель в uint8_t через reinterpret_cast . Также передаём размер блока данных при помощи sizeof()
- Третий вариант – через шаблонную функцию, которой вообще без разницы, какой у данных тип и размер: она принимает данные по ссылке. Далее делаем указатель на первый байт и прямо внутри функции вычисляем размер блока данных через sizeof() . Это самый мощный и универсальный вариант.
Примечание: все три примера занимают одинаковый объём в памяти.
Ссылки
Ссылки – по сути те же самые указатели, но с другим синтаксисом. В отличие от указателя, ссылка
- Должна быть инициализирована при создании
- Не может быть изменена в процессе работы программы
тип_данных &имя_ссылки = данные;
Ссылка может работать в тех же случаях, что и указатель:
- “Перехватывать управление” данными, т.н. ссылка в качестве “псевдонима”, через неё можно читать и писать
- Быть аргументом в функциях
- Осуществлять боле удобный доступ к данным
В отличие от указателя, ссылка не может быть изменена, т.е. всегда указывает на те данные, которые ей показали при инициализации!
Ссылки на типы данных
Рассмотрим первый пример с указателями в этой главе, но с использованием ссылок. Пример полностью аналогичный с точки зрения происходящего, но вместо указателей используются ссылки:
byte b; // просто переменная типа byte b = 10; // b теперь 10 byte &link = b; // link – переменная "ссылка на объект типа byte" link = 24; // b теперь равна 24 (записываем через ссылку) byte s; // переменная s s = link; // s теперь тоже равна 24 (читаем по ссылке)
Вспомним пример с функцией, возводящей переменную в квадрат, и перепишем её через ссылки: передадим ссылку на переменную в качестве аргумента функции.
void setup() < int value = 7; // создали переменную square(value); // передали её в функцию // тут value уже == 49 >void square(int &val) < // val - ссылка на аргумент! val = val * val; >
Работа программы полностью идентична примеру с указателем, но в таком применении ссылки гораздо удобнее, чем указатели: нужно писать меньше символов, код выглядит более читаемым.
Ссылка на String-строку
Строка тоже является объёмом данных, на который можно сослаться. Эту тему я вынес в отдельную главу, потому что она очень важная: строки являются тяжёлым инструментом, который при неправильном использовании может привести к внезапному переполнению оперативной памяти и микроконтроллер зависнет. Основной момент, где нам понадобятся адреса – передача строки в качестве аргумента. Если передавать строку по значению – в оперативной памяти будет создан новый экземпляр строки, то есть на протяжении работы функции у нас в памяти будут две одинаковые строки! Если память достаточно сильно занята, а строка большая – может случиться беда. Простейший пример, функция принимает аргументом строку и просто выводит её в порт:
void setup() < Serial.begin(9600); // для отладки String myString = "hello world! Hello again!"; printString(myString); >void printString(String string) < Serial.println(string); >void loop() <>
Внутри функции printString() в памяти существуют две одинаковые строки, а при её вызове тратится время на выделение памяти под копию строки и копирование её туда.
Оптимизировать такой сценарий очень просто: передавать строку не по значению, создавая её копию, а по адресу, через & ссылку:
void setup() < Serial.begin(9600); // для отладки String myString = "hello world! Hello again!"; printString(myString); >void printString(String &string) < Serial.println(string); >void loop() <>
Такой вариант работы со строкой быстрее и безопаснее для тяжёлых скетчей и больших строк.
Ссылка на структуру
Использование ссылок позволяет упростить доступ ко вложенным структурам:
struct Values < int value1; float value2; >; struct BigStruct < Values values; // элемент типа Values int otherValue; >; BigStruct myStruct; // делаем ссылку на элемент float float &link = myStruct.values.value2; // присваиваем через ссылку link = 3.14; // здесь myStruct.values.value2 == 3.14
Синтаксис опять же чуть компактнее, чем с указателем: обращаемся как обычно, через точку, а не через стрелочку -> .
Полезные страницы
- Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
Как передать ссылку на массив в функцию c
Я постараюсь окончательно разобрать такие тонкие понятия в C и C++, как указатели, ссылки и массивы. В частности, я отвечу на вопрос: являются ли массивы C указателями или нет.
Обозначения и предположения
- Я буду предполагать, что читатель понимает, что, например, в C++ есть ссылки, а в C — нет, поэтому я не буду постоянно напоминать, о каком именно языке (C/C++ или именно C++) я сейчас говорю, читатель поймёт это из контекста;
- Также, я предполагаю, что читатель уже знает C и C++ на базовом уровне и знает, к примеру, синтаксис объявления ссылки. В этом посте я буду заниматься именно дотошным разбором мелочей;
- Буду обозначать типы так, как выглядело бы объявление переменной TYPE соответствующего типа. Например, тип «массив длины 2 int'ов» я буду обозначать как int TYPE[2] ;
- Я буду предполагать, что мы в основном имеем дело с обычными типами данных, такими как int TYPE , int *TYPE и т. д., для которых операции =, &, * и другие не переопределены и обозначают обычные вещи;
- «Объект» всегда будет означать «всё, что не ссылка», а не «экземпляр класса»;
- Везде, за исключением специально оговоренных случаев, подразумеваются C89 и C++98.
Указатели и ссылки
Указатели . Что такое указатели, я рассказывать не буду. 🙂 Будем считать, что вы это знаете. Напомню лишь следующие вещи (все примеры кода предполагаются находящимися внутри какой-нибудь функции, например, main):
int *y = &x; // От любой переменной можно взять адрес при помощи операции взятия адреса "&". Эта операция возвращает указатель
int z = *y; // Указатель можно разыменовать при помощи операции разыменовывания "*". Это операция возвращает тот объект, на который указывает указатель
Также напомню следующее: char — это всегда ровно один байт и во всех стандартах C и C++ sizeof (char) == 1 (но при этом стандарты не гарантируют, что в байте содержится именно 8 бит :)). Далее, если прибавить к указателю на какой-нибудь тип T число, то реальное численное значение этого указателя увеличится на это число, умноженное на sizeof (T) . Т. е. если p имеет тип T *TYPE , то p + 3 эквивалентно (T *)((char *)p + 3 * sizeof (T)) . Аналогичные соображения относятся и к вычитанию.
Ссылки . Теперь по поводу ссылок. Ссылки — это то же самое, что и указатели, но с другим синтаксисом и некоторыми другими важными отличиями, о которых речь пойдёт дальше. Следующий код ничем не отличается от предыдущего, за исключением того, что в нём фигурируют ссылки вместо указателей:
Если слева от знака присваивания стоит ссылка, то нет никакого способа понять, хотим мы присвоить самой ссылке или объекту, на который она ссылается. Поэтому такое присваивание всегда присваивает объекту, а не ссылке. Но это не относится к инициализации ссылки: инициализируется, разумеется, сама ссылка. Поэтому после инициализации ссылки нет никакого способа изменить её саму, т. е. ссылка всегда постоянна (но не её объект).
Lvalue . Те выражения, которым можно присваивать, называются lvalue в C, C++ и многих других языках (это сокращение от «left value», т. е. слева от знака равенства). Остальные выражения называются rvalue. Имена переменных очевидным образом являются lvalue, но не только они. Выражения:
some_struct.some_field, *ptr, *(ptr + 3)
Удивительный факт состоит в том, что ссылки и lvalue — это в каком-то смысле одно и то же. Давайте порассуждаем. Что такое lvalue? Это нечто, чему можно присвоить. Т. е. это некое фиксированное место в памяти, куда можно что-то положить. Т. е. адрес. Т. е. указатель или ссылка (как мы уже знаем, указатели и ссылки — это два синтаксически разных способа в C++ выразить понятие адреса). Причём скорее ссылка, чем указатель, т. к. ссылку можно поместить слева от знака равенства и это будет означать присваивание объекту, на который указывает ссылка. Значит, lvalue — это ссылка.
А что такое ссылка? Это один из синтаксисов для адреса, т. е., опять-таки, чего-то, куда можно класть. И ссылку можно ставить слева от знака равенства. Значит, ссылка — это lvalue.
Окей, но ведь (почти любая) переменная тоже может быть слева от знака равенства. Значит, (такая) переменная — ссылка? Почти. Выражение, представляющее собой переменную — ссылка.
Иными словами, допустим, мы объявили int x . Теперь x — это переменная типа int TYPE и никакого другого. Это int и всё тут. Но если я теперь пишу x + 2 или x = 3 , то в этих выражениях подвыражение x имеет тип int &TYPE . Потому что иначе этот x ничем не отличался бы от, скажем, 10, и ему (как и десятке) нельзя было бы ничего присвоить.
Этот принцип («выражение, являющееся переменной — ссылка») — моя выдумка. Т. е. ни в каком учебнике, стандарте и т. д. я этот принцип не видел. Тем не менее, он многое упрощает и его удобно считать верным. Если бы я реализовывал компилятор, я бы просто считал там переменные в выражениях ссылками, и, вполне возможно, именно так и предполагается в реальных компиляторах.
Более того, удобно считать, что особый тип данных для lvalue (т. е. ссылка) существует даже и в C. Именно так мы и будет дальше предполагать. Просто понятие ссылки нельзя выразить синтаксически в C, ссылку нельзя объявить.
Принцип «любое lvalue — ссылка» — тоже моя выдумка. А вот принцип «любая ссылка — lvalue» — вполне законный, общепризнанный принцип (разумеется, ссылка должна быть ссылкой на изменяемый объект, и этот объект должен допускать присваивание).
Теперь, с учётом наших соглашений, сформулируем строго правила работы со ссылками: если объявлено, скажем, int x , то теперь выражение x имеет тип int &TYPE . Если теперь это выражение (или любое другое выражение типа ссылка) стоит слева от знака равенства, то оно используется именно как ссылка, практически во всех остальных случаях (например, в ситуации x + 2 ) x автоматически конвертируется в тип int TYPE (ещё одной операцией, рядом с которой ссылка не конвертируется в свой объект, является &, как мы увидим далее). Слева от знака равенства может стоять только ссылка. Инициализировать (неконстантную) ссылку может только ссылка.
Операции * и & . Наши соглашения позволяют по-новому взглянуть на операции * и &. Теперь становится понятно следующее: операция * может применяться только к указателю (конкретно это было всегда известно) и она возвращает ссылку на тот же тип. & применяется всегда к ссылке и возвращает указатель того же типа. Таким образом, * и & превращают указатели и ссылки друг в друга. Т. е. по сути они вообще ничего не делают и лишь заменяют сущности одного синтаксиса на сущности другого! Таким образом, & вообще-то не совсем правильно называть операцией взятия адреса: она может быть применена лишь к уже существующему адресу, просто она меняет синтаксическое воплощение этого адреса.
Замечу, что указатели и ссылки объявляются как int *x и int &x . Таким образом, принцип «объявление подсказывает использование» лишний раз подтверждается: объявление указателя напоминает, как превратить его в ссылку, а объявление ссылки — наоборот.
Также замечу, что &*EXPR (здесь EXPR — это произвольное выражение, не обязательно один идентификатор) эквивалентно EXPR всегда, когда имеет смысл (т. е. всегда, когда EXPR — указатель), а *&EXPR тоже эквивалентно EXPR всегда, когда имеет смысл (т. е. когда EXPR — ссылка).
Итак, есть такой тип данных — массив. Определяются массивы, например, так:
Выражение в квадратных скобках должно быть непременно константой времени компиляции в C89 и C++98. При этом в квадратных скобках должно стоять число, пустые квадратные скобки не допускаются.
Подобно тому, как все локальные переменные (напомню, мы предполагаем, что все примеры кода находятся внутри функций) находятся на стеке, массивы тоже находятся на стеке. Т. е. приведённый код привёл к выделению прямо на стеке огромного блока памяти размером 5 * sizeof (int) , в котором целиком размещается наш массив. Не нужно думать, что этот код объявил некий указатель, который указывает на память, размещённую где-то там далеко, в куче. Нет, мы объявили массив, самый настоящий. Здесь, на стеке.
Чему будет равно sizeof (x) ? Разумеется, оно будет равно размеру нашего массива, т. е. 5 * sizeof (int) . Если мы пишем
то, опять-таки, место для массива будет целиком выделяться прямо внутри структуры, и sizeof от этой структуры будет это подтверждать.
От массива можно взять адрес ( &x ), и это будет самый настоящий указатель на то место, где этот массив расположен. Тип у выражения &x , как легко понять, будет int (*TYPE)[5] . В начале массива размещён его нулевой элемент, поэтому адрес самого массива и адрес его нулевого элемента численно совпадают. Т. е. &x и &(x[0]) численно равны (тут я лихо написал выражение &(x[0]) , на самом деле в нём не всё так просто, к этому мы ещё вернёмся). Но эти выражения имеют разный тип — int (*TYPE)[5] и int *TYPE , поэтому сравнить их при помощи == не получится. Но можно применить трюк с void * : следующее выражение будет истинным: (void *)&x == (void *)&(x[0]) .
Хорошо, будем считать, я вас убедил, что массив — это именно массив, а не что-нибудь ещё. Откуда тогда берётся вся эта путаница между указателями и массивами? Дело в том, что имя массива почти при любых операциях преобразуется в указатель на его нулевой элемент.
Итак, мы объявили int x[5] . Если мы теперь пишем x + 0 , то это преобразует наш x (который имел тип int TYPE[5] , или, более точно, int (&TYPE)[5] ) в &(x[0]) , т. е. в указатель на нулевой элемент массива x. Теперь наш x имеет тип int *TYPE .
Конвертирование имени массива в void * или применение к нему == тоже приводит к предварительному преобразованию этого имени в указатель на первый элемент, поэтому:
&x == x // ошибка компиляции, разные типы: int (*TYPE)[5] и int *TYPE
(void *)&x == (void *)x // истина
x == x + 0 // истина
Операция [] . Запись a[b] всегда эквивалентна *(a + b) (напомню, что мы не рассматриваем переопределения operator[] и других операций). Таким образом, запись x[2] означает следующее:
- x[2] эквивалентно *(x + 2)
- x + 2 относится к тем операциям, при которых имя массива преобразуется в указатель на его первый элемент, поэтому это происходит
- Далее, в соответствии с моими объяснениями выше, x + 2 эквивалентно (int *)((char *)x + 2 * sizeof (int)), т. е. x + 2 означает «сдвинуть указатель x на два int'а»
- Наконец, от результата берётся операция разыменования и мы извлекаем тот объект, который размещён по этому сдвинутому указателю
Типы у участвовавших выражений следующие:
x // int (&TYPE)[5], после преобразования типа: int *TYPE
Также замечу, что слева от квадратных скобок необязательно должен стоять именно массив, там может быть любой указатель. Например, можно написать (x + 2)[3] , и это будет эквивалентно x[5] . Ещё замечу, что *a и a[0] всегда эквивалентны, как в случае, когда a — массив, так и когда a — указатель.
Теперь, как я и обещал, я возвращаюсь к &(x[0]) . Теперь ясно, что в этом выражении сперва x преобразуется в указатель, затем к этому указателю в соответствии с вышеприведённым алгоритмом применяется [0] и в результате получается значение типа int &TYPE , и наконец, при помощи & оно преобразуется к типу int *TYPE . Поэтому, объяснять при помощи этого сложного выражения (внутри которого уже выполняется преобразование массива к указателю) немного более простое понятие преобразования массива к указателю — это был немного мухлёж.
А теперь вопрос на засыпку : что такое &x + 1 ? Что ж, &x — это указатель на весь массив целиком, + 1 приводит к шагу на весь этот массив. Т. е. &x + 1 — это (int (*)[5])((char *)&x + sizeof (int [5])) , т. е. (int (*)[5])((char *)&x + 5 * sizeof (int)) (здесь int (*)[5] — это int (*TYPE)[5] ). Итак, &x + 1 численно равно x + 5 , а не x + 1 , как можно было бы подумать. Да, в результате мы указываем на память, которая находится за пределами массива (сразу после последнего элемента), но кого это волнует? Ведь в C всё равно не проверяется выход за границы массива. Также, заметим, что выражение *(&x + 1) == x + 5 истинно. Ещё его можно записать вот так: (&x)[1] == x + 5 . Также будет истинным *((&x)[1]) == x[5] , или, что тоже самое, (&x)[1][0] == x[5] (если мы, конечно, не схватим segmentation fault за попытку обращения за пределы нашей памяти :)).
Массив нельзя передать как аргумент в функцию . Если вы напишите int x[2] или int x[] в заголовке функции, то это будет эквивалентно int *x и в функцию всегда будет передаваться указатель (sizeof от переданной переменной будет таким, как у указателя). При этом размер массива, указанный в заголовке будет игнорироваться. Вы запросто можете указать в заголовке int x[2] и передать туда массив длины 3.
Однако, в C++ существует способ передать в функцию ссылку на массив:
// sizeof (x) здесь равен 5 * sizeof (int)
f (y); // Нельзя, не тот размер
При такой передаче вы всё равно передаёте лишь ссылку, а не массив, т. е. массив не копируется. Но всё же вы получаете несколько отличий по сравнению с обычной передачей указателя. Передаётся ссылка на массив. Вместо неё нельзя передать указатель. Нужно передать именно массив указанного размера. Внутри функции ссылка на массив будет вести себя именно как ссылка на массив, например, у неё будет sizeof как у массива.
И что самое интересное, эту передачу можно использовать так:
// Вычисляет длину массива
Похожим образом реализована функция std::end в C++11 для массивов.
«Указатель на массив» . Строго говоря, «указатель на массив» — это именно указатель на массив и ничто другое. Иными словами:
int (*a)[2]; // Это указатель на массив. Самый настоящий. Он имеет тип int (*TYPE)[2]
int *c = b; // Это не указатель на массив. Это просто указатель. Указатель на первый элемент некоего массива
int *d = new int[4]; // И это не указатель на массив. Это указатель
Однако, иногда под фразой «указатель на массив» неформально понимают указатель на область памяти, в которой размещён массив, даже если тип у этого указателя неподходящий. В соответствии с таким неформальным пониманием c и d (и b + 0 ) — это указатели на массивы.
Многомерные массивы . Если объявлено int x[5][7] , то x — это не массив длины 5 неких указателей, указывающих куда-то далеко. Нет, x теперь — это единый монолитный блок размером 5 x 7, размещённый на стеке. sizeof (x) равен 5 * 7 * sizeof (int) . Элементы располагаются в памяти так: x[0][0] , x[0][1] , x[0][2] , x[0][3] , x[0][4] , x[0][5] , x[0][6] , x[1][0] и так далее. Когда мы пишем x[0][0] , события развиваются так:
x // int (&TYPE)[5][7], после преобразования: int (*TYPE)[7]
x[0] // int (&TYPE)[7], после преобразования: int *TYPE
То же самое относится к **x . Замечу, что в выражениях, скажем, x[0][0] + 3 и **x + 3 в реальности извлечение из памяти происходит только один раз (несмотря на наличие двух звёздочек), в момент преобразования окончательной ссылки типа int &TYPE просто в int TYPE . Т. е. если бы мы взглянули на ассемблерный код, который генерируется из выражения **x + 3 , мы бы в нём увидели, что операция извлечения данных из памяти выполняется там только один раз. **x + 3 можно ещё по-другому записать как *(int *)x + 3 .
А теперь посмотрим на такую ситуацию:
int **y = new int *[5];
for (int i = 0; i != 5; ++i)
Что теперь есть y? y — это указатель на массив (в неформальном смысле!) указателей на массивы (опять-таки, в неформальном смысле). Нигде здесь не появляется единый блок размера 5 x 7, есть 5 блоков размера 7 * sizeof (int) , которые могут находиться далеко друг от друга. Что есть y[0][0] ?
Теперь, когда мы пишем y[0][0] + 3 , извлечение из памяти происходит два раза: извлечение из массива y и последующее извлечение из массива y[0] , который может находиться далеко от массива y. Причина этого в том, что здесь не происходит преобразования имени массива в указатель на его первый элемент, в отличие от примера с многомерным массивом x. Поэтому **y + 3 здесь не эквивалентен *(int *)y + 3 .
Объясню ещё разок. x[2][3] эквивалентно *(*(x + 2) + 3) . И y[2][3] эквивалентно *(*(y + 2) + 3) . Но в первом случае наша задача найти «третий элемент во втором ряду» в едином блоке размера 5 x 7 (разумеется, элементы нумеруются с нуля, поэтому этот третий элемент будет в некотором смысле четвёртым :)). Компилятор вычисляет, что на самом деле нужный элемент находится на 2 * 7 + 3 -м месте в этом блоке и извлекает его. Т. е. x[2][3] здесь эквивалентно ((int *)x)[2 * 7 + 3] , или, что то же самое, *((int *)x + 2 * 7 + 3) . Во втором случае сперва извлекает 2-й элемент в массиве y, а затем 3-й элемент в полученном массиве.
В первом случае, когда мы делаем x + 2 , мы сдвигаемся сразу на 2 * sizeof (int [7]) , т. е. на 2 * 7 * sizeof (int) . Во втором случае, y + 2 — это сдвиг на 2 * sizeof (int *) .
В первом случае (void *)x и (void *)*x (и (void *)&x !) — это один и тот же указатель, во втором — это не так.
- Стандарт языка программирования Си++. Где его найти?
- Как сделать массив объектов, у которых конструктор имеет аргументы
- Борьба с ошибкой линковки "In function . undefined reference to" при сборке с заранее скомпиленными библиотеками
- Создание объектов в цикле
- Про dynamic_cast в сравнении с Pascal
- Проверка кода: какие статические анализаторы существуют?
- Про нужность виртуального деструктора
- Как срабатывают конструкторы, деструкторы и инициализация переменных в C++
- Многомерные динамические массивы
- Как работать с указателями на объект
- Синтаксис объявления указателей на функции в С++ - как построить тип указателя на функцию
- Указатели на функцию в языке Си/Си++
- Как получить адрес функции в языке C++
- Массивы указателей на функцию в языке Си/Си++
- Указатель в языке C++, хорошее объяснение
- Как сделать FastCGI сервер на C/C++
- Концепция объектно-ориентированной парадигмы предельно проста.
- Небольшой логгер стека вызовов для C++
- Руководство новичка по эксплуатации компоновщика в C/C++
- Как препроцессор узнает полный путь к заголовочному файлу библиотеки в GCC
- Лямбда-функции в C++ (стандарт С++11)
- Лямбда-выражения в C++0x (С++11)
- Синтаксис лямбда выражения с языке C++
- Что обозначает декларация const в описании метода
- C++: пример использования const-указателя
- C++: учебные материалы по контейнерным классам и алгоритмам
- Интервью с Бьерном Страуструпом о языке C++
- C++: Ключевое слово explicit. Явные конструкторы
- Модификаторы public, private и protected в C++
- Права доступа при наследовании в C++
- Конструктор копирования в C++: объяснение и пример использования
- Константные методы и константные указатели в C++, краткие примеры
- Книга "Введение в язык Си++", третье издание
- Дружественные функции (методы) в С++
- Дружественные классы в С++
- Еще одна попытка объяснить, что такое ссылка и указатель, и чем они отличаются
- Что такое explicitly shared объекты и implicitly shared и чем они отличаются (краткое объяснение)
- Приведение типов в C++. Терминология, динамический полиморфизм
- Приведение типов в C++
- Указатели, ссылки и массивы в C и C++: точки над i tutorial (подробное и простое объяснение)
- Отличие ссылок от указателей в языке C++
- Как в C++ различать "константный указатель" и "указатель на константу"
- Подходы к разработке встраиваемого ПО на C++: Шаблоны
- Пример перебора std::unordered_map на C++
- Пример передачи #define определений через флаги компилятора в GCC
- Что такое lvalue и rvalue в языке C++
- Что такое l-value и r-value в С++. Простое и короткое объяснение
- Почему строковый литерал в C++ является lvalue?
- Значения Lvalue и Rvalue
- Вопросы и ответы на RSDN. Что это такое lvalue и rvalue?
- Понимание преинкремента и постинкремента в языке C++
- Приоритет операций в языке C++
- Онлайн компиляторы C++
- Как в C++ можно держать основные настройки программы и прочие конфигурирующие данные не в глобальной области видимости
- Порядок инициализации в конструкторах
- Как научиться понимать синтаксис языка C++
- Возврат значений по ссылке, по адресу (указателю) и по значению в C++
- Понимание наследования и механизма виртуальных методов в C++
- Особенности модификатора Const в C++ при работе с указателями и ссылками
- Глава книги "C++. Практика многопоточного программирования" - Разработка конкурентного кода
- Как сделать константное свойство в классе в языке C++
- Примеры работы с байтами через union и через memcpy
- Особенности языка C и C-style кода
- Как скомпилировать C++ программу с минимальным размером бинарника в GCC
- Понимание синтаксиса объявления указателей и массивов
- Техника поиска кода, использующего конструктор копирования или оператор копирования в языке Си++
- Рабочий пример простого парсера математических выражений
- Еще один пример простого парсера математических выражений, с поддержкой именованных констант
- Как узнать, какие директории будут по-умолчанию включены в INCLUDE path при компиляции C/C++ программ в GCC
- Кратко: что делают модификаторы override и final
- Переменные в единицах трансляции: размещение объектов в памяти и время жизни объектов
- Создание шаблонной фабрики объектов в языке C++
- Как бороться с ошибкой компиляции undefined reference to `std::cout'
- Как получить тип переменной в языке C++ в виде строки
- Размеры базовых математических типов int, long в различных ОС в зависимости от битности
- Пример использования constexpr и operator для задания величин с размерностью
- Ключевое слово new, оператор new и их различные формы: стандартный, размещающий, и другие
- Многомерные массивы C++ в динамической памяти через new и delete
- Перегрузка префиксного и постфиксного оператора в C++: фиктивный параметр как самое странное решение разработчиков языка