前言
指针是C语言的灵魂所在,指针重要性可见一斑。
1.内存和地址
这是在了解指针之前的必备知识,不了解内存和地址就开始介绍指针,类似于不打地基就开始建造房子的感觉。
内存:是我们电脑中的硬件之一,用于存储数据的硬件。
地址:为了便于管理内存,我们把庞大的内存空间(8GB/16GB/32GB)划分为每个为1字节的空间单元,并为每个空间单元赋一个编号,这个编号我们称之为地址。
至于如何实现为每个内存空间进行编址,主要是硬件层面进行实现的,我们不予详细说明。
2.指针变量和地址
2.1指针变量的介绍
在说明指针变量之前,首先要认识几个操作符
1.&取地址操作符,用来取出操作变量的地址。
2.*解引用操作符,用来解引用指针变量找到指针指向的空间。
什么是指针变量?
第一,需要明白,指针变量也是一种变量类型,这种变量类型是与整型类型,浮点型类型并列存在的。
第二,这种变量的特点是专门用来存放地址的。
int a = 10; //我想取出变量a的地址,那么我需要存储到指针变量当中 int * p = &a;
下面来简单解析一下如何理解**intp = &a;**这句语句。
==从右先左看,&a的意思是取出变量a在内存中的地址编号,=是把该地址编号赋给变量p,首先p与结合,表示p需要解引用是一个指针,然后解引用指向的内存空间的变量类型为int类型,所以*p的类型为int。==
那么我想说,如果指针指向的内容是char类型呢,指针的类型写什么?char*
如果指针指向的内容是longlong类型呢?指针的类型应该写作longlong*
那我想通过地址改变变量a中的内容可以吗?可以!
int a = 10; int* p = &a; *p = 20; printf("%d\n",a);
那有同学就想要问了,为啥不直接赋值20给a啊,而是通过这种类似于绕个弯的形式进行改变a的值?这里可以简单理解为多了一种方法,并且随着后面的学习就会发现指针的真正意义。
2.2指针变量的大小
前面说过,指针也是一种变量,这种变量也需要申请空间去存放内容啊,只不过指针变量把他里面的内容视为地址而已。
这里说一个关键字sizeof,是用来计算变量大小的关键字。
int a = sizeof(char*); int b = sizeof(int*); int c = sizeof(long long*); int d = sizeof(float*);
上面代码打印结果出来试一下就行了,结果发现都是一样的,4/8字节,明明指向的内容的类型有int有char还有longlong类型大小不一样,为什么指针反而大小一样呢?
其实是因为,指针存放的是地址啊,地址的编写都是依赖于x86或者x64环境的,x86环境下,有32个二进制位来编写地址,那么自然就需要32个bit来存放地址,也就是4个字节啊,x64环境下同理。
3.指针变量不同类型的意义
有人可能就奇怪了,既然每个指针变量类型在同一环境下大小一样,咱们只用一个指针变量不就可以了吗?干嘛用这么多乱七八糟的。
下面来简单解答一下大家的这个疑惑。
3.1不同指针类型的解引用不同
char类型的指针解引用访问一个字节,int类型的指针解引用访问4个字节。(下面是代码验证)
int a1 = 0x11223344; int a2 = 0x11223344; int*p1=&a1; char*p2=&a2; *p1=20; *p2=20; printf("a1=%d;a2=%d\n",a1,a2);
3.2不同指针类型的加减整数效果不同
char类型的指针加整数1就是跳过1个字节,int类型的指针+1就是跳过4个字节
int a = 20; int*p = &a; char ch = 'a'; char*cp = &ch; printf("%p\n",p); printf("%p\n",cp); p++; cp++; printf("%p\n",p); printf("%p\n",cp);
3.3void*指针
void*指针叫做空指针,写作NULL,包含在include<stdio.h>头文件中
这个指针有些特殊,该指针可以接收所有类型的地址,但不可以被解引用。
那有啥用?基本用于函数参数部分用来接收所有类型的数据传入。
4.const修饰指针
const是C语言中的一个关键字,是用来限制某个变量的。
const int a = 10;//这样在变量前面限制非指针变量之后,该变量内容不可被修改 const int* const p=&a;//在指针变量型号前面+const进行限制,该指针变量不可被解引用操作;在型号后面j+const修饰,该指针变量不可以更改内容。
5.指针运算
指针有三大运算规则
5.1指针±整数运算
该指针的运算规则是:看指针指向内容的类型大小,如果是char指针,指向的是char类型变量,+1就跳过1个char类型的大小的内存空间,int同理(如下图)
5.2指针-指针运算
该指针的运算规则是:两指针指向的地址相减,得到的是两个地址之间该指针类型指向的内容类型的元素个数的绝对值。
5.3指针的关系运算
怎么用呢?看你自己的意志哈。
简单说一下我们一般定义在函数内部(包括主函数)的变量都是存储在栈空间的,栈空间的使用是由低地址到高地址进行存储并且使用
比如我可以这样打印一个一维数组的内容(如下代码):
//指针的关系运算 #include <stdio.h> int main() { int arr[12] = {1,2,3,4,5,6,7,8,9,10,11,12}; int *p = &arr[0]; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); while(p<arr+sz) //指针的⼤⼩⽐较 { printf("%d ", *p); p++; } return 0; }
6.野指针
啥是野指针?说白了野指针就是指针使用不当的情况而已。
下面来简单说几个比较容易入坑的野指针情况:
6.1指针未初始化
int a = 10; int*p; *p=20;
6.2指针越界访问
int arr[]={1,2,3,4,5,6}; int*p=arr; int i = 0; for(i=0;i<6;i++) { printf("%d ",*p); p++; } //此时p已经指向了6之后的空间 *p = 20;//指针越界访问
6.3指针指向的空间被释放
void test(void) { int n = 20; return &n; } int main() { int *p = test(); *p=100;//此时p指向的空间已经被回收 return 0; }
7.如何预防野指针?
这个野指针呢只能减少出错,不能完全避免哈,下面是一些针对减少野指针发生的建议
7.1指针初始化
7.2小心指针越界
7.3指针不再使用时,及时置为NULL
7.4避免返回局部变量的地址
8.assert断言
assert是C语言中的一个关键字,包含在<assert.h>头文件中,用来检查C语言程序中的是否按照指定条件来运行的。
在DEBUG版本下,如果assert中的条件为真,那么返回非0的数值,并且什么也不会发生;如果条件为假,那么返回0并且报错,提示报错信息。在RELEASE环境下,assert断言会被直接优化掉。
assert的开关:在其头文件之前定义**#define NDEBUG**
其实有同学会感觉这个assert跟if语句差不多啊,其实是有一些差别的,if elseif是一个逻辑程序,不会报错提示,之后还不能被优化掉,还有没开关,相比之下,assert我感觉更有利于程序员检查自己的代码。。。
指针的意义体会——传值调用和传址调用的区分
前面说了一大堆介绍指针的基本用法,但是指针啥用啊?是不是多此一举?
不是!
下面来通过一个题目来简单体会一下:写一个自定义函数,用来调换定义在main函数中两个变量的值。
然后,有些同学讲就想了,这简单:
#include <stdio.h> void Swap1(int x, int y) { int tmp = x; x = y; y = tmp; } int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap1(a, b); printf("交换后:a=%d b=%d\n", a, b); return 0; }
嗯…乍一看好像没啥问题,试一下就发现不对。。。数值没有交换啊!
而是应该这样写:
void Swap2(int*px, int*py) { int tmp = 0; tmp = *px; *px = *py; *py = tmp; } int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap1(&a, &b); printf("交换后:a=%d b=%d\n", a, b); return 0; }
为啥啊?这两种写法有什么区别啊,非得用指针吗?
其实第一种写法,写的那个形参是实参的临时拷贝,说白了就是你又拷贝了一份新的变量把他俩交换了,然后又回去main函数中去看原先那份变量的值是否交换~
第二种写法,就是创建了两个指针变量去直接在自定义函数中追根溯源找到原本的那份变量进行交换。
可能同学现在比较懵哈,我画个图应该比较好理解了(里面的地址是我瞎编的哈):
第一种基本就是传值调用了,第二种是传址调用,现在体会到一点指针啥用处了吗?
8.2传址调用与传值调用的选择?
两种调用方式怎么选啊?
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。