指针,栈与堆(C语言)

一,指针的定义

      内存区域中的每字节都对应一个编号,这个编号就是“地址”。如果在程序中定义了一个变量,那么在对程序进行编译时,系统就会给这个变量分配内存单元。按变量地址存取变量值的方式称为“直接访问”,如 printf("%d",i);scanf("%d",&i);等;另一种存取变量值的方式称为“间接访问”,即将变量i的地址存放到另一个变量中。在 C 语言中,指针变量是一种特殊的变量,它用来存放变量地址。

定义格式如下:
基类型 *指针变量名;

例如,
int *pointer;

      指针与指针变量是两个概念,一个变量的地址称为该变量的“指针”,可以说,地址和指针是等价的。例如,地址 2000 是变量i的指针。如果有一个变量专门用来存放另一变量的地址 (即指针),那么称它为“指针变量”。int *pointer;中pointer就是一个指针变量。

      那么pointer 本身占多大的内存空间呢?编写的程序是 64 位应用程序,寻址范围为 64 位即8 字节,sizeof(i_pointer)=8。如果编写的程序是 32位,那么寻址范围就是 4 字节。

二,取地址操作符与取值操作符,指针本质

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

指针本质就是做间接访问

#include <stdio.h>
int main() {
    int i=5;
    //指针变量的初始化是某个变量取地址来赋值,不能随便写数字
    int *pointer=&i;//定义了一个指针变量,pointer是变量名
    printf("i=%d\n",i);//直接访问
    printf("*pointer=%d\n",*pointer);//间接访问
    return 0;
}

“ & ”和“ * ”两个运算符的优先级别相同,但要按自右向左的方向结合。因此,&* pointr与&i 相同,都表变量 i 的地址,也就是 pointer.

*&i 的含义是什么呢? 首先进行&i 运算,得到 i 的地址,再进行*运算,*&i 和*pointer 的作用是一样的,它们都等价于变量 i,即*&i 与i 等价

要声明三个指针变量,正确的语句如下:

int *a,*b,*c;

三,指针的传递使用场景

1.指针的传递

例:

#include <stdio.h>
void change(int j){//j是形参
    j=5;
}
int main() {
    int i=10;
    printf("before change i=%d\n",i);
    change(i);//C语言的函数调用值传递,实参赋值给形参,j=i
    printf("after change i=%d\n",i);
    return 0;
}

      我们会发现,i没有发生改变。例 1.1的原理图 如图1.1所示 程序执行过程就是内存的变化过程。我们需要关注的是栈空间的变化,当 main 函数开始执行时,系统会为 main 函数开函数栈空间,当程序走到int i时,main 函数的栈空间就会为变量i分配4 字节大小的空间,调用 change 函数时,系统会为 change 函数重新分配新的函数栈空间,并为形参变量j分配 4 字节大小的空间,在调用change(i)时,实际上是将i的值赋值给 j,我们把这种效果称为值传递 (C 语言的函数调用均为值传递)。因此,当我们在 change 函数的函数栈空间内修改变量 j的值后,change 函数执行结束,其栈空间就会释放,j就不再存在,i的值不会改变

那如何在子函数中修改 main 函数内的某个变量的值呢?如下代码:

#include <stdio.h>
void change(int *j){//j是形参
    *j=5;//*j等价于变量i,只是间接访问
}
int main() {
    int i=10;
    printf("before change i=%d\n",i);
    change(&i);//C语言的函数调用值传递,实参赋值给形参,j=&i
    printf("after change i=%d\n",i);
    return 0;
}

我们将变量i的地址传递给 change 函数时,实际效果是 j=&i,依然是值传递,只是这时我们的 j 是一个指针变量,内部存储的是变量 i 的地址所以通过* 就间接访问到了与变量 i 相同的区域,通过j*=5 就实现了对变量的值的改变,通过单步调试,我们依然可以看到变量j自身的地址是与变量i 的地址依然不相等。

函数调用的原理:值传递,将实参的值传递给形参

不同的函数有自己独有的栈空间

四,指针的偏移使用场景

1.指针的偏移

      在工作中,我们把对指针的加减称为指针的偏移,加就是向后偏移,减就是向前偏移。

#include <stdio.h>
//指针的偏移
#define N 5
int main() {
    int a[N]={1,2,3,4,5};//数组名内存储了数组的起始地址,a中存储的就是一个地址值
    int *p;//定义指针变量p
    p=a;
    int i;
    for(i=0;i<N;i++){
        printf("%3d",*(p+i));//a[i]
    }
    printf("\n---------------\n");
    p=&a[4];//指针变量p指向数组最后一个元素
    for(i=0;i<N;i++){
        printf("%3d",*(p-i));//a[i]
    }
    return 0;
}

偏移的长度是其基类型的长度,也就是偏移 sizeof(int),这样通过*(p+1)就可以得到元素 a[1],这里偏移4个字节。float *p;  p的加减也是偏移4个字节。

2.指针与一维数组

      为什么一维数组在函数调用进行传递时,它的长度子函数无法知道呢? 这是由于一维数组名中存储的是数组的首地址。如下例所示,数组名 c 中存储是一个起始地址 ,所以子函数 change 中其实传入了一个地址。定义一个指针变量时,指针变量的类型要和数组的数据类型保持一致,通过取值操作,就可将“h”改为“H”,这种方法称为指针法。获取数组元素时,也可以通过取下标的方式来获取数组元素并进行修改,这种方法称为下标法。

#include <stdio.h>
//指针与一位数组的传递
//数组名作为实参传递给子函数时,是弱化指针的
void change(char *d){//d[]
    *d='H';
    d[1]='E';//*(d+1)='E';
    *(d+2)='E';
}
int main() {
    char c[10]="hello";
    change(c);
    puts(c);
    return 0;
}

数组名传递给子函数时,是弱化为指针的。

 指针是一个地址,指针变量存储的是一个内存地址,我们只会对其做加减操作,访问其后面,或者前面空间的内容,不会对其做乘除操作。

加减的长度是其基类型的长度。

五,指针与malloc动态内存申请

      很多读者在学习 C 语言的数组后都会觉得数组长度固定很不方便,其实 C 语言的数组长度固定是因为其定义的整型、浮点型、字符型变量、数组变量都在栈空间中,而栈空间的大小在编译时是确定的。如果使用的空间大小不确定,那么就要使用堆空间。请看下面例子:动态内存申请

#include <stdio.h>
#include <stdlib.h>
int main() {
    int size;//size代表申请多大的字节空间
    char *p;//void*类型的指针不能偏移,因此不会定义无类型指针
    scanf("%d",&size);//输入要申请的空间大小
    //malloc返回的void*代表无类型指针
    p=(char*) malloc(size);
    p[0]='H';
    p[1]='O';
    p[2]='W';
    p[3]='\0';
    puts(p);
    return 0;
}

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
    int size;//size代表申请多大的字节空间
    char *p;//void*类型的指针不能偏移,因此不会定义无类型指针
    scanf("%d",&size);//输入要申请的空间大小
    //malloc返回的void*代表无类型指针
    p=(char*) malloc(size);
    strcpy(p,"hello");
    puts(p);
    free(p);//释放申请的空间,给的地址必须是最初malloc返回给我们的地址
    printf("free success\n");
    return 0;
}

      首先我们来看 malloc 函数。在执行#include <stdlib.h>    void*malloc(size_t size);时,需要给 malloc 传递的参数是一个整型变量,因为这里的 size_t 即为int;   返回值为void*类型的指针,void*类型的指针只能用来存储一个地址而不能进行偏移,因为 malloc 并不知道我们申请的空间用来存放什么类型的数据,所以确定要用来存储什么类型的数据后,都会将 void*强制转换为对应的类型。在例中我们用来存储字符,所以将其强制转换为 char*类型
      同时需要注意指针本身大小,和其指向的空间大小,是两码事,不能和前面的变量类比去理
解!

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

堆的效率要比栈低得多。栈空间由系统自动管理,而堆空间的申请和释放需要自行管理,所以在具体例子中需要通过 free 函数释放堆空间。其传入的参数为 void 类型指针,任何指针均可自动转为 void*类型指针,所以我们把 p 传递给free 函数时,不需要强制类型转换。p 的地址值必须是 malloc 当时返回的地址值,不能进行偏移,也就是在 malloc 和free 之间不能进行 p=p+1 等改变变量 p 的操作,原因是申请一段堆内存空间时,内核帮我们记录的是起始地址和大小,所以释放时内核用对应的首地址进行匹配,匹配不上时,进程就会崩溃。如果要偏移进而存储数据,那么可以定义两个指针变量来解决。

六,栈与堆的差异

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//函数栈空间释放后,函数内的所有局部变量消失
char* print_stack(){//低级错误
    char c[100]=" i am print_stack func";
    char *p;
    p=c;
    puts(p);//打印正确
    return p;
}
//char* print_stack(){
//    char c[17]="i am print_stack";
//    puts(c);
//    return c;
//}

//堆空间不会因函数执行结束而释放
char *print_malloc(){
    char *p=(char*) malloc(100);
    strcpy(p,"i am print malloc func");
    puts(p);
    return p;
}
int main() {
    char *p;
    p=print_stack();//数据放在栈空间
    puts(p);//打印出错
    //printf("p=%s\n",p);
    p=print_malloc();//数据放在堆空间
    puts(p);
    free(p);
    return 0;
}

       为什么第二次打印会有异常?原因是 print_stack()函数中的字符串存放在栈空间中,函数执行结束后,栈空间会被释放,字符数组 c 的原有空间已被分配给其他函数使用,因此在调用 print_stack()函数后,printf(“p=%s\n",p); 中的p 不能获取栈空间的数据。而 print_malloc()函数中的字符串存放在堆空间中,堆空间只有在执行 free 操作后才会释放,否则在进程执行过程中会一直有效。

  • 21
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值