Партнерка на США и Канаду по недвижимости, выплаты в крипто
- 30% recurring commission
- Выплаты в USDT
- Вывод каждую неделю
- Комиссия до 5 лет за каждого referral
Лекция 4
1. Указатели
Указатели широко используются в C++. Именно их наличие сделало этот язык более удобным для системного программирования. Идея работы с указателями состоит в том, что пользователь работает с адресом ячейки памяти и имеет возможность динамически создавать и уничтожать переменные.
Как правило, при обработке оператора объявления переменной
тип имя_переменной;
компилятор автоматически выделяет память под переменную в соответствии с указанным типом:
Доступ к объявленной переменной осуществляется по ее имени. При этом все обращения к переменной меняются на адрес ячейки памяти, в которой хранится ее значение. При завершении программы или функции, в которой была описана переменная, память автоматически освобождается.
Доступ к значению переменной можно получить иным способом — определить собственные переменные для хранения адресов ячеек памяти. Такие переменные называют указателями. С помощью указателей можно обрабатывать массивы, строки и структуры, динамически создавать новые переменные в процессе выполнения программы (а не на этапе компиляции), передавать адреса фактических параметров.
Указатель — это переменная, значением которой является адрес памяти, по которому хранится объект определенного типа (другая переменная). Например, если C - это переменная типа char, а P - указатель на C, то, значит, в P находится адрес, по которому в памяти компьютера хранится значение переменной C.
Как и любая переменная, указатель должен быть объявлен. При объявлении указателей всегда указывается тип объекта, который будет храниться по данному адресу:
Например:
int *p
Звездочка в описании указателя относится непосредственно к имени, поэтому, чтобы объявить несколько указателей, ее ставят перед именем каждого из них:
float *x, y, *z;
Операция получения адреса обозначается знаком &. Она возвращает адрес своего операнда. Например:
float a; //объявлена вещественная переменная a
float *adr_a; //объявлен указатель на тип float
adr_a = &a; //оператор записывает в переменную adr_a
//адрес переменной a
Операция разадресации * возвращает значение переменной, хранящееся в по заданному адресу, то есть выполняет действие, обратное операции &:
float a,b; //объявленs вещественная переменная a и b
float *adr_a; //объявлен указатель на тип float
adr_a=&a;
b = *adr_a; //оператор записывает в переменную b
//вещественное значение, хранящееся по адресу adr_a
Значение одного указателя можно присвоить другому. Если указатели одного типа, то для этого применяют обычную операцию присваивания. Рассмотрим ее на примере:
#include <iostream>
using namespace std;
int main()
{
float PI=3.14159, *p1, *p2;
p1=p2=&PI;
cout<<"По адресу p1="<<p1<<" хранится *p1="<<*p1<<"\n";
cout<<"По адресу p2="<<p2<<" хранится *p2="<<*p2<<"\n";
system ("pause");
return 0;
}
Если указатели ссылаются на различные типы, то при присваивании значения одного указателя другому, необходимо использовать преобразование типов.
Пример
#include <iostream>
using namespace std;
int main()
{
float PI=3.14159; //объявлена вещественная переменная PI
float *p1; //объявлен указатель на float - p1
double *p2; //объявлен указатель на double - p2
p1=&PI; //переменной p1 присваивается значние адреса PI
p2=(double *)p1; //указателю на double присваивается значение,
//которое ссылается на тип float
cout<<"По адресу p1="<<p1<<" хранится *p1="<<*p1<<"\n";
cout<<"По адресу p2="<<p2<<" хранится *p2="<<*p2<<"\n";
system ("pause");
return 0;
}
В указателях p1 и p2 хранится один и тот же адрес, но значения, на которые они ссылаются, оказываются разными. Это связано с тем, что указатель типа *float адресует 4 байта, а указатель *double - 8 байт. После присваивания p2=(double *)p1; при обращении к *p2 происходит следующее: к переменной, хранящийся по адресу p1, дописывается еще 4 следующих байт из памяти. В результате значение *p2 не совпадает со значением *p1.
2. Функции
Функция — это поименованный набор описаний и операторов, выполняющих определенную задачу. Функция может принимать параметры и возвращать значение. Информация, передаваемая в функцию для обработки, называется параметром, а результат вычислений функции ее значением. Обращение к функции называют вызовом.
Перед вызовом функция должна быть обязательно описана. Описание функции состоит из заголовка и тела функции:
тип имя_функции (список_переменных)
{
тело_функции
}
Заголовок функции содержит:
- Тип возвращаемого функцией значения. Он может быть любым. Если функция не возвращает значения, указывают тип void;
- имя_функции, под которым она будет вызываться;
- список_переменных — перечень передаваемых в функцию аргументов, которые отделяются друг от друга запятыми; для каждой переменной из списка указывается тип и имя;
Тело функции представляет собой последовательность описаний и операторов, заключенных в фигурные скобки.
В общем виде структура программы на C++ может иметь вид:
директивы компилятора
тип имя_1 (список_переменных)
{
тело_функции_1;
}
тип имя_2 (список_переменных)
{
тело_функции_2;
}
…
//основная функция
тип main (список_переменных)
{
// тело функции может содержать операторы вызова функций имя_1, имя_2….
тело_основной_функции;
}
Стоит отметить, что тексты функции могут следовать после главной функции main(). Однако заголовки необходимо перечислить до нее. Эти заголовки называют прототипом функции. Например:
директивы компилятора
тип имя_1 (список_переменных); //прототип функции имя_1
тип имя_2 (список_переменных); //прототип функции имя_2
…
//основная функция
тип main (список_переменных)
{
// тело функции может содержать операторы вызова функций имя_1, имя_2….
тело_основной_функции;
}
тип имя_1 (список_переменных)
{
тело_функции_1;
}
тип имя_2 (список_переменных)
{
тело_функции_2;
}
Вызвать функцию можно в любом месте программы. Для этого необходимо указать ее имя и в круглых скобках через запятую перечислить имена или значения аргументов, если такие имеются:
имя_функции (список_переменных);
Теперь давайте напишем программу, которая выводит на экран треугольник, построенный из символов «звездочка» и «пробел».
Если тип возвращаемого значения не void, то функция может входить в состав выражений. Для того чтобы функция вернула какое-либо значение, в ней должен быть оператор return (выражение).
Пример 1. Программа, которая выводит таблицу умножения на заданное число. Здесь функция func выполняет операцию умножения двух целых чисел:
#include <iostream>
using namespace std;
int func(int a, int b) //описание функции
{ //функция возвращает результат умножения двух целых чисел:
return (a*b);
}
void main ()
{
int i, j;
cin>>i;
for (j=1; j<=10; j++)
//вызов функции, параметры a и b заменяются именами аргументов i и j
cout<<i<<"*"<<j<<"="<<func(i, j)<< endl;
system ("pause");
}
Переменные, описанные внутри функции, а также переменные из списка аргументов, являются локальными. Например, если программа содержит пять разных функций, в каждой из которых описана переменная N, то для C++ это пять различных переменных. Область действия локальной переменной не выходит за рамки функции.
Переменные, определенные до объявления всех функций и доступные всем функциям, называют глобальными.
Обмен информацией между вызываемой и вызывающей функциями осуществляется с помощью механизма передачи параметров. Список_переменных, указанный в заголовке функции называется формальными параметрами или просто параметрами функции. Список_переменных в операторе вызова функции — это фактические параметры или аргументы.
Передача параметров выполняется следующим образом. Вычисляются выражения, стоящие на месте фактических параметров. Затем формальным параметрам присваиваются значения фактических. Выполняется проверка типов и при необходимости выполняется их преобразование.
Передача параметров в функцию может осуществляться по значению и по адресу.
При передаче данных по значению функция работает с копиями фактических параметров, и доступна к исходным значениям аргументов у нее нет. При передаче по адресу в функцию передается не переменная, а ее адрес, и, следовательно, функция имеет доступ к ячейкам памяти, в которых хранятся аргументов. Таким образом, данные, переданные по значению, функция изменить не может, в отличие от данных, переданных по адресу.
Для передачи данных по адресу требуется после типа переменной указать символ «*» (операция разадресации). Чтобы передать в функцию фактический параметр по адресу, нужно использовать операцию взятия адреса «&».
Если требуется запретить изменение параметра внутри функции, используют модификатор const. Заголовок функции в общем виде будет выглядеть так:
тип имя_функции (const тип_переменной* имя_переменной, …)
Возврат результата из функции в вызывающую ее функцию осуществляется оператором
return (значение);
Работает оператор так. Вычисляется значение выражения, указанного после return и преобразуется к типу возвращаемого функцией значения. Выполнение функции завершается, а вычисленное значение передается в вызывающую функцию. Любые операторы, следующие в функции за оператором return, игнорируются. Программа продолжает свою работу с оператора следующего за оператором вызова данной функции.
#include <iostream>
using namespace std;
int f1(int i) //данные передаются по значению
{
i++;
return (i);
}
int f2 (int* j) //данные передаются по адресу
{
return((*j)++);
}
int f3 (const int* k) //изменение параметра не предусмотрено
{
return (*k);
}
int main ()
{
int a;
cin>>a;
f1(a);
cout<<"f(a)="<<f1(a)<<"\n"<<"a="<<a<<"\n"; //f(a)=6 a=5
f2(&a);
cout<<"a="<<a<<"\n"; //a=6
f3(&a);
cout<<"a="<<a<<"\n"; //a=6
system ("pause");
return 0;
}
Оператор return может отсутствовать в функциях типа void.
Также функция может содержать несколько операторов return, если это определенно потребностями алгоритма.
Пример. Найти наибольший общий делитель (НОД) для значений x, y, x+y.
Идея решения состоит в следующем математическом факте: если х, у, z — три натуральных числа, то НОД(х, у, z) = НОД(НОД(х, у), z) – иначе говоря, нужно найти НОД двух величин, а затем НОД полученного значения и третьего числа.
#include <iostream>
using namespace std;
int evklid(int m, int n) //данные передаются по значению
{
while (m!=n)
if (m>n) m=m-n;
else n=n-m;
return (m);
}
int main ()
{
int x, y,nod;
cin>>x>>y;
nod=evklid(evklid(x, y),x+y);
cout<<"NOD="<<nod<<"\n";
system ("pause");
return 0;
}
Пример. Написать программу, запрашивающую N целых чисел и выводящих в текстовый файл все цифры этих чисел через запятую в обратном порядке.
#include <iostream>
#include <fstream>
using namespace std;
ofstream f;
void vyvod(int n) //данные передаются по значению
{
int k;
while (n!=0)
{
k=n%10;
f<<k;
n=n/10;
if (n!=0) f<<",";
}
f<<endl;
}
int main ()
{
int x, i,n;
f. open("a. txt",ios::out);
cin>>n;
for (i=1;i<=n;i++)
{
cin>>x;
vyvod(x);
}
f. close();
system ("pause");
return 0;
}
3. Классы памяти
Общий вид оператора описания переменных:
[класс памяти] [const] тип имя [инициализатор];
Рассмотрим некоторые составные части этого оператора.
Модификатор const показывает, что значение переменной изменять нельзя. Такую переменную называют именованной константой, или просто константой.
Константа должна быть инициализирована при объявлении. В одном операторе можно описать несколько переменных одного типа, разделяя их запятыми.
const char С = 'С' ; // символьная константа С
Описание переменной, кроме типа и класса памяти, явно или по умолчанию задает ее область действия. Класс памяти и область действия зависят не только от собственно описания, но и от места его размещения в тексте программы.
Область действия идентификатора — это часть программы, в которой его можно использовать для доступа к связанной с ним области памяти. В зависимости от области действия переменная может быть локальной или глобальной.
Если переменная определена внутри блока (напомним, что блок ограничен фигурными скобками), она называется локальной, область ее действия — от точки описания до конца блока, включая все вложенные блоки. Если переменная определена вне любого блока, она называется глобальной и областью ее действия считается файл, в котором она определена, от точки описания до его конца.
Областью видимости идентификатора называется часть текста программы, из которой допустим обычный доступ к связанной с идентификатором областью памяти. Чаще всего область видимости совпадает с областью действия. Исключением является ситуация, когда во вложенном блоке описана переменная с таким же именем. В этом случае внешняя переменная во вложенном блоке невидима, хотя он и входит в ее область действия. Тем не менее, к этой переменной, если она глобальная, можно обратиться, используя операцию доступа к области видимости - :: (например ::a=5).
Класс памяти определяет время жизни и область видимости программного объекта (в частности, переменной). Если класс памяти не указан явным образом, он определяется компилятором исходя из контекста объявления.
Время жизни может быть постоянным (в течение выполнения программы) и временным (в течение выполнения блока).
Классы памяти переменных
Для задания класса памяти используются следующие спецификаторы:
auto — автоматическая (локальная) переменная. Объектам данного типа память выделяется каждый раз при входе в блок и освобождается при выходе из него. Время ее жизни — с момента описания до конца блока. Для глобальных переменных этот спецификатор не используется, а для локальных он принимается по умолчанию, поэтому задавать его явным образом большого смысла не имеет.
register — аналогично auto, но память выделяется по возможности в регистрах процессора. Если такой возможности у компилятора нет, переменные обрабатываются как auto.
static — статическая переменная. Время жизни — постоянное. Инициализируется один раз при первом выполнении оператора, содержащего определение переменной.
Пример (переменная типа static сохраняет свое значение и после выхода из функции).
#include <iostream>
void foo()
{
static int x = 0; // попробуй удалить static
++x;
std::cout << x << "\n";
}
int main()
{
for(int i = 0; i < 10; ++i)
foo();
}
Будут выведены числа от 1 до 10, так как оператор int x=0 будет выполнен только один раз и при выходе их функции переменная типа static сохранит свое значение.
extern
Спецификатор extern означает, что переменная была или будет описана в другом месте программы или в другом программном модуле.
Пример. Допустим, у нас есть два модуля A. cpp и B. cpp. В модуле A определена целая переменная i вне всех классов и функций:
int i = 2;
Такая переменная называется глобальной. В файле A она видна от точки определения и до конца файла. Однако в модуле B эта переменная автоматически не видна. И если вдруг нам потребуется в модуле B присвоить ей другое значение, то возникнут некоторые проблемы. Нельзя просто написать:
i = 1;
В этом случае компилятор при обработке модуля B «не видит» модуль A и ничего не знает об определенной там переменной, поэтому мы получим сообщение о неопределенной переменной. Также нельзя написать:
int i = 1;
Такая запись является повторным определением. Компилятор-то «возражать» не станет — он транслирует модули по отдельности, а вот компоновщик сообщит, что одна и та же переменная определена дважды. Для таких случаев в С++ включено специальное ключевое слово extern. В модуле B надо объявить переменную следующим образом:
extern int i;
После этого можно использовать переменную i в файле B любым разрешенным способом. Например, присвоить новое значение:
i = 1;
Однако попытка совместить объявление с присвоением значения является ошибкой:
extern int i = 1;
Имя переменной должно быть уникальным в своей области действия (например, в одном блоке не может быть двух переменных с одинаковыми именами).
Если при определении начальное значение переменных явным образом не задается, компилятор присваивает глобальным и статическим переменным нулевое значение соответствующего типа. Автоматические переменные не инициализируются.
Пример.
#include <iostream>
using namespace std;
int dc=1; //глобальный для модуля объект 1
void func1()
{
cout<<dc<<"\n"; //виден глобальный объект 1
int dc=2; //локальный для функции объект 2
//внутренний блок
{
cout<<dc<<"\n"; //виден локальный объект 2
int dc=3; //локализованный в блоке объект 3
cout<<dc<<"\n"; //виден локальный объект 3
cout<<::dc<<"\n"; //виден глобальный объект 1
}
cout<<dc<<"\n"; //виден локальный объект 2
cout<<::dc<<"\n"; //виден глобальный объект 1
}
void func2(int *dc)
{
cout<<*dc<<"\n"; //виден параметр функции
cout<<::dc<<"\n"; //виден глобальный объект 1
//внутренний блок для функции
{
int dc=4; //локализованный в блоке объект 4
cout<<dc<<"\n"; //виден локальный объект 4
cout<<::dc<<"\n"; //виден глобальный объект 1
}
}
void main()
{
cout<<::dc<<"\n"; //виден глобальный объект 1
int dc=5; //локальный для функции объект 5
func1();
func2(&dc);
cout<<dc<<"\n";
}
Результат:
1
1
2
3
1
2
1
5
1
4
1
5


