关于指针学习
一、指针
- 地址:在计算机中,给每一个字节的分配对应的编号,这个编号称之为地址。
(相当于是门牌号,每家都有一个门牌号) - 指针:指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(Pointed to)存在电脑存储器中另一个地方的值。
换句话说:指针是一个变量,且这个变量是专门用来存放地址的。
下面一段对话:
老张:首先我们说下地址,地址是什么呢?地址是内存中字节的编号。我们知道内存是由大量字节构成的,每个字节都有一个属于自己的编号,这些编号从0开始,依次递增,这些编号就是我们所说的地址。当我们在程序中声明一个变量的时候,会占用相应个数的字节,第一个字节的编号,就是这个变量的地址。
小豆丁:那指针呢?
老张:指针的本质实际上是一种特殊的数据类型,我们可以声明指针类型的变量。
小豆丁:那指针类型的变量存什么啊?地址吗?
老张:是的啊,指针类型的变量,存储的是地址。举个例子,int num = 5;有个整型的变量num,存储整型数据5。int* p_num = #有个指针类型变量p_num,存储的是num变量的地址,所以我们可以说指针p_num指向变量num。
小豆丁:哦,我明白了!
老张:那指针就是地址这句话对吗?
小豆丁:不对,指针和地址的本质不同,可以说指针能够代表地址,但是不能说指针就是地址!
老张:聪明,孺子可教也!
小豆丁:谢谢老张!
老张:叫谁老张呢!我不老!
二、指针运算
int a = 10; int*p = &a; | ||
---|---|---|
运算符 | &a + n | 向高地址方向偏移n倍的数据类型字节大小 |
+ | p + n | 向高地址方向偏移n倍的数据类型字节大小 |
*(p+n) | 向高地址方向偏移n倍的数据类型字节大小,再取地址中对应的值 | |
*p + n | 先取地址中对应的值,在对值进行进行加n | |
- | &a - n | 向低地址方向偏移n倍的数据类型字节大小 |
p - n | 向低地址方向偏移n倍的数据类型字节大小 | |
*(p - n) | 向低地址方向偏移n倍的数据类型字节大小,再取地址中对应的值 | |
*p - n | 先取地址中对应的值,在对值进行进行减n | |
++ | p++ | 后缀运算,先运算,后向高地址方向偏移一个数据类型的大小 |
++p | 前缀运算,先向高地址方向偏移一个数据类型的大小,再运算 | |
*++p | 先++p,前缀运算,先向高地址方向偏移一个数据类型的大小,再取值 | |
++*p | 先*p,先取地址中的值,在对值进行++运算 | |
*p++ | 先p++,p++为后缀运算,先取地址中的值,在将p向高地址方向偏移一个数据类型的大小 | |
*(p++) | 先p++,p++为后缀运算,先取地址中的值,在将p向高地址方向偏移一个数据类型的大小 | |
(*p)++ | 先取地址中的值,对值进行++运算 | |
*(++p) | 先++p,先向高地址方向偏移一个数据类型字节的大小,在取地址中的值 | |
- - |
三、一维数组和指针
int arr[ ]= {11,22,33}; //定义一个数组,数组名为arr,数组中有三个元素
int*p = arr; //定义一个指针p,指向一维数组的首地址(第一个元素的地址)
总结
一维数组和指针结合,访问地址方式:
arr + i ===> &arr[0] + i ===> &arr[i] ===> p + i ===> &p[0] + i ===> &p[i]
一维数组和指针结合,访问值方式:
*(arr + i) ===> *(&arr[0] + i) ===> arr[i] ===> *(p + i) ===> *(&p[0] + i) ===> p[i]
值:arr[i]–>(&arr[0]+i)–>(arr+i)–>p[i]–>(&p[0]+i)–>(p+i)–>*p++
地址:&arr[i]–>&arr[0]+i–>arr+i–>&p[i]–>&p[0]+i–>p+i–>p++
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
四、二维数组指针
下图
int arr[2][3]= {11,22,33,44,55,66};
int (*p)[3] = arr; //数组指针
&arr[0][0] + 1 :表示二维数组第一行第一列元素地址,偏移四个字节 ==> 列偏移
arr[0] + 1:类似于一维数组名,偏移四个字节 ==> 列偏移
&arr[0] + 1:类似一维数组名取地址,偏移二维数组一行大小 ==> 行偏移
arr + 1 :表示二维数组第一行首地址,偏移二维数组一行大小 ==> 行偏移
&arr + 1 :表示二维数组整体首地址,偏移二维数组整体大小
五、多级指针
一级指针:int* p =====> 存放变量的地址
二级指针:int** p =====> 存放一级指针变量的地址
三级指针:int*** p =====> 存放二级指针变量的地址
六、数组指针(行地址)、指针数组、指针函数、函数指针、函数指针数组
1.数组指针
int (arr)[5]
1> 数组指针本质上是一个指针,能够保存整个数组的起始地址
2> 定义格式:数据类型 ( 指针名)[数组长度];
3> 说明是一个数组指针,指向的的数组长度为中括号中的内容
数组名只有单独放在sizeof内部以及放在&后才代表整个数组的地址。其余情况数组名都表示数组首元素地址。
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
int main(int argc, const char *argv[])
{
int arr[3][4] = {{1,2,3,4}, {5,6,7,8},{9,9,9,9}};
//arr[0] --> arr[0][0] 、arr[0][1] 、 arr[0][2] 、arr[0][3]
//arr[1] --> arr[1][0] 、arr[1][1] 、 arr[1][2] 、arr[1][3]
//arr[2] --> arr[2][0] 、arr[2][1] 、 arr[2][2] 、arr[2][3]
//数组名arr表示第一行的地址 arr <==> &arr[0]
//arr[i]表示的是第i行的第一个元素的地址: arr[i] <==> &arr[i][0]
//定义数组指针完成对数据的访问
int (*p)[4];
p = arr; //将二维数组数组名赋值给数组指针
printf("数组元素为:\n");
for(int i=0; i<3; i++)
{
for(int j=0; j<4; j++)
{
printf("%d\t", *(*(p+i) + j)); //如果是行地址取*得到的是该行首元素的地址,如果对元素地址取*得到的是该元素的值
}
printf("\n");
}
return 0;
}
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main(int argc,const char *argv[])
{
int arr[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
int *p1 = &arr[0][0];//将行地址赋值给指针变量p1
int (*p)[4] = arr;//&arr[0] //给数组指针赋首行地址
for(int i=0;i<3;i++)
{
for(int j=0;j<4;j++)
{
printf("*(*(p+%d)+%d)=%d\n",i,j,*(*(p+i)+j));//p代表行地址
}
}
for(int i=0;i<12;i++)
{
printf("*p1=%d\n",*p1);//*(p1+i)//p1代表元素地址
p1++;
}
}
//编译结果如下:
*(*(p+0)+0)=1
*(*(p+0)+1)=2
*(*(p+0)+2)=3
*(*(p+0)+3)=4
*(*(p+1)+0)=5
*(*(p+1)+1)=6
*(*(p+1)+2)=7
*(*(p+1)+3)=8
*(*(p+2)+0)=9
*(*(p+2)+1)=10
*(*(p+2)+2)=11
*(*(p+2)+3)=12
*p1=1
*p1=2
*p1=3
*p1=4
*p1=5
*p1=6
*p1=7
*p1=8
*p1=9
*p1=10
*p1=11
*p1=12
2.指针数组
指针数组:本质上是一个数组,存储多个类型相同的指针
格式:存储类型 数据类型 *指针数组变量名[常量表达式]
1.常量表达式:数组长度,指针的个数
1> 指针数组本质上是一个数组,只是数组中的每个元素都是指针变量
2> 定义格式:数据类型 * 数组名[数组长度];
int a=100,b=200,c=300; --->int arr[3]={}
int* p1=&a;
int* p2=&b;
int* p3=&c;
int *p[3]={&a,&b,&c}; p的类型是int *[3], 占8*3
// 0 1 2
// p[0] p[1] p[2]
// *p[0] *p[1] *p[2]
3.指针函数
指针函数:本质上是一个函数,返回一个指针
注意:不允许返回局部变量的地址
因为随着函数的结束,局部变量的内存会被系统回收。指针函数能够返回静态局部变量的地址。
指针函数只能返回生命周期比较长的数据的地址
i> 全局变量的地址
ii>静态局部变量的地址
iii>主调函数传过来的变量的地址
iv> 由程序员使用malloc函数在堆区手动申请的空间
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int* fun()
{
int a=100,b=200;
int arr[2]; //0x10局部:调用函数申请内存,。函数调用结束释放内存
arr[0]=a;arr[1]=b;
return arr;//段错误,arr调用函数申请内存,函数调用结束释放内存,
//解决方式:延长生命周期
}
//解决方式1:全局变量
int arr[2];
int* fun1()
{
int a=100,b=200;
arr[0]=a;arr[1]=b;
return arr;//段错误,arr调用函数申请内存,函数调用结束释放内存,
//解决方式:延长生命周期
}
//解决方式2:传参
int* fun2(int *arr)
{
int a=100,b=200;
*arr=a;*(arr+1)=b;
return arr;//段错误,arr调用函数申请内存,函数调用结束释放内存,
//解决方式:延长生命周期
}
int main(int argc, const char *argv[])
{
// int* p=fun();
// int* p=fun1();
// int arr[2];
// int *p=fun2(arr);
int *p=fun3();
printf("*p=%d *(p+1)=%d\n",*p,*(p+1));
return 0;
}
//解决方式3:static
int* fun3()
{
int a=100,b=200;
static int arr[2];
arr[0]=a;arr[1]=b;
return arr;//段错误,arr调用函数申请内存,函数调用结束释放内存,
//解决方式:延长生命周期
}
4.函数指针
int (fun1)(int a,int b)
1> 函数指针本质上是一个指针,指向函数运行的入口地址
2> 定义格式:数据类型 ( 指针名)(参数列表);
3> 函数名本质上就是函数的入口地址
void Sum1(void) void (*p)(void)=Sum1
void Sum2(int a,float b) void (*p1)(int,float)=Sum2
float Sum3(void) float (*p2)(void)=Sum3
float Sum4(int a,float b) float (*p3)(int,float)=Sum4
举个例子:
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
int myMax(int m, int n)
{
return m>n?m:n;
}
int myMin(int m, int n)
{
return m<n?m:n;
}
//int *p=arr
int main(int argc, const char *argv[])
{
//定义函数指针指向函数入库地址
int (*p)(int,int) = myMax;
p = myMax; //给函数指针赋值
printf("%d\n", myMax(520, 1314)); //1314
printf("%d\n", P(520, 1314)); //1314
p = myMin;
printf("%d\n", p(520, 1314)); //520
return 0;
}
5.函数指针数组
- 格式:数据类型 (*指针名[下标])(参数列表)
- 本质是一个数组,数组中存放相同类型的函数指针
函数名可以看作是一个指向该函数在内存中的入口地址。(函数名代表该函数的入口地址)当你调用一个函数时,程序会根据函数名找到该函数在内存中的位置,然后执行该函数的代码。
float add(float a,float b); float (*p)(float a,float b) = add;
float sub(float a,float b); float (*p)(float a,float b) = sub;
float mul(float a,float b); float (*p)(float a,float b) = mul;
float div(float a,float b); float (*p)(float a,float b) = div;
函数指针数组:float (*p[4])(float a,float b) = {add,sub,mul,div};
函数指针数组遍历:(p[i])(3,4)
指针函数:本质是一个函数,返回一个地址(指针) int *fun(int a,int b) 在函数的函数名前加*
函数指针:本质是一个指针,存储的是一个函数的地址(函数名就是该函数的入口地址) int (*p)(int,int)
指针数组:本质是一个数组,每个数组中存的都是地址 int *arr[3] 在数组名前加*
数组指针:本质是一个指针。存储的是数组的地址(行地址) int (*arr)[3];
函数指针数组:本质是一个数组,每个数组存储的是函数的地址 int (*brr[3])(int ,int)
下面代码仅供参考:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int min(int a,int b)
{
return a<b?a:b;
}
int max(int a,int b)
{
return a>b?a:b;
}
int sum(int a,int b)
{
return a+b;
}
//int (*arr[5])(int a,int b)
//函数指针数组
//int (*fun)(int a,int b)
//
int main(int argc,const char *argv[])
{
int (*arr[3])(int a,int b) = {min,max,sum};//arr[0]==min arr[1]==max arr[2]==sum //{&min,&max,&sum}
for(int i=0;i<3;i++)
{
printf("arr[%d]=%d\n",i,arr[i](66,88));//arr[0](66,88);==min(66,88) arr[1]==max(66,88) arr[2]=sum(66,88)
}
// printf("min(66,88)=%d\n",*min(66,88));
}
总结
七、指针与字符数组
格式: char str[]=“hello”;
char *p=str; //字符指针变量p指向str的首地址
char *p=“hello”;//指针变量p指向字符串常量的首地址 p是字符h的地址
1.字符数组和指针格式
(1) char str[ ] = " "; //定义字符数据
(2)char* p = str; //定义指针指向字符数组首地址
2.通过指针操作字符数组
3.指针指向字符数组变量
(1)通过指针修改字符数组元素中的内容:*(p+1) = ‘E’
(2)当不同的数组存储相同的字符串时,地址不同=
4.指针指向字符串常量
(1)不可以通过指针修改字符串常量的内容:*(p+1) = ‘E’ ===> !!!段错误!!!==
(2)当不同的指针存储相同的字符串时,地址相同
下面为其他截图,作参考用的
代码如下:
关于string函数
5.C语言内存分布
八、字符指针数组
1.字符指针数组:本质上是一个数组,数组中存放多个字符指针(字符串)
2.格式:char* 数组名[下标]
3.字符指针数组指向字符数组变量
存储类型 char *指针数组变量名[常量表达式] 因为 [ ]>*
例:char *p[]
char a[]="abc"; puts(a)
char b[]="1234";
char c[]="+-*/";
char *p1=a;
char *p2=b;
char *p3=c;
char *p[3]={a,b,c};//字符指针数组,存储3个字符指针
// 0 1 2
// p[0] p[1] p[2]
// puts(p[i])
char *p[3]={"abc","1234","+-*/"};
补充
c语言中,函数参数的传递方式可以分为两种:传值调用(call by value)和传址调用(call by reference).
1.传值调用(Call by Value):
当函数使用传值调用时,函数接收的是实参的一个副本,而不是实参本身的地址。因此,对形参的修改不会影响到实参本身。
void increment(int x) {
x++;
}
int main() {
int a = 10;
increment(a);
printf("a after increment: %d\n", a); // Output: a after increment: 10
return 0;
}
例子中,increment 函数接收的是 a 的一个副本,即 int x。在函数中对 x 的增加操作 x++ 只会修改这个副本的值,并不会影响到 a 的值。因此,即使在 main 函数中调用 increment(a),a 的值仍然是 10,没有被修改。
2.传址调用(Call by Reference)
当函数使用传址调用时,函数接收的是实参的地址,通过这个地址可以直接操作实参的值。在C语言中,通过指针来实现传址调用。
void increment(int *x) {
(*x)++;
}
int main() {
int a = 10;
increment(&a);
printf("a after increment: %d\n", a); // Output: a after increment: 11
return 0;
}
在这个示例中,increment 函数的参数类型是 int *x,即指向整型数据的指针。在 main 函数中,我们调用 increment(&a),传递了 a 的地址 &a 给 increment 函数。
在 increment 函数内部:int *x 接收到了 a 的地址 &a。通过 (*x)++,我们对指针 x 所指向的值进行了自增操作。
因为 x 指向了 a 的地址,所以在 increment 函数内部对 *x 的修改实际上就是对 a 的修改。因此,在 main 函数中,a 的值在调用 increment(&a) 后变成了 11。
问1:函数内部是怎么通过形参的地址直接操作实参
- 传递实参的地址:调用函数时,将实参的地址传递给函数。在 C 语言中,这可以通过指针作为函数的参数来实现。
void modifyValue(int *ptr) {
*ptr = *ptr * 2; // 通过指针访问实参的地址并修改实参的值
}
int main() {
int num = 10;
modifyValue(&num); // 传递实参 num 的地址
printf("Modified value: %d\n", num); // 输出修改后的实参的值
return 0;
}
- 在函数内部操作实参:函数中的形参通过指针可以访问和修改实参的值。在上面的例子中,函数 modifyValue 接收一个 int* 类型的指针 ptr,它指向 main 函数中变量 num 的地址。通过 *ptr 可以访问 num 的值,并且可以修改它。
- *ptr 是指针 ptr 所指向的地址上的值(也就是 num 的值)。
- *ptr = *ptr * 2 就是通过指针 ptr 直接修改 num 的值为其当前值的两倍。
- 影响实参的值:因为函数 modifyValue 中操作的是 main 函数中变量 num 的实际地址,所以任何对 *ptr 的修改都会直接反映在 main 函数中的 num 变量上。
这种通过指针传递和操作实参的方式,就是传址调用的基本原理。这使得函数能够直接对实参进行更改,而不仅仅是操作它的副本。
问2:下列代码中将++*x改成赋值运算如何表示??
int num = 10;
int *x = #
++*x;
具体解释如下:
*x:x 是一个指针,*x 表示取 x 指针所指向地址的值。
++*x:这是一个前缀递增操作符 ++ 应用在 *x 上。它的作用是先递增 *x 指向的地址处的值,然后返回递增后的值
x 是指向 num 的指针。
*x 就是 num,也就是指针 x 所指向的整数变量的值,即 10。
++*x 就是递增 *x,也就是递增 num 的值。执行后,num 的值变为 11。
表达式 ++*x 可以理解为“递增指针 x 所指向地址的值”。
答:如果要将 ++*x 表达式拆分成赋值运算的形式,可以将它分解为两个步骤:
int num = 10;
int *x = #
// 第一步:解引用 *x 并递增其值
int value = (*x); // 获取 *x 指向的值
value = value + 1; // 递增值
// 第二步:将递增后的值赋回给 *x 指向的地址
*x = value;
这样,原来的 ++*x 表达式被分解成了两个明确的操作:
int value = (*x);:获取 *x 指向的值。 value = value + 1;:递增获取的值。
*x = value;:将递增后的值赋回给 *x 指向的地址。 这些步骤确保了 *x 所指向的变量(在这里是 num)被正确地递增
追问1:为什么不写num=*x
int num = 10; // 定义一个整数变量 num,赋值为 10
int *x = # // 定义一个指针 x,指向 num 的地址
int value = (*x); // value 等于 *x,也就是 num 的值,这里是 10
value = value + 1; // 将 value 递增,变成 11
*x = value; // 将递增后的 value 赋回给 *x 所指向的变量,即 num
这段代码中,*x = value; 这一行的作用是将 value 的值(这里是 11)赋给 *x 指向的变量,也就是 num。因为 x
是指向 num 的指针,所以 *x 就是 num 这个变量本身。这意味着,*x = value; 将 num 的值更新为 11。相比之下,如果我们写成 num = *x;:
这个语句将 x 所指向的变量的值(即 *x,也就是 num 的值)复制给了 num 这个变量。换句话说,它只是将 num
的值复制给了自己,实际上不会改变 num 的值,因为它只是在内部进行了一个赋值操作,没有修改 num 所在的内存。
区别:
- *x = value; 直接修改了 *x 所指向的变量(即 num)的值为 value。
- num = *x; 只是将 *x 的值(即 num 的值)赋给 num 本身,没有实际改变 num 的值。
追问2:那如果分别用两种方式打印,会有什么结果
使用 num = *x; 的方式打印:
int num = 10; // 定义一个整数变量 num,赋值为 10
int *x = # // 定义一个指针 x,指向 num 的地址
num = *x + 1; // 将 *x 的值加 1 后赋给 num
printf("num = %d\n", num); // 打印 num 的值
num = 11
这里 num = *x + 1; 将 *x 的值(即 num 的当前值,这里是 10)加上 1,然后将结果 11 赋给 num。因此,打印结果是 11。
指针 x 指向的地址是 num 的地址,通过 *x 可以获取 num 的值,但不改变地址本身
使用 *x = value; 的方式打印:
int num = 10; // 定义一个整数变量 num,赋值为 10
int *x = # // 定义一个指针 x,指向 num 的地址
int value = (*x); // value 等于 *x,也就是 num 的值,这里是 10
value = value + 1; // 将 value 递增,变成 11
*x = value; // 将递增后的 value 赋回给 *x 所指向的变量,即 num
printf("num = %d\n", num); // 打印 num 的值
输出结果:
num = 11
这里的输出结果仍然是 11,因为 *x = value; 直接修改了 *x 指向的变量(即 num)的值为 value,从而修改了 num的值为 11。
指针 x 依然指向 num 的地址,但通过 *x = value; 直接修改了这个地址处的值(即 num 的值)为 11。
总结
在第一个方式中,指针 x 指向的地址是 num 的地址,通过 *x 可以获取 num 的值,但不改变地址本身。
在第二个方式中,指针 x 依然指向 num 的地址,但通过 *x = value; 直接修改了这个地址处的值(即 num 的值)为 11。
*因此,两种方式的本质区别在于第一种是通过指针间接访问和修改变量的值,而第二种是直接通过指针修改变量的值*