Wednesday, March 7, 2007

С хэлний хичээл No 02

Cи++ хичээл2.
Си++ хэлэнд програмын эх код нь жирийн текст байдаг гэдгийг дээр дурдсан. Хөрвүүлэгч нь үндсэндээ ийм текстийг машины хэл рүү хөрвүүлж компьютер дээр шууд ажиллахаар хэлбэрт оруулах үүрэгтэй. Энэ процесс нь үнэндээ нилээд нийлмэл процесс бөгөөд үүнийг бид энд нарийвчлан авч үзэх гэж байна.

Юуны түрүүнд програмын эх код нь хэд хэдэн текст файлаас бүрдэж болдог. Хөрвүүлэгчээр нэг удаа оруулж болохуйц хэд хэдэн ийм эх файлын нэгдлийг модуль (эсвэл хөрвүүлэлтийн буюу компиляцын нэгж) гэж нэрлэнэ. Нэг програм хэд хэдэн модультай байж болно. Модуль болгонд үндсэн файл гэж нэрлэгдэх цор ганц файл байх ёстой бөгөөд энэ файл дотор түүнтэй хөрвүүлэлтийн үед нэгтгэх бусад файлуудыг зааж өгдөг. Энэ нэгтгэгдэж байгаа файлуудыг толгой файлууд гэж нэрлэдэг. Үндсэн файлаас хэдэн ч толгой файлыг дуудан нэгтгэж болно. Үндсэн файл нь ихэвчлэн “.cpp”, “.cc”, эсвэл “.C” өргөлгөлтэй, толгой файл нь ихэвчлэн “.h”, “.hpp”, эсвэл “.H” өргөтгөлтэй, стандарт сангийн толгой файл бол өргөтгөлгүй байдаг. Жишээ болгож Зураг 1-д нэгэн хөрвүүлэлтийн процессыг дүрсэлжээ. Үүнд test.cpp нэртэй үндсэн файлд test.h ба iostream нэртэй хоёр толгой файлыг нэгтгэхийг шаардсан комманд бичигдсэн байгаа. Ингээд test.cpp файлыг (модулийг) хөрвүүлэх үед хамгийн түрүүнд уг файл препроцессороор боловсруулагдана. Препроцессор нь test.cpp файлын эхэнд test.h ба iostream хоёр файлыг залган нэгтгэж нэг урт текст болгож хувиргахаас гадна энэ текстэд бичигдсэн препроцессорын директивүүд гэж нэрлэгдэх тусгай коммандуудыг гүйцэтгэж програмын эх кодыг өөрчлөх ажиллагаа явуулна. Толгой файлуудыг холбох комманд нь препроцессорын директивийн нэг жишээ болно. Препроцессорын ажиллагааны үр дүнд програм маань Си++ хэл дээрх текст хэвээрээ үлдэнэ гэдгийг анхаараарай. Үүний дараагаар програм нь Си++ хэлний хөрвүүлэгчээр ассемблер хэл рүү хөрвүүлэгдэх ба эцэст нь ассемблерийн хөрвүүлэгчээр орж машины кодыг агуулсан объект модуль (буюу объект файл) гэгдэх тусгай форматад хувирна. (Энд объект гэдэг үг объект хандалтат програмчлалтай ямар ч холбоогүй гэдгийг анхаар.) Объект файлууд нь ихэвчлэн “.obj” эсвэл “.o” өргөтгөлгэй байдаг. Үнэндээ орчин үеийн хөрвүүлэгчид эдгээр бүх алхмуудыг нэгтгэсэн мэтээр ажиллаж шууд объект файлыг гаргадаг бөгөөд завсрын үр дүнгүүдийг, жишээ нь препроцессороос юу гарч байгааг харахын тулд тусдаа препроцессор ашиглах эсвэл хөрвүүлэгчийн тохируулгыг өөрчлөх хэрэгтэй юм.

Объект файлыг шууд “ажиллуулж” болохгүй бөгөөд биелэх файл гаргаж авахад дахиад нэг алхам хэрэгтэй. Нэгдүгээрт, хэрэв програм маань Си++ хэлний стандарт сангаас ямар нэг функц (Си++ хэлэнд дэд програмыг функц гэнэ) дуудаж ашигладаг бол энэ функцын код нь манай объект модульд байхгүй. Хоёрдугаарт, хэрэв програм олон модультай бол модуль дамнасан функц дуудах процесс эрт орой хэзээ нэгэн цагт явагдах ба нэг объект файл зөвхөн нэг л модульд харгалзах функцүүдыг агуулдаг. Гуравдугаарт, биелэх файлууд үйлдлийн системээсээ хамааран түүнийг ажилладаг програм гэдгийг нь илтгэх тусгай “толгойн хэсэгтэй” байх ёстой бөгөөд объект модуль нь ийм толгойн хэсэггүй. Дөрөвдүгээрт, Си++ хэл дээр бичигдсэн програм ажиллаж эхлэхийн өмнө зарим хувьсагчдыг байрлуулж утга олгох, стек санах ойг бэлдэх, програмд ирсэн параметрүүдийг цуглуулах, програмыг ажиллуулахад системийн нөөц хүрэлцэхгүй тохиолдолд програмаас гарах гэх мэт үүрэг гүйцэтгэдэг эхлүүлэх модуль ажиллах шаардлагатай байдаг. Объект модуль нь эхлүүлэх модультай нэгэн адил биелэх програмыг бүрдүүлэгч нэгж бөгөөд тэдгээр нь аль нэгийгээ агуулдаггүй. Нэг модуль - нэг объект модулийг (объект файлыг) үүсгэхэд зориулагддаг болохыг уншигч ажигласан байх. Тэгэхээр хэрэв програм олон модультай бол модуль болгонд харгалзах объект файлуудыг үүсгэх хэрэгтэй. Үүний дараагаар тухайн програмд харгалзах бүх объект файлуудыг холбогч (linker, компоновщик) хэмээгдэх програмын оролтонд өгөх ёстой. Холбогч програм нь объект модулиуд дахь, гадны функцэд хандсан коммандуудын “ээдрээг” бусад объект модулиуд ба стандарт сангуудаас тохирох функцүүдийг хайж олох замаар “тайлж”, стандарт сангуудаас ашиглагдаж буй функцүүдийг объект модулиудын идэвхтэй хэсгүүд ба эхлүүлэх модультай нэгтгэн нэг биелэх файл болгох үүрэгтэй. Стандарт сан гэдэг нь үнэндээ мөн урьдчилан хөрвүүлэгдсэн объект модулиудын нэгдэл бөгөөд дотроос нь ямар нэг функц дуудагдсан объект модуль бүрийг биелэх файлтай холбодог. Зөвхөн дуудагдсан функцүүд биш, мөн бүхэл сан тэр чигээрээ биш, хэрэгтэй дэд програмыг агуулсан объект модулиуд холбогдож буйг анхаараарай. Програм зохиогч сүүлд ашиглах зорилгоор санч (librarian, библиотекарь) хэмээх програмыг ашиглаж объект файлуудыг нэгтгэн өөрийн гэсэн дэд програмуудын санг байгуулж болно. Ингэхдээ нэг объект файлд аль болох цөөн функцүүд байрлуулбал биелэх файлын хэмжээ бага байна гэсэн үг.

Объект файлд програмын идэвхтэй кодоос гадна түүнд агуулагдаж буй өөр модулиас дуудагдах боломжтой (функцүүд, хувьсагчид гэх мэт) объектуудын хаяг ба нэрсийг агуулсан глобаль тэмдэгтүүдийн хүснэгт, мөн тухайн модуль дотроос дуудагдсан өөр модульд орших объектуудын нэрс ба дуудсан коммандуудын хаягийг агуулсан гадагш хандалтын хүснэгт хадгалагддаг. Эдгээр хүснэгтүүд дэх мэдээллийг ашиглаж холбогч програм харгалзах объектын хаягийг уг объектыг дуудаж буй газар орлуулан тавьснаар модулиудын хооронд байнгын логик холбоосыг бий болгодог. Ерөнхий тохиолдолд програмаас функцийг дуудахад түүний ажиллагаанд шаардлагатай аргументуудыг дамжуулж, функцийн ажиллагааны үр дүн болсон буцсан утгыг хүлээж авдаг. Холбогч нь зөвхөн функцийн нэрийг ашигладаг тул холбогчийн түвшинд функцийг тохирох төрлийн аргументуудтай дуудаж тохирох төрлийн хувьсагчид утгыг буцааж авч байна уу гэдгийг шалгах арга байхгүй. Паскаль гэх мэт хэлнүүдэд тусгай форматтай (жишээ нь, Дельфи-гийн хувьд “.dcu” өргөтгөлтэй) “объект” файлд эдгээр мэдээллүүдийг хадгалдаг бол Си++ хэл нь универсаль хэл болох үүднээс ийм стандарт бус объект файлын форматыг ашигладаггүй. Дуудагдаж буй функцийн аргументууд ба буцсан утгын төрлийн зөв эсэхийг зөвхөн хөрвүүлэгчийн түвшинд шалгадаг. Үүний тулд функцийн прото-төрөл буюу зарлалт хэмээх ойлголтыг хэрэглэнэ. Зарлалт нь тухайн функцийг ямар төрлийн аргументууд авч ямар төрлийн утга буцаахыг зааж өгсөн (өөр ямар ч мэдээлэл агуулаагүй) бүтэц бөгөөд модуль тус бүрийн хувьд гадагш нь “экспорт” хийж буй функцүүдийн зарлалтыг нэг толгой файлд байрлуулаад уг функцэд ханддаг бүх модульд нэгтгэж хөрвүүлдэг. Мөн сангийн функцийн зарлалтыг толгой файлд байрлуулах ба уг функцэд ханддаг бүх модульд нэгтгэж хөрвүүлдэг. Ингэснээр функц зөв аргументтай дуудагдаж буй эсэхийг хөрвүүлэгч уг толгой файл дахь зарлалттай жишиж тодорхойлох боломжтой болох юм.

Олон модультай програмыг хөрвүүлэх ерөнхий схемийг Зураг 2-т жишээгээр үзүүлжээ. Энэ жишээнд програм нь харгалзан test.cpp ба aux.cpp үндсэн файлуудыг агуулсан хоёр модулиас тогтоно. test.cpp файлаас дуудагдаж буй aux.cpp файл дахь бүх функцүүдийг aux.h толгой файлд, aux.cpp файлаас дуудагдаж буй test.cpp файл дахь бүх функцүүдийг test.h толгой файлд зарласан байх ёстой. Тэгэхээр жишээ нь, aux.h файл test.cpp файлд залгагдаж хамт хөрвүүлэгдэх тул энэ модулийг хөрвүүлэх агшинд сүүлд холбогдох (aux.obj объект файлд агуулагдах) функцүүдийг яаж зөв дуудахыг хөрвүүлэгч мэдэж байна гэсэн үг. Үүнтэй төстэйгөөр стандарт болон бусад сангуудын функцүүдийг ашиглахын тулд харгалзах (энэ жишээнд iostream) толгой файлуудыг үндсэн файлд залгаж хөрвүүлэх хэрэгтэй. Эцэст нь test.obj ба aux.obj объект файлуудыг сангийн объект модулиуд болон эхлүүлэх модультай холбож test.exe биелэх файлыг гаргаж авна.

Тэмдэглэл. Холбогч стандарт болон бусад сангуудаас өмнө объект файлуудаас эхэлж функцийг эрэх ажиллагаа явуулдаг. Иймд ямар нэг сангийн функцтэй адил нэр (прото-төрөл) бүхий “шинэ” функцийг модуль экспортлож буй нөхцөлд уг сангийн функц “дарагдаж” оронд нь “шинэ” функц үргэлж дуудагдах болно. Энэ нь зарим тохиолдолд олоход хэцүү алдаанд хүргэж мэдэх бөгөөд Си++ хэлний нэрийн огторгуйг ашигласнаар энэ хүндрэлээс гарч болно. Нэрийн огторгуй нь функцийн нэрийг объект файлд бичихдээ автоматаар өөрчилдөг Си++ хэлний нэр чимэх (name decoration, mangling) хэмээх механизмын нэг илрэл юм.

1 comment:

Batsuren said...

yammmar urt hicheel veee...