前言:
都说会用一门语言几个礼拜就可以了。这句话我不敢苟同,至少在我学习C语言指针之后就不这么觉得了。
不信?来上才艺:
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
这两行代码出自《C陷阱和缺陷》
我相信大部分人在第一次看这俩行代码都是一脸懵逼。
是不是头皮发麻?这是啥东西?
如果你是这样,那么请收起你的骄傲,再也不要觉得C语言很“简单”,谦虚一点,好好学习!
如果不是,能一眼看出来是这俩行代码是什么意思的评论区告诉我,我给你点赞 (大佬抱抱)。
好了,其实无论你能否一眼看出来以上代码所表示的意思,我觉得都不应该轻视任何一门语言,编程世界,浩瀚无边,人外有天,天外有人。只有对知识怀着敬畏之心,知识才会源源不断的涌入你的脑袋,挤走水分。
打住!以下开始c语言指针的学习。
1.指针是什么?
要知道,我们的数据存放在计算机的内存里面,这些数据是非常多的,而要在这么多的数据里面找到我们所需的数据就需要对内存里面的每个单元编号,这样一来,每个内存单元都有了自己独立的编号,我们在存放数据以及查找数据时就只需要找到对应的编号在进行操作就可以了。
就像是在一栋二十楼的大厦里面找到张三的住处,如果不知道他的住房编号,那么就只能一个房间一个房间的查了,而如果有张三的住房编号,知道他住几楼几号,那找到他的房子就很简单了。
这样类似于房间编号的编码,就是地址,也就是指针。
既然每一个最小的内存单元都有一个地址,那么这个内存单元多大呢?
首先定义一个一维数组,因为一维数组的元素在内存中是连续存放的,每个元素的空间大小除以每个元素所占连续地址的数量,就是每一个地址所占得空间大小。
这里我们可以看到,a[0]的地址与a[1]的地址相差4,又因为int占四个字节,所以这四个字节都有一个地址。
计算机存储信息的最小单位,称之为位(bit,又称比特)存储器中所包含存储单元的数量称为存储容量,其计量基本单位是字节(Byte。简称B),8个二进制位称为1个字节,此外还有KB、MB、GB、TB等,它们之间的换算关系是1Byte=8bit,1KB=1024B,1MB=1024KB,1GB=1024MB,1TB=1024GB。
所以理解指针有两个要点:
1.指针就是最小内存单元的编号(地址),每个内存单元为一字节。
2.我们口头上表述的指针其实是指针变量,是一个用来存放地址的变量。
指针变量:
是一个存放地址的变量,返回值是指针类型,可以用取地址符&把地址取出来。(上面的代码有用到)。
好了,现在我们知道了指针变量就是存放地址值以及每一个地址都是一个字节的编号。
如何编址:
还有一个问题,就是这个地址是怎么来的呢?是如何编址的呢?
大概就是在计算机里面有一些地址线,如果是32位机器那就是32根线,每一根线在寻址的时候都会产生高电压或者低电压,也就对应着二进制的1和0,也就是说,这32根线可以组成多少个不同的01序列呢?2的32次方。
这些不同的01序列也就一一对应着一个地址,也就是有2的32次方个字节去编址。
0101001010101001010101001010………………
32个0/1位要用多大的空间去储存?1个字节8个比特位,那就是4个字节存放这么一串32位序列。
所以,一个用来存放地址的指针变量也就占4个字节的大小咯!(32位机器)。
那么2的32次方个字节是多大呢?
1GB=1024*1MB=1024*1024*KB=1024*1024*1024*bit=2^30bit.
所以2^32bit=4GB.
那么64位机器有64根地址线,能编多少个地址呢?
好大好大………
总结:
1.指针是用来存放地址的,地址是唯一的一块空间标识。
2.指针变量的大小跟机器操作位数有关,32位的话就是4个字节,64位的话就是8个字节。
2.指针类型:
指针有什么类型呢?给出以下类型:
char* pc = NULL;
int* pi = NULL;
short* ps = NULL;
long* pl = NULL;
float* pf = NULL;
double* pd = NULL;
指针变量的定义:类型 + *.
char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址等等。
那么问题来了,既然每个指针变量存放的都是地址,也就是一个编码,其本质上来说都是一样的,
2.1为什么还要给指针区分类型呢?
指针类型的意义主要体现在以下几个方面:
-
内存管理:指针类型允许我们动态地分配和释放内存。通过指针,我们可以在运行时分配内存块,并在不需要时释放它们,这样可以更有效地利用内存资源。例如,在C语言中,可以使用malloc()函数来动态分配内存,并使用free()函数来释放内存。
-
数据结构:在C语言中,指针类型非常适合用来构建复杂的数据结构,如链表、树和图等。通过指针,可以连接不同的数据节点,并通过指针进行遍历、插入和删除等操作,从而实现高效的数据操作。
-
函数传参:在C语言中,函数的参数传递通常是通过值传递的方式,也就是将实参的值复制给形参。但是对于大型的数据结构或者需要修改实参的情况,通过指针传递参数可以避免数据的复制,提高函数的执行效率,并且可以直接修改实参的值。
-
数组操作:数组在C语言中是通过指针进行访问的。数组名实际上是指向数组首元素的指针。通过指针可以对数组进行遍历、访问和修改等操作,使得数组操作更加灵活高效。
2.2指针加减一个整数:
char ch = 'a';
char* pc = &ch;
int num = 11;
int* pi = #
printf("ch地址 %p\n", pc);//输出char类型变量ch的地址
printf("ch地址+1 %p\n", pc + 1);//输出pc+1的地址
printf("num地址 %p\n", pi);
printf("num地址+1 %p\n", pi + 1);
指针的类型还在结构上决定了指针向前或者向后走一步有多大(距离)。
2.3指针的解引用
定义:指针的解引用是指通过指针访问或修改指针所指向的内存中存储的数据。当我们通过一个指针变量来间接访问它所指向的值时,就称为指针的解引用。
int x = 10;
int* p = &x; // p指向变量x的地址
printf("%d\n", *p); // 输出变量x的值,输出:10
*p = 20; // 修改变量x的值
printf("%d\n", x); // 输出修改后的变量x的值,输出:20
在上述代码中,通过使用"*p"来解引用指针p,我们实际上是在访问或修改p所指向的内存中的值,也就是变量x的值。
需要注意的是,当解引用一个指针时,要确保该指针已经被正确地初始化,且指向有效的内存位置,否则会导致未定义的行为。因此,在对指针进行解引用之前,经常需要对指针进行空指针判断或者有效性检查。
不同的指针类型解引用有什么区别呢?
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节
3.野指针
3.1什么叫野指针?
野指针(Wild Pointer)是指指向非法内存地址的指针变量。这种指针没有被正确初始化,或者指向的内存已被释放,因此不能安全地访问或修改其所指向的数据。
3.2什么情况会引起野指针问题呢?
1.指针没有初始化
在定义指针变量的时候没有给其赋予初值,也没有使其指向有效的空间地址,这样一个没有被定义的指针在被解引用的时候就会因为找不到其指向地址而产生不确定的后果,甚至会导致系统崩溃。
2.指针指向的空间被释放
在一片空间被释放的时候,如果没有将其指针置为NULL或者指向其他的地址,如果继续使用已释放的指针进行解引用操作,可能会导致访问无效的内存,造成程序错误或崩溃。
3.指针越界
当访问或者修改一个指针指向的内存块范围外的空间位置时,该位置也许是一个无效的内存,也有可能已经存放了其他的变量的数据,所以这样的访问可能会导致程序崩溃,数据损坏等错误。
3.3如何避免野指针的出现呢?
1、定义指针时初始化。
2、在使用指针的时候检查有没有越界。
3、在释放内存后,将对应的指针置为NULL。
4、在指针超出其作用域后,将其置为NULL,以免被误用。
5、使用前检查其有效性。
总的来说,为了避免野指针问题,应该养成良好的指针使用习惯。
4.指针运算
4.1指针加减整数
指针+整数:
int arr[5] = { 1,2,3,4,5 };
int* p1 = &arr[0];
for (int i = 0; i < 5; i++) {
printf("%p %d\n", p1 + i, *(p1 + i));
}
表示指针向地址增高的方向移动了若干个元素的距离,如果元素是整数,那么就移动4个字节的距离。
减法也是同理:
4.2指针减指针
int arr[5] = { 1,2,3,4,5 };
int* p1 = &arr[4];
int* p2 = &arr[0];
printf("%d\n", p1 - p2);
char str[] = "abcdefghij";
char pc1 = &str[0];
char pc2 = &str[9];
printf("%d\n", pc2 - pc1);
我们可以发现,两个指针相减得到的结果是中间的元素个数。
4.3指针的关系运算
指针是怎么进行比较的呢?
int arr[] = { 1, 2, 3 };
int* p0 = &arr[0];
int* p1 = &arr [1];
int* p2 = &arr[2];
//分别输出三个指针
printf("p0=%d\n", p0);
printf("p1=%d\n", p1);
printf("p2=%d\n", p2);
//比较三个指针,并输出表达式的值
printf("p0>p1=%d\n", p0 > p1);
printf("p0<p1=%d\n", p0 < p1);
printf("p0>p2=%d\n", p0 > p2);
printf("p0<p2=%d\n", p0 < p2);
printf("p2>p1=%d\n", p2 > p1);
printf("p2<p1=%d\n", p2 < p1);
我们发现,其实指针比较的就是地址大小,返回0表示假,1表示真。
我们可以利用这一点来比较数组元素的相对顺序。
值得注意的一点是,标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较。
5.指针和数组
大多数情况下,数组名和数组首元素的地址是一样的。
int arr[3] = { 1,2,3 };
printf("%p %p\n", arr, arr[0]);
所以数组名表示的就是数组首元素的地址。
只有两种情况例外:
1.sizeof数组名
int arr[3] = { 1,2,3 };
//printf("%p %p\n", arr, &arr[0]);
printf("%d\n", sizeof arr);//输出arr表示的字节大小
printf("%d\n", sizeof arr[0]);//输出首元素的字节的大小
我们可以看到,这个时候arr和arr[0]表示的意思不一样了,此时的arr表示的是整个数组,所以sizeof(arr) 也就是得到整个数组的字节大小。
2.&数组名
void test(int* p) {
printf("%d\n", sizeof p);
}
int main() {
int arr[5] = { 1,2,3,4,5 };
test(&arr);
return 0;
}
为什么此时的sizeof p=4呢?
其实这里的4表示是的是指针变量的空间大小,你换成char* 类型同样也是4。
当用数组名作为参数传参的时候,形参实际上上就是一个指针变量,sizeof 指针=4(32位机器)。
通过指针访问数组
既然可以把数组名当成一个地址存放在指针中,那我们就可以利用这个指针来访问这个数组。
int arr[5] = { 1,2,3,4,5 };
int* p = arr;
int sz = sizeof arr / sizeof arr[0];//得到数组大小
for (int i = 0; i < sz; i++) {
printf(" & arr[%d] =%p <====> p + %d = %p\n",i, &arr[i],i, p + i);
printf(" arr[%d] =%d <====> *(p + %d) = %d\n", i, arr[i], i, *(p + i));
}
所以 p+i 其实计算的是数组 arr 下标为i的地址,也就是说 *(p+i)=arr[i]。 那我们就可以直接通过指针来访问数组。
6.二级指针
看完以上内容相信我们已经初步知道了指针变量的由来以及用法。那么,问题又来了,既然指针变量也是一个变量,那指针变量又是存放在那里呢?指针变量的地址存在那里呢?
一级指针变量的地址存放在二级指针里。
来看以下代码:
int a = 10;
int* p1 = &a;//将变量a的地址赋给指针p1
printf("%p\n", p1);
int** p2 = &p1;//将指针p1的地址赋给p2
printf("%p\n", p2);
//分别进行解引用
printf("%d\n", *p1);
printf("%p\n", *p2);
printf("%p\n", p2);
我们可以看到,我们把变量a的地址赋给了指针p1,再把指针p1的地址赋给指针p2,这个时候解引用p1得到的是a的值,而解引用p2得到的是p1的值,也就是a的地址。
又因为p2存的是p1的地址,*p2得到的是变量a的地址,也就是说再对*p2解引用得到的就是变量a的值了。
int a = 10;
int* p1 = &a;//将变量a的地址赋给指针p1
int** p2 = &p1;//将指针p1的地址赋给p2
printf("%d\n", **p2);
输出10.
学习是一个循序渐进的过程,只有把这些指针的基本知识先了解了才能更好的深入了解指针,下一篇博客我将和大家更加深入的了解指针,感谢大家的支持!