A mutató egy olyan változó vagy konstans, amelynek értéke egy másik változó vagy konstans címe. A mutatók a C nyelv erőteljes, ám potenciálisan veszélyes eszközei. Kihívást jelenthet megérteni a mutatókat.
int i = 5;
Amikor értéket rendelünk egy változóhoz, utána két kérdést tehetünk fel.
Ki vagy?
Én i vagyok.
Mid van?
Egy 5 egész számom van.
Van egy harmadik kérdés is, amit feltehetünk.
Hol vagy?
Az 0x6dfed4 címen.
Várj, mi?
A változók és konstansok az értéküket egy memóriabeli helyen tárolják. Ezt a memóriabeli helyet a változó vagy konstans címének nevezzük.
Ahhoz, hogy megkapjuk egy változó címét, a cím operátort használjuk, ami a &.
int i = 5;
int *i_address = &i;
Várj, mit jelent az a * az i_address előtt? Nem a szorzást jelenti?
Egy típus, utána egy *, utána egy azonosító: így deklarálunk egy mutatót az adott típusú változóra.
A * neve indirekciós operátor, mert közvetetten arra az értékre utal, amelynek a címét a mutató tárolja.
Jó, de miért hozzunk létre egy változót csak azért, hogy egy másik változó címét tárolja?
Egyáltalán miért is érdekes, hogy mi egy változó címe?
Nos, az az érdekes, hogy a változók hol vannak, amikor egy tömbben vannak.
Amikor létrehozunk egy tömböt, mint például
int my_ints[] = {1, 2};
akkor a my_ints értéke valójában a címe, ami egyben épp megegyezik az első elemének a címével.
A második elemének a címe az első elem címe plusz egy int mérete az adott rendszeren.
Hogy ezt jobban lásd, próbáld ki a következőt
#include <stdio.h>
int main() {
// my_int's value is its address
int my_ints[] = {1, 2};
// since my_ints is already an address
// we don't need the address operator to make a pointer from it.
// a pointer to my_ints holds the address of my_ints
int *my_ptr = my_ints;
// the address of my_ints and the value of my_ptr are the same
printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
// the address of the first element is the same as the values in the line above
printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
// the address of the second element has increased by the size of an int
printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); //
return 0;
}
Az előző példa megmutatja, miért 0-tól kezdődik a tömbindexelés 1 helyett.
A tömb indexe megszorozza az indexet a típus méretével, és hozzáadja a tömb kezdőcíméhez.
Mivel az első elem egy vonalban van a tömb elejével, a típust 0-val szorozzuk, hogy a kezdőcímhez adva megkapjuk az első elemet.
A második elemhez a típust 1-gyel szorozzuk, hogy a kezdőcímhez adjuk.
Ha a típus négy byte hosszú, a második elem címe négy byte-tal lesz magasabb, mint az első elem.
Ez az oka annak is, hogy a mutatókat egy bizonyos típusúként deklaráljuk.
Ha egy mutató nem tudná, milyen típusra mutat, nem tudná, hány byte-tal lépjen tovább, amikor 1-et adunk hozzá.
Egy mutatóhoz 1-et adva nem feltétlenül egy byte-ot adunk a címhez.
Azt jelenti, hogy a típusa hosszának 1-szeresét adjuk hozzá byte-ban.
De várj, van még más! Nem gondoltad, hogy végeztünk a *-gal, ugye?
Most, hogy megértettük, hogyan működnek a címek a tömbindexeléssel, mi haszna, ha nem kapjuk meg a címen tárolt értéket?
Hogyan tesszük ezt?
Ahhoz, hogy megkapjuk a címen tárolt értéket, az indirekciós operátort használjuk, ami, mint emlékezhetsz, a *.
Ebben a helyzetben az indirekciós operátort néha dereferáló operátornak is nevezik, mert dereferálja azt az értéket, amelynek a címét a mutató tárolja.
Ha dereferálásra használjuk, a dereferáló operátor elnevezés megkülönbözteti a mutató definiálására szolgáló indirekciós operátortól, de technikailag a * mindkét használatban az indirekciós operátor nevet viseli.
Ugyan már!
Csak viccelsz?
Hány használata van a *-nak?!
Szorzás, mutató deklarálása, és most a dereferálás!
Ez zavaró!
És egyáltalán mi is pontosan a dereferálás?
Egy példa megmutatja, hogyan használjuk az indirekciós operátort a dereferáláshoz
#include <stdio.h>
int main() {
// define the variable
int my_ints[] = {1, 2};
// define the pointer from the address of the variable
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
// prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
return 0;
}
A zárójelek használata segíthet eloszlatni a szándék kétértelműségét mutatóaritmetika használatakor. A következő példa lehet, hogy nem azt írja ki, amit a programozó szándékozott
#include <stdio.h>
int main() {
int my_ints[] = {1, 42};
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *++my_ptr is 42.
printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
// prints: my_ints[1] is 43. ++*my_ptr++ is 43.
printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);
return 0;
}
A * és a ++ azonos precedenciával rendelkezik, de jobbról balra értékelődnek ki.
Az első sorban a my_ptr először a második elemre növekszik, amit aztán dereferálunk.
A második sorban a my_ptr-t, ami az első sorban a második elemre növekedett, először dereferáljuk, majd a dereferált értéket növeljük.
Ha hozzáadjuk ezt a sort a fenti példához
// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);
Ebben a sorban a mutatót a tömbön túlra növeltük.
Túlfutottunk a tömbön, és a memória következő négy byte-jára mutatunk, amit a mutató int-ként értelmez, de valójában ezek a byte-ok egy másik változóhoz vagy változókhoz tartozhatnak.
Ez a négy byte lehet egy double része, vagy lehet négy char.
A probléma az, hogy a mutatónkkal rájuk sem szabadna néznünk.
Használhattuk volna ezt is
// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);
Mivel a tömbindexelés mutatóval van megvalósítva, ugyanaz a túlfutás veszélye áll fenn.
És mivel egy string char-ok tömbje, a mutatók stringekkel is használhatók.
Egy stringet gyakran mutatószintaxissal deklarálunk, így
char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";
Egy kérdés azonban. Miért tesszük a csillagot az azonosító mellé? Miért nem tesszük a típus mellé, hiszen egy arra a típusra mutató mutató? Mert ez
int* my_ptr1, my_ptr2_that_is_really_an_int;
nem két int-re mutató mutatót deklarál, hanem egy int-re mutató mutatót és egy int-et.
Két int-re mutató mutató deklarálása így nézne ki
int *my_ptr1, *my_ptr2;
Tehát jó, ha a *-ot az azonosító közelébe tesszük, hogy minimalizáljuk a kétértelműséget.
Vagy soronként csak egy azonosítót deklaráljunk.
És ha még nem volt elég a *-ból, akkor üdvözlünk itt:
Gyakori, hogy a stringekből álló tömböket így ábrázolják
#include <stdio.h>
void print_strings(char **passed_in_strings) {
printf("%s\n", *passed_in_strings);
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char *my_strings[] = {"One string", "Two string"};
print_strings(my_strings);
return 0;
}
Lehet, hogy nem nyilvánvaló, de a stringekből álló tömb úgy van deklarálva és inicializálva, mint mutató char-tömbök tömbjére.
Amikor a char-tömbök tömbjét átadjuk a print_strings() függvénynek, a szignatúra char-ra mutató mutatókra mutató mutatóként írja le.
Szóval honnan tudjuk, hogy a char ** valójában mutató char-tömbökre mutató mutatók tömbjére, és nem mutató egyetlen char mutatójára?
Nem tudjuk.
Ez az egyik sok dolog, ami zavaróvá teheti a mutatók használatát. A következő talán lefordul
#include <stdio.h>
void print_strings(char **passed_in_strings) {
// prints: A (followed by gibberish)
printf("%s\n", *passed_in_strings);
// prints gibberish
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char my_char = 'A';
char *my_ptr = &my_char;
char **my_ptr_to_ptr = &my_ptr;
print_strings(my_ptr_to_ptr);
return 0;
}
De a print_strings() függvényt nem a szándéknak megfelelően használjuk.
Egy más szignatúra használata sem segít. Ha a szignatúrát erre változtatjuk
void print_strings(char *passed_in_strings[]) {
// code snipped
}
talán még mindig lefordul, és még mindig értelmetlenséget ír ki.
De valójában semmi értelme átadni egy mutatót egyetlen char-ra mutató mutatóra.
Valószínűleg, amikor char **-ot látsz, az char-tömbökre mutató mutatók tömbjét jelöli.
Láttuk, hogyan adunk át mutatót egy függvénynek. De miért adjunk át mutatót egy függvénynek?
C-ben az argumentumok érték szerint adódnak át a függvényeknek. Ez azt jelenti, hogy az érték az argumentumból másolódik, és ez a másolat a függvényen belül lokális. Amikor a függvény véget ér, a másolt érték is.
Nézd meg, mi történik, amikor megpróbálunk értékeket megváltoztatni egy függvényen belül
#include <stdio.h>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
//prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", a, b);
}
int main() {
int a = 3, b = 4;
swap(a, b);
// prints: After 'swap', a is 3 and b is 4
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
Ez azért van, mert bár a változóknak ugyanaz a nevük, a swap()-ben lévő a és b nem ugyanaz, mint a main()-ben lévő a és b.
Ezek az a és b értékeinek másolatai.
Meg tudjuk változtatni a swap-et, hogy a szándékunk szerint működjön?
Igen, meg tudjuk!
Átadhatjuk az a és b címét mutatókként.
De mi haszna, ha a függvény lemásolja a mutatókat?
Az a jó, hogy egy másolt cím ugyanaz a cím marad.
A cím dereferálásával hozzáférhetünk az eredeti értékhez, ahogy az a main()-ben volt definiálva.
#include <stdio.h>
// pass in pointers to a and b
void swap(int *a, int *b) {
// set temp to value at a's dereferenced address
int temp = *a;
// set a's value to value at b's dereferenced address
*a = *b;
// set b's value to temp
*b = temp;
// prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}
int main() {
int a = 3, b = 4;
swap(&a, &b);
// prints: After 'swap', a is 4 and b is 3.
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
Ez most sok * a swap-ben, de most már a szándék szerint működik.
Az indirekciós operátort úgy lehet olvasni, hogy „érték itt”.
Tehát a *a az a-nál lévő érték.
Mennyire működik ez jól stringekkel?
char-raTudjuk, hogy egy tömbtípusú változó értéke az a cím, ahol a tömb kezdődik.
És tudjuk, hogy egy stringet definiálhatunk char-tömbként, char my_string[], vagy char-ra mutató mutatóként, char *.
Miben különböznek?
Ebben a kódban egy char * argumentumot adunk át a describe_me() függvénynek.
Az adjective argumentum char-tömbként van definiálva, és a függvény char-ra mutató mutatóként fogadja el.
#include <stdio.h>
#include <string.h>
// takes pointer to char
void describe_me(char *adjective){
strcpy(adjective, "evil");
// prints: I am evil.
printf("I am %s.\n", adjective);
}
int main() {
// defined as a char array
char adjective[] = "good";
describe_me(adjective);
// prints: I am really evil.
printf("I am really %s.\n", adjective);
return 0;
}
Tartósan meg tudjuk változtatni az adjective-et, ami azt mutatja, hogy a definiált char-tömb elfogadható char-ra mutató mutatóként.
Most próbáljuk meg az adjective-et közvetlenül char-ra mutató mutatóként definiálni.
#include <stdio.h>
#include <string.h>
void describe_me(char *adjective){
// segmentation fault on this line when program is executed
strcpy(adjective, "evil");
printf("I am %s.\n", adjective);
}
int main() {
char *adjective = "good";
describe_me(adjective);
printf("I am really %s.\n", adjective);
return 0;
}
Emlékszel, hogy a mutatók erőteljesek, de potenciálisan veszélyesek is?
De miért nem működik?
Egy tömb egy mutató, és a mutató működött egy char-tömbbel!
Az ok az, hogy korábban az adjective char-tömbként volt definiálva, ami a main()-hez tartozó változó.
Most az adjective egy string literálként van definiálva, ami azt jelenti, hogy a karakterek (chars) egy csak olvasható statikus memóriába íródnak, ami az egész programhoz tartozik.
Mindkettő mutató, de különböző dolgokra mutatnak.
Egy literál string érték megváltoztatása olyan, mint megpróbálni megváltoztatni az 5 literál értékét.
Ez nem megengedett.
Tehát a probléma valójában nem az, hogy mutató.
A probléma az, hogy mire mutat.