🌳指针
🍁什么是指针
指针是内存中一个最小单元的编号,也就是地址
平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
简单来说就是,指针就是地址,我们所说的指针通常指的是指针变量
可以这么来理解,对于内存:
对于指针变量:
我们通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量 |
我们在来通过代码的形式理解:
总结:
指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理) |
指针变量的大小:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
简单来说:指针的大小在32位平台是4个字节,在64位平台是8个字节>
🍁指针类型与指针解引用
指针类型
变量有不同的类型,整形,浮点型等。那指针也有不同的类型,要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,我们给指针变量相应的类型。
指针的定义方式是: type + *
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
指针的类型决定了指针向前或者向后走一步有多大(距离),可以通过代码理解:
char加1地址就是加1,int加1地址就是加4
指针解引用
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。 |
🍁野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的),有时候你自己可能都会忽略野指针的问题
原因:
-
局部变量指针未初始化,默认为随机值
-
越界访问
既然说到了野指针的原因了,我们自然要说一下怎么去避免野指针的出现问题
- 指针初始化
- 小心指针越界
- 指针指向空间释放即使置NULL
- 避免返回局部变量的地址
- 指针使用之前检查有效性
🍁指针运算
- 指针±整数
#define SIZE 5
float arr[SIZE];
float *p;
//指针+-整数;指针的关系运算
for (p = &arr[0]; p < &arr[SIZE];)
{
*p++ = 0;
}
- 指针-指针
以模拟实现strlen为例
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0' )
p++;
return p-s;
}
- 指针的关系运算
for(p = &arr[SIZE]; p > &arr[0];)
{
*--p = 0;
}
化简
for(p = &arr[SIZE]; p >= &arr[0];p--)
{
*p = 0;
}
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。 |
🍁指针和数组
数组名和数组首元素的地址是一样的,数组名表示的是数组首元素的地址。(2种情况除外,sizeof(arr)与&arr)
数组名是地址,那就可以把数组名当成地址存放到一个指针中,我们使用指针来访问
可以看到两个地址是相同的,所以 p+i 其实计算的是数组 arr 下标为i的地址
🍁二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存便存放在二级指针
- *ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa .
- **ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a .
因此,以此类推,实际上,不止只是有二级指针,还有三级指针、四级指针等等…不过用到的地方可能会比较少。
🍁指针数组
存放指针的数组就是指针数组。后面还会有数组指针,不过本篇博客以基础为主,不在这里展开论述。
int* arr[5];
//arr是一个数组,有五个元素,每个元素是一个整形指针。
这就是指针数组,下面我们可以简单通过代码来认识一下指针数组:
代码一:
代码二:
这是普通的二维数组打印
下面通过指针数组与一维数组结合打印出二维数组的效果
好了,关于指针的一些知识就先介绍到这。下面,我们来认识一下结构体。
🌳结构体
结构体是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
🍁结构体的声明与成员的类型
结构体的声明
struct tag
{
member-list;
}variable-list;
//例如一个学生
//typedef:重命名
typedef struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}Stu;//分号
成员的类型
实际上,结构的成员可以是标量、数组、指针,甚至是其他结构体。
🍁结构体变量的定义和初始化及访问
有了结构体类型,那我们就该研究怎么去定义变量
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
一般可以在声明的时候去定义变量。下面我们可以来看看怎么去初始化:
//一、常规初始化
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
//二、嵌套类型
struct Node
{
int data;
struct Point p;//嵌套结构体
struct Node* next; //结构体的自引用
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
定义和初始化完之后,我们自然就要去访问了
结构体变量访问成员
- 结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数。
- 如果我们得到的不是一个结构体变量,而是指向一个结构体的指针,这时候通过(->)操作符进行访问
//上图源代码
#include <stdio.h>
struct Stu
{
char name[20];
int age;
};
void print(struct Stu* ps)
{
printf("name = %s\t age = %d\n", (*ps).name, (*ps).age);
//使用结构体指针访问指向对象的成员
printf("name = %s\t age = %d\n", ps->name, ps->age);
}
int main()
{
struct Stu s = { "zhangsan", 20 };
print(&s);//结构体地址传参
return 0;
}
🍁结构体传参
传参既可以传结构体,也可以传结构体地址>,下面进行代码演示:
可以看到两种传参方式不同,打印出来的效果却是一样的,但是有什么区别呢?对于上面的 print1 和 print2 函数哪个更好呢?
print2函数
其实在函数栈帧的时候就大概多多少少有涉及到,可以去看看我之前写的博客。函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
所以,对于结构体传参的时候,传结构体的地址较好。
💺结语
通过上面的学习,我们对指针和结构体有了一定的认识,好了,就先到这里结束。