目录
一、内存和指针
1.1 指针的使用场景
二、指针变量和地址
2.1 取地址符(&)
2.2指针变量和解引用操作符(*)
2.2.1 指针变量
2.3 指针变量的大小
三、指针变量类型的意义
3.2 指针+-整数
编辑
四、指针计算
五、const修饰指针
5.1 const修饰变量
1.2 const修饰指针变量
六、野指针
6.1 野指针成因
1 指针未初始化
2.指针越界访问
3.指针指向的空间释放
6.2 如何规避野指针
6.2.1 指针初始化
6.2.2 小心指针越界
6.2.3 指针变量不再使用时,即使设置NULL
七、assert断言
7.1 assert是什么
7.2 assert的优缺点
一、内存和指针
再将内存和指针之前,我想引入一个生活中的例子:
- 内存可以类比为一个巨大的仓库,里面有许多存储单元,每个单元都有一个唯一的地址。这些存储单元就像仓库中的货架,每个货架都有一个编号,方便我们找到存放的物品。
- 指针则类似于仓库的管理员,他手里有一张清单,清单上记录了每个货架的编号以及货架上存放的物品。管理员通过这张清单可以快速找到某个物品的位置,而不需要逐个货架去查找。
1.1 指针的使用场景
假设仓库中有多个货架,每个货架上存放着不同的物品。管理员(指针)可以通过清单(指针变量)找到某个货架(内存地址),并查看或修改货架上的物品(数据)。
#include <stdio.h>
int main
{int a = 10; // 在某个货架上存放了数字10int *p = &a; // 管理员记录下这个货架的编号*p = 20; // 管理员找到这个货架,并将上面的数字改为20retrun 0;
}
二、指针变量和地址
2.1 取地址符(&)
c语言中创建变量其实就是想内存申请空间,比如:
#include <stdio.h>
int main()
{int a = 10;//向内存申请4个字节的空间,存放10进去。return 0;
}
而我们应该如何得到地址呢?
#include <stdio.h>
int main()
{int a = 10;&a;//使用“&”,取出a的地址printf("%p\n", a);//%p 地址return 0;
}
2.2指针变量和解引用操作符(*)
2.2.1 指针变量
当我们通过&符号来拿到的地址其实是一个数值,比如0000000A,这个值如果我们想存储起来,那我们应该该如何把这个值给存储到一个合适的地方呢,该存在哪?
c语言中,像这么一个问题,我们一个存入指针变量中
#include <stdio.h>
int main()
{int a = 10;int *pa = &a;//取出a的地址并存储到指针变量pa中 &a指向*pareturn 0;
}
这种变量(指针变量)是用来存放指针的,存放在指针变量中的值都会理解为地址。
2.2.2 如何拆解指针类型
我们看到上面的代码:
int *pa = &a;
pa的类型是int *,*是在说明pa是指针变量
int说明pa只想的是整形(int)类型的对象
- 指针就是地址
- 指针变量就是变量,是专门用来存放地址的变量
- 存放在指针变量的值,就是地址
那我们现在写一个char类型ch变量,我们应该放在什么类型当中呢?
char ch = 'w';
pc = &ch;char * pc = &ch;//pc的类型
2.2.3解应用操作符
- 我们现在所讲的pc = &ch;,只是讲ch这个地址起来,但是我们存起来之后该怎么使用他呢?
- 在现实生活中,我们得知一个柜子编号要去取出或者存放物品。而c语言也一样,我们得知了这个数的地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习一个操作符叫做解引用操作符(*)
#include <stdio.h>
int main()
{int a = 100;int* pa = &a;*pa = 0;//pa就是a,把a改成0return 0;
}
上面代码中第六行的 int* pa = &a; 就是用了"*"解引用操作符,*pa的意思就是通过pa存放的地址,找到指向的空间,*pa其实就是a变量了;所以 *pa = 0,这个操作符就是把a改成了0.
那么我们可以通过代码看的更直观一点:
#include <stdio.h>
int main()
{int a = 100;int* pa = &a;printf("%d\n",*pa);*pa = 0;//pa就是a,把a改成0printf("%d\n",a);return 0;
}
2.3 指针变量的大小
前面内容我们了解到在不同输出环境中,指针变量的大小是不同的,例如X86和X64就有所不同
代码如下 :
int main()
{printf("%zd\n", sizeof(char*));printf("%zd\n", sizeof(int*));printf("%zd\n", sizeof(short*));printf("%zd\n", sizeof(double*));return 0;
}
那么我们在不同环境下有什么差别呢???
X64
X86
结论:
- 32位平台下地址就是32bit位,指针变量大小是4个字节
- 64位平台下地址就是64bit位,指针变量大小是8个字节
三、指针变量类型的意义
指针变量的大小和类型无关,只要是指针变量,在一个平台下,大小都是一样的,为什么还要有各种各样的指针类型呢?
3.1 指针的解引用
对比下面两个代码,主要是观察内存的变化:
int main()
{int n = 0x11223344;int* pi = &n;*pi = 0;return 0;
}int main()
{int n = 0x11223344;char* pc = &n;*pc = 0;return 0;
}
大家看到这里可以自己在内存窗口看看他们两个的差别
int:
char:
调试我们可以看到代码1的4个字节全部改为0,而代码2只有第一个字节改为0。
结论:
- 指针的类型决定了对指针解引用的时候有多大权限(例如int是是四个字节)。
3.2 指针+-整数
int main()
{int n = 10;char* pc = (char*)&n;int* pi = &n;printf("%p\n", &n);printf("%p\n", &pc);printf("%p\n", &pc+1);printf("%p\n", &pi);printf("%p\n", &pi+1);printf("%p\n", &n);
}
运行结果如下:
我们可以看出,char*类型的指针变量+1跳过一个字节,int*类型的指针变量+1跳过的是4个字节。
这就是+1带来的变化,同理-1页是一样。
3.3 void指针
在指针类型中有一个特殊的类型就是void类型,可以理解为无具体类型的指针,这种类型的指针可以接受所有类型的地址。但是也有局限性:他不能+-整数和解引用计算
int main()
{int a = 10;int* pa = &a;double* pc = &a;return 0;}
使用 void*类型的指针接受地址:
#include <stdio.h>int main(){int a = 10;void* pa = &a;void* pc = &a;*pa = 10; *pc = 0;return 0;}
四、指针计算
- 指针+-整数
- 指针-指针
- 指针的关系运算
4.1指针+-整数
因为数组在内存中是连续存放的,只要知道第一个元素的地址,就可以慢慢找到所有元素。
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);for (i = 0;i <sz;i++){printf("%d ", *(p + i));//p+i就是指针+整数}
}
4.2 指针-指针
int my_strlen(char* s)
{char* p = s;while (*p != '\0')p++;return p - s;
}int main()
{printf("%d\n", my_strlen("abc"));return 0;
}
4.3 指针的关系运算
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];int sz = sizeof(arr) / sizeof(arr[0]);while (p < arr + sz)//指针大小比较{printf("%d ", *p);p++;}return 0;
}
五、const修饰指针
5.1 const修饰变量
变量是可以修改的,如果吧变量的地址交给一个指针变量,通过指针我们也是可以修改这个便来那个的,但是我们如果不想让这个变量被修改,那我们该如何做呢,这时候就引出了const。
int main()
{int a = 1;a = 10;printf("%d", a);const int b = 1;b = 10;printf("%d", b);}
大家可以发现运行时是会报错的,无法修改b的值,这就是const的作用。
但是在这中情况下,我们用另一种方法——使用地址,去修改就可以成功啦
int main()
{const int a = 0;printf("n = %d\n", a);int* p = &a;*p = 10;printf("n = %d\n", a);return 0;
}
我们看到我们a的值确实被修改了,但是我们要想一下,我们既然要使用const来修饰a,那就是不想让a可以被修改,所以上面这个方法对于实践应用来说没有什么意义,那我们该如何让p拿到n的地址也不被修改呢?
1.2 const修饰指针变量
int *p ;
int const * p; const放在*左边修饰
int * const P; const放在*左边修饰
int* const ptr;//ptr 是一个 const 指针,指向 int 类型的数据。
//ptr 的指向不可更改,但可以通过 ptr 修改所指向的数据
const int* const ptr;//ptr 是一个 const 指针,指向 const int 类型的数据。
//既不能通过 ptr 修改所指向的数据,也不能修改 ptr 的指向。
见如下代码:
void test1()
{int n = 10;int m = 20;int* p = &n;*p = 20;p = &m;
}//测试const放在*左边的情况
void test2()
{int n = 10;int m = 20;const int* p = &n;//只限制*p,指针指向的内容不能修改,但不限制p*p = 20;p = &m;
}//测试const放在*的右边情况
void test3()
{int n = 10;int m = 20;int* const p = &n; //只能约束p,指针指向的内容可以通过p来改变,但是指针变量不能被改变*p = 20;p = &m;
}
//测试*的左右两边都有const
void test4()
{int n = 10;int m = 20;int const * const p = &n;*p = 20;//两个都约束p = &m;
}int main()
{test1();test2();test3();test4();return 0;
}
结论:
- const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。
- const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
六、野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1 野指针成因
1 指针未初始化
int main()
{int* p;//局部变量指针未初始化,默认为随机值*p = 20;//p就是一个野指针return 0;
}
2.指针越界访问
int main()
{int arr[10] = { 0 };int* p = &arr[0];int i = 0;for (i = 0;i <= 11;i++)//0开始,循环12次{//当指针指向的范围超出数组arr的范围时,p就是野指针*(p++) = i;}
}
3.指针指向的空间释放
int *test()
{ int n = 100;return &n;
}//结束后n还给操作系统了int main()
{int* p = test();printf("%d\n", *p);return 0;
}
return &n的时候,n的生命周期结束了,那么n就会还给操作系统
但结合我们之前的博客,在第二行前加上“static”,那么就没问题啦
int *test()
{ static int n = 100;return &n;
}//结束后n还给操作系统了int main()
{int* p = test();printf("%d\n", *p);return 0;
}
6.2 如何规避野指针
6.2.1 指针初始化
- 如果明确知道指针指向哪里就直接赋值地址(例如int i = 0; int* pi = &i;)
- 不知道的话可以给指针赋值NULL
- NULL是c语言中定义的一个标识符常量,值是0(赋一个空值),但这个地址是无法使用的,读写改地址会报错
初始化如下:
int main()
{int num = 10;int* p1 = #int* p2 = NULL;return 0;
}
6.2.2 小心指针越界
一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。所以我们申请多少内存就使用多少内存。
6.2.3 指针变量不再使用时,即使设置NULL
当一块区域不再访问的时候,我们及时将该指针设置为NULL。
只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL
int main()
{int arr[10] = { 0 };int* p = &arr[0];int i = 0;for(i =0;i<10;i++){*(p++) = i;}
//此时p已经越界了,可以把p设置为NULLp = NULL;//下次使用p的时候,判断p不为NULL的时候再使用p = &arr[0];//再次定义,重新获得地址if (p != NULL){//表达式}return 0;
}
七、assert断言
7.1 assert是什么
在编程中,assert
是一种用于调试的语句,用于验证某个条件是否为真。如果条件为假,assert
会抛出异常(通常是 AssertionError
),帮助开发者快速发现逻辑错误。
assert.h头文件定义了assert(),这个宏通常被称为“断言”
assert(p != NULL);
//验证 p!=NULL 真假——为假不运行,给出报错
#include<assert.h>
int main()
{int* p = NULL;assert(p != NULL);return 0;
}
7.2 assert的优缺点
优点:
- 它不仅能自动标识文件和出问题的行号,还有一种无需更换代码就能开启或关闭assert()的机制
缺点:
- 因为引入了额外的检查,会增加了程序的运行时间