✨✨ 欢送大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:C 语言学习
贝蒂的主页:Betty‘s blog
1. 指针与地址
1.1 概念
咱们都晓得计算机的数据必须存储在内存里,为了正确地拜访这些数据,必须为每个数据都编上号码,就像门牌号、身份证号一样,每个编号是惟一的,依据编号能够精确地找到某个数据。而这些编号咱们就将其称为 地址 或者 指针
1.2 指针变量
数据在内存中的地址称为指针,如果一个变量存储了一份数据的指针(地址),咱们就称它为 指针变量。
那咱们如何应用指针变量呢?
- datatype *name;
*
示意这是一个指针变量,datatype
示意该指针变量所指向的数据的类型
例如:
int* p1;// 指向一个整型的指针
char* p2;// 指向一个字符的指针
float* p3;// 指向一个单精度浮点数的指针
double* p4;// 指向一个双精度浮点数的指针
1.3 & 和 *
咱们早在学习 scanf 时候就用过取地址符 &,它是将某个变量的地址取出来,而解援用 * 的意思就是 通过某个地址找到该地址存储的变量。可能解释起来比拟形象,咱们能够通过一个不失当的例子形象阐明一下。
首先咱们能够失去如下几个关系:
int a = 1;// 第一个客户,&a 为 0x00000001
int b = 2;// 第二个客户,&b 为 0x00000002
int c = 3;// 第三个客户,&c 为 0x00000003
而后咱们能够通过指针变量把他们地址存储进去
int* pa = &a;// 把 a 的地址存进去
int* pb = &b;// 把 b 的地址存进去
int* pc = &c;// 把 c 的地址存进去
在酒店中,咱们能够通过门牌号精确找到每个客户。同理,咱们也能够通过每个地址精确找到每个变量。
printf("a=%db=%dc=%d", *pa, *pb, *pc);// 通过 * 解援用地址找到对应的值
输入后果 a=1 b=2 c=3
并且咱们能够通过指针变量进行赋值。
*pa = 4;
*pb = 5;
*pc = 6;
printf("a=%d b=%d c=%d\n", *pa, *pb, *pc);
输入后果:a=4 b=5 c=6
1.4 void* 指针和 NULL
(1)void* 是一种非凡的指针类型,它能够指向任意类型的数据,就是说能够用任意类型的指针对 void 指针赋值。
void*p1;
int*p2;
p1=p2;// 这是被容许的
- 然而却 不能把 void* 指针赋值给任意指针类型,也不能间接对其解援用
void*p1;
int *p2;
p2=p1;// 不能这样赋值
*p1// 不能间接对 void* 解援用
(2)NULL 是 C 语⾔中定义的⼀个标识符常量,值是 0,0 也是地址,这个地址是⽆法使⽤的,读写该地址会报错。
int*p=NULL;// 初始化指针
1.5 指针变量的大小
咱们晓得,当初常见的计算机分为 32 位机器 和64 位机器 。32 位机器假如有 32 根地址总线,每根地址线进去的电信号转换成数字信号后是 1 或者 0,那咱们把 32 根地址线产⽣的 2 进制序列当做⼀个地址,那么⼀个地址就是 32 个 bit 位,须要4 个字节能力存储。同理,64 位机器须要 8 个字节能力存储。
咱们能够通过以下代码来验证一下。
int main()
{printf("%zd", sizeof(char*));
printf("%zd", sizeof(short*));
printf("%zd", sizeof(int*));
printf("%zd", sizeof(double*));
return 0;
}
输入后果:
32 位机器:4 4 4 4
64 位机器:8 8 8 8
2. 指针的根本运算
2.1 指针 +- 整数
咱们先察看一下如下代码的地址变动
#include <stdio.h>
int main()
{
int n = 10;
char* p1 = (char*)&n;// 将 int* 强转为 char*
int* p2 = &n;
printf("%p\n", &n);
printf("%p\n", p1);
printf("%p\n", p1 + 1);//p1 向后挪动一位
printf("%p\n", p2);
printf("%p\n", p2 + 1);//p2 向后挪动一位
return 0;
}
输入:
&n=005DF8D4
p1=005DF8D4
p1+1=005DF8D5
p2=005DF8D4
p2+1=005DF8D8
咱们能够看出,char 类型的指针变量 + 1 跳过 1 个字节,int 类型的指针变量 + 1 跳过了 4 个字节。由此咱们得出结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(间隔)。
- 因为每次代码运行时,零碎都会从新分配内存,所以输入后果每次都不会一样,然而法则是一样的
咱们晓得数组在内存中是 间断存储 的(地址由低到高),所以咱们只须要只有首元素的地址就能找到数组所有元素的地址,而一维数组的数组名恰好就是咱们首元素的地址。
假如有数组 int arr[10]={1,2,3,4,5,6,7,8,9,10}
arr | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
那咱们如何通过指针拜访每个元素呢?
代码参考如下:
#include <stdio.h>
int main()
{int arr[] = {1,2,3,4,5,6,7,8,9,10};
int* p = &arr[0];//&arr[0]=arr
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);// 计算数组元素个数
for (i = 0; i < sz; i++)
{printf("%d", *(p + i));// 因为数组元素间断存储,所以能够通过 +- 整数找到之后元素
}
return 0;
}
输入后果:1 2 3 4 5 6 7 8 9 10
2.2 指针 - 指针
指针 - 指针其实是指在同一空间内,两个指针之间的元素个数。
晓得这点之后,咱们可不可以本人实现一个字符串库函数 strlen()呢?
思路如下:
思路:首先定义两个指针 p1,p2,让两个指针指向首元素,而后让一个指针 p2 循环 ++,直到指向‘\0’就进行,最初返回 p2-p1,就能失去字符串的长度
代码如下:
int my_strlen(char* p1)
{
char* p2 = p1;// 使两个指针都指向首元素
while (*p2)
{p2++;}
return p2 - p1;// 返回两指针间接的元素的个数就是其长度
}
int main()
{char arr[] = "abcdef";
int len = my_strlen(arr);// 计算 arr 字符串的长度
printf("%d\n", len);
return 0;
}
- 参考贝蒂 string.h 大全
2.3 指针的关系运算
咱们晓得了指针变量实质是寄存的地址,而地址实质就是 十六进制 的整数,所以 指针变量也是能够比拟大小的。
代码示例:
#include <stdio.h>
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz) // 指针的⼤⼩⽐较
{printf("%d", *p);// 打印数组每个元素
p++;
}
return 0;
}
3. const 润饰
咱们晓得变量是能够扭转的,然而在有些场景下,咱们不心愿变量扭转,那咱们该怎么办呢?这就是咱们接下来要讲的 const 的作用啦。
3.1 const 润饰变量
简略来说,通过 const 润饰的变量,能够当做一个常量,而常量是不能扭转的。
int a = 1;// a 可批改的
const int b = 2;
b=3;// b 不可批改的
然而能够通过指针间接批改.
代码如下:
int main()
{
const int b = 2;
int* p = &b;
*p = 3;// 通过指针间接批改
return 0;
}
3.2 const 润饰指针
咱们晓得 const 的作用后,就能够看看上面几段代码。
int a = 10;
const int* p = &a;
*p = 20;// 是否能够
p = p + 1;// 是否能够
通过测试咱们发现,* p 无奈扭转成 20,然而 p 能够扭转成 p +1.
那如果把 const 调换一下地位,又会呈现什么状况呢~
int a = 10;
int* const p = &a;
*p = 20;// 是否能够
p = p + 1;// 是否能够
再次测试之后咱们发现,* p 能够被赋值为 20,然而 p 不能赋值为 p + 1 了
通过上述测试,咱们大抵能够总结出两个论断。
• const 如果放在的右边,润饰的是指针指向的内容,保障指针指向的内容不能通过指针来扭转。然而指针变量本⾝的内容可变。
• const 如果放在 * 的左边,润饰的是指针变量本⾝,保障了指针变量的内容不能批改,然而指针指向的内容,能够通过指针扭转。
4. assert 断言
assert 是一个宏,它的头文件为 <assert.h>, ⽤于在运⾏时确保程序合乎指定条件,如果不合乎,就报错终⽌运⾏。这个宏经常被称为“断⾔”。
举一个简略的例子:
assert(a>0);
- 如果 a 确实大于 0,assert 判断为真,就会通过。
-
如果 a 不大于 0,assert 判断为假,就会报错。
所以 assert 经常用于查看空指针问题,以避免程序因为空指针的问题而出错。
int *p=NULL;
assert(p);// 空指针是 0,0 为假,就会报错
5. 传值调用与传址调用
5.1 传值调用
咱们后面学习函数时候,遇到过这样一段代码。
#include<stdio.h>
void swap(int x, int y)// 返回类型为 void 示意不返回值
{
int temp = 0;// 定义一个长期变量
temp = x;// 把 x 的值赋给 temp
x = y;// 把 y 的值赋给 x
y = temp;// 把 temp 的值赋给 y,实现替换操作
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("替换前:a=%d,b=%d\n", a, b);
swap(a, b);// 替换函数
printf("替换后:a=%d,b=%d\n", a, b);
return 0;
}
输出:3 5
输入:替换后 a =3,b=5
为什么两个值并没有替换呢,这是因为 形参只是实参的一份长期拷贝,对形参扭转,基本不会扭转实参。如果遗记的同学能够再去复习一下贝蒂的函数小课堂
5.2 传址调用
那咱们想在函数中扭转实参的值,那又该如何扭转呢?
其实很简略,咱们学了指针,晓得能够通过地址间接拜访该变量的值,所以咱们只须要把地址传给函数,在函数中通过地址拜访实参,并进行替换。
代码如下:
#include<stdio.h>
void swap(int*x, int*y)// 通过指针变量承受地址
{
int temp = 0;// 定义一个长期变量
temp = *x;// 把 * x 的值赋给 temp
*x = *y;// 把 * y 的值赋给 *x
*y = temp;// 把 temp 的值赋给 *y,实现替换操作
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("替换前:a=%d,b=%d\n", a, b);
swap(&a, &b);// 将地址传给函数
printf("替换后:a=%d,b=%d\n", a, b);
return 0;
}
6. 野指针
概念:野指针 就是指针指向的地位是不可知的(随机的、不正确的、没有明确限度的)
6.1 野指针成因
(1)指针未初始化
#include <stdio.h>
int main()
{
int* p; // 局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
因为 p 是随机值,所以对 p 解援用,零碎无奈通过 p 的地址找到对应的空间,所以出错造成野指针
(2)数组越界拜访
#include <stdio.h>
int main()
{int arr[10] = {0};
int i = 0;
for (i = 0; i < 11; i++)
{
// 数组下标是 0 到 9
printf("%d", *(arr + i));
}
return 0;
}
- 个别呈现这种较大的随机值,个别都是数组越界拜访
(3)指针指向空间开释
#include <stdio.h>
int* test()
{
int n = 10;
return &n;// 返回 n 的地址
}
int main()
{int* p = test();// 用 p 承受 n 的地址
printf("%d\n", *p);// 打印出 n 的值
return 0;
}
这段代码乍一看,如同并没有什么问题,然而大家在学习函数的时候晓得,在函数中定义的变量是长期变量,一旦 出了作用域就会销毁。
一旦销毁,零碎就无法访问该空间,而通过指针咱们还能够拜访该空间,这就造成了抵触,所以出错,造成野指针。
6.2 解决办法
(1) 初始化
NULL 是 C 语⾔中定义的⼀个标识符常量,值是 0,0 也是地址,这个地址是⽆法使⽤的,读写该地址会报错。如下是 NULL 在编译器中的定义:
ifdef __cplusplus
define NULL 0
else
define NULL ((void *)0)
endif
#include <stdio.h>
int main()
{
int* p=NULL;// 用空指针初始化,让其有指向地位
//*p = 20;NULL 地址不能读写
return 0;
}
(2) 小心越界拜访
咱们在应用数组时候,肯定要对数组的元素个数有一个清晰的把控,不然就很容易呈现越界拜访的状况。
(3) 不能返回长期变量的地址
长期变量出了作用域就会销毁,零碎会回收该空间,所以咱们要尽量避免指针指向曾经销毁的空间,尤其在函数中,不能返回长期变量的地址。