Поразрядные операции и операции сдвига над целочисленными данными
Целочисленные данные можно подвергать операции сдвига как влево (знак операции – <<), так и вправо (знак операции – >>) на заданное количество двоичных разрядов:
y=x<<3; // сдвиг влево на три двоичные разряда
z=y>>5; // сдвиг вправо на пять двоичных разрядов
Операция сдвига вправо работает по-разному для целых чисел со знаком и целых чисел без знака. Внимательно посмотрите на результат работы следующей программы:
#include <stdio.h>
#include <conio.h>
int main()
{
int x=5, y=-5;
unsigned z=0xFFFFFFFB;
printf("x=%x y=%x z=%x",x,y,z);
printf("\nx<<2=%x x>>2=%x",x<<2,x>>2);
printf("\ny<<2=%x y>>2=%x",y<<2,y>>2);
printf("\nz<<2=%x z>>2=%x",z<<2,z>>2);
getch();
return 0;
}
//=== Результат работы ===
x=5 y=fffffffb z=fffffffb
x<<2=14 x>>2=1
y<<2=ffffffec y>>2=fffffffe
z<<2=ffffffec z>>2=3ffffffe
Дело в том, что для чисел со знаком операция сдвига на n разрядов эквивалентна умножению на 2n при сдвиге влево или делению на 2n при сдвиге вправо. Поэтому для отрицательного операнда результат сдвига должен остаться отрицательным. С этой целью при сдвиге вправо производится размножение знакового разряда.
Билет 21а
Имена массивов в качестве указателей. Приведенный индекс в двумерных массивах. Почему в Си отказались от функций Low и High?
Если целочисленному указателю p1 присваивается имя массива a или его адрес, то это эквивалентно засылке в p1 адреса первого элемента массива a[0]:
int a[10];
int *p1=a; //p1 смотрит на начало массива a
int *p2=&a[0]; //p2 тоже смотрит на начало массивa a
int *p3=(int *)&a; //p3 тоже смотрит на начало массивa a
Когда указатель p1 "смотрит" на переменную x, то по значению указателя можно извлечь значение переменной x или изменить его:
int x=5,y;
int *p1=&x; //значением p1 является адрес x
..........
y=*p1; //теперь значение переменной y равно 5
*p1=2; //теперь значение переменной x равно 2
Когда указатель p2 "смотрит" на начало массива q, то доступ к элементам этого массива можно организовать одним из следующих способов:
int q[20];
int *p2=q;
...........
y=*(p2+5); //теперь y=q[5]
x=p2[3]; //теперь x=q[3]
*(p2+1)=7; //теперь q[1]=7
Основные операции, чаще всего применяемые к указателям – сложение указателя с целым числом или вычитание из указателя целого числа. Обе они широко применяются в том случае, когда указатель связан с массивом. По сути дела, эти операции эквивалентны аналогичным процедурам над индексами элементов массива. И точно так же как прибавление к индексу означает переход к следующему элементу массива, прибавление 1 к указателю означает увеличение его текущего значения на количество байт, соответствующих типу указателя (точнее, типу данных, на которые указатель обязан смотреть):
int q[6]={1,2,3,4,5,6};
int *p = &q[3];
cout << *p++ <<endl; //сначала выводится 4, потом p=&q[4]
cout << (*p)++ <<endl; //сначала выводится 5, потом p=&q[5]
cout << *(p++) <<endl; //сначала выводится 6, потом p=&q[6]
Если при обработке некоторого массива используются два указателя p1 и p2, продвигаемые навстречу друг другу, то их разность (p2-p1) определяет количество элементов массива расположенных между этим двумя адресами.
Работу с двумерными массивами можно организовать двумя способами. Во-первых, операции над элементами двумерных массивов можно свести к операциям над одномерными массивами, используя приведенные индексы. Во-вторых, можно воспользоваться указателями на строки матрицы (как известно, имя массива одновременно является указателем на ее первую строку). Мы приведем примеры программ, демонстрирующие оба подхода.
Пример 9.10. Формирование единичной матрицы с приведенными индексами.
#include <stdio.h>
#include <math.h>
#include <conio.h>
void eye(int *a, int n)
{ int i,j;
for(i=0; i<n; i++)
for(j=0; j<n;j++)
{ if(i==j) a[i*n+j]=1;
else a[i*n+j]=0;
}
}
void main()
{ int i,j,v[5][5];
eye((int*)v,5);
for(i=0;i<5;i++)
{ for(j=0;j<5;j++)
printf("%3d",v[i][j]);
printf("\n");
}
getch();
}
Массив указателей p на строки двумерного массива v может быть сформирован и другими способами:
int *p[4]={(int *)v,(int *)(v+1),(int *)(v+2),(int *)(v+3)};
int *p[4]={v[0],v[1],v[2],v[3]};
int *p[4]={*v,*(v+1),*(v+2),*(v+3)};
Очевидно, что указатель p[0] "смотрит" на элемент v[0][0]. Поэтому указатель p[0][1]=p[0]+1 "смотрит" на элемент v[0][1], указатель p[0][2] – на элемент v[0][2] и т.д. Можно было бы видоизменить заголовок функции transp следующим образом:
void transp(int **p,int n)
Все эти модификации ничего не меняют в алгоритмах работы программ.
Функции Low и High
При работе с массивами вам часто придется пользоваться функциями Low и High. Как я уже говорил, массив может быть объявлен с произвольными нижней и верхней границами. Функция low возвращает нижнюю границу массива, а функция High — его верхнюю границу
1.6.2. Отличия языка C++ от Паскаля
Как показывает практика, язык С++ удобно изучать вторым - после Паскаля. Во-первых, Паскаль - более дисциплинирующий язык, в нем сложно создать неоднозначные конструкции. Во-вторых, в С++, в силу особенностей синтаксиса, довольно рано приходится разбираться в ссылках и указателях, а эти темы бывают сложны для студентов.
При этом в изучении Паскаля совсем не обязательно доходить до динамических структур данных и объектно-ориентированного программирования, достаточно основных навыков составления программ.
Для облегчения перехода от Паскаля к С++ вкратце перечислю основные отличия этих языков.
1. Компилятор Паскаля однопроходный.
Компилятор С++ двухпроходный. Первый проход его принято называть препроцессором, он формирует окончательную структуру текста программы и обрабатывает собственные директивы (в коде они начинаются символом "#"), например:
#define MAX(a,b) ((a)>(b))?(a):(b)
#include <stdio.h>
Подробнее о директивах препроцессора см. соответствующий раздел.
В силу наличия препроцессора компилятор языка С++ работает значительно медленнее аналогичного компилятора Паскаля.
2. Стандарт Паскаля - блочный язык. В Turbo (Borland) Pascal используются элементы модульности, однако подпрограммы по-прежнему могут вкладываться друг в друга.
Программы на языке С++ имеют модульную структуру, т.е. все функции (подзадачи) программы независимы и равноправны, за исключением функции main(), которая первой вызывается при запуске программы.
3. Для описания внешнего интерфейса (то есть доступных элементов) модуля в Паскале используется раздел interface. При помощи этого раздела модули Паскаля в составе проекта связываются между собой.
Для связи файлов программы в С++ используются так называемые заголовочные файлы (хидеры, headers) "*.h". Обычно они содержат внутри себя объявления переменных и функций, использующихся в соответствующем файле программы, в том числе внешних.
Подробнее об использовании заголовочных файлов говорится в соответствующем разделе.
Обратите внимание: интерфейс модуля в Паскале содержит элементы, которые будут доступны вне модуля. Хидер в С++ содержит элементы любых модулей, которые будет использовать файл, подключивший этот хидер.
4. Паскаль - язык с нежестким контролем регистра символов. Идентификаторы "MyName", "myname" и "myNAME" в нем считаются одинаковыми.
Синтаксис С++ учитывает регистр символов. Возможно, это дает программисту некоторые преимущества, однако чаще мы сталкиваемся с затруднениями:
Велика вероятность опечататься при наборе идентификатора, а это приведет к ошибке на стадии компиляции. Например, в программе есть переменная "Len", а в одном месте она введена, как "len".
Если в системе объявлены переменные, отличающиеся только регистром символов, то случайная опечатка при наборе приведет к возникновению семантической (смысловой) ошибки на стадии выполнения. Поэтому не стоит называть разные переменные одной программы именами "Len", "len" и "LEN", хотя компилятор и будет считать их различными.
Чувствительность к регистру заимствуют и все производные от С языки: PHP, ActionScript, JavaScript и др.
5. Паскаль - язык со строгим синтаксисом. Например, переменные в подпрограмме объявляются строго перед началом реализации. Любое несоответствие структуры выражения синтаксису языка вызывает синтаксическую ошибку.
С++, по сравнению с Паскалем, имеет гораздо более свободный синтаксис выражений. Однако, проблема заключается в том, что логические ошибки в структуре выражений часто приводят не к синтаксическим, а к семантическим ошибкам. Последние же, как известно, намного сложнее в отладке.
6. В языке Паскаль различаются процедуры и функции. Первые не возвращают в вызывающую часть программы ничего, вторые возвращают единственное значение.
В С++ не существует понятия процедуры. Любая подпрограмма - это функция, после выполнения она должна вернуть значение в вызывающую часть программы. Однако позже мы увидим, что функция может вернуть пустое значение, а может вызываться как процедура.
В производных от С языках понятие процедуры также отсутствует.
7. В Паскале существуют отдельные строковый и логический типы, Значения типа char в нем - символы.
В С++ строкового и логического типов не существует, а символьный тип является целочисленным, причем может быть знаковым.
8. В Паскале переменные, константы, типы объявляются строго в соответствующих разделах описания: var, const, type. Эти разделы располагаются до начала реализации подпрограммы или программы.
В С++ переменные, константы, типы можно объявлять в любом месте программы.
Главное отличие между языками Си и Паскаль состоит в том, что
Паскаль выполняет гораздо больше строгих проверок. Эти проверки
гарантируют: что при вызове передается правильное количество и
типы параметров; что значения функции используются способом, со-
ответствующим их типу и т.д. Таким образом, в отличие от языка
Си, в языке Паскаль программа должна быть объявлена либо процеду-
рой (подпрограмма, которая не возвращает значения), либо функци-
ей.
Язык Паскаль также обеспечивает использование автоматических
переменных для памяти локальных данных. Как и в языке Си, в языке
Паскаль нет стандартных решений о назначении порядка локальных
переменных в стеке. Также, как и в языке Си, в Паскале память для
локальных переменных распределяется при вершине стека на входе в
вызываемую программу. Если процедура MyProc использует локальные
переменные LocIndx, LocChar и LocWord, то они должны будут ссы-
латься так, как показано в структуре StackFrame листинга 2-8.
Паскаль эквивалентен программе, похожей на ниже приведенную:
procedure MyProc (Param1, Param2, Param3 : integer) ;
var
LocIndx, LocWord : integer ;
LocChar [1..14] : character ;
begin
...
Из листинга 2-4 можно увидеть, что в отличие от языка Си,
язык Паскаль помещает свои аргументы в порядке, в котором они
объявляются, слева направо. Причина, по которой этот способ воз-
можен, состоит в том, что компилятор языка Паскаль гарантирует,
что все обращения, выдаваемые программой, обеспечивают правильное
количество и типы аргументов. Язык Паскаль просто не позволяет
передавать переменное количество параметров, поэтому порядок пе-
редачи, используемый в языке Си, не требуется в языке Паскаль.
Следствием строгой проверки вызова, выполняемой языком Пас-
каль, является то, что вызываемая программа всегда получает одно
и то же количество аргументов, позволяющее вызываемой программе
использовать инструкцию RET N для очистки стека, нежели зависеть
от вызывающей программы.
Другим сходством с языком Си является то, что язык Паскаль
обычно передает свои переменные по значению, однако, если требу-
ется, то переменные можно передавать по адресу, используя описа-
ние var (переменная).
8.10. Указатели на функцию и передача их в качестве параметров
Многие обрабатывающие процедуры нуждаются в получении информации о функциях, к которым они должны обратиться за недостающими данными.
Одним из таких примеров является функция вычисления определенного интеграла – кроме параметров-значений, задающих пределы интегрирования, ей необходимо сообщить имя подпрограммы, вычисляющей значения подынтегральной функции. В такой ситуации обычно используют указатель на функцию.
Объявление указателя pf на функцию f(x), аргумент которой и возвращаемое значение имеют тип double, выглядит следующим образом:
double (*pf)(double x);
Оно напоминает прототип функции, в котором имя функции заменено именем указателя, заключенным в круглые скобки.
Простейшая функция, вычисляющая определенный интеграл методом прямоугольников, может быть организована следующим образом:
double int_rect(double a, double b, double (*f)(double x))
{ int i, n=100;
double s=0,h=(b-a)/n;
for(i=0; i<=n; i++) s += f(a+i*h);
return s*h;
}
Последним аргументом процедуры int_rect является указатель на функцию. Поэтому в качестве фактического параметра мы должны подставить имя подынтегральной функции. Таковым, в частности, может быть имя любой стандартной функции из библиотеки math.h:
cout << int_rect(0,M_PI,sin) << endl; //результат= 1.99984
cout << int_rect(0,M_PI,cos) << endl; //результат=-4.18544e-17
В качестве второго примера рассмотрим программу нахождения корня уравнения y=f(x), если известно, что на интервале [x1, x2] эта функция меняет знак. Алгоритм базируется на делении отрезка пополам. В точке xmid=(x1+x2)/2 смотрим знак функции f, который совпадет либо со знаком f(x1), либо со знаком f(x2). Выбираем ту половину отрезка, на концах которой функция принимает разные знаки. Затем исследуем его середину и т.д. Как только длина очередного отрезка станет достаточно малой или значение функции в центре отрезка окажется меньше заданной точности, процесс поиска корня прекращается.
#include <iostream.h>
#include <conio.h>
#include <math.h>
double y(double x) //функция f(x)=x2-4
{ return x*x-4; }
double root(double x1,double x2,double eps,double(*f)(double x))
{ double f12,f1,f2,xmid;
f1=f(x1); f2=f(x2);
if(f1*f2>0)
{ cerr<<"Error: sign(f1)=sign(f2)"; getch(); exit(0); }
while(x2-x1 > eps)
{ xmid=(x1+x2)/2.;
f12=f(xmid);
if(fabs(f12) < eps)
return xmid;
if(f12*f1>0) { x1=xmid; f1=f12; }
else {x2=xmid; f2=f12; }
}
return (x1+x2)/2.;
}void main()
{ cout<<root(0,10,1e-4,y);
getch();
}
//=== Результат работы ===
2.00001