江小南

V1

2022/11/22阅读:30主题:萌绿

【408篇】C语言笔记-第六章(指针)

第一节:指针的本质

1. 指针的定义

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

间接访问:将变量i的地址存放到另一个变量中,在C语言中,指针变量是一种特殊的变量,它用来存放变量地址。

指针变量的定义格式:

基类型 *指针变量名;

例如:int *i_pointer;

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

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

64位应用程序指针寻址范围是8个字节。32位应用程序寻址范围是4个字节

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...

第二节:指针的传递

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

1. 指针的传递

#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的值了。

注意:*j=5才是对i的修改,如果写成j=5,那只是对j的修改。因为j和i对应的内存空间不同,只有*j才能取到i的空间地址。

第三节:指针的偏移

1. 指针的偏移

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

#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]的值。如下图:

2. 指针与一维数组

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

#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",这种方法称为指针法。获取数组元素时,也可以通过取下标的方式来获取数组元素并修改,这种方法称为下标法。

第四节:动态指针与内存申请

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

#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

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

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

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

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

堆的效率要比栈低得多

栈空间由系统自动管理,而堆空间的申请和释放需要自行管理。所以需要通过free函数释放堆空间,free函数的头文件及格式为:

#include <stdlib.h>

void free(void *ptr);

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

2. 堆空间和栈空间的差异

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

charprint_stack(){
    char c[17]="I am print_stack";
    puts(c);
    return c;
}
charprint_malloc(){
    char *p;
    p=(char*)malloc(20);
    strcpy(p,"I am print_malloc");
    puts(p);
    return p;
}
int main() {
    char *p;
    p=print_stack();
    printf("p=%s\n",p);  // 打印出来了null,因为栈空间被释放了
    p=print_malloc();
    print_malloc("p=%s\n",p);   // 打印出了实际的数据,因为堆空间不会被释放,除非进程结束
    return 0;
}
F:\Computer\Project\practice\6\6.4-heap_stack\cmake-build-debug\6_4_heap_stack.exe
I am print_stack
p=(null)
I am print_malloc
I am print_malloc

进程已结束,退出代码为 

解析:函数执行结束后,栈空间会被释放,而堆空间不会被释放,除非进行free操作。

分类:

后端

标签:

C#

作者介绍

江小南
V1