Osoittimien perusteiden hallinta C- ja C++-kielillä

Viimeisin päivitys: 12/02/2025
Kirjoittaja: C SourceTrail
  • Osoittimet edustavat muistiosoitteita ja mahdollistavat suoran hallinnan siitä, missä ja miten tietoja tallennetaan ja käytetään.
  • Viittausten poisto, vakioiden oikeellisuus ja osoittimien aritmetiikka ovat olennaisia ​​osoittimien turvalliselle käytölle taulukoiden, rakenteiden ja dynaamisen muistin kanssa.
  • Osoittimet mahdollistavat argumenttien välittämisen viitteen perusteella, dynaamisten rakenteiden rakentamisen ja monitasoisen epäsuoran toiminnan, kuten osoitin-osoitin-tekniikan, toteuttamisen.
  • Null-tarkistukset, oikea allokointi/vapauttaminen ja kurinalainen alustus ovat ratkaisevan tärkeitä määrittelemättömän käyttäytymisen ja kaatumisten välttämiseksi.

osoittimien perusteet

C- ja C++-kielten osoittimilla on legendaarinen maine: ne ovat tehokkaita, hankalia ja kykeneviä kaatamaan ohjelmasi silmänräpäyksessä, jos olet huolimaton. Kuitenkin, kun todella ymmärrät, mikä osoitin on – vain muistiosoite – suuri osa tästä mysteeristä alkaa hälvetä, ja saat käyttöösi yhden monipuolisimmista työkaluista matalan tason ja järjestelmäohjelmoinnissa.

Tämä artikkeli opastaa sinua askel askeleelta muistiosoitteiden ja yksinkertaisten osoittimien perusajattelusta viitteiden, taulukoiden, luokkien ja dynaamisen muistin kautta aina osoittimesta osoittimeen -käyttötapauksiin ja yleisiin sudenkuoppiin asti. Tavoitteena on saada osoittimien kanssa työskentely tuntumaan luonnolliselta, ei pimeältä magialta, jotta voit päätellä, mitä muistissa todella tapahtuu koodisi suorituksen aikana.

Muuttujien ja muistiosoitteiden ymmärtäminen

muisti ja muuttujat

Ennen osoittimien koskettamista tarvitset selkeän mielikuvan siitä, miten muuttujat elävät muistissa. Tietokoneen RAM-muisti on käsitteellisesti pitkä tavujen joukko, jossa jokaisella tavulla on yksilöllinen numeerinen osoite. Kun määrittelet muuttujan, kääntäjä varaa yhden tai useamman näistä tavuista ja yhdistää kyseisen osoitteen muuttujan nimeen.

Ajattele muuttujaa muistissa olevana nimettynä laatikkona: nimi on otsikko, osoite on fyysinen sijainti hyllyllä ja sisältö on laatikon sisään tallennettu arvo. Esimerkiksi, jos sinulla on int Tyypillisessä Arduino UNO:ssa se vie kaksi peräkkäistä tavua RAM-muistissa, ja kääntäjä tallentaa, mitkä tarkat osoitteet sille on varattu.

Muuttujan deklarointi kertoo kääntäjälle, minkä tyypin ja koon sen on varattava, kun taas määritelmä tai sijoitus itse asiassa tallentaa arvon kyseiseen varattuun paikkaan. Esimerkiksi kirjoittaminen int j; ainoastaan ​​ilmoittaa muuttujan ja antaa kääntäjän varata muistia, kun taas j = 10; kirjoittaa numeerisen arvon 10 muistisoluihin, jotka kuuluvat j.

Kääntäjä ylläpitää sisäisesti symbolitaulukkoa, johon se yhdistää jokaisen muuttujan nimen sen muistiosoitteeseen ja tyyppiin. Jos kääntäjä päättää, että j asuu osoitteessa 2020, voit ajatella tilannetta käsitteellisesti näin: tunniste j osoittaa osoitteeseen 2020, ja osoitteessa 2020 olevat tavut sisältävät luvun 10 binääriesityksen.

On erittäin tärkeää erottaa toisistaan ​​käsite "missä jokin on tallennettuna" (sen osoite) ja "mitä siellä on tallennettuna" (sen arvo). Kääntäjäteoriassa ja monissa kirjoissa sijaintia kutsutaan usein ns. arvo (sanasta ”sijaintiarvo”), kun taas sisältöön viitataan nimellä rarvoOsoittimilla pyritään manipuloimaan näitä sijainteja suoraan.

Mikä tarkalleen ottaen on osoitin?

osoittimen perusteet

Osoitin on yksinkertaisesti muuttuja, jonka arvo on muistiosoite, joka osoittaa johonkin toiseen olioon. Se ei tallenna itse dataa, vaan datan sijaintiosoitteen. Osoittimen koko riippuu koneen arkkitehtuurista: 32-bittisissä x86-järjestelmissä se on tyypillisesti 4 tavua, 64-bittisissä x86-64-järjestelmissä se on yleensä 8 tavua ja pienissä mikrokontrollereissa, kuten Arduinossa, osoite voi mahtua kahteen tavuun.

Kun määrittelet osoittimen, määrität paitsi sen, että se tallentaa osoitteen, myös sen objektin tyypin, johon se osoittaa. Esimerkiksi int* p määrittelee osoittimen intTähti on tässä osa tyyppiä, ei kertomerkki, ja se kertoo kääntäjälle, kuinka monta tavua luetaan tai kirjoitetaan, kun käytät sitä myöhemmin. *p.

Operaattorin osoite & antaa sinulle olemassa olevan objektin osoitteen, jonka voit tallentaa osoitinmuuttujaan. Oletetaan, että sinulla on int n = 0;; sitten tämä koodi tallentaa osoitteen n osoittimeen:

Esimerkiksi: int n = 0;
int* p = &n; // p now holds the address of n

Kun osoittimella on kelvollinen osoite, viittauksen poisto-operaattori * antaa sinulle pääsyn kyseisessä osoitteessa sijaitsevaan objektiin. If p on osoitin int, sitten *p käyttäytyy kuin muistiin tallennetun varsinaisen kokonaisluvun alias. Esimerkiksi:

Katkelma: *p = 1; // writes 1 into n through the pointer
std::cout << *p; // reads the current value of n

Keskeinen ajatus on, että tähti tarkoittaa eri asioita eri yhteyksissä: deklaroinnissa käytettynä se muodostaa osoittimen tyypin ja lausekkeessa käytettynä se deviittaa osoittimeen. Näiden kahden roolin sekoittaminen on yksi klassisista aloittelijan virheistä, joten kiinnitä aina huomiota siihen, määritätkö osoittimen vai käytätkö sitä muistin käyttämiseen.

Pienissä laitteissa, kuten Arduinossa, osoittimella, jota ei ole eksplisiittisesti alustettu, on joko kelvollinen 16-bittinen osoite tai se sisältää roskaa. Maagista "tyhjää" arvoa ei ole, ellet tarkoituksella aseta sitä null-osoitinvakioksi, kuten nullptr C++:ssa. Tällaisen roskaosoitteen poistaminen viitteestä on lähes varma tapa lukita mikrokontrolleri.

Vakioiden oikeellisuus ja erilaiset osoittimet

Osoittimet ovat vuorovaikutuksessa const tavoilla, jotka voivat aluksi olla hämmentäviä, mutta tämän hallitseminen on ratkaisevan tärkeää oikean C++:n kirjoittamisen kannalta. Aseman sijainti const suhteessa tähteen ratkaisee, onko osoitettu objekti, osoitin itse vai molemmat muuttumattomia.

Jos kokonaisluku on vakio, osoittimen tyypin on oltava sellainen, että sitä voi vain lukea, ei muokata. Kuvittele tämä koodi:

Demo: auto const cn = int{0}; // cn is a constant int
int const* p = &cn; // pointer to const int

Tyyppi p tässä on ”osoitin vakioon kokonaislukuun”: voit lukea *p mutta ei voi sille määrätä. Yritetään int* p = &cn; olisi tyyppivirhe, koska se lupaisi, että voit muokata vakio-objektia, mikä on kielletty kielossa.

Joskus itse objekti ei ole vakio, mutta haluat tarkoituksella osoittimen, joka sallii vain lukuoikeuden sen läpi. Siinä tapauksessa käytät taas int const*:

Käyttö: auto n = int{0}; // non-const int
int const* p = &n; // can read n via p, but not write through p

Huomaa, että int const* ja const int* tarkoittavat täsmälleen samaa: kokonaisluku on osoittimen kautta vain luettavissa, mutta osoitinta voidaan silti muuttaa osoittamaan jonnekin muualle. Toisaalta, jos kirjoitat int* const p = &n;, sinulla on vakio-osoitin ei-vakiokokoiseen kokonaislukuun: osoite, joka on tallennettu p ei voida muuttaa alustuksen jälkeen, mutta arvoa *p on vapaasti vaihdella.

Voit jopa yhdistää molemmat muodot luodaksesi vakio-osoittimen vakiokokonaislukuun: int const* const p. Se kertoo kääntäjälle, ettei kumpikaan osoite kohdassa p eikä kyseiseen osoitteeseen tallennettua arvoa sallita muuttua. Näiden vaihteluiden ymmärtäminen auttaa sinua ilmaisemaan tarkoituksesi erittäin selkeästi, ja kääntäjä pitää sinut rehellisenä.

Viitteitä rakenteisiin ja luokkiin

Kun osoitin viittaa rakenteeseen tai luokkaan, yleensä halutaan käyttää sen julkista rajapintaa: datajäseniä ja jäsenfunktioita. Viittausten poistaminen * toimii edelleen, mutta syntaksi voi muuttua hieman monimutkaiseksi, joten C++ tarjoaa nuolioperaattorin -> lyhenteenä.

Harkitse yksinkertaista Student arvosanat sisältävä rakenne ja keskiarvon laskeva menetelmä. If Student* p pitää hallussaan osoitetta Student objekti, voit kirjoittaa (*p).grade_2 päästäkseen toiselle luokalle tai (*p).average() kutsuaksesi jäsenfunktiota.

Nuolioperaattori yhdistää viittauksen poiston ja jäsenten käytön yhdessä vaiheessa: p->grade_2 ja p->average() tarkoittaa täsmälleen samaa kuin (*p).grade_2 ja (*p).average(). Konepellin alle, p->member on yksinkertaisesti syntaktinen sokeri (*p).memberSiksi näet lähes aina -> käytetään reaalimaailman koodissa käsiteltäessä osoittimia objekteihin.

Kunhan luokka ei ole ylikuormitettu operator* or operator-> jollain eksoottisella käytöksellä voit kohdella p->member standarditapana käyttää osoittimen takana olevaa objektia. Monet kehykset luottavat näiden operaattoreiden ylikuormitukseen älykkäiden osoittimien osalta, mutta käsitteellisesti ne säilyttävät saman merkityksen: seuraa osoitinta ja käytä sitten jäsentä.

Null-osoittimet ja turvallisuus

Osoitinta, joka ei tällä hetkellä viittaa kelvolliseen objektiin, sanotaan nulliksi, ja nykyaikaisessa C++:ssa kaanoninen tapa ilmaista tämä on nullptr. kirjoittaminen int* p = nullptr; toteaa nimenomaisesti, että p ei vielä osoita mihinkään merkitykselliseen.

Null-osoittimen viittauksen poistaminen on määrittelemätöntä toimintaa, joka tyypillisesti johtaa kaatumisiin, käyttöoikeusrikkomuksiin tai pienillä levyillä järjestelmän jumiutumiseen. Siksi koodi, joka vastaanottaa osoittimen parametrina, tarkistaa usein, onko se null (null), ennen kuin sitä käyttää. Jos logiikkasi sallii "ei objektia" merkityksellisenä tilana, osoittimen parametri on sopiva, koska se voi siirtää kyseisen "poissaolevan" tiedon nullptr.

Idiomaattinen esimerkki on funktio, joka muuntaa C-tyylisen merkkijonon (char const*) Ja std::string mutta sen on käsiteltävä sujuvasti tapaus, jossa syöteosoitin on null. Funktio tarkistaa, onko osoitin ei-null, ennen kuin se muodostaa std::stringJos se on null, se palauttaa tyhjän merkkijonon virheellisen osoitteen deviittauksen sijaan.

Jos parametri on pakollinen eikä voi olla pois, C++-viittaukset ovat yleensä parempi vaihtoehto kuin raa'at osoittimet. Viittausta ei voi asettaa uudelleen, eikä sen ole tarkoitus olla null, joten tyyppijärjestelmä ilmaisee selvästi odotuksen, että kutsujan on annettava kelvollinen objekti. Tämä tekee API:sta turvallisemman ja koodista helpommin ymmärrettävän.

Osoittimet funktion parametreina: arvon mukaan vs. viitteen mukaan

Oletusarvoisesti, kun välität muuttujan funktiolle C- tai C++-kielellä, se välitetään arvon mukaan: funktio saa kopion argumentin arvosta, ei alkuperäistä muuttujaa. Tämä tarkoittaa, että kaikki funktion sisällä olevalle parametrille tehdyt sijoitukset vaikuttavat vain paikalliseen kopioon ja jättävät kutsujan muuttujan ennalleen.

Tämä toiminta on usein toivottavaa – se eristää funktiot ja välttää yllättäviä sivuvaikutuksia – mutta joskus todella haluat funktion muokkaavan kutsujan muuttujia. Saatat ajatella globaalien muuttujien käyttöä, mutta ohjelmien kasvaessa globaaleista muuttujista tulee nopeasti vaikeasti seurattavia ja virhealttiita.

Osoittimet tarjoavat siistin vaihtoehdon: annat muuttujan osoitteen funktiolle, ja funktio voi sitten muuttaa muuttujan arvoa kyseisessä osoitteessa. Tätä kutsutaan "viittauksen ohittamiseksi osoittimien avulla". C++:ssa voit käyttää myös viittausparametreja (int&), jotka ovat usein vielä selkeämpiä, mutta osoittimen muodon ymmärtäminen on silti olennaista.

Kuvittele funktio double_value , jonka pitäisi kaksinkertaistaa kutsujassa määritelty kokonaisluku. Osoitinpohjaista rajapintaa käytettäessä sen tulisi olla int*, ja kutsu sitä välittämällä muuttujasi osoitteen: double_value(&k);Funktion sisällä *k = *k * 2; päivittää alkuperäisen arvon osoittimen kautta.

Tämä tekniikka mahdollistaa myös funktion tehokkaasti "palauttaa" useita tuloksia muokkaamalla useita muuttujia, joiden osoitteet annettiin argumentteina. Monimutkaisen rakenteen palauttamisen sijaan voit hyväksyä useita osoitinparametreja ja päivittää ne kaikki. Nykyaikaisessa C++:ssa suositaan tyypillisesti viittauksia, tupleja tai rakenteita selkeyden vuoksi, mutta osoitinparametrit ovat edelleen yleisiä matalan tason API-rajapinnoissa ja C-kirjastoissa.

Osoitinaritmetiikka ja taulukot

Yksi osoittimien tehokkaimmista – ja vaarallisimmista – ominaisuuksista on osoittimen aritmetiikka, erityisesti taulukoiden yhteydessä. C- ja C++-kielissä taulukko tallennetaan muistiin vierekkäisten elementtien lohkona, ja taulukon nimi voi funktiolle välitettäessä tai tietyissä lausekkeissa käytettäessä muuttua osoittimeksi sen ensimmäiseen elementtiin.

Jos julistat char h[] = {'P','r','o','m','e','t','e','c','\n'};, sitten h voidaan käsitellä osoittimena h[0]. pääsy h[i] on käsitteellisesti sama kuin laskenta *(h + i), Jossa h on perusosoite ja i on siirtymä elementteinä (ei tavuina). Kääntäjä kertoo i kunkin elementin koon mukaan (1 tavu char, 4 tavua int, jne.) ennen sen lisäämistä osoittimeen.

Tämä tarkoittaa, että kun näet lausekkeen, kuten *(h + i), teet klassista osoittimen aritmetiikkaa: siirrät osoitinta eteenpäin h by i positiot ja sitten poista tuloksen viittaus. Suorituskyvyn vuoksi kääntäjät ovat erittäin hyviä optimoimaan tätä mallia, minkä vuoksi C-taulukot ja -osoittimet ovat historiallisesti olleet niin suosittu yhdistelmä matalan tason työssä.

Voit myös luoda eksplisiittisen osoittimen taulukon ensimmäiseen elementtiin ja kasvattaa osoitinta, jotta se kulkee taulukon läpi. Esimerkiksi julistamalla char* ptr = h; ja sitten toistuvasti tulostaa *ptr++ silmukassa käy läpi jokaisen merkin peräkkäin. Jälkiliite ++ siirtää osoitinta jokaisen käytön jälkeen seuraavaan taulukon alkioon.

Tämä kompakti tyyli on idiomaattinen C, mutta se voi olla kryptinen aloittelijoille, joten modernissa C++:ssa monet kehittäjät suosivat eksplisiittisempiä muotoja, kuten for indeksejä käyttävät silmukat tai aluepohjaiset for-silmukat. Osoitinaritmetiikan ymmärtäminen on kuitenkin välttämätöntä vanhan koodin lukemiseksi ja ylläpitämiseksi sekä suorituskykykriittisten rutiinien toteuttamiseksi.

Dynaaminen muisti, uusi/poista ja osoittimen iterointi

Osoittimet ovat myös peruskädensija, jonka saat, kun allokoit objekteja dynaamisesti vapaassa varastossa (usein epävirallisesti kutsutaan keoksi). C++:ssa operaattori new palauttaa osoittimen juuri allokoituun objektiin ja delete vapauttaa muistin, kun sitä ei enää tarvita.

Esimerkiksi Student* p = new Student{...}; varaa riittävästi muistia yhdelle Student objekti ja palauttaa sen osoitteen. Sitten käytät p->member käyttääkseen sen jäseniä tai kutsuakseen sen metodeja. Kun objektia ei enää tarvita, delete p; tuhoaa sen ja vapauttaa muistin takaisin vapaaseen tallennustilaan.

C++ mahdollistaa myös taulukoiden dynaamisen allokoinnin käyttämällä new[], joka palauttaa osoittimen taulukon ensimmäiseen alkioon. Esimerkiksi Student* p = new Student[100]; varaa tilaa 100:lle Student muistissa vierekkäin asetetut objektit, p osoittaa indeksillä 0 olevaan elementtiin.

Osoitinaritmetiikkaa käyttäen lauseke p + i viittaa siihen ikyseisen taulukon -s elementti, joten (p + 4)->grade_1 vastaa p[4].grade_1. käsitteellisesti p on kuin iteraattori, joka aloittaa ensimmäisestä elementistä, ja p + i vie iteraattoria eteenpäin i askeleet rivistöä pitkin.

Myös osoittimien välisillä eroilla on merkitystä: jos q = p + 4;, sitten q - p evaluoituu arvoksi 4, joka on näiden kahden osoittimen välisten elementtien lukumäärä. Tässä mielessä raakaosoitin on yksinkertaisin muoto hajasaanti-iteraattorista. Monet STL-kontit paljastavat samalla tavalla käyttäytyviä iteraattoreita, mutta piilottavat raakaosoittimen tiedot turvallisuuden ja joustavuuden vuoksi.

Vaikka raakana new/delete ovat tehokkaita, mutta moderni C++ suosittelee vahvasti älykkäiden osoittimien ja RAII:n (Resource Acquisition Is Initialization) käyttöä resurssien automaattiseen hallintaan. Älykkäät osoittimet, kuten std::unique_ptr ja std::shared_ptr kapseloi omistajuuden ja vapauttaa muistia automaattisesti, kun sitä ei enää tarvita, mikä vähentää vuotojen ja kaksoispoistojen riskiä.

Osoittimia osoittajiin ja syvempää epäsuoraa tulkintaa

Kun olet tottunut yksinkertaisiin osoittimiin, törmäät väistämättä osoittimiin osoittimissa (ja joskus jopa korkeammilla epäsuoraisuuden tasoilla). Käsitteellisesti osoitin osoittimeksi on vain yksi muuttuja, joka sisältää osoitinmuuttujan osoitteen sen sijaan, että viittaisi suoraan kokonaislukuun (int), tuplaan (double) tai olioon.

Ilmeisin käyttötapaus on dynaamisesti allokoitujen osoitintaulukoiden hallinta, kuten dynaamisesti rakennettu taulukko dynaamisesti allokoiduista merkkijonoista. Yksinkertaisessa C-kielessä klassinen esimerkki on char** argv vuonna main funktio, joka on osoitin C-tyylisten merkkijonojen taulukkoon, joista jokainen on itsessään char*.

Toinen yleinen tilanne on, jossa funktion on muokattava kutsujan tarjoamaa osoitinta, ei pelkästään dataa, johon se osoittaa. Osoittimen välittäminen osoittimelle antaa funktiolle mahdollisuuden vaihtaa alkuperäisen osoittimen viittaamaa objektia tai alustaa sen allokoimalla uuden objektin new or mallocKutsuva koodi näkee sitten päivitetyn osoittimen arvon.

Useita epäsuoraisuuden tasoja esiintyy myös luonnostaan ​​tietyissä tietorakenteissa, erityisesti keolle luoduissa linkitetyissä tietorakenteissa. Esimerkiksi dynaamisesti rakennettu linkitetty lista solmuista, joille on varattu malloc or new voi sisältää osoittimia solmuihin sekä funktioita, jotka vastaanottavat osoittimia näihin osoittimiin elementtien lisäämiseksi tai poistamiseksi samalla, kun päivitetään pääosoitinta.

Ja tietenkin moniulotteiset dynaamiset taulukot esitetään tyypillisesti osoittimina osoittimiin C-tyylisissä rajapinnoissa: "matriisi" mallinnetaan yleensä muodossa int**, jossa jokainen ensimmäisen ulottuvuuden alkio on osoitin rivitaulukkoon. Modernissa C++:ssa saatat pitää parempana std::vector<std::vector<T>> tai mukautettuja matriisiluokkia, mutta osoitin-osoitin-asettelut ovat edelleen perustavanlaatuisia matalan tason API-rajapinnoissa ja sidonnaisuuksissa.

Yleisiä sudenkuoppia ja hyviä käytäntöjä vihjeiden avulla

Suoraan osoittimien kanssa työskentely antaa sinulle tarkkaa hallintaa, mutta se avaa myös oven hienovaraisille ja vaikeasti korjattaville virheille, jos et ole kurinalainen. Monet C- ja C++-koodikantojen legendaariset bugit johtuvat raakaosoitteiden väärinkäytöstä, joko kirjoittamalla muistiin, joka ei kuulu sinulle, tai unohtamalla hallita objektien elinaikoja.

Yksi klassinen virhe on kirjoittaa enemmän tavuja kuin kohdetyyppiin mahtuu, tai muistipaikan tyypin väärintulkinta. Jos esimerkiksi tallennat long arvo int muuttuja tai kirjoita long paikkaan, joka on kooltaan sopiva vain int, päädyt ylikirjoittamaan viereisen muistin, mikä voi vioittaa muita muuttujia tai jopa koodiosoittimia.

Toinen vaara on mielivaltaisten numeeristen arvojen antaminen suoraan osoitinmuuttujille, kuten ptrNum = 7;, ellet tee erittäin matalan tason järjestelmätyötä ja tiedä tarkalleen, mitä kyseisessä osoitteessa sijaitsee. Tavallisessa sovelluskoodissa kokonaisluvun käsitteleminen osoitteena on suora tie määrittelemättömään toimintaan ja epäsäännöllisiin kaatumisiin.

Osoittimien asianmukaisen alustamisen unohtaminen on myös riskialtista: osoitin, jolla on määrittelemätön arvo, saattaa näyttää hyvältä, mutta se voi osoittaa mihin tahansa muistissa. Alusta aina osoittimet – joko kelvollisella osoitteella tai nullptr – ja tarkista ne ennen viittausten poistamista, jos on epäilystäkään siitä, että ne saattavat olla null.

Lopuksi, dynaamisen muistin ansiosta jokainen raaka new tulisi vastata täsmälleen yhtä vastaavaa delete, ja jokainen new[] täsmälleen yhdellä delete[]. Vuotoja tapahtuu, kun dynaamisesti allokoidun muistin seuranta katoaa poistamatta sitä, ja kaksoispoistot (tai sellaisen muistin poistaminen, jota et omista) vahingoittavat allokaattorin sisäisiä rakenteita, mikä yleensä ilmenee ajoittaisina ja erittäin vaikeasti toistettavina virheinä.

Huolellisesti käsiteltyinä osoittimet ovat enemmänkin terävä ja tasapainoinen työkalu kuin satunnainen kaaoksen lähde: niiden avulla voit pohtia ohjelmasi muistin asettelua, suunnitella tehokkaita tietorakenteita ja rakentaa tehokkaita abstraktioita matalan tason ohjauksen päälle. Harjoitellessasi osoitteiden välillä liikkuminen, viittausten poistaminen ja funktioiden ja taulukoiden ja osoittimien vuorovaikutuksen ymmärtäminen tulevat luonnollisiksi, ja alkuperäinen pelko antaa tilaa terveelle kunnioitukselle ja paljon käytännön hyötyä tarjoaville ominaisuuksille.

Related viestiä: