1. 内存和地址
我们知道计算机在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中, 这其实是把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节。以学生宿舍为例,⼀ 个字节空间⾥⾯能放8个⽐特位,就好⽐同学们住 的⼋⼈间,每个⼈是⼀个⽐特位。而有宿舍就有门牌号,也就是地址,有了地址也就能快速找到对应的内存单元了。而在C语言中这类地址被称为了指针。可以理解为内存单元的编号==地址==指针。CPU就是通过地址才能在内存中找到对应的信息的 。
2. 指针变量和地址
2.1取地址操作符(&)
而在C语⾔中创建变量其实就是向内存申请空间
如图所示,我创建了整型a,它便在内存里占四个字节,而这四个字节又有各自的地址分别为0x006FFD70 0x006FFD71 0x006FFD72 0x006FFD73。
那么地址又该怎么拿出来呢?只需要一个&操作符便可以。
1 #include <stdio.h>
2 int main()
3 {
4 int a = 10;
5 &a;//取出a的地址
6 printf("%p\n", &a);
7 return 0;8
8 }
以这段代码为例,它就会打印出a所占4个字节中地址较⼩的字节的地址,006FFD70 ,
注意:打印址用%p
2.2指针变量和解引⽤操作符(*)
就像 我们会设置变量来存储一些值方便后面使用一样,取出地址后为了方便后续使用,我们也需要将地址存放在一个地方,那便是指针变量。
1 #include <stdio.h>
2 int main()
3 {
4 int a = 10;
5 int * pa = &a;
6 //取出a的地址并存储到指针变量pa中
7 return 0;
8 }
上图便是指针变量的创建,pa即为a的指针变量。 这⾥pa左边写的是int*,*是在说明pa是指针变量,⽽前⾯的int是在说明pa指向的a是整型(int),int*才是pa的类型。
同理,要是创建char b='a'的指针变量则为,char* pb=&b
即然已经可以保存地址,那么该怎么使用指针变量呢?这⾥必须用到⼀个叫解引⽤(*)的操作符。
int main()
{
int a = 100;
int* pa = &a;
printf("%d\n",*pa);
*pa = 0;
printf("%d\n",*pa);
return 0;
}
以这段代码为例,*pa的意思就是通过pa中存放的地址,找到指向的空间,此时其实*pa==a。
因此第一次打印的结果会是100,这是a变量原先的值。而第二次打印就会变成0,这是因为*pa修改了a变量的值。
2.3指针变量的⼤⼩
我们知道32位机器有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的⼤⼩就是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个字节
•注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。
3. 指针变量类型的意义
3.1指针的解引用
即然指针的大小和类型无关,那为什么指针又要有类型区分呢。
这分别是两次调试的结果
调试我们可以看到,代码1会将n的4个字节全部改为0,而代码2只是将n的第⼀个字节改为0。
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如:char*的指针解引⽤就只能访问⼀个字节,⽽int*的指针的解引⽤就能访问四个字节。
3.2指针+-整数
我们再来看一段代码
1 #include <stdio.h>
2 int main()
3 {
4 int n = 10;
5 char *pc = (char*)&n;
6 int *pi = &n;
7
8 printf("%p\n", &n);
9 printf("%p\n", pc);
10 printf("%p\n", pc+1);
11 printf("%p\n", pi);
12 printf("%p\n", pi+1);
13 return 0;
14 }
我们可以发现,char*类型的指针变量+1跳过1个字节,int*类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)
3.3void*指针
在指针类型中有⼀种特殊的类型是void *类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性,void*类型的指针不能直接进⾏指针的+-整数和解引⽤的运算。那么可能就有人要问了,即然无法进行解引用和+-操作,那它有什么用呢?事实上,void*指针可以作为函数参数,⽤来接收不同类型数据的地址,实现泛型编程的效果,后续如果想要继续解引用和+-操作也只需进行类型强制转换就行。
4. const修饰指针
4.1const修饰变量
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
以这段代码为例,m就是无法修改的,而n则可以修改,
但是如果我们换个思维,通过地址修改n的值呢?
这样就可以修改n的值了,但这样显然是无意义的,即然要修改,那么为什么一开始要在n前加const呢?有没有什么方法,可以令n值真正的无法修改呢?
答案当然是有的。
4.2const修饰指针变量
⼀般来讲const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不⼀样的。
我们逐个分析。
先看const在*号右边,const所固定的是其后面的变量,也就是pi,pi是一个指针,我们无法修改这个指针,也就是无法对其进行+-操作修改地址。
再看const在*左边,const固定的还是后面的变量,也就是int* pi,int* pi这个其实就是n,也就是说我们无法通过解引用操作来修改n的值。
最后看在两边,这就很好理解了,既无法解引用修改n的值,也无法进行+-操作修改地址
5. 指针运算
指针的基本运算有三种,分别是:
1:指针+-整数
2:指针-指针
3:指针的关系运算
5.1指针+-整数
以数组为例,我们只需要知道首元素地址和数组类型就可以知道后面元素的值。
如arr[3]=*(arr+3),值的注意的是,除了sizeof(数组名)和&数组名这两个的数组表示的是整个数组的大小和地址以外,所有的数组名都表示首元素地址,arr即为arr[0]的地址。
5.2指针-指针
以这段计算字符长度的代码为例,指针减指针计算的是结果是两个指针之间相隔的元素数量
需要注意的是,两个指针类型需要相同,在内存的空间也应连续
5.3指针的关系运算
可见指针也是有大小之分,也可以进行大小比较。
6. 野指针
6.1野指针成因
1:未进行初始化
2:指针越界访问
3:指针释放后未置空
6.2如何规避野指针
基本上上面三个错误不犯就行:)
7. assert断⾔
assert.h头⽂件定义了宏assert(),⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量p是否等于NULL。如果确实不等于NULL,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号,方便我们修改错误。甚至还有⼀种⽆需更改代码就能开启或关闭assert()的机制。如果已经确认程序没有问题,不需要再做断⾔,就在#include 语句的前⾯,定义⼀个宏NDEBUG。
这样编译器就会禁⽤⽂件中所有的assert()语句,减少运行时间,有需要再启动就好了。
8. 指针的使⽤和传址调⽤
8.1strlen的模拟实现
这里我就利用指针的相关知识实现strlen函数的模拟实现
这为strlen的函数参数,我们模仿它,如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是\0字符,计数器就+1,这样直到\0就停⽌。
如图
8.2传值调⽤和传址调⽤
讲了这么多,可能会有人觉得指针没啥用,不如直接修改原变量,拿外卖接下来就看看什么情况下非指针不可。
以这段代码为例,大家觉得a,b两值有进行交换吗?
答案是没有,这是为什么?我们将a和b传入x和y、x和y进行了交换,为什么a和b没有进行交换?
x和y确实接收到了a和b的值,不过x和y作为形参,x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b没法交换。这种将a和b的值传过去的就叫传值调用
结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。
那我们该如何实现交换的功能呢?只需传地址就好了。
这就成功实现了功能,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤。
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。