目录
1.什么是指针
2.指针变量和地址
1.解引用操作符
2.指针变量类型的意义
3.void*指针
4.const修饰指针
1.const放在*左边
2.const放在*右边
3.指针的运算
1.指针加减整数
2.指针减指针
3.指针比较大小
4.野指针
1.没有给指针变量初始化
2.指针指向的空间释放
3.指针越界访问
5.assert断言
6.传值调用和传址调用
1.什么是指针
在c语言中指针就是地址;我们将内存中的空间划分成一个一个字节,每一个字节都有自己的地址;一个字节是8bit位;
我们可以把地址看作一个宿舍的编号,一个宿舍有8个同学;通过这个宿舍的编号可以找到里面居住的同学;
在32位机器上,地址大小为32bit(4字节);
在64位机器上,地址大小为64bit(8字节);
2.指针变量和地址
指针变量的形式:
int a=10;
int* pa=&a;//指针变量
指针变量也是一种变量,只不过它里面存的是地址,同时,只要将一个正整数存入里面,他就会将那个数字看作是地址;那么这种指针类型的大小是多少呢?
我们知道如果是32位机器的话,地址的大小一定是4字节;那么int*的指针是否是4个字节呢?
那指针变量有char*,int*,double*,那么他们的类型又是多大呢?
同时我们要知道,地址的大小事4个字节,那么无论是什么类型的地址,它都是地址,只要是地址在32位机器上就是4个字节,在64位机器上就是8个字节;
我们发现好像类型不相同,大小好像都还是一样的,那么我们指针变量还要有类型的区分呢?
1.解引用操作符
我们取出地址是要去使用的,那么如何去使用?
指针操作中一个很重要的操作符解引用操作符( * );比如上面我们要去通过这个指针变量pa对它指向的对象进行操作:
int main()
{
int a = 10;
int* pa = &a;
*pa = 20;
return 0;
}
解引用操作符的作用就是通过某个地址找到这块地址所属的空间;
2.指针变量类型的意义
前面我们提到了,既然类型不同但是大小还是一样,那为什么还要有这么多不同的类型呢?
这里我们先来看2组代码:
我们通过内存观察发现,指针的类型不同好像操作的范围也不同;char*的指针只能操作一个字节,int*的指针能操作4个字节;
然后再让我们来看一组代码:
我们发现刚开始pa和ps指向的都是a;但是int*的指针加一从c4变成了c8,char*的指针从c4变成了c5;前面我们知道内存的每一个字节都是有自己的编号的,那么是不是就说明int*的指针加一跳过了4个字节,char*的指针跳过了个字节;同理,short*加一跳过2个字节;减法也是同理;
通过上面2个代码的演示我们可以总结2条指针类型的意义:
1.指针类型决定了解引用操作进行访问的字节的最大权限(所能操作的字节个数);
2.指针类型决定了指针进行加一减一运算向前和向后的距离;
3.void*指针
这种类型的指针是无具体类型的指针;这种类型的指针无法进行加一减一的运算,同时也无法进行解引用操作;这种类型有点类型于一个大的垃圾桶,什么类型的指针都能往里面装;
我们知道等号2变的类型必须相等,不然就会报错;
但是void*的类型的指针就不会有这种报错:
void*指针一般在函数里面用的比较多,比如我们进行传参的时候我们也不知道我们传的是什么类型数据的地址,这个时候我们就可以用void* 来接收这个地址;这个我们后面再谈;
4.const修饰指针
const是c语言的关键字,可以用来修饰变量和指针;它可以使变量具有常属性(常量是不可修改的),相当于使得我们无法对这个变量进行修改,从语法形式上限制了我们;
虽然我们从语法形式上限制了变量a,但是我们可以通过地址来找a的空间并将它修改了;这是非常不安全的;
我们不想让这个指针变量通过地址来改变a,就可以通过给指针变量前面加上const来修饰它;
1.const放在*左边
const放在*左边是限制*pa;如果是这样写int const* pa效果是一样的,都是限制了*pa;这样就不能通过地址来改变这个值了;
2.const放在*右边
const放在右边就限制了pa所指向的对象不可修改;但是可以通过地址来修改地址所指向的那块空间;
总结:
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指
向的内容,可以通过指针改变。
3.指针的运算
1.指针加减整数
前面我们就提到了指针加减整数,不同类型的指针加减整数,向前和向后的距离不同,比如:char*的指针加一跳过一个字节,int*的指针加一跳过4个字节;
同理其他的类型也遵循这个规则;
2.指针减指针
这种一般用于同一块空间中进行计算;
数组名等于首元素的地址,arr+9说明此时指针变量指向了数组的最后一个元素;指针减去指针计算的是指针之间的元素个数;
3.指针比较大小
和数字一样,地址之间也有大小之分;
在数组中地址是由低到高变化的,栈区上的地址是从高往低使用的;
比较的方法和整数比较一样:
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]);
while (p < arr + sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}
4.野指针
在我们使用指针的时候一定要正确的使用指针,避免出现野指针;那么野指针又是什么呢?
准确来说野指针是没有明确的指向空间的,可能那块空间并不属于你,但是你通过野指针对那块区域进行操作;那么野指针是如何形成的呢?
1.没有给指针变量初始化
看过前面的栈区的创建和销毁那篇文章应该都知道,我们在创建一个局部变量的时候如果不进行初始化,里面放的就是cccccccc这种随机值;如果将这种随机值放在指针变量中的话,会默认将它看作是一个地址,如果此时我们对这个地址进行加一减一,解引用等操作,这是很危险的;那块空间并不是操作系统分配给我们的,而是一个随机的地址;
2.指针指向的空间释放
我们进行函数调用的时候,在里面所创建的局部变量出了函数作用域都是会销毁的;如果此时我们传回来了一个地址并且还用一个指针变量接收了这个地址;
这里我们虽然能运行起来并且打印的结果还是对的,是因为这个函数的栈帧空间没有被破坏,如果我们在前面随便打印一个东西,就会破坏栈帧空间,打印的就不是那个结果了;总而言之,返回栈空间地址都是不对的;
我们重点不在上面,当函数运行结束之后,函数的栈帧空间就会返回给操作系统,此时pa虽然拿到了那块地址的编号,但是并没有什么用,这块空间已经不属于我们了;此时,pa就是野指针;
3.指针越界访问
这里我们通过指针来访问数组的每一个元素,并把i赋值给每一个元素;当i=10的时候,此时指针已经指向了数组的外面,此时指针就是野指针,如果我们对这个地址进行操作,就是对野指针进行操作,这是很危险的;
在用指针的时候,我们自己要知道指针指向的地址是否是有效地址,避免出现返回栈区地址,指针越界访问等等问题;在我们不使用指针或者指针指向的地址的空间被销毁的时候,要及时将指针置为NULL(空指针);
5.assert断言
在使用指针的时候我们通常都会去判断指针指向的目标是否为NULL,会去判断指针的有效性;
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报
错终⽌运⾏。这个宏常常被称为“断⾔”。
我们可以看到assert在报错的同时还会告诉我们出错的文件路径,还要第几行出现的,非常的好用;同时,在我们不使用的时候还可以关闭它:
我们只需要在assert.h的头文件前面加上#define NDEBUG即可,一定要在它的前面加上才有用;
#define NDEBUG
#include <assert.h>
同时,在Release版本会自动屏蔽assert断言;
6.传值调用和传址调用
传值调用这个很好理解,就是传的数值;比如我们写的加法函数:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("%d", ret);
return 0;
}
这个时候我们传递的是a和b的数值,a和b是形参;修改形参不会影响实参;
传址调用就是传递地址,有时候我们仅仅有传值调用是没办法完成我们的目的的;比如,设计一个函数,让a和b的数值交换;此时,我们如果用传值调用的话是没法完成的,其1是形参无法改变实参,其二是函数的返回值有且仅有一个值;这个时候我们缺的就是如何于主调函数的a和b建立联系;这个时候就可以用传址调用;
void Fact(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main()
{
int a = 10;
int b = 20;
Fact(&a, &b);
printf("交换之后%d %d", a, b);
return 0;
}