Вопрос по языку C++ Что значит эта строчка? int *arr = new int[n];
*arr — «*» указатель, arr — это имя нашего массива, int — тип (integer — числовой), new int[n] — создать в нём кол-во n чисел!
Пример:
int n = 10;
int *arr = new int[n]; // мы создали десять цифр
arr[9] = 132; // отсчёт идёт с нуля, поэтому мы прировняли 10 эл. числу 132!
arr[0] = 3; // Первый элемент массива теперь равен 3
По умолчанию все десять цифр равны нулю!
«*» — это указатель на то что это массив! Массив — это . Массив (в некоторых языках программирования также таблица, ряд) — набор однотипных компонентов (элементов), расположенных в памяти непосредственно друг за другом, доступ к которым осуществляется по индексу (индексам). В отличие от списка, массив является структурой с произвольным доступом.
Источник: Голова
yan_zhelanovПрофи (757) 9 лет назад
По умолчанию все десять цифр равны нулю! «*» — это указатель на то что это массив!
Массив — это .
Массив (в некоторых языках программирования также таблица, ряд) — набор однотипных компонентов (элементов), расположенных в памяти непосредственно друг за другом, доступ к которым осуществляется по индексу (индексам). В отличие от списка, массив является структурой с произвольным доступом.
кирилл кириллУченик (123) 9 лет назад
Спасибо, есть вопрос: вот еще не понятная часть
cin >> arr[i]; //это вводим с клавиатуры, а дальше не понятно
if (arr[i] % 2 == 0)
chet++;
else
neChet++;
yan_zhelanov Профи (757) Переведу на русский: if (arr[i] % 2 == 0) // Если arr[i] с индексом «i», i обычно используют в циклах «for» i может равняться любому числу. Представим что arr[i] = 4! Если 4:2, то будет = 2, если 3 разделить на два, то целого числа не будет! Оператор «%» отвечает за остаток от деления, в твоём случае он должен равняться нулю! То-есть это все чётные числа или числа которые нацело делятся на 2! chet++; // переменой chet прибавить единицу (пример: chet = 2; chet++; // chet будет равен 3) else // Или ещё, если остаток от деления не равен нулю neChet++; // То не чёт + 1! Тоже самое как и вверху!
C — почему &arr не равно arr и как мы получаем размер?
Помогите понять, каким образом у нас arr — это массив(указатель на первый элемент массива). А вот &arr — это указатель на весь целочисленный массив из 6 элементов. Что есть тогда указатель &arr на весь массив при условии, что в size мы получаем размер массива. Как указатель &arr может указывать на весь массив? Пример нашел тут, но понять как тут работает адресная арифметика пока не могу.
Отслеживать
2,279 1 1 золотой знак 7 7 серебряных знаков 27 27 бронзовых знаков
задан 1 апр 2022 в 3:54
771 5 5 серебряных знаков 24 24 бронзовых знака
2 ответа 2
Сортировка: Сброс на вариант по умолчанию
arr имеет тип int [6] — массив из 6 элементов int , это не указатель на первый элемент. Соответственно &arr имеет тип int ( * ) [6] — указатель на массив из 6 элементов int (тот самый «указатель на весь целочисленный массив»). В выражении *(&arr + 1) — arr сначала при сложении происходит адресная арифметика с указателем на массив из 6 элементов int , при этом указатель рассматривается как указывающий на первый элемент массива, элементы которого имеют тип int [6] , далее результат (&arr + 1) разыменовывается и получается int ( & ) [6] — ссылка на массив из 6 элементов int и наконец при вычитании происходит адресная арифметика с двумя массивами из 6 элементов — для этого создаются два временных объекта — указатели на первые элементы этих массивов.
Проверить, что есть что, можно используя std::is_same :
#include int main() < int arr []; static_assert(::std::is_same_v); static_assert(::std::is_same_v); static_assert(::std::is_same_v); static_assert(::std::is_same_v); static_assert(::std::is_same_v); static_assert(::std::is_same_v); return 0; >
Отслеживать
ответ дан 1 апр 2022 в 6:24
user7860670 user7860670
29.7k 3 3 золотых знака 17 17 серебряных знаков 36 36 бронзовых знаков
Проверка азов языка (имею ввиду std::is_same_v) лишнее.
1 апр 2022 в 8:42
int arr[] = ; int size = *(&arr + 1) - arr;
- Массив arr имеет тип int [6] , т.е. массив из шести элементов типа int .
- Выражение &arr имеет тип int (*)[6] , т.е. указатель на (массив из шести элементов типа int ).
- В выражении &arr + 1 к указателю применяется арифметика указателей. Значение указателя наращивается на sizeof(int[6]) байт, что равно sizeof(int) * 6 байт. Получившийся указатель указывает на гипотетический массив из шести элементов типа int , следующий непосредственно за массивом arr .
- В выражении *(&arr + 1) происходит разыменование указателя. Т.е. мы получаем lvalue «ссылающееся» на гипотетический массив после массива arr UB-1) . Тип этого выражения — int[6] 3) .
- В выражении *(&arr + 1) — arr оба операнда бинарного оператора — имеют тип массива — int [6] . И к обоим операндам применяется неявное преобразование из типа массива к типу указателя на первый элемент массива.
Т.е гипотетический массив *(&arr + 1) после массива arr типа int [6] преобразуется к указателю типа int* на свой первый элемент.
Аналогично массив arr преобразуется к указателю на свой первый элемент. - Разность двух указателей типа int* даёт знаковое целочисленное значение типа std::ptrdiff_t, равное количеству объектов типа int между двумя указателями UB-2) .
- Разыменование указателя на гипотетический элемент после массива вызывает неопределённое поведение, так как такой указатель не является указателем на объект.
- a pointer to an object or function (the pointer is said to point to the object or function), or
- a pointer past the end of an object ([expr.add]), or
- the null pointer value for that type, or
- an invalid pointer value.
The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points.
constexpr static int arr[] = ; constexpr std::ptrdiff_t diff = *(&arr + 1) - arr;
может вызвать следующую ошибку компиляции:
//cannot access array element of pointer past the end of object constexpr std::ptrdiff_t diff = *(&arr + 1) - arr; ^
Даже если бы было известно, что по адресу на который указывает указатель на гипотетический элемент после последнего расположен объект, то такой указатель по прежнему нельзя разыменовывать. Код
constexpr static int arr[2][2] = , >; constexpr int val = arr[0][2];
может вызвать следующую ошибку компиляции:
//read of dereferenced one-past-the-end pointer is not allowed in a constant expression constexpr int val = arr[0][2]; ^
- Разность двух указателей, указывающих на элементы разных массивов, вызывает неопределённое поведение.
- If P and Q both evaluate to null pointer values, the result is 0 .
- Otherwise, if P and Q point to, respectively, array elements i and j of the same array object x , the expression P — Q has the value i − j .
- Otherwise, the behavior is undefined.
[Note 1: If the value i − j is not in the range of representable values of type std::ptrdiff_t , the behavior is undefined. — end note]
constexpr static int arr[2][2] = , >; constexpr const int* p00 = &arr[0][0]; constexpr const int* p10 = &arr[1][0]; constexpr std::ptrdiff_t diff = p10 - p00;
//subtracted pointers are not elements of the same array constexpr std::ptrdiff_t diff = p10 - p00; ^
- Результат разыменования указателя на тип T — это lvalue типа T (не ссылка).
The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points. If the type of the expression is “pointer to T ”, the type of the result is “ T ”.
Выражение наподобие std::is_same_v < int ( & ) [6], decltype(*(&arr + 1)) >возвращает true в силу особенностей работы спецификатора decltype , который для lvalue типа T в качестве типа выводит T& .
For an expression E , the type denoted by decltype(E) is defined as follows:
.
otherwise, if E is an lvalue, decltype(E) is T& , where T is the type of E ;
.
Итого: приведённый в вопросе код содержит неопределённое поведение. Результат его выполнения (если вообще скомпилируется) не предсказуем.
Массивы и функции в языке Си. Передача указателя на массив
Массивы, также как остальные переменные, можно передавать в функции в качестве аргументов. Рассмотрим такую программу:
#include #include #include #define N 10 void rand_fill(int arr[], int min, int max); int main() { int numbers[N]; rand_fill(numbers, 30, 90); for (int i = 0; i N; i++) printf("%d ", numbers[i]); printf("\n"); } void rand_fill(int arr[], int min, int max) { srand(time(NULL)); for (int i = 0; i N; i++) arr[i] = rand() % (max - min + 1) + min; }
В теле функции main объявляется массив, состоящий из 10 элементов. Далее вызывается функция rand_fill() , которой передаются в качестве аргументов имя массива и два целых числа.
Если посмотреть на функцию rand_fill , то можно заметить, что ее первый параметр выглядит немного странно. Функция принимает массив неизвестно какого размера. Если предположить, что массивы передаются по значению, т. е. передаются их копии, то как при компиляции будет вычислен необходимый объем памяти для функции rand_fill , если неизвестно какого размера будет один из ее параметров?
На прошлом уроке мы выяснили, что имя массива — это константный указатель на первый элемент массива; т.е. имя массива содержит адрес. Выходит, что мы передаем в функцию копию адреса, а не копию значения. Как мы уже знаем, передача адреса приводит к возможности изменения локальных переменных в вызывающей функции из вызываемой. Ведь на одну и ту же ячейку памяти могут ссылаться множество переменных, и изменение значения в этой ячейке с помощью одной переменной неминуемо отражается на значениях других переменных.
Описание вида arr[] в параметрах функций говорит о том, что в качестве значения мы получаем указатель на массив, а не обычную (скалярную) переменную типа int , char , float и т.п.
Если в функцию передается только адрес массива, то в теле функции никакого массива не существует, и когда там выполняется выражение типа arr[i] , то на самом деле arr ‒ это не имя массива, а переменная-указатель, к которой прибавляется смещение. Поэтому цикл в функции rand_fill можно переписать на такой:
for (int i = 0; i N; i++) *arr++ = rand() % (max - min + 1) + min;
В теле цикла результат выражения справа от знака присваивания записывается по адресу, на который указывает arr . За это отвечает выражение *arr . Затем указатель arr начинает указывать на следующую ячейку памяти, т.к. к нему прибавляется единица ( arr++ ). Еще раз: сначала выполняется выражение записи значения по адресу, который содержится в arr ; после чего изменяется адрес, содержащийся в указателе (сдвигается на одну ячейку памяти определенного размера).
Поскольку мы можем изменять arr , это доказывает, что arr — обычный указатель, а не имя массива. Тогда зачем в заголовке функции такой гламур, как arr[] ? Действительно, чаще используют просто переменную-указатель:
void rand_fill(int *arr, int min, int max);
Хотя в таком случае становится не очевидно, что принимает функция ‒ указатель на обычную переменную или все-таки на массив. В любом случае она будет работать.
Часто при передаче в функцию массива туда же через отдельный аргумент передают и количество его элементов. В примере выше N является глобальной константой, поэтому ее значение доступно как из функции main , так и rand_fill . Иначе, можно было бы определить rand_fill так:
void rand_fill(int *arr, int n, int min, int max) { srand(time(NULL)); for (int i = 0; i n; i++) *arr++ = rand() % (max - min + 1) + min; }
В данном случае параметр n — это количество обрабатываемых элементов массива.
Следует еще раз обратить внимание на то, что при передаче имени массива в функцию, последняя может его изменять. Однако такой эффект не всегда является желательным. Конечно, можно просто не менять значения элементов массива внутри функции, как в данном примере, где вычисляется сумма элементов массива; при этом сами элементы никак не изменяются:
int arr_sum(int *arr) { int s = 0, i; for(i = 0; i N; i++) { s = s + arr[i]; } return s; }
Но если вы хотите написать более надежную программу, в которой большинство функций не должны менять значения элементов массивов, то лучше в заголовках этих функций объявлять параметр-указатель как константу, например:
int arr_sum(const int *arr);
В этом случае, любая попытка изменить значение по адресу, содержащемуся в таком константном указателе, будет приводить к ошибке и программист будет знать, что функция пытается изменить массив.
Усложним программу, которая была приведена в начале этого урока:
#include #include #include #define N 10 void rand_fill(int *arr, int min, int max); void arr_inc_dec(int arr[], char sign); void arr_print(int *arr); int main() { int numbers[N], i, minimum, maximum; char ch; printf("Enter minimum & maximum: "); scanf("%d %d", &minimum, &maximum); rand_fill(numbers, minimum, maximum); arr_print(numbers); scanf("%*c"); // избавляемся от \n printf("Enter sign (+, -): "); scanf("%c", &ch); arr_inc_dec(numbers, ch); arr_print(numbers); } void rand_fill(int *arr, int min, int max) { srand(time(NULL)); for (int i = 0; i N; i++) *arr++ = rand() % (max - min + 1) + min; } void arr_inc_dec(int *arr, char sign) { for (int i = 0; i N; i++) { if (sign == '+') arr[i]++; else if (sign == '-') arr[i]--; } } void arr_print(int *arr) { printf("The array is: "); for (int i = 0; i N; i++) printf("%d ", *arr++); printf("\n"); }
Теперь у пользователя запрашивается минимум и максимум, затем создается массив из элементов, значения которых лежат в указанном диапазоне. Массив выводится на экран с помощью функции arr_print() . Далее у пользователя запрашивается знак + или -. Вызывается функция arr_inc_dec() , которая в зависимости от введенного знака увеличивает или уменьшает на единицу значения элементов массива.
В функциях rand_fill и arr_print используется нотация указателей. Причем в теле функций значения указателей меняются: они указывают сначала на первый элемент массива, затем на второй и т.д. В функции arr_inc_dec используется вид обращения к элементам массива. При этом значение указателя не меняется: к arr прибавляется смещение, которое увеличивается на каждой итерации цикла. Ведь на самом деле запись arr[i] означает *(arr+i) .
При использовании нотации обращения к элементам массива программы получаются более ясные, а при использовании записи с помощью указателей они компилируются чуть быстрее. Это связано с тем, что когда компилятор встречает выражение типа arr[i] , он тратит время на преобразование его к виду *(arr+i) .
Напишите программу, в которой из функции main в другую функцию передаются два массива: «заполненный» и «пустой». В теле этой функции элементам «пустого» массива должны присваиваться значения, так или иначе преобразованные из значений элементов «заполненного» массива, который не должен изменяться.
Курс с решением задач:
pdf-версия
arr[-1] или самые редкие конструкции в С
С — это неоднозначный язык. Сам по себе он небольшой, об этом писал Брайан Керниган в своей знаменитой книге «Язык программирования С». Но это не мешает комбинируя базовые возможности языка составлять более комплексные и запутанные конструкции. Иногда они встречаются в кодах реальных программ довольно редко, из-за своей узкой области применения — о них и пойдёт речь в этой статье.
Структура статьи
Сначала расскажу о базовых и часто встречаемых конструкциях, которые могут и вовсе не удивить опытных программистов на С, но вот уже ниже идут всё более редкие и интересные конструкции, которые уже далеко не часто можно где-либо встретить.
union
Объединения в С встречаются в зависимости от задачи:
union cnt int ival;
float fval;
>;
Это далеко не редкое явление, и эта конструкция вошла в список только по той причине, что по частоте применения она не может конкурировать с подобным ей struct, который использует такой же синтаксис но находит себе место даже в небольших программах гораздо чаще чем объединения.
register
Это ещё одно ключевое слово в С. А вот вы можете вспомнить когда вы создавали регистровую переменную в последний раз? И я нет. Стандарт языка говорит что компилятор может и вовсе проигнорировать это ключевое слово и вместо регистра процессора поместить переменную в оперативную память. К тому же нельзя брать адрес регистровой переменной в независимости от того помещена она в регистр на самом деле, или нет. Из плюсов только то, что к ней вроде как обращение идёт быстрее, так как она находится в регистре, но что-то не особо верится. Думаю понятно, почему это ключевое слово вошло в список — минусов здесь к сожалению больше чем плюсов.
Статические функции
Если объявить автоматическую переменную с ключевым словом static , то переменная в отличии от обычной сохранит своё значение при выходе из функции. Но можно и саму функцию сделать статической:
static char* get_next_password (const password_t stack[], ssize_t *sp) return (sp > 0) ? stack[—*sp].data : NULL;
>
При беглом просмотре можно подумать что функция возвращает static char* , но на деле же она возвращает обычный указатель на строку, а вот статическое объявление говорит о том, что функция видна только в том файле исходного кода, в котором она описана. То есть если вы подключите заголовочный файл с этой функцией к другому, и попытайтесь вызвать её из другого файла, то будет ошибка о том что имя не найдено. Это некий механизм сокрытия имён, только в С. Такой приём часто используется в ядре ОС Linux. Но вот в небольших, а уж тем более одно-файловых программах вы такое вряд-ли найдёте. То же применимо и к статическим глобальным переменным — эффект такой-же как и с функциями. Применение я здесь увидел только одно — хранение 2-х функций с одинаковыми именами в 2-х разных файлах исходного кода.
Константный указатель
Указателем на константу уже никого не удивишь — const char* . Совсем другое дело константный указатель: char * const
Хоть я сам часто пишу на С, но о такой вещи не знал, и встречается она далеко не часто. Для ясности картины:
- char* ptr; — указатель на char
- const char* ptr; — указатель на const char
- char * const ptr; — константный указатель на char
- const char * const ptr; — константный указатель на const char
Конечно никто не запрещает делать так: char const *ptr; , то есть поменять char и const местами. Это также будет указателем на const char.
Препроцессорная склейка строк
Выглядит странно, но следующий фрагмент программы прекрасно компилируется, и запускается:
Оператор \ — это специальный инструмент препроцессора, позволяющий склеить текущую и следующую строку исходного кода перед компиляцией. Зачем это нужно? Например когда слишком длинное условие:
Можно его разделить с помощью знаков \ :
Пример надуманный так как описанный выше код прекрасно работает и без этих символов. Но нельзя просто так переносить слова — они будут восприниматься как два имени, будь-то имена переменных, констант, ключевые слова и т.д. Да и вообще не пишите длинных условий, лучше пишите функции/макросы для таких длинных участков:
> while (access()); /* мм красота */
Также они применяются чтобы красиво оформить макросы
#define macro\
/* code */
Конкатенация строк во время компиляции
Продолжая тематику строк можно ещё вспомнить приём конкатенации строк во время компиляции. То есть если написать две строки рядом, то они склеятся в одну строку.
puts( «Hello, » «world!» ); — склеится в «hello, world!»
А вот это уже будет полезно для работы с длинными строками, чтобы можно было их переносить. Так как просто перенести, не закрыв и/или не открыв двойные кавычки не получится:
const char* ultra_string ; //ошибка
const char* ultra_string = «Lorem»
» ipsum. «; //склеится в «Lorem ipsum. «
Это свойство строк хорошо сочетается с препроцессорным оператором # ,который переводит операнд в строку. Итак можно придумать такой отладочный вывод:
#define dprint(exp)\
printf(#exp » = %i\n», (int)(exp));
При вызове int n = 10; dprint(n/5); будет следующая последовательность операций препроцессора:
- printf(#exp » = %i\n», (int)(exp)); //исходная строка
- printf(#(n/5) » = %i\n», (int)(n/5)); //вставка аргумента
- printf(«n/5″ » = %i\n», (int)(n/5)); //конвертирование в строку
- printf(«n/5 = %i\n», (int)(n/5)); //конкатенация строк
Вот простой пример применения, который был описан в книге Брайана Кернигана.
#undef
На самом деле применений у этой препроцессорной директивы не так уж и много. Разве что она может ограничить действие имени определённого через #define . Или же ставить его перед функциями так:
#undef to_str /* на всякий «пожарный» */
char* to_str (long val);
Но всё же я не думаю, что это сильно полезно, особенно когда программист уверен что такого макроса не существует.
sizeof без ()
И действительно, так как sizeof не является функцией, то по идеи как и в случае с return можно писать выражение, размер которого мы вычисляем без скобок. Но не всегда — стандарт говорит что скобки необходимы в случае с вычислением размера типа. А когда вычисляется размер выражения — скобки вообще не обязательны:
int *dyn_arr = malloc( sizeof *dyn_arr * A_SIZE);
Создание функции в стиле K&R
Вообще в стандарте C2X больше не позволительно объявлять функции так:
size_t move(a,b)
size_t a, b;
a = a ^ b;
b = a ^ b;
a = a ^ b;
>
К сожалению или к счастью практически все современные программы на С которые написаны на С11 и выше уже используют привычную всем конструкцию: тип аргумент, тип аргумент.
Указатели на функции
И всё, что вообще с ними связано. Я думаю они не сильно полезны, максимум как аргумент для другой функции, к примеру qsort() из заголовочного файла использует int (*compare)(const void*, const void*) в качестве параметра. Конечно говорить что они совсем не нужны категорически нельзя. Здесь всё зависит от задачи. Ещё они позволяют чуть лучше понять С и как он работает. Кстати в указатель на функцию можно положить как адрес функции, так и имя функции, так как
f = &f , напоминает ситуацию со статическими массивами: m = &m . Бывают полезны указатели на функции и в случае с массивом из функций, обращение к которому выглядит весьма необычно: (void)funcs[idx](param1,param2);
void parse_line(const char* input, rtable_line* line) sscanf(«%zu %zu %c\n», line->adress, line->hist_cnt, line->state);
>
.
void (*parse_func)(const char*, rtable_line*) = &parse_line; // 1 способ
parse_func = parse_line; //2 способ
parse_func(_input, _line); //вызов по указателю
В приведённом примере выше указать на функцию parse_line можно двумя способами: явно указав амперсанд, или не указывая его вовсе. 2 способа эквивалентны.
Указатели на указатели
на указатели на указатели на указатели.
То есть:
int *p; /* всё нормально — может быть переменная, а может быть массив */
int **p; /* это всё ещё переменная, а может быть двумерный массив. А может надо изменить указатель через его адрес */
int ***p; /* что-то много звёздочек. Трёх-мерный массив? Адрес адреса указателя? Переменная? */
int ****p; /* если вы пишите что-то такое не комментируя — обратитесь к врачу */
Как по мне такая конструкция может встретится на практике в таком виде:
void free_and_setto_NULL (void** ptr) free(*ptr);
*ptr = NULL;
>
Но что-то я сомневаюсь что в таком случае нельзя обойтись макросом, или вовсе сделать все эти операции на месте вызова.
Функции с переменным количеством параметров
Как часто вы видите подобное?
#include
void print_strings (size_t count, . ) va_list cstring;
va_start(cstring, count);
while (count > 0) puts(va_arg(cstring, char*));
—count;
>
va_end(cstring);
>
Можно было например передать массив строк, тогда код получился бы короче:
void print_strings (size_t count, const char* strings[]) while (count > 0)
puts(*strings++), —count;
>
Ну да, вызывать будет посложнее: print_strings( 2, (const char*[])); , но такой способ как минимум уменьшает объём кода в 3 раза. Но зато объявление функций с переменным количеством параметров выглядят красиво.
void print_strings( size_t, . ); /* красота */
Стоит упомянуть, что в C функция, объявленная без параметров принимает произвольное число аргументов. Если вы не собирайтесь передавать в функцию никаких параметров, то при объявлении функции хорошей практикой будет написать void на месте параметров. В таком случае при передаче чего-либо в функцию компилятор не проигнорирует такой вызов в отличии от () на месте аргументов.
Массив + индекс (два в одном)
Редко (очень редко) где, я встречаю подобные выражения:
В самом деле, немногие начинающие программисты знают что можно объявить массив/строку и взять элемент из неё. Наверно с первого взгляда покажется, что данная комбинация вообще не имеет смысла — зачем брать целый массив только ради получения одного элемента из него? Однако данная конструкция находит себе место под солнцем, например в рисовании символов по яркости:
Здесь очень удобно проиллюстрирована возможность не создавать отдельный массив для символов яркости. Или:
Вот так можно обойти создание массива строк, хотя в таких ситуациях более понятным и читабельным всё же будет тернарный оператор ?:
Индекс с массивом, но вверх ногами
int arr[ARR_SIZE], i = ARR_SIZE-1;
while (i>=0)
Здесь может показаться что будет ошибка в выражении i—[arr] , так как индекс и имя массива перепутаны местами. Но всё прекрасно работает — язык С, это язык возможностей. Вот незадача — оно работает, но как? При первом беглом взгляде я подумал об адресах. Быть может если arr представить как адрес, то тогда всё сходится, так как это целое число а индекс должен быть целым числом. Но меня ждал сюрприз во время разбора индекса, я не понял как из i— можно взять к примеру элемент под 3 индексом? 3 бит числа? Но потом прочитав объяснение всё сразу стало понятно.
Когда компилятор встречает выражение на подобие arr[i—] , то он представляет его как *(arr + i—). Иными словами он прибавляет к адресу первой (нулевой) ячейки массива индекс в квадратных скобках, а потом по полученному адресу достаёт значение. А что будет если поменять местами индекс и массив? i—[arr] оттранслируется как *(i— + arr) . От перемены мест слагаемых сумма не меняется, следовательно и итоговый адрес тоже. Если ранее упомянутые конструкции находили себе хоть какое-то применение, то это просто забавный способ запутать программу для человека, который пока что не прочёл эту статью.
strtok(), tmpfile()
Те самые, одни из самых редких функций библиотеки С. char* strtok(s, d) разбивает строку s на токены. При первом вызове надо передавать функции параметр s, а вот уже далее надо передавать NULL , тогда функция вернёт следующий токен из той же строки. d — это строка делителей. То есть те символы, которые разбивают строку на токены. Возвращает указатель на следующий токен.
char str[50] = «Enjoy C programming!»;
char* token = strtok(str, » . «); //токены разделяются одним из символов d
do printf(«token — \’%s\’\n», token);
token = strtok(NULL, » . «);
> while (token);
Такая функция может быть полезна для различного рода парсеров. Хотя для парсинга простых шаблонных строк больше подходит sscanf() . + ко всему функция является небезопасной, так как редактирует исходную строку s, которую она принимает на вход.
А tmpfile() просто возвращает указатель на промежуточный файл (или поток). В большинстве случаев достаточно просто массива. Если посмотреть использование функции в интернете на разных обучающих сайтах, то вы не найдёте ничего кроме заголовка FILE* tmpfile(void) ,и демонстрации открытия и закрытия этого файла.
Возврат указателя на функцию из функции
int (*foo(void))(size_t, size_t);
Страшное объявление, но это всего лишь функция, возвращающая указатель на функцию, которая имеет сигнатуру int f(size_t,size_t) . Хорошей практикой будет избегать таких объявлений, используя typedef:
typedef int (*funcptr)(size_t, size_t);
funcptr foo(void); //и всё понятно
Встречается такой стиль гораздо реже, но к нему надо стремиться. Ведь цель написать и быстрый и понятный код, а не только быстрый. (К программистам на Assemler не относится понятный, можете не волноваться по поводу читабельности). Да и вообще причиной ненависти к С у многих людей являются именно сложные объявления, а особенно те, в которых используются функции.
volatile
Иногда компилятор может оптимизируя удалять целые куски кода. Чтобы такого не было желательно ставить ключевое слово volatile перед объявлением переменной, если вы хотите чтобы компилятор 100% не применил свои коварные оптимизации к этой переменной. (Хорошо что слово volatile не может игнорироваться, как иногда бывает с register , а то в этой жизни пришлось бы больше ни в чём не быть уверенным)
Макросы с переменным количеством параметров
Вы думали всё заканчивается на функциях? К удивлению для меня в некоторых стандартах (а в частности и С11) можно делать макросы с переменным количеством аргументов, точно так же как и функции
А как к параметрам-то обратиться? Всё просто — тут вам никаких . Всего лишь в тексте для замены макросом нужно прописать __VA_ARGS__ . Может показаться странным, но если вы подумайте как работает препроцессор — это просто обработчик текста, и он не может думать о том, как работать с этими аргументами и их типами. Точно также как и в макросы с фиксированным числом аргументов препроцессор С вставляет их на их места, так и здесь. Отличие лишь в том что в случае с переменным количеством параметров на место вставится не один, а столько сколько было указано в круглых скобках при макро-вызове аргументов.
#define myprint(str, . )\
printf(str, __VA_ARGS__);
Также такой способ даёт почву для размышлений о том какие розыгрыши можно выдумать с таким инструментом:
#define for(. ) //обезвредили все циклы for
auto
Не знаю зачем, может в ранних версиях языка без этого слова было никак не обойтись. В современных реалиях int x; объявленная внутри функции и так подразумевается автоматической, несмотря на то что здесь отсутствует явное auto int x; На моей теме VScode оно даже не подсвечивается как ключевое слово. Хотя в C++ оно активно используется в случае, когда программист абсолютно не переживает о том какой тип имеет переменная:
for (auto idx = 0; idx < 10; ++idx)
Использование возвращаемых значений scanf() и printf()
Что может быть абсурднее? Кто-то может первый раз слышать о том, что printf() из стандартной библиотеки возвращает кол-во напечатанных байтов на экран. Его братья sprintf() , vprintf() , fprintf() делают тоже самое. Соответственно scanf() возвращает кол-во считанных байт. Один раз видел даже такой код:
assert (printf(«Some str») == strlen(«Some str»));
Предохраниться лишний раз никогда не бывает лишним.
Каламбур типизации
Одно из моих любимых. Если знаешь как устроена память — то власть в твоих руках. Каламбур типизации это в большинстве случаев небезопасные / неопределённые стандартом / платформо-зависимые и опасные решения, основывающиеся на знании устройства памяти, и использующих инструменты языка. Объяснить мощь каламбура можно на следующем простом примере
Задача: дано шестнадцатеричное число uint64_t x = 0x2E6D6172676F7270;
В каждом байте числа записан символ. Вывести число как строку символов.
Первое что приходит в голову — извлекать текущий байт, отображать его на экране как символ, затем сдвигать число направо на один байт пока число не станет нулём.
Такая запись вполне работает, и выводит каждый байт числа как символ. Но всегда есть альтернативные пути решения такой-же задачи, но заметно короче. «Короче» не всегда значит лучше!
В один вызов printf уложилась задача, ранее решаемая с помощью цикла. Но как?
Дело в том что память в компьютере хранится в ячейках, и каждая ячейка имеет свой размер в байтах. Если представить x не как одно большое 8-байтное число как массив байтов (символов), то получим вот это:
10111001 10110101 10000101 11001001 10011101 10111101 11001000 00000000> x
Далее мы берём адрес первого байта этого числа:
10111001 10110101 10000101 11001001 10011101 10111101 11001000 00000000
^
&x
И далее мы преобразуем адрес из указателя на число в указатель на строку приведением типов:
10111001 10110101 10000101 11001001 10011101 10111101 11001000 00000000 >
^
(char*)&x
И вот мы и получили строку, так как знаем что uint64_t это просто массив из 8 байт, и с помощью изменения адреса мы изменили представление памяти этого конкретного куска для компилятора, которому принципиально не важно что именно находится по адресу — надо лишь чтобы размер соответствовал. Формат %.8s неспроста имеет цифру 8. В ином случае мы рискуем вывести всё что лежит в памяти, пока не встретим 0. Поэтому поставим ограничение на 8 символов.
Почему это плохо?
Многие люди задаются этим вопросом. Это ведь сокращает код! Может показаться что это плохо из-за того что это не предусмотрено стандартом языка или из-за такого что такой код тяжёлый для чтения. Однако проблема совсем в другом. Вот пример базовой функции, возвращающей компонент структуры:
struct point int x, y;
>;
int get_y(const struct point* p)< return p->y; >
Выражение p->y здесь уж слишком банальное. Давайте усложним его
Здесь сначала берётся адрес переменной p->x. Затем зная что переменные в структуре идут в памяти по порядку мы можем сдвинутся на 4 байта вправо и получить адрес переменной p->y . Это производится с помощью приведения типа указателя на символ (байт), сдвижке его на 4 ячейки вправо. Теперь полученный адрес обязательно нужно привести к указателю на целую ячейку, и перейти по нему. Такой пример в отличии от предыдущего демонстрирует не сокращение объёма кода, а наоборот его увеличение, а что ещё хуже — увеличению количества операций которое нужно выполнить. Из этих примеров вывод такой — каламбур типизации в реальных программах это плохо потому, что
- Размер ячеек на каждой платформе может отличаться
- Разные компиляторы могут по разному хранить переменные в памяти
Оба этих фактора могут обеспечить неправильное или неопределённое поведение программы.
Отрицательные индексы
Изюминка этого чарта. arr[-1] — вполне себе привычная запись для программиста на Pascal. Всё потому что в этом языке при создании массива указывается не только правая, но ещё и левая граница массива:
var arr : array[-100..100] of integer;
Но С — язык другой. Каждый программист наизусть знает что индексы массивов здесь начинаются с нуля как и в любых других С-подобных языках. Тем не менее не забывайте, что оператор [] применим не только к массивам но и у указателям.
int real_array[100], *arr = real_array+3;
arr[-1] = 3;
arr[-2] = 65;
arr[-3] = 3278;
printf(«arr[-1] = %d, arr[-2] = %d, arr[-3] = %d\n», arr[-1], arr[-2], arr[-3]);
Если вы всё равно не поняли как это работает, то вот то как компилятор представил запись arr[-1] :
Или же эквивалент ей:
Такого вы точно скорее всего не видели в уже готовых работающих проектах на языке С.
Выводы
Язык С намного больше, обширнее и глубже чем кажется — необходимо как минимум несколько лет практики и опыта чтобы знать большинство возможностей этого инструмента. Хоть язык и имеет множество конструкций которые редко применяются, всё же они как никакие другие нужны в некоторых задачах. Я постарался дать примеры использования практически всех конструкций из этого списка. Надеюсь вы узнали что-то новое для себя из этой статьи, или хотя бы вспомнили хорошо забытое старое. Всем желаю чтобы ваши индексы находились в границах массивов, а malloc() никогда не возвращал NULL .
- C
- низкоуровневое программирование
- тонкости программирования
- программирование