Основы программирования
Изучение основных правил формального описания синтаксиса языка программирования. Ознакомление с требованиями к байту перед вызовом функции. Исследование процесса организации данных в виде стека. Рассмотрение и характеристика примеров работы с файлами.
Рубрика | Программирование, компьютеры и кибернетика |
Вид | курс лекций |
Язык | русский |
Дата добавления | 12.10.2016 |
Размер файла | 156,4 K |
Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже
Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.
int choice; /* Текущий выбор пункта меню */
double fun, x; /* Значения функции и аргумента */
printf( "\nВведите аргумент x=" );
scanf( "%lf", &x );
loop=1;
while ( loop )
{
printf ( "\n Введите номер функции:\n" );
printf ( "1. sin(x)\n2. cos(x)\n" );
printf ( "3. tan(x)\n4. Конец работы\n" );
scanf ( "%d", &choice );
if (choice==1) fun=sin(x);
else if (choice==2) fun=cos(x);
else if (choice==3) fun=tan(x);
else if (choice==4) { loop=0; continue; }
else { printf( "Неверный выбор\n" ); continue; }
printf( "Значение функции %lf\n", fun );
}
}
Данная программа выводит на экран дисплея список возможных действий. Пользователь должен выбрать нужное, введя его номер.
Для управления выходом из цикла и организации конца работы программы используется флаг loop.
8.8 Множественный выбор. Оператор переключения
В предыдущей программе осуществлялся так называемый "множественный выбор", когда в зависимости от значения некоторой переменной целого типа выбиралось одно из нескольких действий.
Для осуществления подобных операций в языке Си существует специальный оператор "switch" -переключатель. Используя этот оператор, вместо вложенных "if" в предыдущей программе можно записать следующую конструкцию:
switch ( choice )
{
case 1 : fun=sin(x); break;
case 2 : fun=cos(x); break;
case 3 : fun=tan(x); break;
case 4 : loop=0; break;
default: printf( "Неверный выбор\n" ); break;
}
Здесь ключевое слово "case" (случай) указывает на то, что следующая константа является значением переменной choice, для которого выполняются соответствующие действия.
Оператор "break;" осуществляет выход из оператора "switch", а не из цикла. Именно поэтому для выхода из цикла пришлось использовать специальный флаг loop.
Ключевое слово "default" ( умолчание ) означает, что следом записаны действия, выполняющиеся, если значение choice не совпадет ни с одной из констант, указанных за "case".
Оператор switch в общем виде выглядит так:
БНФ:
оператор_переключения =
"switch" "(" выражение ")"
"{"
"case" константа ":" { оператор }
"case" константа ":" { оператор }
[ "default" ":" { оператор } ]
"}"
В качестве выражения можно использовать любое выражение, имеющее символьный или целый тип. Константы тоже должны быть символьного или целого типа, например case 4 , case 'A'.
Допускается использовать несколько констант для пометки одной группы операторов, например
case 2 :
case 4 :
case 8 : f = sin(x); break;
Если группа операторов не завершается оператором break, то будут выполняться все последующие операторы независимо от наличия case с константами до ближайшего break или другого оператора, прерывающего естественную последовательность действий. Иными словами ключевое слово case с константой надо рассматривать как метку для передачи управления. Так, в следующем операторе
switch( ch )
{
case 'A' :
case 'B' : x++;
case 'C' :
case 'D' : f = sin(x); break;
case 'E' : f = cos(x); break;
}
будет вычислено f=sin(x+1), при ch равном 'A' или 'B'; f=sin(x), при ch равном 'C' или 'D'; f=cos(x), при ch равном 'E'.
При использовании оператора switch необходимо внимательно следить за правильностью написания ключевого слова default и за тем, чтобы между ключевым словом case и числовой константой обязательно имелся пробел. В противном случае, эти конструкции будут приняты за правильно записанные метки для оператора goto, и компилятор не выдаст никакой диагностики.
8.9 Оператор цикла do-while
Оператор цикла do-while предназначен для реализации циклических алгоритмов и имеет следующую форму записи
БНФ:
цикл_do-while =
"do" оператор "while" "("выражение")" ";"
Оператор выполняется циклически до тех пор, пока выражение отлично от нуля. В отличие от оператора while, тело оператора do-while выполняется хотя бы один раз до первого вычисления условия.
Работу оператора do-while проиллюстрируем на примере программы, которая определяет корень уравнения x-cos(sin(x))=0 методом итераций, который заключается в циклическом вычислении очередного приближения x_new по предыдущему приближению x_old, согласно выражению x_new=cos(sin(x_old)), вытекающему из исходного уравнения. Процесс итерации заканчивается тогда, когда x_new станет равен x_old. Программа, реализующая этот алгоритм, приведена ниже.
#include <stdio.h>
#include <math.h>
/* Решение уравнения x-cos(sin(x))=0 */
void main (void)
{
double x_new=0.9, x_old, eps=0.0001;
do
{
x_old = x_new;
x_new = cos(sin(x_old));
} while ( fabs( x_new - x_old ) > eps );
printf ( "x=%lf", x_new );
}
Сравнение двух вещественных чисел осуществляется с использованием точности eps. Это необходимо потому, что из-за погрешностей округления прямая проверка на равенство двух вещественных чисел скорее всего даст в результате 0 (ложь).
8.10 Перечисления. Работа с клавиатурой IBM PC
Перечисления используются для задания символических имен константам целого типа.
БНФ:
перечисление =
"enum" [ имя_перечисления ]
"{"
имя_конст [ "=" конст_выр ]
{ "," имя_конст [ "=" конст_выр ] }
"};"
Здесь имя_перечисления - любое символическое имя; имя_конст - символическое имя, назначаемое константе; конст_выр - константное выражение, то есть такое, которое не содержит переменных и функций и может быть определено на этапе компиляции.
Если константное выражение отсутствует, то имени назначается значение предыдущего выражения, увеличенное на единицу. Если отсутствует выражение, соответствующее первому имени константы, то ему назначается значение 0. Например:
enum DAYS { MON=1, TUE, WED, THU, FRI, SAT, SUN };
enum MONTH { JAN=1, FEB, MAR, APR, MAY, JUN,
JUL, AUG, SEP, OCT, NOV, DEC };
В дальнейшем в программе эти имена можно использовать вместо целых констант. Например, DEC вместо 12, THU вместо 4 и т. д.
Рассмотрим использование перечислений для организации удобной работы с клавиатурой IBM PC.
В библиотеке conio имеется функция, осуществляющая ввод одиночного символа (точнее его кода) без отображения его на экране дисплея. Она имеет следующий прототип:
int getch( void );
При обращении к этой функции выполнение программы приостанавливается до нажатия на клавишу. После нажатия на клавишу код соответствующего символа возвращается в виде целого числа.
Некоторым клавишам клавиатуры не соответствует ни один символ из кодовой таблицы. При нажатии на подобные клавиши getch() вначале возвращает нулевое значение. Если при этом обратиться к функции повторно, то она возвратит условный номер клавиши на клавиатуре, так называемый скэн-код. Это свойство используется в функции GetCh() для расширения возможностей getch(). Функция GetCh() будет возвращать коды символов в обычных случаях. При нажатии специальной клавиши GetCh() возвратит скэн-код, увеличенный на 256 (0x100) или на 512 (0x200), в зависимости от того, была ли нажата клавиша Shift или нет. Текст функции GetCh() приводится ниже.
#include <conio.h>
#include <bios.h>
/* Ввод одиночного символа с клавиатуры */
int GetCh( void )
{
int ch;
if( ( ch = getch() ) == 0 )
ch = getch() | ( bioskey(2) & 3 ? 0x200 : 0x100 );
return ch;
}
Здесь, выражение bioskey(2) & 3 осуществляет проверку нажатия клавиши Shift и отлично от нуля, если последняя нажата.
Прототип функции GetCh() и коды специальных клавиш перечисления KeyboardCodes, получаемых с помощью этой функции, следует поместить в файл, например, keyboard.h и в дальнейшей использовать не числовые значения кодов, а только символические имена. Фрагмент перечисления KeyboardCodes приведен ниже:
enum KeyboardCodes
{
kbF1 = 315, kbF2, kbF3, kbF4, ..., kbF10,
kbShiftF1 = 596, kbShiftF2, kbShiftF3, ..., kbShiftF10,
kbCtrlF1 = 350, kbCtrlF2, kbCtrlF3, ..., kbCtrlF10,
kbAltF1 = 360, kbAltF2, kbAltF3, ..., kbAltF10,
kbAlt1 = 376, kbAlt2, kbAlt3, ..., kbAlt0,
kbAltQ = 272, kbAltW, kbAltE, ..., kbAltP,
kbAltA = 286, kbAltS, kbAltD, ..., kbAltL,
kbAltZ = 300, kbAltX, kbAltC, ..., kbAltM,
kbCtrlA = 1, kbCtrlB, kbCtrlC, ..., kbCtrlZ,
kbUp = 328, kbDown = 336, kbTab = 9,
kbCtrlUp = 397, kbCtrlDown = 401, kbCtrlTab = 404,
kbAltUp = 408, kbAltDown = 416, kbAltTab = 421,
kbShiftUp = 584, kbShiftDown = 592, kbShiftTab = 527,
kbRight = 333, kbLeft = 331, kbEsc = 27,
kbCtrlRight = 372, kbCtrlLeft = 371,
kbAltRight = 413, kbAltLeft = 411, kbAltEsc = 257,
kbShiftRight = 589, kbShiftLeft = 587,
kbPgUp = 329, kbPgDn = 337, kbIns = 338,
kbCtrlPgUp = 388, kbCtrlPgDn = 374, kbCtrlIns = 513,
kbAltPgUp = 409, kbAltPgDn = 417, kbAltIns = 418,
kbShiftPgUp = 585, kbShiftPgDn = 593, kbShiftIns = 594,
kbHome = 327, kbEnd = 335, kbDel = 339,
kbCtrlHome = 375, kbCtrlEnd = 373, kbCtrlDel = 515,
kbAltHome = 407, kbAltEnd = 415, kbAltDel = 419,
kbShiftHome = 583, kbShiftEnd = 591, kbShiftDel = 595,
kbEnter = 13, kbBackspace = 8,
kbCtrlEnter = 10, kbCtrlBackspace = 127,
kbAltEnter = 284, kbAltBackspace = 270
};
Предложенная методика работы с клавиатурой IBM PC не требует никаких изменений исходных текстов программ, использующих функцию GetCh(), в случае изменения аппаратных средств.
8.11 Пример организации светового меню
Меню современных программ, как правило, представляет собой набор строк по которому перемещается светлый прямоугольник - световое окно. Для написания программ с подобным меню необходимо уметь выводить в нужное место экрана информацию разными цветами.
Экран IBM PC имеет 25 строк и 80 позиций. Нумерация строк и позиций начинается с 1. Первая строка находится вверху, первая позиция слева. Все необходимые функции работы с экраном IBM PC имеются в библиотеке компилятора, их прототипы находятся в файле conio.h. Рассмотрим некоторые из этих функций:
void clrscr( void ); - осуществляет стирание экрана;
void gotoxy( int x, int y ); - перемещает курсор в позицию x строки y;
void cprintf( char *format, ... ); - выполняет то же самое, что и printf, но выводит информацию, используя установленный цвет фона и цвет символа;
void textcolor( int color ); - установка цвета символа с кодом color;
void textbackground( int color ); - установка цвета фона с кодом color;
Последние функции не изменяют цвет уже выведенных символов. Их влияние распространяется на все последующие выводы с помощью функции cprintf.
При установке цвета допускается использовать шестнадцать цветов символа с кодами 0...15, и восемь цветов фона с кодами 0...7. Для удобства работы с цветами в conio.h определены мнемонические имена для цветов:
enum COLORS {
/* цвета для символов и фона */
BLACK /* черный */, BLUE /* синий */,
GREEN /* зеленый */, CYAN /* салатовый */,
RED /* красный */, MAGENTA /* малиновый */,
BROWN /* коричневый */, LIGHTGRAY /* светло-серый */,
/* цвета только для символов */
DARKGRAY /*темно-серый */, LIGHTBLUE /* ярко-синий */,
LIGHTGREEN /*ярко-зеленый*/, LIGHTCYAN /*ярко-салатовый*/,
LIGHTRED /*ярко-красный*/, LIGHTMAGENTA /*ярко-малиновый*/,
YELLOW /* желтый */, WHITE /* белый */ };
Приведенная ниже программа вычисляет функции sin(x), cos(x) и tan(x) в зависимости от выбора пользователя. Выбор осуществляется с помощью светового меню. Движение светового окна организуется путем перерисовки пункта меню другим цветом фона. Основной цикл этой программы управляет изменением переменной choice, в которой хранится текущий выбор пользователя.
#include <stdio.h>
#include <math.h>
#include <conio.h>
#include "keyboard.h"
#define N 4
#define ROW 10
#define COL 35
#define TEXT_C WHITE
#define TEXT_BG BLACK
#define CHOICE_BG LIGHTGRAY
void out_str( int num, int bg_color );
/* Организация светового меню */
void main ( void )
{
int loop; /* Флаг конца работы */
int choice; /* Текущий выбор пункта меню */
int old_choice; /* Старый выбор пункта меню */
double fun, x; /* Значения функции и аргумента */
int i;
textbackground( TEXT_BG ); textcolor( TEXT_C );
clrscr(); gotoxy( COL, ROW-1 );
cprintf( "Аргумент x=" ); scanf( "%lf", &x );
/* Начальный вывод всех пунктов меню */
i=1; while( i <= N ) { out_str( i, TEXT_BG ); i++; }
loop = 1; choice = 1; old_choice = 2;
while ( loop )
{
out_str( old_choice, TEXT_BG );
out_str( choice, CHOICE_BG );
old_choice = choice;
switch( GetCh() )
{
case kbUp :
if ( choice>1 ) choice--; else choice = N; break;
case kbDown :
if ( choice<N ) choice++; else choice = 1; break;
case kbEnter :
switch ( choice )
{
case 1 : fun=sin(x); break;
case 2 : fun=cos(x); break;
case 3 : fun=tan(x); break;
case 4 : loop=0; continue;
}
textbackground( TEXT_BG ); gotoxy( COL-5, ROW+6 );
cprintf( "Значение функции %lf\n", fun );
}
}
}
/* Функция вывода строки меню с указанным цветом фона */
void out_str( int num, int bg_color )
{
textbackground( bg_color ); gotoxy( COL, ROW+num );
switch( num )
{
case 1: cprintf( "1. sin(x) " ); break;
case 2: cprintf( "2. cos(x) " ); break;
case 3: cprintf( "3. tan(x) " ); break;
case 4: cprintf( "4. Конец работы" ); break;
}
}
На аналогичных принципах могут быть построены более сложные диалоговые программы, имеющие разнообразные вертикальные, горизонтальные, вложенные и выпадающие меню, управляемые световым окном.
9. Массивы. Адресная арифметика языка Си
Массив - это совокупность элементов данных одного и того же типа, объединенных общим именем и расположенных в непрерывной области памяти ЭВМ так, что к каждому элементу массива можно получить доступ, зная его порядковый номер или индекс.
9.1 Описание массива
Описание массива производится с помощью обычного оператора описания, при этом за именем массива в квадратных скобках должна быть записана целая положительная константа или константное выражение, равное размеру этого массива, то есть максимально возможному числу элементов. Например:
int a[100], ab[2*40];
double c[200], speed[100];
char name[20];
Имя массива без квадратных скобок за ним имеет значение, равное адресу первого элемента этого массива.
Имя массива с квадратными скобками, в которых записано индексное выражение целого типа, обозначает значение соответствующего элемента массива. В языке Си нумерация элементов массива начинается с нуля, то есть для массива d из пяти элементов допустимы следующие обозначения: d[0], d[1], d[2], d[3], d[4].
Индексированные переменные могут использоваться в любых выражениях в тех местах, где допускается применение переменных соответствующих типов.
При работе с индексированными переменными необходимо внимательно следить за тем, чтобы индексы не вышли из допустимого диапазона, определяемого описаниями массивов. Дело в том, что компилятор не проверяет факт выхода индексов за границы массива, а при ошибочном занесении данных за пределы массива может запортиться нужная информация и, скорее всего, компьютер зависнет.
9.2 Ввод-вывод массива
Язык Си не имеет встроенных средств для ввода-вывода массива целиком, поэтому массив вводят и выводят поэлементно с помощью циклов, как, например, в следующей программе:
#include <stdio.h>
void main(void)
{
double a[100]; int n, i;
printf("Введите количество чисел n = ");
scanf("%d", &n);
if( n>(sizeof a)/sizeof(double) )
{ printf("Слишком много элементов\n"); return; }
for(i=0; i<n; i++)
{
printf("a[%d] = ", i); scanf("%lf", &a[i]);
}
/* Операторы, обрабатывающие массив */
}
Во многих случаях удобно возложить на программу подсчет числа элементов, вводимого массива, при этом ввод завершается при появлении во входном потоке признака конца данных. Таким признаком в следующей программе служит число большее 1.0e300
#include <stdio.h>
void main(void)
{
double a[100], temp; int n, end;
for(end=n=0; n<(sizeof a)/sizeof(double); n++)
{
printf("a[%d] = ", n); scanf("%lf", &temp);
if( temp>=1.0e300 ) { end=1; break; }
a[n] = temp;
}
if( end )
{
/* Операторы, обрабатывающие массив */
}
else
printf("Переполнение массива\n");
}
Вывод массива, содержащего большое количество элементов желательно производить в несколько строк с остановом после заполнения экрана.
Следующий фрагмент программы выводит массив строками по 5 элементов. После вывода 120 элементов программа останавливается для просмотра выдачи. Очередные 120 элементов выводятся после нажатия на любую клавишу.
for (i=0; i<n; i++)
{
printf("%10.3lf ", a[i]);
if( (i+6) % 5 == 0 ) printf("\n");
if( (i+121) % 120 == 0 ) { getch(); clrscr(); }
}
Здесь стандартная функция clrscr() очищает экран.
9.3 Инициализация массива
Инициализация - присвоение значений вместе с описанием данных. Ранее была рассмотрена инициализация простых переменных, например:
int a = 5;
Для инициализации массива за его именем располагают знак присваивания и список инициализации, который представляет собой заключенные в фигурные скобки и разделенные запятыми инициализирующие значения. Ниже приведен пример инициализации массива:
int a[4] = { 15, 21, 1, 304 };
индексы элементов -> 0 1 2 3
Констант в списке инициализации должно быть не больше, чем объявленный размер массива. Если их меньше, то элементы для которых нет констант обнуляются. Для инициализируемого массива допускается вообще не указывать размер. В этом случае размер массива определяется по количеству констант, например по описанию
int c[] = { 1, 15, 18, 11, 20 };
транслятор выделит 10 байт для хранения массива из 5 двухбайтовых целых чисел.
Частный случай инициализации массива - инициализация строк. Массив символов может быть проинициализирован стандартным образом:
char s[] = { 'A', 'B', 'C', 'D' };
Строка символов дополнительно должна завершаться нуль-символом.
char s[] = { 'A', 'B', 'C', 'D', '\0' };
В связи с тем, что инициализацию строк приходится организовывать довольно часто, язык Си предусматривает для этого упрощенную форму записи:
char s[] = "ABCD";
В этом случае нуль-символ автоматически дописывается в конец строки. Два последних примера инициализации строки совершенно эквивалентны.
9.4 Программа вычисления длины строки символов
В качестве примера использования массива, рассмотрим программу определяющую длину строки символов, вводимой с клавиатуры.
#include <stdio.h>
void main (void)
{
int len;
char str[81];
printf("Введите строку: "); scanf("%s", str);
for(len=0; str[len]; len++);
printf("Длина строки = %d\n", len);
}
В этой программе используется цикл for с пустым оператором тела цикла. Цикл будет выполняться до тех пор, пока в строке не встретится нуль-символ, то есть пока выражение str[len] будет отлично от нуля. После окончания цикла переменная len станет равной количеству символов строки str, исключая нуль-символ.
Фрагмент вычисления длины строки можно оформить в виде отдельной функции и затем использовать в разных программах. Эта функция может выглядеть следующим образом:
int StrLen (char str[])
{
int len;
for(len=0; str[len]; len++);
return len;
}
При наличии функции StrLen два последних оператора предыдущей программы можно заменить одним
printf("Длина строки = %d\n", StrLen(str));
9.5 Двумерные массивы (массивы массивов)
Элементом массива может быть в свою очередь тоже массив. Таким образом, мы приходим к понятию двумерного массива или матрицы. Описание двумерного массива строится из описания одномерного путем добавления второй размерности, например: int a[4][3];
Анализ подобного описания необходимо проводить в направлении выполнения операций [], то есть слева направо. Таким образом, переменная a является массивом из четырех элементов, что следует из первой части описания a[4]. Каждый элемент a[i] этого массива в свою очередь является массивом из трех элементов типа int, что следует из второй части описания.
Для наглядности двумерный массив можно представить в виде таблицы с числом строк, равным первому размеру массива, и числом столбцов, равным второму размеру массива, например:
Массив а |
Столбец 0 |
Столбец 1 |
Столбец 2 |
|
Строка 0 |
18 |
21 |
5 |
|
Строка 1 |
6 |
7 |
11 |
|
Строка 2 |
30 |
52 |
34 |
|
Строка 3 |
24 |
4 |
67 |
Имя двумерного массива без квадратных скобок за ним имеет значение адреса первого элемента этого массива, то есть значение адреса первой строки - одномерного массива из трех элементов. При использовании в выражениях тип имени двумерного массива преобразуется к типу адреса строки этого массива. В нашем примере тип имени массива a в выражениях будет приведен к типу адреса массива из трех элементов типа int и может использоваться во всех выражениях, где допускается использование соответствующего адреса.
Имя двумерного массива с одним индексным выражением в квадратных скобках за ним обозначает соответствующую строку двумерного массива и имеет значение адреса первого элемента этой строки. Например, в нашем случае a[2] является адресом величины типа int, а именно ячейки, в которой находится число 30, и может использоваться везде, где допускается использование адреса величины типа int.
Имя двумерного массива с двумя индексными выражениями в квадратных скобках за ним обозначает соответствующий элемент двумерного массива и имеет тот же тип. Например, в нашем примере a[2][1] является величиной типа int, а именно ячейкой, в которой находится число 52, и может использоваться везде, где допускается использование величины типа int.
В соответствии с интерпретацией описания двумерного массива (слева-направо) элементы последнего располагаются в памяти ЭВМ по строкам.
Инициализация двумерного массива также проводится по строкам, например, для того чтобы получить вышеописанный массив a, можно было бы провести следующую инициализацию
int a[][3] = {
{ 18, 21, 5 },
{ 6, 7, 11 },
{ 30, 52, 34 },
{ 24, 4, 67 }
};
Здесь первый размер массива будет определен компилятором. Следует отметить, что второй размер массива должен быть всегда указан. Это необходимо для того, чтобы сообщить компилятору размер строки массива, без которого компилятор не может правильно разместить двумерный массив в памяти ЭВМ.
Для инициализации двумерного массива символов можно использовать упрощенный синтаксис инициализации строк:
char s[][17] = {
"Строка 1",
"Длинная строка 2",
"Строка 3"
}
Размер памяти заказанный под каждую строку в этом случае должен быть равным длине самой длинной строки с учетом нуль-символа. При этом, для части строк (строка 1 и строка 3) будет выделено излишнее количество памяти. Таким образом, хранение строк различной длины в двумерном массиве символов недостаточно эффективно с точки зрения использования памяти.
Ввод двумерного массива осуществляется поэлементно с помощью двух вложенных циклов. Следующий фрагмент программы предназначен для ввода по строкам двумерного массива элементов типа double размером n строк на m столбцов
for (i=0; i<n; i++)
for (j=0; j<m; j++)
{
printf("a[%d][%d] = ", i, j);
scanf ("%lf", &a[i][j]);
}
Для ввода массива по столбцам достаточно поменять местами строки программы, являющиеся заголовками циклов.
Вывод такого же двумерного массива иллюстрирует следующий фрагмент:
for (i=0; i<n; i++)
{
for (j=0; j<m; j++) printf ("%9.3lf ", a[i][j]);
printf("\n");
}
В данном фрагменте после вывода очередной строки массива осуществляется переход на следующую строку дисплея.
В языке Си допускается использовать не только двумерные, но и трехмерные, четырехмерные и т. д. массивы. Их использование ничем принципиально не отличается от использования двумерных массивов, однако на практике они применяются значительно реже.
9.6 Адресная арифметика языка Си
Язык Си имеет средства работы непосредственно с областями оперативной памяти ЭВМ, задаваемыми их адресами (указателями). В языке C указатели строго типизированы, т. е. различают указатели (адреса) символьных, целых, вещественных величин, а также типов данных, создаваемых программистом.
Для описания указателя на какой-либо тип данных перед именем переменной ставится *. Например в строке int *a, *b, c, d; описываются два адреса и две переменные целого типа. В строке double *bc;
описан адрес переменной вещественного типа. Никогда не следует писать знак * слитно с типом данных, например как в следующей строке: int* a, b;
В этой строке создается ложное впечатление о том, что описаны два указателя на тип int, в то время как на самом деле описан один указатель на int, а именно a, и одна переменная типа int.
Описание переменных заставляет компилятор выделять память для хранения этих переменных. Описание указателя выделяет память лишь для хранения адреса. В этом смысле указатель на целое данное и на тип double будут занимать в ЭВМ одинаковое количество байт памяти, зависящее от модели памяти, на которую настроен компилятор. Например, в 16-ти разрядной Small модели длина указателя равна двум байтам, а в 16-ти разрядной Large модели - четырем.
При описании указателей в качестве имени типа данных можно использовать ключевое слово void, например
void *vd;
При таком описании с указателем не связывается никакой тип данных, то есть получаем указатель на данное произвольного типа.
Для указателей одного и того же типа допустимой является операция присваивания, кроме того указателю типа void может быть присвоено значение адреса данного любого типа, но не наоборот, например
int *a, *b;
double *d;
void *v;
...
a = b; /* Правильно */
v = a; /* Правильно */
v = d; /* Правильно */
b = v; /* Неправильно */
d = a; /* Неправильно */
В случае неправильного присваивания указателей компиляторы обычно выдают предупреждающие сообщения, которыми никогда не следует пренебрегать. Например, компилятор фирмы Borland выдает сообщение:
"Suspicious pointer conversion", которое переводится как "Подозрительное преобразование указателей".
Если по какой-либо причине необходимо выполнить операцию присваивания между указателями разного типа, то следует использовать явное преобразование типов, например для указателей из предыдущего примера
b = (int *) v;
d = (double *) a;
При этом ответственность за корректность подобных операций целиком ложится на программиста. Действительно, в предыдущем примере a является указателем на ячейку памяти для хранения величины типа int. Обычно это ячейка размером 2 байта. После присваивания указателей с явным преобразования типов, делается возможным обращение к этой ячейке посредством указателя d, как к ячейке с величиной типа double. Размер этого типа обычно 8 байт, да и внутреннее представление данных в корне отличается от типа int. Никакого преобразования самих данных не делается, ведь речь идет только об указателях. Дальнейшая работа с указателем d скорее всего заденет байты, соседние с байтами на которые указывает a. Результат интерпретации этих байт будет тоже неверным.
Для поддержки адресной арифметики в языке Си имеются две специальные операции - операция взятия адреса & и операция получения значения по заданному адресу * (операция разадресации).
Операция & может применяться к любому объекту программы, адрес которого в принципе может быть определен. Результатом операции является указатель того же типа, что и тип объекта, имеющий значение адреса объекта. Если эту операцию применить к указателю, то результатом будет адрес ячейки памяти, в которой хранится значение указателя. Результат операции & можно использовать в любом выражении, где допускается использование указателя соответствующего типа.
Операция * может применяться только к указателям и только в том случае когда они типизированы. При необходимости применить эту операцию к указателю типа void следует использовать явное преобразование типов. Результатом операции * является значение того объекта, к адресу которого применялась операция *, тип результата совпадает с типом объекта. Результат операции * можно использовать в любом выражении, где допускается использование объекта соответствующего типа.
Рассмотрим работу вышеописанных операций на следующем примере
int *p, a, b;
double d;
void *pd;
p = &a;
*p = 12;
p = &b;
*p = 20;
/* Здесь a содержит число 12, b - число 20 */
pd = &d;
*( (double *) pd ) = a;
/* Здесь d содержит число 12.0 */
Поясним первые четыре присваивания рисунком, в котором прямоугольники изображают ячейки памяти для хранения величин типа int и указателей, внутри которых проставлены значения величин, а над ними записаны их названия и адреса.
Состояние ячеек до первого присваивания
P, адрес 1000 |
a, адрес 2000 |
b, адрес 4000 |
|
мусор |
мусор |
мусор |
Состояние ячеек после присваивания p = &a
p, адрес 1000 |
a, адрес 2000 |
b, адрес 4000 |
|
2000 |
мусор |
мусор |
Состояние ячеек после присваивания *p = 12
p, адрес 1000 |
a, адрес 2000 |
b, адрес 4000 |
|
2000 |
12 |
мусор |
Состояние ячеек после присваивания p = &b
p, адрес 1000 |
a, адрес 2000 |
b, адрес 4000 |
|
4000 |
12 |
мусор |
Состояние ячеек после присваивания *p = 20
p, адрес 1000 |
a, адрес 2000 |
b, адрес 4000 |
|
4000 |
12 |
20 |
Описание указателя не является требованием на выделение памяти для хранения данных. Память выделяется только для хранения адреса. Поэтому прежде, чем использовать указатель, ему нужно присвоить значение адреса реально существующего объекта. В противном случае результат работы программы непредсказуем. Рассмотрим, например, следующую последовательность строк
double *a, b;
b = *a;
*a = 135.7;
В этой последовательности используется указатель, которому предварительно не присвоено никакого значения. В ячейке a находится произвольное значение, возможно оставшееся от работы предыдущей программы. Первая операция присваивания приведет к тому, что переменная b получит значение из ячейки памяти с непредсказуемым адресом. Вторая - к тому, что по непредсказуемому адресу будут записаны 8 байт, являющиеся двоичным представлением числа 135.7. Если эти байты попадут на область данных программы, то программа скорее всего выдаст неправильный результат. Если они попадут на область кода программы или на системную область MS DOS, то в лучшем случае программа аварийно завершится, а в худшем компьютер полностью зависнет.
Если делается попытка присвоить какое-либо значение по адресу указателя, значение которого равно нулю, то многие компиляторы выдают сообщение Null pointer assingment
К сожалению, это сообщение выдается уже после того, как программа завершилась, если она смогла завершиться. Для компилятора фирмы Borland легко можно отследить момент некорректного обращения к памяти. Для этого нужно в окно просмотра значений выражений поместить выражение (char *) 4, затем пошагово выполнять программу до тех пор, пока строка-подпись фирмы Borland в окне просмотра не изменится. Если программа имеет большой размер, то более целесообразно выполнять ее от одной точки останова до другой.
Следует также опасаться случая, когда указатель содержит адрес объекта программы, завершившего свое существование. Например, результат работы следующей программы неверен и непредсказуем:
#include <stdio.h>
#include <math.h>
double * Cube(double x)
{
double cube_val;
cube_val = x*x*x;
return &cube_val;
}
void main(void)
{
double *py;
py = Cube(5);
printf("y1 = %lf\n", *py);
sin(0.7);
printf("y1 = %lf\n", *py);
}
Это происходит потому, что функция Cube() возвращает указатель на локальную переменную cube_val, которая существует только в пределах функции Cube(). После возврата из функции память, ранее распределенная под переменную cube_val, освобождается и может быть использована компилятором для других целей несмотря на то, что значение возвращенного указателя по-прежнему содержит ее адрес.
9.7 Указатели и одномерные массивы
В языке C понятие массива тесно связано с понятием указателя. Действительно, как было описано выше, имя массива представляет собой адрес области памяти, распределенной под этот массив, или иными словами адрес первого элемента массива. Пусть описаны следующие данные: int a[100], *pa;
и осуществлено присваивание: pa = a;
Оно является корректным, поскольку имя a обозначает адрес первого элемента массива a и поэтому имеет тип указателя на int. После этого присваивания
pa[0] или *pa будет обозначать a[0];
pa[1] или *(pa+1) будет обозначать a[1];
pa[2] или *(pa+2) будет обозначать a[2] и т. д. И вообще обозначения вида *(pa+n) и pa[n] являются полностью эквивалентными. Точно также эквивалентны выражения *(a+i) и a[i].
На первый взгляд кажется, что массив и указатель полностью эквивалентны. Однако имеется два существенных отличия массива от указателя:
- массиву при описании выделяется память для хранения всех его элементов, а указателю только для хранения адреса;
- адрес массива навсегда закреплен за именем, то есть имя массива является адресной константой и выражение вида a = pa недопустимо.
Результат прибавления к указателю или вычитания из него целочисленной величины является указателем того же типа, значение которого отличается от значения исходного указателя на число байт, определяемое произведением целочисленной величины на размер данного, которое адресует указатель. Например, если имеются описания
int A[20], *pA = A;
double B[20], *pB = B;
то указатель (pA+3) будет иметь значение на 6 байт больше, чем pA, и будет адресовать элемент A[3] массива A. Указатель (pB+3) будет иметь значение на 24 байта больше, чем pB, и будет адресовать элемент B[3] массива B. С указателями типа void подобные операции выполнены быть не могут, поскольку компилятор не знает размера адресуемого данного.
Для указателей определены операции увеличения и уменьшения на целочисленную величину, как альтернативная форма записи выражений
pA = pA + i; эквивалентно pA += i;
pA = pA - i; эквивалентно pA -= i;
pA = pA + 1; эквивалентно pA++; или ++pA;
pA = pA - 1; эквивалентно pA--; или --pA; При этом, работа префиксных и постфиксных операций ++ и -- совпадает с их работой для арифметических данных.
Указатели допускается использовать в операциях сравнения. При этом всегда возможно сравнение указателя с нулем и сравнение двух однотипных указателей. Однако правильность результата последнего сравнения для 16-ти разрядного режима работы IBM PC гарантируется только в том случае, если сравниваемые указатели являются указателями на элементы одного и того же массива данных или если они предварительно подвергаются нормализации (см. ниже).
Разность двух однотипных указателей представляет собой целочисленную величину равную количеству элементов данных (не байт) между соответствующими адресами памяти. Правильность результата этой операции для 16-ти разрядного режима работы IBM PC тоже гарантируется только в том случае, если указатели имеют значение адресов элементов одного и того же массива данных или если они предварительно подвергаются нормализации (см. ниже).
В следующем фрагменте программы иллюстрируется использование вышеописанных операций
double A[100], *pA, *pA100;
int i;
/* Заполняем массив A. Работаем с массивом */
for (i=0; i<100; i++) A[i]=0;
/* Заполняем массив A. Работаем с указателями */
for (pA=A, pA100=pA+100; pA<pA100; pA++) *pA=11.9;
Последний вариант заполнения массива может оказаться более эффективным.
9.8 Указатели и двумерные массивы
Пусть имеются следующие определения массивов и указателей:
int A[4][2], B[2];
int *p, (*pA)[4][2], (*pAstr)[2];
Здесь A представляет собой двумерный массив из четырех строк и двух столбцов, B - одномерный массив из двух элементов. Для каждого из этих массивов будет выделено соответствующее количество памяти, достаточное для хранения всех их элементов.
Указатель p представляет собой указатель на величину int, указатель pA - указатель на двумерный массив из четырех строк и двух столбцов, pAstr - указатель на одномерный массив из двух элементов. Все указатели имеют размер, равный размеру адреса для данных в используемой модели памяти. Память для хранения данных, естественно, не выделяется. Количество элементов данных из описания массивов будет использовано лишь для корректного изменения значения указателя при выполнении над ним допустимых арифметических операций.
Смысл трактовки этих указателей определяется направлением слева-направо для подряд следующих операций [], а также изменением приоритета операции * с помощью круглых скобок. Если не поставить круглых скобок, то следующее определение
int *pa[4][2];
рассматривается как определение двумерного массива из указателей на тип int.
Для вышеописанных указателей допустимы следующие операции присваивания, поскольку слева и справа от операции присваивания находятся указатели на один и тот же тип данных:
p = B;
p = &B[1];
p = &A[0][0];
p = A[2];
Следующее присваивание:
p = A; /* неверно */
является неверным, так как слева от операции присваивания находится указатель на тип int, а справа - указатель на первый элемент массива A, который (элемент) представляет собой массив из двух элементов типа int. В таких случаях компиляторы выдают предупреждающее сообщение о подозрительном преобразовании указателя.
Если программист уверен в своих действия, то он может использовать операцию явного приведения типа для устранения этого сообщения, но при этом компилятор снимает с себя всякую ответственность за корректность использования такого указателя. Так, после присваивания
p = (int *) A;
элементы, на которые ссылается указатель, и элементы массива A находятся в следующем соответствии:
p[0] эквивалентно A[0][0]
p[1] эквивалентно A[0][1]
p[2] эквивалентно A[1][0]
p[3] эквивалентно A[1][1]
p[4] эквивалентно A[2][0]
p[5] эквивалентно A[2][1]
p[6] эквивалентно A[3][0]
p[7] эквивалентно A[3][1]
Совершенно корректными являются следующие присваивания pAstr = A;
после которого использование массива A и указателя pAstr совершенно эквивалентны: pAstr[i][j] эквивалентно A[i][j]
Присваивание pAstr = &A[2];
устанавливает следующее соответствие между элементами, на которые ссылается указатель pAstr и элементами массива A:
pAstr[0][0] эквивалентно A[2][0]
pAstr[0][1] эквивалентно A[2][1]
pAstr[1][0] эквивалентно A[3][0]
pAstr[1][1] эквивалентно A[3][1]
Следующие присваивания корректны
pA = &A; /* Указатель на двумерный массив */
pAstr = &B; /* Указатель на одномерный массив */
и устанавливают следующее соответствие элементов:
(*pA)[i][j] эквивалентно A[i][j]
(*pAstr)[i] эквивалентно B[i]
Массивы указателей удобны для хранения символьных строк:
char *str[] = {
"Строка 1",
"Строка 2",
"Длинная строка 3"
};
В этом случае каждый элемент массива представляет собой адрес соответствующей строки символов, а сами строки располагаются компилятором в статическом сегменте данных. Никакой лишней памяти, связанной с различной длиной строк, при этом не расходуется.
9.9 Указатели и функции
Функции, как и другие объекты программы, располагаются в памяти ЭВМ. Любая область памяти имеет адрес, в том числе и та, в которой находятся функция. Имя функции без круглых скобок за ним представляет собой константный адрес этой области памяти. Таким образом, имея функции со следующими прототипами:
double sin(double x);
double cos(double x);
double tan(double x);
мы можем в программе использовать имена sin, cos и tan, которые будут обозначать адреса этих функций.
Можно описать и указатель на функцию. Например, для функции с аргументом типа double, возвращающей значение типа double, описание такого указателя будет выглядеть следующим образом: double (*fn)(double x);
Здесь, как и в случае указателя на массив, круглые скобки увеличивают приоритет операции *. Если бы они отсутствовали, то была бы описан не указатель на функцию, а функция, возвращающая значение указателя на double.
После того, как описан указатель на функцию, становятся возможными следующие операции:
fn = sin; /* Настройка указателя на функцию sin */
a = fn(x); /* Вызов функции sin через указатель */
fn = cos; /* Настройка указателя на функцию cos */
b = fn(x); /* Вызов функции cos через указатель */
Можно описать массив указателей на функцию и проинициализировать его:
double (*fnArray[3])(double x) = { sin, cos, tan };
Теперь становится возможным следующий цикл:
for(i=0; i<3; i++)
printf( "F(x) = %lf\n", fnArray[i](x) );
Можно описать функцию возвращающую значение указателя на функцию:
double (*fnFunc(int i)) (double x)
{
switch(i)
{
case 0 : return sin;
case 1 : return cos;
case 2 : return tan;
}
}
Описанная функция имеет параметр типа int и возвращает значение указателя на функцию с аргументом типа double, возвращающую значение типа double.
После описания функции fnFunc становится возможным следующий цикл:
for(i=0; i<3; i++)
printf( "F(x) = %lf\n", fnFunc(i)(x) );
9.10 Оператор typedef
Описания, подобные описаниям предыдущего раздела, достаточно сложны для понимания. Для упрощения описаний сложных типов в языке Си предусмотрен оператор typedef. Его использование иллюстрируется следующим синтаксисом:
БНФ:
typedef описание_одного_имени
Под описанием_одного_имени подразумевается любое, сколь угодно сложное описание данного. Но в этом случае имя будет обозначать не имя данного, а имя нового типа, который соответствует типу данного и может быть использован в качестве имени типа в любых других определениях данных. Рассмотрим пример:
typedef double DArray[100];
...
DArray A, B, C;
Если бы в первом описании отсутствовало бы ключевое слово typedef, то имя DArray представляло бы имя массива из 100 элементов типа double, для которого бы выделялся соответствующий объем памяти. При наличии typedef компилятор будет воспринимать имя DArray как имя нового типа данных, а именно, типа массива из 100 элементов типа double. Очевидно, никакой памяти при этом не выделяется.
Во втором описании используется имя нового типа DArray. Каждое из определяемых имен A, B и C будет считаться массивом из ста элементов типа double, и для каждого из них будет выделен соответствующий объем памяти.
Описания указателей на функции из предыдущего раздела можно существенно упростить, используя оператор typedef:
typedef double (*Fun)(double x); /*Тип указателя*/
Fun fnArray[3] = { sin, cos, tan }; /*Массив функций*/
Fun fnFunc(int i) /* Функция, возвращающая функцию */
{
switch(i)
{
case 0 : return sin;
case 1 : return cos;
case 2 : return tan;
}
}
Совершенно очевидно, что последние описания значительно понятнее.
9.11 Дополнительные описания указателей для IBM PC
Рассмотрим некоторые особенности режимов работы процессоров, используемых в компьютерах IBM PC. При этом следует учитывать, что процессоры фирмы Intel с типом ниже 80386 обеспечивают 16-ти битный режим работы, а процессоры 80386 и выше - как 16-ти, так и 32-битный режимы.
Типичный режим работы процессора - 16-битный, который обеспечивается собственно системой MS DOS, или DOS-сессией эмулируемой 32-битной системой Windows-95, Windows NT или OS/2.
В этом режиме процессор может использовать и двухбайтовые и четырехбайтовые адреса.
При использовании четырехбайтовых адресов процессор в 16-ти битном режиме может адресовать область памяти не более, чем в 1 мегабайт. При этом, адрес состоит из двух частей: так называемой сегментной части, которая находится в старших 2 байтах адреса, и смещения, содержащегося в младших 2 байтах адреса.
Физический адрес памяти компьютера вычисляется процессором по следующей формуле:
Физический_адрес = seg * 16 + offs, где seg - двухбайтовый сегментный адрес, offs - двухбайтовое смещение.
Одно двухбайтовое смещение может адресовать не более, чем 64 килобайта памяти (216), то есть так называемый сегмент. Добавление сегментной части к смещению по вышеприведенной формуле и обеспечивает адресацию 1M памяти. Однако, при такой трактовке адреса различные адреса могут указывать на один и тот же байт памяти. Рассмотрим три адреса 246:330, 256:170 и 266:10. И сегментная часть адреса, и смещение в этих адресах записаны в десятичной системе счисления, через двоеточие. Рассчитаем физические адреса для каждого из этих значений:
246 * 16 + 330 = 4266
256 * 16 + 170 = 4266
266 * 16 + 10 = 4266
Из расчета видно, что разные адреса определяют один и тот же физический адрес памяти. Это и объясняет, почему в некоторых случаях сравнение указателей может происходить некорректно. Для устранения этого противоречия вводится понятие нормализованного адреса, то есть такого адреса, у которого значение смещения не превышает 16. Последний из трех адресов примера - нормализованный.
Если для всех данных и кода программы зафиксировать сегментную часть адреса, то для адресации достаточно 2-х байтового адреса, состоящего из одного смещения. При этом, размер адресуемого пространства не может превысить 64 килобайта.
Для обозначения соответствующих адресов, используются специальные ключевые слова: near - обозначает 2-х байтовый (близкий) адрес, far - 4-х байтовый (дальний) адрес.
Все действия над адресами типа far выполняются так, что их сегментная часть не меняется. Это позволяет несколько ускорить операции с указателями, но накладывает ограничение в 64K на массив, адресуемый указателем. Если массив должен быть больше 64K, то следует использовать указатели типа huge, которые автоматически поддерживают нормализацию адреса и, поэтому, могут адресовать массив больший 64K.
Примеры описания подобных указателей:
int near *pi; /* 2-х байтовый указатель */
char far *name; /* 4-х байтовый без нормализации */
double huge *pA; /* 4-х байтовый с нормализацией */
Использовать вышеприведенные описатели указателей можно только при полной уверенности в своих действия. Значительно более просто использовать различные типы адресов, меняя модели памяти. Рассмотрим 16-разрядные модели памяти IBM PC.
В крошечной (Tiny) модели памяти сегментные части всех адресов указывают на один и тот же сегмент, размером не более 64K, в котором располагается и код программы, и данные, и стек. Все адреса двухбайтовые (near).
В маленькой (Small) модели памяти сегментная часть адреса кода указывает на один сегмент, размером не более 64K, сегментная часть адресов данных указывает на другой сегмент, размером не более 64K, в котором располагаются данные и стек. Все адреса двухбайтовые (near).
В средней (Medium) модели памяти адреса кода 4-х байтовые (far), то есть размер кода может достигать 1M. Сегментная часть адресов данных указывает на сегмент, размером не более 64K, в котором располагаются данные и стек. Адреса данных двухбайтовые (near).
В компактной (Compact) модели памяти адреса кода 2-х байтовые (near), то есть размер кода не может превышать 64K. Адреса данных 4-х байтовые (far), то есть размер данных может достигать 1M. Однако, максимальный размер статических данных и стека не превышает 64K. По умолчанию стек устанавливается значительно меньше, например 4K.
В большой (Large) модели памяти все адреса 4-х байтовые (far), то есть и размер кода, и размер данных может достигать 1M. Однако, как и в предыдущей модели, максимальный размер статических данных и стека не превышает 64K. По умолчанию стек устанавливается размером 4K.
В громадной (Huge) модели памяти все организовано так же как и в большой, но размер статических данных может достигать 1M.
Следует обратить внимание на то, что ни в одной модели памяти (даже в huge) нет указателей на данные типа huge. Поэтому работа с массивами большими 64K требует специального описания указателей.
В 32-битных режимах работы 386 процессоров far-адрес состоит из 2-х байтового селектора сегмента и 4-х байтового смещения в сегменте. При этом размер смещения позволяет адресовать 4-х гигабайтное адресное пространство (232).
Для 32-х разрядных режимов могут существовать все вышеперечисленные модели памяти. Однако на практике чаще всего используется так называемая плоская (flat), безсегментная модель памяти. На самом деле она соответствует модели Small с учетом того, что размер сегмента может достигать 4 гигабайт, а смещение в сегменте имеет размер 4 байта. Можно считать, что во flat модели сегментов нет вообще, размер адреса равен 4-м байтам и соответствует физическому адресу (виртуальному) памяти компьютера.
9.12 Непосредственная работа с экранной памятью
В качестве примера использования указателей различных типов рассмотрим ряд функций, предназначенных для вывода текстовой информации непосредственно в экранную память компьютера. Эти функции работают быстрее соответствующих функций из стандартных библиотек.
Рассмотрим принцип организации экранной памяти для вывода информации на экран в текстовом режиме. Каждому знакоместу экрана соответствует слово памяти (2 байта). Младший байт содержит код символа, старший атрибут символа, устанавливающий его цвет. В свою очередь, атрибут символа в старших четырех битах содержит цвет фона (самый старший бит может устанавливать режим мигания), а в младших четырех битах - цвет символа. Вычислить атрибут символа, зная цвет фона - fon и цвет символа sym, можно по следующей формуле:
attr = fon * 16 + sym;
attr = fon << 4 | sym;
Таким образом, каждой строке экрана соответствует 160 байт экранной памяти. Экранная память начинается с адреса 0xB8000000L. Буква L в конце адресной константы необходима для того, чтобы компилятор создал константу длиной 4 байта.
Ниже приведен текст функции, выводящей строку символов, на которую указывает указатель str, начиная с колонки col из строки row экрана. При выводе строки используется атрибут attr.
void PutsXY (int col, int row, char *str, int attr)
{
char far *p = (char far *) 0xB8000000L;
p+= 160*(row-1)+2*(col-1); /* Начало строки */
while (*str)
{
*(p++) = *(str++); /* Вывод символа */
*(p++) = attr; /* Вывод атрибута */
}
}
Единственное замечание, которое следует сделать, заключается в том, что для адресации экранной области памяти используется дальний (far) указатель на символы. Такой указатель необходимо использовать для того, чтобы независимо от того, в какой 16-разрядной модели памяти создается программа (даже в Tiny), была возможной адресация этой памяти.
Кроме того, следует обратить внимание на то, что целочисленную константу необходимо явно преобразовать к типу длинного указателя на символы. В противном случае компилятор будет считать инициализацию некорректной.
Подобным образом можно организовать функцию, которая окрашивает прямоугольную полосу длиной len согласно заданному атрибуту attr с указанной позиции экрана. Окрашивание реализуется путем вывода новых атрибутов в соответствующие места экранной памяти:
void PutAttr (int col, int row, int len, int attr)
{
char far *p = (char far *) 0xB8000000L;
p+= 160*(row-1)+2*(col-1)+1;
while (len--)
{
*p = attr; /* Вывод атрибута */
p += 2; /* Переход к следующему атрибуту */
}
}
Рассмотренную функцию можно использовать для организации всякого рода световых меню.
10. Дополнительные сведения о функциях
10.1 Области видимости и глобальные данные
Любые имена программы на языке Си могут быть объявлены либо вне всех функций и блоков (то есть, на уровне файла), либо внутри функции или блока. В связи с этим различают файловую или глобальную область видимости (действия) имен и локальную область видимости (область видимости блока или функции).
Объявленные на уровне файла функции и данные видны от точки объявления до конца файла и могут быть использованы в любых функциях и блоках, находящихся в том же файле после соответствующих объявлений. Это же относится и к макросам препроцессора.
Имена, объявленные в блоке, видны и могут быть использованы только в этом блоке, причем, в случае конфликта имен, локальное объявление данных создает совершенно новую переменную и перекрывает глобальное объявление или объявление более высокого уровня. После выхода из блока становится доступным старое имя с объявлением более высокого уровня. Это не относится к макросам препроцессора, которые ведут себя, как и при файловом определении (препроцессор не обязан знать синтаксис и семантику языка программирования).
Подобные документы
Цели и задачи дисциплины "Технология программирования". Программные средства ПК. Состав системы программирования и элементы языка. Введение в систему программирования и операторы языка Си. Организация работы с файлами. Особенности программирования на С++.
методичка [126,3 K], добавлен 07.12.2011Понятие математического программирования. Класс как тип структуры, позволяющий включать в описание типа не только элементы данных, но и функции. Рассмотрение основных особенности языка программирования C++. Характеристика среды MS Visual Studio 2008.
контрольная работа [318,0 K], добавлен 13.01.2013Особенности способов описания языков программирования. Язык программирования как способ записи программ на ЭВМ в понятной для компьютера форме. Характеристика языка Паскаль, анализ стандартных его функций. Анализ примеров записи арифметических выражений.
курсовая работа [292,0 K], добавлен 18.03.2013История создания языка Java. Основные принципы объектно-ориентированного программирования. Структура, особенности синтаксиса и примеры прикладных возможностей использования языка Java, его преимущества. Перспективы работы программистом на языке Java.
курсовая работа [795,9 K], добавлен 14.12.2012Изучение способов организации консольного ввода/вывода данных в режиме черного экрана. Ознакомление со стандартными типами данных (целый, вещественный, символьный, логический) и методами описания переменных, использующихся на языке программирования С++.
презентация [2,2 M], добавлен 17.04.2010Изучение теоретических основ разработки программы и правил выбора языка программирования. Рассмотрение основных задач по созданию сайта автоклуба. Основы разработки базы данных, создания web-дизайна, текстового наполнения сайта и его публикации.
курсовая работа [687,9 K], добавлен 07.04.2014Изучение общей структуры языка программирования Delphi: главные и дополнительные составные части среды программирования. Синтаксис и семантика языка программирования Delphi: алфавит языка, элементарные конструкции, переменные, константы и операторы.
курсовая работа [738,1 K], добавлен 17.05.2010Рассмотрение и ознакомление с одним из наиболее используемых языков программирования - С++. Его применение в процессе работы со строковыми типами данных и символами. Исследование кодов написания программ в режиме разработки консольного приложения.
курсовая работа [6,1 M], добавлен 20.01.2016Ознакомление с ситуацией распространения на рынке языков программирования. Определение плюсов и минусов Pascal, C++, VBA. Сравнение и анализ синтаксиса программ на основе одной задачи. Выявление лучшего языка для освоения первоначальных навыков.
курсовая работа [1022,0 K], добавлен 13.10.2014Исследование базовых концепций программирования приложений под операционную систему Windows. Изучение истории создания универсального языка программирования Си. Разработка графического пользовательского интерфейса. Обзор правил игры и алгоритма работы.
курсовая работа [58,2 K], добавлен 09.11.2012