目录
前言
作为一名C程序员,学会指针是我们的首要关键,灵活运用指针又是对个人能力的体现,想从此刻学好指针请跟着博主来
指针是什么
在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向
(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以
说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元
总结:指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)
每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==
232/1024/1024MB==232/1024/1024/1024GB == 4GB) 4G的空闲进行编址。
同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算。
这里我们就明白:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所
以一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地
址。
总结:
1、指针是用来存放地址的,地址是唯一标示一块地址空间的。
2、指针的大小在32位平台是4个字节,在64位平台是8个字节
指针和指针类型
总结:指针的类型决定了指针向前或者向后走一步有多大(距离)。
指针作用域
int main()
{
int n = 0x11223344;
char *pc = (char*)&n;
int *pi = &n;
*pc = 0;
pi = 0;
return 0;
}
总结: 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的
在我们的调试窗口中可以看到,此时的pi,pc指针都指向着变量n,程序进一步往下走
注意:断点走到哪一行表示这一行还没执行,断点的上一行刚刚执行,此时的断点走到了11行,表示第10行已经执行,这个过程我们记录了*pc只改变了变量n在内存中的第一个字节,
而指针*pi却能改变四个字节
总结:char指针解引用就只能访问一个字节,而 int 的指针的解引用就能访问四个字节。
指针的关系比较
以下代码的作用是遍历修改数组中的每一个元素
#define N_VALUES 5
float values[N_VALUES];
float *vp;
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
}
眼尖的小伙伴可能会觉得这样一弄不是就越界了吗?
在这里可以注意的是下标为5的数组地址可以用来必较VP地址间的关系,这是因为标准规定
vp < &values[N_VALUES],指针的关系运算,两个指针之间比较大小
指针-指针
指针 - 指针 得到的数字的绝对值是指针和指针之间元素的个数,这句话是什么意思?
读代码理解
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%d\n",&arr[9] - &arr[0]);//9
printf("%d\n",&arr[0] - &arr[9]);//-9
return 0;
}
这句话的意思其实无非就是说,在一段连续的空间里(我这里是数组),有了地址头(&arr[0]),又有了地址尾(&arr[9]),这两个指针一减,得到的结果值其实就是我们的元素个数
模拟strlen函数(指针减指针)
以下是通过模拟strlen函数的实现,也是通过两地址相减,取得字符间的个数
int my_strlen(char *str)
{
char* start = str;//指针备份
if(*str != '\0')
{
str++;
}
return str - start;//两指针相减得到字符个数
}
指针和数组的关系
数组名是数组首地址,但是有两个例外
1、sizeof(数组名) - 数组名不是首元素地址,是表示整个数组,这里计算的是整个数组的大小,单位是字节
2、&数组名 - 这里的数组名不是首元素的地址,是表示整个数组的,拿到的是这个数组的地址 -
额外补充(图解):
以上观察貌似区别不大,似乎也不足以证明&arr就是整个数组的地址,看下一步
我们直到arr 跟 &arr[0]都是数组首地址(数组首元素地址),当它们 + 1后是跳过一个整形的地址,而&arr表示的是整个数组的地址,整个数组的地址不就是40字节吗?在窗口中可以看到光标选中的那一行,跟它的下一行之间是相差了40个字节的,因为16进制的28 就是十进制的40,这一点足以证明&arr取出的是整个数组的地址
既然可以把数组名当成地址存放到一个指针中,那么我们就可以使用指针来访问一个数组
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int len = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for(i = 0; i < len; i++)
{
printf("%d",*(p + i));
}
return 0;
}
二级指针
int main()
{
int a = 10;
int *p = a; //指针变量存放a的地址
int **pp = &p;//二级指针存放一级指针的地址
int ***ppp = &pp;//三级指针存放二级指针的地址
return 0;
}
上述代码可以这么理解和记忆
int *p = a ==》 *p表示的是一个指针变量,指针变量指向的对象类型是int类型
int* *pp = &p ==》 *p表示的是一个指针变量,指针变量指向的对象类型是int*类型
int** *ppp = &pp ==》*p表示的是一个指针变量,指针变量指向的对象类型是int**类型
以下主要讲解到二级指针的逻辑,因为二级指针已经是我们理解和使用的极限了,代码书写的时候并不是写的越复杂,才越是高质量,恰恰是以最能让别人能够看得懂才是优质的代码,因为这样大大便于软件的维护和测试
这是以上代码得到的测试结果,为了便于大家理解,简单的打个比方
1、假设我们现在有3个抽屉和2把钥匙
2、抽屉1存放的是局部变量a
3、抽屉2存放的是*p
4、抽屉3存放的是**pp
逻辑梳理:首先我们如果想改变局部变量a的值是可以的并且有两种方法,
方法一:直接修改变量a的值
方法二:通过一级指针*p拿到变量a的地址,从而修改变量a
方法三:通过二级指针 *pp 一解引用拿到了一级指针,再通过对一级指针解引用就会找到局部变量a,从而修改变量a的值
主要讲解二级指针:如果我们把二级指针看成一个抽屉的话,那么打开抽屉的第一时间就会找到一把钥匙,这把钥匙就是&p
钥匙对应打开的箱子是序号2吧,当我们打开了第二个箱子的话,此时2号箱子里存放的就是&a钥匙了,那有了这一把钥匙于是乎就可以再去打开1号箱子了,1号箱子里存放的是变量a,那么是不是就可以直接操作变量a了?答案是可以
指针数组
如何理解指针数组呢?
首先指针数组的本质上是一个数组,指针数组的属性是每一个元素都是一个指针变量,既然他是一个数组的话想必一定是一块连续存储的空间了,每一个元素都是一个指针变量?那么是不是每一个元素都可以用来存放一个变量的地址,指向这个变量?
#include<stdio.h>
int main()
{
int a = 0;
int b = 2;
int c = 4;
int *arr[3] = {&a,&b,&c};
int len = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < len; i++)
{
printf("%d ",*(arr[i]));
}
return 0;
}
注意不要写成 int len = sizeof(arr) / sizeof(*(arr[0]));这种形式,数组类型是整形数组时必然不会存在什么问题但是如果是char 类型呢,
在我们的调试窗口下可以很清晰的看得到len的值居然是12,所以不要写成这种方式的,原因也很简单,因为指针在32位系统下是默认占4字节,我们测试的环境就是在
32系统下,那么sizeof(arr)计算的是整个数组的大小(字节),而 sizeof((arr[0]))他是取出指针变量指向的那个地址上对应的值,由于这里涉及到隐士类型转换,所以最终sizeof((arr[0]))求得的是1个字节,12 / 1 = 12,答案不就出来了吗,这里交代的是一个细节问题
回归到正题想必大家应该知道这段代码的功能吧,没错就是依次取出数组中的指针,再对指针解引用,找到指针指向的那个变量逐个打印出来
看到这里大家应该已经明白了,指针数组是一个数组,由于指针数组的每一个元素都是一个指针变量,指针变量具有指向性
野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针的原因
1、指针未初始化
2、指针越界访问
3、指针指向的空间释放
避免野指针的方法
1、指针初始化
2、小心指着越界
3、指针指向空间释放即使置NULL
4、指针使用之前检查有效性
结构体
结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量
结构体的声明和定义
struct Stu //结构体的声明,创建一个结构体类型
{
char name[20];
int age;
char id[20];
};
struct Stu //创建一个结构体类型
{
char name[20];
char author[15];
float price;
}b1,b2; //创建两个全局的结构体变量
struct Point //创建一个结构体类型
{
int x;
int y;
}p1,p2; //创建两个个全局的结构体变量
int main()
{
struct Stu b1;//创建一个局部的结构体变量
struct Stu b2;//创建一个局部的结构体变量
return 0;
}
以上两种创建结构体变量的方式都是允许的,只不过再main中创建的结构体变量是存放在栈上的,而b1、b2、p1、p2是全局的结构体变量,全局变量是存放在静态区的,注意创建一个结构体类型时分号不能丢
创建结构体的偷懒式写法(简洁版)
例如描述一个动物
typedef struct Animal //创建一个结构体类型
{
char families[20] // 描述动物科
char skill[20] //动物技能
char breed[20] //动物的繁殖方式
}Animal;
int main()
{
struct Animal tiger //创建一个老虎对象
Animal mew //创建一个海鸥对象
return 0;
}
在主函数中使用以上两种定义结构体变量的方式都可以,这里值得一提的是Animal是一个类型使用typedef给整个结构体类型取了一个别名叫Animal,简单说明以下typedef关键字,这个关键字是给类型取别名的,比如int类型是一个整形,我也可以使用typedef给int类型取别名为intjj,此时的
intjj 表示的还是一个整形,未来在使用的时候intjj类型定义一个变量,这个变量就是一个整形
值得一提的是使用typedef定义出的新类型,只是一个类型而并不是一个变量,跟之前的在末尾定义结构体变量的意义是截然不同的,一种是声明结构体类型的时候定义结构体变量,而第二种使用typedef只是给原先的结构体类型取别名
结构体变量定义初始化
为了便于大家理解还是采用之前的代码
typedef struct Animal //创建一个结构体类型
{
char families[20] // 描述动物科
char skill[20] //动物技能
char breed[20] //动物的繁殖能力
}Animal; //结构体类型别名,表示的是同一种类型
int main()
{
struct Animal tiger = {"猫科","捕猎","***"};
Animal mew = {"欧科","飞翔","**"};
return 0;
}
是不是非常简单的初始化方式?没错
结构体嵌套
typedef struct birthday //生日
{
int year;//年
int month;//月
int day;//日
}birthday;
typedef struct Student //学生
{
int age;//年龄
char name[20];//姓名
birthday b1;
char occupation[20];//职业
}Student;
int main()
{
Student s1 = { 18,"张三",{2002,4,8},"经理", };//结构体嵌套初始化
printf("%d %s %d %d %d %s",s1.age,s1.name,s1.b1.year,s1.b1.month,s1.b1.day,s1.occupation);
return 0;
}
使用操作符访问结构体成员
typedef struct birthday
{
int year;
int month;
int day;
}birthday;
typedef struct Student
{
int age;
char name[20];
birthday b1;
char occupation[20];
}Student;
int main()
{
Student s1 = { 18,"张三",{2002,4,8},"经理", };
printf("%d %s %d %d %d %s\n",s1.age,s1.name,s1.b1.year,s1.b1.month,s1.b1.day,s1.occupation);
Student *p = &s1;
printf("%d %s %d %d %d %s\n", p->age,p->name,p->b1.year,p->b1.month,p->b1.day,p->occupation);
printf("%d %s %d %d %d %s\n", (*p).age, (*p).name, (*p).b1.year, (*p).b1.month, (*p).b1.day, (*p).occupation);
return 0;
}
结构体变量传参
1、值传递
2、地址传递
首先先看值传递
void print(Student s1)
{
printf("%d %s %d %d %d %s\n", s1.age, s1.name, s1.b1.year, s1.b1.month, s1.b1.day, s1.occupation);
}
int main()
{
Student s1 = { 18,"张三",{2002,4,8},"经理", };
print(s1);
return 0;
}
参数传递以值传递的方式,就是一次数据的拷贝(每次都会拷贝sizeof(结构体变量)),将实参s1拷贝到形参s1,值拷贝是会额外地分配一块空间的(sizeof(结构体变量)),结构体变量的大小(字节)一般来讲都会比较大一些,如果每次都是以值拷贝的方式将实参的成员变量挨个拷贝至形参的成员变量,这对于运行效率和空间占用来讲是会大大损耗的(不推荐),
解决方案:地址传递
地址传递
void print(Student *p)
{
printf("%d %s %d %d %d %s\n", p->age, p->name, p->b1.year, p->b1.month, p->b1.day, p->occupation);
}
int main()
{
Student s1 = { 18,"张三",{2002,4,8},"经理", };
print(&s1);
return 0;
}
但是如果是地址传递的话,那就不一样了,因为指针的默认字节数是看操作系统的,那如果是在32位平台下的话,指针默认4字节,每次都只需要传递一个地址过去,只占四字节,极大地提升了效率,减少了空间的浪费