portaldacalheta.pt
  • Главни
  • Агиле Талент
  • Финансијски Процеси
  • Дизајн Бренда
  • Трендови
Технологија

Како функционише Ц ++: Разумевање компилације



Бјарне Строуструп’с Програмски језик Ц ++ има поглавље под називом „Обилазак Ц ++: Основе“ - Стандард Ц ++. То поглавље, у 2.2, на пола странице помиње поступак компајлирања и повезивања на Ц ++. Компилација и повезивање су два врло основна процеса која се дешавају све време током развоја софтвера Ц ++, али што је необично, многи програмери Ц ++ их не разумеју добро.

цурење меморије у Јава примеру

Зашто се изворни код Ц ++ дели на заглавље и изворне датотеке? Како компајлер види сваки део? Како то утиче на компилацију и повезивање? Много је више питања попут ових о којима сте можда размишљали, али сте их прихватили као конвенцију.



Без обзира да ли дизајнирате Ц ++ апликацију, имплементирате нове функције за њу, покушавате да решите грешке (посебно одређене чудне грешке) или покушавате да Ц и Ц ++ код раде заједно, знајући како ће компилација и повезивање уштедети пуно времена и чине те задатке много пријатнијим. У овом чланку ћете научити управо то.



Чланак ће објаснити како компајлер за Ц ++ ради са неким основним језичким конструкцијама, одговорити на нека уобичајена питања која су повезана са њиховим процесима и помоћи ће вам да заобиђете неке повезане грешке које програмери често праве у развоју Ц ++.



Напомена: Овај чланак садржи пример изворног кода са којег се може преузети хттпс://битбуцкет.орг/даниелмуноз/цпп-артицле

Примери су састављени у ЦентОС Линук машини:



$ uname -sr Linux 3.10.0-327.36.3.el7.x86_64

Коришћење верзије г ++:

$ g++ --version g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-11)

Достављене изворне датотеке требале би бити преносиве на друге оперативне системе, иако би датотеке Макефиле које их прате за аутоматизовани процес израде требало да буду преносиве само на системе сличне Унику.



Цјевовод изградње: предпроцес, компајлирање и веза

Свака изворна датотека Ц ++ треба да се компајлира у објектну датотеку. Објектне датотеке које настају компилацијом више изворних датотека се затим повезују у извршну датотеку, дељену библиотеку или статичку библиотеку (последња од њих је само архива објектних датотека). Изворне датотеке Ц ++ углавном имају наставке .цпп, .цкк или .цц.

Изворна датотека Ц ++ може да садржи и друге датотеке, познате као датотеке заглавља, са #include директива. Датотеке заглавља имају наставке попут .х, .хпп или .хкк или уопште немају додатак као у Ц ++ стандардној библиотеци и заглавним датотекама других библиотека (попут Кт). Проширење није битно за Ц ++ претпроцесор, који ће дословно заменити ред који садржи #include директиву са целокупним садржајем укључене датотеке.



Први корак који ће компајлер учинити на изворној датотеци је покретање претпроцесора на њему. Компајлеру се прослеђују само изворне датотеке (ради претпроцесирања и компајлирања). Датотеке заглавља се не прослеђују компајлеру. Уместо тога, они су укључени из изворних датотека.

Свака датотека заглавља може се отворити више пута током фазе предобраде свих изворних датотека, у зависности од тога колико их изворних датотека укључује или колико их садржи и других датотека заглавља које су обухваћене изворним датотекама (индиректности може бити много) . С друге стране, изворне датотеке компајлер (и претпроцесор) отвори само једном када му се проследе.



За сваку изворну датотеку Ц ++, претпроцесор ће израдити јединицу за превођење уметањем садржаја у њу када истовремено пронађе директиву #инцлуде када ће уклонити код из изворне датотеке и заглавља када пронађе условна компилација блокови чија директива процењује на false. Такође ће учинити нешто остале задатке попут замене макроа.

Једном када претпроцесор заврши са стварањем те (понекад огромне) јединице за превођење, компајлер започиње фазу компилације и производи објектну датотеку.



Да би се добила та јединица за превођење (унапред обрађени изворни код), -E опција се може проследити компајлеру г ++, заједно са -o опција за специфицирање жељеног имена претходно обрађене изворне датотеке.

У cpp-article/hello-world директоријума, постоји датотека примера „хелло-ворлд.цпп“:

#include int main(int argc, char* argv[]) { std::cout << 'Hello world' << std::endl; return 0; }

Креирајте претходно обрађену датотеку тако што ћете:

$ g++ -E hello-world.cpp -o hello-world.ii

И погледајте број линија:

$ wc -l hello-world.ii 17558 hello-world.ii

У мојој машини има 17.588 линија. Такође можете само покренути make у том директоријуму и урадиће те кораке уместо вас.

Видимо да компајлер мора да компајлира много већу датотеку од једноставне изворне датотеке коју видимо. То је због укључених заглавља. А у наш пример смо уврстили само једно заглавље. Јединица за превод постаје све већа и већа како стално укључујемо заглавља.

Овај поступак претпроцесирања и компајлирања је сличан за језик Ц. Прати правила Ц за компајлирање, а начин на који укључује датотеке заглавља и производи објектни код је приближно исти.

Како изворне датотеке увозе и извозе симболе

Погледајмо сада датотеке у cpp-article/symbols/c-vs-cpp-names именик.

Како се обрађују функције.

Постоји једноставна изворна датотека Ц (не Ц ++) која се зове сум.ц која извози две функције, једну за додавање две целобројне вредности и једну за додавање два пловка:

int sumI(int a, int b) { return a + b; } float sumF(float a, float b) { return a + b; }

Саставите га (или покрените make и све кораке за креирање две примере апликација које треба извршити) да бисте креирали датотеку објекта сум.о:

$ gcc -c sum.c

Сада погледајте симболе које извози и увози ова датотека објекта:

$ nm sum.o 0000000000000014 T sumF 0000000000000000 T sumI

Ниједан симбол се не увози и два симбола се извозе: sumF и sumI. Ти симболи се извозе као део .тект сегмента (Т), тако да су то имена функција, извршни код.

Ако друге изворне датотеке (и Ц или Ц ++) желе да позову те функције, морају да их пријаве пре позивања.

Стандардни начин за то је стварање заглавне датотеке која их декларише и укључује у било коју изворну датотеку коју желимо да их зовемо. Заглавље може имати било које име и додатак. Изабрао сам sum.h:

#ifdef __cplusplus extern 'C' { #endif int sumI(int a, int b); float sumF(float a, float b); #ifdef __cplusplus } // end extern 'C' #endif

Који су то ifdef / endif блокови условне компилације? Ако укључим ово заглавље из изворне датотеке Ц, желим да постане:

int sumI(int a, int b); float sumF(float a, float b);

Али ако их укључим из изворне датотеке Ц ++, желим да постану:

extern 'C' { int sumI(int a, int b); float sumF(float a, float b); } // end extern 'C'

Ц језик не зна ништа о extern 'C' директива , али Ц ++ то чини и потребна му је ова директива која се примењује на декларације функције Ц. То је зато Ц ++ имена манглес функција (и метода) јер подржава преоптерећење функције / методе, док Ц не.

То се може видети у изворној датотеци Ц ++ под називом принт.цпп:

#include // std::cout, std::endl #include 'sum.h' // sumI, sumF void printSum(int a, int b) { std::cout << a << ' + ' << b << ' = ' << sumI(a, b) << std::endl; } void printSum(float a, float b) { std::cout << a << ' + ' << b << ' = ' << sumF(a, b) << std::endl; } extern 'C' void printSumInt(int a, int b) { printSum(a, b); } extern 'C' void printSumFloat(float a, float b) { printSum(a, b); }

Постоје две функције са истим именом (printSum) које се разликују само по типу својих параметара: int или float. Преоптерећење функције је карактеристика Ц ++ који није присутан у Ц. Да би применио ову функцију и разликовао те функције, Ц ++ мангира назив функције, као што можемо видети у њиховом извоженом имену симбола (одабрат ћу само оно што је релевантно из резултата нм-а):

$ g++ -c print.cpp $ nm print.o 0000000000000132 T printSumFloat 0000000000000113 T printSumInt U sumF U sumI 0000000000000074 T _Z8printSumff 0000000000000000 T _Z8printSumii U _ZSt4cout

Те функције се извозе (у мом систему) као _Z8printSumff за флоат верзију и _Z8printSumii за инт верзију. Свако име функције у Ц ++-у је искривљено ако није декларисано као extern 'C'. Постоје две функције које су декларисане са Ц везом у print.cpp: printSumInt и printSumFloat.

Због тога се не могу преоптеретити, или би њихова извезена имена била иста, јер нису искварена. Морао сам да их разликујем међусобно постављањем Инт или Флоат-а на крај њихових имена.

Како нису кварни, могу се позвати из Ц кода, као што ћемо ускоро видети.

Да бисмо видели искривљена имена као што бисмо их видели у изворном коду Ц ++, можемо користити -C (демангле) опција у nm команда. Поново ћу копирати само исти релевантни део резултата:

$ nm -C print.o 0000000000000132 T printSumFloat 0000000000000113 T printSumInt U sumF U sumI 0000000000000074 T printSum(float, float) 0000000000000000 T printSum(int, int) U std::cout

Са овом опцијом, уместо _Z8printSumff видимо printSum(float, float), а уместо _ZSt4cout видимо стд :: цоут, која су више прилагођена људима.

Такође видимо да наш Ц ++ код позива Ц код: print.cpp позива sumI и sumF, што су Ц функције декларисане као да имају Ц везу у sum.h. То се може видети у нм излазу принт.о горе, који обавештава о неким недефинисаним (У) симболима: sumF, sumI и std::cout. Ти недефинисани симболи би требало да се налазе у једној од објектних датотека (или библиотека) које ће бити повезане заједно са излазом ове датотеке датотеке у фази везе.

До сада смо компајлирали изворни код у објектни код, још увек нисмо повезани. Ако објектну датотеку која садржи дефиниције тих увезених симбола не повежемо заједно са овом објектном датотеком, повезивач ће се зауставити са грешком „недостајући симбол“.

Такође имајте на уму да од print.cpp је Ц ++ изворна датотека, компајлирана са Ц ++ компајлером (г ++), сав код у њој је компајлиран као Ц ++ код. Функције са Ц везом попут printSumInt и printSumFloat су такође Ц ++ функције које могу да користе Ц ++ функције. Са Ц су компатибилна само имена симбола, али код је Ц ++, што се види по томе што обе функције позивају преоптерећену функцију (printSum), што се не би могло догодити ако printSumInt. | или printSumFloat састављени су у Ц.

Погледајмо сада print.hpp, заглавну датотеку која се може укључити из изворних датотека Ц или Ц ++, што ће омогућити printSumInt и printSumFloat да се позивају и из Ц и из Ц ++, и printSum за позивање из Ц ++:

#ifdef __cplusplus void printSum(int a, int b); void printSum(float a, float b); extern 'C' { #endif void printSumInt(int a, int b); void printSumFloat(float a, float b); #ifdef __cplusplus } // end extern 'C' #endif

Ако га укључујемо из изворне датотеке Ц, само желимо да видимо:

void printSumInt(int a, int b); void printSumFloat(float a, float b);

printSum не може се видети из Ц кода, јер је његово име искривљено, тако да немамо (стандардни и преносни) начин да га декларишемо за Ц код. Да, могу их прогласити као:

void _Z8printSumii(int a, int b); void _Z8printSumff(float a, float b);

И повезивач се неће жалити јер је то тачно име које је мој тренутно инсталирани компајлер измислио за њега, али не знам да ли ће то функционисати за ваш повезивач (ако ваш компајлер генерише другачије искривљено име), или чак за следећа верзија мог повезивача. Не знам ни да ли ће позив функционисати онако како се очекивало због постојања различитих позивање на конвенције (како се преносе параметри и враћају враћене вредности) који су специфични за компајлер и могу се разликовати за позиве Ц и Ц ++ (посебно за функције Ц ++ које су функције чланице и примају овај показивач као параметар).

Ваш компајлер потенцијално може користити једну конвенцију позивања за редовне функције Ц ++, а другу ако су проглашене да имају спољну везу „Ц“. Дакле, варање компајлера рекавши да једна функција користи конвенцију позивања Ц, док заправо користи Ц ++ јер може донети неочекиване резултате ако се конвенције које се користе за сваку разликују у вашем ланцу компајлирања.

Постоје стандардни начини мешања Ц и Ц ++ код и стандардни начин позивања Ц ++ преоптерећених функција из Ц је умотајте их у функције са Ц везом као што смо то урадили умотавањем printSum са printSumInt и printSumFloat.

Ако укључимо print.hpp из изворне датотеке Ц ++, __cplusplus макро претпроцесора ће бити дефинисан и датотека ће се видети као:

void printSum(int a, int b); void printSum(float a, float b); extern 'C' { void printSumInt(int a, int b); void printSumFloat(float a, float b); } // end extern 'C'

Ово ће омогућити Ц ++ коду да преоптерећену функцију позове принтСум или њене омоте printSumInt и printSumFloat.

Хајде сада да креирамо Ц изворну датотеку која садржи главну функцију, која је улазна тачка за програм. Ова главна функција Ц ће позвати printSumInt и printSumFloat, односно позваће обе Ц ++ функције са Ц везом. Запамтите, то су Ц ++ функције (њихова тела функција извршавају Ц ++ код) које само немају Ц ++ искривљена имена. Датотека се зове c-main.c:

#include 'print.hpp' int main(int argc, char* argv[]) { printSumInt(1, 2); printSumFloat(1.5f, 2.5f); return 0; }

Саставите га да бисте генерисали објектну датотеку:

$ gcc -c c-main.c

И погледајте увезене / извезене симболе:

$ nm c-main.o 0000000000000000 T main U printSumFloat U printSumInt

Извози главно и увози printSumFloat и printSumInt, како се очекивало.

Да бисмо све то заједно повезали у извршну датотеку, морамо да користимо повезивач Ц ++ (г ++), јер је најмање једна датотека коју ћемо повезати, print.o, компајлирана у Ц ++:

$ g++ -o c-app sum.o print.o c-main.o

Извршење даје очекивани резултат:

$ ./c-app 1 + 2 = 3 1.5 + 2.5 = 4

Покушајмо сада са главном датотеком на Ц ++, која се зове cpp-main.cpp:

#include 'print.hpp' int main(int argc, char* argv[]) { printSum(1, 2); printSum(1.5f, 2.5f); printSumInt(3, 4); printSumFloat(3.5f, 4.5f); return 0; }

Саставите и погледајте увезене / извезене симболе cpp-main.o датотека објекта:

$ g++ -c cpp-main.cpp $ nm -C cpp-main.o 0000000000000000 T main U printSumFloat U printSumInt U printSum(float, float) U printSum(int, int)

Извози главну и увози Ц везу printSumFloat и printSumInt, и обе искривљене верзије printSum.

Можда се питате зашто се главни симбол не извози као изопачени симбол попут main(int, char**) из овог Ц ++ извора, јер је то Ц ++ изворна датотека и није дефинисана као extern 'C'. Па, main је посебна функција дефинисана имплементацијом и чини се да је моја имплементација одлучила да користи везу Ц без обзира да ли је дефинисана у изворној датотеци Ц или Ц ++.

Повезивање и покретање програма даје очекивани резултат:

$ g++ -o cpp-app sum.o print.o cpp-main.o $ ./cpp-app 1 + 2 = 3 1.5 + 2.5 = 4 3 + 4 = 7 3.5 + 4.5 = 8

Како раде заштитници заглавља

До сада сам пазио да два пута, директно или индиректно, не укључим заглавља из исте изворне датотеке. Али пошто једно заглавље може да садржи и друга заглавља, исто заглавље може индиректно да буде укључено више пута. А пошто је садржај заглавља управо уметнут на место одакле је укључен, лако је завршити дуплираним декларацијама.

Погледајте примере датотека у cpp-article/header-guards.

// unguarded.hpp class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; // guarded.hpp: #ifndef __GUARDED_HPP #define __GUARDED_HPP class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; #endif // __GUARDED_HPP

Разлика је у томе што у гуардед.хпп окружујемо цело заглавље условом који ће бити укључен само ако __GUARDED_HPP макро претпроцесора није дефинисан. Први пут када претпроцесор укључи ову датотеку, она неће бити дефинисана. Али, будући да је макро дефиниран унутар тог заштићеног кода, следећи пут када буде укључен (из исте изворне датотеке, директно или индиректно), претпроцесор ће видети линије између #ифндеф и #ендиф и одбацит ће сав код између њих.

Имајте на уму да се овај процес дешава за сваку изворну датотеку коју компајлирамо. То значи да се ова датотека заглавља може укључити једном и само једном за сваку изворну датотеку. Чињеница да је укључена из једне изворне датотеке неће спречити да буде укључена из друге изворне датотеке када је та изворна датотека компајлирана. Само ће спречити да се више пута укључи из исте изворне датотеке.

Пример датотеке main-guarded.cpp укључује guarded.hpp два пута:

#include 'guarded.hpp' #include 'guarded.hpp' int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }

Али унапред обрађени излаз приказује само једну дефиницију класе A:

$ g++ -E main-guarded.cpp # 1 'main-guarded.cpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'main-guarded.cpp' # 1 'guarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 2 'main-guarded.cpp' 2 int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }

Стога се може саставити без проблема:

$ g++ -o guarded main-guarded.cpp

Али main-unguarded.cpp датотека садржи unguarded.hpp два пута:

#include 'unguarded.hpp' #include 'unguarded.hpp' int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }

А унапред обрађени излаз показује две дефиниције класе А:

$ g++ -E main-unguarded.cpp # 1 'main-unguarded.cpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'main-unguarded.cpp' # 1 'unguarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 2 'main-unguarded.cpp' 2 # 1 'unguarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 3 'main-unguarded.cpp' 2 int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }

Ово ће узроковати проблеме при компајлирању:

$ g++ -o unguarded main-unguarded.cpp

У датотеци укљученој из main-unguarded.cpp:2:0:

unguarded.hpp:1:7: error: redefinition of 'class A' class A { ^ In file included from main-unguarded.cpp:1:0: unguarded.hpp:1:7: error: previous definition of 'class A' class A { ^

Ради краткоће, у овом чланку нећу користити заштићена заглавља ако то није потребно, јер је већина кратких примера. Али увек чувајте датотеке заглавља. Не ваше изворне датотеке, које неће бити нигде укључене. Само датотеке заглавља.

Прођите поред вредности и исправности параметара

Погледајте by-value.cpp датотека у cpp-article/symbols/pass-by:

#include #include #include // std::vector, std::accumulate, std::cout, std::endl using namespace std; int sum(int a, const int b) { cout << 'sum(int, const int)' << endl; const int c = a + b; ++a; // Possible, not const // ++b; // Not possible, this would result in a compilation error return c; } float sum(const float a, float b) { cout << 'sum(const float, float)' << endl; return a + b; } int sum(vector v) { cout << 'sum(vector)' << endl; return accumulate(v.begin(), v.end(), 0); } float sum(const vector v) { cout << 'sum(const vector)' << endl; return accumulate(v.begin(), v.end(), 0.0f); }

Пошто користим using namespace std директиву, не морам да квалификујем имена симбола (функције или класе) унутар стд простора имена у остатку јединице за превод, што је у мом случају остатак изворне датотеке. Да је ово датотека заглавља, не бих требало да убацим ову директиву, јер би датотека заглавља требало да буде укључена из више изворних датотека; ова директива би у глобални опсег сваке изворне датотеке довела читав стд простор имена од тачке у коју је укључено моје заглавље.

Чак ће и заглавља која су у тим датотекама укључена након мог имати те симболе. То може довести до сукоба имена јер нису очекивали да ће се то догодити. Стога, немојте користити ову директиву у заглављима. Користите га само у изворним датотекама ако желите и тек након што сте укључили сва заглавља.

Обратите пажњу на то како су неки параметри цонст. То значи да се они не могу мењати у телу функције ако то покушамо. То би дало грешку у компилацији. Такође имајте на уму да се сви параметри у овој изворној датотеци преносе по вредности, а не по референци (&) или показивачу (*). То значи да ће их позивалац направити копију и прећи на функцију. Дакле, за позиваоца није важно да ли су цонст или не, јер ако их изменимо у телу функције, модификоваћемо само копију, а не оригиналну вредност коју је позивалац пренео функцији.

Будући да цонстнесс параметра који се прослеђује према вредности (цопи) није важан за позиваоца, он није искривљен у потпису функције, као што се може видети након компајлирања и прегледа објектног кода (само релевантни излаз):

$ g++ -c by-value.cpp $ nm -C by-value.o 000000000000001e T sum(float, float) 0000000000000000 T sum(int, int) 0000000000000087 T sum(std::vector) 0000000000000048 T sum(std::vector )

Потписи не изражавају да ли су копирани параметри цонст или не у телима функције. Није важно. Било је важно само за дефиницију функције, да читаоцу тела функције на први поглед покаже да ли ће се те вредности икада променити. У примеру је само половина параметара декларисана као цонст, тако да можемо да видимо контраст, али ако желимо цонст-исправно сви би требали бити декларисани, јер ниједан није измењен у телу функције (а не би требало).

Будући да за декларацију функције није важно шта је оно што позивалац види, можемо створити by-value.hpp заглавље попут овог:

#include int sum(int a, int b); float sum(float a, float b); int sum(std::vector v); int sum(std::vector v);

Овде је дозвољено додавање квалификатора цонст (чак можете да се квалификујете као цонст променљиве које нису цонст у дефиницији и то ће функционисати), али то није неопходно и само ће непотребно учинити изјаве непотребним.

Прођите поред референце

Да видимо by-reference.cpp:

#include #include #include using namespace std; int sum(const int& a, int& b) { cout << 'sum(const int&, int&)' << endl; const int c = a + b; ++b; // Will modify caller variable // ++a; // Not allowed, but would also modify caller variable return c; } float sum(float& a, const float& b) { cout << 'sum(float&, const float&)' << endl; return a + b; } int sum(const std::vector& v) { cout << 'sum(const std::vector&)' << endl; return accumulate(v.begin(), v.end(), 0); } float sum(const std::vector& v) { cout << 'sum(const std::vector&)' << endl; return accumulate(v.begin(), v.end(), 0.0f); }

Умереност приликом проласка поред референце важна је за позиваоца, јер ће позиваоцу рећи да ли ће позивани корисник изменити његов аргумент или не. Стога се симболи извозе са својом постојаношћу:

$ g++ -c by-reference.cpp $ nm -C by-reference.o 0000000000000051 T sum(float&, float const&) 0000000000000000 T sum(int const&, int&) 00000000000000fe T sum(std::vector const&) 00000000000000a3 T sum(std::vector const&)

То би такође требало да се одрази у заглављу које ће позивачи користити:

#include int sum(const int&, int&); float sum(float&, const float&); int sum(const std::vector&); float sum(const std::vector&);

Имајте на уму да нисам написао променљиве у декларацијама (у заглављу) као до сада. Ово је такође легално, за овај пример и за претходне. Имена променљивих нису потребна у декларацији, јер позивалац не мора да зна како желите да именујете своју променљиву. Али имена параметара су обично пожељна у декларацијама, тако да корисник на први поглед може знати шта сваки параметар значи и према томе шта да пошаље у позиву.

Изненађујуће, имена променљивих нису потребна ни у дефиницији функције. Они су потребни само ако параметар стварно користите у функцији. Али ако га никада не користите, параметар можете оставити са типом, али без имена. Зашто би функција декларисала параметар који никада не би користила? Понекад су функције (или методе) само део интерфејса, попут интерфејса за повратни позив, који дефинише одређене параметре који се прослеђују посматрачу. Посматрач мора да креира повратни позив са свим параметрима које интерфејс наводи, јер ће их све позивалац послати. Али посматрача можда неће занимати сви они, па уместо да добије упозорење компајлера о „неискоришћеном параметру“, дефиниција функције може само да га остави без имена.

Прођите поред Поинтер

// by-pointer.cpp: #include #include #include using namespace std; int sum(int const * a, int const * const b) { cout << 'sum(int const *, int const * const)' << endl; const int c = *a+ *b; // *a = 4; // Can't change. The value pointed to is const. // *b = 4; // Can't change. The value pointed to is const. a = b; // I can make a point to another const int // b = a; // Can't change where b points because the pointer itself is const. return c; } float sum(float * const a, float * b) { cout << 'sum(int const * const, float const *)' << endl; return *a + *b; } int sum(const std::vector* v) { cout << 'sum(std::vector const *)' begin(), v->end(), 0); v = NULL; // I can make v point to somewhere else return c; } float sum(const std::vector * const v) { cout << 'sum(std::vector const * const)' begin(), v->end(), 0.0f); }

Да бисте прогласили показивач на елемент цонст (инт у примеру), можете декларирати тип као било који од:

int const * const int *

Ако такође желите да сам показивач буде цонст, односно да се показивач не може променити тако да указује на нешто друго, након звезде додајте цонст:

int const * const const int * const

Ако желите да сам показивач буде цонст, али не и елемент који је на њега указан:

int * const

Упоредите потписе функције са детаљном инспекцијом објектне датотеке:

$ g++ -c by-pointer.cpp $ nm -C by-pointer.o 000000000000004a T sum(float*, float*) 0000000000000000 T sum(int const*, int const*) 0000000000000105 T sum(std::vector const*) 000000000000009c T sum(std::vector const*)

Као што видите, nm алат користи прву нотацију (цонст након типа). Такође, имајте на уму да је једина констност која се извози и која је важна за позиваоца да ли ће функција модификовати елемент на који показује показивач или не. Сама чврстоћа показивача није битна за позиваоца, јер се сам показивач увек предаје као копија. Функција може само да направи сопствену копију показивача да би указала на неко друго место, што је небитно за позиваоца.

Дакле, датотека заглавља може се креирати као:

#include int sum(int const* a, int const* b); float sum(float* a, float* b); int sum(std::vector* const); float sum(std::vector* const);

Пролазак поред показивача је попут проласка поред референце. Једна разлика је у томе што се када проследите референцу очекује да се претпоставља да је позивалац проследио важећу референцу елемента, не указујући на НУЛЛ или неку другу неважећу адресу, док показивач може на пример усмерити на НУЛЛ. Показивачи се могу користити уместо референци када додавање НУЛЛ има посебно значење.

Пошто се вредности Ц ++ 11 такође могу проследити са померите семантику . Ова тема неће бити обрађена у овом чланку, али се може проучавати у другим чланцима попут Преношење аргумента у Ц ++ .

Још једна сродна тема која овде неће бити обрађена је како позвати све те функције. Ако су сва та заглавља укључена из изворне датотеке, али нису позвана, компилација и повезивање ће успети. Али ако желите да позовете све функције, биће неких грешака јер ће неки позиви бити двосмислени. Компајлер ће моћи да изабере више од једне верзије збира за одређене аргументе, посебно када бира да ли ће проследити копију или референцу (или цонст референцу). Та анализа је ван делокруга овог чланка.

Састављање са различитим заставама

Погледајмо сада стварну ситуацију повезану са овом темом у којој се могу наћи тешко пронађене грешке.

Идите у директоријум cpp-article/diff-flags и погледајте Counters.hpp:

class Counters { public: Counters() : #ifndef NDEBUG // Enabled in debug builds m_debugAllCounters(0), #endif m_counter1(0), m_counter2(0) { } #ifndef NDEBUG // Enabled in debug build #endif void inc1() { #ifndef NDEBUG // Enabled in debug build ++m_debugAllCounters; #endif ++m_counter1; } void inc2() { #ifndef NDEBUG // Enabled in debug build ++m_debugAllCounters; #endif ++m_counter2; } #ifndef NDEBUG // Enabled in debug build int getDebugAllCounters() { return m_debugAllCounters; } #endif int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: #ifndef NDEBUG // Enabled in debug builds int m_debugAllCounters; #endif int m_counter1; int m_counter2; };

Ова класа има два бројача, који почињу као нула и могу се повећавати или читати. За верзије отклањања грешака, како ћу назвати градње где NDEBUG макро није дефинисан, додајем и трећи бројач, који ће се увећавати сваки пут када се повећа било који од друга два бројача. То ће бити врста помоћника за отклањање грешака за ову класу. Многе класе независних библиотека или чак уграђена заглавља Ц ++ (у зависности од компајлера) користе овакве трикове да би омогућили различите нивое отклањања грешака. Ово омогућава грађевинама за отклањање грешака да открију итераторе који излазе из домета и друге занимљиве ствари о којима би произвођач библиотека могао размишљати. Назваћу релеасе буилдс 'гради где NDEBUG макро је дефинисан. “

За верзије издања, претходно састављено заглавље изгледа (користим grep за уклањање празних редова):

$ g++ -E -DNDEBUG Counters.hpp | grep -v -e '^$' # 1 'Counters.hpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'Counters.hpp' class Counters { public: Counters() : m_counter1(0), m_counter2(0) { } void inc1() { ++m_counter1; } void inc2() { ++m_counter2; } int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: int m_counter1; int m_counter2; };

Иако ће за верзије отклањања грешака изгледати:

$ g++ -E Counters.hpp | grep -v -e '^$' # 1 'Counters.hpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'Counters.hpp' class Counters { public: Counters() : m_debugAllCounters(0), m_counter1(0), m_counter2(0) { } void inc1() { ++m_debugAllCounters; ++m_counter1; } void inc2() { ++m_debugAllCounters; ++m_counter2; } int getDebugAllCounters() { return m_debugAllCounters; } int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: int m_debugAllCounters; int m_counter1; int m_counter2; };

Као што сам раније објаснио, постоји још један бројач у верзијама за отклањање грешака.

Такође сам креирао неке помоћне датотеке.

// increment1.hpp: // Forward declaration so I don't have to include the entire header here class Counters; int increment1(Counters&); // increment1.cpp: #include 'Counters.hpp' void increment1(Counters& c) { c.inc1(); } // increment2.hpp: // Forward declaration so I don't have to include the entire header here class Counters; int increment2(Counters&); // increment2.cpp: #include 'Counters.hpp' void increment2(Counters& c) { c.inc2(); } // main.cpp: #include #include 'Counters.hpp' #include 'increment1.hpp' #include 'increment2.hpp' using namespace std; int main(int argc, char* argv[]) { Counters c; increment1(c); // 3 times increment1(c); increment1(c); increment2(c); // 4 times increment2(c); increment2(c); increment2(c); cout << 'c.get1(): ' << c.get1() << endl; // Should be 3 cout << 'c.get2(): ' << c.get2() << endl; // Should be 4 #ifndef NDEBUG // For debug builds cout << 'c.getDebugAllCounters(): ' << c.getDebugAllCounters() << endl; // Should be 3 + 4 = 7 #endif return 0; }

И а Makefile која може прилагодити заставице компајлера за increment2.cpp само:

all: main.o increment1.o increment2.o g++ -o diff-flags main.o increment1.o increment2.o main.o: main.cpp increment1.hpp increment2.hpp Counters.hpp g++ -c -O2 main.cpp increment1.o: increment1.cpp Counters.hpp g++ -c $(CFLAGS) -O2 increment1.cpp increment2.o: increment2.cpp Counters.hpp g++ -c -O2 increment2.cpp clean: rm -f *.o diff-flags

Дакле, хајде да све то компајлирамо у режиму отклањања грешака, без дефинисања NDEBUG:

$ CFLAGS='' make g++ -c -O2 main.cpp g++ -c -O2 increment1.cpp g++ -c -O2 increment2.cpp g++ -o diff-flags main.o increment1.o increment2.o

Сада покрените:

$ ./diff-flags c.get1(): 3 c.get2(): 4 c.getDebugAllCounters(): 7

Излаз је баш онакав какав се очекивао. Хајде сада да компајлирамо само једну од датотека са NDEBUG дефинисано, који ће бити режим пуштања, и погледајте шта ће се догодити:

$ make clean rm -f *.o diff-flags $ CFLAGS='-DNDEBUG' make g++ -c -O2 main.cpp g++ -c -DNDEBUG -O2 increment1.cpp g++ -c -O2 increment2.cpp g++ -o diff-flags main.o increment1.o increment2.o $ ./diff-flags c.get1(): 0 c.get2(): 4 c.getDebugAllCounters(): 7

Излаз није онакав какав се очекивао. increment1 функција је видела издану класу Цоунтерс, у којој постоје само два поља инт члана. Дакле, повећало је прво поље, мислећи да је то m_counter1, и није повећало ништа друго јер не зна ништа о m_debugAllCounters поље. Кажем да increment1 повећао бројач јер је метода инц1 у Counter је инлине, тако да је било у increment1 функцијско тело, које се из њега не позива. Компајлер је вероватно одлучио да га уврсти јер -O2 коришћена је застава нивоа оптимизације.

Дакле, m_counter1 никада није повећан и m_debugAllCounters је уместо њега повећан грешком у increment1. Због тога видимо 0 за m_counter1 али и даље видимо 7 за m_debugAllCounters.

Радећи у пројекту у којем смо имали тоне изворних датотека, груписаних у многим статичким библиотекама, догодило се да су неке од тих библиотека компајлиране без опција отклањања грешака за std::vector, а друге са тим опцијама.

Вероватно су у неком тренутку све библиотеке користиле исте заставе, али како је време пролазило, нове библиотеке су додаване не узимајући их у обзир (оне нису биле подразумеване, већ су додаване ручно). За компајлирање смо користили ИДЕ, тако да сте, да бисте видели заставице за сваку библиотеку, морали копати по картицама и прозорима, имајући различите (и вишеструке) заставице за различите режиме компајлирања (издање, отклањање грешака, профил ...), па је било још теже да приметимо да заставе нису биле доследне.

То је проузроковало да у ретким приликама када је датотека објекта, састављена са једним скупом заставица, прошла std::vector објектној датотеци компајлираној са различитим скупом заставица, која је радила одређене операције на том вектору, апликација се срушила. Замислите да отклањање грешака није било лако јер се извештавало да се пад догодио у издању издања, а није се догодио у верзији отклањања грешака (бар не у истим ситуацијама као што је пријављено).

Програм за отклањање грешака такође је радио луде ствари јер је отклањао грешке у врло оптимизованом коду. Падови су се дешавали у исправном и тривијалном коду.

Састављач чини много више него што можда мислите

У овом чланку сте сазнали о неким основним језичким конструкцијама Ц ++-а и како компајлер ради са њима, почев од фазе обраде до фазе повезивања. Знање како то функционише може вам помоћи да на цео процес гледате другачије и пружиће вам бољи увид у ове процесе које узимамо здраво за готово у развоју Ц ++.

Од процеса компилације у три корака до руковања именима функција и стварања различитих потписа функције у различитим ситуацијама, компајлер ради пуно посла да понуди снагу Ц ++-а као компајлираног програмског језика.

Надам се да ће вам знање из овог чланка бити корисно у вашим Ц ++ пројектима.

Повезан: Како научити језике Ц и Ц ++: коначна листа

Портфељи најбољих УКС дизајнера - надахњујуће студије случаја и примери

Укс Дизајн

Портфељи најбољих УКС дизајнера - надахњујуће студије случаја и примери
Финансирање почетника за осниваче: Ваша листа за проверу

Финансирање почетника за осниваче: Ваша листа за проверу

Финансијски Процеси

Популар Постс
Како створити бот за анализу расположења е-поште: Водич за НЛП.
Како створити бот за анализу расположења е-поште: Водич за НЛП.
Поуке из инвестиционе стратегије Варрена Буффетта и његове грешке
Поуке из инвестиционе стратегије Варрена Буффетта и његове грешке
Зашто отплата дељења не успева? Неки предложени лекови
Зашто отплата дељења не успева? Неки предложени лекови
Повећајте своју продуктивност помоћу Амазон Веб Сервицес
Повећајте своју продуктивност помоћу Амазон Веб Сервицес
Развој Андроид ТВ-а - Долазе велики екрани, припремите се!
Развој Андроид ТВ-а - Долазе велики екрани, припремите се!
 
Доступност на мрежи: зашто се стандарди В3Ц често игноришу
Доступност на мрежи: зашто се стандарди В3Ц често игноришу
Алати наредбеног ретка за програмере
Алати наредбеног ретка за програмере
Зен девРант-а
Зен девРант-а
Заступства и гаранција: Алат за спајања и преузимања о коме би сваки продавац требао знати
Заступства и гаранција: Алат за спајања и преузимања о коме би сваки продавац требао знати
Значај дизајна усмереног на човека у дизајну производа
Значај дизајна усмереног на човека у дизајну производа
Популар Постс
  • цсс цхеат схеет са примерима
  • како слати нежељену пошту за кредитне картице пдф
  • шта је информациона архитектура?
  • пример аутентификације засноване на пролећном безбедносном токену
  • врсте алата за визуелизацију података
  • висок ниво ривалства типично резултира конкуренцијом цена која повећава постојеће цене.
  • шта је хијерархија у дизајну
Категорије
  • Агиле Талент
  • Финансијски Процеси
  • Дизајн Бренда
  • Трендови
  • © 2022 | Сва Права Задржана

    portaldacalheta.pt