指针
- C语言
- 2024-03-25
- 4193热度
- 0评论
指针
指针的定义
指针是一种特殊的数据类型,用于存储内存地址。指针可以指向任何数据类型(如整数、字符、数组、结构体等),并允许对该内存地址进行间接访问,从而操作或获取存储在该地址处的数据。按变量地址存取变量值的方式称为"直接访问",如printf("%d",i);、scanf("%d",&i);等,另一种存取变量值的方式称为"简介访问",即将变量i的地址存放到另一个变量中(指针)。 指针变量的定义格式如下:基本类型 *指针变量名
举例如下:
int *ptr; // 声明一个指向整数类型的指针变量
char *ptr_char; // 声明一个指向字符类型的指针变量
float *ptr_float; // 声明一个指向浮点数类型的指针变量
int num = 10;
int *ptr_num = # // 声明并初始化一个指向整数 num 的指针
ptr = NULL; // 将指针 ptr 初始化为 NULL
在C语言中,指针和指针变量是密切相关但又不同的概念:
- 指针(Pointer):
- 指针是一种特殊的数据类型,用于存储内存地址。
- 指针本身并不存储实际的数据,而是存储一个地址,该地址指向内存中的某个位置,该位置存储着真正的数据。
- 通过指针可以实现对所指向内存位置的间接访问,可以读取或修改该位置存储的数据。
- 指针变量(Pointer Variable):
- 指针变量是声明为指针类型的变量,用于存储指针。
- 它是存储指针值的具体容器,即指针变量中存储的是一个内存地址,这个地址指向某个数据的存储位置。
- 指针变量本身也占用内存空间,通常是根据指针类型的大小来确定的。
取地址操作符和取值操作符(指针本质)
取地址操作符位&,也称引用,通过该操作符可以获取到一个变量的地址值;取值操作符为*,也称解引用,通过该操作符可以得到地址值对应的数据。#include <stdio.h>
int main(){
int i=5;
int *p=&i;
printf("i=%d\n",i); //直接访问
printf("*p=%d\n",*p); //间接访问
return 0;
}
读者需要注意:
1、指针变量前面的"*"表示该变量为指针型变量,而指针变量名是"p",不是"*p"。
2、在定义指针变量时必须指定其类型,只有整型变量的地址才能放到指向整型变量的指针变量中。
#include <stdio.h>
int main(){
float a;
int *pointer_1;
pointer_1=&a;
printf("%c",pointer_1);
return 0;
}
上述例子会提示错误:[Error] cannot convert 'float' to 'int' in assignment(无法将类型转换)
&*pointer_1语句的含义:"&"和"*"两个运算符的优先级别相同,但是要按照自右向左的方向结合,因此&*pointer_1与&a相同,都表示变量a的地址。
*&a语句的含义:首先进行取地址(&)运算,再进行(*)运算,*&a和*pointer_1作用一样。
声明多个指针需要养成好习惯
int *a,*b,*c
指针的传递
王道的龙老师总结出来,指针的使用场景通常只有两个,即传递和偏移,读者应时刻记住只有在这两种场景下使用指针。 下面的例子使用自定的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);
printf("after change i=%d\n",i);
return 0;
}
通过编译可以发现变量i的值并没有发生改变
当函数change()
被调用时,参数j
是按值传递的,这意味着传递给change()
函数的是变量i
的值的拷贝,而不是i
本身的内存地址。因此,change()
函数内部对参数j
的任何修改都只会影响到j
的副本,而不会影响到main()
函数中的变量i
。
可以通过以下方式来理解为什么变量i
的值不会发生改变:
- 内存分配:在函数调用时,会为参数
j
分配内存空间,并将变量i
的值复制到这个新的内存空间中。这个新的内存空间是change()
函数的局部变量,与main()
函数中的变量i
不同。 修改参数:在
change()
函数中,虽然对参数j
的值进行了修改,但实际上是修改了change()
函数内部局部变量的值,而不是main()
函数中的变量i
的值。这是因为参数j
只是main()
函数中变量i
的一个拷贝,对其进行的修改不会影响到原始的变量i
。栈帧:在函数调用时,每个函数都会有自己的栈帧,用于存储局部变量、参数等信息。当函数返回时,栈帧被销毁,其中的局部变量也随之消失。因此,
change()
函数中对参数j
的修改只在函数执行期间有效,函数返回后,其影响范围就结束了。
综上所述,虽然change()
函数内部对参数j
的值进行了修改,但这个修改只在函数内部有效,不会影响到main()
函数中的变量i
。
对代码进行修改,实现对变量i数值的修改
#include <stdio.h>
void change(int *j)
{
*j=5; //间接访问得到变量i的值
}
int main(){
int i=10;
printf("before change i=%d\n",i);
change(&i);
printf("after change i=%d\n",i);
return 0;
}
执行程序可以发现变量i的值确实发生了变化,这是因为将变量i的地址传递给change函数时,实际效果是j=&i,依然是值传递,但是此时j是指针变量,内存存储的是变量i的地址,所以通过*j间接访问到了与变量i相同的区域,但是变量j的地址和变量i的地址依然是不相同的。
指针的偏移
前面介绍了指针的传递,指针即地址,指针的另一个场景就是对其进行加减(偏移)#include <stdio.h>
#define N 5
int main() {
int a[N]={1,2,3,4,5};
int *p;
int i;
p=a; //将数组a的首地址传递给指针变量p,需保证等号两边的数值类型一样
for(i=0;i<N;i++)
{
printf("%3d",*(p+i)); //正序输出
}
printf("\n\n");
p=&a[4]; //让p指针指向数组a的最后一个元素地址
for(i=0;i<N;i++)
{
printf("%3d",*(p-i)); //倒序输出
}
return 0;
}
上述代码可以实现数组的正序和倒序输出,编译器在编译时,数组取下标的操作正是转换为指针的偏移来完成的。偏移的长度是其基类型的长度,也就是偏移sizeof(int),这样通过*(p+1)就可以得到元素a[1]。
指针和一维数组
一维数组在函数调用进行传递时,数组的长度子函数无法识别,这是由于一维数组名中存储的是数组的首地址。#include <stdio.h>
#include <string.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;
}
数组名c存储了一个起始地址(可在clion内存视图内查看),子函数在进行调用时起始是对指针指向的初始地址进行修改,子函数内定义了不同修改的方法,第一个方法为指针法,通过下标获取数组元素并进行修改的方法称为下标法。
指针与动态内存申请
在前面的学习中,每次使用数组都需要定义它的长度,其实是因为定义的整型、浮点型、字符型变量和数组变量都在栈空间中,而栈空间的大小在编译时是确定的,如果使用的空间大小不确定,那么就要使用堆空间。#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main() {
int i;
char *p;
scanf("%d",&i);
p=(char*) malloc(i); //请求内存空间,并强制转换类型
strcpy(p,"malloc susses");
puts(p);
free(p);
return 0;
}
malloc函数返回值是void*类型的指针,它只能用来存储地址而不能进行偏移,由于malloc函数并不知道用户申请的空间用来存放什么类型的数据,所以用户在确定存放的数据类型后,都需要将void*类型进行转换,上述代码用于存放字符,所以强制转换为char*类型。
在使用指针时,需要注意指针本身的大小和其指向空间的大小,这是为了确保程序的正确性和稳定性。下面是一些需要注意的原因:
- 内存越界访问:如果指针本身的大小不足以容纳其指向的内存空间的大小,那么在使用指针进行间接访问时,可能会导致内存越界访问。这种情况会导致未定义的行为,可能会破坏其他数据或导致程序崩溃。
缓冲区溢出:当指针指向的内存空间大小小于实际要存储的数据大小时,如果不加以检查就进行写入操作,可能会导致缓冲区溢出。这会破坏程序的内存布局,可能会影响到程序的其他部分,甚至导致安全漏洞。
指针算术运算:在进行指针算术运算时,指针的大小和指向空间的大小决定了指针加减操作的有效范围。如果指针的大小与指向空间的大小不匹配,可能会导致算术运算产生错误的结果。
类型安全性:指针本身的大小通常取决于其所指向的数据类型。因此,如果将指针用于不兼容的数据类型,可能会导致类型错误或数据解释错误,从而引发程序错误。
栈空间与堆空间的差异
#include <stdio.h>
#include <string.h>
#include <stdlib.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);
//printf("p=%s\n",p);
return 0;
return 0;
}
上述代码执行的结果为
I am print_stack
p=(null)
I am print_malloc
I am print_malloc
第二次打印出现异常的原因是print_stack()函数在执行完成后,栈空间会被释放,字符数组c原本空间会分配给其他函数使用,所以在打印时无法获取原本栈空间的数据,而print_malloc()函数的字符串存放在堆空间中,只有执行free操作才会释放。
在C语言中,堆空间和栈空间是两种不同的内存管理方式,它们用于存储程序运行时所需的数据和变量,但它们有着不同的特点和用途。
- 栈空间(Stack Space):
- 栈空间是由操作系统自动分配和管理的,用于存储函数的局部变量、函数的参数值以及函数调用过程中的一些临时数据。
- 栈空间的大小在程序运行前就已经确定,并且是有限的,通常比较小。这是因为栈空间的分配是静态的,由操作系统在程序运行时确定。
- 栈空间的分配和释放速度比较快,因为它采用了一种简单高效的先进后出(LIFO)的数据结构。
- 栈空间的生命周期与所在函数的执行周期相关联,函数执行结束后,栈上的内存将会被自动释放。
- 堆空间(Heap Space):
- 堆空间是由程序员手动分配和释放的,用于存储程序运行时动态分配的内存,如使用
malloc()
、calloc()
、realloc()
等函数分配的内存块。 - 堆空间的大小不固定,可以根据程序的需要动态地进行分配和释放。堆空间的大小受到系统内存的限制,通常比栈空间大得多。
- 堆空间的分配和释放速度较慢,因为它涉及到更复杂的内存管理和分配算法,如内存碎片整理等。
- 堆空间的生命周期不受限于函数的执行周期,需要手动释放分配的内存,否则可能会导致内存泄漏。
- 堆空间是由程序员手动分配和释放的,用于存储程序运行时动态分配的内存,如使用