文章目录
- 一、内存和地址
- 二、指针变量和地址
- 2.1 取地址操作符 '&'
- 2.2 创建指针变量
- 2.3 使用指针变量
- 三、指针变量类型的意义
- 3.1 指针的解引用
- 3.2 void* 指针
- 四、指针运算
- 4.1 指针 + - 整数
- 4.2 指针 - 指针
- 4.3 指针的关系运算
- 五、const修饰指针
- 5.1 const 修饰变量
- 5.2 const修饰指针变量
- 六、野指针
- 6.1野指针的成因
- 6.1.1 指针未初始化
- 6.1.2 指针越界访问
- 6.1.3 指针指向的空间释放
- 6.2 如何避免野指针
- 6.2.1 指针初始化
- 6.2.2 小心指针越界
- 6.2.3 避免返回局部变量的地址
- 七、assert断言
- 八、传值调用和传址调用
- 8.1 传值调用
- 8.2 传值调用
一、内存和地址
在计算机处理数据的时候,数据都需要先从内存当中读取出来,处理完后的数据还需放回内存当中;那么从内存中拿取数据处理完后又该如何放回到原来的空间呢?我们都知道,在我们购买电脑的时候有8G、16G、32G等更高的内存,那么这些内存空间如何高效的管理呢?
其实也就是把整个内存单元逻辑划分为多个小的内存单元,每个小的内存单元占1个字节,而每个小的内存单元都有一个独属于自己的地址,这些地址就像是一个个房间的门牌号,正是有了这些’‘门牌号’',计算机才能够准确且高效的对数据进行查找处理,处理完后再准确的放回
二、指针变量和地址
有了上面的概念后,我们就能更好的理解,在C语言中创建一个变量,其实就是向内存中申请一块空间,这片空间是这个变量独属的,比如:当创建一个int类型的变量a的时候,就会向内存申请一块四个字节的空间,用于存放int类型的变量a
或8个字节(x64环境)
int a = 5; int* p = &a;//创建一个指针变量pint表示取地址的对象a为int类型,所以在我们创建一个指针变量时,要根据取地址的对象的类型来确定指针变量的类型,而’ * '表示该变量为指针变量
2.3 使用指针变量
当我们创建了a的指针变量后,我们要通过p对a的值进行操作的时候,就需要用到 ’ * ‘解引用操作符,解引用操作符’ * ‘不同于创建指针变量时所使用的’ * ',一个是对指针变量p的说明,一个是对指针变量p的解引用
例如:
#include<stdio.h>intmain(){inta=5;int*p=&a;*p=10;//通过指针变量p,将a的值修改为10printf("%d",a);return0;}为什么通过指针变量p能够更改a的值呢?其实是因为p存放的就是a的地址,对p解引用后进行操作就是对a进行操作
三、指针变量类型的意义
3.1 指针的解引用
指针变量的大小和类型无关,在同一平台下,同一环境中,指针变量的大小都是相同的,既然大小都相同,为什么还要有各种各样的指针类型呢?
其实指针类型是有特殊的含义的,当指针类型不同时,所能操作的字节数也可能不同
例如:
//代码1:#include<stdio.h>intmain(){inta=0x11223344;int*p1=&a;*p1=0;printf("%d",a);}//代码2:#include<stdio.h>intmain(){inta=0x11223344;char*p1=&a;*p1=0;printf("%d",a);}当我们运行这两个代码的时候就会发现:
代码1会将a的4个字节全部改成0,而代码2只会将a的第一个地址改为0
由此,我们可以得到一个结论:指针的类型决定了对指针解引用时有多大的权限(一次能够操作多少个字节)
就比如上方的代码:int* 的指针解引用就能访问一个字节,而char*的指针解引用只能访问1个字节
3.2 void* 指针
在指针类型中,还有一种特殊的指针类型——void* 类型的指针,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接收任意类型地址。但是也有局限性,void*类型的指针不能直接进行指针的±整数和解引用的运算
例如:
#include<stdio.h>intmain(){inta=0x11223344;void*p=&a;*p=10;printf("%d",a);}当我们运行这段代码时,编译器就会报错
那么void* 的指针到底有什么用呢?
一般void* 类型的指针时使用在函数参数部分,用来接收不同类型的数据的地址,这样设计可以实现泛型编程的效果。使得一个函数可以用来处理多种类型的数据
四、指针运算
4.1 指针 + - 整数
因为数组在内存种是连续存储的,所以,当我们知道首元素地址的时候,就可以根据首元素地址+整数,从而找到后面的元素
例如:
#include<stdio.h>intmain(){intarr[]={1,2,3,4,5,6,7,8,9};int*p=arr;//数组名==数组首元素地址intsz=sizeof(arr)/sizeof(arr[0]);inti=0;for(i=0;i<sz;i++){printf("%d ",*(p+i));//指针+整数}}通过上面的代码就能打印出数组arr中所有的元素
需要注意的是,指针变量±指针跳过的字节数跟指针类型有关,比如一个int类型的指针变量+1就是跳过4个字节,一个char类型的指针变量+1就是跳过1个字节,所以在进行指针±运算时,一定要正确的判断该使用哪种类型的指针变量
4.2 指针 - 指针
当我们需要求一个字符串的长度的时候,就可以使用指针-指针运算
#include<stdio.h>size_tmy_strlen(char*ps1){char*ps2=ps1;while(*ps2!='\0'){ps2++;}returnps2-ps1;}intmain(){chars[]="abcdefg";size_tret=my_strlen(s);printf("%zu\n",ret);return0;}4.3 指针的关系运算
#include<stdio.h>intmain(){chars[]="abcdefg";char*p1=s;char*p2=&s[1];if(p1>p2)//p1和p2两个地址相比较,看哪个地址更大{printf(">");}else{printf("<=");}return0;}五、const修饰指针
5.1 const 修饰变量
const用于给变量加上限制,使得该变量无法被改变
例如:
#include<stdio.h>intmain(){constinta=10;a=20;}当我们对被const修饰的变量进行修改时,编译器就会报错
5.2 const修饰指针变量
const修饰指针变量,可以放在’ * ‘的左边,也可以放在’ * ‘的右边
当const放在’ * '左边的时候,指针所指向的内容被限制了,不能通过指针对指针指向的内容进行修改,但是指针变量本身的值可以修改
指针变量本身的值可以修改:
#include<stdio.h>intmain(){inta=10;intb=20;constint*p=&a;p=&b;printf("%d\n",*p);}当const放在’ * ‘右边的时候,修饰的是指针变量本身,保证了指针变量本身不能够被修改,但是指针指向的内容可以被修改
当然,const也可以同时存在与’ * '的左右两边,表示该指针变量本身和所指向的内容都不能被修改
六、野指针
野指针就是指向的位置是不可以预知的(随机的、不正确的、没有明确限制的)
6.1野指针的成因
6.1.1 指针未初始化
#include<stdio.h>intmain(){int*p;*p=20;printf("%p\n",*p);}像这样不初始化指针变量而直接使用时,编译器就会报错
6.1.2 指针越界访问
假设一个数组有10个元素,我们创建一个指针变量p,把数组首元素地址放进p里面,然后*(p+10),这样就会出现指针越界访问的问题,因为整个数组一共就只有十个元素,我们对*(p+10)其实就是指向了第11个元素,这样就超出了数组arr的范围,这就使得p成为了野指针,并且p所指向的内容是未知的
6.1.3 指针指向的空间释放
#include<stdio.h>int*test(){intn=100;return&n;}intmain(){int*p=test();printf("%d",*p);}当出test函数时,局部变量所申请的空间会返还给内存
6.2 如何避免野指针
6.2.1 指针初始化
当我们明确知道创建的指针需要指向哪里的时候,我们就可以直接对该指针变量进行赋值;而当我们需要创建一个指针,但还不知道这个指针需要指向哪里的时候,可以先赋值NULL。
NULL指针是C语言所定义的一个标识符常量,值是0,这个地址是无法使用的,读写该地址会报错
int* p = NULL;6.2.2 小心指针越界
一个程序向内存申请了哪些空间,通过指针也只能访问那些空间,不能超出访问范围,超出了就是越界访问
6.2.3 避免返回局部变量的地址
当我们调用一个函数时,不要使用该函数所返回的指向该函数内部局部变量的地址
七、assert断言
assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定的条件,如果不符合,就报错中职运行。这个宏常常被称为断言
例如:
八、传值调用和传址调用
设计一个函数,用于交换a和b的值
8.1 传值调用
运行这个代码后,发现,a和b的值并没有成功交换,为什呢?
其实是因为,a和b的值传递给了Swap函数,Swap函数内部创建了x和y两个变量用于接收a和b的值,但是,当我们分别打印x、y和a、b的地址的时候,就会发现,他们对应的地址并不相同,也就是说x、y其实是独立的空间,对该空间进行修改,并不会影响到a、b的值,所以当Swap函数调用完返回main函数后,a和b的值并没有交换
8.2 传值调用
因此,当我们想要交换两个值的时候,就需要用到传址调用,把两个参数对应的地址传递给Swap函数,Swap函数用两个指针变量来进行接收,这样,对x和y的修改也会影响到a和b
传址调用可以让函数与主函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所以,在以后写代码时,只是需要主调函数中的变量来实现计算的时候,就可以采用传值调用;如果函数内部要修改主调函数中变量的值,就需要使用传址调用
完