返回介绍

3. 指针

发布于 2024-10-12 21:58:09 字数 5548 浏览 0 评论 0 收藏 0

和地址用于标注内存位置不同,指针是 实体 ,需要为它 分配内存 ,或者说是种用于存储地址的 整数类型

  • 获取地址,保存到指针: int *ptr = &var
  • 透过指针间接读写目标: *ptr*ptr = val
                  ptr                  var               int *ptr = &var;
  =====//=====+========+=====//=====+=======+=========
              | 0x??10 |            |  100  |           memory
  =====//=====+========+=====//=====+=======+=========
                   |                0x??10 (addr)
                   |                    ^
                   |                    |     
                   +--------------------+
                   

初始化

作为一个特殊的整型变量,指针也需要初始化。

  • NULL :空指针,零值。
  • void* : 无类型指针。(仅表达地址存储,转型后使用)
  • T* : 类型指针。
int main (void)
{
    int x = 100;
    int *p = &x;

    *p = 200;

    return 0;
}

有种习惯性写法,不过并不推荐。

int x = 100, *p = &x;

无类型指针可隐式转换为类型指针。

int main (void)
{
    void *p = malloc(sizeof(int));
    int *x = p;
    *x = 100;

    x = NULL;
    free(p);

    return 0;
}

当指针不再使用,可以考虑将其赋值为空指针,以避免成为悬垂指针或野指针。

  • 悬垂指针 (dangling pointer):所指向内存已经释放。
  • 野指针 (wild pointer):指向不确定地址。未初始化,或目标内存另做他用。

运算符

对指针相等或不等比较来判断是否指向同一对象。

int x = 10;

int *p1 = &x;
int *p2 = &x;

assert(p1 == p2);

对指针进行加法运算,使其指向某个元素。注意,步进单位是元素长度,而不是字节。

int x[] = { 0, 1, 2, 3 };

int *p = &x[0];
assert(*(p + 2) == x[2]);

p++;
assert(*p == x[1]);

对指针进行减法运算,获取元素索引或偏移量。

int x[] = { 0, 1, 2, 3, 4 };  // x ==> &x[0]
int *p = &x[3];

assert(p - x == 3);

限定符

使用 const 约束指针,使其成为 指向常量的指针 (ponter to constant object)或 指针类型的常量 (constant pointer)。鉴于简称容易混淆,可标注英文,或用全称。

  • 常量指针 :将目标对象看作常量,不能透过指针修改目标,但指针可变。
  • 指针常量 :指针自身为常量,可用指针修改目标,但不能修改指针自身。
   int const *p1 = &x;  常量指针,const 限定目标对象(*p1)。
   int *const p2 = &x;  指针常量,const 限定指针自身(p2)。


                         你俩瞎扯淡!
                         v
   +======+          +=======+          +======+
   |  p1  | -------> |   x   | <------- |  p2  |
   +======+          +=======+          +======+
      ^                                    ^
      我觉着 x 是块又臭又硬的石头。             没工夫管他,我在练金刚不坏神功。
    
// 常量指针

int main (void)
{
    int x = 100;
    int const *p1 = &x;    // 习惯写成 const int *p1。

    // 不能透过该指针修改目标。
    // *p1 += 1;
    //     ^~ error: assignment of read-only location '*p1'

    // 限制只针对该指针,对其他方式无效。
    x += 1;
    assert(*p1 == 101);

    // 指针可变,指向新对象。
    // 依旧保留 const 限定,始终无法透过它修改目标。
    int y = 200;
    p1 = &y;
    assert(p1 != &x);

    // *p1 += 1;
    //     ^~ error: assignment of read-only location '*p1'

    return 0;
}
// 指针常量

int main (void)
{
    int x = 100;
    int *const p2 = &x;

    // 指针自身为常量,可修改目标。
    *p2 += 1;
    assert(x == 101 && *p2 == x);

    // 指针自身不可变。
    int y = 200;
    // p2 = &y;
    //    ^ error: assignment of read-only variable 'p2'

    return 0;
}

当然,还可以混合起来。

int main (void) 
{
    const int x = 100;
    const int *const p = &x;  // 两者混合:常量指针 + 指针常量。

    // *p += 1;
    //    ^~ error: assignment of read-only location '*p'

    int y = 200;
    // p = &y;
    //   ^ error: assignment of read-only variable 'p'

    return 0;
}

对函数中的指针参数,如确定不会修改,可使用 const 限定,避免误操作影响实参。

只要存储内存不是只读,那么就可用强制类型转换方式修改。

从这点上说,const 无论是限定指针还是目标对象,都只是种 “提示”。

int main (void) 
{
    const int x = 100;        // 栈内存分配。
    const int *const p = &x;

    int *xp = (int *)(p);     // 强制类型转换。
    *xp = 200;

    assert(x == 200);

    return 0;
}

另一指针限定符 restrict,并不会对编码有直观影响。它的主要作用是告诉编译器,接下来的操作,我只会用该指针去读写目标对象,不会有其他任何方式(变量名或另一指针)。这样,编译器就无需考虑数据刷新(缓存)等问题,从而尝试生成更加优化的代码。通常不需要处理这些,只在某些特定算法内做性能提升时才会尝试。

多级指针

既然指针是个实体,是个特殊整数,那么我们也可以用另外一个指针指向它,形成 指针的指针 ,或二级指针。常用案例是函数输出参数(out)或字符串数组。

    int x = 100;    int *p = &x;   int **pp = &p;
    

                x                      p                 pp
  =====//====+=====+======//======+========+===//===+========+===
             | 100 |              | 0x??12 |        | 0x??34 |     memory
  =====//====+=====+======//======+========+===//===+========+===
             0x??12(addr)         0x??34                 |
                ^                    |   ^               |
                |                    |   |               |
                +-------- *p --------+   +----- *pp -----+
                |                                        |
                +------------------------------ **pp ----+
                
int main (void) 
{
    int x = 100;
    
    int *p = &x;
    int **pp = &p;

    assert(*p == x);
    assert(*pp == p && **pp == x);

    return 0;
}

多级指针不好维护。相比下面的传出参数,可能返回值更合适,且不用关心 pp 的生命周期。

void test(int **pp)
{
    // 申请一块堆内存,并赋值。
    int *x = malloc(sizeof(int));
    *x = 100;

    // pp 是指针的指针,那么 *pp 就是指针的内存。
    // 把从堆申请的内存地址保存到指针内存。
    *pp = x;
}

int main (void) 
{
    // 虽然没有赋值,但内存是分配了的。
    int *p;

    // 将 p 的地址传入 test 函数。
    // 因为 p 是指针,取指针的指针。
    test(&p);
    assert(*p == 100);

    free(p);
    return 0;
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文