Рассылка закрыта
При закрытии подписчики были переданы в рассылку "Программирование на Visual С++" на которую и рекомендуем вам подписаться.
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
C++ для всех
Информационный Канал Subscribe.Ru |
C++ для всех. Выпуск 1
Здравствуйте уважаемые подписчики!
В связи с первым выпуском данной рассылки я просто обязан рассказать о содержании и структуре всего того, о чем Вы сможете здесь прочитать. Весь материал будет условно разбит на две части: первая часть - вступительная,- разговор пойдет о базовых конструкциях языка (12-15 выпусков), и основная, в которой будут обсуждаться и оцениваться различные способы решения типичных программистских задач; границы второй части, соответственно, не ограничены. Структура выпуска будет примерно такой: базовая информация плюс примеры использования, советы по найлучшему использованию, блок вопрос-ответ.
Начиная первые главы, я буду делать предположение, что читатель не знаком с языком C++, но знает методику программирования и понимает что такое переменная, адрес, компилятор и т.п., это позволит начать изучать этот язык каждому сомневающемуся. По возможности, примеры будут подаваться платформо-независимые. На этом вступительную часть и закончим.
Выпуск 1 Рекомендации по компиляторам. Компиляция программы. Переменные, типы переменных. Указатели, ссылки. Области видимости переменных. Пространства имен. Рекомендации по компиляторам. Компиляция программы |
Сушествует достаточно много компиляторов языка С++, все мы рассматривать не будем, остановимся на наиболее известных. Нас будут интересовать в первую очередь те, которые по возможности, максимально поддерживают стандарт С++ (ISO/IEC 14882.1998).
Рассмотрим сам процесс компиляции (точнее способ получения из исходников исполнимой программы). Как пример возьмем компилятор gcc, т.к. при работе с интегрированными средами все намного проще и сводится к подключению новых файлов к проекту.
Имеем три файла: file1.h,file1.cpp,main.cpp Хотим получить исполнимый файл example (example.exe для windows) g++ -c -o main.o main.cpp g++ -c -o file1.o file1.cpp g++ main.o file1.o -o example
Если на стадии линковки Вы увидите сообщения типа "unresolved external...", - очень частая причина паники начинающих программистов на С++/С, то это говорит о том, что не подключен объектный файл с реализацией соответствующей функции (*.obj,*.o,*.lib - для win-систем; *.obj,*.o,*.a - для unix-систем).
Теперь приступим непосредственно к языку.
В С++, как и в большинстве других язаков программирования, существуют два типа переменных: базовые или фундаментальные, и типы, определенные пользователем (название "определенные пользователем" условно).
Базовые типы:
- логический тип (bool)
- символьный тип (char)
- целые типы (int,short int,unsigned int,long int,long long int ...)
- типы с плавающей запятой (float,double,long double)
- тип void массивы
Типы, определенные пользователем:
- перечислимые типы (enum)
- указатели
- ссылки
- структуры и классы
Опишем вкратце каждый из типов.
Логические типы
Может принимать два значения истина(true) и ложь(false). Используется как результат логических операций. В числовом еквиваленте false соответствует нулевому значению переменной, true - любому ненулевому. Иногда можно встретить сравнение true-результата логической операции с единицей, однако это опасная практика, так как стандартом определено, что true - это любое ненулевое значение. В тоже время стандартом определены правила преобразования, согласно которым гарантируется приведение bool-типа для true - к единице, false - к нулю.
int v = true; //v содержит 1 int v1 = false; //v1 содержит 0
Размер bool-переменной на большинстве машин равен 1 байту. Но, по правилам хорошего программирования (вернее, правильного программирования), при необходимости использовать размер переменных, следует применять оператор sizeof.
int sz = sizeof(bool); //sz содержит размер типа bool
Данная рекомендация очень важная, т.к. размеры одинаковых типов на разных машинах могут отличаться. Кстати, широко используемый в win api тип BOOL (не путать с bool) имеет совсем другой размер.
Символьные типы
Тип char предназначен для хранения одного символа. Практически везде размер этого типа равен одному байту, поэтому максимум мы можем хранить 256 символов.
unsigned char c = 'r'; //переменная с содержит символ 'r' std::cout << c << '\n'; //выведет символ 'r' std::cout << int(c) << '\n'; //выведет числовое значение символа 'r'
При использовании символьных типов как числовых возникает проблема неправильной интерпретации: какой диапазон числа, - от 0 до 255 или от -128 до 127? Поэтому для исключения подобной неоднозначности следует использовать явное определение типов как signed char(-128..127) и unsigned char(0..255).
Некоторые символы являются предопределенными и используются для форматирванного ввода-вывода:
новая строка \n горизонтальная табуляция \t забой \b вертикальная табуляция \v возврат каретки \r прогон листа \f звонок \a обратная косая черта \\ вопрос \? одиночная кавычка \' двойная кавычка \"
Целые типы
К данному типу относятся различные варианты int. Также как и char могут быть явно представлены как signed и unsigned. Размер также пожет регулироваться с помощью short,long,long long. Диапазон значений, и соответственно размер переменных может отличаться на разных машинах, но на х86 как правило определены такие величины:
short int | -32768..32767 |
int,long int | -2147483648..2147483647 |
long long int | -9223372036854775808..9223372036854775807 |
unsigned short int | 0..65535 |
unsigned int,unsigned long int | 0..4294967295 |
unsigned long long int | 0..18446744073709551615 |
Более подробно о размерах и граничных значения можно узнать с помощью шаблона numeric_limits, который определен в файле <limits>.
Например, выведем граничные значения для типа double: std::cout << "double\t"<< std::numeric_limits::min() << ".." << std::numeric_limits ::max() << '\n';
При использовании unsigned-типов следует помнить, что преобразование из типа signed int в unsigned и обратно может привести к совершенно неожидаемым результатам в случае выхода из диапазона конечного типа значения переменной. Хотя, большинство компиляторов пишут предупреждения при подобном неявном преобразовании.
#include... unsigned int i = std::numeric_limits ::max()+1; //i содержит 2147483648 int x = i; //x содержит -2147483648 std::cout << "i=" << i << '\n'; std::cout << "x=" << x << '\n';
Типы с плавающей запятой
Бывают трех типов float, double и long double. Можно сказать, что самый проблематический тип, так как возникает масса проблем при точном вычислении для переносимых (многоплатформенных) задач. В определенной мере компиляторозависимый тип, для Intel 7.1, к примеру, определен тип long float, который соответствует double.
Для целочисленных вычислений nипы с плавающей запятой использовать не рекомендуется, т.к. скорость работы с ними намного ниже чем с int.
Тип void
Данный тип самостоятельно существовать не может, но он "широко" применяется при приведении типов и для обозначения того, что функция ничего не возвращает. Слово "широко", я взял в кавычки, поскольку в С++ приведение из типа void*, совершенно не приветствуется (все делается красиво через шаблоны), однако при разработке базовых закрытых классов он остается очень нужным. На данный тип накладываются определенные ограничения, например недопустимо использовать оператор delete для указателя типа void*, невозможность разименовывания...
В то же время к данному типу автоматически могут приводится любые типы, обратное же возможно только явным приведением.
int *pint = new int; void *v = pint; //pint = v; //вызовет ошибку приведения на этапе компиляции pint = (int*)v; //delete v; //скомпилируется, но поведение не определено 3.9.6, 5.3.5.5 delete (int*)v;
Массивы
Массив - это набор однотипных елементов, расположенных в памяти последовательно. Нумерация массивов производится от 0. Размер массива должен определятся константой, т.е. нижеприведенный вариант не сработает:
void setArray(int size) { char buf[size]; //ошибка на этапе компиляции }
Размер (количество елементов) массива можно узнать оператором sizeof. При передаче в функцию массива в качестве параметра, передается указатель на первый елемент, поэтому sizeof из функции вернет размер указателя на первый елемент:
void checkArray(int *val,int len) { std::cout << sizeof(val) << '\n'; //sizeof вернет размер указателя for(int i=0;i<len;i++) std::cout << val[i] << '\n'; } int main() { const int SIZE = 10; int buf[SIZE]; memset(buf,0,sizeof(buf)); //здесь sizeof правильно покажет размер массива checkArray(buf); return 0; }
Массив на этапе конструирования можно инициализировать определенными значениями:
char buf[] = "aaa"; int size = sizeof(buf); //size равен 4. Учитывается символ окончания строки char cbuf[] = {'a','a','a'}; //теперь размер массива 3 char cbuf[10] = {'a','a','a'}; //размер 10, проинициализированы первые три елементы, остальные содержат случайные значения
Аналогично инициализировать значениями можно и массив сложных типов. Порядок инициализации (конструирования) елементов массива слева направо, деструктор вызывается в обратном порядке. Кроме того, у елементов массива сложных типов, для случая отсутствия списка инициализации (или неполного списка), должен быть определен конструктор по умолчанию):
struct SRec{ int x; SRec(int v):x(v){} }; ... SRec buf[2]; //ошибка, - у елементов нет конструктора по умолчанию SRec buf[2] = { SRec(1),SRec(2) }; //все правильно struct SRec{ int x; SRec(int v=0):x(v){} //теперь это конструктор по умолчанию }; SRec buf[2]; //работает
Многомерные массивы аналогичны обычным.
SRec buf[2][3] = {0,1,2,3,4,5}; buf[0][2].x = 9;
Читаются многомерные массивы слева направо, т.е. (для вышеприведенного примера): массив из двух елементов, каждый из которых является массивом из трех елементов.
Мы рассмотрели статические массивы, основными преимуществами которых являются высокая скорость выполнения (все локальные статические массивы располагаются в стеке, скорость работы которого выше скорости обращения к куче), автоматический деструктор и определенное поведение при возникновении исключительной ситуации при конструировании елементов (точнее, для динамических массивов поведение также определено, но возможны потери памяти). С другой стороны, главный недостаток - это фиксированный размер.
За динамические массивы говорит само их название. Однако для динамических массивов выделение и освобождение занимаемой памяти ложится на плечи программиста. Выделяем память оператором new, освобождаем оператором delete (точнее сказать, в этом случае идет конструирование елементов):
SRec *p = new SRec[10]; //выделили память на 10 елементов delete [] p; //освободили память
Проблема может возникнуть только тогда, когда понадобится изменить размер массива: в данном случае это возможно только созданием нового массива и копированием нужных элементов в новый массив:
const int SIZE = 10; SRec *p = new SRec[SIZE]; //увеличим размер массива на 20 елементов { SRec *p1= new SRec[SIZE+20]; for(int i=0;i<SIZE;i++) //для простых типов лучше использовать memcpy p1[i] = p[i]; delete [] p; //удалим старый p = p1; //присвоим указателю другой адрес } ... delete [] p; //освободим память
Кроме оператора new, для создания массива можно использовать и функцию malloc, для освобождения памяти в этом случае используют free. Размер памяти, выделенный через malloc, можно динамически изменять с помощью функции realloc, причем гарантируется перенос старого содержимого массива на новое место в случае невозможности расширить блок памяти по текущему указателю:
const int SIZE = 10; SRec *p = (SRec*)malloc(SIZE*sizeof(SRec)); p = realloc(p,(SIZE+20)*sizeof(SRec)); //увеличим размер массива на 20 елементов ... free(p); //освободим память
Возникает закономерный вопрос, а зачем тогда нужен new, если malloc-realloc оказывается еще и гибче? Дело в том, что при создании массива с помощью new для каждого елемента вызывается конструктор (при удалении через delete, соответственно, деструктор), при создании через malloc(free) никаких конструкторов(деструкторов) не вызывается, - происходит просто выделение сырой памяти. Поэтому, использование malloc-realloc-free оправдано при создании и частом изменении размеров массивов базовых типов, для массивов елементов сложных типов (классы и структуры с собственными динамическими переменными и конструкторами) необходимо использовать new-delete. Несколько важных ошибок, которые часто допускают начинающие программисты:
- 1.Смешивание разных операторов создания-удаления
Нельзя смешивать разные операторы выделения и освобождения памяти, т.е. создать елемент через new, а удалять через free и наоборот, создать с помощью malloc, а удалять вызовом delete, - поведение в данном случае неопределено, т.е. может быть все что угодно от вылета программы до, образно говоря, форматирования жесткого диска. Очень опасная и труднонаходимая ошибка.
- 2.Вызов оператора delete вместо оператора delete []
Происходит удаление только первого елемента, - в лучшем случае приводит к потерям памяти:
char *p = new char[10]; ... delete p; //забыли после delete поставить []
- 3.Повторный вызов delete или free для уже удаленных объектов
Как правило, на практике происходит немедленное аварийное завершение программы, но по стандарту поведение не определено, поэтому возможна труднонаходимая блуждающая ошибка.
int *p = new int; ... delete p; ... delete p; //попали!!!
- 4.Отсутствие операторов удаления delete (free)
Если логика программы не нуждается в вызове деструкторов, то приводит только к потерям памяти.
Пример создания массива массивов: const int SIZE = 10; SRec **pbuf = new (SRec*)[SIZE]; for(int i=0;i<SIZE;i++) pbuf[i] = new SRec[SIZE]; ... for(int i=0;i<SIZE;i++) delete [] pbuf[i]; delete [] pbuf;
Перечисления
Перечисление - это тип переменной, у которого заранее определен набор данных.
Например: enum eNames{ ered,eblack,egreen }; enum val; val = ered; if(val != eblack){...}
По умолчанию данные нумеруются с нуля, но можно установить номер и самостоятельно:
enum eNames{ ered=10,eblack,egreen }; //eblack-11,egreen-12
Допускается преобразование в int. Номера именованых данных не обязательно уникальны, т.е. допускается:
enum eNames{ ered=10,eblack=10,egreen=10 }; //eblack-11,egreen-12
По этой причине не допускается присвоение объекту типа enum значения int. Хотя явным преобразованием можно добиться всего, при наличии имен с одинаковыми номерами использовать эту методику настоятельно не рекомендуется.
Указатели, ссылки
Указатели, как и ссылки широко распространены в языке С++, поскольку в значительной мере и обеспечивают ту гибкость и эффективность, которой славится язык. Неотделимы также эти понятия и от массивов. Указатель и ссылка на низком уровне представляют собой одно и тоже, - фактически это адрес переменной, но в синтаксисе языка С++ это совершенно разные вещи. Во-первых, ссылка не может существовать сама по себе, т.е. невозможно определить какой либо ссылочный тип без инициализации:
int &x; //ошибка компиляции int c = 10; int &x = c;
Во-вторых, однажды проинициализировав ссылку объектом, невозможно заставить ссылку ссылаться на другой объект. Точнее, если возможно, то для объекта, который именует ссылка, будет вызван копирующий конструктор с новым объектом в качестве параметра.
int x = 10; int y = 20; int &sx = x; sx = y; //х теперь содержит 20
Ссылка фактически является вторым именем объекта, операция взятия адреса ссылки вернет адрес объекта, любые действия над ссылкой являются действиями для реального объекта. Деструктор для ссылки не вызывается.
int x = 10; int &sx = x; std::cerr << (&sx==&x) << '\n';
Указатель, в отличие от ссылки, явно содержит адрес определенного объекта. Разыменование указателя позволяет получить объект, на который указатель указывает. Вызов оператора delete к указателю вызывает деструктор и функцию освобождения памяти для данного объекта. Применение оператора delete к нулевому указателю не приводит ни к каким действиям.
int x = 10; int *px = &x; *px = 20; //x теперь содержит число 20 ... px = 0; delete px; //допустимо но не нужно
Над указателями возможны арифметические и логические действия. Вычитание указателей (одного типа) позволяет узнать, сколько елементов между ними находится. Если к указателю прибавить, к примеру, единицу, то он будет указывать на следующий елемент массива:
Обход и печать массива char: const char *p = "aaaaaaaaaa"; while(*p){ std::cerr << *p++; }
Данный пример показывает, как можно проходить массивы с помощью указателей. Как было сказано выше, массивы и указатели используются очень тесно, в частности для передачи в функции. Интересно, а что предпочтительнее использовать: указатели или ссылки? Рекомендации такие,- исключая конкретные случаи, например передачу массивов, где ссылки просто неудобно использовать, при передаче тех параметров, указатель на которые не может принимать нулевое значение (по логике программы), следует использовать ссылки, в остальных, соответственно, указатели. Это избавит от лишнего цикла проверки на нуль заведомо ненулевых значений в теле функции.
Указатели, как и ссылки, могут содержать выражение const, данное выражение накладывает определенные ограничения на объект или на сам указатель.
Например: int x,y; const int *px = &x; //указатель на константу *px = 20; //ошибка delete px; //интересный момент, изменять нельзя, а удалять можно (определено стандартом) int * const px1 = &x; //константный указатель px1 = &y; //ошибка - нельзя изменять *px1 = 30; //можно const int * const px2 = &x; //константный указатель на константу px2 = &y; //ошибка *px2 = 30; //ошибка char *pc = "aaaaaaaa"; //фактически получается указатель на константу pc[1] = 'd'; //почти гарантированная (зависит от компилятора) ошибка во время выполнения
Указатели могут явно или неявно приводится к другим типам: явно - к любому типу, неявно к родительским или типу void*.
Области видимости переменныхФактически, есть три области видимости переменной: локальная, глобальная и область видимости класса.
Локальная область видимости ограничивается скобками {}, переменная, объявленная внутри этих скобок, не будет видна вне их. Локальные переменные (базовых типов) по умолчанию значениями не инициализируются, хотя различные компиляторы могут инициализировать нулем (опционально, разумеется).
int main(){ { int x = 10; } x = 20; //ошибка компиляции - переменная 'х' не определена return 0; }
Если переменная объявлена вне какой-либо функции, класса или пространства имен, то говорят, что она находится в глобальной области видимости, и к этой переменной можно непосредственно получить доступ с любой части файла после объявленния, где она определена. Все глобальные переменные базовых типов (числовые, логические, символьные, указатели) инициализируются нулем. К переменной также можно обратиться из любого другого файла если:
Например: --------file1.cpp------------------------------------ int global_value = 10; ----------------------------------------------------- --------main.cpp------------------------------------- extern int global_value; int main() { std::cerr << global_value << '\n'; return 0; } -----------------------------------------------------
Без директивы extern мы получим ошибку на стадии линковки типа 'повторное определение переменной'.
Если имена переменных в глобальной области видимости и локальной совпадают, то при входе в блок глобальная будет скрыта. Для доступа к глобальной переменной следует использовать оператор ::
int value; int main() { int value = ::value; return 0; }
При выходе из области видимости для автоматических (все кроме созданных с помощью new,malloc) типов вызывается деструктор. Для статических переменных несколько другая методика: для статических локальных переменных конструктор (и возможная инициализация значением) вызывается при первом использовании переменной, деструктор вызывается при прекращении работы программы в случае использования этой переменной (т.е. конструктор для нее был вызван). Конструктор глобальной статической переменной вызывается перед вызовом главной (main) функции в любом случае, деструктор, соотвественно, вызывается в порядке, обратном вызову конструкторов. Если имеется несколько глобальных статических переменных, то невозможно определить порядок вызова для них конструкторов, - это прерогатива компилятора.
Пространства именПространство имен - это механизм, позволяющий сгруппировать интерфейсы, данные и методы. Для доступа к членам пространства используется уникальный идентификатор. Обращение к членам происходит через оператор ::
Например: namespace MyNamespace{ int x; double val; void func1(){} void func2(){} void func3(); } void MyNamespace::func3() { } int main() { MyNamespace::x = 10; MyNamespace::func1(); return 0; }
Как правило, пространства имен применяются для объединения достаточно больших, логически однонаправленных интерфейсов. Реальный пример, - библиотека STL, определенная в пространстве имен std.
Все члены пространство имен является открытыми, т.е. нет механизмов, обеспечивающих сокрытие информации. Единственным способом является создание новых пространств, которые через using открывают определенные методы требуемых пространств имен. Например, из приведенного выше пространства MyNamespace нам нужно открыть только функции, делаем так:
namespace LimitNamespace{ using MyNamespace::func1; using MyNamespace::func2; using MyNamespace::func3; }
Аналогично можно объединять несколько пространств имен в одно, например:
namespace FullNamespace{ using namespace std; using namespace LimitNamespace; } ... FullNamespace::cerr << "!!!!!!!!\n";
Также можно добавлять в любые пространства имен свои функции, классы, короче все что угодно:
namespace std{ void myFunc(){} } ... std::myFunc();
На этом закончим первый выпуск. Пожелания, предложения, вопросы прошу на iqsoft@cg.ukrtel.net
В следующем выпуске: Функции. Параметры функций. Шаблоны функций.
(с) Юрий Гордиенко iqsoft@cg.ukrtel.net
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||