【C语言】C语言期末突击/考研--指针(一篇就够)

目录

一、指针的本质(间接访问原理)

1.1.指针的定义

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

二、指针的传递使用场景 

2.1.什么是指针的传递

2.2.指针的传递使用场景

三、指针的偏移使用场景

3.1.指针的偏移

3.2.指针与一维数组

四、指针与malloc动态内存申请,栈与堆的差异

4.1.指针与动态内存申请

4.2.栈空间与堆空间的差异

五、练习题

1.1~1.2

2.1~2.2

3.1~3.2

4.1~4.2


一、指针的本质(间接访问原理)

1.1.指针的定义

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

基类型*指针变量名;

例如:

int *i pointer;

    指针与指针变量是两个概念,一个变量的地址称为该变量的“指针”。例如,地址2000是变量i的指针。如果有一个变量专门用来存放另一变量的地址(即指针),那么称它为“ 指针变量"。例如,下图中的i_ pointer就是-一个指针变量:

    那么i_pointer本身占多大的内存空间呢?本文中编写的程序都是64位应用程序,寻址范围为64位即8字节,所以对于本文来说sizeof(i_ pointer)=8.。如果编写的程序是32位,那么寻址范围图指针变量就是4字节(考研中往往会强调程序是32位的程序)。

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

    取地址操作符为&,也称引用,通过该操作符我们可以获取一个变量的地址值;取值操作符为*,也称解引用,通过该操作符我们可以得到一个地址对应的数据。如下例所示,我们通过&i获取整型变量i的地址值,然后对整型指针变量p进行初始化,p中存储的是整型变量i的地址值,所以通*p(printf 函数中的*p)就可以获取整型变量i的值。p中存储的是一个绝对地址值,那为什么取值时会获取4字节大小的空间呢?这是因为p为整型变量指针,每个int型数据占用4字节大小的空间,所以p在解引用时会访问4字节大小的空间,同时以整型值对内存进行解析。
【例】取地址与取值(引用与解引用)

#include <stdio.h>
//&符号是取地址,指针变量的初始化一定 是某个变量取地址
int main(){
    int i= 5;
    int* p=&i;
    printf("i=%d\n",i);//直接访问
    printf("*p=%d\n", *p);//间接访问
    return 0;
}

读者需要注意以下4点:
(1)指针变量前面的“*”表示该变量为指针型变量。例如:

float *pointer_1;

注意指针变量名是pointer_1,而不是*pointer_1。
(2)在定义指针变量时必须指定其类型。需要注意的是,只有整型变量的地址才能放到指向整型变量的指针变量中。例如,下 面的赋值是错误的:

float a;
int *pointer_1;
pointer_1=&a; //毫 无意义而且会出错,有兴趣的读者可以自行尝试

(3)如果已执行了语句

pointer_ 1=&a;

那么&* pointer_1的含义是什么呢?
“&”和“*”两个运算符的优先级别相同,但要按自右向左的方向结合。因此, &* pointer_1与&a相同,都表示变量a的地址,也就是pointer_1。
*&a的含义是什么呢?
首先进行&a运算,得到a的地址,再进行*运算。*&a 和*pointer_1的作用是一样的,它们都等价于变量a,即*&a 与a等价。
(4) C语言本质上是一种自由形式的语言,这很容易诱使我们把“*”写在靠近类型的一侧,如int*a这个声明与前面一个声明具有相同的意思,而看上去更清晰,a被声明成类型为int*的指针。但是,这并不是一个好习惯,因为类似int *a,b,c 的语句会使人们很自然地认为这条语句把所有三个变量声明为指向整型的指针,但事实上并非如此,“*” 实际上是*a的一部分,只对a标识符起作用,但其余两个变量只是普通的整型变量.要声明三个指针变量,正确的语句如下:

int *a,*b,*c;

二、指针的传递使用场景 

2.1.什么是指针的传递

    很多初学者不喜欢使用指针,觉得使用指针容易出错,其实这只是因为没有掌握指针的使用场景.经过多年的实战经验总结,指针的使用场景通常只有两个,即传递与偏移,读者应时刻记住只有在这两种场景下使用指针,才能准确地使用指针。多加练习之后,我们会发现指针其实很简单。

2.2.指针的传递使用场景

    我们首先来看本例的主函数中,定义了整型变量i,其值初始化为10,然后通过子函数修改整型变量i的值。但是,我们发现执行语句printf("after change i=%d\n" ,i);后,打印的i的值仍为10,子函数change并未改变变量i的值.下面我们通过执行程序来查看为什么会出现这种情况.
【例】指针的传递使用场景:

#include <stdio.h>

void change(int j){
    j=5;
}
int main() {
    int i = 10;
    printf("before change i=%d\n", i);//这里打断点
    change(i);//在这一步按下箭头,进入change函数
    printf("after change i=%d\n", i);
    return 0;
}

    上例中代码提示位置打断点,然后调试程序,在内存视图中输人&i,可以看到变量i的地址是 0x61fe1c。按向下键(或 F7)进入change函数,这时变量j的值的确为10,但是&j 的值为0x61fdf0,也就是j和i的地址并不相同。运行j=5后, change函数实际修改的是地址0x61fdf0上的值,从10变成了5,接着change函数执行结束,变量i的值肯定不会发生改变,因为变量i的地址是0x61fe1c而非0x61fdf0(由于程序每次重新编译,因此大家那里的地址和我这里是不一样的,这个没关系,关键是观察i和j的地址不一样)。
上例的原理图如下图所示。程序的执行过程其实就是内存的变化过程,我们需要关注的是栈空间的变化。当main函数开始执行时,系统会为main函数开辟函数栈空间,当程序走到int i时,main 函数的栈空间就会为变量i分配4字节大小的空间。调用change函数时,系统会为change函数重新分配新的函数栈空间,并为形参变量j分配4字节大小的空间。在调用change(i)时,实际上是将i的值赋值给j,我们把这种效果称为值传递(C语言的函数调用均为值传递)。因此,当我们在change函数的函数栈空间内修改变量j的值后,change 函数执行结束,其栈空间就会释放,j 就不再存在,i 的值不会改变。

    有的读者会想,难道就不能在子函数中修改main函数内的某个变量的值?答案是可以的,我们将程序进行了如下例所示的修改。
【例】在子函数中修改main函数中某个变量的值:

#include <stdio.h>
void change(int* j)
{
    *j=5; //间接访问得到变量 i
}
//指针的传递
int main(){
    int i=10;
    printf("before change i=%d\n",i);
    change(&i); //传递变量i的地址
    printf("after change i=%d\n",i);
return 0;
}

    我们可以看到程序执行后,语句printf("after change i=%d\n",i);打印的i的值为5,难道C语言函数调用值传递的原理变了?并非如此,我们将变量i的地址传递给change函数时,实际效果是j=&i,依然是值传递,只是这时我们的j是一个指针变量,内部存储的是变量i的地址,所以通过*j就间接访问到了与变量i相同的区域,通过*j=5就实现了对变量i的值的改变。通过单步调试,我们依然可以看到变量j自身的地址是与变量i的地址依然不相等。

三、指针的偏移使用场景

3.1.指针的偏移

    上面介绍了指针的传递。指针即地址,就像我们找到了一栋楼,这栋楼的楼号是B,那么往前就是A,往后就是C,所以应用指针的另-一个场景就是对其进行加减,但对指针进行乘除是没有意义的,就像家庭地址乘以5没有意义那样.在工作中,我们把对指针的加减称为指针的偏移,加就是向后偏移,减就是向前偏移。下面我们来看下面的例子。
【例】指针的偏移使用场景:

#include <stdio.h>
#define N 5
//指针的偏移
int main() {
    int a[N] = {1, 2, 3, 4, 5};
    int *p;
    int i;
    p = a; //保证等号两边的数值类型一致
    for (i = 0; i < N; i++) //正序输出
    {
        printf("%3d", *(p + i));
    }
    printf("\n---------------\n");
    p = &a[4]; //让 p指向最后一一个元素
    for (i = 0; i < N; i++) //逆序输出
    {
        printf(" %3d", *(p - i));
        printf("\n");
        return 0;
    }
}

     如下图所示,数组名中存储着数组的起始地址0x61fdf0,其类型为整型指针,所以可以将其赋值给整型指针变量p,可以从监视窗口中看到p+1的值为0x61fdf4.那么为什么加1后不是0x61fdf1呢?因为指针变量加1后,偏移的长度是其基类型的长度,也就是偏移sizeof(int),这样通过*(p+1)就可以得到元素a[1]。编译器在编译时,数组取下标的操作正是转换为指针偏移来完成。

3.2.指针与一维数组

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

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

四、指针与malloc动态内存申请,栈与堆的差异

4.1.指针与动态内存申请

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
    int i;
    char *p;
    scanf("%d",&i); //输入要申请的空间大小
    p=(char*)malloc(i); //使用malloc动态申请堆空间
    strcpy(p,"malloc success");
    puts(p);
    free(p); //free时必须使用malloc申请时返回的指针值,不能进行任何偏移
    printf("free success\n");
    return 0;
}

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

    既然都是内存空间,为什么还要分栈空间和堆空间呢?栈是计算机系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈操作、出栈操作都有专门的指令执行,这就决定了栈的效率比较高;堆则是C/C++函数库提供的数据结构,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法请参考关于数据结构、操作系统的书籍)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能由于内存碎片太多),那么就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后返回.显然,堆的效率要比栈低得多。 (这段了解即可)
栈空间由系统自动管理,而堆空间的申请和释放需要自行管理,所以在具体例子中需要通过free函数释放堆空间. free 函数的头文件及格式为:

#include <stdlib.h>
void free(void *ptr);

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

下面我们来了解栈空间和堆空间在时间有效性上的差异。

4.2.栈空间与堆空间的差异

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//函数栈空间释放后,函数内的所 有局部变量消失
char* print_stack()
{
    char c[17]="I am print_stack";
    puts(c);
    return c;
}
//堆空间不会因函数执行结束而释放
char* print_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);
    p=print_malloc(); //数据放在堆 空间
    puts(p);
    return 0;
}

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

五、练习题

1.1~1.2

1、prit("%d",);. scanf("%d" ,&i);是直接访问
A正确B错误
答案: A
解释:直接访问是指我们通过变量名访问变量值,或者直接拿到变量的地址空间进行访问
2、int i= 5;int* p=&i;print("*p= -%dn", *p);这样去访问i的值的方式是间接访问
A正确B错误
答案: A
解释:我们先获取指针变量p的值,*p 相当于通过p内部所存的地址值,对所在地址进行
取值操作,因此*p 是间接访问
3、“&”和“*”两个运算符的优先级别相同,但要按自右向左的方向结合,&是取值,*
是取地址
A正确B错误
答案: B
解释:“&” 和“*”两个运算符的优先级别相同是正确的,但是&是取地址,*代表取值,
这两个的意义一定要记清楚
4、int*a,b,c定义了 3个整型指针变量,分别是a,b,c .
A正确B错误
答案: B
解释: a是指针变量, b和c是整型变量,如果要定义3个指针变量,方法是int *a,*b,*c; .
5、int a;如果我们编写*&a这样的表达式没有意义,因为其与a等价的
A正确B错误
答案: A
解释: *也就是取值运算符,和&也就是取地址运算符,优先级别相同自右向左的方向结合,
因此*&a这样的表达式没有意义,与a等价

2.1~2.2

1、main函数和change函数有各自的函数栈空间
        A正确        B错误
答案: A
解释:不同的函数有自己独有的栈空间


2、函数调用是值传递,将实参的值传递给形参
        A正确        B错误
答案: A
解释:这个非常重要,需要记住、因为理解这个, 才能明白我们视频中的代码实例的原理,同时研究生复试时,导师可能提问函数调用的原理


3、虽然函数调用是值传递,但是我们可以通过指针间接访问的原理,来实现子函数中改变
main函数中某个变量的值
        A正确        B错误
答案: A
解释:我们将变量i的地址传递给change函数时,实际效果是j=&i,依然是值传递,只是这时我们的j是一个指针变量,内部存储的是变量i的地址,所以通过*j就间接访问到了与变量i相同的区域,通过*j=5就实现了对变量i的值的改变

3.1~3.2

1、指针做加碱就是指指针的偏移
        A正确        B错误
答案: A
解释:指针是一个地址,我们会对其做加减运算


2、我们会对指针变量进行乘除,得到一个新地址,来使用
        A正确        B错误
答案: B
解释:指针变量存储的是一个内存地址,我们只会对其做加减操作,访问其后面,或者前面空间的内容,不会对其做乘除操作


3、int a[N]={1,2,3,4,5};int *p;p a;对于这个操作,我们如果进行p: p+1, p的值增大了一个字节
        A正确        B错误
答案: B
解释:对于指针变量做加法运算时,每次加1,增加的长度是其基类型的长度,因为基类型是int,
所以p的值增大了4个字节。

4、数组名传递给子函数时,是弱化为指针的
        A正确        B错误
答案: A
解释:C语言的子函数形参位置,并没有数组变量的设计,我们]把数组名实参传递给子函数时,形参接收到的是数组名内存储的数组的起始地址

4.1~4.2

1、p=(char* )malloc(20);代表在堆空间上,申请20个字节,由于malloc返回的是void*类型指针,我们要往内部存储字符串,所以强转为char*
        A正确        B错误
答案: A
解释: malloc 是盱申请堆内存空间的,非常重要,一定要掌握, 我们给malloc 传递的值为多大,就是申请多少字节


2、malloc 申请的空间,如果我们不使用时,-定要free,否则会一直占用内存空间,直到进程结束
        A正确        B错误
答案: A
解释:这个需要记住,因为在中级阶段,数据结构部分,我们不用的空间一定要free,避免
初试扣分


3、子函数结束后,其栈空间会被释放
        A正确        B错误
答案:A
解释: 函数执行结束后,其函数栈空间会被全部释放

下一期:

【C语言】C语言期末突击/考研--函数

  • 20
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值