1.指针是内存中一个最小的编号,也就是地址
2.平时口语中说的指针,通常指的是指针变量,用来存放内存地址的变量
如上图,通过&
(取地址符号)取出变量的内存起始地址,把地址可以存放到一个变量中,这个变量就是指针变量,我们通过解引用,可以使用指针变量中的地址所对应的空间。
我们将地址存放在指针变量当中,那么指针变量的大小是多少呢?一个小的内存单元是多大呢?首先来说,一个地址所占的空间为一个字节,他就是内存当中最小的单元。至于指针变量的大小,那就与操作系统有关,如果是32位的环境下,我们机器就会有32根地址线,那么每跟地址线在寻址的时候会产生高电平和低电平,就是1或者0,那么32跟地址线产生的地址就是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
…
11111111 11111111 11111111 11111111
所以在32位的机器上,地址位32个0或1组成二进制序列,一个二进制位就是一个比特位,一个字节占八个比特位,所以在32位的机器上,一个指针变量的大小为4个字节,同理在64位的机器上,指针变量的大小为8个字节。
我们都知道变量有不同的类型,那指针变量有没有类型呢?答案是也是有的,比如
char* pc = NULL;
int* pc = NULL;
short* pc = NULL;
long* pc = NULL;
float* pc = NULL;
double* pc = NULL;
我们可以了解到,指针的定义方式是:type + *,指针变量都是用于存储地址的,为什么还要区分类型呢,指针类型的意义在哪里?听我讲给你听!
int main()
{
int i = 20;
int* pi = &i;
char* pc = &i;
printf("pi = %p\n", pi);
printf("pc = %p\n", pc);
pi++;
pc++;
printf("pc = %p\n", pc);
printf("pi = %p\n", pi);
}
我们可以由输出的结果判断,指针的类型决定了指针加减后向前走或者向后走的步伐有多大。比如char*
这个类型的指针加一后,向后挪动一个字节,因为char
类型占一个字节,而int*
加一后向后挪动四个字节,因为int
类型占四个字节。
int main()
{
int i = 0x11223344;
char* pa = &i;
*pa = 0;
}
如图我们想将0赋给i,通过字符指针获取地址后,解引用,我们通过调试发现,我们仅仅改变了一个字节里面的值,其他值并没有变,而当我们用整形指针时
int main()
{
int i = 0x11223344;
int* pb = &i;
*pb = 0;
}
四个字节里面的值都被我们改成了0,所以我们可以说指针的类型决定了,对指针解引用时有多大的权限。(能操作几个字节)。
总结一下:
指针类型的作用:
1.指针的类型决定了指针向前或者向后走一步有多大(距离)。
2.指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
野指针就是指针指向的位置是不可知的,(随机的、不确定的、没有明确限制的)。
例如:
1.指针未初始化
int main()
{
int* p;
*p = 20;
return 0;
}
2.指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i < 11; i++)
{
*p++ = i;
}
return 0;
}
当我们的指针指向的范围超出数组arr的范围时,p就是野指针。
那我们该如何规避野指针呢?
- 指针初始化
- 小心指针越界
- 指针指向空间释放,及时置NULL
- 避免返回局部变量的地址
- 指针使用之前检查有效性
例如:当我们不知道指针变量指向哪里时,我们可以初始化指针变量
int main()
{
int* p = NULL;
}
我们可以设计一些条件来自查,指针是否为野指针
int main()
{
int a = 10;
int* p = &a;
if (p != NULL)
{
*p = 20;
}
}
还有其他的方法大家可以在练习中自己感受。
指针的地址加上或者减去指针类型乘整数的值。利用这样的性质我们可以使用指针遍历数组。
int main()
{
int arr[4] = { 1,3,4,5 };
int* p = arr;
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d", *(p + i));
}
return 0;
}
我们可以通过指针减指针获取两个指针之间元素的个数。我们可以通过这个性质来模拟实现strlen()
库函数。注意:两个指针相减的前提是:指针指向的同一块连续的空间。
int my_strlen(char* arr)
{
assert(arr);
char* p = arr;
while (*p != '\0')
{
p++;
}
return (p - arr);
}
int main()
{
char arr[10] = "abcd";
int len = my_strlen(arr);
printf("%d", len);
return 0;
}
指针与指针也可以比较大小,值得我们注意的是指针比大小的标准:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
int main()
{
int arr[10] = { 0 };
printf("arr = %p,&arr[0] = %p", arr, &arr[0]);
return 0;
}
由此我们可以看出,数组名其实就是数组的首元素地址,我们可以通过指针的形式来访问数组。
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<sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
因为指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里呢?当然是二级指针里了。
int main()
{
int i = 0;
int* p = &i;
int** pp = &p;
}
i的地址存放在p中,p的地址存放在pp中,p是一级指针,pp是二级指针。
对于二级指针pp,我们**pp
先通过*pp
找到p,然后对p进行解引用,找到i。
指针数组,到底是指针还是数组呢?答案是数组,存放指针的数组
int main()
{
int a = 0;
int b = 1;
int c = 2;
int* arr[3] = { &a,&b,&c };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%p ", arr[i]);
}
return 0;
}
如图就是一个指针数组,里面有3个元素存放了,a b c三个变量的地址。
结构体是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
例如我们描述一个学生变量
int main()
{
struct student {
char name[20];//姓名
int age;//年龄
char id[20];//学号
char sex[5];//性别
};//分号不能丢
}
结构体中的成员变量可以是标量、数组、指针、甚至是结构体。
int main()
{
struct id {
int id;
};
struct student {
char name[20];
struct id ;
int age;
};
struct id i_d = { 123 };//结构体的初始化
struct student st1 = { "kkk",{124},18 };//结构体的嵌套初始化
}
结构体变量访问成员
结构体变量的成员是通过点操作符(
.
)访问的,点操作符接受两个操作数。
int main()
{
struct student {
char name[20];
int age;
};
struct student stu = { "kkk",18 };
printf("%s %d", stu.name, stu.age);
}
结构体指针访问指向变量的成员
有时候我们得到的不是一个结构体变量,而是一个结构体指针,我们使用(->
)访问成员。
struct student {
char name[20];
int age;
};
void print(struct student* pstu)
{
printf("%s %d \n", pstu -> name, pstu -> age);
printf("%s %d \n",(*pstu).name, (*pstu).age);
}
int main()
{
struct student stu = { "kkk",18 };
print(&stu);//结构体地址传参
}
结构体传参可以传结构体,也可以传地址
struct student {
char name[20];
int age;
};
void print1(struct student* pstu)
{
printf("%s %d \n", pstu->name, pstu->age);
printf("%s %d \n", (*pstu).name, (*pstu).age);
}
void print2(struct student stu)
{
printf("%s %d \n", stu.name, stu.age);
}
int main()
{
struct student stu = { "kkk",18 };
print1(&stu);//结构体地址传参
print2(stu);//传结构体
return 0;
}
那么print1
与print2
函数哪个更好一点呢?我们选择传地址,因为函数传参时,参数需要压栈。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。所以结构体传参时,要传结构体的地址。