地址
指针
定义指针的常用操作
定义一个指针变量 ->把变量地址的值赋值给指针变量-> 访问一个可用的地址的值
#include
int main(void)
{
int* p;//int *表示只能存放整形变量的地址 所以可以对整形取地址&
//int *p不表示定义了一个名字叫做*p的变量(数据类型为 int*,*p不表示是一个变量)
//int *p应该是:p是变量名 ,p变量的类型是int *类型
//所谓int *类型就是存放int变量地址的类型
int i = 12;
p = &i;//不能存放int类型的值 比如i 这里只是对i取地址
//所以修改p的值不影响i的值 修改i的值也不会影响p的值 注意是p 不是*p
//如果一个指针变量指向了某个普通变量 则 *指针变量(*p) 就完全等同于普通变量(i)
//注意是*p 所有出现i的地方都可以换为*p 反过来也一样
//*p就是以p的内容为地址的变量 我们可以以这个内容 去访问我们指向变量的值
printf("%p,%p \n",i,*p);
return 0;
//对指针的一些补充
//指针就是地址 地址就是指针 地址就是内存单元的编号
//指针变量是存放地址的变量
//指针和指针变量是两个不同的概念
//指针变量是一个变量 但是是一个存储地址的变量(储存内存单元的地址) 不是储存内存单元的内容
}
//运行结果
000000000000000C
000000000000000C
E:\VSCode\2022.10.28\x64\Debug\date_one.exe (进程 17908)已退出,代码为 0。
按任意键关闭此窗口. . .
根据以上结构可以知道 p的地址就是i的地址(这要得益于p对i的取地址,所以p变量里面存放了i的地址(改变p的地址对i没影响,改变i的地址对p也没影响),这个时候我们对p进行取地址就是i的地址,所以i的值就是p的值)这也是为什么我们只输出p的地址的时候p有一个不定的地址 因为p没有得到初始化,所以漂泊不定,下面的图可以非常清楚的解释指针变量如何调用i的值:
常见错误
//程序一:
int main(void)
{
int* p;
int i = 5;
*p = i;//因为*p没有指向i 所以i赋给了一个没有目标的一个值(垃圾值)所以报错
printf("%d\n", *q);
return 0;
}
//运行结果
// 程序报错
//程序二:
int main(void)
{
//常见错误
int* p;
int i = 5;
int* q;
p = &i;
*q=p//报错原因为类型不一致 int 与Int *
printf("%d\n", *q);
return 0;
}
//程序三:
int main(void)
{
//常见错误
int* p;
int i = 5;
int* q;
p = &i;
*q = *p; //类型一致 但程序报错 因为q没有赋值 可以改为 q=p
//改成q=p程序正常运行 因为q也指向了i
printf("%d\n", *q);
return 0;
}
int main(void)
{
int* p;
int i = 5;
int* q;
p=&i;
p = q;//如果q没有赋值 q里面是一个垃圾值 因为q赋值给了p 所以p也变成了一个垃圾值
//q是属于本程序的单元 控制权在自己,所以可以读写q的
//但是如果q内部是一个垃圾值,则本程序不能读写*q的内容
//*q不可读 不可写(*q代表一个不知但单元 向内存开辟的单元 控制权限不在自己)
printf("%d\n", *q);
return 0;
}
补充一个关于内存泄漏(野指针的问题)
如果开辟的一个内存单元 不free() 将会造成内存泄漏的问题
一直不free()的话将会一直消耗内存,但内存消耗完就会造成死机
这也是电脑为什么用久了会卡顿的问题(电脑程序一定有地方是忘记了要free的地方 所以电脑用久了卡顿)
直接看代码
//只交换了地址 没有交换内存单元的内容 我们可以对他们的地址进行打印查看
void huhuan(int* p, int* q)
{
printf("p:%p\nq:%p\n", p, q);
int* t;
t = p;
p = q;
q = t; //注意这样的写法 只是互换了指针变量(p q)里面内存单元地址的值
//注意是地址的值 我们并没有对地址里面的值进行修改 指针也无法做到互换一个静态变量的地址
printf("p:%p\nq:%p\n", p, q);
}
int main(void)
{
int a = 3;
int b = 5;//两个地址已经定死 不可以互换地址
printf("a:%p\nb:%p\n", &a, &b);
huhuan(&a, &b);//接收的是地址 所以进行取地址&
printf("a=%d\n b=%d\n", a, b);
return 0;
}
//程序结果
a:0000006C3399FC34
b:0000006C3399FC54
p:0000006C3399FC34
q:0000006C3399FC54
p:0000006C3399FC54
q:0000006C3399FC34
a=3
b=5
E:\VSCode\2022.10.28\x64\Debug\pointer_two.exe (进程 22944)已退出,代码为 0。
按任意键关闭此窗口. . .
//程序互换了a b的内容
void huhuan(int* p, int* q)
{
printf("p:%p\nq:%p\n", p, q);
int t;
t = *p;
*p = *q;
*q = t;//这样写才是互换了a,b的内存单元里面的内容 所以我们*q *p来互换即可 整形对整形
//这样的写法接管的是地址 所以可以改变主函数里面的值
//这也是为什么void huhuan(int p, int q) 这样的写法来传参数是不可以改变主函数里面的值的
printf("p:%p\nq:%p\n", p, q);
}
int main(void)
{
int a = 3;
int b = 5;//两个地址已经定死 不可以互换地址
printf("a:%p\nb:%p\n", &a, &b);
huhuan(&a, &b);//接收的是地址 所以进行取地址&
printf("a=%d\nb=%d\n", a, b);
return 0;
}
//运行结果
a:0000002B799AF974
b:0000002B799AF994
p:0000002B799AF974
q:0000002B799AF994
p:0000002B799AF974
q:0000002B799AF994
a=5
b=3
E:\VSCode\2022.10.28\x64\Debug\pointer_two.exe (进程 14016)已退出,代码为 0。
按任意键关闭此窗口. . .
int main(void)
{
int a = 3;
int b = 8;//两个地址已经定死 不可以互换地址
int* p;
p = &a;
a = b;//这样的写法 只是a的内存单元里面的内容改变了 但没有对a进行取地址操作来修改a的值
b = *p;//这里指针取到的值还是a的地址 所以b取到的值是a刷新后的内存单元
printf("a=%d\nb=%d",a,b);
return 0;
}
//运行结果
a=8
b=8
E:\VSCode\2022.10.28\x64\Debug\pointer_two.exe (进程 1152)已退出,代码为 0。
按任意键关闭此窗口. . .
实参为相关变量的地址
形参为以该变量的类型为类型的指针变量
通过指针可以很容易修改被调函数的值(修改主调函数一个以上变量的值)
在被调函数中通过 *形参变量名 的方式就可以修改主函数相关变量的值
指针和一维数组
数组名
一维数组名是一个指针常量 它存放的是一维数组第一个元素的地址
int main(void)
{
int a[5] = {0};
int* p;
//int a[3][4];//a[i][j]表示第i+1行 j+1列 long int 输出为%ld 如果想要一个值为16进制输出可以%#X
int b[5] = {0};
p = a;//因为一维数组名是一个指针常量 所以我们可以直接让p=a 他们的数据类型是一致的
printf("%#X\n",p);
printf("%d\n", *p);//还是如此 p是存放了a的地址 所以*p可以取到地址中的元素
//*(p+i)的作用跟a[i]的作用相同 如下验证
printf("%d\n",*(p+1));
printf("%d\n",p[1]);//因为p指向了a可以用a[数组下标]方式来表示 所以也可以用p[数组下标]的方式来表示
printf("%d\n",*(a+1));//既然a是一维数组名的指针变量 所以可以用*(a+i) 来表示第i个元素
//printf("%#X\n",&a[0]);//一维数组名是一个指针常量 存放的是一维数组名的地址
//printf("%#X\n", a);
//printf("%#X\n", (a+1));
//数组的第i个元素(a[i]) 编译器本就是先通过a来查找a的地址(首地址)的
// 然后再查找到了a+i的地址 ,最后才取到a[i]的内容
//那么我们是不是可以直接*(a+i)来直接输出a的值呢 这样编译器也省去了很多步骤 提高了效率
return 0;
}
下标和指针的关系
a[i]<<==>>*(a+i)
假设指针变量的名字是p 则p+i存放的内容就是第i+1个元素的地址 (p所指向的变量所占的字节数)
如何通过被调函数修改主调函数中一维数组的内容【如何界定一维数组】
void f(int* parr,int len)//数组的长度通过数组名是无法获取的 确定一个数组需要的2个参数
//parr存放的是第一个元素的地址 所以可以通过parr+i的方式访问到宿主的元素
//因为没有可以当为数组结束的标记 而字符串是有\0的 所以我们在函数中 我们需要
//把a发送给parr parr[3]是先找到首元素的地址 然后在搜索到parr+3的地址 最后取得元素 a[3]也是同样的道理
//这就是指针与下标的知识 指针对数组可以快速的传输数据
{
//parr[3] = 88;//因为搜索的是a的地址 parr存放的是以数组地址为内容的值 所以我们可以通过查找到地址
//进而修改里面的值
int i;
for (i = 0; i < len; i++)
{
printf("%d",*(parr+i));//*(parr+i)等价于parr[i]
}
printf("\n");
}
int main()
{
int a[5] = {1,2,3,4,5};
int b[6] = {-1,-2,-3,4,5,6};
int c[100] = { 1,99,22,33 };
//printf("%d", a[3]);
f(a, 5);
//printf("%d", a[3]);//加取地址为把a[3]的地址以10进制表示
f(b, 6);
f(c, 100);
return 0;
}
//运行结果
12345
-1-2-3456
1992233000000
E:\VSCode\2022.10.28\x64\Debug\point_three.exe (进程 20212)已退出,代码为 0。
按任意键关闭此窗口. . .
指针变量的运算
int main(void)
{
char ch = 'A';
int i = 99;
double x = 66.6;
char* p = &ch;
int* q=&i;//我们只保存了第一个字节的编号
double* r = &x;
printf("%2d %2d %2d\n", sizeof(p), sizeof(q), sizeof(r));//32为占4个字节 64位占8个字节
//p q r指向的是第一个字节的地址 但是保存却用了8个字节的 所以是编号用了8个字节来表示 64根线 2^64种状态 所以64位系统下地址范围应该时0-8G-1
printf("%2c %2d %2f",*p,*q,*r);
return 0;
}
//运行结果
8 8 8
A 99 66.600000
E:\VSCode\2022.10.28\x64\Debug\point_four.exe (进程 1660)已退出,代码为 0。
按任意键关闭此窗口. . .
传统数组的缺点:
数组长度必须事先制定,且只能是常整数,不能是变量 //c99已经支持 也就是int len=5 int a[len]的形式
传统形式定义的数组,该数组的内存程序员无法手动释放 因为它存放在栈区 只能是函数结束时由系统自动释放
数组的长度一旦定义 它的长度就不能在函数运行过程中动态的扩充或缩小
A定义的数组 只能A在运行期间被别的函数使用 A运行完毕后 不能被别的函数使用(因为已经被释放了)
void g(int* parr, int len)
{
parr[2] = 88;
}
void f()
{
int a[5] = {1,2,3,4,5};//这20个字节的数据 不能手动释放 因为它存放在栈区 只能是函数结束时由系统自动释放
g(a, 5);//只能在函数运行时调用其他的函数 数组才可以在其他函数使用
printf("%d\n",a[2]);
}
int main(void)
{
f();
return 0;
}
//运行结果
88
为什么要动态分配内存:
动态分配内存解决了传统数组的缺点
传统数组就是静态数组
//英文小知识 malloc 是memory(内存) allocate(分配)的缩写
#include
int main(void)
{
int i = 5;
//*p
int* p = (int*)malloc(4);//如果是100 就是25个整形变量 形参必须是整形 malloc函数会分配4字节的空间 但只能返回第一个字节的地址(p只有首地址)
//如果不成功 就返回NULL
// *p代表的是一个int变量 只不过*p这个整形变量的内存分配跟6行的不同 是动态的
//int*表示第一个字节的地址转换为整形的地址(注意是地址) 4表示分配4个字节的空间 所以强制类型转换是很有必须要的
//第7行分配了8个字节 4个静态(本身所占的内存是静态分配的) 4个动态(指向的内存是动态分配的) 64位下应该是12个字节 8个静态 4个动态
//printf("%d\n", sizeof(*p));//这里打印4的意思是 int的整形类型指针是4个字节
printf("%d\n", sizeof(p));//这里的8是64位系统下输出的结果
free(p);//所指的内存释放了
return 0;
}
//第2个补充
int main(void)
{
int a[5] = { 1,2,3,4,5 };
int* p;
char* l;
p = a;
printf("%d\t%d\n", sizeof(p), sizeof(l));//不管什么数据类型 指针变量占取的空间都是8个字节
printf("%p\t%p",p,&p);//p储存在&p的内存地址上 该储存单元储存了内存单元为(a[0])的地址
return 0;
}
//运行结果
8 8
0000005DB08FF9A8 0000005DB08FF9D8
E:\VSCode\2022.10.28\x64\Debug\point5.exe (进程 21024)已退出,代码为 0。
按任意键关闭此窗口. . .
动态构造一维数组
动态内存可以不先指定长度
动态内存可以在程序运行中动态的扩充或缩小(realloc)
可以手动申请 手动释放
int main(void)
{
//int a[5];//一共20个字节,每个占4个字节(表示空间大小 不是地址大小)
int len;
int* parr;
scanf_s("%d",&len);
parr = (int*)malloc(4 * len);//这样的写法就是造出了一个 动态的一维数组 类似于int parr[len];
//parr指向的是前面4个字节的地址 *parr就是前4个字节代表的变量 parr+1代表第2个4个字节
//如果是开始指向的是8个字节 parr+1就是第2个8个字节
//对一维数组进行操作
for (int i = 0; i < len; i++)
{
scanf_s("%d",&parr[i]);
}
//对一维数组进行输出
for (int i = 0; i < len; i++)
{
printf("%d\n",parr[i]);
}
free(parr);//释放掉了动态分配的数组
return 0;
}
//运行结果
5
1
2
3
4
5
1
2
3
4
5
E:\VSCode\2022.10.28\x64\Debug\point_malloc.exe (进程 13596)已退出,代码为 0。
按任意键关闭此窗口. . .
void f(int *p)//变成二维指针**p便可以接收一维指针变量的地址&p
{
*p = 200;//如果再加一个* 也就是**p 因为*p是一个整形变量
//如果这里写free(p)下面的第2个printf将不能打印 因为空间释放了
}
int main(void)
{
//int* p;
int* p = (int*)malloc(sizeof(int)*10);//如果p不初始化 是不可以直接对*p赋值的 //也就是没有对p的存储单元开辟空间
//sizeof(int)返回值为一个int变量返回的字节数
//malloc函数只能返回第一个字节的地址
*p = 10;
printf("%d\n",p[0]);
printf("%d\n",sizeof(*p));
f(p);
printf("%d\n", *p);
free(p);
return 0;
}
//运行结果
10
4
200
E:\VSCode\2022.10.28\x64\Debug\point_malloc.exe (进程 18284)已退出,代码为 0。
按任意键关闭此窗口. . .
动态内存可以跨函数使用 但上面程序没有体现
在栈区 存放函数的参数 由函数完成执行 系统会自动释放栈区的内存 不需要用户管理 在VS默认为1M
特点
读取速度快,存储和释放的思路是按照数据结构中的栈进行的,存数据就是压栈,释放就是出栈。
空间小,基本类型的数据占用空间的大小不会随着值的改变而改变,而且占用空间小。
堆区
由编程人员手动申请,手动释放,然后不手动释放(free)系统会在程序结束后由系统回收
生命周期是整个程序运行期间。使用malloc或者new进行堆的申请,堆的总大小为机器的虚拟内存的大小。
特点 读取速度慢
空间大:引用类型的数据大小是动态的,会随着数据的增加而改变大小。
malloc返回的是void*
指针可以指向一个普通类型的变量,例如int、double、char等,也可以指向一个指针类型的变量,如int*、double*、char*等。
如果一个指针指向的是另一个指针,我们就成他为二级指针,或者指向指针的指针。
转换会代码
int a=100;
int *p=&a;
int **q=&p;
因为指针变量会占取内存空间 ,所以可以用&来获取它的指针 ,C语言没有限定指针的级数,每增加一级指针,在定义指针变量时就得增加一个号。p是一级指针,指向普通类型的数据,定义时有一个;q是二级指针,指向一级指针p,定义时又两个*,当然我们也可以定义三极指针来指向二级指针
假设a、p、q、r的地址分别为0X001、0X002、0X003、0X004,他们之间的关系可以用下图来描述:
下面看代码
void f(int **q)//我们在下方把p的地址传到f函数去,那么q的内容就是p的地址了,
//f函数的形参int **q 的*q就是p的内容(i的地址) 所以*q=p **q=i;
{
**q = 20;
}
void g()
{
int i = 0;
int* p = &i;
printf("%d\n",*p);
f(&p);//p是int *类型,&p(对一级指针的取地址)就是 int **类型
printf("%d\n",*p);
}
int main(void)
{
g();
return 0;
}
//运行结果
0
20
E:\VSCode\2022.10.28\x64\Debug\point_point.exe (进程 1440)已退出,代码为 0。
按任意键关闭此窗口. . .
void f(int* q)
{
*q= 50;
}
void g(int** r)
{
**r = 100;
}
int main(void)
{
int* p = (int*)malloc(sizeof(int));
* p = 10;
printf("%p\n",p);
printf("第一次定义的值:%d\n",*p);
f(p);//对于一级指针传一级指针 传过去的是开辟空间的地址 所以可以修改
printf("f函数修改的值:%d\n", *p);
g(&p);//类型不同 因为g函数定义的r只能存放int *类型的地址
//所以要对p进行取地址 也就是规定的二级指针对一级指针的操作
printf("g函数修改的值:%d\n",*p);
free(p);
return 0;
}
//运行结果
00000294D1546090
第一次定义的值:10
f函数修改的值:50
g函数修改的值:100
E:\VSCode\2022.10.28\x64\Debug\point_point.exe (进程 11328)已退出,代码为 0。
按任意键关闭此窗口. . .
//静态变量不可以跨函数使用
void f(int **p)//存放指针变量的地址只能写**类
{
int j = 5;
//*p等价于q p和**p都不等价于q
*p = &j;
printf("i的地址:%p\n", &j);
}
int main(void)
{
int* q;
int i = 10;
q=&i;
printf("q指向i的值 %d\n",*q);
f(&q);//这里只能发送地址才能修改*q的值
printf("f函数修改q后的值:%d\n", *q);//这句程序语法没问题,但逻辑上存在问题
//f的内存已经被释放了(出栈) 但编译器没有检测出来
printf("第二次打印修改后的值:%d\n", *q);//连续输出两次*q的内容我们可以得到答案 这里输出的为乱码
printf("访问q的地址%p\n", q);//这里与f函数中i的地址对比可以知道 q可以访问i的地址 但不能访问i的空间(内存单元内容)
return 0;
}
//运行结果
q指向i的值 10
i的地址:000000956EDAF994
f函数修改q后的值:5
第二次打印修改后的值:-858993460
访问q的地址000000956EDAF994
E:\VSCode\2022.10.28\x64\Debug\stat_dyna.exe (进程 23476)已退出,代码为 0。
按任意键关闭此窗口. . .//
//动态内存可以跨函数使用
void f(int **q)
{
*q = (int*)malloc(sizeof(int));//malloc函数返回的是开辟字节的第一个的地址
//等价于p=(int *)malloc(sizeof(int))
**q = 5;
}
int main(void)
{
int* p;
f(&p);
printf("%d\n",*p);//动态内存在堆里面分配 内存由程序员手动开辟 手动释放 所以可以在f函数中修改*p的值
printf("%d\n", *p);//正常打印
free(p);
printf("%d\n", *p);//已经释放 打印乱码
return 0;
}
//运行结果
5
5
-572662307
E:\VSCode\2022.10.28\x64\Debug\stat_dyna.exe (进程 21016)已退出,代码为 0。
按任意键关闭此窗口. . .