江小南

V1

2023/01/08阅读:25主题:萌绿

聊聊指针

阅读本文,需要对C语言的基础知识有所了解。C语言基础,我给大家准备了详细的学习资料,微信公众号主页回复“C语言”即可领取。

引言

如果在程序中定义了一个变量,那么在对程序进行编译时,系统就会给这个变量分配内存单元,按变量地址存取变量值的方式我们称之为“直接访问”,如:printf(“%d”,i);scanf(“%d”,&i);等。

另外,如果将变量i的地址存放到另一个变量中,我们通过一个变量拿到另一个变量的地址,这种方式叫做“间接访问”,我么也把它称之为“指针”。

在C语言中,指针变量是一种特殊的变量,它用来存放变量地址。

1. 定义格式

基类型 *指针变量名;

例如:

int *i_pointer;

说明:指针与指针变量是两个概念,一个变量的地址称为该变量的“指针”。

如图:2000是地址,即变量i的指针。i_pointer存放的是2000,那么i_pointer称为指针变量。

2. 取地址操作符与取值操作符

取地址操作符为&,也称引用。通过该操作符我们可以获取一个变量的地址值;取值操作符为*,也称解引用。通过该操作符我们可以得到一个地址对应的数据。

指针变量的初始化一定是某个变量的地址。

例如:

#include <stdio.h>

int main() {
    int i=5;
    int *p=&i;  // 指针变量初始化
    printf("i=%d\n",i);  // 直接访问
    printf("*p=%d\n",*p);  // 直接访问
    return 0;
}
F:\Computer\Project\practice\6\6.1-pointer\cmake-build-debug\6_1_pointer.exe
i=5
*p=5

进程已结束,退出代码为 0

注意点:

  1. 指针变量前面的* 表示该变量的类型为指针变量。指针变量名为p,而不是*p。
  2. 在定义指针变量时必须指定其类型,指针变量类型和变量类型要保持一致。例如:int i;那么指针变量应为int *p,都是int类型。
  3. &和*两个运算符的优先级别相同,但要按自右向左的方向结合。
  4. 如果要同时定义多个指针变量,需要使用:int *a,*b,*c…

3. 指针的简单应用

指针的使用场景通常只有两种,传递和偏移。

指针的传递

#include <stdio.h>

void change(int j){
    j=5;
}

int main() {
    int i=10;
    printf("before change i=%d\n",i);
    change(i);
    printf("after change i=%d\n",i);
    return 0;
}
F:\Computer\Project\practice\6\6.2-transmit\cmake-build-debug\6_2_transmit.exe
before change i=10
after change i=10

进程已结束,退出代码为 0

疑问:i=10经过了change函数,值应当变为5才对,为何没有改变?

分析:当main函数开始执行时,系统会为main函数开辟函数栈空间,当走到int i时,main函数的栈空间就会为变量i分配4个字节大小的空间,调用change函数时,系统会为change函数重新分配新的函数栈空间,并为形参j分配4个字节大小的空间,所以i和j对应空间地址是不同的。在调用change(i)时,实际上是将i的值赋给了j。我们把这种效果称为值传递(C语言的函数调用均为值传递)。因此,当我们在change函数的函数栈空间内修改变量j值后,change函数执行结束,其栈空间会释放,j就不存在了,i的地址和它不同,并没有被修改。

我们将以上程序做一些修改:

#include <stdio.h>

void change(int *j){
    *j=5;  // 注意这里不是j=5,而是*j=5
}

int main() {
    int i=10;
    printf("before change i=%d\n",i);
    change(&i);  // 传递变量i的地址
    printf("after change i=%d\n",i);
    return 0;
}
F:\Computer\Project\practice\6\6.2-transmit\cmake-build-debug\6_2_transmit.exe
before change i=10
after change i=5

进程已结束,退出代码为 0

很明显,当我们把i的地址传递给chang函数,那么它取到的值就是i的实际地址对应的值。当然修改的就是i的值了。

指针的偏移

指针的另一种场景就是对其进行加减。在实际中,我们把对指针的加减称为指针的偏移,加就是向后偏移,减就是向前偏移。

#include <stdio.h>

#define N 5
int main() {
    int a[N]={1,2,3,4,5};
    int *p=a;  // 保证等号两边的数值类型一致。一维数组本身存放的就是首地址,所以没有必要写&取地址
    int i;
    for(i=0;i<N;i++){ // 正序输出
        printf("%3d",*(p+i));
    }
    printf("\n------------------\n");
    p=&a[N-1];  // 让P指向最后一个元素,注意有&
    for(i=0;i<N;i++){  // 逆序输出
        printf("%3d",*(p-i));
    }
    printf("\n");
    return 0;
}
F:\Computer\Project\practice\6\6.3-deviation\cmake-build-debug\6_3_deviation.exe
  1  2  3  4  5
------------------
  5  4  3  2  1

进程已结束,退出代码为 0

说明:数组本身就是存放的就是指针,不需要加&,但是里面的具体数据需要通过&取值。

同时,偏移的长度是其基类型的长度,也就是偏移sizeof(int),这样通过*(p+1)就可以得到元素a[1]的值。

4. 指针与一维数组

疑问:为什么一维数组在函数调用进行传递时,它的长度子函数无法知道呢?

#include <stdio.h>

void change(char *d)// 形参实际上接收到的是一个地址(数组的首地址)
    *d='H';  // 指针法
    d[1]='E';  // 下标法
    *(d+2)='L';  // 指针法
}

int main() {
    char c[10]="hello";
    puts(c);
    change(c);  // 数组名作为参数传递给子函数时,是弱化为指针的
    puts(c);
    return 0;
}
F:\Computer\Project\practice\6\6.3-pointer_arr\cmake-build-debug\6_3_pointer_arr.exe
hello
HELlo

进程已结束,退出代码为 0

解析:因为一维数组名中存储的是数组的首地址。所以子函数change中其实传入了一个地址。定义一个指针变量时,指针变量类型要和数组类型保持一致,通过取值操作,就可以将"h"改为"H",这种方法称为指针法。获取数组元素时,也可以通过取下标的方式来获取数组元素并修改,这种方法称为下标法。

5. 动态指针与内存申请

平时我们定义的整型、浮点型、字符型变量、数组变量都是在栈空间中,而栈空间的大小在编译时确定的,不会改变。如果使用的空间大小不确定,就要使用堆空间。

堆和栈我们在后面的文章中会详细讲解。

#include <stdio.h>
#include <stdlib.h>  // malloc需要的头文件
#include <string.h>

int main() {
    int i;
    char *p;
    char q[50];   // 也可以写成char *q
    scanf("%d",&i);
    p=(char*) malloc(i); // 使用malloc动态申请堆空间,注意强制类型转换。堆空间刚申请下来是void类型,需要转换
    strcpy(p,"malloc success");
    strcpy(q,p);
    puts(p);
    free(p);   // free时必须使用malloc申请时返回的指针值,同时不能进行任何偏移
    puts(p);  // 可以看到释放完后是乱码
    puts(q);
    return 0;
}
F:\Computer\Project\practice\6\6.4-apply\cmake-build-debug\6_4_apply.exe
20
malloc success

malloc success

进程已结束,退出代码为 0

说明:在执行void *malloc(size_t size)时,需要给malloc传递的参数是一个整型变量。

当确定了用来存储什么类型的数据后,需要将void*强制转换为对应的类型。

同时需要注意指针本身的大小,和其指向空间的大小是两回事。

如图所示,定义的整型变量i,指针变量p均在main函数的栈空间中,通过malloc申请的空间会返回一个堆空间的首地址,我们把首地址存入变量p,知道了首地址,就可以通过strcpy函数往对应的空间存储字符数据。

堆的效率要比栈低得多。

栈空间由系统自动管理,而堆空间的申请和释放需要自行管理。

其传入的参数为void类型指针,任何指针均可自动转换为void*类型指针,所以p传递给free函数时,不需要强制类型转换。p的地址值必须是malloc当时返回的地址值,不能进行偏移等操作。

分类:

后端

标签:

C++

作者介绍

江小南
V1