Глава 1: Въведение в програмирането на С++.
Съдържание на първа глава :
1.1. Решаване на проблеми.
1.2. С++ програма.
1.3. Първоначален поглед върху вход-изход.
1.4. Няколко думи за коментарите.
1.5. Директиви на предпроцесора.
При самостоятелно запознаване с езика С++ естествено възникват два въпроса:
1. Какво представлява една С++ програма? Как се пише?
2. Веднъж написана, как може да бъде изпълнена?
Тази глава описва основните знания и действия, необходими за получаването на изпълнима програма от изходен код на С++.
1.1. Решаване на проблеми
Програмите най-често се пишат в отговор на проблеми или задачи, които трябва да бъдат решени. Нека да разгледаме един пример. Магазин за книги въвежда във файл заглавието и издателя на всяка книга, която продава. Информацията се въвежда по реда на продаваните книги. Всеки две седмици собственикът на ръка изчислява броя на екземплярите на всички продадени книги, както и бройките, отнасящи се до различните издатели. Списъкът се подрежда в азбучен ред и се използува за реорганизация. Вие сте помолени да предложите програма за тази работа.
Един от методите за решаване на голям проблем е раздробяването му на няколко малки проблема. За щастие, тези малки проблеми са по-лесни за решаване. Проблемът на книжарницата се разделя на четири подпроблема или задачи:
1. Четене от файла за продажби.
2. Преброяване на продажбите по заглавие и издател.
3. Подреждане по заглавие и издател.
4. Записване на изходните резултати.
Точки 1, 3 и 4 представят проблеми, които знаем как да решим; те не се нуждаят от допълнително разпадане. Точка 2, обаче, представя проблем, който не може да бъде решен директно. Затова прилагаме своя метод към нея:
2а. Подреждаме продадените книги по издатели.
2б. За всеки издател подреждаме продажбите по заглавие.
2в. Сравняват се съседните заглавия във всяка издателска група. За всяка съответствуваща двойка, се увеличава броят на появите на първият елемент на двойката и се изтрива вторият.
Точки 2а, 2б и 2в също вече представят проблеми, които знаем как да решим. Тъй като вече сме в състояние да решим всички подпроблеми, които бяхме формулирали, то фактически сме решили и по-големия проблем. Последователността от действия ще бъде:
1. Четене от файла продажби.
2. Подреждане на файла продажби - първо по издател, а после в групите на издателите.
3. Сгъстяване на повтарящите се заглавия.
4. Записване на резултатите в нов файл.
Описаната последователност от действия може да бъде наречена алгоритъм. Следващата стъпка е описване на алгоритъма в термините на някой програмен език - в този случай на С++.
1.2. С++ програма
В С++ всяко действие се представя чрез израз. Израз, ограничен с ";" представлява оператор. Най-малката независима единица в езика е оператора. В естествените езици аналогичната конструк ция е изречение. Следващите изрази, например, са оператори на С++.
int value;
value = 7 + 1;
cout << value;
Първият оператор е един декларативен оператор. Той дефинира област от паметта на компютъра, свързана с името value, където могат да се записват цели числа. Вторият оператор е оператор за присвояване. Той помества в паметта на компютъра, свързана с името value, резултата от събирането на 7 и 1. Третият оператор е оператор за изход. Сout описва изход, насочен към потребителския терминал. << е оператор за изход. Операторът записва в cout, т.е. на потребителския терминал, стойността, съхранена в областта от компютърната памет, свързана с името value.
Операторите логически се групират в единици, наречени функции. Например, всички оператори, необходими за четене от файла продажби, са организирани във функция, наречена readIn().
Съответно, създаваме и функциите sort(), compact() и print().
В С++ всяка програма трябва да съдържа функция, наречена main(), написана от програмиста, преди да може да бъде изпълнена. Следва пример за дефиниране на main() за гореописания алгоритъм.
int main() {
readIn();
sort();
compact();
print();
return 0;
}
Програмата на С++ започва да се изпълнява от първия оператор на main(). В този случай, програмата започва с изпълнението на функцията readIn(). Изпълнението й продължава чрез последователното изпълнение на операторите в main(). Тя приключва нормално с изпълнението на последния оператор на main().
Всяка функция се състои от четири части: тип на резултата, име на функцията, списък от аргументи и тяло. Първите три елемента заедно образуват прототипа на функцията. Списъкът от аргументи, затворен в скоби, съдържа списък от нула или повече аргументи, отделени със запетая. Тялото на функцията е затворено в скоби от вида "{ }". То съдържа последователност от програмни оператори.
В този пример, в тялото на main() се извикват за изпълнение функциите readIn(), sort(), compact() и print(). Когато тези функции приключат работа, се изпълнява операторът return 0. Това е един предварително дефиниран оператор на С++, предоставящ метод за прекратяване на изпълнението на функция. Когато операторът е съпътствуван от стойност, 0 например, тази стойност става стойност, която функцията връща. В този случай, връщаната стойност 0 показва успешното приключване на изпълнението на main().
Нека сега да подготвим програмата за изпълнение. Първо трябва да предложим дефиниции за readIn(), sort(), compact() и print(). В този момент следните фиктивни примери са достатъчно добри.
void readIn() { cout << "readIn()n"; }
void sort() { cout << "sort()n"; }
void compact() { cout << "compact()n"; }
void print() { cout << "print()n"; }
void се използува за да укаже, че функцията не връща стойност. Както е дефинирано, всяка функция просто ще съобщи за своето съществуване чрез написване на съответния текст върху терминала на потребителя, когато бъде извикана от main(). По-късно бихме могли да заменим фиктивния пример със самото тяло, когато то бъде реализирано. Този метод на последователно увеличаване на кода на програмата е полезен за контролиране на програмните грешки, които неминуемо допускаме. Да се опитваме да направим работеща програма отведнъж е просто много сложно и объркващо.
Всеки файл, съдържащ текст на програма се състои от две части - име на файл и разширение. Разширението често определя съдържанието на файла. Това разширение ще се променя при различните реализации на С++. При операционната система UNIX, името на С++ файла може да бъде завършено както със ".с", така и със ".С". Употребата на малка буква ".с" показва съществуващата връзка между програмните езици С++ и С. (Всички файлове, съдържащи програми на С трябва да завършват със ".с"). За да бъде различаван С++ програмен файл, използувайте разширение ".С". (ў)
Въведете следната пълна програма в един С++ кодов файл.
#include <stream.h>
void read() { cout << "read()n"; }
void sort() { cout << "sort()n"; }
void compact() { cout << "compact()n"; }
void write() { cout << "write()n"; }
int main() {
read();
sort();
compact();
write();
return 0;
}
Stream.h е един от т.н. заглавни файлове. Той съдържа информация за cout, която е необходима за написаната програма. #include е една предпроцесорна директива. Тя указва добавянето на съдържанието на stream.h към нашия текстов файл.
След като вече програмата е записана във файл може да бъде компилирана. Това се прави така ($ е системния показалец):
$ СС prog1.C
Името на командата, използувано за извикване на компилатора на С++ ще бъде различно за различните реализации. СС е името на командата, използувано от AT&T C++ Language System, Release 2.0. Направете справка в своя наръчник или попитайте вашия системен администратор за името на С++ компилатора във вашата система.
Част от работата на вашия компютър е да анализира текста на програмата за коректност. Компютърът не може да установи дали логиката на вашата програма е правилна, но може да открие грешки във формата на програмата. Най-общо програмните грешки, които компилаторът открива са:
1. Синтактични грешки. Програмистът е направил "граматическа грешка" според езика С++. Например:
int main(
{ // error: missing `)ґ
readIn: // error: illegal character `:ґ
sort();
compact();
print();
return 0 // error: missihg `;ґ
}
2. Грешки за типове. Данните в С++ се свързват с типове. Стойността 10, например, е цяла. Думата "hello", заградена от двойни кавички е символен низ. Ако на дадена функция очакваща аргумент от цял тип се подаде аргумент тип низ, компилаторът ще сигнализира за грешка при типовете.
Всяко съобщение за грешка съдържа номер на ред и кратко описание на причината, поради която се счита, че вие сте сгрешили. Препоръчва се грешките да се коригират в последователността, в която се появяват. Често единична грешка може да предизвика лавинообразен ефект и компилаторът да съобщи за повече грешки, отколкото фактически съществуват. След като коригирате текста на програмата си, можете да я компилирате отново.
За операционната система MS-DOS, обаче, разширението ".c" не е допустимо - MS-DOS не прави разлика между малки и големи букви.
Реализацията на С++ под управлението на MS-DOS поддържа следните разширения на файлове:
cxx (x е + завъртян на 90 градуса).
cpp (p съответствува на "плюс"-а).
Втората част от работата на компилатора е да преведе формално коректния програмен текст. Този превод, наречен генерация на код, фактически генерира обектов или асемблерен текст, разбираем за компютъра на който програмата ще бъде изпълнявана.
Резултатът от успешна компилация е изпълним файл. По подразбиране, при системата на автора този файл се нарича a.out. Той може да бъде изпълнен както следва:
$ a.out
Изходът на програмата изглежда така:
readIn()
sort()
compact()
print()
Командната опция "-о име" позволява на програмиста да даде име на програмния файл, различно от a.out. Например, командният ред
$ CC prog1 prog1.C
ще създаде изпълним файл prog1. За да се изпълни тази програма трябва да се напише:
$ prog1
Освен езиковия компилатор реализацията на С++ предлага и набор от стандартни библиотеки. Библиотеките са съвкупност от предварително дефинирани функции. В С++, например, входът и изходът се поддържат от стандартна С++ библиотека. Програмистите могат да използуват библиотечните функции в програмите си по същия начин, по който използуват функциите, които са написали.
Най-голямото предимство на програмните библиотеки е това, че позволяват на добри програмисти да окомплектоват своите знания за използуване от множество други програмисти. Малко от нас, например, знаят как работи електричеството, но повечето знаят как да включат осветлението. Добрата библиотека може да бъде използувана така лесно, както и ключа за осветление. В следващата част се прави един кратък преглед на входно-изходната библиотека.
1.3. Първоначален поглед върху вход-изхода.
Входът и изходът не са част от езика С++, но се поддържат от библиотека, написана на С++ и позната като iostream (вх/изх. поток). Приложение А разглежда подробно тази библиотека(ў). В този дял е дадена достатъчна информация за начинаещия читател.
Входът, постъпващ от потребителският терминал, наречен стандартен вход, е свързан с предварително дефинирания във вх/изх. поток вход cin (произнася се "see-in"). Изходът, насочен към потребителския терминал, се нарича стандартен изход и се свързва с предварително дефинирания във вх/изх. поток изход cout (произнася се "see-out").
Операторът за изход ("<<") се използува за изпращане на стойност към стандартния изход. Например:
cout << "The sum of 7 + 3 = ";
cout << 7 + 3;
cout << "n";
Последователността от символи n представя символа нов ред. При писане той предизвиква насочване на печата на следващия ред. Последователност от входно-изходни оператори може да бъде обединена в един оператор. Например,
cout << "The sum of 7 + 3 = " << 7 + 3 << "n";
Всеки следващ изходен оператор може да бъде добавен на свой ред към cout. За четимост обеднените изходни оператори могат да се разположат на няколко реда. На следващите три реда е записан един изходен оператор.
Читателят, разполагащ с реализация на С++, която не се основава на версия 2.0, има по-ранната версия на библиотеката, наречена stream. Примерите в тази книга използуват подмножество, общо за двете версии.
cout << "The sum of ";
<< v1 << " + ";
<< v2 << " = " << v1 + v2 << "n";
Съответно операторът за вход (" >> ") се използува за четене на стойност от стандартния вход. Например, следната програма реализира прост алгоритъм за четене на две стойности, определяне на по-голямата от тях и отпечатването й.
#include <stram.h>
void read2 ( int1&, int2& );
int max( int, int );
void writeMax( int );
main() {
int val1, val2;
read2( val1, val2 );
int maxVal = max( val1, val2 );
writeMax( maxVal );
return 0;
}
void read2(int& v1, int& v2); {
cout << "nPlease enter two numeric values: ";
cin >> v1 >> v2;
}
int max( int v1, int v2); {
if ( v1 > v2 )
return v1;
else
return v2;
}
viod writeMax( int val ) {
cout << val << " is the largest value.n";
}
По тази програма могат да бъдат направени няколко забележки. Описани са три имена на функции преди да бъде дефинирана функцията main(). Първоначалният списък се нарича предварителна декларация, съобщаваща на програмата, че тези функции съществуват и описанията им могат да бъдат намерени някъде в текста на програмата, представена от този файл или в отделен файл. Всяка функция трябва да бъде декларирана преди да може да бъде викана. Предварителната декларация е един от начините да се направи това. val1 и val2 се наричат променливи. Операторът
int val1, val2;
дефинира тези променливи за програмата. И променливите трябва да бъдат обявени преди да могат да бъдат използувани. Подробен преглед на променливите е направен в глава 1.
v1 и v2, наричани формални параметри, изграждат списъка от аргументи на функциите read2() и max(). val е единствения формален параметър на writeMax(). Същността на дефиницията им е разгледана в дял 4.6, където се обсъжда как се описват параметрите на функции.
Читателят сигурно е забелязал, че main() e описана малко по- различно този път. Не е описан явно типа на връщания резултат. Това е разрешено в С++: функция, за която не е зададен тип за връщане, връща тип цяло число по подразбиране. Когато програмата бъде компилирана и изпълнена се получава следния изход (потребителят е въвел стойностите 17 и 124):
Please enter two numeric values: 17 124
124 is the largest value.
Двете стойности, 17 и 124, са разделени с интервал. Интервалите, табулациите и новите редове са празно пространство (white space) за С++. Операторът
cin >> v1 >> v2;
правилно чете двете стойности, защото операторът за изход (">>") пропуска цялото празно пространство, което е въведено.
Предварителната дефиниция на cin и cout се съдържа в stream.h. Ако програмистът забрави да включи този файл, всички обръщения към cin и cout ще бъдат отбелязани от компилатора като грешки. Включването на предварителните декларации показва първичната употреба на заглавния файл. ( Част 5.5 разглежда заглавните файлове в подробности).
Съществува и един трети вх/изх. поток, cerr (произнася се "see-err"), наречен стандартна грешка, който също се свързва с потребителския терминал. cerr се използува за да уведомява потребителя относно някои особени състояния на програмата по време на изпълнение. Например, следната част от програма предпазва програмиста от деление на нула:
if (v2 == 0 ) {
cerr << "nerror: attempt to divide by zero";
return;
}
v3 = v1/v2;
Следващата програма чете по един символ от стандартния вход докато не срещне end-of-file. Тя брои прочетените редове и символи. Изходът й има следния вид:
lineCount characterCount
Описанието й има вида:
#include <stream.h>
main() {
char ch;
int lineCnt=0, charCnt=0;
while ( cin.get(ch) ) {
switch ( ch ) {
case `tґ:
case ` ` : break;
case `nґ: ++lineCnt;
break;
default : ++charCnt; break;
}
}
cout << lineCnt << " " << "n";
return 0;
}
get() е функция, принадлежаща на вх/изх. поток, която чете символи и ги записва като свой аргумент - в този случай, ch. Последователността от два символа n представя символа tab.
Операторът switch осигурява условен преход според стойността на селектора си. Ако стойността му съвпада с някой от описаните след думата case стойности, се изпълнява оператора, съответствуващ на тази стойност. Ако съответствие не може да бъде намерено, се изпълняват операторите, записани след default. За всеки прочетен символ за нов ред се увеличава стойността на lineCnt с 1. charCnt се увеличава с 1 всеки път, когато е прочетен символ, който не е табулатор, интервал или нов ред.
Операторът while, наричан оператор за цикъл, осигурява многократно изпълнение на група от оператори докато някакво условие има стойност истина. В този случай, операторът switch се изпълнява докато get() чете символ от стандартния вход. (Операторите while и switch са описани в глава 6).
Упражнение 0-1. Въведете програмата във файл prog2.c и я компилирайте в изпълним файл, наречен prog2.
Упражнение 0-2. Изпълнете програмата с текста на предишното упражнение. Изпълнете я отново с текст, съдържащ само празни интервали. Изпълнете я за текст, съдържащ само знака end-of-file.
Упражнение 0-3. Преработете програмата така, че да брои табулаторите (tabCnt) и интервалите (blankCnt). Нека изходът да изглежда така.
Total Characters: xx
Lines: x
Chars: x
Tabs: x
Blanks: x
1.4. Няколко думи за коментарите
Коментарите се пишат за да подпомагат четенето на програмите. Те могат да поясняват алгоритъма на функцията, да описват употребата на променливите или да поясняват други аспекти по текста на програмата. Коментарите не увеличават размера на изпълнимата програма. Те се отстраняват от компилатора преди генерацията на кода.
В С++ са дефинирани два типа коментари. Двойката ("/* /") описва коментарите в езика С. Началото на коментара се определя чрез "/". Всичко, което следва тези символи ще бъде третирано от компилатора като коментар до срещането на символите "*/". Коментар може да бъде поставян там, където могат да се поставят табулатори, интервали или символи за нов ред и той може да заема няколко реда от програмата. Например,
/*
This is a first look at a C++ class definition.
Classes are used both in data abstraction and
object-oriented programming. An implementation
of the Screen class is presented in Chapter 5. */ class Screen {/* This is referred to as the class body */ public:
void home(); /* move cursor to 0,0 */
void refresh(); /* redraw Screen */
private:
/* Classes support "information hiding". */
/* Information hiding restricts a programґs */
/* access to the internal representation of */
/* a class (its data). This is done through */
/* use of the "private:" label */
char cursor; / current Screen position */
};
Многото коментари, разположени хаотично в програмния код, могат да направят текста неясен. Например, обградената с коментари дефиниция на cursor почти я скрива. Изобщо, за предпочитане е коментарът да се помества в цялостен блок над текста, който пояснява.
Коментарите не могат да бъдат влагани. Т.е., коментар не може да се поставя във вътрешността на друг коментар. Въведете програмата, написана по-нататък в текста. Тя ще предизвика генерирането на серия от грешки. Как бихте могли да се справите с това?
#include <stream.h>
/*
comment pairs /* */ do not nest.
"do not nest" is considerd sourse code,
as are both these lines and the next.
*/
main() {
cout << "hello, worldn";
}
Вторият тип коментари се означава чрез двойката символи //, ограничаващи коментар от един ред. Текстът, записан от дясно на този ограничител, се третира като коментар и се игнорира от компилатора. Например,
#include <strem.h>
#include "myIO.h"
int isOdd( int );
main() {
int v1, v2; // hold values from user
read2( v1, v2 ); // declared in myIO.h
if ( isOdd( v1 ) == 1 )
cout << v1<< " is oddn";
if ( isOdd( v2 ) == 1 )
cout << v2<< " is oddn";
return 0;
}
isOdd( int val ) {
/* return 1 if val is odd; otherwise, return 0 *
% is the modulus operator; 3 % 2 yields 1. */
return( val % 2 != 0 );
}
Тази програма определя дали стойностите са четни или нечетни.
Тя използува функцията read2(), дефинирана в предходния подраздел. Прототипът на тази функция е зададен в заглавния файл myIO.h. Когато програмата бъде компилирана и изпълнена, ще бъде изведено следното (стойностите 497 и -25 са въведени от потребителя):
Please enter two numeric values: 497 -25
497 is odd
25 is odd
Двойката символи, ограничаваща коментарите, не може да бъдат разделяни с интервал. Следните два реда, например, няма да бъдат разглеждани като коментари, а по-скоро като част от програмния текст:
/ * not a comment: white space not allowed */
// also not a comment: must be //
Обикновено програмите съдържат както единия тип коментари, така и другия. Обширните разяснения най-често се организират в коментар като се използува двойката коментарни ограничители. За бележки, разположени на половин или един ред се използуват символите //.
1.5. Директиви на предпроцесора
Редом със стандартните библиотеки се предлага набор от стандартни заглавни файлове, такива като stream.h. Тези заглавни файлове съдържат цялата информация, необходима на потребителя, за да използува лесно библиотеките. За да имаме достъп до променливите и функциите, дефинирани в тези библиотеки, ние трябва да включим съответния заглавен файл в нашата програма.
Заглавните файлове стават част от нашата програма като се използува директивата include. Директивите се определят като се постави знака # в първата колона от реда в нашата програма. Директивите се обработват преди извикването на езиковия компилатор. Програмата, която обработва директивите се нарича пред-процесор.
Директивата #include чете от съдържанието на именуван файл. Тя има един от следните два формата:
#include <stream.h>
#include "myIO.h"
Ако името на файла е затворено в ъглови скоби ("<>") се подразбира, че файлът е предварително дефиниран, или стандартен, заглавен файл. Търсенето за откриването му ще бъде проведено в предварително дефинирано множество от местоположения, което може да бъде променяно чрез опцията -I на командата СС. Например, командата
$ CC -I incl -I/usr/local/include prog1.c
указва на предпроцесора да потърси първо в директория incl, а после в /usr/local/include, преди да преглежда предварително дефинираното множество от местоположения. При първият открит екземпляр на файла се преустановява търсенето.
Ако името на файла е затворено в двойни кавички се подразбира, че това е заглавен файл, създаден от потребителя. Търсенето му започва от текущата директория. Ако не бъде намерен там се преглежда множеството на предварително дефинираните местоположения. Опцията -I също работи със заглавни файлове, дефинирани от потребителя.
Всеки включен файл може също да съдържа директива #include. Поради това е възможно даден заглавен файл да бъде включен неколкократно в изходен файл. Могат да бъдат използувани условни директиви за да се предотврати многократната обработка на даден заглавен файл. Например,
#ifndef STRING_H
#define STRING_H
/* String.h contents go here */
#endif
Условната директива #ifndef приема стойност истина, когато името, което следва не е дефинирано все още. Когато условната директива приеме стойност true съвкупността от редове до #endif се считат включени. Ако условната директива приеме стойност false, тези редове ще бъдат игнорирани.
Директивата #define дефинира името, което я следва. В този случай тя дефинира STRING_H. Ако заглавният файл String.h се включва отново директивата #ifndef ще приеме стойност false и съдържанието на Stream.h няма да бъде включено повторно. Директивата #ifdef приема стойност true, когато името, записано след нея, е дефинирано. Например,
#ifdef u3b2
/* system specific code
for AT&T 3B ocmputers goes here */
#endif
#ifdef sun
/* system spesific code
for Sun computers goes here */
#endif
С++ предварително дефинира името __cplusplus (две подчертаващи тиренца). Потребител, който желае да смесва програми, написани на С и С++, трябва да запише следното:
#ifdef __cplusplus
extern min( int, int );
int *pi = new int;
#else
extern min();
int *pi;
#endif
Операторите между директивите #else и #endif ще бъдат включени ако директивата #ifdef приеме стойност true. Предпроцесорът е тясно свързан с езика С (често се нарича cpp, С - пред-процесор). Много реализации просто използуват предпроцесора на С и следователно не разпознават коментара на С++, ограничаван от //. Ако желаете да включите коментар в директива #define по-безопасно е да използувате обичайния за С коментар.
#ifdef u3b2
#define SYSV /* UNIX System V */
#endif