✨✨ 欢送大家来到贝蒂大讲堂✨✨
养成好习惯,先赞后看哦~
所属专栏: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) 不能返回长期变量的地址
长期变量出了作用域就会销毁,零碎会回收该空间,所以咱们要尽量避免指针指向曾经销毁的空间,尤其在函数中,不能返回长期变量的地址。