目录
一、内存和地址
(一)内存
(二)内存单元
(三)地址
(四)拓展:CPU与内存的联系
二、指针变量和地址
(一)创建变量的本质
(二)取地址操作符:&
(三)指针变量和解引用操作符:*
1、指针变量
2、指针变量的理解
(1)【int* pa】的理解
(2)【int*】的理解
3、解引用操作符:*
(四)指针变量的大小
三、指针变量类型的意义
(一)解引用操作时,决定可以操作多少个字节
(二)指针 + - 整数时,向前/向后走多大的区别
(三)void* 指针
四、const修饰指针
(一)const修饰变量
(二)const修饰指针变量
五、指针的运算
(一)指针 + 或 - 整数
(二)指针 - 指针
(三)指针的关系运算(指针的比较)
六、野指针
(一)野指针造成的原因
1、指针未初始化
2、指针越界访问
3、指针指向的空间释放
(二)如何规避野指针
1、指针初始化
2、小心指针越界
3、指针变量不再使用时,及时置NULL,指针使用之前检查有效性
4、避免返回局部变量的地址
七、assert断言
八、指针的使用和传址调用
(一)指针的使用:strlen的模拟实现
(二)传值调用和传址调用
一、内存和地址
(一)内存
又称内存储器或主存储器,计算机中所有程序的运行都在内存中进行,计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,这样使用内存则需要高效地管理内存空间;
(二)内存单元
就是把内存划分为一个个的内存单元,每个内存单元的大小取1个字节(8个比特位),每个内存单元都有⼀个编号,有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间;
(三)地址
在计算机中我们把【内存单元的编号】也称为【地址】,C语言中给【地址】起了新的名字叫:【指针】
可以理解为:【内存单元的编号 == 地址 == 指针】
(四)拓展:CPU与内存的联系
有三条总线将CPU与内存连接彼此,交换数据:①地址总线;②数据总线;③控制总线
交换过程:地址信息通过【地址总线】被下达给内存,在内存上就可以找到相应的数据,将数据通过【数据总线】传入CPU做处理,【控制总线】则负责传递对数据的操作,如读操作、写操作等
二、指针变量和地址
(一)创建变量的本质
创建变量的本质是在内存中申请空间,例如创建一个 int 变量就是向内存申请4个字节的空间,每个字节都有自己的编号(地址),变量的名字仅仅是给程序员看的,编译器不看名字,编译器是通过地址找内存单元的
(二)取地址操作符:&
使用:拿到变量的地址
例如:
int a = 10;
&a;
&a 就可以拿到变量a的地址,虽然整型变量占用4个字节,我们只要知道了第⼀个字节地址,春藤摸瓜访问到4个字节的数据也是可行的
注:当一个变量占多个内存单元的时候,总会取出该变量的第一个内存单元(地址较小的那个字节)
(三)指针变量和解引用操作符:*
1、指针变量
通过取地址操作符(&)拿到的地址是⼀个数值,比如:0x0012ff40,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量中,例如:
#include <stdio.h>
int main()
{
int a = 10;
int * pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
}
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址
2、指针变量的理解
上面例子的写法中的 int *pa 拆开来理解:
(1)【int* pa】的理解
①【int *】是变量pa的类型;
② pa是一个变量,用来存放地址(指针)的,所以pa又叫指针变量
(2)【int*】的理解
① * 表示pa是指针变量;
② int 表示【pa 指针变量中保存的地址】所指向的【变量 a】的类型是int
3、解引用操作符:*
又称为间接访问操作符,用法:
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
此处*pa == a,相当于对a进行修改
return 0;
}
总结:通过【指针变量pa】找到指向的变量a—— *pa(通过pa的值,找到a)
① pa —— 指针变量
② &pa —— 指针变量pa的地址
③ *pa —— pa指向的变量a
(四)指针变量的大小
【指针变量类型的大小】取决于【地址的大小】,而地址大小由计算机是32位操作系统还是64位操作系统决定
① 指针变量是用来存放地址的,一个地址的存放需要多大空间,那么指针变量类型就是多大,所以32位平台总共有32根地址总线,每根线的电信号转化成数字信号后是1或0,那我们把32根地址总线产生的2进制序列作为一个地址,那么一个地址就是32个比特位,就是4个字节;同理,在64位的机器中,一个地址的大小就是8字节
② 地址的大小与【指向的原变量的类型大小】无关,就是4字节或者8字节
#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
return 0;
}
X86环境输出结果如下:
X64环境输出结果如下:
结论:
• 32位平台下地址是32个bit位,指针变量大小是4个字节
• 64位平台下地址是64个bit位,指针变量大小是8个字节
• 指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的
三、指针变量类型的意义
指针变量的大小和类型无关,只要是指针变量,在同⼀个平台下,大小都是⼀样的,都是4字节或者8字节,为什么还要有各种各样的指针类型呢?
(一)解引用操作时,决定可以操作多少个字节
如下演示:
#include <stdio.h>
int main()
{
int a = 0x11223344;
int* p = &a;
*p = 0;
return 0;
}
变量a的地址与4个字节的值如下:
经过 *p = 0;的语句后,4个字节的值全部改为0,如下:
若代码中指针变量的类型改为char*:
#include <stdio.h>
int main()
{
int a = 0x11223344;
char* p = &a;
*p = 0;
return 0;
}
变量a的地址与4个字节的值如下:
经过 *p = 0;的语句后,4个字节的值只有一个字节改为0,如下:
结论:指针的类型决定了,解引用操作时,决定可以操作多少个字节
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节
(二)指针 + - 整数时,向前/向后走多大的区别
如下代码演示:
#include <stdio.h>
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);
return 0;
}
代码结果如下:
从结果可以得出:char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节;
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)
补充:
int* pa;
pa+1——> +1 * sizeof (int)
pa+n——> +n * sizeof (int)char* pa;
pa+1——> +1 * sizeof (char)
pa+n——> +n * sizeof (char)总结:
类型* 变量名;
变量名 + 1 ——> +1 * sizeof(指针指向的变量类型)
(三)void* 指针
void* ——无具体类型的指针(泛型指针)
可以接收任何类型的地址,但是正因为他是泛型指针,所以没有特定类型指针的用法,即无法解引用和进行指针的 + - 操作;
作用:⼀般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得⼀个函数来处理多种类型的数据
四、const修饰指针
(一)const修饰变量
const修饰变量的时候,叫:常变量;
本质还是变量,只是不能被修改;
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量,若不想变量被直接修改,就使用const修饰变量起限制作用
#include <stdio.h>
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的
return 0;
}
上述代码中n是不能被修改的,其实n本质是变量(无法在数组长度中使用),只不过被const修饰后,在语法上加了限制,只要我们在代码中对n进行修改,就不符合语法规则,就报错,致使没法直接修改n
但是可以拿到n的地址,通过指针对它进行修改,但这是在打破语法规则
int main()
{
const int n = 0;
printf("n = %d\n", n);
int*p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
结果如下:
这里的初衷是不让变量改变,但是通过指针还是能打破const的限制,接下来就要对这一象限改进,直接对指针变量做const限制
(二)const修饰指针变量
⼀般来讲const修饰指针变量,可以放在 * 的左边,也可以放在 * 的右边,意义是不⼀样的
int * p;//没有const修饰
int const * p;//const 放在*的左边做修饰
int * const p;//const 放在*的右边做修饰
如下代码演示:
代码一:
int a = 10;
int b = 20;
int const * p = &a;
*p = 200;err
p = &b;√
代码一分析:
这个const限制的是 *p,即p指向的变量a不能改变;但是并没有限制p,所以可以修改p所指向的变量;
放在*的左边,限制的是指针指向的内容,也就是不能通过指针变量来修改它所指的内容;但是指针变量本身可以改变的
代码二:
int a = 10;
int b = 20;
int * const p = &a;
*p = 200;√
p = &b;err
代码二分析:
放在*的右边,限制的是指针变量本身,也就是指针变量本身不可以改变,但可以通过指针变量来修改它所指的内容
结论:const修饰指针变量的时候
const如果放在 * 的左边,修饰的是【指针指向的内容 *p】,保证指针指向的内容不能通过指针来改变,但是【指针变量本身 p】的内容可变;
const如果放在*的右边,修饰的是【指针变量本身 p】,保证了指针变量的地址指向不能修改,但是【指针指向的内容*p】,可以通过指针改变
五、指针的运算
指针的基本运算有三种,分别是:
• 指针 + 或 - 整数
• 指针 - 指针
• 指针的关系运算(指针的比较)
(一)指针 + 或 - 整数
因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺腾摸瓜就能找到后⾯的所有元素
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
#include <stdio.h>
//指针+- 整数
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]);
for(int i = 0; i < sz; i++)
{
printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
}
return 0;
}
注意:指针运算是指对 p 进行运算,而不是对*p,若对 *p 运算,就是对变量a运算了
在数组中,指针能够“顺腾摸瓜”的原因是:
①指针类型决定了【指针+1】的步长,和指针解引用之后的权限;
②数组在内存中的地址是连续的
错误演示代码:
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
char *p = &arr[0];
int sz = sizeof(arr)/sizeof(arr[0]);
for(int i = 0; i < sz; i++)
{
printf("%d ", *p);
p += 4;
}
return 0;
}
代码分析:
每次打印时,都让p += 4,在打印1~10时恰好正确,
每次访问都只会访问第一个字节,后面三个字节是直接跳过的,所以两位数的时候是正确的,但是数字大一些就会忽略掉第二个字节的数字,就会出错
(二)指针 - 指针
【指针 - 指针】的运算前提条件是两个指针指向的是同一个空间,否则运算无意义;
指针 - 指针的【绝对值】,是指针和指针之间【元素的个数】
应用:求字符串长度 ,如下代码演示:
#include <stdio.h>
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;
}
拓展:指针 + 指针?
答:无意义,类似于 【日期 +- 天数(计算日期)】、【日期 - 日期(算的是两个日期之间差多少天)】有意义,而【日期 + 日期】无意义
(三)指针的关系运算(指针的比较)
应用:做判断条件使用,数组中,若一个地址小于另一个地址,则执行语句
#include <stdio.h>
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;
}
六、野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
(一)野指针造成的原因
1、指针未初始化
指针变量也是局部变量,不初始化就会给随机值;
如果将未初始化的指针变量的值作为地址来进行解引用操作,就会形成非法访问
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
2、指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
3、指针指向的空间释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
(二)如何规避野指针
1、指针初始化
如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL,NULL 是C语言中定义的⼀个标识符常量,值是0(这个0在C语言中会被强制转化为void*类型),0也是地址,这个地址是无法使用的,读写该地址会报错
演示代码如下:
#include <stdio.h>
int main()
{
int num = 10;
int* p1 = #
int* p2 = NULL;
return 0;
}
2、小心指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问
3、指针变量不再使用时,及时置NULL,指针使用之前检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL;因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL
演示代码如下:
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
for(int i = 0; i<10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使⽤的时候,判断p不为NULL的时候再使⽤
//...
p = &arr[0];//重新让p获得地址
if(p != NULL) //判断
{
//...
}
return 0;
}
4、避免返回局部变量的地址
不要返回局部变量的地址
七、assert断言
assert.h 头文件定义了宏 assert ( ) ,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行,这个宏常常被称为“断言”
使用:#include <assert.h>;assert(表达式)
作用:判断是否符合指定条件,如果不符合就会终止运行;【通常用来判断指针变量的有效性】
判断:判断为真则程序继续向下走,判断为假则报错
int* p = NULL;
...
assert(p != NULL);
此处经过一些列的代码后,若 p 不等于NULL则正常运行下去,若还是等于NULL,则程序报错,终止运行
若想取消assert断言,则在#include <assert.h>上面 #define NDEBUG;
assert断言只在Debug版本中有效,在Release版本中会被优化掉
缺点:引入了额外的检查,增加了程序的运行时间
八、指针的使用和传址调用
(一)指针的使用:strlen的模拟实现
库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数
函数原型如下:
size_t strlen ( const char * str );
参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回长度;
如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直到 \0 就停止,代码如下:
size_t my_strlen(const char * str)
{
int count = 0;
assert(str);//为了保险,判断传来的是不是空地址
while(*str)
{
count++;
str++;
}
return count;
}
int main()
{
size_t len = my_strlen("abcdef");
printf("%zd\n", len);
return 0;
}
注:代码中的 const(不希望原值被修改)和 assert(保险判断)来加强代码使用时的健壮性(鲁棒性)
(二)传值调用和传址调用
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用,如果函数内部要修改主调函数中的变量的值,就需要传址调用
以上内容仅供分析,若有错误,请多多指正