Göstericiler adres tutar ve adresler sayıdır - dolayısıyla onlarda aritmetiğin çalışmasını beklemek makuldür. Çalışır, ama düz tam sayı aritmetiğinin çalıştığı gibi değil. C, her gösterici işlemini gösterilen şeyin boyutuna göre ölçekler; bu da "bir ekle"yi çok daha yararlı olan "bir sonraki elemana geç"e dönüştürür.
O tek tasarım kararı, bir diziyi göstericiyle dolaşmanın C'de indeksle dolaşmak kadar doğal olmasının sebebidir.
1 Eklemek Bir Bayt Değil Bir Nesne Kaydırır
Bir gösterici alın, 1 ekleyin ve her iki adresi yazdırın:
int * 4 bayt atlar, double * 8 atlar, char * 1 atlar - her biri tam olarak kendi tipindeki bir sonraki nesnenin başlayacağı yere iner. Kural basittir:
ptr + n == (ptr icindeki adres) + n * sizeof(*ptr)
Bir göstericinin neden bir tipi olduğunun sebebi tam olarak budur. Tipi olmayan çıplak bir adres, "sonraki"nin ne kadar uzakta olduğunu bilemezdi.
Bir Diziyi Göstericiyle Dolaşmak
Şimdi kazanç. Bir dizinin elemanları ardışık bellekte durur, dolayısıyla bir göstericiyi adımlamak onları sırayla ziyaret eder:
Her iki döngü de aynı beş sayıyı yazdırır. İkincisinde scores ilk elemanına bir göstericiye dönüşür, p++ bir int ilerler ve *p orada duran elemanı okur. Bellekte serilmiş hâliyle:
scores[0] scores[1] scores[2] scores[3] scores[4]
+---------+---------+---------+---------+---------+
| 88 | 92 | 75 | 60 | 100 |
+---------+---------+---------+---------+---------+
^ ^ ^
p p+1 scores+5
(baslangic) (4 bayt sonra) (sonun bir otesi)
İki biçim arasındaki ilişki kesindir ve ezberlenmeye değer: scores[i], *(scores + i) olarak tanımlanır. Bu konuda daha fazlası göstericiler ve diziler sayfasında.
Artırma, Azaltma ve *p++ Tuzağı
++ ve -- göstericilerde aynı ölçeklemeyle çalışır.
Herkesi şaşırtan ifade *p++'dır. Son ek ++, *'dan daha sıkı bağlanır, dolayısıyla "mevcut p'yi al, p'yi ilerlet, sonra eski değeri dereferans et" demektir:
Üç ifade, üç farklı anlam:
| İfade | Sonrasında gösterici | Üretilen değer |
|---|---|---|
*p++ | ilerlemiş | kaymadan önceki eleman |
*++p | ilerlemiş | kaymadan sonraki eleman |
(*p)++ | değişmemiş | eski değer ve eleman artırılır |
*p++ deyimsel C'dir - metin ve tampon kopyaları böyle yazılır - ama başka bir şey kastediyorsanız parantezleri yazın.
İki Göstericiyi Çıkarmak
Bir göstericiden diğerini çıkarmak, aralarındaki bayt sayısını değil eleman sayısını verir:
Sonuç tipi, <stddef.h>'deki işaretli bir tam sayı tipi olan ptrdiff_t'dir ve %td ile yazdırılır. İki farklı diziye giden göstericileri çıkarmak tanımsız bir sonuç üretir - standart işlemi yalnızca tek bir nesne içinde tanımlar.
Bir göstericiden bir tam sayı da çıkarabilirsiniz (p - 3 üç eleman geri gider), ama iki göstericiyi toplayamazsınız. "İki adresin toplamı" hiçbir şeyi adlandırmaz.
Göstericileri Karşılaştırmak
İlişkisel operatörler aynı diziye giden göstericilerde çalışır ve bellek yerleşiminin düşündürdüğü şeyi tam olarak ifade ederler: p < q, p daha erken bir elemanı adreslediğinde doğrudur.
front < back, döngünün tüm sonlanma hikâyesidir ve doğrudur, çünkü her iki gösterici de tek bir dizinin içinde kalır. == ve != de bir döngüyü son işaretine karşı test etmenin doğal yoludur; bu bizi tüm bunları yasal kılan kurala getiriyor.
Sonun-Bir-Ötesi Kuralı
C, bir dizinin son elemanından bir sonraki konuma bir gösterici oluşturmanıza açıkça izin verir. Onu hesaplayabilir, saklayabilir ve onunla karşılaştırabilirsiniz. Onu dereferans edemezsiniz.
int arr[4];
arr[0] arr[1] arr[2] arr[3] (eleman yok)
+------+ +------+ +------+ +------+ +- - - -+
| | | | | | | | | |
+------+ +------+ +------+ +------+ +- - - -+
^ ^
arr arr + 4
kullanmak yasal OLUSTURMAK ve KARSILASTIRMAK yasal
dereferans etmek asla yasal degil
O kural, standart döngü biçimini geçerli kılan şeydir:
for (int *p = arr; p != arr + n; p++) {
/* ... */
}
Son yinelemede p, arr + n olur, karşılaştırma başarısız olur ve döngü çıkar - o konumu hiç okumadan.
Yine de iki şey tanımsız kalır. Sonun iki ötesine ya da başlangıcın bir öncesine gösterici oluşturmak, onu hiç dereferans etmeseniz bile tanımsız davranıştır:
int *bad1 = arr + n + 1; // tanımsız - çok fazla ileri
int *bad2 = arr - 1; // tanımsız - başlangıcın öncesi
O ikincisi pratikte önemlidir: for (int *p = arr + n - 1; p >= arr; p--) şeklinde yazılmış geriye doğru bir döngü, son azaltmasında arr - 1'i hesaplar. Her yaygın derleyicide çalışır ve yine de biçimsel olarak tanımsızdır. Temiz geriye doğru döngü bundan kaçınır:
void Göstericiler ve Yapamayacağınız Aritmetik
Bir void *, tipi olmayan bir adres tutar; malloc ve qsort genel kalmayı böyle başarır. Eleman boyutu olmadığı için bir void * üzerinde gösterici aritmetiğine standart izin vermez - önce somut bir tipe (ya da bayt düzeyindeki işler için char *'a) dönüştürün.
void *v = buffer;
// v + 1; // standart C değil - ölçeklenecek boyut yok
char *b = v;
b + 1; // sorunsuz - bir bayt ileri
GCC ve Clang, void * aritmetiğini onu char * gibi ele alan bir uzantı olarak kabul eder, dolayısıyla ona dayanan kod, daha katı bir yerde derlenene kadar derlenir. Uyarılmak istiyorsanız -std=c17 -pedantic ile derleyin.
Bu Size Ne Kazandırır
Gösterici aritmetiği ara sıra uzandığınız bir iyileştirme numarası değildir - dizi indekslemenin, metin işlemenin ve standart kütüphanedeki her tampon dolaşımının altındaki mekanizmadır. strlen, sonlandırıcı '\0''a ilerletilmiş ve başlangıçtan çıkarılmış bir göstericidir. memcpy, aynı adımla ilerleyen iki göstericidir.
Sıradaki: bir diziyi bir fonksiyona ilk geçirdiklerinde herkesi yakalayan sizeof tuzağı dahil, dizi/gösterici eşdeğerliğinin gerçekte nasıl çalıştığı: göstericiler ve diziler.
Sıkça Sorulan Sorular
C'de ptr + 1 ne yapar?
Göstericiyi bir bayt değil bir nesne ileri taşır. Tipik bir makinede bir int * için bu 4 bayttır; bir double * için 8'dir. Derleyici sizin için sizeof(*ptr) ile çarpar; göstericinin tipinin önemli olmasının sebebi budur.
C'de iki gösterici çıkarılabilir mi?
Evet, ikisi de aynı diziyi gösterdiğinde. end - start, aralarındaki eleman sayısını ptrdiff_t tipiyle verir (%td ile yazdırın). İlgisiz nesnelere giden göstericileri çıkarmak tanımsız davranıştır.
Bir dizinin sonunun bir ötesini göstermek yasal mıdır?
Evet - sonuncudan bir eleman sonrasına gösterici oluşturmaya, for (int *p = arr; p != arr + n; p++) gibi döngüler çalışsın diye açıkça izin verilir. O göstericiyi hesaplayıp karşılaştırabilirsiniz, ama onu dereferans etmek tanımsız davranıştır.
*p++ neden (*p)++ ile aynı değildir?
*p++, göstericiyi artırır ve eski değeri dereferans eder - mevcut elemanı okur, sonra ilerler. (*p)++, göstericiye dokunmaz ve gösterdiği değeri artırır. Son ek ++, *'dan daha sıkı bağlanır, dolayısıyla anlamı değiştiren parantezlerdir.