Отвращение к массивам
Во вводных книгах по Си часто утверждается, что указатели более или менее являются массивами. Разве это не огромное упрощение, в лучшем случае?
Существует тип массива в C, и он может вести себя совершенно иначе, чем указатели, например:
#include <stdio.h>
int main(int argc, char *argv[]){
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *b = a;
printf("sizeof(a) = %lu\n", sizeof(a));
printf("sizeof(b) = %lu\n", sizeof(b));
return 0;
}
дает вывод
sizeof(a) = 40
sizeof(b) = 8
или как другой пример a = b
выдаст ошибку компиляции (GCC: "присваивание выражению с типом массива").
Конечно, существует тесная связь между указателями и массивами, в том смысле, что да, содержимое самой переменной массива является адресом памяти первого элемента массива, например int a[10] = {777, 1, 2, 3, 4, 5, 6, 7, 8, 9}; printf("a = %ul\n", a);
печатает адрес, содержащий 777.
Теперь, с одной стороны, если вы "скрываете" массивы в структурах, вы можете легко скопировать большие объемы данных (массивы, если вы игнорируете структуру упаковки), просто используя =
оператор (и это даже быстро):
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ARRAY_LENGTH 100000000
typedef struct {int arr[ARRAY_LENGTH];} struct_huge_array;
int main(int argc, char *argv[]){
struct_huge_array *a = malloc(sizeof(struct_huge_array));
struct_huge_array *b = malloc(sizeof(struct_huge_array));
int *x = malloc(sizeof(int)*ARRAY_LENGTH);
int *y = malloc(sizeof(int)*ARRAY_LENGTH);
struct timeval start, end, diff;
gettimeofday(&start, NULL);
*a = *b;
gettimeofday(&end, NULL);
timersub(&end, &start, &diff);
printf("Copying struct_huge_arrays took %d sec, %d µs\n", diff.tv_sec, diff.tv_usec);
gettimeofday(&start, NULL);
memcpy(x, y, ARRAY_LENGTH*sizeof(int));
gettimeofday(&end, NULL);
timersub(&end, &start, &diff);
printf("memcpy took %d sec, %d µs\n", diff.tv_sec, diff.tv_usec);
return 0;
}
Выход:
Copying struct_huge_arrays took 0 sec, 345581 µs
memcpy took 0 sec, 345912 µs
Но вы не можете сделать это с самими массивами. Для массивов x, y
(того же размера и того же типа) выражение x = y
незаконно
Тогда функции не могут возвращать массивы. Или, если массивы используются в качестве аргументов, C сворачивает их в указатели - ему все равно, задан ли размер явно, поэтому следующая программа выдает результат sizeof(a) = 8
:
#include <stdio.h>
void f(int p[10]){
printf("sizeof(a) = %d\n", sizeof(p));
}
int main(int argc, char *argv[]){
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
f(a);
return 0;
}
Есть ли какая-то логика за этим отвращением к массивам? Почему в Си нет действительно надежного типа массива? Что плохого случилось бы, если бы был один? В конце концов, если массив скрыт в struct
Массив ведет себя так же, как в Go, Rust, ..., т. е. массив - это весь кусок памяти, и его передача скопирует его содержимое, а не только адрес памяти первого элемента. Например, как в Go следующая программа
package main
import "fmt"
func main() {
a := [2]int{-777, 777}
var b [2]int
b = a
b[0] = 666
fmt.Println(a)
fmt.Println(b)
}
дает вывод:
[-777 777]
[666 777]
3 ответа
Эта часть вопроса...
Есть ли какая-то логика за этим отвращением к массивам? Почему в Си нет действительно надежного типа массива? Что плохого случилось бы, если бы был один?
... на самом деле это не вопрос кода и открыт для размышлений, но я думаю, что короткий ответ может быть полезным: когда C создавался, он был нацелен на машины с очень небольшим объемом ОЗУ и медленными процессорами (измеряется в килобайтах и мегагерцах, соответственно). Он должен был заменить Assembler как язык системного программирования, но без дополнительных затрат, необходимых для других существующих языков высокого уровня. По тем же причинам C по-прежнему является популярным языком для микроконтроллеров, поскольку он дает вам контроль над сгенерированной программой.
Введение "надежного" типа массива могло бы снизить производительность и сложность как для компилятора, так и для среды выполнения, что не все системы могли себе позволить. В то же время C предлагает программисту возможность создавать свой собственный "надежный" тип массива и использовать его только в тех ситуациях, когда его использование было оправдано.
Я нашел эту статью интересной в этом контексте: Деннис Ритчи: Развитие языка Си (1993)
Язык C был изначально разработан в начале 1970-х годов на мини-компьютере PDP, который, как сообщается, только что занял половину комнаты, несмотря на его огромную память 24 КБ. (Это КБ, а не МБ или ГБ).
Встраивание компилятора в эту память было настоящей проблемой. Таким образом, язык C был разработан, чтобы позволить вам писать компактные программы, и было добавлено немало специальных операторов (таких как +=, - и?:) для ручной оптимизации.
Добавление функций для копирования больших массивов в качестве параметров не приходило в голову дизайнерам. Это не было бы полезно в любом случае.
В предшественнике C, языке B, массив представлялся как указатель на хранилище, выделенное отдельно (см. Ссылку в ответе Ларса). Ричи хотел избежать этого дополнительного указателя в C и поэтому решил, что имя массива можно превратить в указатель при использовании в местах, не ожидающих массив:
Это исключило материализацию указателя в хранилище и вместо этого вызвало создание указателя, когда имя массива упоминается в выражении. Правило, которое сохраняется в сегодняшнем C, состоит в том, что значения типа массива преобразуются, когда они появляются в выражениях, в указатели на первый из объектов, составляющих массив.
Это изобретение позволило большинству существующих B-кодов продолжать работать, несмотря на лежащие в основе изменения в семантике языка.
А также struct
s не были добавлены к языку до позже. То, что вы можете передавать массив внутри структуры в качестве параметра, было функцией, которая предлагала другую опцию.
Изменение синтаксиса для массивов было уже слишком поздно. Это сломало бы слишком много программ. Там уже были сотни пользователей...
Массивы - это массивы, а указатели - это указатели, они не совпадают.
Но чтобы сделать что-либо полезное из массивов, компилятор должен использовать квалифицированные указатели.
По определению массив - это непрерывная и однородная последовательность элементов в памяти. Пока все хорошо, но как с этим взаимодействовать?
Чтобы объяснить концепцию, которую я уже использовал, на других форумах, пример сборки:
;int myarray[10] would be defined as
_myarray: .resd 10
;now the pointer p (suppose 64 bit machine)
_p: .resq 1
Это код, испускаемый компилятором для резервирования массива из 10 int
и указатель на int
в глобальной памяти.
Теперь, когда вы обращаетесь к массиву, что вы думаете, вы можете получить? Просто адрес конечно (или лучше адрес первого элемента). А адрес какой есть? Стандарт гласит, что он должен называться квалифицированным указателем, но теперь вы действительно можете понять, почему это так.
Теперь посмотрите на указатель, когда мы ссылаемся на него, компилятор выдает код для извлечения содержимого местоположения по адресу p
, но мы можем даже получить p
сам адрес указателя переменной, используя &p
, но мы не можем сделать это с массивом. С помощью &myarray
вернет адрес первого элемента снова.
Это означает, что вы можете назначить myarray
Отправить p
, но не наоборот;-)