Открыть Электронные книги
Категории
Открыть Аудиокниги
Категории
Открыть Журналы
Категории
Открыть Документы
Категории
void InvFunc(double z)
{
if(z==0 ){cout<<"Division by zero! "<<endl; return;}
double x;
x=1/z;
cout<<"l/z = " << x << endl;
}
У функции InvFunc () один аргумент типа double, а в качестве типа
возвращаемого результата указано void (функция результат не возвращает). В результате
выполнения функции, в зависимости от значения аргумента, возможны два сценария. При
ненулевом аргументе на экран выводится значение, обратное к аргументу. Если аргумент
у функции нулевой, выводится сообщение соответствующего содержания. Проверка
аргумента на предмет равенства нулю осуществляется в условном операторе. Блок
условного оператора, выполняющийся при нулевом аргументе, состоит из двух команд:
выводится сообщение и затем завершается выполнение функции (команда return).
Если аргумент ненулевой, объявляется переменная х типа double. В качестве
значения ей присваивается величина, обратная к аргументу функции, после чего
полученное значение выводится на экран. В обеих командах вывода в функции
использована инструкция завершения строки endl. Кроме того, в данном случае
фактически инструкция return использована как альтернатива к else-блоку условного
оператора: можно было бы ту часть кода, что выполняется при ненулевом аргументе
функции, разместить в else-блоке условного оператора, отказавшись при этом от
инструкции завершения функции return. Тем не менее, в более сложных программах
использование такой инструкции часто бывает не только оправданным, но и существенно
упрощает структуру кода.
По большому счету, описывать функцию можно где угодно - главное, чтобы ее
прототип был указан до ее первого использования.
Все величины, описанные внутри функции, а также ее параметры, являются
локальными. Областью их действия является функция. При вызове функции, как и при
входе в любой блок, в стеке выделяется память под локальные автоматические
переменные. Кроме того, в стеке сохраняется содержимое регистров процессора на
момент, предшествующий вызову функции, и адрес возврата из функции для того, чтобы
при выходе из нее можно было продолжить выполнение вызывающей функции.
При выходе из функции соответствующий участок стека освобождается, поэтому
значение локальных переменных между вызовами одной и той же функции не
сохраняются. Если этого требуется избежать, при объявлении локальных переменных
используется модификатор static.
При совместной работе функции должны обмениваться информацией. Это можно
осуществлять с помощью глобальных переменных, через параметры и через возвращаемое
функцией значение.
Функцию можно определить как встроенную с помощью модификатора inline,
который рекомендует компилятору вместо обращения к функции помещать ее код
непосредственно в каждую точку вызова. Модификатор inline ставится перед типом
функции. Он применяется для коротких функций, чтобы снизить накладные расходы на
вызов (сохранение и восстановление регистров, передача управления). Директива inline
носит рекомендательный характер и выполняется компилятором по мере возможности.
Использование inline-функций может увеличить объем исполняемой программы.
Определение функции должно предшествовать ее вызовам, иначе вместо inline-
расширения компилятор сгенерирует обычный вызов.
Глобальные переменные
Глобальные переменные видны во всех функциях, где не описаны локальные переменные
с теми же именами, поэтому использовать их для передачи данных между функциями
очень легко. Тем не менее, это не рекомендуется, поскольку затрудняет откладку
программы и препятствует помещению функций в библиотеку общего пользования.
Нужно стремиться к тому, чтобы функции были максимально независимы, а их интерфейс
полностью определялся прототипом функции.
Параметры функции
Механизм параметров является основным способом обмена информацией между
вызываемой и вызывающей функциями. Параметры, перечисленные в заголовке описания
функции, называются формальными параметрами, или просто параметрами, а
записанные в операторе вызова функции – фактическими параметрами, или
аргументами.
При вызове функции в первую очередь вычисляются выражения, стоящие на месте
аргументов; затем в стеке выделяется память под формальные параметры функции в
соответствии с их типом, и каждому из них присваивается значение соответствующего
аргумента. При этом проверяется соответствие типов и при необходимости выполняются
их преобразования. При несоответствии типов выдается диагностическое сообщение.
Существует два способа передачи параметров в функцию: по значению и по
адресу.
При передаче аргумента функции по значению в стек заносятся копии значений
аргументов, и операторы функции работают с этими копиями. Доступа к исходным
значениям параметров у функции нет, а, следовательно, нет и возможности их изменить.
После завершения выполнения кода функции эти копии уничтожаются (выгружаются из
памяти).
При передаче по адресу в стек заносятся копии адресов аргументов, а функция
осуществляет доступ к ячейкам памяти по этим адресам и может изменить исходные
значения аргументов:
При передаче аргументов функции по ссылке функция получает непосредственный
доступ (через ссылку) к переменным, указанным аргументами функции.
I j k
1 2 3
1 3 4
Совет
Рекомендуется указывать const перед всеми параметрами, изменение которых в
функции не предусмотрено. Это облегчает отладку больших программ, так как по
заголовку функции можно сделать вывод о том, какие величины в ней изменяются, а
какие нет. Кроме того, на место параметра типа const& может передаваться константа, а
для переменной при необходимости выполняются преобразования типа.
Таким образом, исходные данные, которые не должны изменяться в функции,
предпочтительнее передавать ей с помощью константных ссылок.
По умолчанию параметры любого типа, кроме массива и функции (например,
вещественного, структурного, перечисление, объединение, указатель), передаются в
функцию по значению.
Массивы как параметры функций
Одномерные массивы
#include <iostream.h>
#include <conio.h>
int dl(char[]); // прототип функции dl
void main ()
{
clrscr();
char c[]="kafedra ASOI";
cout << "\n kol-vo simvolov:" << dl(c);
getch();
}
void main()
{
float z[5],y;
for (int i=0; i<5; i++)
{
cout <<"\nВведите очередной элемент массива:";
cin >> z[i];
}
y=sum(z) ;
cout <<"\n Сумма элементов массива:"<< y;
}
void main()
{
float z[6];
for (int i=0;i<6;i++)
{
cout <<"\nВведите очередной элемент массива:";
cin >> z[i];
}
cout <<"\nМаксимальный элемент массива:"<< max(6,z);
}
В результате выполнения программы на экран монитора будет выдано сообщение со
значением максимального элемента массива z, который в данном примере состоит из
шести элементов.
В приведенной программе прототип отсутствует, поскольку описание функции следует
раньше ее вызова. В функции тах() используется два параметра: первый указывает число
элементов в массиве, а второй — имя и размерность массива. Аргументы указаны при
вызове функции max(6, z). Здесь 6 — размер массива, a z - имя массива.
void main ()
{
clrscr();
int a[]={0,1,2,3,4};
int b[]={5,6,0,7,1};
int d[5];
maxl (5,a,b,d) ;
cout << "\n";
for (int i=0;i<5;i++)
cout << "\t" << d[i] ;
getch();
}
Многомерные массивы
Особенностью языка C++ является несамоопределенность массивов, т. е. по имени
массива невозможно узнать его размерность и размеры по каждому измерению. Кроме
того, в C++ многомерные массивы не определены. Например, если объявлен массив float
d[3][4][5], то это не трехмерный, а одномерный массив d, включающий три элемента,
каждый из которых имеет тип float [4][5]. В свою очередь, каждый из четырех элементов
типа float [5]. И, соответственно, каждый из этих элементов является массивом из пяти
элементов типа float. Эти особенности затрудняют использование массивов в качестве
параметров функций.
При передаче массивов в качестве параметров через заголовок функции следует
учитывать, что передавать массивы можно только с одной неопределенной границей
мерности (эта мерность должна быть самой левой).
void main()
{
float z[4][3] = {0,1,2,3,4,5,6,7,7,6,5,4};
cout << "\n Сумма элементов матрицы равна " << summa(4,z);
}
#include <iostream.h>
void main()
{
float d[3][4]={1,2,-2,4,5,0,-3,18,-9,6,7,9};
float *r[]={(float*)&d[0], (float*)&d[1], (float*)&d[2]};
int m=3;
int n=4;
cout<<"\n Минимальный элемент матрицы равен "<<min(m,n,r);
}
Динамические массивы
При использовании в качестве параметра массива в функцию передается указатель на
его первый элемент, то есть массив всегда передается по адресу. При этом информация о
количестве элементов массива теряется, и следует передавать его размерность через
отдельный параметр.
#include <iostream.h>
int sum (const int *mas, const int n); //прототип
int const n=10;
int main()
{
int marks[n] = {3, 4, 5, 4, 4};
cout << "Сумма элементов массива: " << sum(marks, n);
return 0;
}
void main()
{
int **pi; // указатель на массив указателей
int m1; // число строк в матрице
cout << "\n Введите число строк матрицы:";
cin >> m1;
int n1; // число столбцов в матрице
cout << "\n Введите число столбцов матрицы:";
cin >> n1;
int i,j;
// выделение вспомогательного массива указателей
pi=new int* [m1];
for (i=0;i<m1;i++)
// формирование i-й строки матрицы
pi[i]=new int [n1];
fun(m1,n1,pi); // обращение к функции
for (i=0;i<m1;i++) // цикл перебора строк
{
cout << "\n строка " << i+1 << ":";
for (j=0;j<n1;j++)
cout << "\t" << pi[i][j];
}
//
for (i=0;i<m1;i++)
delete []pi[i];
delete []pi;
}
В функции fun() есть три следующих параметра: int т — число строк, int n — число
столбцов матрицы и int * *uc — указатель на массив указателей.
В функции main для определенности значение числа строк т1 задано 3, а число
столбцов матрицы n1 задано 4. При выполнении оператора присваивания pi=new int*[m1];
операцией new выделяется память для вспомогательного массива указателей. В результате
выполнения этой операции для массива указателей выделяется требуемое количество
памяти, а адрес первого элемента выделенной памяти присваивается указателю pi. При
выполнении в теле цикла второй операции new формируются т1 строк матрицы.
При обращении к функции происходит присваивание элементам матрицы
последовательно значений целых чисел от 0 до 11. В конце программы с помощью delete
выделенная память освобождается.
void main()
{
int n1; //порядок матрицы
cout << "\n Введите порядок матрицы:";
cin >> n1;
int **ma; //указатель для формирования матрицы
ma=matr(n1); //обращение к функции
//печать элементов матрицы
for (int i=0;i<n1;i++)
{
cout << "\n строка " << i+1 << ":";
for (int j=0;j<n1;j++)
cout << "\t" << ma[i][j];
}
//освобождение памяти
for (i=0;i<n1;i++)
delete []ma[i]; //удаляем содержимое строк
delete []ma; //удаляем массив указателей на
строки
}
При выполнении программы на экране появится подсказка:
Введите порядок матрицы:
Если в ответ на подсказку пользователь введет с клавиатуры порядок матрицы, равный
5, то на экране дисплея он получит сообщение из пяти строк вида:
строка 1: 1 0 0 0 0
строка 2: 0 1 0 0 0
строка 3: 0 0 1 0 0
строка 4: 0 0 0 1 0
строка 5: 0 0 0 0 1
В данной программе функция matr() имеет один параметр int n — порядок матрицы. В
теле функции формируется квадратная матрица размером nxn (создаются n одномерных
массивов с элементами типа int и массив указателей на эти одномерные массивы) и ее
элементы заполняются необходимыми значениями. Возвращаемым значением функции
matr() является значение указателя на сформированную матрицу.
int sum(const int *a, const int nstr, const int nstb);
int main()
{
clrscr();
int b[2][2]= {{2, 2},{4,3}};
printf ("Cумма элементов b: %d\n", sum(&b[0][0], 2, 2));
//имя массива передавать в sum нельзя из-за несоответствия типов
int i,j, nstr, nstb, *a;
printf ("Введите количество строк и столбцов: \n");
scanf("%d%d", &nstr, &nstb);
a=(int *)malloc(nstr* nstb* sizeof(int)); //1
printf ("Введите элементы массива: \n");
for (i=0; i<nstr; i++)
for (j=0; j<nstb; j++)
scanf("%d", &a[i * nstb + j]); //2
printf("Сумма элементов a: %d\n", sum(a, nstr, nstb));
free(a);
return 0;
}
int sum(const int *a, const int nstr, const int nstb)
{
int i,j,s=0;
for (i=0; i<nstr; i++)
for (j = 0; j<nstb; j++)
s += a[I * nstb + j];
return s;
}
Память выделяется сразу под все элементы массива (оператор 1), то есть, по сути
двумерный динамический массив занимает сплошной участок памяти. При заполнении
массива используется формула для определния индекса соответствующего элемента (2). В
конце работы программы массив удаляется.
Можно решить эту задачу по-другому выделяя память (программа написана в стиле
С++).
#include <iostream.h>
int sum(int **a, const int nstr, const int nstb);
int main()
{
int nstr, nstb;
cout<<"Введите количество строк и столбцов: \n";
cin >> nstr >> nstb;
int i,j, **a;
a=new int *[nstr];
for (i=0; i<nstr; i++)
a[i]=new int [nstb];
cout<<"Введите элементы массива: \n";
for (i=0; i<nstr; i++)
for (j=0; j<nstb; j++)
cin>>a[i][j];
cout<<"Сумма элементов a:"<<sum(a, nstr, nstb);
//освобождение памяти
for (i=0;i<nstr;i++)
delete []ma[i]; //удаляем содержимое строк
delete []ma; //удаляем массив указателей на
строки
return 0;
}
В этом случае память сначала выделяется под столбец указателей на строки матрицы, а
затем в цикле под каждую строку. Освобождение памяти должно выполняться в обратном
порядке.
Параметры со значениями по умолчанию
Чтобы упростить вызов функции, в ее заголовке можно указать значения
параметров по умолчанию. Эти параметры должны быть последними в списке и могут
опускаться при вызове функции. Если при вызове параметр опущен, должны быть
опущены и все параметры, стоящие за ним. В качестве значений параметров по
умолчанию могут использоваться константы, глобальные переменные и выражения:
int f(int a, int b=0);
f(100);
f(a, 1); //варианты вызова функции
f
f1(a); f1(a,10); f(a,10,"Vasia"); //варианты вызова функции f1
fl(a, ,”Vasia") // неверно!
В случае, когда прототип функции указывается до ее определени, значения по
умолчанию указываются только в прототипе.
Функция main()
Функция, которой передается управление после запуска программы, должна иметь
имя main. Она может возвращать значение в вызвавшую систему и принимать параметры
из внешнего окружения. Возвращаемое значение должно быть целого типа. Стандарт
предусматривает два формата функции:
// без параметров:
тип main (){ /*-*/}
// с двумя параметрами:
тип main (int argc, char* argv[]){ /*-*/}
При запуске программы параметры разделяются пробелами. Имена параметров в
программе могут быть любыми, но принято использовать argc u argv. Первый параметр
(argc) определят количество параметров, передаваемых функции, включая имя самой
программы, второй параметр (argv) является указателем на массив указателей типа char*.
Каждый элемент массива содержит указатель на отдельный параметр командной строки,
хранящийся а виде С-строки, оканчивающейся нуль-символом. Первый элемент массива
(argv[0]) ссылается на полное имя запускаемого на выполнение файла, следующий
(argv[l]) указывает на первый параметр, argv[2] — на второй параметр, и так далее.
Параметр argv[argc] должен быть равен 0.
//программа выводит на экран все переданные в командной строке
//параметры функции
#include <iostream.h>
void main (argc, char* argv[]) {
for (int i = 0; i<argc; i++)
cout << argv[i] << '\n';
}
Первый целочисленный argc аргумент особых вопросов не вызывает. Второй
аргумент, объявленный как char* argv[ ] - массив, элементами которого являются
текстовые строки, реализованные в виде символьных массивов. индексная переменная i
пробегает значения в соответствии с размером массива (от 0 до size-1 включительно).
Для каждого значения этого индекса на экран выводится соответствующий параметр
командной строки, для чего используется ссылка argv[i].
Если функция main() ничего не возвращает, вызвавшая система получит
значение, означающее успешное завершение. Ненулевое значение означает аварийное
завершение. Оператор возврата из main() можно опускать. Если параметры в командную
строку передавать не планируется, аргументы для метода main()можно не указывать (а
можно указывать).
Возвращаемое значение
Механизм возврата из функции в вызывавшую ее функцию реализуется
оператором
Return [выражение];
Значение выражения, если оно задано, возвращается в вызывающую функцию в
качестве значения вызываемой функции. Если выражение опущено, то возвращаемое
значение не определено. Выражение может быть заключено в (). Функция может
содержать несколько операторов return (это определяется потребностями алгоритма).
Выражение, указанное после return, неявно преобразуется к типу возвращаемого
функцией значения и передается в точку вызова функции. Если в функции отсутсвует
оператор , то передача управления в вызывающую функцию происходит после последнего
оператора вызываемой функции. При этом возвращаемое значение не определено.
Примеры:
int f1() {return 1;} //правильно
Перегрузка функций
В C++ допускается наличие нескольких одноименных функций, выполняющих
аналогичные действия над данными разных типов, и отличающиеся типом
возвращаемого результата. Причем вполне достаточно хотя бы одного отличия. При
вызове презагруженной функции в программе выбор нужного варианта функции
осуществляется исходя из использованного синтаксиса вызова функции.
Например, пусть в программе определены две функции с прототипами:
int max(int,int);
float max(float,float);
В этом случае в программе допустимо указание следующих операторов:
float х,у;
cout<<"max="<<max (5,8) <<endl;
х=12.5; у=24.8;
cout<<"max="<<max (х, у) ;
В процессе компиляции программы при обращении к функциям тах() в зависимости
от типа и числа аргументов будет осуществляться загрузка требуемого экземпляра
функции. Описанный механизм называется перегрузкой функций.
Если в программе описано несколько одноименных функций, то при компиляции
возможны следующие ситуации:
1. Если возвращаемые значения и сигнатуры (тип и число параметров) нескольких
функций совпадают, то второе и последующие объявления трактуются как
переобъявления первого. Например:
extern double max(double a,double b);
double max(double c,double d);
2. Если сигнатуры нескольких одноименных функций совпадают, но возвращаемые
значения различны, то второе и последующие объявления при компиляции
рассматриваются как ошибочные. Например, при наличии двух одноименных функций со
следующими прототипами
unsigned int max(unsigned,unsigned);
extern long max (unsigned,unsigned);
при компиляции будет выдано сообщение об ошибке. Причина его в том, что при
определении возможности перезагрузки функции тип возвращаемого значения не
принимается во внимание, а сигнатуры в данном случае совпадают.
3. Если сигнатуры нескольких одноименных функций различны (параметры имеют
различия по типам или количеству), тогда функция считается перегружаемой при
условии объявления ее экземпляров в одной области видимости. Например:
int max(int, int);
float max(float, float);
float max (float c,float d)
{ if (c<d) return(c);
return(d)
}
int max(int a,int b)
{ if (a>b) return(a);
if (a<=b) return(b);
}
В данном случае мы имеем два экземпляра перегружаемой функции тах().
Массивы структур
Как и обычные переменные, структуры могут быть элементами массивов.
//Массивы структур
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
struct Marks
{
char name[80];
int phys;
int chem;
int maths;
};
int main()
{
const int n=3;
bool state;
char s [80];
Marks students[n];
for (int i=0 ;i<n;i++)
{
cout << ("Student name:");
gets(students[i].name);
students[i].phys=3+rand()%3;
students[i].chem=3+rand()%3;
students[i].maths=3+rand()%3;
}
do
{
cout<<"What is the student name?";
gets (s);
if (!strcmp(s,"exit")) return 0 ;
state=true;
for(int i=0 ;i<n;i++)
{
if (!strcmp(students[i].name,s))
{ state=false;
cout<<"Physiscs:"<<students[i].phys<<endl;
cout<<"Chemistry:"<<students[i].chem<<endl;
cout<<"Mathematics:"<<students[i].maths<<endl;
break;
}
}
if (state) cout<<"There is no student with such name\n";
}while(true);
}
Пример выполнения программы может выглядеть следующим образом:
Student name: Sergei Ivanov
Student name: Igor Petrov
Student name: Ivan Sidorov
What is the student name? Sergei Ivanov
Physics: 5
Chemistry: 5
Mathematics: 4
What is the student name? Ivan Sidorov
Physics: 3
Chemistry: 3
Mathematics: 4
What is the student name? Igor Petrovski
There is no student with such name
What is the student name? Igor Petrov
Physics: 4
Chemistry: 5
Mathematics: 4
What is the student name? exit
В программе командой Marks student s[n] определяется массив students
переменных структуры Marks из n элементов (предварительно объявлена целочисленная
константа const int n=3). Заполнение элементов массива выполняется в рамках
оператора цикла. Индексная переменная i пробегает значения от 0 до n-1 включительно.
Каждый раз выводится запрос на ввод имени учащегося и это имя командой
gets(students[i].name) записывается в поле name переменной структуры
students[i]-элемента массива students. Оценки по трем предметам для каждого
учащегося определяются как случайные числа командами students[i].
phys=3+rand()%3, students[i].chem=3 + rand( )%3 и
students[i].maths=3+rand () %3. К базовой оценке 3 прибавляется целое случайное
число в диапазоне от 0 до 2 включительно (результат команды rand()%3 - остаток от
деления случайного числа на 3).
Командой char s [80] объявляется символьный массив, в который будет выполняться
считывание вводимого пользователем имени учащегося для отображения его оценок. Этот
массив используется в операторе цикла do{ . . . }while (true). Причем стоит
обратить внимание, что в качестве проверяемого условия указано логическое значение
true, что означает, что формально цикл бесконечный. Возможность выхода из этого цикла
предусмотрена в самом цикле. В начале цикла командой cout<<"What is the student
name?" выводится запрос на ввод имени учащегося. Введенное пользователем имя с
помощью команды gets(s) считывается и записывается в массив s. После того, как имя
учащегося введено, выполняется условный оператор
if (!strcmp(s, "exit")) return 0.
Этой командой предусмотрена возможность не только выхода из оператора цикла, но и
завершения работы программы: если пользователь в качестве имени введет exit, работа
программы завершается (инструкция return 0). Там же командой state=true
логической переменной state (переменная используется для индикации того, найдено
совпадение имен или нет) присваивается значение true. После этого посредством
оператора цикла выполняется последовательный перебор элементов массива students и
производится сравнивание значений строк, записанных в массив s и в поле-массив
students[i].name. Для сравнения строк используется функция strcmp(). Напомним,
что если строки совпадают, то в качестве значения функцией возвращается 0, поэтому в
условном операторе указано условие !strcmp(students[i].name, s). Если условие
истинно, командой state=false меняется состояние логической переменной state,
после чего выводится информация об оценках соответствующего учащегося. В конце
условного оператора размещена команда break для преждевременного выхода из
оператора цикла - если совпадение найдено, продолжать поиск не имеет смысла. Если
совпадения нет (т.е. введенное пользователем имя не найдено при просмотре полей
элементов массива students), переменная state имеет значение true. На этот случай
предусмотрена команда
if (state) cout << "There is no student with such name\n".
Битовые поля
Битовые поля - это особый вид полей структуры. Они используются для плотной
упаковки данных, например, флажков типа «да/нет». Минимальная адресуемая ячейка
памяти – 1 байт, а для хранения флажка достаточно одного бита. При описании полей
структуры в явном виде можно указывать размер полей. Минимальный размер поля
структуры - один бит. Общий синтаксис объявления структуры с явным указанием
размеров полей имеет вид:
struct имя
{
тип_поля1 имя_поля1: размер1;
тип_поля2 имя_поля2: размер2;
тип_поляЫ имя_поляЫ: размеры;
};
При описании битового поля после имени через двоеточие указывается длина поля в
битах (целая положительная константа):
Struct Options
{
Bool cеnterX: 1;
Bool centerY: 1;
Unsigned int shadow : 2;
Unsigned int pаlette : 4;
};
Битовые поля могут быть любого целого типа. При этом для части полей можно указывать
размер, а для других - нет. Если поле имеет размер в один бит, в качестве его типа
указывается unsigned (у числа, реализованного с помощью одного бита, не может быть
знака). Имя поля может отсутствовать, такие поля служат для выравнивания на
аппаратную границу. Доступ к полю осуществляется обычным способом - по имени.
Адрес поля получить нельзя, однако в остальном битовые поля можно использовать точно
так же, как обычные поля структуры. Следует учитывать, что операции с отдельными
битами реализуются гораздо менее эффективно, чем с байтами и словами, так как
компилятор должен генерировать специальные коды, и экономия памяти под переменные
оборачивается увеличением объема кода программы. Размещение битовых полей в памяти
зависит от компилятора и аппаратуры.
//Явное указание размеров полейструктуры
#include <iostream>
using namespace std;
struct BitFields
{ unsigned int state:1;
int n:2;
int m;
} str;
int main ()
{ cout<<"Enter a number:";
cin>>str.m;
str.state=str.m%2;
str.n=str.m%4-2;
cout<<"state=" << str.state<<endl;
cout<<"n=”<< str.n<<endl;
return 0;
}
У структуры BitFields три поля: однобитовое целочисленное поле state, двухбитовое
целочисленное поле n и целочисленное поле m (размер поля не указан).
Поле state может принимать всего два значения: 0 или 1. Поле n принимает
целочисленные значения в диапазоне от -2 до 1 включительно, т.е. всего 4 возможных
значения: напомним, что старший бит определяет знак числа, поэтому двухбитовое число
11 соответствует числу -2, двухбитовое число 10 соответствует числу -1, число 00
соответствует нулю и число 01 соответствует числу 1.
В главном методе программы выводится запрос на ввод пользователем целого числа. Это
число записывается в поле m переменной str структуры BitFields. В переменную
state заносится остаток от деления этого числа на 2, а в переменную n записывается
остаток от деления введенного пользователем числа на 4 минус 2. Значения полей
переменной str структуры BitFields выводятся на экран. В результате можем получить
нечто наподобие следующего:
Enter a number: 9
state = 1
n = -1
Таким образом, путем явного указания размеров полей структуры удалось добиться
экономии системных ресурсов: для записи значений используется минимально
необходимое количество бит. Возникает естественный вопрос: а что будет, если
присваиваемое битовому полю (т.е. полю, для которого явно указан размер) значение,
выходит за допустимые для этого поля пределы? Например, что будет, если полю п
значение присваивать с помощью команды str.n=str.m%4-4? Ответ такой: в указанных
случаях происходит автоматическое отбрасывание лишних бит. В частности, если поле m
равно 9, а поле n определяется как str.n=str.m%4-4, то этому полю будет присвоено
значение 1. Поясним это. Так, остаток от деления 9 на 4 есть 1. Если отнять 4, получим -
3. В двоичном представлении число -3 имеет вид (последние три бита, все старшие биты
равны 1) 101. Старшие биты отбрасываются, и в результате в двухбитовом представлении
получаем 01, что соответствует числу 1.
Объединение (union)
Объединение (union) представляет собой частный случай структуры, все поля которой
располагаются по одному и тому же адресу. Формат описания такой же, как у структуры,
только вместо ключевого слова struct используется слово union. Длина объединения
равна наибольшей из длин его полей. В каждый момент времени в переменной типа
объединение хранится только одно значение, и ответственность за его правильное
использование лежит на программисте.
Объединение применяют для экономии памяти в тех случаях, когда известно, что больше
одного поля одновременно не требуется:
#include <iostream.h>
int main()
{
enum Paytype {CARD, CHECK};
Paytype ptype;
union payment
{
char card[25];
long check;
} info;
/*присваивание значений info и ptype*/
Switch (ptype)
{
case CARD: cout <<Оплата по карте: “<<info.card; break;
case CHECK: cout <<Оплата чеком: “<<info.check; break;
}
return 0;
}
Объединение часто используют в качестве поля структуры, при этом в структуру удобно
включить дополнительное поле, определяющее, какой именно элемент объединения
используется в каждый момент. Имя объединения можно не указывать, что позволяет
обращаться к его полям непосредственно:
#include <iostream.h>
int main()
{
enum Paytype {CARD, CHECK};
struct
{
Paytype ptype;
Union
{
char card[25];
long check;
};
} info;
... /*присваивание значений info*/
switch (info.ptype)
{
case CARD: cout <<Оплата по карте: “<<info.card: break;
case CHECK: cout <<Оплата чеком: “<<info.check; break;
}
return 0;
}
//Использование объединений
#include <iostream>
using namespace std;
union nums
{ unsigned short int n;
short int m;
};
void show(nums a )
{ cout << "n =”<< a.n << endl;
cout << "m = "<<a.m<<endl;
cout<<endl ;
};
int main()
{ nums un;
un.m=1;
show(un);
un.m=32767;
show(un);
un.m=65535;
show(un);
un.m=-1;
show(un);
un.m=-65536;
show(un);
return 0;
}
Объявленное в программе объединение nums содержит два члена: целочисленную
беззнаковую переменную n типа unsigned short int и целочисленную переменную m
типа short int. Подчеркнем, что для записи обеих переменных используется общая
область памяти. Обычно объем памяти для хранения данных объединения выбирается
исходя из размера самой большой (по типу) переменной. В данном случае размер памяти
для хранения переменных одинаков, разница только в интерпретации записанных в эту
память значений.
Ситуация следующая: при обращении к переменной n или m, являющихся членами
объединения, просматривается одна и та же область памяти. Поэтому изменяя
переменную n мы изменяем переменную m и наоборот. На все происходящее можно
посмотреть и с другой точки зрения: есть область памяти, к которой можно обращаться
через разные переменные, и в зависимости от типа переменной по-разному
интерпретировать записанное в память значение. Нечто подобное иллюстрирует и
приведенный в листинге программный код: в главном методе программы создается
экземпляр un объединения nums, одному из членов экземпляра объединения
присваивается значение, после чего проверяется значение другого члена. Для удобства в
программе описана функция show(), которой выводятся на экран значения членов
экземпляра объединения (имя экземпляра объединения указывается аргументом
функции).
Проследим, каков будет результат выполнения программного кода. Для этого необходимо
вспомнить некоторые особенности форматов unsigned short int и short int. Для
используемого компилятора диапазон изменения чисел типа short int составляет от -
32768 до 32767. Значения для чисел типа unsigned short int лежат в пределах от 0 до
65535. И в том, и в другом случае числа реализуются в виде 16-битовых двоичных
последовательностей. Другими словами, в область памяти, выделенную под экземпляр
объединения, записывается последовательность из 16-ти нулей и единиц. Если обращение
к области памяти осуществляется через переменную m, то этот двоичный код
интерпретируется как число типа short int, а при обращении к памяти через
переменную n код интерпретируется как число типа unsigned short int. Но сам код
один и тот же! Сначала выполняется присваивание m=1. В этом случае как член n, так и
член m получают одинаковые значения (точнее, одно и то же значение интерпретируется
одинаково), поэтому результатом выполнения команды show(un) будет
n = 1
m = 1
Аналогичный результат получаем в результате присваивания un.m=32767 (оба члена
имеют одинаковое значение):
n = 32767
m = 32767
Ситуация принципиально меняется после выполнения команды un.m=65535. В
результате получим:
n = 65535
m = -1
Чтобы понять, почему так происходит, более детально рассмотрим процедуру
интерпретации чисел в двоичном коде для разных типов данных. Значение 65535 выходит
за допустимые пределы диапазона данных short int, поэтому при записи значения в
переменную m старшие биты в двоичном представлении числа отбрасываются. Само по
себе значение 65535 в двоичном коде представляется в виде 0...00111...11, т.е. 16-ть
единиц с нулевыми старшими битами (общее количество бит зависит от специфики
используемого компилятора). После отбрасывания “лишних” битов остаются последние
16 единиц.
Таким образом, в память, выделенную под экземпляр объединения, записано значение
111... 11. Если это значение интерпретируется как unsigned short int (переменная n),
получаем в десятичной системе значение 65535. Если же значение интерпретируется как
такое, что относится к типу short int, ситуация несколько иная. В этом случае старший
бит отвечает за знак числа, и единичный старший бит означает, что число отрицательное.
Для получения соответствующего десятичного значения необходимо выполнить
побитовую инверсию, прибавить единицу, перевести число в десятичную систему
счисления и добавить “минус”. После побитовой инверсии получаем 000. ..00. Прибавив
единицу, получаем число вида 000. ..01, что соответствует числу 1, а со знаком “минус”
это -1 (значение переменной m).
Аналогичная ситуация складывается после выполнения команды un.m=-1.
Описанные выше преобразования необходимо проделать в обратном порядке, после чего
станет ясно, что значением переменной n будет 65535. Кстати, такой же результат
получим, если присвоим un.n=65535. А вот если выполнить команду un.m=-65536 и
затем вывести значения переменных экземпляра объединения, получим
n = 0
m = 0
Чтобы перевести число -65536 в двоичное представление, необходимо модуль этого
числа (т.е. 65536) перевести в двоичный код, инвертировать каждый бит и прибавить
единицу. Число 65536 в двоичном представлении имеет вид 000...001000...00 (в данном
случае количество N старших незначащих нулевых битов значения не имеет). После
инвертирования получаем 111...110 111...11. Прибавив единицу, получим
1000...001000...00.
В переменную m записываются последние 16 бит, т.е. число 000...00, или 0. Такое же
значение и у переменной n.
Объединения применяются также для разной интерпретации одного и того же битового
представления (но, как правило, в этом случае лучше использовать явные операции
преобразования типов). В качестве примера рассмотрим работу со структурой,
содержащей битовые поля:
struct Options
{
bool centerX:1;
bool centerY:2;
unsigned int shadow:2;
unsigned int palette:4;
};
union
{
unsigned char ch;
options bit:
} option = {0xC4};
cout << option.bit.palette;
option.ch= 0xF0; // наложение маски
По сравнению со структурами на объединения налагаются некоторые ограничения. Смысл
некоторых из них станет понятен позже:
1. Объединение может инициализироваться только значением его первого
элемента;
2. Объединение не может содержать битовые поля;
3. Объединение не может содержать виртуальные методы, конструкторы,
деструкторы и операцию присваивания;
4. Объединение не может вxодить в иерархию классов.