这一章我们主要讲解C语言指针的初阶,进阶版我会在讲解完结构体之后进行介绍。
主要分为以下七个模块:
目录
1.指针是什么
2.指针和指针类型
3.野指针
4.指针运算
5.指针和数组
6.二级指针
7.指针数组
指针是什么?
指针理解的2个要点:
1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,平常我们口中说的指针是指的是指针变量。
我们可以这样理解:
指针变量
我们可以通过&(取地址操作符)取出变量的内存真实地址,把地址可以存放到一个变量中,这个变量就是指针变量。(指针变量就是用来存储这种例如0xFFFFFFFF地址的)
代码:
#include
int main()
{
int a = 10;//在内存中开辟一块空间
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
//中,p就是一个指针变量。
return 0;
}
总结:指针变量,就是用来存放地址的变量。
注意事项:
~指针是用来存放地址的,地址是唯一标示一块地址空间的。
~指针的大小在32位平台是4个字节,在64位平台是8个字节。
接下来,我们讨论一下指针的类型。
我们知道,之前变量有各种类型,比如int整型、float单浮点型、char字符型等等...
那指针有没有类型呢?
答案是肯定的,它和变量一样,例如:
int* p =NULL;
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
指针都有对应的类型。
例如你想存储字符型数据'a'的地址,你可以
char c = 'a';
char* p = &c;
这样就成功的把数据a的地址存到指针变量p里面去啦。
其实:
char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址
and 等等...
那么指针类型的意义是什么呢?
1.指针 +- 整数
看下列一段代码:
#include
int main()
{
int a = 10;
int* p = &a;
char c = 'a';
char* pc = &c;
printf("%p\n", &a);
printf("%p\n", p);
printf("%p\n", p+1);
printf("%p\n", pc);
printf("%p\n", pc+1);
return 0;
}
我们输出它:
我们发现指针类型是int的时候,+1后的地址相当于加了4个字节(00DCFB8C到00DCFB90)。
当指针类型是char的时候,+1后的地址相当于加了1个字节(00DCFB77到00DCFB78).
我们不同类型的指针+1,它的地址向后加的字节都不一样。
而且我们发现,地址向后加的字节和其类型有关,int类型指针向后移动4个字节,char类型向后移动1个字节。还有double类型向后移动8个字节等等...,移动的字节数与其数据类型大小有关。
但是每个指针类型大小都是4个字节。不管是char*,int*,float*...等等,在32位平台都是4个字节,64位平台都是8个字节。
总结:指针的类型决定了指针向前或者向后走一步有多大(距离)。
2. 指针的解引用
看下面的代码:
#include
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;//将int类型强转为char类型
int *pi = &n;//正常取n地址
*pc = 0; //重点在调试的过程中观察内存的变化。
*pi = 0; //重点在调试的过程中观察内存的变化。
return 0;
}
我们先看此时n在内存中的存储:
可以看到n在内存中已经被存储好了
接下来 我们执行*pc = 0,,看看内存如何变化:
我们发现4个字节中只有一个字节被改为了0,剩下三个字节都没变动。
接下来执行*pi = 0:
我们发现四个字节都变为了0。
这是为什么呢?
答案是char类型指针解引用每次只对内存操作一个字节
而int类型指针解引用每次对内存操作4个字节。
类似的,double类型操作8个字节、float操作4个字节。
总结:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
概念:野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)。
我们看看它的成因:
3.1 野指针的成因
1.指针未初始化
在我们创建指针变量的时候,若未对指针进行初始化,便会造成野指针。
#include
int main()
{
int* p;//没有对指针进行初始化,默认为随机值
*p = 20;
return 0;
}
2.指针越界访问
这里很大可能就是出现在数组里面,访问时超出了数组的长度。
#include
int main()
{
int arr[10] = {0};
int *p = arr;//将arr首地址赋给p
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
//当i = 10的时候,就已经开始越界了,因为数组此时已经满了[0,1,2,3,4,5,6,7,8,9]
*(p++) = i;
}
return 0;
}
3.指针指向的空间释放
这里涉及到动态内存开辟,到时候我会提一下。这里简单说一下:我们利用malloc函数向内存申请了一片空间,最后我们程序结束了必须利用free函数释放掉它,否则会造成野指针(内存泄漏
)等问题。
如何规避野指针?
当然避免上述问题就可以了。
1.注意指针要初始化。
2.小心指针越界。
3.指针指向的空间释放的时候要将其置为NULL。
4.避免返回局部变量的地址。
5.指针使用之前检察有效性。
第4条我来举一个例子来说明:
int* test()
{
int a =10;
printf("%d",a);
return &a;
}
int main()
{
int* p = test();
*p =100;
}
我们可以看到,先从main函数开始,执行test函数,将a赋值为10,然后打印a,最后返回a的地址,我们用了指针变量p来接收这个地址,然后再将其解引用赋值为100,这一套过程看起来很行云流水,没任何问题。
但我们再仔细思考一下,便会发现问题。
我们知道,函数结束时所有在函数内部创建的局部变量都会被销毁,我们在函数里面创建了局部变量a,我们把a的地址返回了,但是与此同时,a这个变量也被销毁了。a本身这个变量已经不存在了,那么它的地址便已经没有意义,但我们却还利用这个地址进行操作,造成了非法访问,也是野指针的问题。所以我们要规避这种写法。
第五:指针使用之前检察有效性。
这句话的意思就是说每次使用指针的时候都进行一次判断,判断其是否为空。
例如:
#include
#include//为了使用exit函数
int main()
{
int a = 5;
int* p = &a;
if(p != NULL)//使用前对指针进行判断,是否为空
{
printf("%p",p);
}
else
{
exit(-1);//否则直接结束程序
}
return 0;
}
这就是第五点表达的意思了。
主要有三种运算:1. 指针 +- 整数 2. 指针- 指针 3.指针的关系运算
一.指针 +- 整数
在第二步的时候,我们也有所涉及了,加多少就是指针向后移动 (当前类型字节数)*移动的步数.
看下面这一段程序
#include
#include
int main()
{
int arr[5];
int* p;
//将初始值赋值为数组首元素的地址,然后每次循环将p的地址+1.注意这里的运算优先级,是先进行++,再进行解引用的,终止条件是当p的地址小于最后一个元素的地址时。
//这样相当于是把数组中每一个数全部初始为0了
for (p = &arr[0]; p < &arr[5];)
{
*p++ = 0;
}
for (int i = 0; i < 5; i++)
{
printf("%d ", arr[i]);
}
}
每一次我们将数组里的元素解引用并赋值为0,如此循环直到最后一个元素,结果应该全是0,我们输出一下:
完美符合。
当然。指针--就是向前进行移动
二.指针 - 指针
这个被通常用来结算字符串或数组的大小.
看下面这段函数:
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0' )
p++;
return p-s;
}
我们首先把字符串s传入了函数,将s的地址赋给p,将p循环直到'\0'(字符串结束标志),此时p即为字符串的末位,然后我们返回p-s,即末位元素地址 - 首元素地址.即字符串的大小.
例如我们将字符串“abcdef”传入,我们看一下返回结果:
答案是6,符合我们的说法。
三.指针的关系运算
这个也没什么可以说的,第一步也有所设计,就是指针之间的进行大小比较。但有个需要注意的地方,看下面代码:
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
*vp = 0;
}
这个也是将数组中每一位元素赋值为0,但它和前面的不同的是,他这是从后向前赋值.
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
所以还是尽量避免这种写法.
我们先来看下面一个例子。
#include
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
运行结果:
我们发现两个地址是一样的,这说明数组的地址和数组首元素的地址相同. 既数组名表示的是数组首元素地地址.
但并不是任何情况下数组名都代表首元素地址,有且仅有两种情况例外:
1.&数组名,这个时候数组名便代表整个数组的地址,虽然地址和首元素地址一样,但是意义不同,下面这个程序能很好的验证.
我们可以看到 数组名 和 &数组名 的地址一样
但是我们让arr + 1, 地址只增加了4个字节,说明增加了一个数组元素的长度.说明arr只是代表数组的首元素.
而&arr + 1, 地址却增加了40个字节,说明增加了一整个数组的长度,这也能说明&arr是取的整个数组的地址.
2.sizeof(数组名),此时数组名代表的是整个数组的地址。
下面来验证:
我们可以看到此时的arr也不是代表首元素,而是代表整个数组,所以大小是4 * 10 =40字节
而下面那个首元素地址的大小为4个字节,只代表一个元素,符合我们猜想.
接着刚开始的内容,如果数组名代表的是首元素的地址,那么这样写代码是可行的.
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址
根据前面所讲,假设p为首元素的地址,p+i就代表下标为i的元素的地址,我们再解引用就可以得到对应的值了。我们就可以利用指针来访问数组:
如下:
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组的大小,用数组总长度/一个元素的长度
int i = 0;
for (i = 0; i
输出结果:
这样就完整的用指针把数组输出出来了.
前面我们讲的是一级指针,就是把变量的地址取出来放到这个指针变量里面,但是是变量都应该有地址,这个指针变量的地址存放在哪里呢?这就是二级指针.
int main()
{
int p = 10;
int* pa = &p;//将整型变量p的地址存放到pa中.
int** pc = &pa;//将指针变量pa的地址存放到pc中,此时pc即为二级指针
}
那我们如何使用呢?
和一级指针一样,都是解引用,只不过想要修改原本的值需要解引用两次.
int main()
{
int p = 10;
int* pa = &p;
int** pc = &pa;
**pc = 20;//靠近pc的第一个*是解引用pc,此时得到的是pa,既p的地址,第二个*是对pa既对p的地址进行解引用,修改值为20
printf("%d\n",p);
}
可以看到结果已经被修改了.
数组元素类型 是 指针 的数组 被称为指针数组.
与此同时会有一个数组指针,这个会在后面进阶版的指针讲解,既指向数组的指针,现在不理解也没关系,后面的会细讲.
int main()
{
int arr[15];//我们平常这么创建数组的时候,一般最前面是元素类型,后面是数组名+数组数量,这个是一个数组大小为15、元素类型为int的数组.
char arr[15];//同样的道理,不过是char类型
}
同样地,既然是元素类型为指针,那我们也如法炮制的写出来。
int* arr[15];
那要具体怎么用呢?
int main()
{
int a=10,b=11,c=12,d=13,e=14;
int* arr2[5]={&a,&b,&c,&d,&e};//指针数组
for(int i=0;i<5;i++)
{
printf("%d ",*(arr2[i]));
}
}
我们把abcde的地址分别存在了arr2指针数组里了,最后我们输出的时候分别对其进行解引用.
完美的输出了出来。
还有什么用法呢?我们想到刚才所说的,既然数组的地址也是一个指针,那我们是不是可以把每个数组的地址分别存入到指针数组中?
#include
int main()
{
int data1[] = { 1,2,3,4,5 };
int data2[] = { 2,3,4,5,6 };
int data3[] = { 3,4,5,6,7 };
int* arr[3] = { data1,data2,data3 };
}
这样就把data1,data2,data3数组的地址全部放入到arr指针数组里面了。
如何访问它们呢?
当我们访问data1时是首元素的地址,我们刚才也讲到了可以利用指针访问数组。我们可以尝试将data1里面的数据输出出来.
#include
int main()
{
int data1[] = { 1,2,3,4,5 };
int data2[] = { 2,3,4,5,6 };
int data3[] = { 3,4,5,6,7 };
int* arr[3] = { data1,data2,data3 };
for (int i = 0; i < 5; i++)
{
printf("%d ", *(arr[0] + i));//arr[0]为data1数组首元素地址,加i就是下标为i的地址,最后进行解引用.
}
}
结果如下
当然要将data1-3全部输出出来,可以再在外面套一层循环:
指针数组到此为止啦~
到此,指针的初阶内容已经全部讲完,通过本篇如果能对指针有个最基本的认识,便算是成功!
如果有什么看不懂或有错的地方,欢迎私信哦.