目录
1.内存和地址
2.指针变量和地址
3.指针变量类型的意义
4.const修饰指针
5.指针运算
6.野指针
7.assert断言
8指针的使用和传址调用
1.内存和地址
补充:计算机中的单位(从小到大)
bit (比特位)Byte (字节)KB MB GB TB
1Byte=8bit
1KB=1024Byte
1MB=1024KB
以此类推
1.1内存
1.内存划分为一个个的内存单元,每个内存单元的大小是1个字节。
2.每个内存单元都有一个编号,有了这个编号,cpu就可以快速找到一个内存空间。
3.内存单元的编号== 地址 ==指针
2.指针变量和地址
地址打印用%p
2.1取地址操作符&
int main()
{
int a=20;
//变量创建的本质就是在内存中申请空间
//向内存申请四个字节的空间存放20这个数值
//(int)大小为四个字节
//这四个字节都有编号(地址)
//变量的名字仅仅是给程序员看的,编译器不看名字
//编译器是通过地址找内存单元的
&a;//拿到变量a的地址
//拿的是第一个地址
return 0;
}
2.2指针变量
那我们可以不可以 将a的指针存放进另一个变量中呢?答案是可以的,直接上图:
如上图,我们就成功的把a的指针存进了类型为int*的变量pa中,那么这个变量也叫做指针变量。
由此我们也得到了指针变量的概念:顾名思义,用来存放指针的变量。
小总结:
1.*表示pa是指针变量
2.int表示pa指向的变量a的类型是int .
举例 如果我们想创建一个char类型指针变量该怎么写呢?(下图a)
char b=0;
char*a=&b;
int main()
{
int a=20;
int*pa=&a;
*pa=200;//*就是解引用操作符
//(间接访问操作符)
//pa里面存的是a的地址,*pa就相当于
//通过pa中的地址找到a,
//甚至我们可以把*pa看作a,那么上述表达
//式就相当于把200赋给了a。
return 0;
}
2.3指针变量的大小
指针变量的大小取决于地址的大小。
如果是32位机器,有32根地址总线,存放一个地址就需要32个比特位,那一个指针变量大小也就是4个字节。
如果是64位机器,那一个地址就需要64个比特位,那一个指针变量大小也就是8个字节。
x86是32位,x64是64位。
指针变量的大小与数据类型无关。
3.指针变量类型的意义
3.1指针的解引用
答案:指针的类型决定了,对指针解引用的时候有多大权限(一次能操作几个字节)
比如:char的指针解引用只能访问一个字节,而int的解引用就能访问四个字节。
3.2指针 ±整数
结论:指针类型决定了指针向前或者向后走一步有多大(距离)
3.3void*指针
在指针类型中有一种特殊的类型是void类型,可以理解为无具体类型的指针(泛型指针),这种类型的指针可以用来接受任意类型地址,但是也有局限性,void类型的指针不能直接进行指针的±整数和解引用的运算。
4.const修饰指针
4.1const
const修饰的变量常变量,本质上还是变量,只是不能被修改。
int main()
{
int num=100;
num=200;
printf("%d\n",num);
return 0;
}
上述代码中num作为一个变量,它的值是可以改变的,但是如果我们不希望这个变量的值被篡改,那就可以在前面加上const。
如上图,这样编译器编译的时候就会认为num是一个常量(具有了常属性),无法修改。
但是:我们不能通过对num直接赋值来改变,但是可以通过指针来改。
int main()
{
const int num=100;
int*p=#
*p=200;
return 0;
}
这样num的值确实被修改了,但是我们还是要思考一下,我们用const修饰num就是为了不能被修改,如果通过指针修改就打破了const的限制,这是不合理的,所以应该让p拿到num的地址也不能修改num ,那怎么做呢?
4.2const修饰指针变量
一般来讲const修饰指针变量,可以放在**左边,也可以放在*右边,意义是不一样的。
int main()
{
int a=10;
int*p=&a;//将a的地址放入p
p//指针变量,里面有a的地址
*p//p指向的对象,即a
&p//p变量的地址
return 0;
}
int*p;//没有*修饰
(const)int const *p;//放左边修饰
int *const p;//放右边修饰
放在左边: (const) int const p;
限制的是p即指针指向的内容(上图的a),也就是说不能通过指针变量来修改它所指的内容。
但是指针变量本身是可以改变的,比如一开始存储的是变量a的地址,我仍然可以对存储的内容进行修改,比如换成b的地址。
**放在右边:**int * const p;
限制的是指针变量本身,指针不能改变它的指向(里面存的地址不能改了)
但是可以通过指针变量修改它所指向的内容,(比如里面放的a的地址,我依然可以*p=常数改变a的值)
5.指针运算
指针的运算有三种:
指针±整数
指针
指针
5.1指针±整数
因为数组在内存中是连续存放的,只要知道第一个元素的地址,顺藤摸瓜就能找到后面的所有元素。
我们可以利用指针±来打印数组,如下:
5.2指针-指针
指针-指针的绝对值是指针和指针之间元素的个数
int main()
{
int fxk[10]={1,2,3,4,5,6,7,8,9,10};
printf("%d\n",&fxk[9]-&fxk[0])
return 0;
}
指针-指针计算的前提条件是两个指针指向的是同一块空间
5.3指针的关系运算
6.野指针
概念:野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
int*p;
*p=20;//p就是野指针。
图6.1
如果把指针比作狗,那野指针就是心中没有装着主人(地址)的野狗。
6.1野指针成因
1.未初始化
如上图6.1,如果不初始化,p存放的值就是随机值,这时候解引用这个值(即地址)会造成非法访问。
2.指针越界访问
int main()
{
int fxk[10]={0};
int*p=&fxk[0];
int i=0;
for(i=0;i<=10;i++)
{
*p=i;//当p超过了数组的范围
//(申请的空间)就是野指针。
p++;
}
return 0;
}
3.指针指向的空间释放
6.2如何避免野指针
6.2.1指针初始化
如果明确知道指针指向哪里,就直接赋值,如果不知道,可以赋值为NULL.
NULL是c语言中定义的一个标识符常量,值为0,0也是地址,这个地址是无法使用的,读写该地址会报错。
为什么要这么做呢,你赋 了值指针变量里面起码不是随机值
6.2.2小心指针越界
你访问的值一定要是在申请空间内,不能超出范围访问。
6.2.3指针变量不再使用,及时置NULL,指针使用之前检查有效性。
7.assert断言
头文件:assert.h
作用:用于在运行中确保程序符合指定条件,如果不符合,就终止运行。
assert后面通常为一个表达式,如果表达式成立则无事发生,如果不成立则终止程序并报错。
如下例:
assert(p!=NULL)
上图中表达式意思为指针不为空,那使用assert的作用就是如果p不为空则无事发生,如果p为空,则报错并终止程序。
如果不需要assert来检查代码了,即确定代码没有错误,也不需要删除assert,只需要在头文件前面加上#define NDEBUG就可以了,它可以说是assert的“开关”,当加上这句话时,整个代码中的assert就全部失效。
#define NDEBUG
#include<assert.h
8.指针的使用和传址调用
8.1strlen的模拟实现
库函数strlen的功能时是求字符串长度,统计的是字符串\0前的个数。
8.2传值调用和传址调用
学习指针是为了解决问题,那么什么问题非指针不可呢?
8.1.1输入a,b的值然后进行交换
图1
这样的代码看似很完美,但实际运行的时候你就会惊喜的发现:你换了寂寞
原因:当实参传递给形参的时候,形参是实参的一份临时拷贝,对形参的修改不会影响实参
所以你在函数中对a,b嘎嘎一顿修改,没有用,对主函数中的a,b不会有任何影响。
图2
如上,通过指针进行修改就可以确保你做的是”有用功“。
图1就是传值调用,图2就是传址调用,
传址调用可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主函数的变量。
如果只是想需要主调函数中的变量值来进行计算,就可以采用传值调用,如果函数内部要修改主函数的中的变量的值,就需要传址调用。