引言
在学习指针之前我们必须先了解计算机中的常见单位。计算机中最小的单位是(比特位)bit,8个比特位组成一个字节(byte)。1KB=1024byte,1MB=1024KB,1GB=1024MB,1TB=1024GB,1PB=1024TB......
一,内存地址的介绍
-
什么是内存地址?
内存在计算机中主要用来存储和检索数据,内存在结构上看主要有存储体组成,存储体又被划分为若干个存储单元。每一个存储单元中只能存放一串2进制(0或1)信息,其长度一般为8个二进制位,也就是一个字节。每个储存单元相当于一间房,都对应一个(编号)门牌号,在计算机中我们称之为地址,通常用一个16进制数来表示,例如:0x0012FF33就是一个储存单元的地址。这个地址在C语言中还有一个名字成为指针。
当我们需要储存数据时,就会将其储存在内存单元中,并且用不同的标识符来标记这些内存单元。每当需要处理某个数据是,就通过标识符找到内存单元,进而对其中所储存的数据进行操作。
内存单元的编号==地址==指针
二,指针的引入
-
为什么要引入指针类型?
虽然在内存中每个内存单元都有一个地址,但其并不可见,我们不能直接操作其中的内容。因此,C语言引入了一种新的数据类型,称为指针类型(用 * 表示)。用这种类型定义的变量称为指针变量,指针变量是专门用来储存内存单元的地址的一种变量,我们可以通过取地址运算符(&)获取地址,将其存放在指针变量中,以后就可以使用指针变量间接的对内存单元中的数据进行操作了。
指针变量作为一种变量,其本身也占用内存空间,不论何种类型的指针变量,其只占用4个字节的空间。
三,指向单个变量的指针变量
-
如何定义指向单个变量的指针变量?
定义指向单个变量的指针变量需要使用某种合法的数据类型和*运算符。
例如:
其中 * 表明变量a,b,c,d......是指针变量,其类型为int,char,float,double......需要注意的是,要使用指针变量,在其创建时必须给其赋一个确定的地址值,即初始化,否则直接使用会出现错误。这样的指针称为野指针。
-
那么什么是野指针呢?
概念:野指针就是指针指向的位置是不可知的(随机的,不正确的)
-
野指针又是怎么出现的呢?
1.指针未初始化
2.指针越界访问
3.指针指向的空间被释放
-
如何为指向单个变量的指针变量赋值?
指针变量在使用前必须用内存单元的地址值为其初始化,不能使用某个整数常量。赋值时必须保证指针变量的类型与内存单元所存放的数据类型是一致的,并且也只有同类型的指针变量才能相互赋值。
例如:
如上所示,利用取地址运算符 & 获取变量a和变量b的地址,将其交给了指针变量pa和pb。
下面代码:
就是错误将int* 类型的指针变量 赋给了double* 类型的指针变量。
-
如何利用指向单个变量的指针操作内存单元的数据?
例如:
到此,我们除了可以学习了两种操作内存单元中的数据的方法
(1)使用变量标识符直接存取
(2)使用指针变量间接存取
-
空指针NULL和void* 类型指针的区别
空指针NULL
NULL是定义在stdio.h头文件中的一个宏名,其定义如下:
#define NULL ((void*)0),将数值0强制转换成void*,表面NULL是一个指针类型中的0值,空指针在内存中的地址为0x00000000,系统中将0地址视为不被使用的地址,这意味着它是一个无效的指针,不指向任何空间。
void*类型的指针
void*类型的指针指向了内存中的一块空间,只不过其类型是未知的,只有在使用时进行强制类型转换,改为所需要的类型即可。其可以转换成任意类型的指针。
四,使用指针访问数组
-
数组名的理解
有以下代码,arr数组中存放 着是个十个整形元素,我们可以通过&操作符,获取某个元素的地址,比如&arr[0],获取到了数组中下标为0的元素的地址。
1 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
2 printf("%p\n", &arr[0]);
3 printf("%p\n", arr);
4 printf("%d",sizeof(arr));
通过打印3,4行代码,我们发现数组名其实就是数组首元素的地址。在绝大多数情况下数组名其实就是数组首元素的地址,但有两个例外:
1. sizeof(数组名) 表示的是整个数组,计算的是整个数组的大小,打印上述第四行代码,结果为40,因为每个int型数据在内存中占4个字节,arr数组共有10个元素,结果就是40
2. &数组名 取出的是整个数组的地址,我们知道,整个数组的地址和数组首元素的地址打印出来其实是一样的,但其实际意义并不相同。
学到这,我们可能会有疑问,数组名和&数组名都表示数组首元素的地址,那么到底有什么区别呢? 分析完以下代码,我们对数组名,&数组名,有了更深刻的理解。
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%p\n", &arr);
printf("%p\n", arr + 1);
printf("%p\n", &arr[0] + 1);
printf("%p\n", &arr + 1);
return 0;
}
我们发现arr+1和&arr[0]+1的地址相同,因为它们都表示数组首元素的地址,+1都跳过4个字节;而arr+1和&arr+1的地址不一样,arr+1跳过了4个字节,而&arr+1跳过了40个字节,这是因为&arr获取的是整个数组的地址,+1跳过整个数组。
-
使用指针访问数组
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* pa = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
scanf("%d", pa + i);
}
for (i = 0; i < 10; i++)
{
printf("%d ", *pa + i);
printf("%d ", *(pa + i));
}
return 0;
}
因为指针变量pa存放的是数组首元素的地址,因此我们使用指针变量pa就可以对数组中进行操作了,
-
一维数组传参的本质
有以下代码
#include<stdio.h>
void test(int arr[])
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz2);
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz1);
test(arr);
return 0;
}
按理来说,我们将数组arr传递给test函数后,test函数内部计算出的数组arr中的元素个数sz2和在main函数中计算出的sz1的值都应该是10,因为sizeof(arr) / sizeof(arr[0])表示的是数组arr中的元素个数,但实际结果并不是我们所预期的那样。因为一维数组传递的参数的本质是数组首元素的地址。因此,我们也可以通过指针的形式对一维数组传参,指针的本质就是地址,只需将参数部分修改为int* arr即可。
-
什么是二级指针?
指针变量也是变量,有自己的地址,那指针变量的地址存放在哪?答案是二级指针。
#include<stdio.h>
int main()
{
int a = 1;
int* pa = &a;
int** ppa = &pa;
return 0;
}
这里的ppa就是二级指针,存放的是一级指针pa的地址,对ppa解引用*ppa找到的就是pa,那么同理,对*ppa解引用**ppa找到的就是a,他们的顺序是:先一个*找到pa,再来一个*即*pa找到的就是a。
-
什么是指针数组?
从其名字上来看,指针数组,就是存放指针的数组,数组中的每一个元素都是指针。如图所示
五,字符,数组,函数指针变量
-
字符指针变量
字符指针变量是用来存放字符串的地址的变量。
#include<stdio.h>
int main()
{
char* pc;
char ch = 'a';
pc = &ch;
printf("%c\n", *pc);
*pc = 'c';
printf("%c\n", *pc);
return 0;
}
单个字符:上述代码中,我们定义了一个指针变量pc,类型为char*,通过&获取字符变量ch的地址,存放到pc中,后面我们就可以直接使用指针来操作ch了。
#include<stdio.h>
int main()
{
char *str = "abcdef";
printf("%c\n", *str);
printf("%c\n", str[0]);
printf("%s\n", str);
return 0;
}
字符串:C语言中的字符串实际上就是char类型的一维数组,字符串常量和数组名一样,也是被编译器当成指针来对待的,它的值就是字符串的首元素的地址。因此字符指针变量*str存放的其实是首字符的地址,*str的打印结果为a,又因为它类似于数组,在内存中是连续存放的,我们可以通过下标来找到它的位置,打印str[0]其实就是打印字符串的第一个元素。我们想打印整个字符串的内容,就可以使用%s。
-
数组指针变量
数组指针变量是用来存放数组的地址的,能够指向数组的指针变量。
格式如下:
指向的数组的类型(*指针变量名) [指向的数组的元素个数],例如:int (*p)[10]。
因为()的优先级是高于 [ ] 的,所以*p表示我们定义了一个指针变量,[ ]表示是数组,int表示数组中的元素类型是int型,而除去变量名p后,int (*) [10]就是数组指针的类型。
- 数组指针变量的初始化及其使用:
#include <stdio.h>
int main()
{
int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*pa)[10] = &a;
for (int i = 0; i < 10; i++)
{
printf("%d ", (*pa)[i]);
//printf("%d ", *(*pa+i));
}
return 0;
}
通过&a操作符获取整个数组的地址(这里不能直接使用a,我们知道数组名实际上就是数组首元素的地址,而不是整个数组的地址),然后将其存放到数组指针变量中,这样就对其完成了初始化,接下来我们可以通过数组指针来访问数组中的元素了,for循环中的打印结果为:
-
函数指针变量
我们想将函数的地址储存起来就得用到函数指针变量。
格式如下:
数据类型 (*指针变量名)(函数参数列表),例如:int (*pfun)(int,int)。
pfun为函数指针变量名,前面的int是pfun指向函数的返回类型,(int,int)是pfun指向函数的参数类型和个数,除去变量名pfun后,int(*)(int,int)就是函数指针变量的类型。
- 函数指针变量的初始化及其使用
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 2;
int b = 3;
int(*p_Add)(int, int); //函数指针的创建
p_Add = Add; //初始化
int ret = (*p_Add)(a, b); //调用
int ret1 = p_Add(a, b);
printf("%d\n", ret);
printf("%d\n", ret1);
return 0;
}
上述代码就用到了函数指针,求两个数的和。在调用函数的时候,可以使用函数指针变量,也可以使用函数名,但实际上函数名和数组名实际上都不是指针,但是在使用时可以退化成指针,编译器可以帮助我们实现自动的转换,因此第二种方式也可以编译通过。