C/C++ 指针语法详解

C/C++ 指针语法详解

C 语言是一门很强大的语言,它把指针这个概念连同相关语法一起交给了我们。指针本质上也是一个变量,只不过它存的是内存地址,靠它就能直接访问和操作内存里的数据。

  1. C 指针类型的作用
  2. 多级指针

1. 指针与内存

指针和 int、float 一样,就是一种普通的数据类型。唯一的区别在于,int、double 这类变量存的是值,而指针存的是另一个变量的地址,如下图所示:

上面定义的 value 是普通的 int 变量,它那块内存里存的是普通数据。而指针变量 p 的内存里存的,则是另一个变量的地址。这里有个问题:从内存角度看,不管是指针还是普通变量,存的都是二进制数据,那我们怎么区分一个变量存的是普通数据还是地址数据呢?这就要靠语法来区分了,如下:

int value = 100;
int* p = 0x3456;

只要看到变量定义旁边带个星,就表示这个变量存的是地址。另外,定义指针时还得说清楚这个地址指向的是什么类型的变量。比如 int* 表示 0x3456 那块内存里存的是 int 类型数据,所以我们也可以说 p 指向一个 int 类型的变量。

指针也是一个变量,所以它为了存地址值同样要占用内存空间,而且它自己也有地址。

理解了这条规律,那么如果指针指向的变量本身又是个指针,就像下图这样:

p2 的类型是 int*,那 p1 是什么类型,该怎么表示呢?答案是二级指针,写法如下:

// * 星号表示 p1 变量存储的是地址,是个指针变量
// int* 表示 p1 指向的变量是 int* 类型
// 合起来写成 int**
int**

以此类推,如果再来一个指针 p0 指向 p1,那 p0 就是三级指针,写作 int*** p0。

那怎么拿到一个变量的地址呢?对变量用取地址符 & 就行,输出地址时用占位符 %p。示例代码如下:

#include <stdio.h>

int main() {


    int iv = 100;
    int* ip = &iv;   // 获得变量 iv 的内存地址,赋值到 ip 变量中
    int** ipp = &ip;  // 获得变量 ip 的内存地址,赋值到 ipp 变量中

    printf("iv 变量的地址是: %p %p\n", &iv, ip);
    printf("ip 变量的地址是: %p %p\n", &ip, ipp);


    // 其他类型指针
    double dv = 3.14;
    double* dp = &dv;
    double** dpp = &dp;

    return 0;
}

程序输出结果:

iv 变量的地址是: 0x7ffee79be6f8 0x7ffee79be6f8
ip 变量的地址是: 0x7ffee79be6f0 0x7ffee79be6f0

2. 指针运算规则

基本类型的变量能做算术运算,比如加减。指针变量其实也能做运算。这里我们重点讲两种操作:指针的加减法和解引用。

  1. 指针的加减法,指的是移动指针在内存中的指向,这个操作很危险,一定要当心。
  2. 指针的解引用,就是取出指针指向那块空间里的值。

2.1 指针加减法

#include <stdio.h>

int main() {

    // NULL 表示指针指向地址为0的空间
    int* ip = NULL;
    printf("ip 存储的地址: %ld\n", ip);
    ip += 1;
    printf("ip 存储的地址: %ld\n", ip);

    printf("--------------\n");

    double* dp = NULL;
    printf("dp 存储的地址: %ld\n", dp);
    dp += 1;
    printf("dp 存储的地址: %ld\n", dp);
    
    return 0;
}

程序运行结果:

ip 存储的地址: 0
ip 存储的地址: 4
--------------
dp 存储的地址: 0
dp 存储的地址: 8

从输出能看到,ip 和 dp 都是指针变量,但做完 +1 之后的值却不一样。这里就要引出指针步长的概念了。指针 +1 可以理解成指针在内存里向前挪一步,至于这一步跨几个字节,取决于它指向的数据类型占多大。例如:

  1. ip 指向 int 类型,int 是 4 字节,所以 ip 挪一步就在内存里前进 4 个字节。
  2. dp 指向 double 类型,double 是 8 字节,所以 dp 挪一步就在内存里前进 8 个字节。

那么问题来了,如果 ip + 2,会挪多少个字节呢?

2.2 指针解引用

拿到一个指针变量之后,怎么通过它取出指向内存里存的值呢?这就是解引用操作,如下代码:

多字节变量的地址指的是首字节的地址。指针 p 存的是多字节类型的首字节地址,解引用时会根据类型大小读出相应数量字节的数据。

#include <stdio.h>

int main() {

    int value = 100;
    int* p = &value;

    // *p 表示从 p 变量中取出存储的内存地址,并寻址到该空间。此时,我们可以
    // 从该空间中取值,如下:
    printf("p 指向变量的地址: %d\n", *p);
    // 修改该空间的值,如下:
    *p = 200;
    printf("p 指向变量的地址: %d\n", *p);

    // 通过指针间接修改了 value 变量的值
    printf("value 变量值是: %d\n", value);


    return 0;
}

程序运行结果:

p 指向变量的地址: 100
p 指向变量的地址: 200
value 变量值是: 200

2.3 越界内存访问

了解了指针的算术运算和解引用,就必须警惕一个 C/C++ 里特别容易出的致命错误,叫越界内存读写。所谓越界,就是你通过指针去碰了一块自己没有操作权限的内存。有权限的内存,指的就是我们自己申请的那些。

#include <stdio.h>

int main() {

    int value = 100;
    int* p = &value;

    // p 指针进行移动,此时指向我们没有申请的内存
    p += 1;

    // 如果只是访问越界内存的值,只是数据可能是错误的
    printf("越界内存中的值:%d\n", *p);

    // 注意: 如果尝试去修改越界内存,这是极其危险的操作
    // 有些系统会直接终止我们程序的运行
    // 有些则看起来允许你进行修改,但是这是未定义的行为
    *p = 200;
    printf("越界内存中的值:%d\n", *p);

    return 0;
}