Out.close();//закрытие потока




}

}

 

Исполнение обработчика начинается с выполнения кода записанного внутри круглыз скобок оператора if().

После выбора имени файла оно переписывается в строку MyFile. Если по каким то причинам попытка оказалаь неудачной, появляется сообщение "Невозможно открыть файл " с указанием имени файла.

Если диалог прошел удачно, то вызывается конструктор выходного потока, суказанием имени файла, в который будут записываться данные. Если в силу каких то причин поток не улалось создать, то пользователь получит сообщение "Не удалось создать выходной поток". Если поток создан, то элементы массива по очереди передаются в файл.

 

void __fastcall TForm1::OpenClick(TObject *Sender)

{

if(SaveDialog1->Execute())

MyFile=SaveDialog1->FileName;

else{

ShowMessage("Не удается открыть файл "+MyFile);

return;

}

ifstream in;

in.open(MyFile.c_str());

if(in.fail()){ //поток не создан, то сообщение и выход

ShowMessage("Не удается создать файл "+MyFile);

return;

}

else{ //если поток создан

int i=0;

char str[100];

in>>str[0];

while(str[i]!=EOF){//Пока не будет достигнут коней файла

i++;

in>>str[i];

}

Edit2->Text=AnsiString(str); //Преобразование строк

in.close();

}

}

 

Как и в предыдущем случае исполнение обработчика начинается с выполнения кода записанного внутри круглыз скобок оператора if(). При удачном открытии файла создается входной поток. В данном случае он создается с помощью конструктора без параметров. Это сделано для того, чтобы проиллюстрировать иной способ создания потока. Функция fail() возвращает значение true, если поток удалось создать. Ввиду того, что размер файла заранее неизвестен приходится созать массив размер которого будет больше, чем размер файла, в данном случае это str[100]. После того как массив заполнен, о чем свидетельствует возвращение потоком признака конца файла EOF, строка переводится в тип AnsiString и выводится в Edit2.

 

Обработчик выхода из программы вообще выглядит очень просто

void __fastcall TForm1::ExitClick(TObject *Sender)

{

exit(0);

}

 

В данном примере выходной поток стирает в файле записанные ранее данные.

Если в левом окне написать несколько слов через пробел и записать их в файл, то то в правом окне в считанной строке будут отсутствоваьь пробелы.

 

 

Это связано с тем, что операция взятия из потока (>>) читает не всю строку, а только одну лексему, т.е. последовательность символов между двумя пробелами.

 

 

Дополнительно о классе ofstream

Конструктор класса ofstream перегружен. Наиболее полныей вид:

ofstream(const char* szName, int nMode = ios::out, int nProt = filebuf::openprot);

 

Первый аргумент – имя выходного файла, и это единственный обязательный аргумент. Второй аргумент задает режим, в котором открывается поток. Этот аргумент – логическое ИЛИ из следующих величин:

 

ios::app при записи данные добавляются в конец файла, даже если текущая позиция была перед этим перемещена;
ios::ate при создании потока текущая позиция помещается в конец файла; однако, в отличие от режима app, запись ведется в текущую позицию;
ios::in поток создается для ввода; если файл уже существует, он сохраняется;
ios::out поток создается для вывода (режим по умолчанию);
ios::trunc если файл уже существует, его прежнее содержимое уничтожается, и длина файла становится равной нулю; режим действует по умолчанию, если не заданы ios::ate, ios::app или ios::in;
ios::binary ввод-вывод будет происходить в двоичном виде, по умолчанию используется текстовое представление данных.

 

Третий аргумент используется только в том случае, если создается новый файл; он определяет параметры создаваемого файла.

Можно создать поток вывода с помощью стандартного конструктора без аргументов, а позднее выполнить метод open с такими же аргументами, как у предыдущего конструктора:

 

void open(const char* szName, int nMode = ios::out, int nProt = filebuf::openprot);

Только после того, как поток создан и соединен с определенным файлом (либо с помощью конструктора с аргументами, либо с помощью метода open), можно выполнять вывод. Выводятся данные операцией <<. Кроме того, данные можно вывести с помощью методов write или put:

 

ostream& write(const char* pch, int nCount);ostream& put(char ch);

Метод write выводит указанное количество байтов (nCount), расположенных в памяти, начиная с адреса pch. Метод put выводит один байт.

Например, если имеется поток с именем fout,оператор

fout.put(‘Z’);

выведет в поток символ «Z».

Функции put() допускают сцепление, например

fout.put(‘Z’).put(‘\n’);

В данном случае в поток попадет сивол «Z», а за ним перевод на новую строку.

 

 

Для того чтобы переместить текущую позицию, используется метод seekp:

ostream& seekp(streamoff off, ios::seek_dir dir);

Первый аргумент – целое число, смещение позиции в байтах. Второй аргумент определяет, откуда отсчитывается смещение; он может принимать одно из трех значений:

ios::beg смещение от начала файла
ios::cur смещение от текущей позиции
ios::end смещение от конца файла

 

Сместив текущую позицию, операции вывода продолжаются с нового места файла.

После завершения вывода можно выполнить метод close, который выводит внутренние буферы в файл и отсоединяет поток от файла. То же самое происходит и при уничтожении объекта.

 

Пример.

 

char s[80], c;

ifstream infile("test.txt");

if(!infile){

ShowMessage("Файл не удается открыть");

return;

}

int i=0;

while((c=infile.get())!=EOF){

if(c=='\n')

//занесение нулевого символа в конец строки

s[i]=0;

// обработка строки

...

i=0;

}

//формирование строки

else s[i++]=c;

}

//закрытие файла

infile.close();

 

Дополнительно о классе ifstream

 

Класс ifstream, осуществляющий ввод из файлов, работает аналогично. При создании объекта типа ifstream в качестве аргумента конструктора можно задать имя существующего файла:

ifstream(const char* szName, int nMode = ios::in, int nProt = filebuf::openprot);

Можно воспользоваться стандартным конструктором, а подсоединиться к файлу с помощью метода open.

 

 

В классе ifstream есть методы чтения из потока.

Метод write() выводит в файл из символьного массива на который указывает его первый параметр число символов заданных вторым параметром. Например,

out.write(str,5);

записывае в поток out 5 символов из массива str. Символы не обрабатываются. Это значит, что если встречаются управляющие символы, например, символ конца строки ’\0’, то он рассматривается просто как символ.

 

В классе ifstream есть методы чтения из потока.

Метод read(), предназначен для чтения из потока без обработки.

Функция gcount() сообщает количество символов, прочитанных последней операцией ввода.

Метод get() имеет три модификации:

get() Возвращает одиночный символ без анализа.
get(char) Возвращает ссылку на на тот объект потока. для которого вызывался метод
get(char*, int n, char delim)    

 

Примеры.

 

char s[80], c;

ifstream infile("test.txt");

if(!infile){

ShowMessage("Файл не удается открыть");

return;

}

int i=0;

while((c=infile.get())!=EOF){

if(c=='\n')

//занесение нулевого символа в конец строки

s[i]=0;

// обработка строки

...

i=0;

}

//формирование строки

else s[i++]=c;

}

//закрытие файла

infile.close();

 

 

Управление потоком ввода-вывода

Для того, чтобы увидеть результаты работы программы они должны быть представлены соответствующим образом. В самом деле, если вы рассчитываете, например, таблицу натуральных логарифмов y= ln x, то и вывод должен быть организован в виде таблицы чисел с равным количеством знаков. Для большей информативности числа должны быть одинаково сориентированы относительно границ таблицы, а та, в свою очередь, относительно краев листа. Печать всех значений подряд просто бессмысленна.

Размещение вывода называют форматом. В С++ форматом можно управлять, т.е. устанавливать число пробелов между выводимыми символами, определять число цифр после десятичной точки и т.д. Команды форматирования можно применять к любому выходному потоку. Как правило, команды форматирования содержат, так называемые, флаги. Флаг – это инструкция, разрешающая выполнение одного из двух возможных действий. Наверное, сейчас трудно определитьопределить, откуда в программировании появилось слово флаг, предписывающее дальнейшее поведение программы. Возможно, что это связано с техникой управления войсками, когда команды отдавались с помощью различных флагов.

Для ссылки на флаг используется функция потокового класса setf(имя_флага). Функция-член с именем setf()(сокращение от set flag – установить флаг) класса ofsteam позволяет установить флаги, некоторые из них описаны в приведенной ниже таблице. Функция-член с именем unsetf() напротив дает возможность сбросить флаг.

 

Флаг Значение Состояние по умолчанию
ios::fixed Установка флага приводит к тому, что числа с плавающей точкой не используют запись с основанием e. Установка флага автоматически сбрасывает флаг ios::scrintific. Сброшен
ios::scientific Установка флага приводит к тому, что числа с плавающей точкой записываются с использованием e. Установка флага автоматически сбрасывает флаг ios::fixed Сброшен
ios::showpoint При установке флага в записи чисел с плавающей точкой всегда присутствует десятичная точка и замыкающие нули. Если флаг сброшен, то числа, в которых после десятичной точки стоят все нули, будут выводитсявыводиться без десятичной точки и следующих за ней нулей Сброшен
ios::showpos При установке флага перед положительными числами выводится знак “+”. Знак минус перед отрицательными числами ставится всегда, независимо от установки флагов. Сброшен
ios::right Если установлен этот флаг и в вызове функции-члена width указано значение ширины поля, то следующий элемент будет выводитсявыводиться прижатым к правому краю пространства с шириной равной, указанной в width количеству интервалов. Другими словами перед выводимым элементом будет добавлено недостающее число пробелов. Установка этого флага автоматически сбрасывает флаг ios::left. Установлен
ios::left Если установлен этот флаг и в вызове функции-члена width указано значение ширины поля, то следующий элемент будет выводиться прижатым к левому краю пространства с шириной равной, указанной в width количеству интервалов. Другими словами перед выводимым элементом будет добавлено недостающее число пробелов. Установка этого флага автоматически сбрасывает флаг ios::right. Сброшен

 

 

Для управления флагами в классе ios есть методы flags, setf и unsetf.

Перед осуществлением вывода можно установить несколько флагов. Пусть в программе организован поток с именем outStream. Ниже приведен фрагмент программы с установкой флагов для этого потока

outStream.setf(ios::scientific); //выводимые числа будут

// представлены с использованием е

outStream.setf(ios::showpoints); //используется десятичная точка

outStream.setf(ios::showpos); //перед положительными числами

//ставится +

 

 

Для управления выводом используется несколько функций-членов класса ofsteam. Одна из таких функций нам уже встречалась, это функция precision(int n). Применение этой функции приводит к тому, что любое число с десятичной точкой будет выводитсявыводиться в поток с n значащими цифрами после десятичной точки. Например, если имеется поток outStream, то после инструкции

outStream.precision(2);

все числа будут выводиться с двумя знаками после десятичной точки. Если потом потребуется изменить количество значащих цифр после точки, достаточно переписать последнюю строку с другим параметром, который должен быть целым неотрицательным числом, например,

outStream.precision(3);

Вот пример программы с использованием флагов. Программа рассчитывает выражение для 10 значений n от 0 до 9

#include <iostream>

#include <cmath>

#include <fstream>

#define pi 3.1415926

using namespace std;

 

main(){

ifstream inStream;//объявление потоков

ofstream outStream;

 

long double db1[10], db2[10];

outStream.open("character.dat"); //присоединение к файлу

for(int n=0;n<10;n++){

db1[n]=100*sin(pi/(n+1));

outStream<<db1[n]<<"\n";

}

outStream.close(); //закрытие потоков

inStream.open("character.dat"); //присоединение к файлу

if (inStream.fail()){

//Печать сообщения об ошибке

cout<<"Не удается открыть файл character.dat\n";

exit(1);// Функция аварийного завершения.

}

 

for(int i=0; i<10;i++)

inStream>> db2[i];

inStream.close(); //закрытие потоков

cout.width(20); //для вывода числа отводится 20 позиций

cout.setf(ios::right); //Выравнивание по правому краю

cout.setf(ios::fixed); /*выводимые числа представлены в

форме c фиксированной точкой*/

cout.setf(ios::showpos); //перед положительным числом знак +

for(int i=0; i<10;i++)

cout<<db2[i]<<endl;

 

char z;

cin>>z;

}

 

Видно, что выравнивание первого числа происходит по правому краю таблицы. После небольшого изменения программы можно выполнить выравнивание для всех строк, как это показано на втором рисунке..

 

Попробуйте теперь применить выравнивание по левому краю и сравните результаты.

Очень удобной функцией форматирования в потоке cout является функция width(интервал). Этой функцией мы уже воспользовались в предыдущем примере, а теперь пояснимпоясним, как она работает.

Функция width() позволяет отвести количество символов для вывода числа, точнее расстояние от начала печати. Если количество выводимых символов окажется большим, то функция на количество выводимых разрядов не влияет. Вот пример программы

#include <iostream>

using namespace std;

#include <windows>

main(){

SetConsoleOutputCP(1251);

int a=1;

while(a<1000000){ //организация условно-бесконечного цикла

cout<<"Введите целое число a=";

cin>>a;

cout.width(4);

cout<<a<<endl;

}

}

Вот результат

 

Видно, что при выводе для результата отводится 4 позиции. Свободные позиции заполняются пробелами. Если выделенных позиций не хватает, то интервал увеличивается на число необходимых позиций.

 

5.2. Ввод имен файлов

В примере, который мы только, что рассмотрели, имя файла было задано в программе в виде, так называемой, строки, т.е. текста заключенного в кавычки. О строках мы еще будем говорить позднее. А сейчас подумаем вот над чем. Нельзя ли имя файла прочитать из другого файла? Ответ можно предсказать – конечно, это возможно. Но чтобы не запутаться в файлах заранее дадим им имена. Рабочий файл в который записывается информация назовем workFaile.dat, а файл содержащий имя рабочего файла будем называть nameFile.

Далее поступим следующим образом. В разрабатываемой программе создадим массив символов nameWork[n] в который запишем имя рабочего файла в кавычках. Это значит, что размер массива n должен хотя бы на единицу превосходить количество символов в имени рабочего файла, в данном случае workFile.dat содержит 12 символов, т.е. величина n должна быть не менее 13. Напомним, что индексация элементов массива начинается с нуля. Если имя рабочего файла заранее неизвестно, то создавая такой массив следует заранее предусмотреть максимально возможный размер. После того как имя рабочего файла будет помещено в массив, последний можно записать в виде файла, подобно тому как мы только что сделали в предпоследнем примере. При необходимости это имя будет прочитано и программа создаст необходимые потоки ввода-вывода.

Как всегда рассмотрим пример. Создадим программу, которая вводит 4 целых числа. Затем спрашивает имя рабочего файла. После чего вычисляет разность соседних чисел, которая записывается в рабочий файл. Далее программа читает рабочий файл и выводит его на экран.

 

#include <iostream> //библиотека ввода вывода с использованием

//стандартных устройств

#include <fstream> //библиотека ввода из файла

#include <cstdlib> //библиотека вывода в файл

#include<windows>

using namespace std;

 

main(){

ifstream inStream; //объявление потока ввода из файла

ofstream outStream; //объявление потока вывода в файл

int number[4], diskr[3];

char nameWork[15]; //строка для записи имени рабочего файла

SetConsoleOutputCP(1251);

cout<<"Введите 4 числа \n";

for(int i=0; i<4;i++) {

cin>>number[i];

}

 

cout<<"Введите имя рабочего файла \n"; /*Имя файла в который

записываются числа*/

cin>>nameWork;

outStream.open("nameFile"); //присоединение файла с именем к потоку

outStream<< nameWork; //запись имени рабочего файла в потоке

inStream.open("nameFile"); //присоединение файла с именем

// рабочего файла к входному потоку

for(int i=1; i<4;i++) {

number[i]=number[i]-number[i-1];

outStream<< number[i];

}

inStream.open(nameWork);

inStream.fail();

if (outStream.fail()){

cout<<"Не удается открыть файл character.dat\n";

exit(1);

}

 

for(int i=0; i<4; i++){

inStream>>diskr[i];

cout<< diskr[i];

}

inStream.close();

outStream.close();

cin >>number[0];

}

 

 

5.3. Манипуляторы

Параметры форматирования потока можно установить не только посредством установки и снятия флагов, но и с помощью специальных функций, которые называются манипуляторами. Манипуляторы можно включать в выражения ввода-вывода. На самом деле нам уже известен один манипулятор, это endl. Этот манипулятор позволяет выводить символы с новой строки.

Приведем пример:

cout<<”Пример манипулятора”<<endl;

Этот манипулятор не использует никаких параметров, но есть манипуляторы которым для форматирования нужно указать дополнительные сведения или, говоря иначе, параметры.

Некоторые манипуляторы без параметров описаны в следующей таблице.

Манипулятор Назначение Функция
dec Устанавливает флаг десятичной системы счисления. При вводе и выводе
hex Устанавливает флаг шестнадцатеричной системы счисления. При вводе и выводе
oct Устанавливает флаг восьмеричной системы счисления. При вводе и выводе
ws Пропускает пробельные символы – пробел, знаки табуляции ‘\t’ и ‘\v’, символ перевода строки ‘\n’, символ возврата каретки ‘\r’, символ перевода страницы ‘\f’. Действует только при выводе.
endl Обеспечивает включение в выходной поток символа новой строки и выгружает содержимое этого потока. Действует только при выводе.
ends Вставляет в поток нулевой символ ‘\0’, что соответствует признаку конца строки. Действует только при выводе.
flush “Сбрасывает” поток, т.е. переписывает содержимое буфера, связанного с потоком, на соответствующее устройство. Действует только при выводе.

 

Здесь полезно заметить, что манипуляторы endl и ends играют важную роль при выводе. Без них нельзя гарантировать, что информация не останется в буфере.

 

5. Указатели

Познакомимся с еще одним типом переменных – это указатель. Указатель позволяет работать с памятью компьютера.

Как вы знаете, память компьютера образуют пронумерованные восьмиразрядные байты, в которых хранятся значения переменных. Причем каждая переменная может занимать несколько соседних байтов в зависимости от ее типа. Эти несколько байтов называются ячейкой. Номер первого байта ячейки называется ее адресом. Поэтому адреса двух переменные, хранящиеся в соседних ячейках памяти, могут отличаться на единицу - для символьных переменных, на 4 - для целых типа int, на 8 - для типа double, и вообще говоря, на практически любое целое число, если это массив, структура или переменная иного типа созданного программистом.

На заре появления вычислительных машин, когда языков программирования, в том виде, в котором мы знаем их сейчас, не было, программисты работали именно с адресами. То есть, каждый оператор программы представлял собой код, состоящих из нулей и единиц, который соответствовал командам типа взять число из памяти с адресом n, поместить число по адресу m, выполнить операцию p и т.п. Такой подход сохранился до сих пор в языках низкого уровня типа ассемблера. Конечно, писать всю программу на подобном языке сложно, но определенные преимущества в этом есть. Например, нужно создать программу работы с массивом, размеры которого заранее неизвестны, или изменить размеры уже созданного массива. В таких задачах можно указать адрес, с которого начинается продолжение массива. Подобные примеры можно продолжить. Вы уже вероятно поняли, что если такой подход в программировании имеет преимущества, то он реализован в С++.

Вся память компьютера делится на несколько кусков или как принято говорить сегментов. Байты с младшими адресами отданы для операционной системы, которая управляет компьютером. После этого идет сегмент, в котором хранится программа, включая значения всех ее переменных и функций. За тем идет сегмент, выделенный для хранения статических и внешних переменных. Дальше идет свободная память, ее программа может использовать в своих целях. Занятую часть свободной памяти принято называть кучей (heap). Таким образом, куча это присоединенная к программе память. Этот сегмент памяти принято называть также динамической памятью. Переменные, значения которых хранятся в динамической памяти, называются динамическими переменными.

Операционная система Код программы Статические и внешние данные Куча Свободная оперативная память Стек Неиспользуемая память Буферы, ПЗУ, видеопамять
Младшие адреса   Старшие адреса

 

 

Объем оперативной памяти для кучи определяется запросами программы.

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

Указатель – это переменная, которая хранит адрес первого байта другой переменной или функции, вообще говоря, указатель хранит адрес любого объекта, но о таких указателях мы будем говорить позже. Естественно, что значение указателя, это целое число. Но не нужно думать, что переменная типа указатель это то же самое, что переменная типа int. В противном случае с указателями можно было бы осуществлять математические операции, результаты которых были бы заведомо неверными. Указатель это переменная специального типа, причем каждый указатель может работать только с переменными своего типа. Например, указатель объявленный для чисел типа double не может применяться к другим типам переменных. При объявлении, указателям должны быть даны имена. Для имен указателей применяются обычные правила, например, указатель можно назвать словом pointer.Но такое имя может иметь переменная. Для того, что бы программа при объявлении не перепутала имена указателей и переменных впереди имени указателя ставится знак звездочка ' * '’, т.е синтакс указателей таков:

тип [модификатор] * имя_указателя;

В данном случае:

тип – имя типа переменной, адрес которой будет содержать переменная указатель (на которую он будет указывать), например char или int;

имя – идентификатор переменной типа указатель;

* - вспомогательный символ, говорящий о том, что происходит объявление указателя.

Например,

int *pointer1, x1;

double * pointer2, x2;

char *pointer3;

Такая запись означает, объявление 5 переменных. Переменная x1 – будет содержать целое число тиа int, x2-дробное типа double. Кроме того, объявлены 3 переменные типа указатель, о чем свидетельствует звездочка перед их именами. На самом деле имена указателей это pointer1, pointer2, pointer3. Звездочка является признаком указателя, и не входит в имя. Иногда, для того, чтобы выделить имена указателей используются круглые скобки.

int (*pointer1), x1;

double (* pointer2), x2;

char (*pointer3);

 

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

Первый указатель рассмотренного примера, pointer1, предназначен для работы с переменными типа int, второй с переменными типа double, третий с переменными типа char. Существует более общий тип указателей с именем типом void *. Указатель типа void * отличается от указателей других типов тем, что для него не указывается размер соответствующего ему участка памяти.

Язык С++ дает возможность использования адресов переменных с помощью унарных операций & и *. Операции & и *. можно писать вплотную с именем операнда или через пробел. Для определения адреса переменной используется оператор получения адреса - &. Да и вообще когда появляется оператор & его можно читать как слово адрес, так, например,

pointer1=&x1;

pointer2=&x2;

означает, что указателю pointer1 следует присвоить адрес переменной x1, указателю pointer2 адрес x2. Теперь нам известны адреса x1 и x2, но как с ними работать? Например, как изменить в ячейках с этими адресами значения переменных. Ответ прост. Для этого есть специальный операторы – звездочка ' (* '). Звездочка это оператор, который читается так – значение переменной на которую указывает … далее имя указателя. Например,

*pointer1=2; /* значение переменной на которую указывает

* указатель pointer1 присвоить число равное 2 */

*pointer2=3;

Это все равно, что написать

x1=2;

x2=3;

Теперь к переменным x1 и x2 можно обращаться не только через имена, но и через указатель.

Оператор звездочка, с помощью которого переменной x1 было присвоено новое значение, называется оператором разыменовывания или косвенной адресации, а переменная-указатель разыменованой. По-существу разыменовывание означает для переменной получение второго имени, а не потерю первого, как это могло показаться. В рассмотренном примере x1 и *pointer1 это одна и та же переменная. То же самое можно сказать о x2 и *pointer2.

Приведем для примера некоторую законченную программу с использованием указателей.

#include <iostream>

using namespace std;

int main(){

int *pointer1, x1,x2;

x1=1;

 

pointer1 =&x1; //указателю присвоить адрес x1

*pointer1 =2; /*переменной на которую указывает pointer1 присвоить

значение равное 2 */

x2=3*(*pointer1+5); /* разыменнованная переменная может

быть операндом */

cout<< "\n"<<x1; //Вывести x1

cout<<"\n"<< *pointer1; //Вывести x1, но с помощью указателя

cout<<"\n"<<x2; //Вывести x2

cout<< "\n"<<pointer1; //вывести адрес записанный в указатель

return 0;

}

на экране появится

Последнее число может быть любым. Просто на момент компиляции адрес переменной x1 был назначен таким: 1245060. Нужно иметь в виду, что адрес это шестнадцатиричное число.

Указатели можно инициализировать при их объявлении, например:

int ir;

int *pir=&ir //здесь инициализирован указатель, но не переменная

Как и любая переменная, указатель хранится в памяти, т.е. имеет адрес и значение. Адрес и значение такой переменной определяются с помощью все тех же унарных операций & и *. Переменная типа указатель занимает в памяти 2 байта.

Говоря об указателях, все времявремя встречается слово переменная. Это не значит, что нельзя применять указатели к константам. Вот пример объявления указателя для константы:

string str="Hello!";

string *const pstr=&str;

При объявлении указателя он сам может быть объявлен константой

 

char const *const name;

Рассмотрим еще один пример иллюстрирующий возможности указателей.

#include<iostream>

#include<string>

using namespace std;

void main(){

char *psz="Dog Motja"; /*В указателе будет храниться адрес

первого символа строки */

for(int i=strlen(psz)-1;i>=0;i--) /*функция strlen вычисляет длину

строки, описана в string. Длина строки в этом примере (string)=9 в этом примере.

Нумеруются элементы массива от 0 т.е. в этом случае от 0 до 8*/

cout<<psz[i];

cout<<endl<<psz;

int z;

cin>> z;

}

 

А вот результат

Из данного примера следует, что оператор & позволяет определить адрес первой ячейки переменной. Зная длину строки можно вывести отдельные элементы символьного массива.

Рассмотрим случай неправильного использования оператора &.

pic=&48; //число 48 не связано ни с одной ячейкой памяти

int ir=5;

pir=&(ir+15); /*арифметическое выражение не связано

с ячейкой памяти */

 

Поскольку указатели позволяют работать с памятью, то возникает вопрос, а нельзя ли в память сразу загружать значения переменных, минуя их объявление. Ответ – можно, ведь переменная, и её указатель, это одно и тоже. Делается это с помощью оператора new, например, нужно ввести новую переменную с помощью определенного заранее указателя. Фрагмент кода выглядит следующим образом

int *point; /* объявили указатель для переменной типа int,

* т.е. предупредили компилятор, что в программе

* будут использованы указатели с перечисленными

* именами – в данном случае одно */

point=new int; /* просим для переменной типа int дать

* свободный адрес.В дальнейшем к этой

* переменной будем обоащаться через указатель */

После оператора new указывается тип создаваемой переменной. Переменные объявленные через указатель называются динамическими переменными. Именно эти переменные хранятся в разделе памяти, который называетсямым кучей. Эти переменные не имеют имен. Обращение к ним возможно только через адреса.

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

#include <iostream>

using namespace std;

int main(){

int *point1, *point2; //объявление переменных типа

point1=new int; /*выделение места в памяти для переменной

*типа int. Администатор кучи выделит место и

* запишет адрес в point1, т.е. инициализирует

* указатель */

*point1=1; /* переменной на которую указывает point1

* присвоить значение равное 1

point2=point1; /*обратите внимание указателю присваивается

* значение другого указателя, подобно присвоению

* переменных a=b, в итоге оба указателя имеют

* одинаковый адрес */

cout<<"\n *point1="<<*point1;

cout<<"\n *point2="<<*point2;

*point1=2; //новое значение динамической переменной

cout<<"\n *point1="<<*point1;

cout<<"\n *point2="<<*point2;

point1=new int; //новое место в памяти для point1

*point1=3; //новое значение динамической переменной

cout<<"\n *point1="<<*point1;

cout<<"\n *point2="<<*point2;

return 0;

}

В результате выполнения программы на экране должно появиться следующее.

Пример показывает, что указателю можно присваивать не только значение переменной, но и значение другого указателя. Можно использовать один и тот же указатель для хранения разных адресов.

При объявлении указателей приходится несколько раз пользоваться звездочкой, например

int * p1,*p2,*p3,*p4;

Чтобы каждый раз не ставить перед именем указателя звездочку можно сначала объявить тип указателя c помощью зарезервированного слова typedef, а затем воспользоваться созданным типом. Например,

typedef int* point; // создать новый тип указателей с именем point

point p1,p2,p3,p4; //объявление переменных типа point

Первую строку можно прочитать так. Объявляется новый тип указателя для переменных типа int.Далее можно читать так - объявляются переменные типа point, эти переменные имеют имена p1,p2,p3,p4. Две строки рассмотренного кода эквивалентны одной, содержащей звездочки.

Для хранения динамических переменных отведен специальный раздел памяти, который называется кучей. Размер кучи указать нельзя, он зависит от памяти компьютера, операционной системы, компилятора и многих других факторов. Если для динамических переменных не хватает размера кучи, то программа останавливается с выводом сообщения об ошибке. Для того, чтобы уменьшить вероятность переполнения кучи нужно освобождать зарезервированную память от динамических переменных, которые далее не используются в программе. С этой целью применяется оператор delete с указанием на динамическую переменную. Например,

delete point1;

После такого оператора значение указателя point1 не определено, в том смысле, что в ячейке памяти с таким адресом находится неопределенное значение, т.к. динамическая переменная уничтожена. Еще раз уточним, неопределенное значение означает, что это может быть любое число. Если на эту переменную указывает другой указатель, то он также становится неопределенным. Указатель, который указывает на область памяти с неопределенным значением, называется висящим указателем. Висящий указатель, как вы понимаете, довольно опасная вещь.

 

 

5.1.Типы указателей и операции с указателями

Указатели о которых мы говорили выше это, так называемые, указатели-переменные. Наряду с ними существуют указатели-константы. А вообще указатели делятся на две категории – указатели на объекты и указатели на функции. Здесь встретилось новое слово объект. Пока под объектом мы будем понимать все то, что не функция, т.е. это переменные любого типа, массивы и структуры. Позднее мы узнаем, что множество объектов значительно богаче. Определение указателей, как отмечалось, может быть совмещено с их инициализацией так

тип *имя_указателя = выражение;

тип *имя_указателя (выражение);

 

Пример

int x=1,y=2;

int * p1=&x, *p2=&y;

Инициализирующее выражение должно давать адрес, т.е это можни быть явно заданный адрес, уже инициализированный указатель, или выражение, позволяющее получить адрес с помощью операции &. Это значит, что над указателями могут проводиться какие то операции. Действительно, это так, однако по сравнению с операциями над базовыми типами список операций над указателями значительно беднее. Над указателями можно проводить след



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2016-02-16 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: