Суббота, 20.04.2024, 08:15
Приветствую Вас Гость | RSS

Кузница миров

Меню сайта
Категории раздела
Мои статьи [2]
Курс : "Основы С++ для начинающих программистов игр." [25]
WindMill Engine [3]
XNA4 RPG [0]
Перевод туториалов но созданию RPG на C# c XNA4.
C# & Leadwerks [5]
Программирование Leadwerks Engine на языке С# с помощью врапера Le.NET.

Каталог статей и уроков

Главная » Статьи » Курс : "Основы С++ для начинающих программистов игр."

5.4. Функции и указатели.
5.4. Функции и указатели.


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

Массивы и указатели.

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

#include <iostream>
using namespace std;

void main()
{
   int array[10];
   cout<<'\n'<< array;  // отображение адреса массива
   cout<<'\n'<<&array [0];    // отображение адреса первого элемента
   cin.get();
}


Оба напечатанных адреса будут идентичными.
Напомню, что для отображения адреса первого элемента использовался оператор взятия адреса &, ведь первый элемент это число, а не указатель.
Что же нам это дает? Да ничего особенного, если не знать, что к указателям также можно применять операторы инкременты и декременты. При этом адрес изменяется не на единицу, как обычно, а на размер переменной в байтах.
В результате доступ к элементам массива можно выполнять еще одним способом. Его вы видите в примере:
Code

#include <iostream>
using namespace std;

void main()
{
   int a[10];
   for(int i=0;i<10;i++)
   {
    a[i]=i;
    cout<<a[i]<<' ';
   }
   for(int i=0;i<10;i++)
   {
    cout<<*(a++)<<' ';
   }
   cin.get();
}


Вот только работать этот пример не будет. Ведь а это константный указатель, то есть изменить его нельзя. Чтобы пример заработал необходимо создать указатель обычный и передать ему адрес из константного:
Code

#include <iostream>
using namespace std;

void main()
{
   int a[10];
   for(int i=0;i<10;i++)
   {
    a[i]=i;
    cout<<a[i]<<' ';
   }
   cout<<'\n';
   int *pa=a;
   for(int i=0;i<10;i++)
   {
    cout<<*(pa++)<<' ';
   }
   cin.get();
}

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

#include <iostream>
using namespace std;

void main()
{
   int a[10];
   for(int i=0;i<10;i++)
   {
    a[i]=i;
    cout<<a[i]<<' ';
   }
   cout<<'\n';
   int *pa=a;
   for(int i=0;i<11;i++)
   {
    cout<<*(pa++)<<' ';
   }
   cin.get();
}

В этом примере происходит доступ к несуществующему 11 элементу(с индексом 10, ведь как вы помните отсчет начинается с 0). В результате программа выдает нам какое-то, непонятно откуда взявшееся число.

Динамическое распределение памяти.


А сейчас настало время познакомится с одним из самых распространенных способов использования указателей.
Когда ваша программа начинает свою работу, операционная система выделяет для нее место в памяти. Называется это место стек. Размер стека не безграничный, поэтому иногда возникает проблема, когда ваши данные, например большой массив, в него попросту не помещаются. Эта проблема получила название – переполнение стека, и приводит к краху работы программы.
Обидно получается. В компьютере вон сколько памяти, а попытка создать парумегабайтный массивчик делает программу неработоспособной. Но выход есть.
Ко всей этой неиспользуемой, находящейся за пределами стека памяти, можно получить доступ. Память эта, кстати, называется динамической(точнее динамически распределяемой), то есть непостоянной, в противовес постоянному стеку.
Для того, чтобы разместить данные в динамической памяти используется указатель и специальный оператор - new.
Работает он очень просто. По запросу программы он резервирует где-то в динамической памяти свободный кусочек, и возвращает его адрес.

Синтаксис оператора следующий:
Code

указатель  =  new  тип_данных;

Пример:
Code

int *pi = new int;

согласно этой записи оператор new выделит в области динамического распределения некий, соответствующий размеру типа данных, объем памяти, и передаст указателю pi его адрес. Как мы помним, размер целочисленного типа данных 4 байта, столько и будет выделено.
В дальнейшем доступ к этой памяти производится через указатель.
Code

#include <iostream>
using namespace std;

void main()
{
   int *pi = new int;
   *pi=10;
   cout<<*pi;
   cin.get();
}


Ключевой особенностью использования динамически распределенной памяти является ее независимость от области видимости. Будучи созданной один раз она доступна везде, куда можно передать ее адрес:
Code

#include <iostream>
using namespace std;

int* func()
{
   int* pa=new int;    // динамическое выделение памяти
   *pa=123123;
   return pa; // возврат адреса
}

void main()
{
   int* pi;
   pi=func(); // получение адреса на распределенную память
   cout<<*pi;
   cin.get();
}

Здесь функция func() возвращает адрес на динамически выделенную память. Теоретически после завершения работы функции все созданные в ней переменные должны быть утрачены. Но это не относится к динамическому распределению памяти. Что и подтверждает строчка cout<<*pi;, печатающая ранее присвоенное в функции и никуда не подевавшееся корректное значение.
Динамическое распределение памяти настолько мощное средство, что даже по завершению программы выделенная в ней память остается недоступной для других программ. Такую ситуацию называю утечкой памяти, поскольку память остается недоступной до перезагрузки компьютера. Поэтому когда память под переменную становится не нужной, ее нужно освободить. Для этих целей используется оператор delete.

Синтаксис следующий:
Code

delete указатель;

здесь указатель – это указатель на динамически-распределенную область памяти.
Пример:
Code

#include <iostream>
using namespace std;

int* func()
{
   int* pa=new int;   
   *pa=123123;
   delete pa;  // освобождение ранее выделенной памяти.
   return pa;   
}

void main()
{
   int* pi;
   pi=func();   
   cout<<*pi;
   cin.get();
}

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

Code

#include <iostream>
using namespace std;

int* func()
{
   int* pa=new int;   
   *pa=123123;
   return pa;   
}

void main()
{
   int* pi;
   pi=func();   
   cout<<*pi;
   delete pi; // освобождение ранее выделенной памяти.
   cout<<*pi;
   cin.get();
}


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

С выделением памяти под обычные переменные разобрались, теперь рассмотрим как это делается с массивами.
Code

указатель  =  new  тип_данных[размер массива];

В принципе разница небольшая. Необходимо всего лишь указать после типа данных размер массива. Для того чтобы освободить выделенную таким образом память используется следующая запись.
Code

delete [] указатель;   

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

Что то мой разговор об указателях затягивается, пора возвращаться к функциям.

Параметры-адреса


Давайте рассмотрим такой простой пример:
Code

#include <iostream>
using namespace std;

void print(int a)
{
   a = 32;
   cout<<a<<'\n';
}

void main()
{
   int i=10;
   print(i);
   cout<<i<<'\n';
   cin.get();
}


Здесь у нас есть функция print, которой передается переменная i. Внутри функции этой переменной присваивается новое значение. Но потом, после функции оказывается что значение переменной i осталось неизменным. То есть изменить значение переменной, которая передавалась функции как аргумент, нельзя.
Так происходит потому, что функции на самом деле передается не оригинальная переменная, а всего лишь хранящееся в ней значение. Поэтому такой способ передачи называют передачей по значению. Любые манипуляции с этим значением внутри функции оригинальную переменную не затрагивают.
Получается, что бы не делала функция результат ее работы за пределы функции не выйдет. В таком случае зачем они вообще нужны. Ах, да. Функция же может возвращать значение. Вот только всего одно. А если нам нужно больше?
Тут на помощь опять приходят указатели. Функция работает с указателями так же, как и с обычными переменными, то есть передает их значение. А что у нас хранится в указателе? Правильно, адрес ячейки памяти, в которой хранится некое значение. А значит мы можем в полной мере воздействовать на это значение. Это проиллюстрировано следующим примером:
Code

#include <iostream>
using namespace std;

void print(int* a)
{
   *a = 32;
   cout<<*a<<'\n';
}

void main()
{
   int i=10;
   int *pi = &i;
   print(pi);
   cout<<i<<'\n';
   cin.get();
}

Здесь в функции объявляется переменная i, которой присваивается число 10. Затем эта переменная передается функции print. Причем передается не сама переменная, а указатель на нее, как того требует прототип функции:
Code

void print(int* a);

аргументом функции является указатель, то есть попросту говоря адрес. Внутри функции с помощью оператора разыменования * по адресу присваивается новое значение. По завершении работы функции оказывается, что теперь значение переменной i изменилось, и равно тому, что было присвоено в функции. То есть нам удалось изменить значение переданной функции переменной. Что и требовалось. Вот такие вот полезные указатели. Такой способ передачи аргументов называется передачей по указателю.
Добавлю, что запись
Code

int *pi = &i;
   print(pi);

можно сократить так:
Code

   print(&i);

здесь адрес берется непосредственно при передаче переменной в функцию.

Кроме передачи по значению и передачи по указателю есть еще и третий способ, называемый передача аргументов по ссылке.
Он не требует от программиста непосредственной работы с указателями, хотя тоже работает с адресами переменных. Но вся эта работа скрыта от программиста.
Вот предыдущий пример, измененный так, чтобы продемонстрировать передачу по ссылке.
Code

#include <iostream>
using namespace std;

void print(int& a)
{
   a = 32;
   cout<<a<<'\n';
}

void main()
{
   int i=10;

   print(i);
   cout<<i<<'\n';
   cin.get();
}

Здесь ключевую роль играет запись аргументов в объявлении функции.
Code

void print(int& a)


int& a означает что будет производится передача по ссылке(можно сказать что переменная а это новый тип данных, который называется ссылка, и является синонимом адреса). В коде примера нет ни одного указателя. Но функция print(i)получает не значение переменной, а ее адрес, и записывает 32 по этому адресу. То есть результат тот же, что и при использовании указателей.
По-моему, намного проще.
Категория: Курс : "Основы С++ для начинающих программистов игр." | Добавил: nilrem (11.05.2012)
Просмотров: 1459 | Комментарии: 2 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0