Тверской государственный университет
Опубликован: 02.12.2009 | Доступ: свободный | Студентов: 3450 / 677 | Оценка: 4.41 / 4.23 | Длительность: 09:18:00
ISBN: 978-5-9963-0259-8
Лекция 6:

Массивы

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

Проект к данной лекции Вы можете скачать здесь.

Общий взгляд

Массив задает способ организации данных. Массивом называют упорядоченную совокупность элементов одного типа. Каждый элемент массива имеет индексы, определяющие порядок элементов. Число индексов характеризует размерность массива. Каждый индекс изменяется в некотором диапазоне [a,b]. В языке C#, как и во многих других языках, индексы задаются целочисленным типом. В других языках, например, в языке Паскаль, индексы могут принадлежать счетному конечному множеству, на котором определены функции, задающие следующий и предыдущий элемент. Диапазон [a,b] называется граничной парой, a - нижней границей, b - верхней границей индекса. При объявлении массива границы задаются выражениями. Если все границы заданы константными выражениями, то число элементов массива известно в момент его объявления и ему может быть выделена память еще на этапе трансляции. Такие массивы называются статическими. Если же выражения, задающие границы, зависят от переменных, то такие массивы называются динамическими, поскольку память им может быть отведена только динамически в процессе выполнения программы, когда становятся известными значения соответствующих переменных. Массиву, как правило, выделяется непрерывная область памяти.

В языке C# снято существенное ограничение языка C++ на статичность массивов. Массивы в языке C# являются динамическими. Как следствие этого, напомню, массивы относятся к ссылочным типам, память им отводится динамически в "куче". К сожалению, не снято ограничение 0-базируемости, означающее, что нижняя граница массивов C# фиксирована и равна нулю. Было бы гораздо удобнее во многих задачах иметь возможность работать с массивами, у которых нижняя граница изменения индекса не равна нулю.

В языке C++ "классических" многомерных массивов нет. Здесь введены одномерные массивы и массивы массивов. Последние являются более общей структурой данных и позволяют задать не только многомерный куб, но и изрезанную, ступенчатую структуру. Однако использование массива массивов менее удобно, и, например, классик и автор языка C++ Бьерн Страуструп в своей книге "Основы языка C++" пишет: "Встроенные массивы являются главным источником ошибок - особенно когда они используются для построения многомерных массивов. Для новичков они также являются главным источником смущения и непонимания. По возможности пользуйтесь шаблонами vector, valarray и т. п.".

Шаблоны, определенные в стандартных библиотеках, конечно, стоит использовать, но все-таки странной является рекомендация не пользоваться структурами, встроенными непосредственно в язык. Замечу, что в других языках массивы являются одной из любимых структур данных, используемых программистами.

В языке C#, соблюдая преемственность, сохранены одномерные массивы и массивы массивов. В дополнение к ним в язык добавлены многомерные массивы. Динамические многомерные массивы языка C# являются весьма мощной, надежной, понятной и удобной структурой данных, которую смело можно рекомендовать к применению не только профессионалам, но и новичкам, программирующим на C#. После этого краткого обзора давайте перейдем к более систематическому изучению деталей работы с массивами в C#.

Объявление массивов

Рассмотрим, как объявляются одномерные массивы, массивы массивов и многомерные массивы.

Объявление одномерных массивов

Напомню общую структуру объявления:

[<атрибуты>] [<модификаторы>] <тип> <объявители>;

Забудем пока об атрибутах и модификаторах. Объявление одномерного массива выглядит следующим образом:

<тип>[] <объявители>;

Заметьте, в отличие от языка C++ квадратные скобки приписаны не к имени переменной, а к типу. Они являются неотъемлемой частью определения типа, так что запись T[] следует понимать как тип, задающий одномерный массив с элементами типа T.

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

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

int[] a, b, c;

Чаще всего при объявлении массива используется имя с инициализацией. И опять-таки, как и в случае простых переменных, могут быть два варианта инициализации. В первом случае инициализация является явной и задается константным массивом. Вот пример:

double[] x= {5.5, 6.6, 7.7};

Следуя синтаксису, элементы константного массива необходимо заключать в фигурные скобки.

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

int[] d= new int[5];

Итак, если массив объявляется без инициализации, то создается только висячая ссылка со значением void. Если инициализация выполняется конструктором, то в динамической памяти создается сам массив, элементы которого инициализируются константами соответствующего типа (ноль для арифметики, пустая строка для строковых массивов), и ссылка связывается с этим массивом. Если массив инициализируется константным массивом, то в памяти создается константный массив, с которым и связывается ссылка.

Как обычно задаются элементы массива, если они не заданы при инициализации? Они либо вычисляются, либо вводятся пользователем. Давайте рассмотрим первый пример работы с массивами из проекта с именем Arrays, поддерживающего эту лекцию:

public void TestDeclaration()
  {
  	//объявляются три одномерных массива A,B,C
  	int[] A = new int[5], B= new int[5], C= new int[5];
  	Arrs.CreateOneDimAr(A);
  	Arrs.CreateOneDimAr(B);
  	for(int i = 0; i<5; i++)
    C[i] = A[i] + B[i];
  	//объявление массива с явной инициализацией
  	int[] x ={5,5,6,6,7,7};
  	//объявление массивов с отложенной инициализацией
  	int[] u,v;
  	u = new int[3];
  	for(int i=0; i<3; i++) u[i] =i+1;
  	// v = {1,2,3}; //присваивание константного массива недопустимо
  	v = new int[4];  	
  	v = u; //допустимое присваивание
  	Arrs.PrintAr1("A", A); Arrs.PrintAr1("B", B);
  	Arrs.PrintAr1("C", C); Arrs.PrintAr1("X", x);
  	Arrs.PrintAr1("U", u); Arrs.PrintAr1("V", v);
  }

На что следует обратить внимание, анализируя этот текст?

  • В процедуре показаны разные способы объявления массивов. Вначале объявляются одномерные массивы A, B и C, создаваемые конструктором. Значения элементов этих трех массивов имеют один и тот же тип int. То, что они имеют одинаковое число элементов, произошло по воле программиста, а не диктовалось требованиями языка. Заметьте, что после такого объявления с инициализацией конструктором все элементы имеют значение, в данном случае - ноль, и могут участвовать в вычислениях.
  • Массив x объявлен с явной инициализацией. Число и значения его элементов определяется константным массивом.
  • Массивы u и v объявлены с отложенной инициализацией. В последующих операторах массив u инициализируется в объектном стиле, его элементы получают в цикле значения.
  • Обратите внимание на закомментированный оператор присваивания. В отличие от инициализации использовать константный массив в правой части оператора присваивания недопустимо. Эта попытка приводит к ошибке, поскольку v - это ссылка, которой можно присвоить ссылку, но нельзя присвоить константный массив. Ссылку присвоить можно. Что происходит в операторе присваивания v = u.? Это корректное ссылочное присваивание: хотя u и v имеют разное число элементов, но они являются объектами одного класса. В результате присваивания память, отведенная массиву v, освободится, ей займется теперь сборщик мусора. Обе ссылки u и v будут теперь указывать на один и тот же массив, так что изменение элемента одного массива немедленно отражается на другом массиве.
  • Для поддержки работы с массивами создан специальный класс Arrs, статические методы которого выполняют различные операции над массивами. В частности, в примере использованы два метода этого класса, один из которых заполняет массив случайными числами, второй - выводит массив на печать.

Вот текст первого из этих методов:

public static void CreateOneDimAr(int[] A)
  {    	
  	for(int i = 0; i<A.GetLength(0);i++)
    A[i] = rnd.Next(1,100);
  }//CreateOneDimAr

Здесь rnd - это статическое поле класса Arrs, объявленное следующим образом:

private  static Random rnd = new Random();

Процедура печати массива с именем name выглядит так:

public static void PrintAr1(string name,int[] A)
  {
  	Console.WriteLine(name);
  	for(int i = 0; i<A.GetLength(0);i++)
    Console.Write("\t" + name + "[{0}]={1}", i, A[i]);
  	Console.WriteLine();  	
  }//PrintAr1

На рис. 6.1 показан консольный вывод результатов работы процедуры TestDeclarations:

Результаты объявления и создания массивов

Рис. 6.1. Результаты объявления и создания массивов

Особое внимание обратите на вывод, связанный с массивами u и v.

Динамические массивы

Во всех вышеприведенных примерах объявлялись статические массивы, поскольку нижняя граница равна нулю по определению, а верхняя всегда задавалась в этих примерах константой. Напомню, что в C# все массивы, независимо от того, каким выражением описывается граница, рассматриваются как динамические и память для них распределяется в "куче". Полагаю, что это отражение разумной точки зрения: ведь статические массивы скорее исключение, а правилом является использование динамических массивов. Действительно реальные потребности в размере массива, скорее всего, выясняются в процессе работы в диалоге с пользователем.

Чисто синтаксически нет существенной разницы в объявлении статических и динамических массивов. Выражение, задающее границу изменения индексов, в динамическом случае содержит переменные. Единственное требование - значения переменных должны быть определены в момент объявления. Это ограничение в C# выполняется, поскольку C# контролирует инициализацию переменных.

Приведу пример, в котором описана работа с динамическим массивом:

public void TestDynAr()
  {
  	//объявление динамического массива A1
  	Console.WriteLine("Введите число элементов массива A1");
  	int size = int.Parse(Console.ReadLine());
  	int[] A1 = new int[size];
  	Arrs.CreateOneDimAr(A1);
  	Arrs.PrintAr1("A1",A1);
  }//TestDynAr

В особых комментариях эта процедура не нуждается. Здесь верхняя граница массива определяется пользователем.

Многомерные массивы

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

Размерность массива это характеристика типа. Как синтаксически при объявлении типа массива указать его размерность? Это делается достаточно просто, за счет использования запятых. Вот как выглядит объявление многомерного массива в общем случае:

<тип>[, … ,] <объявители>;

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

public void TestMultiArr()
  {
  	int[,]matrix = {
          {1,2},
          {3,4}
     };
  	Arrs.PrintAr2("matrix", matrix);
  }//TestMultiArr

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

public void TestMultiMatr()
  {
  	int n1, m1, n2, m2,n3, m3;
  	Arrs.GetSizes("MatrA",out n1,out m1);
  	Arrs.GetSizes("MatrB",out n2,out m2);
  	Arrs.GetSizes("MatrC",out n3,out m3);
  	int[,]MatrA = new int[n1,m1], MatrB = new int[n2,m2];
  	int[,]MatrC = new int[n3,m3];
  	Arrs.CreateTwoDimAr(MatrA); Arrs.CreateTwoDimAr(MatrB);
  	Arrs.MultMatr(MatrA, MatrB, MatrC);
  	Arrs.PrintAr2("MatrA",MatrA); Arrs.PrintAr2("MatrB",MatrB);
  	Arrs.PrintAr2("MatrC",MatrC);  	
  }//TestMultiMatr

Три матрицы MatrA, MatrB и MatrC имеют произвольные размеры, выясняемые в диалоге с пользователем, и использование для их описания динамических массивов представляется совершенно естественным. Метод CreateTwoDimAr заполняет случайными числами элементы матрицы, переданной ему в качестве аргумента, метод PrintAr2 выводит матрицу на печать. Я не буду приводить их код, похожий на код их одномерных аналогов.

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

public void MultMatr(int[,]A, int[,]B, int[,]C)
  {
  	if (A.GetLength(1) != B.GetLength(0))
    Console.WriteLine("MultMatr: ошибка размерности!");
  	else
    for(int i = 0; i < A.GetLength(0); i++)
    	for(int j = 0; j < B.GetLength(1); j++)
    	{
      int s=0;
      for(int k = 0; k < A.GetLength(1); k++)
  s+= A[i,k]*B[k,j];
      C[i,j] = s;
    	}
  }//MultMatr

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

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

Умножение матриц

увеличить изображение
Рис. 6.2. Умножение матриц
Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?

Илья Ардов
Илья Ардов

Добрый день!

Я записан на программу. Куда высылать договор и диплом?