【C语言督学训练营 第六天】对C语言的指针做到知根知底

前言

谭浩强老师曾在红皮书中谈到指针是C语言的灵魂,从这句话中就可以明白指针在C语言中的核心地位,C语言因为指针的存在无论是从效率、灵活性哪方面来说都是高级编程语言中的佼佼者,今天本篇博客将会介绍到指针的概念、使用指针常常出现的问题,指针的一些特性等方面入手,通透指针。

一、有关指针的基本概念

1.指针的定义

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

在这里插入图片描述
在这里插入图片描述

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=%dn",i);//直接访问
printf("*p=%dn"*p);//间接访问
return O;
}

看完间接访问,再通过以下一个例子加深一下对指针的印象吧!以下代码是通过手动将变量地址存进指针变量,然后通过指针变量将值放进原变量所在的地址。

//
// Created by Zhu Shichong on 2023/1/9.
//
#include <stdio.h>
#include<stdlib.h>
void change(int *a){
    
    
    (*a)=*a/2;

}
int main() {
    
    
    int a;
    int* p;
    printf("%d\n",&a);
    scanf("%d",&p);
    scanf("%d",p);
    printf("%d\n",a);
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、指针的特性

1.传递特性使用场景

我们首先来看一个例子。在本例的主函数中,定义了整型变量i,其值初始化为10,然后通过子函数修改整型变量i的值.但是,我们发现执行语句printf(" after change i=%d\n", i);后,打印的i的值仍为10,子函数change并未改变变量i的值.下面我们通过执行程序来查看为什么会出现这种情况。
在这里插入图片描述
调试程序可以发现,change函数中的j与main函数中的i的地址并不是同一个地址,也就可想而知j的值发生改变i的值不会发生改变,如果两个变量的地址是同一个地址,那么两者将会同时改变。通过指针变量可以轻松实现这个需求。除此之外,函数中的变量会随着函数调用而初始化,函数结束而销毁。main函数会最后结束,main函数中定义的变量并不会随着其余函数调用完毕而销毁,其余函数可以通过指针改变主函数中变量的值!
在这里插入图片描述

2.偏移特性使用场景

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

对于数组而言,数组的变量名存储的为数组首地址,也就是通过数组名便可以找到数组的首地址继而找到整个数组,正如下图所示:a=&a。数组在作为函数参数传递时,传递过去的也是首地址,这也是为什么函数不知道数组类型参数长度的原因。在作用上函数参数写法 *p与p[]等价,但*p更简便较为常用!
在这里插入图片描述
注意:指针的偏移量是基类型的长度!实际上p就相当于p[0] 实际上(p+1)就相当于p[1] 以此类推。

三、动态内存申请原理剖析

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

1. malloc的使用

#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 o;
}

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

需要注意指针本身大小,和其指向的空间大小,是两码事,不能和前面的变量类比去理解!

在这里插入图片描述
既然都是内存空间,为什么还要分栈空间和堆空间呢?

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

栈空间由系统自动管理,而堆空间的申请和释放需要自行管理,所以在具体例子中需要通过free函数释放堆空间。free函数的头文件及格式为:
在这里插入图片描述
在这里插入图片描述

2.栈与堆比较

//
// Created by Zhu Shichong on 2023/1/9.
//
#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_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);
    p=print_malloc();
    puts(p);
    free(p);//只有free时,堆空间才会释放
    return 0;
}

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

四、C++引用与C语言的关系

int a;
void modifynum(int &b)l
	b=b+1;
}
调用:modifynum(a)

int *p=NULL;
void modify_pointer(int *&p)
	p=q;
}
调用: modify_pointer(p)

如上面两个例子所示,我们在修改函数外的某一变量时,使用了引用后,在子函数内的操作和函数外操作手法一致,这样编程效率较高,对于初学者理解也非常方便,王道数据结构书籍中均采用了这种手法。下面两段代码在作用上效果是一样的,值得注意的是,C++引用修改的就是变量本身(相当于对同一地址起别名),而C语言指针传递相当于通过另外一个变量,去寻找要修改的位置!

#include <stdio.h>
void modify_num(int &b){
    
    
	b=b+1;
}
int main() {
    
    
	int a=10;
	modify_num(a);
	printf("after modilfy_num a=%d\n",a);
	return 0;
}

#include <stdio.h>
void modify_num(int *b){
    
    
	*b=*b+1;
}
int main(){
    
    
	int a=10;
	modify_num(&a);
	printf("after modify_num a=%d\n",a);
	return 0;
}


只有在了解最根本的原理后,才不会碰到728的问题。今天有关指针上的问题最主要的就是要记住栈堆原理上差异带来的潜在性问题,还有就是指针的传递与偏移。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/apple_51931783/article/details/129011638