Главная | Обратная связь | Поможем написать вашу работу!
МегаЛекции

Выполнение последовательности операций индексирования и вызова функции




Неявные преобразования

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

На С++ это разрешено, но при выполнении операции присваивания компилятор организует неявное приведение правой части к типу левой части.

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

Результат этого правила при пересылке данных из более короткого числа в длинное иллюстрируется следующим фрагментом программы:

unsigned char T2=253; Байт без знака может хранить числа от 0 до 255.

short int T3= T2; В переменную T3 заносится число 253, для этого старший байт T2 заполняется нулями.

char T1= T2; Число 253 нельзя записать в T1, так как байт, как число со знаком, может принимать только значения от минус 128 до +127. Но двоичный код из одного байта в другой можно записать без искажения, что и выполняется. Записанный код (см. пример в предыдущем пункте) понимается, как число минус три.

T3= T1; Число минус 3, можно записать в T3 без искажения, для этого старший байт T3 заполняется единицами.

При записи вещественного числа в целочисленную переменную, дробная часть не округляется, а отбрасывается. В результате выполнения оператора

char С=-3.7;

в переменную c запишется целое число минус три. Как указывалось ранее, если нужно округлять число до ближайшего целого в сторону уменьшения или увеличения, надо применять функции floor() и ceil(), прототипы которых находятся в заголовочном файле math.h.

В языке С++ указание знакового или беззнакового типа данных влияет на результат операций сравнения. Объявим в следующей программе два числа uA,uB без знака, два числа iA, iB со знаком и сравним их.

#include <stdio.h>

#include <conio.h>

unsigned char uA,uB;

char iA,iB;

Void main(void)

{ clrscr();

uA=253;uB=3;

 if (uA > uB) printf ("\ n А> B "); else   printf ("\ nB > A "); Сравнение чисел без знака определит, что 253 больше, чем 3, то есть, на экран выведется строка A>B.

iA = uA; iB = uB; Те же двоичные коды перепишутся в iA, iB, но теперь это числа со знаком, 3 не изменилось, а 253 рассматривается как отрицательное число –3, поэтому следующий оператор выведет B>A.

if(iA>iB) printf("\n А >B"); else printf("\nB>A");

getch();

}

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

1. Данные тип сhar перед выполнением операции приводятcя к int (даже если это операция с одним или двумя байтами):

char C2, C=-2;

Только простое присваивание

C2= C;

будет выполняться пересылкой байтов. Но уже при выполнении следующего оператора

C2=- C;

значение C сначала преобразуется в целое (из двух или четырех байтов в зависимости от платформы). Потом вычислится противоположное ему значение и уже после этого младший байт полученного целого числа запишется в переменную C.

Если в операции участвуют число со знаком и без знака, оба приводятся к типу без знака. Объявим байт со знаком и целое без знака

long L;

unsigned U1=2;

char C1=-3;.

Диапазон значений типа int не включает в себя старшей половины типа unsigned, но для 16-битной арифметики тип long, (а для 32-битной тип int64) включают в себя диапазоны char и unsigned. И тем не менее, выполняя в программе для DOS оператор

L= U1+ C1;

мы получим результат не минус 1 и не 255, а 65535:

сначала байт C1 преобразуетсяв целое со знаком – это минус 3 с двоичным кодом

1111111111111101. Далееэтот код складываетсяс числом 2 (команда сложения. как мы помним, одинаково складывает числа со знаком и без знака) и получается код 11111111111111111. Потом этот код преобразуется в тип long как число без знака (старшее слово заполняется не знаковым разрядом, а нулем) и получившийся код 00000000000000001111111111111111 – это 65535.

При выполнении операции учитываются только типы участвующих в ней операндов, а не тип левой части операции присваивания.

Объявим, например, переменные

int B=1, C=1000;

float f;

long lC;

Оператор 

f=B/4;

запишет в переменную f число 0, а не 0.25:

– операция B/4 выполнится как деление целых чисел, получится 0 и 1 в остатке;

– целое число 0 преобразуется в вещественное 0.0 и запишется в f.

Чтобы получился результат 0.25, надо было делить B на вещественное число – f=B/4.0;.

Аналогично, при выполнении в DOS оператора

lC=300* C; в переменную lC не занесется значение триста тысяч из-за переполнения при умножении целых чисел 300 и C. Для получения правильного результата следует написать lC=300 l* C;

Буква l в числе 300l указывает, что это четырехбайтовая константа, поэтому C автоматически приводится к типу long, что обеспечивает правильный результат умножения.

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

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

#include <stdio.h>

Void main(void)

 {

unsigned int j;

          j= 60000;

    printf(“j= %d j= %u j= %#x ", j, j, j);

        }

переменная j объявлена как целое без знака, но в результате работы программы будет выведена cтрока j = -5536 j= 60000 j= 0xea60.

При выводе по спецификации %d переменная j читается из памяти и преобразовывается в целое число со знаком. При преобразовании значение числа сохранить не удается (значение 60000 выходит за диапазон данных типа int но двоичный код числа не изменяется. Как число со знаком он равен -5536.

Другая спецификация формата % u, заставляет выводить переменную j без преобразования - как беззнаковое целое.

Спецификация % x – выводит целое число 60000 в шестнадцатеричном коде - ea60. (Если, как в данном примере, после процента вставлен знак #, то перед числом выводятся символы 0x - 0xea60).

Если в данном примере переменную j объявить как длинное целое, то при занесении в j больших чисел результат печати будет неправильным – при преобразовании длинного целого в шестнадцатибитное число (как требуют спецификации l, x, u) старшие разряды числа теряются. Чтобы этого не происходило, после процента ставится буква l, предписывающая преобразовывать число перед выводом в 32- разрядное.

Итак, для вывода длинных целых надо использовать спецификации % ld % lu % lx. Тоже относится и к вещественным числам: для вывода данных типа float используют форматы % f % g % e а для вывода данных типаdouble - % lf % lg % le.

Очень важно помнить, что неправильные спецификации формата искажают и резултаты ввода данных функуцией scanf: при вводе данных в переменные типа double, long, unsigned long надо обязательно добавлять в спецификации букву l.

1.1.3. Явное преобразование типов данных.

Явное преобразование программист организует при помощи операции приведения типов данных, которая имеет следующую структуру:

(имя нового типа)преобразуемая величина.

В предыдущих примерах можно применить явное преобразование следуюшим образом:

f=(float) B/4; Приоритет операции приведения типов выше, чем приоритет деления, поэтому B сначала преобразуется в вещественное число, потом разделится на четыре.

lC=300*(long) C;.

Из данных примеров видно, что приведение типов языка С++ существенно отличается от аналогичной операции языка Паскаль. В Паскале приведение типов не выполняет никаких вычислений – оно просто разрешает рассматривать область памяти, занимаемую переменной, как данные другого типа. Поэтому объявленный и новый типы должны обязательно иметь одинаковый размер. В С++ приведение вызывает реальную процедуру преобразования, которая преобразует двухбайтовое целое B в четырехбайтовое вещественное число.

1.1.4. Литерные данные

В языке Паскаль были предусмотрены разные типы данных для хранения символа и восьмибитного целого числа.

В Паскале объявление целочисленной переменной I имело вид:

I: byte;

Объявление литерной (содержащей букву или алфавитно-цифровой символ) - C: char;

При таком объявлении в операторной части программы на Паскале можно занести в I число, а в C символ:

I:=65;

C:=’ A ’;

Но операторы

I:= ’A’;

C:= 65;

вызовут ошибку компиляции – символы и целые числа несовместимы по присваиванию. Данные ограничения (как и вообще строгий контроль типов) введены для раннего выявления ошибок программиста. Считается, что нет разумных причин для записи буквы в переменную целого типа.

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

I     db   ‘A’

или целым числом

       I     db 65.

Си создавался как язык системного программирования – альтернатива языку ассемблера. В нем переменная, объявленная как 

char К;     

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

K =’ A ’;

K =65;.

Для задания шестнадцатеричных констант в языке С указываются символы 0x перед значением числа. Так, оператор

K =0 xA 2; занесет в переменнуюKдесятичное число 162.

Язык С++ развивается в сторону более строгого контроля соответствия типов, но в этой части его синтаксис пока совпадает с С.

При выводе данных на экран строка формата указывает, как надо понимать содержимое байта:

оператор printf (“% d ”, I); выведет число 65 (I автоматически преобразуется в int);

 оператор printf (“% c ”, I); выведет символ А.

(Напомним, что в Паскале оператор write(I) интерпретирует тот набор битов, что хранится в памяти, исходя из типа, указанного при объявлении переменной I).

Ослабление синтаксических ограничений требует от программиста большей внимательности, но позволяет писать более эффективные программы. Например, на Паскале совсем не просто вывести на экран 16-ричный код целого числа или символа. Необходимо написать процедуру, которая преобразует целое число в строку символов. На Си нужно только задать требуемый формат вывода.

 

В следующей программе

#include <stdio.h>

char s;

Void main(void)

{ scanf ("%c",&s);

printf ("\ n Символ - %с десятичный код % d шестн. код %# x ", s, s, s);

getch ();

}

оператор printf использует разные спецификации:

– выводит значение переменной s как символ;

% d – как целое десятичное число;

% x  – как целое шестнадцатеричное число.

Заметим, что код символа хранится в одом байте и состоит из двух шестнадцатеричных цифр, а спецификация % x  задает преобразование переменной s в целое число из четырех цифр. Поэтому если в данной программе ввести строчную русскую букву ‘а’, то она выведет код символа 0xffa0. Чтобы получить привычный вид из двух цифр, выводимую переменную достаточно явно преобразовать в беззнаковую:

printf("\n Символ - %с десятичный код %d шестн. код %#x",s,s,(unsigned char)s);.

Спецификация %#x все равно преобразует ее в целое, но два старших незначащих нуля выводиться не будут.

Строки символов

Для строк символов отдельного типа данных не предусмотрено. Строка – это массив элементов типа char, заканчивающийся терминальным символом – байтом с кодом 0, например, с har S [80]; – это объявление строки.. (Заметим, что в авторской версии языка Паскаль также не было типа string, а использовался массив символов).

Наличие терминального символа позволяет написать простую функцию для определения длины строки:

#include <stdio.h>

#include <conio.h>

char S[80]=" Строка символов ";

int dln(char *s) {for(int i=0;s[i];i++); return i;}

Обратите внимание, как в этой функции организована проверка окончания цикла.

Void main (void)

{ printf("%d",dln(S));

getch ();

}

Таким образом, мы объявляем и применяем строку, как обычный массив, но все-таки есть и специфические особенности.

1. Кроме инициализации, принятой для массивов: char A[]={20,7,51,0};

 или char A[]={‘T’,’E’,’K’,’C’,’T’,0} разрешается объявлять строку символов так: char A []=” TEKCT ”;

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

Обратите внимание: 

‘A’ – это символ, он занимает один байт,

“A” – это строка, состоящая из двух байтов.

2. Наличие признака конца строки позволяет упростить функцию копирования строк и другие функции обработки строк, прототипы которых находятся в файле string.h. Для копирования обычных массивов используется функция (ее прототип – в mem.h)

 memcpy(Dest, Src, L)

 где Dest - имя массива-приемника; Src - имя массива-источника; L количество копируемых байтов).

Аналогичная функция для строк имеет вид strcpy(Dest, Src). В ней нет третьего параметра, так как длина строки-источника автоматически определяется по терминальному символу. Но надо отметить, функция strcpy() не контролирует размер строки приемника – всегда копируются все символы источника. Если фактическая длина строки Src больше, чем количество байтов, выделенное при объявлении строке Dest, при копировании стираются данные, размещенные в памяти после Dest.

 Благодаря наличию нулевого байта в строке, объявленной как

char Src[256]=”cтрока”;

 стандартная функция strlen(Src) определит и возвратит фактическую длину строки – число 6 (без терминального нуля). Уже известную функцию sizeof (Src) также можно применить к строке, но она даст значение 256 – количество байтов, выделенное компилятором для хранения строки.

В файле string.h перечислен еще ряд функций для работы со строками, с которыми надо ознакомиться самостоятельно, пользуясь справочной системой среды разработки.

3. При выводе строки на экран не обязательно организовывать цикл для поочередного вывода каждого элемента массива – можно использовать спецификацию % s вывода строки.

 При работе со строками на С++ мы, как и в других случаях, получаем по сравнению с Паскалем большую свободу, но и больше возможностей ошибаться. Рассмотрим простейший пример, который хорошо смотрелся тридцать лет назад на ЭВМ М6000: компьютер запрашивает имя пользователя, а потом здоровается с ним. Для решения задачи мы объявим строку char hello[]={"Hello"}, строку для ввода имени char name[7];, объединим содержимое этих строк в строку ответа char answer[12]; и выведем сформированный ответ на экран. В языке С++ нет операции объединения (конкатенации) строк, но можно воспользоваться стандартной функцией strcat(Dest,Src), которая дописывает содержимое строки Src в конец строки Dest.

#include <stdio.h>

#include <conio.h>

#include <string.h>

char hello[]={"Hello"};

char name[7];

char answer[12];

Void main(void)

{

clrscr();

printf("What is your name?");

scanf ("% s ", name);       // –вводим имя (помним, что name – тоже, что &name[0])

strcpy (answer, hello); Копируем в ответ слово Hello

strcat (answer, name); Дописываем в ответ введенное имя

int i = strlen (answer); Узнаем длину ответа, чтобы вывести его в центр строки

gotoxy (40- i /2,10);    Изменяем положение курсора

printf ("% s ", answer); Выводим ответ

Getchar ();

getchar ();

}

Для проверки программы я ввел имя Саша и машина меня поприветствовала. Это мне понравилось, я запустил программу еще раз, ввел имя Александр и получил такой ответ:

HellоАлексанHelloA

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

Если выполнять по шагам:

-видно, что лишние буквы не теряет, не контролирует длину массива – заползает на

answer и в нем пишет финальный ноль. (В отладчике смотрим name – видим длинное имя. Смотрим answer – видим хвост этого имени без первых семи букв.)

-скопировали hello в answer – изменился и name – та часть, что попала на answer заменилась на HELLO

– добавляем к в answer строку name – добавляется name вместе с hello

– и ноль, которым кончался name, (он же в ответе) копированием стерся!! при этом стало видно, что строка, записанная в каычках непосредственно в printf лежала в памяти дальше, ее частично стерли, а остаток вывелся и его-то ноль позволил завершить вывод.

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

char mms [2][4]={{" AB "},{" DEF "}};

В этом случае элемент mms[0] можно использовать как имя первой строки "AB", а элемент mms[1] - как имя второй строки "DEF". К одиночным символам можно обращаться как к элементам двумерного массива: mms[1][2] – это символ F, mms[1][3] – терминальный ноль второй строки.

 Ниже приведена программа, которая выводит элементы массива, как целые числа и как строки:

#include <stdio.h>

#include <string.h>

char mms [2][4]={{" ABC "},{" DEF "}};

       Функция pr() выводит элементы одномерного массива как целые числа 

void pr(char Mas[],int k)

{ for (int i=0;i<k;i++)printf("%5d",Mas[i]);

printf("\n");

}

Void main(void)

{ Следующий цикл два раза вызовет функцию pr() и передаст ей сначала строку mms[0], потом строку mms[1]:

for(int i=0;i<2;i++) pr(mms[i],4);

А в каждом проходе этого цикла выводятся строка (как единое целое при помощи спецификации %s) и длина этой строки.

for(i=0;i<2;i++)

printf("\n%s %d",mms[i],strlen(mms[i]));

getchar();

}

Запустив программу мы увидим, что второй цикл выведет такие данные

ABC 3

DEF 3,

т.е., функция возвращает длину строки, не считая терминального символа.

Изменим в рассмотренной программе объявление массива:

char mms[2][4]={{"ABC1"},{"DEF"}};

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

ABC1DEF 7

DEF         3.

Указав в первой строке четыре символа мы не оставили места для нулевого байта. Первая строка выводится до нулевого байта, который встретится только в конце второй строки. По той же причине неправильно вычислит длину функция strlen().

 

1.2. Данные типа указатель

1.2.1. Объявление указателей

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

Пусть в программе объявлена переменная I. С теми же данными, за которыми закреплено имя I, можно работать, используя указатель pI – отдельную переменную, хранящую адрес той области памяти, которая была выделена компилятором для хранения I.

Синтаксис объявления указателя: <базовый тип> * <имя указателя>. Например, указатель на ячейку, содержащую переменную типа int объявляется как int *pI. При этом знак * слева от имени pI говорит о том, что объявлен указатель.

Если в указатель записать адрес переменной I, у нас появляется два пути доступа к данным:

– записать число в переменную, используя имя I, например, I=27;

– записать число в переменную I, используя вместо ее имени указатель pI.

Примеры объявлений:

int K, I; это объявлены переменные I, K, D в которых

char D;                                                               можно хранить целые числа;

int * pI, *P1 это объявлены указатели pI, P1 в каждом из которых можно хранить адрес ячейки памяти, содержащей целое число типа int;

char * pC; а это объявлен указатель pС, в котором можно хранить адрес ячейки памяти, хранящей целое число типа char.

Замечание: далее мы для краткости вместо громоздких конструкций типа «ячейка памяти, адрес которой хранится в указателе pI» будем писать - «переменная, на которую указывает (или ссылается) pI».

В процессорах младших моделей компьютеров семейства IBM PC и в реальном режиме старших моделей память разбита на сегменты по 64К байтов. А eказатель может хранить только смешение ячейки памяти в сегменте, тогда он состоит из 16 битов и называется ближним. Чтобы компилятор сформировал ближний адрес, при объявлении указателя указывают модификатор near:

near int * nP;.

Если указатель содержит и адрес сегмента и смещение, он состоит из 32 битов и называется дальним. Чтобы компилятор сформировал дальний адрес, при объявлении указателя указывают модификатор far:

far int * fP;.

Когда не указаны ни near ни far, разрядность указателя по умолчанию определяется выбором «модели памяти». Если в настройках интегрированной среды выбрать модель small, по умолчанию формируются 16-разрядные указатели, а если выбрать модель large – 32- разрядные. Среда разработки дает возможность выбирать одну из нескольких моделей, но детально на понятии «модели памяти» и отличиях разных моделей мы останавливаться не будем, потому что при программировании в 32-разрядной среде (Windows, Unix) всегда используются 32-разрядные указатели, задающие смещение в сегменте размером до 4 Гб.

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

.Как видим, при объявлении указателя int *pI сначала записывают базовый тип (спецификатор типа), за ним следует описатель – это имя указателя с предшествующей звездочкой (или группой звездочек).

Для занесения в указатель адреса конкретной переменной используется операция получения адреса &:

pI =& K; из объявления переменной pI следовало, что она может указывать на данные типа int. Сейчас мы занесли в указатель pI адрес конкретной ячейки K.

В записанном выше операторе значение &K заносится в переменную pI. Когда требуется указать, что обращение происходит не к указателю pI, а через него к переменной, на которую он сcылается, слева от имени pI надо записать знак * (он называется операцией разыменования). То есть, после занесения в указатель адреса &K, обозначение *pI, это как бы синоним имени K:

* pI =23; операция присваивания заносит число 23 в переменную K.

I =* pI; число 23 из переменной K заносится в переменную I.

printf (“% d ”, I); //На экране увидим 23.

Заметим, что в языке С, указатели на разные базовые типы считались совместимыми по присваиванию: при обработке оператора pC=pI; компилятор выводит предупреждение о подозрительном присваивании, но не считает это ошибкой. В С++ для уменьшения количества ошибок указатели на разные базовые типы сделали, как и в Паскале, несовместимыми по присваиванию. Можно выполнить оператор

P 1= pI;

но нельзя операцией pC=pI; записать в указатель pC адрес переменной I, так как pI указывает на int, а pC должен указывать на char.

Ранее было сказано, что нельзя объявить переменную типа void. Указатель на void объявить можно, он называется нетипизированным указателем:

void * pV;. Пусть мы ничего не знаем про базовый тип void, но для хранения любого указателя требуется четыре байта – вот этим объявлением мы их и выделяем для хранения pV. Правда, нельзя работать с данными, на которые ссылется pV: мы не знаем, что читать или писать – байт, слово, двойное слово и пр. Если необходимо, через указатель типа void * можно работать с данными, используя приведение к конкретному типу данных:

       double M1; объявляем вещественную переменную;

       voud *pVoid=&M1; объявляем указатель и инициализируем адресом переменной M1;

     (double *) pVoid=1.5: - заносим число 1.5 в переменную M1.

Но основное назначение такого указателя в C, как и типа pointer в Паскале – обойти запрет на совместимость по присваиванию указателей на разные базовые типы:

pV= pI; заносим в нетипизированный указатель значение ссылки на int;

pC= pV;. а уже из негопереписываем в указатель на char.

 Так же использовался void * и в С++. Но в последних версиях и этот тип стал несовместимым с другими указателями. Поэтому сейчас в С++ эту задачу решают без указателя типа void *– при помощи переопределения типа данных:

pC=(char *) pI;.

Многие стандартные функции С++ и системные функции операционной системы возвращают указатель void *, чтобы их результат могли получать указатели разных типов. Возвращаемое такими функциями (malloc(), SelectObject() и пр.) значения также требуется переопределять.

В С++ разрешено объявить указатель на любой тип данных, кроме файлового. Значит, в качестве базового типа можно взять указатель, например, float * на вещественное число. Чтобы объявить указатель на тип float *, надо записать имя базового типа, знак *, имя переменной. Вот так и получается конструкция с несколькими звездочками

float ** ppF – указатель на указатель на вещественное число. Применение к ppF одной операции разадресации позволяет получить указатель

float * pF= * ppF;

Для получения вещественного числа, на которое ссылается цепочка из двух указателей, надо применить разадресацию дважды:

float F= ** ppF;

Иногда по значению указателя программисту требуется определить, записан ли в указатель адрес переменной базового типа или он не содержит ссылки на данные (указатель объявили, но адреса переменной в него не заносили). Для этого служит специальное значение NULL ­ – его записывают в указатель, который не ссылается ни на какие данные. В языке Паскаль есть аналогичная константа NIL, при знакомстве с которой мы отмечали, что это не число ноль, это специальный вид константы «пустой указатель». 

В С++ значение NULL, это именно число ноль и этим определяются многие особенности применения указателей. Чтобы убедится в этом, попробуем в объявленный выше указатель ppF записать число 2:

ppF=2;

и получим сообщение компилятора

            error C2440: '=': cannot convert from 'const int' to 'float **

Но ни один из операторов

ppF =0;

int N = NULL;

не вызовет сообщения об ошибке. Это позволяет компилятору использовать однотипные конструкции для проверки на ноль числовых выражений и указателей, например, if(ppF==0){...}.

1.2.2. Адресная арифметика

В языке С++ к двум указателям можно применять операцию вычитания. Результатом будет целое число – разность адресов тех ячеек памяти, на которые ссылаются указатели.

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

В С++ разрешено также складывать или вычитать значения указателя и целого числа. Объявим целочисленную переменную I и указатель на вещественное число, например, так:

int I=-2;

double F;

double A[10];

double pF=& A[4];, При инициализации в указателе pF будет записан адрес четвертого элемента массива.

Синтаксически правильными будут выражения pF +1, 1+pF, pF-1, pF +I и т. п. Но при их вычислении применяются правила адресной арифметики:

– сумма указателя и целого числа – это тоже указатель;

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

В данном случае размер базового типа double – 8 байтов. Поэтому адрес pF +1 на восемь байтов больше адреса pF, то есть pF+1 – это указатель на следующий элемент массива – A[5]. Применяя к pF+1 операцию разадресации мы можем прочитать значение элемента A[5]:

F=*(pF+1); - скобки необходимы, потому что приоритет сложения ниже приоритета разадресации. Оператор F=*pF+1; записал бы в F сумму значения A[4] и числа 1.

1.3. Данные типа структура

1.3.1. Объявление структур

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

Объявление переменных типа структура часто совмещают с описанием их типа.

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

Struct student

{

char name[30];

int kurs;

char group [3];

} St1,St2,*St;

Данное объявление начинается со служебного слова struct, после чего следуют:

– необязательное имя типа (шаблона структуры) - student;

– заключенные в фигурные скобки объявления полей данных (двух строк и целого числа);

– также необязательные переменные (St1,St2 типа - student, указатель *St на структуру типа student).

В сделанном объявлении можно было не указывать имя типа student, но когда оно есть, его можно далее использовать для объявления других переменных:

struct student stud1={“Иванов А.И. % d”, 4,”3а”}; - инициализированная переменная типа структура;

struct student Ms[5]; - массив из пяти структур student;

struct student (* Fun)(); - указатель нафункцию, возвращающую структуру типа student.

Функция, возвращающая указатель на структуру будет описываться следующим образом

struct student * Fun()

{

}.

Последние два объявления мы еще раз прокомментируем при изучении операций языка.

В сделанных выше объявлениях на языке С надо было обязательно при указании типа «структура» писать слово struct (имя student называлось именем шаблона структуры, а не именем типа данных). В С++ слово struct обязательно только при описании типа, а при объявлении переменных его можно опускать:

student stud1;

student Ms[5];

В состав структур, объявленных на С++ кроме полей данных можно также включать и функции, работающие с этими полями. Этот вопрос будет рассматриваться при изучении объектно-ориентированного программирования.

1.3.2. Операции со структурами.

К структуре в целом, в отличие от массивов, применима операция присваивания. При выполнении оператора

St1= stud1; все поля структуры stud1 скопируются в одноименные поля St1. В данном случае для объявления обеих переменных использован один шаблон. В Паскале в подобных ситуациях требовалась эквивалентность типов. Так и здесь – если типы (шаблоны структур) полностью совпадают по названиям и типам полей, но описаны под разными именами, присваивание невозможно.

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

Для обращения к полям структуры нужно указать ее имя и, через точку, имя поля:

St1. kurs=5;

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

Записать строку в поле name третьего элемента массива Ms можно следующим оператором:

Ms[2]. name=”Петрова М.М.”;. А если надо обратиться к третьей букве записанной строки, то придется написатьMs[2].name[2]=’s’. В последнем выражении операции равного приоритета выполняются слева направо: сначала выполняется первая операция индексирования и в массиве Ms выбирается третья структура, потомоперация обращения к полю структуры задает работу с полем name, и наконец, второе индексирование указывает, что символ s запишется в третью позицию строки.

Оператор

printf(stud1.name, stud1. kurs);

выведет на экран фамилию студента и номер курса из структуры stud1. Обратите внимание – вместо обычной взятой в кавычки строки формата, первым параметром указано поле структуры stud1. Но посмотрите выше - это поле и есть строка, содержащая фамилию и задающая вывести второй параметр, stud1. kurs, по формату %d. Отсюда можно сделать вывод:

 используемая для инициализации переменных строка текста (например, такая - “текст”)сама по себе имеет тип char * и значение, равное адресу первого символа строки.

(Потому, например, в функции копирования строки можно указывать имена массивов strcpy(Dest,Src);, а можно непосредственно строку strcpy(Dest,“текст );).

Выше был объявлен указатель St на структуру student. Если занести в него адрес структуры

St=& stud1;, то к ее полям можно обращаться через указатель двумя способами.

1. Поскольку St это указатель, то после применения операции разыменования * St – это то, на что он указывает, то есть синоним имени stud1. А как обращаться к полям структуры, зная ее имя *St, мы уже знаем: (*St).name (*St).kurs (*St).group.

Скобки необходимы для того, чтобы операция разадресации выполнилась раньше операции обращения к полю структуры. Если написать *St1.name, операция точка, которая применяется только к структурам, будет применяться к St1 до операции *. Поскольку St1 указатель, а не структура, получим синтаксическую ошибку.

2. В языке предусмотрено более короткое обозначение -> для обращения к полям структуры, на которую ссылается указатель:

St-> name - это краткая запись для (*St).name, St-> group – то же, что (*St).name и т.п.

Полями структуры могут быть любые типы данных, кроме функций и файлов. Если полем структуры в свою очередь является структура, как в следующем примере

Struct

{

struct student Stn;

int year;

} M1, M2;

, то ддля занесения в переменную M1 фамилии студента придется написать:

M1. Stn. name=”Павлов Ф.Ф.”;

Обратите внимание, что здесь отсутствует имя типа (его также называют меткой шаблона структуры), поэтому других переменных такого типа, кроме M1, M2, мы далее объявить не сможем.

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

Пусть объявления переменных имеют вид

struct fulladdr {

char index[7];

char region [30];

struct addr {

 char city [30];

                    char street[30];

                    int house;

                 }

addres;

Поделиться:





Воспользуйтесь поиском по сайту:



©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...