data/method/一些思考/工作材料/072—指针(下)——指针高级.txt

314 lines
15 KiB
Plaintext
Raw Normal View History

2024-01-29 10:44:43 +08:00
一、C语言中的内存管理
C语言程序在编译后需要载入内存中才能开始运行。内存中对于数据的划分不是随机的而是根据这个数据的性质分段进行划分的。某段内存区域只会存储相应的数据。
具体来说C语言对于内存空间的划分可以分为以下几个区域
1代码区这段区域主要用来存储编译后的函数体的二进制代码以及会用到的字符串常量。该区域是只读的。
2数据区这个区域主要存储已初始化的全局变量、静态变量、一般常量。
3BSS区这个区域主要存储未初始化的全局变量、静态变量。
4堆区由程序员手动申请、手动释放回收。若程序员不手动释放则在程序结束后由操作系统回收。所对应的函数是malloc()、calloc()、free()等。
5栈区由系统自动分配、自动释放回收存放函数的参数值、局部变量等。
6命令行参数区存放环境变量等例如main()函数的传递的参数值。
其中堆区与栈区的内存是在程序执行时由系统分配的。当该程序需要分配内存时才会分配不需要时不会分配或者分配后直接回收。而BSS区、数据区、代码区是在程序执行开始阶段就由编译器分配内存这三个区域的内存在程序运行时会一直存在不会被临时回收。
示例:程序中列举了常见的数据及数据所在的存储区域。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int a=0;//全局变量初始化区
char *p1;//全局变量未初始化区
int main()
{
int b;//栈区
char s[]="123456";//s在栈区
char *p2;//栈区
char *p3="123456";//p3在栈区"123456"在代码区
static int c = 0;//data区
p1=(char*)malloc(10);//堆区
p2=(char*)malloc(10);//堆区
printf("a address is %p\n",&a);
printf("p1 address is %p\n",&p1);
printf("b address is %p\n",&b);
printf("s address is %p\n",s);
printf("p2 address is %p\n",&p2);
printf("123456 address is %p\n",p3);
printf("123456 address is %p\n",&"123456");
printf("c address is %p\n",&c);
printf("malloc1 address is %p\n",p1);
printf("malloc2 address is %p\n",p2);
return 0;
}
二、野指针与空指针
1、野指针
指针变量在定义的时候若未初始化或指向某对象则该指针所存的地址值并不是空NULL而是一个随机值。这时这个指针指向一段未知内存这样的指针就称为“野指针”。
野指针产生的原因主要有两个:
1.定义指针变量时未初始化或之后未指向正确的对象
2.指针释放后指针未及时置空
示例:
#include<stdio.h>
int main()
{
int *p;
*p = 100;//p此时是野指针对野指针进行操作非法
return 0;
}
虽然程序编译没有问题但运行时会报错Segmentation fault (core dumped)
Segmentation fault (core dumped):中文翻译为“段错误”。发生段错误的原因是程序对内存进行了非法操作,系统为了保护内存而强制终止了程序的运行。
发生段错误的原因很多,主要有:数组越界、修改只读内存、操作未知内存(操作野指针)等。
野指针无法通过语法检查而查出错误,只能通过编程人员的自身素养规避。初学者要十分小心使用指针变量,尤其不要出现野指针。
2、空指针
在C语言中有宏定义“NULL”代表“空”。若指针的值为NULL则表示该指针未指向任何内存。
int *p = NULL;//指针置空
初学者要养成良好习惯:若定义指针后暂时不指向任何对象,应该在定义时就把该指针置空防止出现野指针。
我们可以通过判断语句来判断一个指针是否是空指针:
if(p == NULL)
{
……//对指针p进行操作
}
三、返回指针值的函数(指针函数)
一个函数既可以返回int类型、char类型等也可以返回一个指针类型数据即返回一个地址。返回指针值的函数称为“指针函数”。
指针函数的定义形式为:
类型名 *函数名(参数列表)
例如:
int *a(int x,int y)
char *strcpy(char *s1,const char *s2)
示例有3个学生每个学生有4门课成绩存放在一个二维数组中。要求用户输入学生的序号1或2或3之后能够输出这个学生的4门成绩。使用指针函数实现。
#include<stdio.h>
int *search(int(*p)[4],int n)
{
return *(p+n);//注意返回的是int*型而不是int型
}
int main()
{
int score[3][4]={{60,70,80,90},{56,89,67,82},{78,90,66,100}};
int n,i;
printf("查询第几个学生的成绩1~3\n");
scanf("%d",&n);
if(n!=1 && n!=2 && n!=3)
{
printf("输入错误!\n");
return 0;
}
n--;
int *a = search(score,n);
for(i=0;i<4;i++)
{
printf("%d ",a[i]);
}
printf("\n");
return 0;
}
注意:当指针作为函数的返回值的时候,主调函数需要考虑指针指向的数据是否已经被回收。由于被调函数的局部变量存储在栈区,因此当被调函数执行完毕后系统会回收该段内存空间,这样被调函数内数据就会丢失。例如:
char *mem()
{
char p[]="Hello World";
return p;//返回字符数组首地址
}
int main()
{
char *str=mem();
printf("%s\n",str);
return 0;
}
运行程序会发现我们不会得到"Hello World"而是会输出乱码。
思考:怎样修改被调函数,使得主调函数可以输出"Hello World"
四、指向函数的指针(函数指针)
在程序中定义的函数在编译时系统也会对函数代码分配一段存储空间,这段存储空间的起始地址(又称“入口地址”)称为这个函数的指针。
那么我们就可以定义一个指向这个函数入口地址的指针变量用于存储这个函数的入口地址,就意味着该指针指向该函数。
1、定义函数指针
函数指针的定义形式为:
<类型名> (*<指针变量名>)(参数列表)//参数列表只写形参类型即可,不必写形参名
注意:函数指针与指针函数的定义主要区别就在于是否用()把函数名括起来,请注意区分。
例如:
int (*p)(int,int);
这个指针变量p的类型是int (*)(int,int)表示这是一个指向有两个int型参数、返回值为int型的函数的指针。
2、使用函数指针调用函数
示例写一个函数求出2个数中较大值。调用函数用函数指针实现。
#include<stdio.h>
int max(int x,int y)
{
return x>y?x:y;
}
int main()
{
int (*p)(int,int);
p = max;
int a,b;
printf("请输入2个整数");
scanf("%d%d",&a,&b);
printf("较大值是%d\n",(*p)(a,b));
return 0;
}
注意:
1.定义了一个函数指针但不代表该函数指针可以指向任何类型的函数。示例程序中的指针p只能指向参数是2个int型、返回值是int型的函数。不能指向非这种类型的函数。
2.使用函数指针调用函数前,一定要先将该指针变量指向该函数。
3.在给函数指针赋值时,只需函数名即可,千万不要写成函数调用。
p = max;//正确
p = max(a,b);//错误
4.使用函数指针时,只需用*<指针变量名>来代替普通函数调用即可。
5.函数指针不能进行算数运算。如p+1、p-1、p++、p--等都是非法的。
练习写两个函数一个求2个数中较大值另一个求2个数中较小值。用户选择1或2输入1代表调用较大值函数输入2代表调用较小值函数。调用函数用函数指针实现。
答案:
#include<stdio.h>
int max(int x,int y)
{
return x>y?x:y;
}
int min(int x,int y)
{
return x<y?x:y;
}
int main()
{
int (*p)(int,int);
int a,b,choice;
printf("请输入2个整数");
scanf("%d%d",&a,&b);
do{
printf("请选择1或者2");
scanf("%d",&choice);
if(choice != 1 && choice != 2)
{
printf("输入错误,请重新输入!\n");
}
}while(choice != 1 && choice != 2);
if(choice==1)
p = max;
else if(choice==2)
p = min;
printf("%d\n",(*p)(a,b));
return 0;
}
由练习题可以看出,如果有多个函数参数列表相同、返回值相同、函数功能相近,则我们可以使用函数指针来调用不同的函数,这样更加方便灵活。
五、const关键字修饰指针变量
在C语言中常用const关键字修饰一个变量或函数表示“不可修改”的意思。指针变量同样也是变量因此也可以使用const关键字修饰。
const关键字修饰指针变量有3种用法在使用时要注意区分。
1、const <数据类型> *<指针变量名>
例如const char *src;
说明const在数据类型前表示限制该指针指向的对象的内容不可修改。这样可以保护指针指向的数据。
在C库函数中涉及到数组等操作尤其在字符串操作函数中常把形参设为const类型以保护数据。如
char *strcpy(char *s1,const char *s2);
strcpy()函数意为将字符数组或字符串s2的内容复制到字符数组s1中。s2指向的对象需要数据保护即不可修改因此使用const关键字加以保护。
2、<数据类型> *const <指针变量名>
例如int *const a;
说明const在数据类型名后、指针变量名前使用表示限制该指针指向的位置唯一即不可修改该指针指向的对象。但仍可以使用*<指针变量名>来修改指向的对象的内容。
3、const <数据类型> *const <指针变量名>
例如const int *const a;
const既在前又在后表示既不可修改指针的值即不可修改该指针指向的对象也不可修改所指向对象的内容。
六、指针数组和多级指针
1、指针数组
如果有一个数组其所有元素均为指针类型,则这个数组称为“指针数组”。也就是说,指针数组的每个元素都是一个指针变量。
1定义一个指针数组
定义一维指针数组的一般形式为
<类型名> *<数组名>[数组长度]
例如int *p[4];
注意不要写成int (*p)[4];
2指针数组的应用
示例:
#include<stdio.h>
#include<string.h>
void print_string(char *name[],int n)
{
int i;
for(i=0;i<n;i++)
{
printf("%s\n",name[i]);
}
}
int main()
{
char *name[]={"Follow me","C language","Flowers","Basic","Great Wall"};
int n=5;
print_string(name,n);
return 0;
}
2、指针数组在main()函数中作为形参
我们平时书写的main()函数一般为:
int main()
实际上这是省略了main函数形参的写法。main()函数的完整写法是:
int main(int argc,const char *argv[])
其中argc和argv就是main()函数的参数,他们是程序的“命令行参数”。
argc(argument count):参数个数
argv(argument vector):参数向量表
例如如果我们执行a.out
./a.out
则这个就是“命令行参数”可以传递到main()函数中。此时argc=1argv[0]="./a.out"
这样我们可以在执行程序时携带参数。例如我们在执行main()函数时携带参数:
./a.out 1 2 3
则此时argc=4argv[0]="./a.out"argv[1]="1"argv[2]="2"argv[3]="3"
这时我们就可以直接通过访问argv[]数组获得携带的参数。
3、多级指针
如果一个指针变量存储的也是一个指针变量的地址,则可以视为该指针也指向了另一个指针,即“指向指针的指针”,简称为二级指针。同理还有三级指针、四级指针等。统称为多级指针。
定义一个二级指针:
int **p;//等价于int *(*p);
int *q;
p = &q;//将二级指针指向一级指针
我们之前学习过的“指向二维数组的指针”和“指针数组”都可以看做是一种二级指针。多级指针并不经常使用。
七、void类型指针和malloc函数的用法
1、空类型指针void*
一般情况下指针的数据类型取决于该指针指向的对象的数据类型例如int*。但是有些情况下我们暂时无法确定指针所指向数据的类型此时我们可以使用空类型指针void*来暂时表示指针的类型。
空类型指针void*表示该指针所指向的数据类型不确定。但是void*类型指针无法直接使用必须通过强制类型转换将void*类型指针转换成指定类型的指针。例如:
void *a;//空类型指针
int *b = (int*)a;//强制类型转换成int*型
char *c = (char*)a;//强制类型转换成char*型
2、动态内存分配函数malloc()和free()
malloc函数是memory allocation的缩写中文译为“动态内存分配”。当我们需要动态地申请一块内存空间的时候我们可以使用malloc()函数在内存中开辟一块空间。
函数原型void* malloc(unsigned int size);
参数需要申请的内存大小单位字节注意size是unsigned int类型即没有负数。malloc(-1)这种用法是非法的)
返回值void*型指针成功或NULL失败
说明:
程序是在内存中运行的。其中栈区是系统负责开辟和回收的而堆区是我们程序开发人员手动申请与回收的。例如函数调用时系统在栈区开辟空间将局部变量等数据存放进这块区域。当该函数调用完毕系统将该区域回收。这也是函数的局部变量在该函数调用完毕后无法再次使用的原因。而堆区不是系统负责的是由程序开发人员负责开辟和回收的。堆区是预留给程序开发人员来使用的存储数据的区域。malloc()函数就是在堆区开辟一块内存空间。如果我们使用了堆区的变量,即使函数调用完毕,堆区的变量仍存在。申请堆区的变量并使用完毕后要记得手动释放,否则可能造成内存空间不足等情况。
释放一块内存使用free()函数
函数原型void free(void *p)
参数:需要释放的地址
返回值:无
malloc()函数和free()函数在头文件stdlib.h中使用前不要忘记包含头文件。
示例使用malloc()函数申请一块空间存放一个数组,并在使用完毕后回收这块区域
#include<stdio.h>
#include<stdlib.h>
#include<memory.h>//malloc函数头文件
#define MAX 10
int *get_memory(unsigned int a)
{
int *p = (int*)malloc(a);
if(p==NULL)//申请失败
{
printf("申请空间失败");
return NULL;
}
return p;
}
int main()
{
int *a = get_memory(MAX*sizeof(int));
if(!a)
{
printf("申请空间失败!\n");
return 0;
}
int i;
for(i=0;i<MAX;i++)
{
a[i]=i;
}
for(i=0;i<MAX;i++)
{
printf("%d ",a[i]);
}
printf("\n");
free(a);//释放这块内存空间
a = NULL;//及时将指针置空防止出现野指针
return 0;
}
在示例中虽然get_memory()函数在执行完毕后被系统回收但它申请的空间在堆区并没有被系统回收因此在main()函数中仍可使用这块空间。
注意在使用完毕后一定要free()这块空间。之后需要对指针置空避免出现野指针。
注意free()函数释放一个指针不可连续释放多次。如果把示例程序的后几行改成:
free(a);
free(a);//多加一行free(a)
……
则程序运行会崩溃。请思考为什么?
答案虽然调用free()函数将a指向的内存空间释放掉了但指针a并不是空指针此时指针a属于野指针存放了一块未知内存地址。如果第二次free()的话则会释放这块未知内存地址的内存空间,造成崩溃。