Форум программистов, компьютерный форум, киберфорум
Наши страницы

Pascal (Паскаль)

Войти
Регистрация
Восстановить пароль
 
Рейтинг: Рейтинг темы: голосов - 61, средняя оценка - 4.62
volvo
Супер-модератор
Эксперт Pascal/DelphiАвтор FAQ
25276 / 16924 / 5343
Регистрация: 22.10.2011
Сообщений: 29,940
Записей в блоге: 6
#1

Как не надо писать программы - Pascal

22.09.2014, 21:10. Просмотров 10948. Ответов 4
Метки нет (Все метки)

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

Итак:

Когда вы пишете графическую программу, вы не должны ...

... рассчитывать на то, что размер экрана постоянен, и равен 640 пикселей по горизонтали на 480 по вертикали. Простейший код - заставка: приветствие пользователя. В центре экрана должна появиться надпись 'Hello'. Что делают новички?
Pascal
1
2
{ инициализация графики }
OutTextXY(320, 240, 'Hello');
Так делать не надо. Потому что это сейчас программа запускается в режиме VgaHi (640x480). Позже вам может понадобиться несколько видеостраниц, а в этом режиме их нет. Придется инициализировать или VgaMed (640x350), или VgaLo (640x200). В случае VGALo вы вообще не увидите "заставки", просто потому что она будет вне экрана.
Написав код правильно:
Pascal
1
2
{ инициализация графики }
OutTextXY(GetMaxX div 2, GetMaxY div 2, 'Hello');
, вы избегаете таких проблем, это будет корректно работать в любом режиме, хоть 320*200, хоть 640*480 (и даже в более высоких разрешениях).

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

Этот пункт является по сути продолжением предыдущего пункта. Итак, была поставлена задача вывести несколько строк, и обвести их прямоугольником (что-то подобное всплывающему меню в Windows-приложениях). Вот так эта задача была решена:
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uses graph;
var gd, gm, i: integer;
const
  s: array[1 .. 5] of string = (
    'one', 'two', 'three', 'four', 'five'
  );
begin
  initgraph(gd, gm, '');
  for i := 1 to 5 do
  begin
    outtextxy(10, 10 + 15 * (i - 1), s[ i ]);
  end;
  rectangle(5, 5, 60, 10+15*5);
  readln;
  closegraph;
end.
При запуске программы получается достаточно приемлемый результат. И тогда программист берет и выносит этот код в отдельную процедуру, справедливо полагая, что это поможет ему выводить разные "менюшки" в разных местах экрана:
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
uses graph;
var
  gd, gm, i: integer;
  i: integer;
 
procedure Print(px, py: integer;
                const s: array of string; n: integer);
var i: integer;
begin
  for i := 1 to n do
  begin
    outtextxy(px, py + 15 * (i - 1), s[i - 1]);
  end;
  rectangle(px - 5, py - 5, px + 50, py+15*n);
end;
 
const
  s: array[1 .. 5] of string = (
    'one', 'two', 'three', 'four', 'five'
  );
begin
  initgraph(gd, gm, '');
  print(10, 10, s, 5);
 
  readln;
  closegraph;
end.
Этот код работает абсолютно так же, как и предыдущий:

Как не надо писать программы

после чего программист, считая, что здесь прокола не будет, использует эту процедуру в своей программе "по полной". И в какой-то момент сталкивается с проблемой:

Как не надо писать программы

Шрифт был изменен с дефолтного на SmallFont, отображение уже не такое, как прежде, и придется искать неправильную константу и исправлять ее (скорее всего - методом подбора). А этого опять же можно было избежать, сразу ориентируясь на работу с разными шрифтами:
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
procedure Print(px, py: integer;
                const s: array of string; n: integer);
var i, curr_x, curr_y, max_x: integer;
begin
  max_x := 0; curr_y := 0;
  for i := 1 to n do
  begin
    curr_x := textwidth(s[i - 1]);
    if curr_x > max_x then max_x := curr_x;
 
    outtextxy(px, py + curr_y, s[i - 1]);
    inc(curr_y, textheight(s[i - 1]) + 5);
  end;
  rectangle(px - 5, py - 5, px + max_x + 5, py + curr_y);
end;
Никаких "магических чисел", ширина и высота текста вычисляются библиотечными функциями. Такая процедура будет работать с любыми выбранными шрифтами, и картинка будет одинаково правильной:
(smallfont, 4)

Как не надо писать программы


(gothicfont, 5)

Как не надо писать программы

... использовать числовые значения вместо именованных констант.
В общем-то, этого правила стоило бы придерживаться не только при программировании графики, но и вообще при любом программировании, особенно начинающему программисту. Однако, как показывают вопросы, задаваемые на форумах, этого правила новички как раз и не придерживаются. А зря. Мне, например, гораздо сложнее понять, что такое:
Pascal
1
2
3
4
5
SetFillStyle(1, 12);
fillEllipse(i, j, r, r);
delay(1000);
SetFillStyle(1, 0);
fillEllipse(i, j, r, r);
или
Pascal
1
2
3
4
SetTextJustify(1,2);
OutTextXY(ygmin-5, xgmin-10, 'fi');
SetTextJustify(2,1);
OutTextXY(ygmax+170, xgmax-160, 't');
, чем разобраться вот в таких кодах:
Pascal
1
2
3
4
5
SetFillStyle(SolidFill, LightRed);
fillEllipse(i, j, r, r);
delay(1000);
SetFillStyle(SolidFill, Black);
fillEllipse(i, j, r, r);
и
Pascal
1
2
3
4
SetTextJustify(CenterText, TopText);
OutTextXY(ygmin-5, xgmin-10, 'fi');
SetTextJustify(RightText, CenterText);
OutTextXY(ygmax+170, xgmax-160, 't');
Ведь компилятор все равно подставит те же самые значения (выигрыша от того, что задаются числа не будет никакого), но программисту будет гораздо проще понять код, и, следовательно, сопровождать и модифицировать его.

Ну, и еще одно замечание. Не перемешивайте логику и интерфейс. Я уже устал говорить об этом, но, похоже, меня не слышат. Если вы делаете графическое меню, то функция отображения должна только отображать его, ни в коем случае не выполняя действия, "подключенные" к этому пункту меню. Потому, что если вам потом захочется сделать другое меню (или текстовое, или тоже графическое, но, скажем, трехмерное), то при разделенных логике/интерфейсе достаточно будет написать новую функцию, отображающую менюшки, а все остальное останется без изменения. В большинстве же проектов, которые пишут студенты начальных курсов ВУЗов, проще переписать всю программу заново, чем чуть-чуть изменить ее внешний вид (потому что все переплетено настолько вычурно, что только потяни за одну ниточку, все запутается окончательно).
10
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Similar
Эксперт
41792 / 34177 / 6122
Регистрация: 12.04.2006
Сообщений: 57,940
22.09.2014, 21:10
Я подобрал для вас темы с готовыми решениями и ответами на вопрос Как не надо писать программы (Pascal):

Как писать программы? - Pascal
Подскажите как чайнику написать прогу

Надо расписать уже решённые программы!!! - Pascal
у меня есть 2 решённые задачи. Мне нужно понять эти задачи, каждый шаг и знать что обозначает каждый элемент. Помогите пожалуйста расписать...

Надо закончить преобразование программы из с++ на язык паскаля - Pascal
Задание. Заполнить секторы матрицы, которые лежат выше и ниже главной и побочной диагоналей ЛП, от левого верхнего угла вправо - вниз....

откомментируйте пожалуйста код программы очень надо - Pascal
сделайте хотя бы основные коментарии.. буду очень благодарен. заранее спасибо uses crt; var a:array of string; ...

создание программы обработки без IF. Очень надо!!!! - Pascal
Помогите!!!!!!!!!!! хотим разобраться с задачкой "Написать программу, в результате выполнения которой булевская переменная t...

Как дать пользователю самому писать код? - Pascal
Всем привет! Пишу интерактивный самоучитель по Pascal (на совсем базовом уровне). Столкнулся с очевидной проблемой: как разрешить...

4
volvo
Супер-модератор
Эксперт Pascal/DelphiАвтор FAQ
25276 / 16924 / 5343
Регистрация: 22.10.2011
Сообщений: 29,940
Записей в блоге: 6
22.09.2014, 21:16  [ТС] #2
(продолжаем...)

Турбо Паскаль и нехватка памяти / ошибки №48, №49

В связи с возросшим количеством вопросов на темы "Программе не хватает памяти, что делать", "Ошибка компиляции №48 (или №49), помогите", "Программа вылетает с переполнением стека, куда копать?", хотелось бы привести основные моменты, на которые стоит обратить внимание при написании программ.

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


Ошибка №48 - Code segment too large (сегмент кода слишком большой).

Возникает в случае, когда размер кода, получаемого при компиляции программы, превышает 65520 байт (то есть, размер одного сегмента).
С этой ошибкой бороться относительно легко, и путей борьбы здесь несколько.
  1. Принимая во внимание тот факт, что код из разных модулей (Unit-ов) компилируется в разные сегменты, а программа на Паскале этих самых сегментов кода может иметь несколько, первое решение - "в лоб" - разбить программу на модули. С большой степенью вероятности ошибка исчезнет.
  2. Второй способ борьбы с ошибкой №48 - это уменьшение размеров самого кода, без разбиения его на модули. В основном, это касается программ, написанных по технологии Copy/Paste, то есть тех, которые чрезвычайно раздуты, и подпрограммы в которых зачастую различаются одной единственной строкой.

Как пример (из реальной программы, по которой был задан подобный вопрос):
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
Procedure op1;
const m2=15;
var mas2:array[1..m2] of string;
    c1,c2:string;
    cf:text;
begin
  mas2[1]:='1.Синий';
  mas2[2]:='2.Зеленый';
  mas2[3]:='3.Бирюзовый';
  mas2[4]:='4.Красный';
  mas2[5]:='5.Малиновый';
  mas2[6]:='6.Коричневый';
  mas2[7]:='7.Светло-серый';
  mas2[8]:='8.Темно-серый';
  mas2[9]:='9.Светло-голубой';
  mas2[10]:='10.Светло-зеленый';
  mas2[11]:='11.Светло-бирюзовый';
  mas2[12]:='12.Светло-красный';
  mas2[13]:='13.Светло-малиновый';
  mas2[14]:='14.Желтый';
  mas2[15]:='15.Белый';
  x1:=100;
  y1:=100;
  cleardevice;
  outtextxy(150,50,'Введите номер цвета текста');
  settextstyle(4,0,1);
  for  b1:=1 to 15 do
  begin
    setcolor(b1);
    outtextxy(x1,y1,mas2[b1]);
    y1:=y1+30;
  end;
  readln(lc);
  cvet:=lc;
  assign(cf,'config.ini');
  reset(cf);
  readln(cf,c2);
  c2:='';
  read(cf,c2);
  close(cf);
  assign(cf,'config.ini');
  rewrite(cf);
  str(lc,c1);
  writeln(cf,c1);
  writeln(cf,c2);
  close(cf);
  v_menu;
end;
Procedure op2;
const m2=15;
var mas2:array[1..m2] of string;
    c1,c2:string;
    cf:text;
begin
  mas2[1]:='1.Синий';
  mas2[2]:='2.Зеленый';
  mas2[3]:='3.Бирюзовый';
  mas2[4]:='4.Красный';
  mas2[5]:='5.Малиновый';
  mas2[6]:='6.Коричневый';
  mas2[7]:='7.Светло-серый';
  mas2[8]:='8.Темно-серый';
  mas2[9]:='9.Светло-голубой';
  mas2[10]:='10.Светло-зеленый';
  mas2[11]:='11.Светло-бирюзовый';
  mas2[12]:='12.Светло-красный';
  mas2[13]:='13.Светло-малиновый';
  mas2[14]:='14.Желтый';
  mas2[15]:='15.Белый';
  x1:=100;
  y1:=100;
  c1:='';
  c2:='';
  cvet2:=0;
  cleardevice;
  outtextxy(150,50,'Введите номер цвета фона');
  settextstyle(4,0,1);
  for b1:=1 to 15 do
  begin
    setcolor(b1);
    outtextxy(x1,y1,mas2[b1]);
    y1:=y1+30;
  end;
  readln(lc);
  cvet2:=lc;
  assign(cf,'config.ini');
  reset(cf);
  read(cf,c1);
  close(cf);
  assign(cf,'config.ini');
  rewrite(cf);
  str(lc,c2);
  writeln(cf,c1);
  writeln(cf,c2);
  close(cf);
  x1:=0;
  y1:=0;
  v_menu;
end;
Чем различаются процедуры? Правильно, в одном случае работаем с цветом самого текста, в другом - с цветом фона. Все. Больше - никакой разницы. А в сегменте кода место занимают обе процедуры (а ведь вполне достаточно одной, с одним дополнительным параметром). Хотя, в этих процедурах можно сделать ещё кое-что, но об этом я скажу чуть позже.
Отвлечемся ненадолго от ошибки №48, и посмотрим ...

Ошибка №49: Data segment too large (слишком большой сегмент данных)

Это происходит в случае, когда общий размер данных (глобальных данных, нужно заметить, потому что локальные размещаются не в сегменте данных) всех модулей превышает отведённый для них размер в 65520 байт. Простым разбиением на модули эту ошибку побороть нельзя: дело в том, что программа на Паскале может содержать несколько сегментов кода, и только один (общий для всех модулей) сегмент данных. Так что, если каждый из ваших модулей по отдельности компилируется - это ещё ничего не значит, при линковке exe-файла все ещё может "всплыть" сообщение об ошибке №49. Например:
Первый модуль:
Pascal
1
2
3
4
5
unit u01;
interface
var a: array[1 .. 10000] of longint;
implementation
end.
Второй модуль:
Pascal
1
2
3
4
5
unit u02;
interface
var b: array[1 .. 10000] of longint;
implementation
end.
Основная программа:
Pascal
1
2
3
4
5
6
7
8
9
10
11
uses u01, u02;
begin
  {
    если этого не сделать, то описания массивов
    будут просто проигнорированы, массивы A и B
    не будут включены в EXE
  }
  a[1] := 1; b[1] := 1; 
  
  writeln;
end.
Каждый модуль в отдельности прекрасно компилируется, при попытке "собрать" основную программу получаем вышеназванную ошибку.
Но и с этой ошибкой можно бороться. И тоже не одним способом:

Способ первый - проверяем, действительно ли нужны массивы типа LongInt. Как пример: нужно хранить в массиве рост 10000 школьников с точностью до сантиметра. И зачем тут longint? Достаточно типа byte (поправьте меня, если я ошибаюсь, но рост более 255 сантиметров для школьника - глупо), экономия четырехкратная. После изменения базового типа на byte даже только в одном из модулей мы избавляемся от "ошибки №49".

Особое внимание обратите на строковые типы: по умолчанию для string-а резервируется 256 байт. Это не всегда нужно, можно существенно уменьшить размер данных ограничением длины строки через string[длина].

То же самое касается вещественных типов: вместо использования real (6 байт) используем single (4 байта). Во-первых, экономия памяти в случае использования массивов, во вторых - увеличение быстродействия программы (тип real вообще очень медленный, поскольку он является "чужеродным" для сопроцессора, и его конвертация в сопроцессорный тип занимает кучу времени).

Способ второй - используем динамическое выделение памяти вместо статического, в результате данные хранятся не в сегменте данных, а в "куче", следовательно, опять избавляемся от ошибки №49:
Модуль №1
Pascal
1
2
3
4
5
6
7
8
9
unit u01;
interface
type
  ta = array[1 .. 10000] of longint;
var
  a: ^ta;
 
implementation
end.
Модуль №2
Pascal
1
2
3
4
5
6
7
8
unit u02;
interface
type
  tb = array[1 .. 10000] of longint;
var
  b: ^tb;
implementation
end.
Основная программа:
Pascal
1
2
3
4
5
6
7
8
uses u01, u02;
 
begin
  new(a); new(b); { выделяем память под оба массива }
  a^[1] := 1; b^[1] := 1;
  { ... }
  dispose(a); dispose(b); { освобождаем память }
end.
Поскольку размер "кучи" больше размера сегмента в 10 раз, то таким образом можно работать с данными гораздо большего объема.
5
volvo
Супер-модератор
Эксперт Pascal/DelphiАвтор FAQ
25276 / 16924 / 5343
Регистрация: 22.10.2011
Сообщений: 29,940
Записей в блоге: 6
22.09.2014, 21:19  [ТС] #3
RunTime error 202: Stack overflow error
(или "Программа вылетает с переполнением стека")

Причиной этого является то, что в стеке закончилось место для размещения локальных переменных подпрограммы. То есть, если есть программа:
Pascal
1
2
3
4
5
6
7
8
9
procedure a;
var arr: array[1 .. 20000] of byte;
begin
  arr[1] := 0;
end;
 
begin
  a;
end.
, то при попытке ее выполнить произойдет переполнение стека (размер стека по умолчанию равен 16Кб = 16384 байта, а здесь - попытка разместить 20000 байт).
(Также эта ошибка очень часто возникает при разработке - неправильной, естественно - рекурсивных подпрограмм. Об этом - чуть ниже)

И опять же есть несколько способов борьбы и с этой ошибкой:
  1. Что называется "в лоб" - увеличение размера стека. Заходим в Options|Memory sizes и в поле Stack size меняем значение с 16384 (по умолчанию) на любое значение до 65535 (максимально возможное). Если это не помогло, и ошибка по-прежнему вылетает, значит переходим к способу...
  2. Выделение памяти динамически. Точно так же, как и при борьбе с переполнением сегмента данных, выделяем память через New и освобождаем через Dispose, при этом данные будут храниться не в стеке, а в "куче".
  3. Использование глобальных переменных. Я не сторонник этого метода (глобальные переменные увеличивают вероятность совершить ошибку),
    Небольшое дополнение
    глобальные переменные не обязательно описываются в самом начале программы, перед всеми процедурами и функциями. Если описать такую переменную непосредственно перед подпрограммой, в которой возникает ошибка переполнения стека, а саму эту подпрограмму - как можно ниже по тексту программы - то можно свести вероятность побочных эффектов к минимуму.

    но при возникновении переполнения стека использование глобальных переменных может помочь: ведь глобальные переменные хранятся не в стеке, а в сегменте данных, следовательно, стек разгружается.
  4. Есть ещё один способ борьбы с переполнением стека. И вот тут я бы хотел вернуться к тому фрагменту программы, который я привел выше, помните? Так вот. В таких случаях желательно не только "укоротить" размер строки в описании массива mas2, но и описывать mas2 не как переменную, а как типизированную константу:
    Pascal
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    Procedure op1;
    const 
      m2=15;
      mas2:array[1..m2] of string = (
        '1.Синий',
        '2.Зеленый',
        '3.Бирюзовый',
        '4.Красный',
        '5.Малиновый',
        '6.Коричневый',
        '7.Светло-серый',
        '8.Темно-серый',
        '9.Светло-голубой',
        '10.Светло-зеленый',
        '11.Светло-бирюзовый',
        '12.Светло-красный',
        '13.Светло-малиновый',
        '14.Желтый',
        '15.Белый'
      );
    var
        c1,c2:string;
        cf:text;
    begin
      { ... }
    Во-первых, это избавляет нас от необходимости инициализировать все элементы массива "вручную", программа становится более "стройной", а во вторых - типизированные константы не хранятся в стеке несмотря на то, что они описаны локально. Они всегда хранятся в сегменте данных, то есть, стек опять разгружается.

Что касается рекурсии:
  1. Необходимо следить за тем, что рекурсивная подпрограмма имеет условие окончания (иначе рекурсия становится бесконечной, и возникает ошибка переполнения стека), а также необходимо обращать внимание, чтобы не было того, о чем я написал вот здесь: Ошибки при организации меню
  2. Ни в коем случае не отключайте режим контроля стека через {$S-}, это не заставит автоматически программу работать правильно, а вот ошибку, предупреждающую, что в программе что-то не так, подавит... И будете потом надеяться на то, что это работает, а оказывается - нет.

Ну, и еще несколько общих рекомендаций при разработке программ:
  1. Всегда при отладке компилируйте программы с ключами
    {$R+} (Options|Compiler, группа Runtime Errors - отметить Range Checking) - это поможет найти выходы за пределы массивов, если они есть в программе. В окончательной, уже отлаженной версии, этот ключ можно убрать, это немного ускорит выполнение программы.
    {$S+} (Options|Compiler, группа Runtime Errors - отметить Stack Checking) - об этом я написал выше, помогает "поймать" переполнение стека.
  2. Будьте очень аккуратны с использованием ключа {$B}, чревато ошибками: Оптимизация
6
volvo
Супер-модератор
Эксперт Pascal/DelphiАвтор FAQ
25276 / 16924 / 5343
Регистрация: 22.10.2011
Сообщений: 29,940
Записей в блоге: 6
22.09.2014, 21:22  [ТС] #4
(заканчиваем)

Паскаль - это язык с очень мощной системой типов. Если правильно использовать этот факт, то можно ловить некоторые ошибки в программе еще на этапе компиляции. Например:
(неправильное использование)
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type
  Direction = (left, right, up, down);
  RPoint = record
    X, Y: integer;
  end;
  
{ Здесь могут быть еще описания типов }
 
const
  deltas: array[left .. down] of RPoint = (
    (X:-1; Y:0), (X:+1; Y:0), (X:0; Y:-1), (X:0; Y:+1)
  );
  currPos: RPoint = (X:10; Y:10);
var
  dir: Direction;
 
{ где-то в тексте программы }
  dir := left;
  Inc(currPos.X, deltas[dir].X);
  Inc(currPos.Y, deltas[dir].Y);
Почему неправильное? Потому что при внесении в программу дополнений тип Direction мог измениться, например, стать таким:
Pascal
1
2
3
4
5
type
  Direction = (
    left, right, up, down,
    leftup, leftdown, rightup, rightdown
  );
, но изменение типа Direction никак не заставляет программиста изменить и константу Deltas, хотя именно это было бы самым логичным. Если при измененном Direction значение Dir станет равно leftup (или любому другому добавленному значению), программа немедленно вылетит с ошибкой. А если не включен контроль индексов - то еще хуже, будет портить память, и результаты ее работы вообще станут невменяемыми.
(правильное решение)
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
type
  Direction = (left, right, up, down);
  RPoint = record
    X, Y: integer;
  end;
 
const
  { !!! Описание deltas изменилось !!! }
  deltas: array[Direction] of RPoint = (
    (X:-1; Y:0), (X:+1; Y:0), (X:0; Y:-1), (X:0; Y:+1)
  );
  currPos: RPoint = (X:10; Y:10);
Теперь при добавлении новых значений в тип Direction, программа не откомпилируется до тех пор, пока не будет изменена и константа deltas (точнее - пока она не будет содержать ровно столько элементов, сколько значений в типе Direction). Что сохранит программисту нервы при отладке программы.

Повторяю: Паскаль - это язык с очень мощной системой типов. Пользуйтесь этим. Не экономьте на введении новых типов в свою программу. Простое описание типа не увеличивает размер exe-файла программы, какого бы большого размера не был описываемый тип. Т.е., не экономьте подобным образом (описанием типа [1 .. 1], я имею в виду):
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{ Чуть выше я говорил, что не надо выключать $R }
{$R+}
Type
  arrType = Array[1 .. 1] of Integer;
  PArrType = ^arrType;
  { для реализации динамического массива,
    если кто не догадался }
 
var
  P: PArrType;
  i: integer;
 
begin
  { Если теперь сделать так: }
  GetMem(P, 1024 * SizeOf(Integer));
  for i := 1 to 1024 do
    P^[ i ] := i; {, то программа вылетит при i = 2 }
  FreeMem(P, 1024 * SizeOf(Integer));
end.
Почему программа вылетит? Да потому, что компилятору все равно, сколько памяти вы реально выделили под массив. Все, что он знает - это то, что переменная P указывает на массив, индексы которого изменяются от 1 до 1, то есть, на массив из одного элемента. При обращении по индексу, лежащему вне этого интервала возникает ошибка. Для того, чтобы программа заработала, надо отключить проверку индексов, при этом вы остаетесь без диагностики ошибок в других местах программы. Не будете же вы каждый раз когда дело касается чтения/записи в массив выключать контроль границ, а потом - сразу включать?
Решается это очень просто:
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{$R+}
Type
  arrType = Array[1 .. MaxInt div SizeOf(Integer)] of Integer;
  PArrType = ^arrType;
 
var
  P: PArrType;
  i: integer;
 
begin
  GetMem(P, 1024 * SizeOf(Integer));
  for i := 1 to 1024 do
    P^[ i ] := i; { Все прекрасно работает ... }
  FreeMem(P, 1024 * SizeOf(Integer));
end.
, и контроль границ остался включенным.

Еще один пример, когда описание нового типа помогает справиться с задачей быстрее/проще, чем было без него. Всем хорошо известная задача: поменять в матрице вторую и четвертую строку. "Ну, чего тут сложного," - говорит новичок, и лихо пишет программу:
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const
  A: array[1 .. 5, 1 .. 5] of integer = (
    (11, 12, 13, 14, 15),
    (21, 22, 23, 24, 25),
    (31, 32, 33, 34, 35),
    (41, 42, 43, 44, 45),
    (51, 52, 53, 54, 55)
  );
 
var
  i, j, T: integer;
begin
  for i := 1 to 5 do
  begin
    for j := 1 to 5 do write(A[i, j]:4);
    writeln;
  end;
  writeln;
 
  for j := 1 to 5 do
  begin
    T := A[2, j]; A[2, j] := A[4, j]; A[4, j] := T;
  end;
 
 
  for i := 1 to 5 do
  begin
    for j := 1 to 5 do write(A[i, j]:4);
    writeln;
  end;
  writeln;
end.
Все прекрасно, все работает. Только это же самое можно было сделать вот так:
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
type
  TVector = array[1 .. 5] of Integer;
const
  A: array[1 .. 5] of TVector = (
    (11, 12, 13, 14, 15),
    (21, 22, 23, 24, 25),
    (31, 32, 33, 34, 35),
    (41, 42, 43, 44, 45),
    (51, 52, 53, 54, 55)
  );
 
var
  i, j: integer;
  T: TVector;
begin
  for i := 1 to 5 do
  begin
    for j := 1 to 5 do write(A[i, j]:4);
    writeln;
  end;
  writeln;
 
  { С массивами работаем, как с любым простым типом }
  T := A[2]; A[2] := A[4]; A[4] := T;
 
  for i := 1 to 5 do
  begin
    for j := 1 to 5 do write(A[i, j]:4);
    writeln;
  end;
  writeln;
end.
Не знаю, как вам, но мне второй способ нравится больше...

Ну, и еще кое-что. Я постоянно говорю об этом на форумах, но все время встречаю ошибки, связанные с нежеланием придерживаться этого простого совета: объявляйте переменную как можно ближе к тому месту, где она должна использоваться. Особенно это касается счетчиков цикла. Вот программа, иллюстрирующая ошибку:
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var
  arr: array[1 .. 5] of integer;
  i: integer;
 
procedure print;
begin
  for i := 1 to 5 do write(arr[ i ]:3);
  writeln;
end;
 
begin
  for i := 1 to 5 do
  begin
    arr[ i ] := 10 * i;
    print;
  end;
end.
, и сидим, гадаем, почему программа не показывает процесс заполнения массива. Да потому, что переменная i описана глобально, изменяется внутри процедуры, и после первого же вызова процедуры ее значение превышает конечное для цикла... Причем, это никак не фиксируется компилятором. Никаким. Вот если попробовать изменить значение этой переменной внутри цикла, то современные компиляторы (Дельфи, FPC) забьют тревогу: нельзя модифицировать переменную цикла. А изменение глобальной переменной не приводит к ругани компилятора.

Вот так надо было писать программку:
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type
  arrType = array[1 .. 5] of integer;
 
{
  передаем массив как параметр, а не описываем его глобально !!!
}
procedure print(const arr: arrType);
var i: integer; { максимально близко к месту использования }
begin
  for i := 1 to 5 do write(arr[ i ]:3);
  writeln;
end;
 
var
  arr: arrType;
  i: integer; { это - для цикла заполнения массива }
 
begin
  for i := 1 to 5 do
  begin
    arr[ i ] := 10 * i;
    print(arr);
  end;
end.
, тогда программа работает, как ожидалось.
И еще одно уточнение, касающееся этой программы: заметили в 14-ой строке, что я не написал:
Pascal
1
2
var
  arr: array[1 .. 5] of integer;
?
Повторяю еще раз: Паскаль - это язык с очень мощной системой типов, и это накладывает свои требования: даже две переменных (казалось бы одинакового типа)
Pascal
1
2
3
var
  a: array[1 .. 5] of integer;
  b: array[1 .. 5] of integer;
на самом деле имеют совершенно разный тип, потому что описаны похожие, но разные с точки зрения компилятора конструкции. Чтобы тип был действительно один и тот же, надо описать его в разделе type, и потом использовать для обеих переменных. Вот тогда у компилятора претензий не будет.
7
volvo
Супер-модератор
Эксперт Pascal/DelphiАвтор FAQ
25276 / 16924 / 5343
Регистрация: 22.10.2011
Сообщений: 29,940
Записей в блоге: 6
22.09.2014, 21:24  [ТС] #5
Решил дополнить список того, как делать не надо.
То, о чем будет сказано ниже - относится в основном к новому компилятору - FPC...


Не используйте в качестве параметров функционального (процедурного) типа функции с неправильной сигнатурой.


Объясню на примере. Вы все, конечно, слышали, что для передачи в подпрограмму своей функции, надо предварить ее имя операцией взятия адреса? Далеко ходить не буду, открываем один из примеров, идущих с FPC, называется winhello.pp, находится он в папке \FPC\{версия}\demo\win32, и создается в этом примере простейшее Win32 приложение. Вот одна из процедур этой программы:
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{ Register the Window Class }
function WinRegister: Boolean;
var
  WindowClass: WndClass;
begin
  WindowClass.Style := cs_hRedraw or cs_vRedraw;
  WindowClass.lpfnWndProc := WndProc(@WindowProc); { <--- !!! }
  WindowClass.cbClsExtra := 0;
  WindowClass.cbWndExtra := 0;
  WindowClass.hInstance := system.MainInstance;
  WindowClass.hIcon := LoadIcon(0, idi_Application);
  WindowClass.hCursor := LoadCursor(0, idc_Arrow);
  WindowClass.hbrBackground := GetStockObject(WHITE_BRUSH);
  WindowClass.lpszMenuName := nil;
  WindowClass.lpszClassName := AppName;
 
  Result := RegisterClass(WindowClass) <> 0;
end;
Обратите внимание на значок "@", присутствующий в отмеченной мной строке...

Еще примеры? Пожалуйста, открываем DRKB, смотрим пример использования функции EnumWindows. Я его немного упростил, вот в таком виде программа компилируется FPC:
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{$mode delphi}
uses windows;
 
function AddWinInfo(WinHandle: HWnd): Boolean; stdcall;
var
  WinCaption,WinClass: array[0..255] of Char;
begin
  Result:=True;
  GetClassName(WinHandle,WinClass,SizeOf(WinClass));
  GetWindowText(WinHandle,WinCaption,SizeOf(WinCaption));
  WriteLn(WinClass + ' - ' + WinCaption);
end;
 
procedure Test;
begin
  EnumWindows(@AddWinInfo, LParam(0));
end;
 
begin
  Test;
end.
Что видим при вызове EnumWindows? Опять тот же значок "@"? Попробуйте убрать его - программа перестает компилироваться. Ужас, правда?
Кто-нибудь задумывался, почему, собственно, надо добавлять этот вездесущий "@"? Посмотрите в любой книге по Турбо-Паскалю тему "Процедурные типы". Вы там хоть один такой символ при передаче функции как параметра в другую функцию видели? Что же произошло, что теперь компиляторы требуют везде брать адрес, интересно?
А ничего не произошло. Все дело - в том, что этот значок никому не нужен. Вы этим самым оказываете и себе и компилятору медвежью услугу - вместо того, чтобы описать Callback функцию правильно, вы берете адрес этой функции, который совместим с любым другим указателем.
Вспомните, что
Pascal
1
2
3
4
5
6
7
8
9
10
type
  pInt = ^Integer;
  pDouble = ^Double;
var
  p_i: PInt;
  p_d: PDouble;
begin
  // ...
  p_i := p_d; // <--- Ошибка - указатели несовместимы !!!
end.
Но в то же время:
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
type
  pInt = ^Integer;
  pDouble = ^Double;
var
  p_i: PInt;
  p_d: PDouble;
  p: Pointer;
begin
  // ...
  p := p_d;  // Вот она, та же самая медвежья услуга
  p_i := p;  // Теперь p_i = p_d, обошли ограничение
end.
Вот нечто подобное происходит в примере, описанном выше. Заставляем компилятор закрыть глаза на то, что функция вызывается не так, как должна вызываться. Возможно, это удобнее, но искать возникающие при таком подходе ошибки на порядок сложнее.

Что же надо сделать, чтобы исправить приведенные выше примеры? Очень немного.
1. Оконная функция. Смотрим в MSDN (если не помним наизусть) сигнатуру оконной функции:
C++
1
2
3
4
5
6
LRESULT CALLBACK WindowProc(
/* __in */ HWND hwnd,
/* __in */ UINT uMsg,
/* __in */ WPARAM wParam,
/* __in */ LPARAM lParam
);
, убеждаемся, что сигнатура описанной в примере функции полностью совпадает с правильной, и просто убираем "взятие адреса":
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{ Register the Window Class }
function WinRegister: Boolean;
var
  WindowClass: WndClass;
begin
  WindowClass.Style := cs_hRedraw or cs_vRedraw;
  WindowClass.lpfnWndProc := WindowProc;
  // Ни @ ни приведение типа не нужны !!!
 
  WindowClass.cbClsExtra := 0;
  WindowClass.cbWndExtra := 0;
  WindowClass.hInstance := system.MainInstance;
  WindowClass.hIcon := LoadIcon(0, idi_Application);
  WindowClass.hCursor := LoadCursor(0, idc_Arrow);
  WindowClass.hbrBackground := GetStockObject(WHITE_BRUSH);
  WindowClass.lpszMenuName := nil;
  WindowClass.lpszClassName := AppName;
 
  Result := RegisterClass(WindowClass) <> 0;
end;
Перекомпилируем программу (ошибок нет, странно, правда?), и запускаем ее. Программа работает точно так же, как и раньше. Значит, не надо приказывать компилятору делать глупости?

2. Функция перечисления окон. Аналогично: смотрим описание EnumWindows, чтобы узнать, какая функция ожидается первым параметром. А вот такая:
C++
1
2
3
4
BOOL CALLBACK EnumWindowsProc(
/* __in */ HWND hwnd,
/* __in */ LPARAM lParam
);
Теперь понимаете, почему требовалось "замазать глаза" компилятору? Не та сигнатура!!! Переделываем на правильную:
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{$mode delphi}
uses windows;
 
// Мало того, что не хватало второго параметра,
// так еще типы Boolean и BOOL - это не одно и то же
function AddWinInfo(WinHandle: HWnd; param: LParam): BOOL; stdcall;
var
  WinCaption,WinClass: array[0..255] of Char;
begin
  Result:=True;
  GetClassName(WinHandle,WinClass,SizeOf(WinClass));
  GetWindowText(WinHandle,WinCaption,SizeOf(WinCaption));
  WriteLn(WinClass + ' - ' + WinCaption);
end;
 
procedure Test;
begin
  EnumWindows(AddWinInfo, LParam(0)); // Адрес не берем !!!
end;
 
 
begin
  Test;
end.
, и все прекрасно работает!

То же самое касается и излюбленных хаков: запуска функций через RunDll32. Очень много в интернете советов подобного рода:

rundll32.exe user32.dll, LockWorkStation
или
rundll32 user32.dll LockWorkStation
(блокировка компьютера)

Windows Vista:
rundll32 user32.dll, SwapMouseButton
(поменять местами кнопки мыши... Обратная смена невозможна !!!)
Нельзя этого делать! Почему? Да все по той же причине. Сначала давайте посмотрим, что ожидается RunDll32 в качестве параметра:
Интерфейс программ Windows Rundll и Rundll32

Вот что:
C++
1
2
3
4
5
void CALLBACK EntryPoint(HWND hwnd, HINSTANCE hinst,
  LPCSTR lpszCmdLine, int nCmdShow);
// или
void CALLBACK EntryPointW(HWND hwnd, HINSTANCE hinst,
  LPWSTR lpszCmdLine, int nCmdShow);
А какой прототип имеют функции LockWorkStation или SwapMouseButton?

И после этого люди еще удивляются, что "обратная смена невозможна"? Удивляться-то надо не этому, а тому, что оно вообще хоть как-то работает. Опять же, отсюда и эти самые "работает только на Win98, или только на Win95". Запуск функции с подходящими сигнатурами поддерживается на всех версиях Windows. А вот поддерживать то, что изначально не предполагалось - извините, никто не обязан.

P.S. Я говорю об этом каждый раз: Паскаль - очень мощный язык, но используйте его правильно, не пытайтесь "объегорить" ни компилятор ни ОС. Выйдет себе дороже.

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

Желающие скачать этот топик отдельным PDF-файлом могут это сделать, в аттаче точная копия топика.
13
Вложения
Тип файла: pdf dont_do_it.pdf (207.8 Кб, 27 просмотров)
22.09.2014, 21:24
MoreAnswers
Эксперт
37091 / 29110 / 5898
Регистрация: 17.06.2006
Сообщений: 43,301
22.09.2014, 21:24
Привет! Вот еще темы с ответами:

Как перенести блок схему, что нужно писать в этой фигуре - Pascal
Что здесь нужно написать ?

Надо ли в конце программы до end писать readln - Pascal ABC
Надо ли в конце программы до end писать readln, чтобы просто курсор перешел на след. строку и когда повторно выполняешь программу, не...

Помогите писать на С++ через шаблоны. Консуле я писал, но надо писать исползуя шаблоны - C++
В одномерном массиве, состоящем из п вещественных элементов, вычислить: 1) количество элементов массива, равных 0; 2) сумму элементов...

Как создать программу которая бы выводила рандомно примеры и надо было правильно писать ответы - Delphi БД
Как написать программу которая рандомно ставила примеры и надо было только писать правильные ответы к примеру 1+1= ответ 2 2-1= ответ 1...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
5
Закрытая тема Создать тему
Опции темы

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.
Рейтинг@Mail.ru