C 中的指针 – 个人笔记

个人笔记,仅供学习【

注:Markdown 内的 # 号被认为是 Hashtag 的问题直到本文撰写时虽然修复了,但是暂未发布新版本(见此)。 Update: 已修复。

指针简要介绍

假设一个指针变量名是 ptr,可以编写如下语句:

ptr = &pooh; // 把 pooh 的地址赋给 ptr

对于这条语句,我们说 ptr “指向” pooh。ptr 和 &pooh 的区别是 ptr 是变量,而 &pooh 是常量。 还可以把ptr指向别处:

ptr = &bah; // 把 ptr 指向 bah,而不是 pooh

这样我们就可以使用间接运算符 *(indirection operator)找出储存在 bah 中的值:

val = *ptr; // 找出 ptr 指向的值

语句 ptr = &bah;val = *ptr; 放在一起相当于下面的语句:

val = bah;

地址运算符 &,后跟一个变量名时,& 给出该变量的地址。 例如:&nurse 表示变量 nurse 的地址。

地址运算符 ,后跟一个指针名或地址时, 给出储存在指针指向地址上的值。

示例:

nurse = 22;
ptr = &nurse; // 指向 nurse 的指针
val = *ptr; // 把 ptr 指向的地址上的值赋给val

执行以上3条语句的最终结果是把22赋给val。

声明指针

int *p; // p 是一个返回整型数据的指针 首先从 p 处开始,先与 * 结合,所以说明 p 是一个指针。 然后再与 int 结合,说明指针所指向的内容的类型为int 型。

通常,程序员在声明时使用空格,在解引用变量时省略空格。

输出地址用 %p。

函数、数组与指针

total = sum(marbles); // 可能的函数调用
// ...
int sum(int * ar, int n) { // more general approach
    int i;
    int total = 0;
    for( i = 0; i < n; i++) { // use n elements
        total += ar[i]; // ar[i] the same as *(ar + i)
    }
    return total;
}

这里,第1个形参告诉函数该数组的地址和数据类型,第2个形参告诉函数该数组中元素的个数。 注意,只有在函数原型或函数定义头中,才可以用 int ar[] 代替 int * arint sum(int ar[], int n);

int *ar 形式和 int ar[] 形式都表示 ar 是一个指向 int 的指针。 (int ar[] 用于提醒读者:指针 ar 指向的不仅仅一个 int 类型值,还是一个 int 类型数组的元素。)

由于函数原型可以省略参数名,所以下面4种原型都是等价的:

int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int);

在函数定义中不能省略参数名。下面两种形式的函数定义等价:

int sum(int *ar, int n) {
    // code goes here
}

int sum(int ar[], int n); {
    // code goes here
}

上例中,sum() 函数使用一个指针形参标识数组的开始,用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型)。 还有一种方法是传递两个指针,第1个指针指明数组的开始处(与前面用法相同),第2个指针指明数组的结束处。

下面的程序表明了指针形参是变量,这意味着可以用索引表明访问数组中的哪一个元素。

/* sum_arr2.c -- sums the elements of an array */

#include <stdio.h>
#define SIZE 10

int sump(int * start, int * end);

int main(void) {
    int marbles[SIZE] = {20,10,5,39,4,16,19,26,31,20};
    long answer;

    answer = sump(marbles, marbles + SIZE);
    printf("The total number of marbles is %ld.\n", answer);

    return 0;
}

/* use pointer arithmetic */

int sump(int * start, int * end) {
    int total = 0;

    while (start < end) {
        total += *start; // add value to total 把数组元素的值加起来
        start++; // advance pointer to next element 让指针指向下一个元素
    }
    return total;
}

指针 start 开始指向 marbles 数组的首元素,所以赋值表达式 total += *start 把首元素(20)加给 total。 然后,表达式 start++ 递增指针变量 start,使其指向数组的下一个元素。 因为 start 是指向 int 的指针,start 递增1相当于其值递增 int 类型的大小。

注意,前文的 sum() 函数把元素的个数作为第二个参数,把该参数作为循环测试的一部分。 而 sump() 函数则使用第二个指针来结束循环。

因为 while 循环的测试条件是一个不相等的关系,所以循环最后处理的一个元素是 end 所指向位置的前一个元素。 这意味着 end 指向的位置实际上在数组最后一个元素的后面。

C 保证在给数组分配空间时,指向数组后面第一个位置的指针仍是有效的指针。 这使得 while 循环的测试条件是有效的,因为 start 在循环中最后的值是 end(在最后一次while循环中执行完 start++; 后,start的值就是end的值)。

因为下标从 0 开始,所以 marbles + SIZE 指向数组末尾的下一个位置。

上述的循环体也可以写成一行代码:

total += *start++;

一元运算符 * 和 ++ 的优先级相同,但结合律是从右往左,所以 start++ 先求值,然后才是 *start(指针 start 先把指针指向位置上的值加到 total 上后指向)。 如果使用 *++start,顺序则反过来,先递增指针,再使用指针指向位置上的值。 如果使用 (*start)++,则先使用 start 指向的值,再递增该值,而不是递增指针。

以下为一个实例:

/* order.c -- precedence in pointer operations */

#include <stdio.h>
int data[2] = {100, 200};
int moredata[2] = {300, 400};

int main(void) {
    int * p1, * p2, * p3;

    p1 = p2 = data;
    p3 = moredata;

    printf(" *p1 = %d, *p2 = %d, *p3 = %d\n", *p1 , *p2 , *p3);
    printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n", *p1++ , *++p2 , (*p3)++);
    printf(" *p1 = %d, *p2 = %d, *p3 = %d\n", *p1 , *p2 , *p3);

    return 0;
}

输出结果是这样的:

*p1 = 100, *p2 = 100, *p3 = 300
*p1++ = 100, *++p2 = 200, (*p3)++ = 300
*p1 = 200, *p2 = 200, *p3 = 301

只有 (*p3)++ 改变了数组元素的值,其他两个操作分别把 p1 和 p2 指向数组的下一个元素。

指针表示法和组表示法

C 语言中,ar[i]*(ar+1) 这两个表达式都是等价的。 无论 ar 是数组名还是指针变量,这两个表达式都没问题。 但是,只有当 ar 是指针变量时,才能使用 ar++ 这样的表达式。

指针操作

如果编译器不支持 %p 转换说明,可以用 %u 或 %lu 代替 %p; 如果编译器不支持用 %td 转换说明打印地址的差值,可以用 %d 或 %ld 来代替。

// ptr_ops.c -- pointer operations

#include <stdio.h>

int main(void) {
    int urn[5] = {100,200,300,400,500};
    int * ptr1, * ptr2, *ptr3;

    ptr1 = urn; // assign an address to a pointer 把一个地址赋给指针
    ptr2 = &urn[2]; // ditto 把一个地址赋给指针
                    // dereference a pointer and take 解引用指针
                    // the address of a pointer 以及获得指针的地址
    printf("pointer value, dereferenced pointer, pointer address:\n");
    printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);

    // pointer addition  指针加法
    ptr3 = ptr1 + 4;
    printf("\nadding an int to a pointer:\n");
    printf("ptr1 + 4 = %p, *(ptr4 + 3) = %d\n", ptr1 + 4, *(ptr1 + 3));
    ptr1++; // increment a pointer 递增指针
    printf("\nvalues after ptr1++:\n");
    printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
    ptr2--; // decrement a pointer 递减指针
    printf("\nvalues after --ptr2:\n");
    printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2);
    --ptr1; // restore to original value 恢复为初始值
    ++ptr2; // restore to original value 恢复为初始值
    printf("\nPointers reset to original values:\n");
    printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);

    // subtract one pointer from another 一个指针减去另一个指针
    printf("\nsubtracting one pointer from another:\n");
    printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1);

    // subtract an integer from a pointer 一个指针减去一个整数
    printf("\nsubtracting an int from a pointer:\n");
    printf("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 - 2);

    return 0;
}

输出结果是这样的:

pointer value, dereferenced pointer, pointer address:
ptr1 = 0x7fff5fbff8d0, *ptr1 =100, &ptr1 = 0x7fff5fbff8c8

adding an int to a pointer:
ptr1 + 4 = 0x7fff5fbff8e0, *(ptr4 + 3) = 400

values after ptr1++:
ptr1 = 0x7fff5fbff8d4, *ptr1 =200, &ptr1 = 0x7fff5fbff8c8

values after --ptr2:
ptr2 = 0x7fff5fbff8d4, *ptr2 = 200, &ptr2 = 0x7fff5fbff8c0

Pointers reset to original values:
ptr1 = 0x7fff5fbff8d0, ptr2 = 0x7fff5fbff8d8

subtracting one pointer from another:
ptr2 = 0x7fff5fbff8d8, ptr1 = 0x7fff5fbff8d0, ptr2 - ptr1 = 2

subtracting an int from a pointer:
ptr3 = 0x7fff5fbff8e0, ptr3 - 2 = 0x7fff5fbff8d8

指针变量的基本操作:

赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的 变量名、另一个指针进行赋值。 注意,地址应该和指针类型兼容。

解引用:* 运算符给出指针指向地址上储存的值。 本例中,*ptr1 的初值是 100,该值储存在编号为 0x7fff5fbff8d0 的地址上。

取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,& 运算符给出指针本身的地址。 本例中,ptr1 储存在内存编号为 0x7fff5fbff8c8 的地址上,该存储单元储存的内容是 0x7fff5fbff8d0,即urn的地址。 因此 &ptr1 是指向 ptr1 的指针,而 ptr1 是指向 utn[0] 的指针。

指针与整数相加:可以使用+运算符把指针与整数相加,或整数与指针相加。 无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。

递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。 注意!变量不会因为值发生变化就移动地址。

指针减去一个整数:可以使用 – 运算符从一个指针中减去一个整数。 指针必须是第 1 个运算对象,整数是第 2 个运算对象。

递减指针:当然,除了递增指针还可以递减指针。

指针求差:可以计算两个指针的差值。

比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。

注意,这里的减法有两种:可以用一个指针减去另一个指针得到一个整数,或者用一个指针减去一个整数得到另一个指针。

千万不要解引用未初始化的指针

考虑下面的例子:

int * pt;   // 未初始化的指针
*pt = 5;    // 严重的错误

第2行的意思是把5储存在pt指向的位置。但是pt未被初始化,其值是一个随机值,所以不知道5将储存在何处。 这可能不会出什么错,也可能会擦写数据或代码,或者导致程序崩溃。

因此,在使用指针之前,必须先用已分配的地址初始化它。


如果喜欢本文,欢迎点击下方的「鼓掌」按钮!

如果上面没有加载出任何东西,可以点击这里