Memahami Pointer di C

Sebenarnya tentang pointer ini pernah saya tulis di tulisan saya yang lain: Memory Address dan C. Bedanya, ditulisan ini saya ingin membahas tentang pointer bahasa pemrograman C lebih detail.
Pointer
Pointer merupakan cara untuk kita mengakses value yang sama dengan alamat memory yang sama dengan variabel yang berbeda. Memang terdengar sedikit tidak berguna: “Kenapa harus punya 2 variabel yang sama untuk value yang sama juga?” makes sense, tapi kita coba lihat kegunannya nanti.
Kita bisa membuat pointer dengan bantuan 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;
}
Kita bisa memberikan variabel pointer dengan alamat memory dari variabel lain. Alamat memory dari sebuah variabel bisa menggunakan ampersand (&).
Berikut contoh kita untuk mengambil alamat memory dari sebuah variabel
#include <stdio.h>
int main(void){
int x = 10;
// akan mencetak alamat memory
// tempat dimana variabel x
// disimpan.
// contoh dalam kasus ini
// menampilkan: 0x7ffc580a4904
printf("%p\n", &x);
return 0;
}
“%p” dalam printf bisa dibaca dengan pointer atau format untuk menampilkan memory address dari sebuah variabel.
Karena kita sudah tau cara mengambil memory address, selanjutnya kita bisa menambahkan memory address ke variabel pointer.
#include <stdio.h>
int main(void){
int x = 10;
int *p = &x;
return 0;
}
Kode diatas menunjukkan bahwa variabel pointer p dan variabel x memiliki alamat memory yang sama, 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
Jika kita ingin membuat variabel yang memiliki tipe data yang sama, kita bisa declare 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
Derefercing merupakan cara kita untuk membaca value dari alamat yang sudah ditunjuk oleh variabel poiter, kita bisa menggunakan asterisk (*) lagi 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.
sizeof dan 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 itu sendiri, 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.
Array, Pointer dan Function
Jika kita memiliki function dengan parameter pointer, seperti ini
int my_strlen(char *s);
Maka, kita bisa mengisinya dengan pointer atau array untuk melempar argumen ke function tersebut.
int main(void){
char *p = "Hello, world!";
char s[] = "Hello, string!";
my_string(p); // valid
my_string(s); // valid
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.
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.
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;
}
Operasi Matematika dan Pointer
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);. 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 NULL Character 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;
}
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