Memahami Pointer di C

Sebenarnya tentang pointer ini pernah saya tulis di Memory Address dan C. Bedanya, ditulisan ini saya ingin bahas pointer bahasa pemrograman C lebih detail.
Memori
Komputer bisa menyimpan semua data di dalam memori: int , float, char atau lainnya. Data di dalam komputer akan disimpan dalam memori dalam bentuk block-block, dan setiap block hanya bisa menyimpan 1 byte atau 8 bit.

Setiap block memory memiliki alamat yang digunakan untuk menandai tempat dimana data disimpan. Alamat memory biasanya dituliskan dalam bentuk hexadecimal, kenapa? agar lebih ringkas, karena 1 digit hex sama dengan 4 bit, dan karena satu block dapat menyimpan data sebanyak 8 bit, maka kita hanya perlu menuliskan 2 digit hex untuk setiap blocknya, lebih ringkas dan mudah dibaca dibanding menulis 8 bit.
Misalnya kita memiliki data int dengan value 10, di memori akan terlihat seperti ini
int x = 10;

Gambar diatas, jika kita lihat lebih teliti, variabel int x memakan 4 block memori, ini berkaitan dengan bagaimana tipe data disimpan didalam memory, dalam hal ini int membutuhkan 4 bytes atau 4 block memory.
Lalu, bagaimana jika kita ingin mencetak alamat memory dari sebuah variabel? kita bisa menggunakan ampersand (&) untuk melihat alamat memori dari sebuah variabel
#include <stdio.h>
int main(void){
int x = 10;
// mencetak alamat memory
printf("%p\n", &x); // output: 0x7ffe197f9524
return 0;
}
Kode diatas kita mencetak alamat dari sebuah variabel x, jika kita coba gambarkan didalam memory akan tampak seperti gambar dibawah ini.

Pointer
Pointer adalah variabel yang digunakan untuk menyimpan alamat memory.
Pointer bisa dibuat dengan menambahan asterisk (*) yang ditempatkan sebelum nama variabel dan setelah tipe data.
Berikut adalah contoh variabel pointer
#include <stdio.h>
int main(void){
// variabel pointer
int *p;
return 0;
}
Oke kita sudah tau cara membuat variabel pointer. Perlu diingat bahwa yang disimpan dari variabel pointer adalah alamat memory, sehingga kita tidak bisa menambahakan value secara langsung seperti ini
int *p = 10; // Error
Karena variabel pointer hanya bisa menyimpan alamat memory dari variabel lain, maka kita bisa menambahkan ampersand (&) variabel lain ke variabel pointer.
Berikut contoh kita untuk mengambil alamat memory dari sebuah variabel
#include <stdio.h>
int main(void){
int x = 10;
int *p = &x;
return 0;
}
Kode diatas menunjukkan bahwa variabel pointer p akan melakukan copy alamat memory dari variabel x sehingga variabel pointer dan variabel x diatas memiliki alamat memory yang sama
#include <stdio.h>
int main(void){
int x = 10;
int *p = &x;
printf("Alamat x: %p\n", &x); // Alamat x: 0x7ffcb0b8af7c
printf("Alamat p: %p\n", p); // Alamat p: 0x7ffcb0b8af7c
return 0;
}
Karena kedua variabel tersebut memiliki alamat memory yang sama, maka akan tampak keduanya saling berhubungan, sehingga ketika salah satu diubah, maka kedua value variabel tersebut akan berubah.
#include <stdio.h>
int main(void){
int x = 10;
int *y = &x;
printf("%d \n", x); // 10
// kita coba ubah variabel
// pointer y
*y = 100;
// 100, disini variabel x
// juga ikut berubah
printf("%d \n", x);
// sekarang kita coba
// sebaliknya
x = 200;
// jika kita coba akses
// variabel pointer y
// maka hasilnya 200
printf("%d \n", *y);
return 0;
}
Declaring Pointer
Terdapat beberapa cara untuk membuat variabel pointer dan jika kita ingin membuat variabel yang memiliki tipe data yang sama, kita bisa mendeklarasikan variabel tersebut dengan dua cara.
Pertama di baris terpisah dan setiap variabel punya tipe data sendiri, meskipun sama
int a;
int b;
Kedua, kita bisa menggabungkan jadi satu baris, dengan menuliskan satu tipe data dan memisahkan antara variabel dengan koma
int a, b;
Kedua cara tersebut valid untuk mendefinisikan variabel.
Lalu, bagaimana jika terdapat dua variabel, salah satunya merupakan pointer, konsepnya sama, kita bisa memisahkan dengan koma jika memiliki tipe data yang sama, hanya saja symbol asterisk (*) menempel pada nama variabel. Contohnya
int a;
int *b;
// kita bisa tuliskan seperti ini juga
int a, *b;
Dereferencing
Variabel pointer hanya menyimpan alamat memori, sehingga kita tidak dapat langsung menggunakan variabel pointer untuk membaca value dari alamat memori tersebut.
#include <stdio.h>
int main(void){
int x = 10;
int *p = &x;
printf("%p\n", p); // menampilkan alamat memori
printf("%d\n", p); // menampilkan angka acak (bukan value dari variabel x, yaitu 10)
return 0;
}
Dereferencing merupakan cara untuk membaca value dari alamat yang sudah ditunjuk oleh variabel pointer, kita bisa menggunakan asterisk (*) untuk membaca valuenya.
#include <stdio.h>
int main(void){
int x = 10;
int *y = &x;
printf("%d\n", *y); // 10
printf("%p\n", y); // 0x7ffe2aa98844
return 0;
}
Tanpa menggunakan asterisk, yang ditampilkan hanyalah alamat memory atau hanya angka acak dan bukan value sebenarnya.
Ukuran Pointer
Function sizeof() digunakan untuk melihat ukuran dari sebuah variabel atau tipe data. Kita coba lihat kode dibawah ini ketika mencoba print ukuran dari tipe data, variabel pointer dan variabel itu sendiri (tanpa pointer).
// jika, kita punya variabel x yang point ke int
int *x;
printf("Sizeof int: %zu\n", sizeof(int)); // print size of int | 4
printf("Sizeof *x: %zu\n", sizeof(*x)); // print size of int | 4
printf("Sizeof x: %zu\n", sizeof(x)); // print size of int* | 8
Kenapa int dan pointer int* memiliki size yang berbeda? alasan logisnya karena int hanya digunakan untuk menyimpan angka, sedangkan pointer int* menyimpan memory address, sehingga perlu tempat penyimpanan yang lebih besar dibanding int.
Tapi, kenapa 8? ini ditentukan dari arsitektur komputer yang kita gunakan, misalnya di komputer 64 bit, artinya alamat memory yang disimpan sebesar 64 bit atau 8 bytes. di komputer 32 bit akan memiliki size 4 bytes untuk pointer int*.
Function dan Pointer
Sama seperti variabel lain, variabel pointer juga bisa digunakan sebagai parameter atau dilempar sebagai argumen ke sebuah function.
Contohnya ketika kita memiliki function dengan nama sum_two yang akan menambahkan 2 setiap argumen yang ditambahkannya.
#include <stdio.h>
void sum_two(int *p){
*p += 2;
}
int main(void){
int x = 10;
int *p = &x;
sum_two(p);
printf("%d\n", x); // 12
printf("%d\n", *p); // 12
return 0;
}
Jika kita lihat dari kode diatas, kedua variabel tersebut memiliki angka yang sama yaitu 12, padahal function tersebut tidak melakukan return value, dan inilah yang dinamakan dengan Pass By Reference, yang artinya function tersebut akan merubah value aslinya dari argumen yang dikirimkan.
String dan Pointer
Pertama kita akan lihat bagaimana string disimpan di dalam memori. Lets say, kita punya satu variabel s yang merupakan array dengan tipe data char, lalu memiliki value Hi!.
#include <stdio.h>
int main(void){
char s[] = "Hi!";
printf("%s\n", s); // output: Hi!
return 0;
}
Data string variabel s jika kita lihat di memori akan tampak seperti ini

Wait, kenapa ada \0? itu adalah NUL terminator, yang merupakan tanda dari akhir sebuah string.
Lalu, kita memiliki variabel pointer untuk menampung alamat memori dari variabel string s, pertanyaanya, apakah variabel pointer tersebut menyimpan semua alamat memori dari string tersebut? ternyata tidak, yang disimpan dalam variabel pointer adalah alamat memori dari string pertama saja.

Kita coba buktikan dengan menampilkan alamat memory yang disimpan dari pointer p dan alamat memori dari karater pertama variabel string s.
#include <stdio.h>
int main(void){
char s[] = "Hi!";
char *p = s;
printf("%p\n", &s[0]); // output: 0x7ffdca3b6ba4
printf("%p\n", &s[1]); // output: 0x7ffebf9cf3b5
printf("%p\n", p); // output: 0x7ffdca3b6ba4
return 0;
}
Anyway, kita bisa membuat string dengan pointer
char s[] = "Hi!";
char *p = "Hi!";
Bedanya, string yang dibuat dengan pointer bersifat read-only, jadi tidak bisa mengubah karakter dari string yang dibuat dengan pointer.
Array dan Pointer
Terdapat persamaan antara pointer dan array, jika kita coba rumuskan, maka array dapat dituliskan sebagai persamaan berikut ini
a[b] = *(a + b);
Karena hal tersebut kita bisa melakukan pengambilan array member dengan cara pointer (*(a + b)).
Contoh
int main(void){
int i;
int arr[3] = {1,2,3};
// menggunakan pointer
for(i = 0; i < 3; i++){
printf("Value dari arr[%d]: %d\n", i, *(arr + i));
}
// menggunakan array
for(i = 0; i < 3; i++){
printf("Value dari arr[%d]: %d\n", i, arr[i]);
}
return 0;
}
Dari kode diatas kita melakukan looping isi dari array variabel arr tapi kita membaca dengan cara pointer yaitu *(arr + i). Ketika kita coba untuk membaca value dari array menggunakan cara array itu sendiri arr[i] juga valid.
Jika kita melihat persamaan diatas, artinya ketika kita memiliki function dengan parameter pointer, maka kita bisa melempar argumen berupa pointer atau array seperti ini
int my_string(char *s);
int main(void){
char *p = "Hello, world!";
char s[] = "Hello, string!";
my_string(p); // valid
my_string(s); // valid
return 0;
}
Struct dan Pointer
Kita bisa memberikan argumen berupa pointer struct ke sebuah function, seperti ini
struct animal {
char *name;
int leg_count;
};
void set_animal_name(struct animal *a, char *new_animal_name){
// TODO
}
Di dalam function set_animal_name kita tidak bisa hanya menuliskan a.name = new_animal_name;, karena dot operator hanya berjalan jika digunakan untuk struct, bukan untuk pointer struct.
Sehingga kita harus melakukan dereference untuk mendapatkan member dari struct tersebut, kita bisa tuliskan seperti ini
struct animal {
char *name;
int leg_count;
};
void set_animal_name(struct animal *a, char *new_animal_name){
// a.name = new_animal_name; // ERROR
(*a).name = new_animal_name;
}
Kode diatas berjalan, dan tidak ada masalah, tapi yang umum digunakan untuk mengakses pointer struct adalah arrow operator
Arrow Operator
Arrow operator ditulis dengan tanda minus diikut dengan tanda lebih besar dari (→). Sehingga function yang sebelumnya bisa kita tulis ulang menggunakan arrow operator sebagai berikut
#include <stdio.h>
struct animal {
char *name;
int leg_count;
};
void set_animal_name(struct animal *a, char *new_animal_name){
a->name = new_animal_name;
}
int main(void){
struct animal a1 = {.name="Kerbau", .leg_count=4};
set_animal_name(&a1, "Sapi");
printf("Name: %s\n", a1.name); // Name: Sapi
return 0;
}
Pointer Arithmetic
Kita bisa melakukan operasi matematika dengan pointer, penjumlahan dan pengurangan.
Perlu diingat bahwa, ketika kita melakukan perubahan pada pointer, kita perlu memastikan bahwa yang ditunjuk oleh pointer harus valid sebelum melakukan dereferencing, jika tidak akan terjadi crash, atau undefined.
Penambahan Pointer
Penambahan pointer yang dimaksud disini masih sama dengan persamaan antara pointer dan array, dimana kita bisa menambahkan variabel pointer dengan angka, sehingga alamat yang ditunjuk menjadi berubah dan value yang dibawa akan ikut berubah juga.
Persamaannya adalah a[b] = *(a + b); dan dengan persamaan tersebut kita bisa mengakses value dari sebuah array dengan cara berikut
#include <stdio.h>
int main(){
int arr[3] = {1,2,3};
int *p = arr;
// dengan persamaan tadi kita bisa membaca value dari array
// dengan cara *(p+1)
printf("value dari arr[1] menggunakan pointer: %d\n", *(p+1));
return 0;
}
Contoh diatas menunjukkan kita bisa melakukan manipulasi value dari sebuah pointer dengan cara menambahkannya dengan angka.
Merubah Pointer
Kita bisa melakukan perubahan pada pointer itu sendiri untuk mengambil value yang berbeda, agar lebih mudah, kita coba buat skenario, dimana kita memiliki sebuah variable array dan variable pointer, dimana keduanya memiliki value yang sama.
#include <stdio.h>
int main(void){
int arr[3] = {1,2,3};
int *p = arr;
return 0;
}
Pada kasus ini kita ingin membaca member dari array melalui variabel pointer, kita bisa lakukan dengan cara looping pointer p lalu kita tambahkan variabel tersebut dengan angka 1, sehingga jika dituliskan akan menjadi seperit berikut
#include <stdio.h>
int main(void){
int arr[3] = {1,2,3};
int *p = arr;
while(*p != 3){
printf("value dari pointer p: %d\n", *p);
p++;
}
return 0;
}
Kita bisa melakukan pergeseran value dengan menambahkan variable pointer p dengan angka 1 (p++)
Pengurangan Pointer
Pointer bisa dikurangkan dengan pointer lainnya, dalam contoh ini kita bisa melakukan perhitungan panjang dari sebuah string dengan cara kita kurangkan dua variabel pointer.
Skenarionya adalah kita punya dua variable pointer, pertama terisi string, dan pointer kedua akan pointing ke variable pointer pertama, jika dituliskan akan seperti berikut
#include <stdio.h>
int main(void){
char s[100] = "Hello, world!";
char *p = s;
return 0;
}
Idenya, kita akan looping variable pointer p, lalu ketika mencapai NUL Terminator dimana adalah batas akhir dari sebuah string, maka loop akan berhenti lalu kita kurangkan dengan variable array yang pertama, dengan begitu kita bisa mendapatkan selisih dari dua variable pointer tersebut.
#include <stdio.h>
int main(void){
char s[100] = "Hello, world!";
char *p = s;
while(*p != '\0'){
p++;
}
printf("Len dari string: %d\n", p-s);
return 0;
}
Null Pointer
Pointer dengan tipe data apapun bisa memiliki value spesial yang disebut dengan null. Artinya, pointer tersebut tidak mengarah ke alamat memory manapun.
int *p;
p = NULL;
Karena pointer tidak mengarah ke manapun, ketika kita melakukan dereferencing maka akan terjadi undefined behavior dan hasilnya akan crash.
int *p = NULL;
*p = 12;
Outputnya akan terlihat seperti ini: [1] 39166 segmentation fault (core dumped), sehingga hal seperti ini perlu dihindari.
Void Pointer
void di function merupakan keyword yang digunakan ketika function tersebut tidak membutuhkan parameter atau function tersebut tidak memiliki return value.
void sum(){} // function tidak memiliki return value
int main(void){} // function tidak memiliki parameter apapun
void di pointer sedikit berbeda, void disini diartikan sebagai penampung yang bisa digunakan untuk tipe data apapun, kita bisa mengisi pointer tersebut dengan int, char atau tipe data lainnya.
Umumnya, terdapat dua usecase untuk void pointer,
- Function yang melakukan operasi byte-by-byte. Contohnya adalah function
memcpy()dimana function tersebut melakukan copy bytes dari satu pointer ke pointer yang lain, dan pointer tersebut bisa merupakan tipe data apapun. - Function yang memanggil function lain, dan function yang dipanggil akan mengembalikan nilai. Kita tahu tipe data apa yang akan ditambahkan di function yang kita panggil, tapi function lain yang juga ikut terpanggil tidak tahu apa yang harus dikembalikan, sehingga dalam kasus ini penggunaan
voidakan membantu function tersebut menentukan tipe data yang akan dikembalikan, dan tipe data akan ditentukan oleh function pemanggilnya. Contohnya adalah functionqsort()danbsearch()yang menggunakan void pionter untuk kasus ini.
Case Pertama
Pertama kita coba lihat bagaimana memcpy() dibuat, jika dituliskan akan terlihat seperti ini
void *memcpy(void *s1, void *s2, size_t n);
Function tersebut akan melakukan copy sebanyak n bytes berawal dari memory address s2 ke memory address s1.
Tapi, pertanyaannya kenapa variabel s1 dan s2 merupakan void pointer? singkatnya, karena function ini dapat memindahkan data dari satu pointer ke pointer lain tanpa melihat tipe data yang digunakan.
Pertama, kita coba untuk memindahkan data pointer ke pointer lain dengan tipe data char.
#include <stdio.h>
#include <string.h>
int main(void){
char s1[] = "Hello, world!";
char s2[100];
memcpy(s2, s1, 14);
printf("string s2: %s\n", s2);
return 0;
}
Kita bisa memindahkan data dari pointer satu ke pointer lain dengan tipe data int.
#include <stdio.h>
#include <string.h>
int main(void){
int i1[] = {1, 2, 3};
int i2[3];
memcpy(i2, i1, 3 * sizeof(int));
for(int i = 0; i < 3; i++){
printf("Index: %d, value: %d\n", i, i2[i]);
}
return 0;
}
Satu function dapat digunakan untuk tipe data yang berbeda.
Selain itu, kita juga bisa coba untuk tipe data lain seperti floats atau struct dengan memcpy().
struct antelope my_antelope;
struct antelope my_clone_antelope;
// ...
memcpy(&my_clone_antelope, &my_antelope, sizeof my_antelope);
Kita bisa lihat bagaimana function memcpy() sangat dinamis terhadap tipe data dari argumen yang diberikan. Tanpa void pointer mungkin kita akan memerlukan memcpy() khusus char, int dan tipe data lainnya, dan itu sangat redundant.
Void pointer juga memiliki kekurangan atau keterbatasan, yaitu
- Kita tidak dapat melakukan pointer arithmetic dengan
void* - Kita tidak dapat melakukan dereference sebuah
void* - Kita tidak dapat menggunakan arrow operator untuk
void*, karena arrow operator melakukan dereference. - Kita tidak dapat melakuakn array notation untuk
void*, karena juga melakukan dereference.
Jika kita lihat lagi, rules diatas masuk akal, karena operasi yang dilakukan diatas memerlukan sizeof dari tipe data yang akan dibaca dan dengan void*, kita tidak tau size data yang di pointing.
Jika pada rules diatas kita tidak dapat melakukan dereferencing, lalu apa yang harus dilakukan?
Seperti memcpy(), void pointer membantu kita untuk membuat generic function yang dapat menerima tipe data apapun. Jika kita lihat kembali, untuk melakukan dereference pada void pointer, kita harus convert void pionter ke tipe data yang kita butuhkan, sebelum melakukan dereference.
Berikut contoh kita melakukan convert tipe data dari void pionter lalu melakukan dereference
#include <stdio.h>
int main(void){
char x = 'x';
void *vp = &x;
char *cp = vp;
printf("void pointer: %c\n", *vp); // error: invalid use of void expression
printf("char pointer: %c\n", *cp);
return 0;
}
Case Kedua
Selanjutnya kita akan melihat bagaimana void pointer digunakan dalam function yang memanggil function lain (callback), dalam contoh ini kita akan coba bahas function qsort().
Function qsort(arr, n, size, comp) memerlukan 4 parameter
- arr: Pointer ke element pertama dari array
- n: banyaknya element dari array
- size: ukuran dari setiap element
- comp: function yang menentukan urutan atau bagaimana element diurutkan
qsort() akan mengurutkan block dari bytes berdasarkan hasil dari function comp() yang kita tambahkan di parameter terakhir dari function qsort().
Comparator function ini memiliki aturan
- function harus menerima dua argumen
const void* - akan return
<0jika argumen pertama diletakkan sebelum argumen kedua - akan return
0jika kedua argumen sama - akan return
>0jika argumen pertama diletakkan setelah argumen kedua
Aturan pertama bilang function tersebut harus menerima dua argumen void pointer, kenapa? karena qsort() tidak tau isi array itu apa, yang qsort() tau cuma ukuran dari setiap element array, sehingga qsort() harus menerima generic type yang ditulis menggunakan void pointer. Siapa yang tau tipe datanya? kita, makanya kita perlu ubah void pointer menjadi tipe data yang kita inginkan ketika membuat comparator function.
Contoh, mengurutkan struct
#include <stdio.h>
#include <stdlib.h>
struct animal {
char *name;
int leg_count;
};
// comparator function
int compar(const void* a, const void *b){
// kita akan ubah void pionter menjadi tipe data
// yang kita inginkan
const struct animal *animal1 = a;
const struct animal *animal2 = b;
// aturan 4: return >0 jika argumen pertama diletakkan setelah argumen kedua
if(animal1->leg_count > animal2->leg_count) return 1;
// aturan 2: return <0 jika argumen pertama diletakkan sebelum argumen kedua
if(animal1->leg_count > animal2->leg_count) return -1;
// aturan 3: return 0 jika keduanya sama
return 0;
}
int main(void){
struct animal a[4] = {
{.name="Dog", .leg_count=4},
{.name="Monkey", .leg_count=2},
{.name="Antelope", .leg_count=4},
{.name="Snake", .leg_count=0}
};
qsort(a, 4, sizeof(struct animal), compar);
for (int i = 0; i < 4; i++) {
printf("%d: %s\n", a[i].leg_count, a[i].name);
}
return 0;
}
- C Programming Language