1.指针的定义
计算机的CPU在处理数据时,其所需的数据是从内存中读取的,而为了准确无误的读取其所需要的数据,内存被划分为一个个内存单元,每个内存单元都是一个字节,而每一个内存单元都有自己的一个编号,这就方便了CPU快速地找到需要的数据。这一个内存单元的编号就是C语言中的指针。
2.指针变量和地址
在C语言中,每当创建一个变量的时候,就是向内存申请了一块空间,这块空间用来存放你所创建的变量。
int main()
{
int a = 0;
return 0;
}
这一块就是内存划分给变量a的地址。
取地址操作符&
如何得到这一块地址的编号呢?这里就用到了取地址操作符(&)了。
#include<stdio.h>
int main()
{
int a = 1;
printf("%p", &a);
return 0;
}
一个int类型的整数占4个字节,&a取出来的是较小的第一个字节,有了第一个字节的地址,a的位置推一下就能知道了。
指针变量
上面取出了a的地址,如果以后要用到a的地址,那么这时就需要一个变量来存储a的地址,这个变量就叫做指针变量。
int main()
{
int a = 1;
int* p = &a;
return 0;
}
指针变量也是一种变量,存放在指针变量里面的值都会被理解为地址。
那么这种变量的类型就是变量名称的前面的东西,例如上面p的类型就是int*
3.解引用操作符
2中指针变量的初始化时用到了一个*号,这就是一个解引用的操作符。
我们拿到了一个变量的地址时,如果想要根据这个地址找到它所指向的变量并使用这个变量,那么我们就要用到解引用操作符。
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
}
*pa的意思就是通过pa这个地址,找到其所指向的变量就是a,*pa就是a,然后*pa=0,其实就是把a的变量赋值为0,可以到编译器运行一下结果,看一下是不是a的最终值变成了0。
4.指针的大小
在32位机器中,共有32根数据总线,一个数据总线传输一个bit位大小的信息,那么32根就是32bit,就是说在32位机器中,一个指针的大小就是4个字节。
按照上面的说法,在64位机器中,一个指针的大小就是8个字节。
#include <stdio.h>
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位 64位环境下去观察一下上述类型的大小是不是理论所说。
注意:指针变量的大小只跟位数环境有关,跟其是什么类型的指针变量无关。
5.指针变量类型及其意义
指针的变量的分法就是数据类型后面加上一个解引用操作符,那分指针变量类型肯定不是闲来无事,肯定具有它存在的意义。
#include <stdio.h>
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
#include <stdio.h>
int main()
{
int n = 0x11223344;
int *pa = (char *)&n;
*pa = 0;
return 0;
}
上述两段代码唯一的不同就是第二段代码中pa本该是一个int*型的指针,那后面加了一个(char*),给它强转成了一个char*类型的指针,下面在对其进行解引用操作,第一段代码类型是int*,那么它可以一下子修改四个字节的空间,就是把地址改为了0x00000000,第二段代码是char*类型的指针,那么它只可以修改一个字节的地址,这就是两个不同类型的指针所有的不同的性质。大家可以去试一下最终结果。
指针和整数运算
有了上面对指针类型的了解,指针与整数的运算也就比较好理解了。
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
可以看到char*类型的指针加1后会跳过一个字节
而int*类型的指针加1则会跳过四个字节。
特殊类型的指针
在指针类型当中存在着一种特殊类型的指针void*,它是可以接受任何类型变量的地址,但是它却没有自己的大小,它不能进行解引用操作,也不能与整数进行运算。
不能进行这一系列运算,那void*是用来干嘛的呢?
void*指针一般会出现在函数参数的部分,用来接受不同类型的地址,可以使一个函数来处理不同的类型的数据。
const修饰指针变量
变量是可以修改的,那么如何让变量不可被修改呢?就是加上一个const,用const修饰变量。
#include <stdio.h>
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的
return 0;
}
但是这样可以绕过变量,把变量交给一个指针,通过指针同样可以修改变量的值。
#include <stdio.h>
int main()
{
const int n = 0;
printf("n = %d\n", n);
int*p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
如果想让这个变量不可被修改,那么就要学会用const修饰指针变量。
const在修饰指针变量时,const放在*左边和右边是不一样的。
void test2()
{
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;//ok?
p = &m; //ok?
}
const 放在*左边时,解引用指针来改变变量值的操作不被允许。
void test3()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = 20; //ok?
p = &m; //ok?
}
const在*右边时,通过修改地址来改变变量值的操作不被允许。
void test4()
{
int n = 10;
int m = 20;
int const * const p = &n;
*p = 20; //ok?
p = &m; //ok?
}
而在两边都有const时,两种方法均不被允许。
所以可以得出一个结论:const修饰谁,谁不能被修改。
6.指针的加减运算
1.指针+-整数
根据上述指针变量类型及其意义,可以用int类型的指针加减整数打印int类型的数组。每加一就会跳过四个字节指向数组中下标+1的元素然后打印,从而实现了指针加减整数打印数组。
#include <stdio.h>
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]);
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));//p+i 指针加减整数
}
return 0;
}
2.指针加减指针
指针和整数的运算最后的值还是指针,指针与指针运算得到的值是两指针之间元素的个数。
模拟实现strlen
#include <stdio.h>
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0' )
p++;
return p-s;
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
函数内部创建一个指针变量让其等于传来的指针变量,接着让创建的指针p++,直到遇见‘\0’结束,然后返回p-s即是元素的个数。
7.野指针
所谓野指针指的就是不合法的指针(指向的位置不可知),这些指针可能会对内存非法访问。
野指针的成因
1.未初始化
#include <stdio.h>
int main()
{
int *p;//
局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
2.越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
*(p++) = i;
}
return 0;
}
arr大小只有10,i==11的时候就越界访问了,此时p就是野指针。
3.指向的空间被释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
这里令*p等于一个函数的返回值,函数在运行完毕以后便归还了其创建的变量,那块空间被释放掉了,所以*p接收到的是一块没有东西的空间,那p就成了野指针。
注:我们应该养成良好的习惯,规避野指针的形成。要给暂时不用的野指针初始化置为NULL,不要越界访问,在用完指针不再使用时,及时置为NULL。
再介绍一种方法来规避野指针。
assert断言
需要用到头文件<assert.h>
assert(p != NULL);
代码运行到次就会判断()里面的语句,如果为真,那么接着往下运行,如果为假,那么程序中止。
当写完代码没有问题了,如果想要取消assert的功能,可以直接用#define NDEBUG来取消断言。