Ссылка — это псевдоним для другой переменной. Они объявляются при помощи символа &. Ссылки должны быть проинициализированы при объявлении, причем только один раз.
Тип «ссылка на type» определяется следующим образом:
type& имя_перем.
int& i, double& var.
Инициализация ссылки производится следующим образом:
int i = 0;
int& iref = i;
Физически iref представляет собой постоянный указатель на int — переменную типа int* const. Ее значение не может быть изменено после ее инициализации. Ссылка отличается от указателя тем, что используется не как указатель, а как переменная, адресом которой она была инициализированна:
iref++; //тоже самое, что i++
int *ip = &iref; //тоже самое, что ip = &i;
Таким образом, iref становится синонимом переменной i.
При передаче больших объектов в функции с помощью ссылки его не копируют в стек и следовательно, повышают производительность.
//Программа 1.
#include <iostream.h>
void incr (int&);
void main(void)
{
int i = 5;
incr(i);
cout<< «i= » << i << «\n»;
}
void incr (int& k)
{
k++;
}
Поскольку ссылка это псевдоним, то при передаче объ-екта в функцию по ссылке внутри нее объект можно изменять. Для указания компилятору что ссылка используется только в целях повышения эффективности используется модификатор const.
Ссылки не могут ссылаться на другие ссылки или на поле битов.
Не может быть массивов ссылок или указателей на ссылку.
Ссылка может использоваться для возврата результата из функции. Возвратить результат по ссылке — значит возвратить не указатель на объект и не его значение, а сам этот объект.
//Программа 2.
#include <iostream.h>
int& func (int&);
void main (void)
{
int i = 5;
int& y = func(i+1);
cout << «i = » << i << «\n»;
cout << «y = » << y << «\n»;
}
int& func (int& k)
{
return k;
}
1?2? Спецификатор const.
Существуют 3 вида именнованных констант:
— имя любого массива или функции;
— имена членов перечисления;
— любое имя любого типа, в определении которого присутствует модификатор const.
const i = 5;
const char *ip = &c;
Поскольку модификация такого объекта-константы запре-щена, он должен быть инициализирован.
const int *ip; //константой является объект на который указывает указатель;
int* const ip; //сам указатель является константой
const char *pc = «Это строка»;
pc[2] = ‘a’; //ошибка
pc = «Это другая строка»; //верно
char* const pc = «Это строка»;
pc[2] = ‘a’; //верно
pc = «Это другая строка»; //ошибка
Использование const предпочтительнее по сравнению с #define, так как использование константы контролирует компилятор.
1?3? Спецификатор enum.
Спецификатор enum позволяет программисту создавать собственные типы.
enum weekDays { Monday, Tuesday, Wensday, Thursday, Friday };
weekDays days;
Переменная days теперь может принимать одно из 5 значений.
days = Wensday;
Идентификаторы перечисления представляют собой целочисленные переменные, которые по умолчанию имеют значения 0,1,…, если не указаны другие значения.
enum colors { Red=2, Green=3};
1?4? Cпецификатор inline.
Может быть использован перед определением функции для того, чтобы компилятор помещал ее код непосредственно в место вызова функции.
//Программа 3.
#include <stdio.h>
#include <conio.h>
#include <dos.h>
void notinline(void) { };
inline void isinline(void) { };
void PrintTime(void);
void main(void)
{
printf(«Starting notinline\n»);
PrintTime();
for(long index=1; index<=5000000; index++)
notinline();
PrintTime();
printf(«Starting isinline\n»);
for(long index=1; index<=5000000; index++)
isinline();
PrintTime();
}
void PrintTime(void)
{
struct time t;
gettime(&t);
printf(«Сейчас: %2d:%02d:%02d.%02d\n»,
t.ti_hour, t.ti_min, t.ti_sec, t.ti_hund);
}
1?5? Динамически распределяемая память
В отличие от статических и автоматических данных, память под которые распределяется компилятором, динамически распределяемая память выделяется программой самостоятельно. Время жизни таких объектов также определяется программой. Память выделяется по мере необходимости и должна осво-бождаться как только данные, содержащиеся в ней больше не нужны. Доступ к ней осуществляется при помощи указателей.
1?5?1? Объявления указателей и приведение типов
Указатель представляет собой переменную, содержащую адрес другой переменной. Объявление указателя всегда должно устанавливать его на некоторый конкретный тип, даже если этот тип void (что фактически означает указатель на любой тип).
Если type есть любой предопределенный или опре-деленный пользователем тип, включая void, то объявление
type *ptr;
объявит ptr как «указатель на тип type». К объявленному таким образом объекту ptr применимы все правила, связанные с контекстом, продолжительностью жизни и видимостью.
Указатель со значением NULL это адрес, гарантированно отличный от любого допустимого указателя, используемого в программе. Объявление
void *vptr;
объявляет, что vptr — это родовой указатель, которому может быть присвоено любое значение «указатель на тип type» без выдачи компилятором сообщений. Без правильного приведения типов между «указателем на тип type1» и «указателем на тип type2», где type1 и type2 это различные типы, присвоение может вызвать предупреждение или ошибку компилятора. Если type1 это указатель на void, приведения типов не требу-ется. Если type2 это указатель на тип void, то в С приве-дение не нужно.
Ограничения присвоения также существуют относительно указателей разных размеров (near, far и huge). Можно присвоить меньший указатель большему, не вызвав ошибки, но нельзя выполнить обратную операцию, не выполнив явную опе-рацию приведения. Например,
char near *ncp;
char far *fcp;
fcp = ncp; // допустимо
ncp = fcp; // недопустимо
ncp = (char near*)fcp; // теперь допустимо
1?5?2? Операции с указателями
Получить значение объекта, который адресуется указа-телем, можно при помощи операции * :
int * s; //s не определено
int b, a = 10; //b не определено
s = &a; //s указывает на а
b = *s; //b = 10
Внутренние арифметические операции с указателями зависят от действующей модели памяти и наличия переопре-деляющих модификаторов указателя. Разность между двумя значениями указателей имеет смысл только в том случае, если оба они указывают на один массив.
Арифметические операции с указателями ограничены сложением, вычитанием и сравнением. Арифметические операции с указателями объектов типа «указатель на тип type» автоматически учитывают размер этого типа, то есть число байт, необходимое для хранения в памяти объекта данного типа.
При выполнении арифметических операций с указателями предполагается, что указатель указывает на массив объектов. Таким образом, если указатель объявлен как указатель на type, то прибавление к нему целочисленного значения перемещает указатель на соответствующее количество объек-тов type. Если type имеет размер 10 байтов, то прибавление целого числа 5 к указателю этого типа перемещает указатель в памяти на 50 байт. Разность представляет собой число элементов массива, разделяющих два значения указателей. Например, если ptr1 указывает на третий элемент массива, а
ptr2 на десятый, то результатом выполнения вычитания ptr2 — ptr1 будет 7.
Когда с «указателем на тип type» выполняется операция сложения или вычитания целого числа, то результат также будет «указателем на тип type». Если type не является массивом, то операнд указателя будет рассматриваться как указатель на первый элемент «массива типа type» длиной sizeof(type).
Конечно, такого элемента, как «указатель на следующий за последним элемент», однако указатель может принимать это значение. Если P указывает на последний элемент массива, то значение P+1 допустимо, но P+2 неопределено. Если P указывает на элемент за последним элементом массива, то допустимо значение P-1, когда указатель установлен на последний элемент массива. Однако использование указа-теля на элемент вне массива ведет к непредсказуемым результатам работы программы.
Для информации: P+n можно представить себе как перемещение указателя на (n*sizeof(type)) байт вперед, пока указатель остается в допустимых границах (не далее первого за концом массива элемента). Контроль за допустимыми зна-чениями указателей возлагается на программиста.
1?5?3? Преобразования указателей
Указатели одного типа могут быть преобразованы в указатели другого типа при помощи следующего механизма приведения типов:
char *str
int *ip
str = (char*)ip;
В более общем виде, приведение (type*) преобразует указатель в тип «указатель на тип type».
1?5?4? Операции new и delete
Операции new и delete выполняют динамическое распределение и отмену распределения памяти, аналогично, но с более высоким приоритетом, нежели стандартные библиотечные функции семейства malloc и free.
Упрощенный синтаксис:
указатель-имени = new имя <инициализатор-имени>;
…
delete указатель-имени;
Имя может быть любого типа, кроме «функция, возвра-щающая…»(однако, указатели функций здесь допустимы). new пытается создать объект с типом «имени», распределив
(при возможности) sizeof(имя) байт в свободной области па-мяти (которую также называют «кучей»). Продолжительность существования в памяти данного объекта — от точки его создания и до тех пор, пока операция delete не отменит распределенную для него память, либо до конца работы прог-раммы.
В случае успешного завершения new возвращает указатель нового объекта. Пустой указатель означает неудачное завершение операции (например, недостаточный объем или слишком большая фрагментированность кучи). Как и в случае malloc, прежде чем пытаться обращаться к новому объекту, следует проверить указатель на наличие пустого значения. Возвращаемый указатель будет иметь правильный тип, «ука-затель имени», без необходимости явного приведения типов.
name *nameptr // name может иметь любой тип,
//кроме функции
…
if (!(nameptr = new name)) {
errmsg(«Недостаточно памяти для name»);
exit (1);
}
// использование *nameptr для инициализации объекта
//new name
…
delete nameptr; // удаление name и отмена
//распределения sizeof(name)
//байтов памяти
new, будучи ключевым словом, не нуждается в прототипе.
1?5?5? Операция new с массивами
Если «имя» это массив, то возвращаемый new указатель указывает на первый элемент массива. При создании с помощью new многомерных массивов следует указывать все размерности массива:
mat_ptr = new int[3][10][12]; // так можно
mat_ptr = new int[3][][12]; // нельзя
…
delete [] mat_ptr; //освободить память, занятую
//массивом, на который указывает
//mat_ptr
Существует глобальная переменная _new_handler, представляющая собой указатель на функцию типа void _new_handler(void). Если new не может выделить память, она проверяет _new_handler. Если _new_handler == NULL, то возвращается 0, иначе вызывается функция, на которую указывает _new_handler.
Пример:
void MyNewHandler(void)
{
puts(«Out of memory.»);
// здесь выполняются необходимые действия по
// завершению программы
exit(1);
}
_new_handler = &MyNewHandler;
1?5?6? Инициализаторы с операцией new
Другим преимуществом операции new по сравнению с malloc является возможность инициализации. При отсутствии явных инициализаторов объект, создаваемый new, содержит непредсказуемые данные («мусор»). Объекты, распределяемые new, за исключением массивов, могут инициализироваться соответствующим выражением в скобках:
int_ptr = new int(3);
1?5?7? Проблемы, возникающие при использовании динамически распределяемой памяти
1). Недоступные блоки — блоки памяти, указатели на которые потеряны.
2). Висящие ссылки — указатели на освобожденные блоки памяти.
3). Повторное освобождение динамической памяти. Недос-тупные блоки возникают после выделения памяти при присваивании указателю какого — либо другого значения или при уничтожении локального указателя после выхода из области видимости.
Пример 1:
int *a1, *a2;
a1 = new int[1000];//выделили память
… //что-то делаем
a1 = a2; //ошибка — присвоение а1 другого
//значения — память недоступна
Пример 2:
void func(void)
{
int * a1;
a1 = new int[1000];
…
} //ошибка — при выходе из функции автоматически
//уничтожен a1,а память тем не менее осталась
//занята и недоступна.
Необходимо следить за указателями на выделенную память:
int * c;
void func1(void) {
int * a1;
a1 = new int[1000];
c = a1; //если данные по адресу a1 необходимы вне
//func1
… //иначе освободить перед выходом из функции
}
void func2(void)
{
…
delete c;
}
Висящие ссылки возникают при освобождении памяти, на которую указывает более чем 1 указатель:
int *a = new int[1000];
int *a1 = a;
…
delete a;
d = *(a1+50);
//опасно — a1 уже нельзя использовать для обращения к
//массиву!
…
Если нет освобождения памяти, программист в зависимости от конкретной ситуации может посчитать это ошибкой, а может и нет (хотя это в любом случае уменьшает доступные ресурсы памяти), повторное освобождение безусловно ошибочно и скорее всего приведет к зависанию системы. Как правило, подобные ошибки не проявляются немедленно после появления, что затрудняет процесс отладки.
1?5?8? Указатель на void
Такому указателю можно присвоить значение указателя на любой базовый тип.
void *v_ptr;
int *int_ptr = new int;
v_ptr = int_ptr;
//Программа 4.
#include <stdio.h>
#include <conio.h>
void swap (void *&item1, void *&item2)
{
void *temp = item1;
item1 = item2;
item2 = temp;
}
void main(void)
{
int *i = new int,
*j = new int;
*i = 5;
*j = 20;
clrscr();
swap((void *&)i,(void *&)j);
printf(» *i = %d, *j = %d \n», *i,*j);
float *x = new float,
*z = new float;
*x = 5.0;
*z = 20.0;
swap((void *&)x,(void *&)z);
printf(» *x = %f, *z = %f \n», *x,*z);
}
1?6? Перегрузка функций.
Имена функций могут быть перегружены в пределах одной области видимости. Компилятор отличает одну функцию от другой по сигнатуре. Сигнатура задается числом, порядком следования и типами ее параметров.
//Программа 5.
#include <stdio.h>
#include <string.h>
int noName (int first)
{
return first*first;
}
int noName (unsigned first)
{
return first*first;
}
char noName (char first)
{
return first*first;
}
int noName (int first,char *second)
{
return first*strlen(second);
}
float noName (float r)
{
return r*r;
}
double noName (double r)
{
return r*r;
}
void main(void)
{
printf(«%d\n», noName(4));
printf(«%d\n», noName((unsigned)4));
printf(«%c\n», noName(‘c’));
printf(«%d\n», noName(4,»cлово»));
printf(«%0.2f\n», noName((float)1.2));
printf(«%0.2lf\n», noName((double)1.2));
}
1?7? Значение формальных параметров по умолчанию.
Формальный параметр может иметь значение по умолчанию. Все параметры стоящие справа от него тоже должны иметь значения по умолчанию. Эти значения передаются в функцию, если при вызове данные параметры не указаны.
//Программа 6.
#include <stdio.h>
void noName1 (float x, int y, char z=’b’)
{
printf(«x = %0.1f y = %d, z = %d \n», x,y, (int)z);
}
void noName2 (float x, int y=16, char z=’a’)
{
printf(«x = %0.1f y = %d, z = %d \n», x,y, (int)z);
}
void noName3 (float x=1.3, int y=4, char z=’c’)
{
printf(«x = %0.1f y = %d, z = %d \n», x,y, (int)z);
}
void main(void)
{
noName1(1.0,2);
noName2(100.0);
noName3();
}
1?8? Использование системы ввода/вывода
Система ввода/вывода – неотъемлемая часть среды прог-раммирования С++, и она занимает в языке особое место?
В заголовочном файле Iostream.h содержаться сле-дующие объявления одного потока ввода и трех потоков вывода:
extern istream_withassign _Cdecl cin; //Объект потока ввода
extern ostream_withassign _Cdecl cout; //Объект потока вывода
extern ostream_withassign _Cdecl cerr; //Объект потока вывода ошибок
extern ostream_withassign _Cdecl clog; //Объект буфферизованного потока вывода ошибок
cin >> v; // Прочитать из стандартного потока ввода
cout << v; // Записать в стандартный поток вывода
Обычно стандартный поток вывода cout присоединен к экрану терминала? Простейшая программа, использующая опера-цию вывода, имеет следующий вид:
#include <iostream.?h>
void main(void)
{
cout << “Hello, world!”;
}
Приоритет операций << и >> позволяет помещать в опера-тор вывода арифметические выражения, не прибегая к исполь-зованию скобок, зато при использовании логических вы-ражений или операций присваивания скобки использовать необ-ходимо:
cout << “x+y =” << x+y <<”; x&y =” << (x&y) << “\n”;
Для ввода данных в программу используется стандартный поток ввода? Он имеет много общего с выводом данных? Обычно стандартный поток ввода соединен с клавиатурой cin.
int i;
float y;
cin >> i >> f ;
1.8.1. Форматирование вывода.
Для форматирования вывода можно установить несколько флагов, для этого используются функции-члены flags, setf, unsetf.
unsigned v =12345;
cout << «Before: » << v << endl;
cout.setf(cout.hex); //Модификация потока
cout << «After: » << v << endl;
Для форматирования можно подключить заголовочный файл Iomanip.h, тогда используем манипуляторы
cout << «In hexadecimal v == » << hex << v << endl;
cout << «In decimal v == » << dec << v << endl;
ends вставить нулевой завершающий символ в строку
endl начать новую строку
flush выполнить довывод в поток
oct 8-ричная система счисления
setbase(int n) установить систему счисления с основанием n
setfill(int c) использовать символ с для заполнения при выравнивании
setw(unt n) установить ширину поля
setprecision(int n) установить точность вывода значений после запятой
Можно писать свои собственные манипуляторы, включив заголовочный файл Iomanip.h и определив функцию ссылочного типа
ostream&.
ostream& bell(ostream &os) {
return os << «\a»; //код звонка
}
cout << bell << «Ding!»;
Для выравнивания по правому краю целочисленных пере-менных можно задать: cout.width(8); но он не оказывает влияние на следующее выводимое значение.
1.9. Лабораторная работа 1
Цель работы: Ознакомиться с некоторыми расширениями языка С, позволяющими перейти к объектно-ориентированному прог-раммированию С++.
1? Порядок выполнения работы.
1. Ознакомьтесь с теоретическими сведениями?
2. Разберите работу программ 1,2,3. В программе 3 об-
ратите внимание на время выполнения первого и вто- рого цикла.
3. Разберите работу программы 4.
Что изменится, если изменить на swap(void *item1,void *item2)?
4. Разберите работу программы 5.
Что если из последней строки программы убрать перацию приведения типов (double)?
Что если добавить определение функции?
float noName(int first) {
return(float) first*first;
}
5. Разберите работу программы 6.
Что если изменить имя второй функции на noName1, а вторую строку функции main на noName1(100.0)?
Что будет, если параметру х в noName1 присвоить значение по умолчанию равное 15, а остальным не присваивать значений по умолчанию?
6? Выполните индивидуальное задание?
2? Индивидуальные задания.
В программах использовать стандартные потоки ввода-вывода.
1. Составить программу, в которой задается размер числового массива, вводятся данные в массив и рассчи-тывается их среднее арифметическое и дисперсия.
2. Написать функцию, которая получает по ссылке координаты левого верхнего и правого нижнего углов и символ, которым рисуется рамка по координатам углов.
3. Написать функцию, которая по ссылке получает символьную строку из программы и подсчитывает частоту появления букв латинского алфавита в этой строке символов. Oкончание строки — Enter.
Другие символы игнорировать. Количество букв вернуть в вызывающую программу и вывести на экран.
4. Составить свою функцию сравнения двух строк символов, получающую в качестве параметров ссылки на строки и возвращающую 1,если строки одинаковы, или 0, если строки различны.
5. Составить свою функцию выделения подстроки, по-лучающую ссылку на строку и номера символов начала и конца подстроки.
6. Составить свою функцию для копирования строки. Передается ссылка на строку-источник и ссылка на строку-результат.
7. Мистер Браун, человек со странностями, всегда читает справо налево. Составить функцию, облегчающую диалог с мистером Брауном, для которой передается ссылка на введенную строку и выводится на экран реверсированная строка. Например: ввод «Галина», вывод «анилаГ».
8. Составить программу, в которой в массив переменной длины, определяемой пользователем, вводятся числовые данные с клавиатуры, а затем сортирются в функции сортировки и записываются в файл.
9. Составить программу, которая читает определенное количество данных из файла в массив переменной длины. Размерность массива задается с клавиатуры.
10. Составить программу, которая формирует список группы студентов (ФИО, адрес, возраст, телефон) и записывает на диск в файл. Количество записей вводится с клавиатуры. Использовать динамически выделяемую память под массив.
11. Определить угол (в градусах) между положением стрелок в начале суток и ее положением в h-часов, m-минут, s-секунд. Ввод времени осуществлять в основной программе и передавать по ссылке в функцию.
12. Определить h-количество часов, m-минут прошедших от начала суток до того момента, когда часовая стрелка повернулась на f-градусов. Написать несколько перегруженных функций для разного ввода параметра (в градусах, в виде строки, в радианах).
3? Отчет по работе
• Название работы и ее цель;
• Ответы на поставленные вопросы в порядке проведения работы;
• Листинг разработанной и отлаженной программы индивидуального задания?
4? Вопросы по лабораторной работе
1. Операции выделения и освобождения памяти в С++. Отличия в создании динамических и статических массивов.
2. Ошибки при распределении памяти.
3. Обработка ошибок при распределении памяти.
4. Указатели. Определение, инициализация и использо-вание указателей в программе. Указатель со значением NULL. Указатель на void.
5. Определение и инициализация ссылки в программе. В чем различие между ссылкой и указателем? Операции над ссылкой.
6. Константы. Преимущества использования const.
7. Перегружаемые функции. Каким образом компилятор определяет какая из перегруженных функций вызывается в данный момент в программе.
8. Использование значений формальных параметров по умолчанию в функции. Правила назначений параметров по умолчанию.
9. inline-функции. Отличие inline-функций от простых функций.
10. Перечисления. Использование перечислений в програм-ме.
11? Стандартные потоки ввода/вывода? Использование их в программе?
