Партнерка на США и Канаду по недвижимости, выплаты в крипто

  • 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