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


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

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



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

Синтаксис остаётся прежним, добавляется только новая размерность

 

<тип> <имя>[размерность1][размерность2]...;

 

Например, двумерный массив

 

int a[2][3];

 

Трёхмерный массив

 

int a[3][4][5];

 

Доступ до элементов массива осуществляется также, как и в одномерном массиве.

 

#include <conio.h>

#include <stdio.h>

 

#define SIZE 5

 

void main() {

int M[3][3];

unsigned i, j;

 

for (i = 0; i < 3; i++) {

for (j = 0; j < 3; j++) {

M[i][j] = i * j;

}

}

 

do {

printf("enter indexes:\n");

scanf("%d", &i);

scanf("%d", &j);

if (i < 3 && j < 3) {

printf("M[%d][%d] == %d\n", i, j, M[i][j]);

} else {

break;

}

} while (1);

 

for (i = 0; i < 3; i++) {

for (j = 0; j < 3; j++) {

printf("\t%d", M[i][j]);

}

printf("\n");

}

getch();

}

 

Особенностью является то, что по своему строению многомерный массив является обыкновенным, "одномерным", массивом. Все элементы расположены друг за другом. Доступ до элемента a[i][j] – по существу сдвиг на i*число столбцов + j. В двумерном массиве, таким образом, элементы расположены "по рядам", в трёхмерном - "по слоям", внутри которых элементы расположены "по рядам" и т.д.

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

 

int a[][3] = {0};

int b[][5][25] = {0};

 

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

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

 

int a[2][3] = {1, 2, 3, 4, 5, 6};

 

Можно опустить первую размерность

 

int a[][2][3] = {1, 2, 3, 34, 5, 6, 7, 8, 9, 10, 11, 12};

 

Можно с помощью фигурных скобок сделать данные более удобными для чтения

 

int a[2][3] = {{1, 2, 3}, {4, 5, 6}}

 

или

 

int a[][3][4] =

{{{1, 2, 3, 4}, {2, 4, 6, 8}, {3, 6, 9, 12}},

{{1, 2, 3, 4}, {2, 4, 6, 8}, {3, 6, 9, 12}},

{{1, 2, 3, 4}, {2, 4, 6, 8}, {3, 6, 9, 12}}};

 

Также, как и в одномерных массивах, если заявлено данных больше, чем указано при инициализации, то оставшиеся заполняются нулями. Например, единичная матрица 3 на 3

 

int zero3[3][3] = {{1}, {0, 1}, {0, 0, 1}};

 

Из того, что многомерный массив является одномерным по структуре, вытекают некоторые интересные свойства. Например, доступ до элемента может быть осуществлён через его порядковый номер

 

a[i][j] === a[0][i*число столбцов + j] и т.д.

 

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

Для сортировки обычно используется два подхода - превращение двумерного массива в одномерный, сортировка, обратно превращение одномерного в двумерный, либо запутанное обращение к элементам через индекс. Можно сделать всё проще: работать с многомерным массивом как с одномерным.

 

#include <conio.h>

#include <stdio.h>

 

#define ROWS 4

#define COLS 3

 

void main() {

int a[ROWS][COLS] =

{{1, 4, 5},

{2, 6, 8},

{1, 0, 9},

{4, 2, 8}};

int i, j, tmp, flag;

 

do {

flag = 0;

for (i = 1; i < ROWS*COLS; i++) {

if (a[0][i] < a[0][i-1]) {

tmp = a[0][i];

a[0][i] = a[0][i-1];

a[0][i-1] = tmp;

flag = 1;

}

}

} while(flag);

 

for (i = 0; i < ROWS; i++) {

for (j = 0; j < COLS; j++) {

printf("%3d", a[i][j]);

}

printf("\n");

}

 

_getch();

}

 

Замечание: по стандарту явно такое поведение не определено, но косвенно должно поддерживаться.


Тема №7

Строки в си. Введение

Это вводная статья по строкам в си. Более подробное описание и примеры будут, когда мы научимся работать с памятью и указателями. В компьютере все значения хранятся в виде чисел. И строки тоже, там нет никаких символов и букв. Срока представляет собой массив чисел. Каждое число соответствует определённому символу, который берётся из таблицы кодировки. При выводе на экран символ отображается определённым образом.

Для хранения строк используются массивы типа char. Ещё раз повторюсь – тип char – числовой, он хранит один байт данных. Но в соответствии с таблицей кодировки каждое из этих чисел связано с символом. И в обратную сторону – каждый символ определяется своим порядковым номером в таблице кодировки.

Например.

 

#include <conio.h>

#include <stdio.h>

 

void main() {

char c = 'A';

int i = 65;

printf("display as char %c\n", c);

printf("display as int %d\n", c);

printf("display as char %c\n", i);

printf("display as char %d\n", i);

getch();

}

 

Мы создали две переменные, одна типа char, другая int. Литера 'A' имеет числовое значение 65. Это именно литера, а не строка, поэтому окружена одинарными кавычками. Мы можем вывести её на печать как букву

 

printf("display as char %c\n", c);

 

Тогда будет выведено

 

A

 

Если вывести её как число, то будет

 

 

Точно также можно поступить и с числом 65, которое хранится в переменной типа int.

Спецсимволы также имеют свой номер.

 

#include <conio.h>

#include <stdio.h>

 

void main() {

printf("%c", '\a');

printf("%d", '\a');

printf("%c", 7);

getch();

}

 

Здесь будет сначала "выведен" звуковой сигнал, затем его числовое значение, затем опять звуковой сигнал.

Строка в си – это массив типа char, последний элемент которого хранит терминальный символ '\0'. Числовое значение этого символа 0, поэтому можно говорить, что массив оканчивается нулём.

Например.

 

#include <conio.h>

#include <stdio.h>

 

void main() {

char word[10];

word[0] = 'A';

word[1] = 'B';

word[2] = 'C';

word[3] = '\0';

//word[3] = 0; эквивалентно

printf("%s", word);

getch();

}

 

Для вывода использовался ключ %s. При этом строка выводится до первого терминального символа, потому что функция printf не знает размер массива word.

Если в этом примере не поставить.

 

word[3] = '\0';

 

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

 

#include <conio.h>

#include <stdio.h>

 

void main() {

char word[10] = "ABC";

char text[100] = {'H', 'E', 'L', 'L', 'O'};

printf("%s\n", word);

printf("%s", text);

getch();

}

 

В данном случае всё корректно. Строка "ABC" заканчивается нулём, и ею мы инициализируем массив word. Строка text инициализируется побуквенно, все оставшиеся символы, как следует из главы про массивы, заполняются нулями.

Чтение строк

Для того, чтобы запросить у пользователя строку, необходимо создать буфер. Размер буфера должен быть выбран заранее, так, чтобы введённое слово в нём поместилось. При считывании строк есть опасность того, что пользователь введёт данных больше, чем позволяет буфер. Эти данные будут считаны и помещены в память, и затрут собой чужие значения. Таким образом можно провести атаку, записав нужные байты, в которых, к примеру, стоит переход на участок кода с вредоносной программой, или логгирование данных.

 

#include <conio.h>

#include <stdio.h>

 

void main() {

char buffer[20];

 

scanf("%19s", buffer);

printf("%s", buffer);

 

getch();

}

 

В данном случае количество введённых символов ограничено 19, а размер буфера на 1 больше, так как необходимо хранить терминальный символ. Напишем простую программу, которая запрашивает у пользователя строку и возвращает её длину.

 

#include <conio.h>

#include <stdio.h>

 

void main() {

char buffer[128];

unsigned len = 0;

 

scanf("%127s", buffer);

 

while (buffer[len]!= '\0') {

len++;

}

 

printf("length(%s) == %d", buffer, len);

 

getch();

}

 

Так как числовое значение символа '\0' равно нулю, то можно записать

 

while (buffer[len]!= 0) {

len++;

}

 

Или, ещё короче

 

while (buffer[len]) {

len++;

}

 

Теперь напишем программу, которая запрашивает у пользователя два слова и сравнивает их.

 

#include <conio.h>

#include <stdio.h>

 

/*

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

0 если слова равны

1 если первое слово больше второго в лексикографическом порядке

2 если второе слово больше

*/

 

void main() {

char firstWord[128]; //Первое слово

char secondWord[128]; //Второе слово

unsigned i; //Счётчик

int cmpResult = 0; //Результат сравнения

 

scanf("%127s", firstWord);

scanf("%127s", secondWord);

 

for (i = 0; i < 128; i++) {

if (firstWord[i] > secondWord[i]) {

//Больше даже если второе слово уже закончилось, потому что

//тогда оно заканчивается нулём

cmpResult = 1;

break;

} else if (firstWord[i] < secondWord[i]) {

cmpResult = -1;

break;

}

}

 

printf("%d", cmpResult);

 

getch();

}

 

Так как каждая буква имеет числовое значение, то их можно сравнивать между собой как числа. Кроме того, обычно (но не всегда!) буквы в таблицах кодировок расположены по алфавиту. Поэтому сортировка по числовому значению также будет и сортировкой по алфавиту.

Указатели

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

Определение

Указатель – это переменная, которая хранит адрес области памяти. Указатель, как и переменная, имеет тип. Синтаксис определения указателей

 

<тип> *<имя>;

 

Например

 

float *a;

long long *b;

 

Два основных оператора для работы с указателями – это оператор & взятия адреса, и оператор * разыменования. Рассмотрим простой пример.

 

#include <conio.h>

#include <stdio.h>

 

void main() {

int A = 100;

int *p;

 

//Получаем адрес переменной A

p = &A;

 

//Выводим адрес переменной A

printf("%p\n", p);

 

//Выводим содержимое переменной A

printf("%d\n", *p);

 

//Меняем содержимое переменной A

*p = 200;

 

printf("%d\n", A);

printf("%d", *p);

 

getch();

}

 

Рассмотрим код внимательно, ещё раз

 

int A = 100;

 

Была объявлена переменная с именем A. Она располагается по какому-то адресу в памяти. По этому адресу хранится значение 100.

 

int *p;

 

Создали указатель типа int.

 

p = &A;

 

Теперь переменная p хранит адрес переменной A. Используя оператор * мы получаем доступ до содержимого переменной A.

Чтобы изменить содержимое, пишем

 

*p = 200;

 

После этого значение A также изменено, так как она указывает на ту же область памяти. Ничего сложного.

Теперь другой важный пример.

 

#include <conio.h>

#include <stdio.h>

 

void main() {

int A = 100;

int *a = &A;

double B = 2.3;

double *b = &B;

 

printf("%d\n", sizeof(A));

printf("%d\n", sizeof(a));

printf("%d\n", sizeof(B));

printf("%d\n", sizeof(b));

 

getch();

}

 

Будет выведено

 

 

Несмотря на то, что переменные имеют разный тип и размер, указатели на них имеют один размер. Действительно, если указатели хранят адреса, то они должны быть целочисленного типа. Так и есть, указатель сам по себе хранится в переменной типа size_t (а также ptrdiff_t), это тип, который ведёт себя как целочисленный, однако его размер зависит от разрядности системы. В большинстве случаев разницы между ними нет. Зачем тогда указателю нужен тип?

Арифметика указателей

· Во-первых, указателю нужен тип для того, чтобы корректно работала операция разыменования (получения содержимого по адресу). Если указатель хранит адрес переменной, необходимо знать, сколько байт нужно взять, начиная от этого адреса, чтобы получить всю переменную.

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

 

операция + N сдвигает указатель вперёд на N*sizeof(тип) байт.

 

Например, если указатель int *p; хранит адрес CC02, то после p += 10; он будет хранить адрес СС02 + sizeof(int)*10 = CC02 + 28 = CC2A (Все операции выполняются в шестнадцатиричном формате). Пусть мы создали указатель на начало массива. После этого мы можем "двигаться" по этому массиву, получая доступ до отдельных элементов.

 

#include <conio.h>

#include <stdio.h>

 

void main() {

int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

int *p;

 

p = A;

 

printf("%d\n", *p);

p++;

printf("%d\n", *p);

p = p + 4;

printf("%d\n", *p);

 

getch();

}

 

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

 

p = A;

 

Массив, по сути, сам является указателем, поэтому не нужно использовать оператор &. Мы можем переписать пример по-другому

 

p = &A[0];

 

Получить адрес первого элемента и относительно него двигаться по массиву.

Кроме операторов + и - указатели поддерживают операции сравнения. Если у нас есть два указателя a и b, то a > b, если адрес, который хранит a, больше адреса, который хранит b.

 

#include <conio.h>

#include <stdio.h>

 

void main() {

int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

int *a, *b;

 

a = &A[0];

b = &A[9];

 

printf("&A[0] == %p\n", a);

printf("&A[9] == %p\n", b);

 

if (a < b) {

printf("a < b");

} else {

printf("b > a");

}

 

getch();

}

 

Если же указатели равны, то они указывают на одну и ту же область памяти.

 


Указатель на указатель

Указатель хранит адрес области памяти. Можно создать указатель на указатель, тогда он будет хранить адрес указателя и сможет обращаться к его содержимому. Указатель на указатель определяется как

 

<тип> **<имя>;

 

Очевидно, ничто не мешает создать и указатель на указатель на указатель, и указатель на указатель на указатель на указатель и так далее. Это нам понадобится при работе с двумерными и многомерными массивами. А вот простой пример, как можно работать с указателем на указатель.

 

#include <conio.h>

#include <stdio.h>

 

#define SIZE 10

 

void main() {

int A;

int B;

int *p;

int **pp;

 

A = 10;

B = 111;

p = &A;

pp = &p;

 

printf("A = %d\n", A);

*p = 20;

printf("A = %d\n", A);

*(*pp) = 30; //здесь скобки можно не писать

printf("A = %d\n", A);

 

*pp = &B;

printf("B = %d\n", *p);

**pp = 333;

printf("B = %d", B);

 

getch();

}

 



Поделиться:


Последнее изменение этой страницы: 2017-01-20; просмотров: 417; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.143.168.172 (0.133 с.)