10«В» — Комментарии к решениям самостоятельной работы №1.
Ошибки
Не используйте неинициализированную память!
Такие ошибки очень тяжело найти —
программа иногда работает, иногда нет.
В следующем тексте первый цикл ничего не записывает в последний элемент массива.
А во втором цикле все элементы массива (в том числе и последний) проверяются
на некоторое условие!
int i,j,boo=0;
int t[m];
for(i=0;i<m-1;i++){
t[i]=i+2;
}
for(i=0;i<m;i++){
for(j=0;j<i;j++){
if(t[i]%t[j]==0) boo=1;
...
}
...
}
|
|
Пишите правильную букву (тип в спецификаторе формата) в scanf и printf!
Например, для unsigned требуется %u.
Если пользователь следующей программы введёт -1, он получит очень интересный результат…
unsigned int m;
scanf ("%d", &m);
...
|
|
Задавайте правильное число параметров правильного типа для функций scanf и printf!
- Лишние параметры задавать не стоит.
- Мало параметров задавать нельзя!
Иначе ваша программа почти наверняка станет "дырой" в безопасности!
Не надо делать так, как в следующем тексте:
int n, i, j;
printf("\n", i, j);
|
|
Задача B. Двумерные массивы
Ввести число n. Заполнить массив n × n числами
1, 2,… ,n2 по спирали.
|
Варианты ваших (правильных!) решений
Легко ли понять, что эти программы действительно правильные?
Интересно, что в примере 1 есть только 4 записи в массив!
С другой стороны, это наименее понятный текст, да и вопросов к нему хватает
(зачем обнуляется массив?
почему не вычислить s так: s=(m+1)/2 ?
ну и — массивы C++).
void spirale(int m){
int t[m][m];
int i,j,s=((m%2)+m)/2,r,f=1;
for(i=0;i<m;i++){
for(j=0;j<m;j++){
t[i][j]=0;
}
}
for(r=0;r<s;r++){
for(i=r;i<m-1-r;i++){
t[i][r]=f;
f++;
}
for(i=r;i<(m-1)-r;i++){
t[(m-1)-r][i]=f;
f++;
}
for(i=m-r-1;i>r-1;i--){
t[i][m-1-r]=f;
f++;
}
for(i=(m-2)-r;i>r;i--){
t[r][i]=f;
f++;
}
}
...
}
|
|
int n, i, j, l=1;
unsigned int k[100][100];
/* ... ввод и проверка n ... */
for (i=0; i<(n/2); i++)
{
for (j=i; j<n-i-1; j++)
{
k[i][j]=l;
l+=1;
}
for (j=i; j<n-i-1; j++)
{
k[j][n-i-1]=l;
l+=1;
}
for (j=n-i-1; j>i; j--)
{
k[n-i-1][j]=l;
l+=1;
}
for (j=n-i-1; j>i; j--)
{
k[j][i]=l;
l+=1;
}
}
if (n%2==1)
{
k[n/2][n/2]=n*n;
}
|
|
#define N 100 // задаём максимальную длину массива
int n, i, j, k = 1;
int arr[N][N];
/* ... ввод и проверка n ... */
for (i = 0; i <= (n - 1) / 2; ++i)
{
arr[i][i] = k;
++k;
for (j = i + 1; j <= n - i - 1; ++j)
{
arr[i][j] = k;
++k;
}
for (j = i + 1; j <= n - i - 1; ++j)
{
arr[j][n - i - 1] = k;
++k;
}
for (j = n - i - 2; j >= i; --j)
{
arr[n - i - 1][j] = k;
++k;
}
for (j = n - i - 2; j >= i + 1; --j)
{
arr[j][i] = k;
++k;
}
}
|
|
|
А если сделать так?
Больше всего это похоже на пример 2 из левой колонки.
Основное отличие в том, что здесь сразу видно,
сколько раз выполняются внутренние циклы.
В примерах слева — приходится соображать.
|
Заполнение 2-мерного массива по спирали
(послойно, снаружи внутрь)
#define NMAX 100
int m[NMAX][NMAX];
int n, value = 1;
int i, len, j;
/* ... ввод и проверка n ... */
/*
* (i,i) -- координаты угловой клетки слоя (от 0 до n/2)
* len*4 -- количество клеток в слое
* (кроме центральной, но в неё мы сейчас не попадём)
*/
for (i = 0, len = n - 1; len > 0; len -= 2, i++)
{
int r = i; // row
int c = i; // column
for (j = 0; j < len; j++) m[r ][c++] = value++;
for (j = 0; j < len; j++) m[r++][c ] = value++;
for (j = 0; j < len; j++) m[r ][c--] = value++;
for (j = 0; j < len; j++) m[r--][c ] = value++;
}
if (len == 0) // при чётном n в конце будет len < 0
m[i][i] = value; // иначе -- запись в центральную клетку
|
|
Вот совершенно отличный от предыдущих вариант
(сделан на основе программы Георгия Соколова)
Здесь только один цикл. И начальное обнуление массива необходимо.
Так как именно данные массива (и его границы) управляют обходом
клеток.
Похожая смена направлений обхода двумерного массива встречается
в алгоритмах обработки изображений. Вдобавок, завершение таких
алгоритмов также управляется данными в массиве.
|
|
Заполнение 2-мерного массива по спирали
(управляется данными в массиве)
#define NMAX 100
int r = 0; // начальная клетка спирали: row
int c = 0; // column
const int dc[4] = {1, 0, -1, 0}; // приращения координат
const int dr[4] = {0, 1, 0, -1}; // в спирали (4 направления)
int dir = 0; // начальное направление
// (меняется так: 0 1 2 3 0 1 2 3 ...)
int m[NMAX][NMAX];
int n, value, maxval;
int i, j;
/* ... ввод и проверка n ... */
for (i = 0; i < n; i++)
for (j = 0; j < n; j++)
m[i][j] = 0; // сначала в массиве ничего нет (= 0)
maxval = n*n; // столько клеток надо заполнить
/*
* идём в текущем направлении, пока можно,
* затем меняем направление на следующее
* (после смены направления всегда можно сделать хотя бы один шаг)
*/
for (value = 1; value <= maxval; value++)
{
int next_r, next_c;
m[r][c] = value; // записали очередное значение
next_r = r + dr[dir]; // такие будут координаты следующей клетки,
next_c = c + dc[dir]; // если направление не изменится
if ( next_r >= 0 && next_r < n // можно ли не менять направление? --
&& next_c >= 0 && next_c < n // row и column попадут в массив?
&& m[next_r][next_c] == 0 ) // следующая клетка ещё свободна?
{ // да, всё хорошо,
r = next_r; // записываем новые координаты
c = next_c; // в r, c
}
else // нет, надо менять направление
{
dir = (dir + 1) & 3; // новое направление (0,1,2,3 -> 1,2,3,0)
r += dr[dir]; // новые
c += dc[dir]; // координаты
}
}
|
|
|
Как теперь вывести двумерный массив?
Варианты ваших решений и их выдача
Не user friendly здесь только вариант 1.
Вариант 2 можно оформить короче и красивее.
Вариант 3 можно сделать оптимальнее.
|
|
Можно было сделать так
Если ограничиться массивом 100 × 100,
то, конечно, можно отвести по 5 символов на число.
Как в примере 1 или 2 (обратите внимание на правильный последний
printf в 1 и на %u для unsigned в 2!).
Или же посчитать (заранее!) нужное количество, как в примере 3.
|
|
#define N 100 // задаём максимальную длину массива
int n, i, j;
int arr[N][N];
for (i = 0; i < n; ++i)
{
for (j = 0; j < n; ++j)
{
printf("%d ", arr[i][j]);
}
printf("\n", i, j);
}
|
|
|
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
|
|
|
#define N 100 // задаём максимальную длину массива
int n, i, j;
int arr[N][N];
for (i = 0; i < n; ++i)
{
for (j = 0; j < n; ++j)
{
printf(" %5d", arr[i][j]);
}
printf("\n");
}
|
|
|
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
|
|
|
int n, i, j;
unsigned int k[100][100];
for (i=0; i<n; i++)
{
for (j=0; j<n; j++)
{
if (k[i][j]<10)
{
printf ("%d ", k[i][j]);
}
else
{
if (k[i][j]<100)
{
printf ("%d ", k[i][j]);
}
else
{
if (k[i][j]<1000)
{
printf ("%d ", k[i][j]);
}
else
{
if (k[i][j]<10000)
{
printf ("%d ", k[i][j]);
}
else
{
printf ("%d ", k[i][j]);
}
}
}
}
}
printf ("\n");
}
|
|
|
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
|
|
|
int n, i, j;
unsigned int k[100][100];
for (i = 0; i < n; i++)
{
for (j = 0; j < n; j++)
{
unsigned int m = k[i][j];
if (m < 10) printf("%u ", m); else
if (m < 100) printf("%u ", m); else
if (m < 1000) printf("%u ", m); else
if (m < 10000) printf("%u ", m); else
printf("%u ", m);
}
printf("\n");
}
|
На самом деле, всё проще:
|
int n, i, j;
unsigned int k[100][100];
for (i = 0; i < n; i++)
{
for (j = 0; j < n; j++)
{
printf("%-5u ", k[i][j]);
}
printf("\n");
}
|
|
|
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
|
|
|
#define NMAX 500
int arr[NMAX][NMAX];
int n, i, j;
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
printf("%d ", arr[i][j]);
for (k = 10; k <= n * n; k *= 10) {
if (arr[i][j] < k) printf(" ");
}
}
printf("\n");
}
|
|
|
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
|
|
|
#define NMAX 500
int arr[NMAX][NMAX];
int n, i, j;
int width, k;
width = 0;
for (k = n * n; k > 0; k /= 10)
width++;
for (i = 0; i < n; i++)
{
for (j = 0; j < n; j++)
printf("%-*d ", width, arr[i][j]);
printf("\n");
}
|
|
|
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
|
|
Проверка правильности ввода и зацикливание программы
По-отдельности каждое из этих требований не слишком сложно.
В самом деле: проверили входные данные "на вшивость" и вышли из программы, если плохо.
Или: поместили всю работу программы в цикл (спрашивая в конце, надо ли продолжать).
Проблема возникает, когда нам надо сделать и то, и другое.
Предположим, пользователь ввёл настолько неправильные данные, что после их обработки какими-нибудь функциями ввода
(scanf и т.п.) в буфере ввода осталось ещё что-то.
Например, если вместо целого числа программе было выдано такое: 123 Yyye-e-e-essss!!!, то весь "хвост"
после 123 останется в буфере и будет использован при вводе следующих данных (ответа на вопрос, продолжать ли,
целого числа для следующего цикла работы и т.д.). Последствия могут быть самые разные…
Ниже показан один из возможных вариантов решения этой проблемы.
Слева приведена общая структура программы. Весь ввод вынесен в функции, которые, помимо сообщения о правильности/неправильности данных,
могут вернуть ещё и признак того, что программе надо завершиться (в наших консольных программах такое бывает,
если поток ввода закончился: например, встретился конец входного файла — EOF —
или пользователь ввёл этот признак с клавиатуры — Ctrl-Z в Windows, Ctrl-D в Linux).
Справа — возможная реализация функций ввода, использующая только функцию scanf.
Идея состоит в том, чтобы после каждого ввода смотреть, что ещё осталось в буфере ввода, но не во всём буфере,
а только до конца текущей строки. И если там остались "непустые" символы (проверяется функцией isspace
из ctype.h), то считать, что ввод был неправильный. Конечно, этот "хвост" строки надо в любом случае
удалить из буфера, чтобы больше не мешался.
Такое решение — неоднозначное, мы тем самым несколько ограничиваем возможности ввода для пользователя.
Но оно вполне приемлемо, особенно если учесть, какими простыми средствами мы воспользовались.
|
Общая структура программы (параметры в глобальных переменных)
int n, k; // входные параметры
/*
* return > 0 -- параметры введены и правильные (EOF мог быть после них)
* return ==0 -- параметры не введены или ошибочны, был встречен EOF
* return < 0 -- параметры не введены или ошибочны, EOF не было
*/
int input_and_check(void)
{
... ввод параметров (n,k) и проверка ...
}
void do_work(void)
{
... использование параметров (n,k) ...
}
/*
* return > 0 -- repeat (Y or y)
* return ==0 -- do not repeat (N or n or EOF)
* return < 0 -- ошибка
*/
int is_cycling(void)
{
printf("Repeat? (Y/N) ");
... ввод и проверка ответа ...
}
int main(void)
{
int cycle; // 0 - надо завершать программу
// >0 - OK, работаем и зацикливаемся
// <0 - плохие параметры - зацикливаемся
do {
cycle = input_and_check();
if (cycle <= 0) // если ошибка при вводе
continue; // или если надо завершать
do_work();
do { // повторяем вопрос,
cycle = is_cycling();
} while (cycle < 0); // пока не получим Y or N
} while (cycle != 0); // while (cycle); -- то же самое
return 0;
}
|
Общая структура программы (параметры в локальных переменных)
|
/*
* return > 0 -- параметры введены и правильные (EOF мог быть после них)
* return ==0 -- параметры не введены или ошибочны, был встречен EOF
* return < 0 -- параметры не введены или ошибочны, EOF не было
*/
int input_and_check(int *pn, int *pk)
{
... ввод параметров (*pn,*pk) и проверка ...
}
void do_work(int n, int k)
{
... использование параметров (n,k) ...
}
/*
* return > 0 -- repeat (Y or y)
* return ==0 -- do not repeat (N or n or EOF)
* return < 0 -- ошибка
*/
int is_cycling(void)
{
printf("Repeat? (Y/N) ");
... ввод и проверка ответа ...
}
int main(void)
{
int n, k; // входные параметры
int cycle; // 0 - надо завершать программу
// >0 - OK, работаем и зацикливаемся
// <0 - плохие параметры - зацикливаемся
do {
cycle = input_and_check(&n, &k);
if (cycle <= 0) // если ошибка при вводе
continue; // или если надо завершать
do_work(n, k);
do { // повторяем вопрос,
cycle = is_cycling();
} while (cycle < 0); // пока не получим Y or N
} while (cycle != 0); // while (cycle); -- то же самое
return 0;
}
|
|
|
Реализация функций ввода (параметры в локальных переменных)
Удобно использовать вспомогательную функцию, которая будет проверять
конец строки в буфере ввода на пустоту и удалять его из буфера.
#include <ctype.h>
/*
* input and discard string of chars (up to, including, \n)
*
* return > 0 -- OK -- пустая строка (with \n)
* return ==0 -- пустая строка, ошибка scanf (EOF - конец потока ввода)
* return < 0 -- непустая строка (with or without \n)
*/
int inp_blank_string(void)
{
int i, spc = 1;
char ch;
// обязательно вводим всё вместе с концом строки
while ((i = scanf("%c", &ch)) > 0 && ch != '\n')
if (!isspace(ch)) // или:
spc = 0; // spc &= isspace(ch);
if (!spc) return -1; // непустая строка
if (i <= 0) return 0; // пустая, ошибка scanf
return 1; // пустая, вместе с концом строки
}
/*
* return > 0 -- параметры введены и правильные (EOF мог быть после них)
* return ==0 -- параметры не введены или ошибочны, был встречен EOF
* return < 0 -- параметры не введены или ошибочны, EOF не было
*/
int input_and_check(int *pn, int *pk)
{
int num, i, eof = 0;
printf("1st parameter (from 1 to 20) = ");
num = scanf("%d", pn); // num > 0 - OK
i = inp_blank_string();
eof ||= !i;
if (i < 0) // непустая строка
num = -1;
if (num > 0) // есть параметр
{
if (*pn < 1 || *pn > 20) // проверка параметра
num = -1; // если -- плохой
}
if (num <= 0)
{
printf("? - 1st parameter is invalid\n");
if (eof) return 0;
return -1;
}
printf("2nd parameter (from 1 to %d) = ", *pn);
num = scanf("%d", pk); // num > 0 - OK
i = inp_blank_string();
eof ||= !i;
if (i < 0) // непустая строка
num = -1;
if (num > 0) // есть параметр
{
if (*pk < 1 || *pk > *pn) // проверка параметра
num = -1; // если -- плохой
}
if (num <= 0)
{
printf("? - 2nd parameter is invalid\n");
if (eof) return 0;
return -2;
}
return 1;
}
/*
* return > 0 -- repeat (Y or y)
* return ==0 -- do not repeat (N or n or EOF)
* return < 0 -- ошибка
*/
int is_cycling(void)
{
int i = 1; // хороший ввод (i <= 0 -- плохой)
char ch;
printf("Repeat? (Y/N) ");
if (scanf(" %c", &ch) <= 0) // с пропуском начальных "пустых"
return 0; // если EOF -- do not repeat
if (inp_blank_string() < 0)
i = -1; // непустая строка
else
{
ch = toupper(ch); // upper case
if (ch != 'Y' && ch != 'N') // проверка ввода
i = -1; // если -- плохой ввод
}
if (i <= 0) return -1; // ни Y, ни N
if (ch == 'Y') return 1; // Y
return 0; // N
}
|
Обратите внимание на вызов scanf(" %c", &ch) в функции is_cycling,
пропускающий все пробельные символы (в том числе \n).
|
|
|
Конечно же, такого рода функции прямо напрашиваются, чтобы их вынесли в заголовочный файл.
Затем этот файл можно просто подключать в свои программы директивой #include "my_inp_funcs.h".
(Конкретно здесь для этого не очень пригодна функция input_and_check — хотелось бы вводить и проверять
любой набор параметров любых типов!)