1. 内存和地址
2. 指针变量和地址
3. 指针变量类型的意义
4. const修饰指针
5. 指针运算
6. 野指针
7. assert断⾔
8. 指针的使⽤和传址调⽤
一、内存和地址
内存:
我们首先来举一个例子,在大学的学生有很多,我们如果想找到一个学生,那么怎么样才可以找到呢?因此我们每个人都拥有了自己的学号,老师,校园领导可以通过我们的学号来找到我们本人。有了学号增加了我们确定学生就有了效率
把上边的知识引述到计算机里边我们就可以很好的了解内存和地址的关系
我们在处理计算机的数据的时候,数据都是在内存中储存的。
通常计算机会把内存划分为一个一个的内存单元,一个内存单元的大小是一个字节。
一个字节是8个比特位
一个比特位可以储存二进制位的0或1(这就是我们时常听见的计算机是只能识别二进制的)
如果我们把学号叫做地址的话,我们就可以快速的通过学号来定位一个学生。
同样可以理解的是如果我们给每个内存也都有自己的编号,我们可以通过编号来找到内存中储存的数据(这里的编号又叫地址),但在C语言中我们给其重新起了一个名字叫做指针
二、指针变量和地址
理解了内存和地址的关系,我们就可以知道了,创建变量的本质就是向内存申请空间。
如果我们创建了一个变量
> int a = 0;内存中会申请到4个字节来存放整数10,每个字节都有自己的地址。
假设的每个字节的地址是:
1字节 0x006FFD70
2字节 0x006FFD71
3字节 0x006FFD72
4字节 0x006FFD73
那么我们如何得到a的地址呢?
这里我们就可以学习一个“&”操作符,它的作用就是取出a的地址
> 注意:我们那拿出的地址是四个字节中最小的哪个,所以我们只需要知道第一一个地址就可以知道剩下的地址了。
2.2 指针变量和解引用操作符(*)
我们在创建整形的时候会把用类型加名字的组合储存起来,指针是否也是可以储存起来的呢?答案是可以的。
整形我们用整形变量来储存起来
那么指针我们就用指针变量来储存起来
比如:
int main()
{
int a = 5; //去掉名字留下的就是类型
&a;
int* pa = &a; //同样去掉名字留下的就是类型
return 0;
}
指针变量也是一种变量,这种变量就是用来储存地址的,存放在指针变量的值都会理解为指针
那么对于一个指针我们如何拆分呢?
int a = 5; &a ; int* pa = &a;
解引用操作符
我们把地址储存出来是要使用的,那么我们怎么来使用的呢?
这里我们举一个例子,在生活中我们知串亲戚,需要知道人家的地址在那里然后才可以找到,其实在C语言中也是一样的,我们需要拿到地址(指针),才可以通过地址(指针)找到对象,这里我们需要了解一个操作符叫解引用操作符(*)。
#include<stdio.h>
int main()
{
int a = 0;
&a;
int * pr = &a;
*pr = 5;
printf("%d ", *pr);
return 0;
}
通过取地址操作符,我们可以把a的地址拿出来(&a),随后我们通过*(&a),拿到a的值,然后就可以对齐进行修改。那么我们难道不可以直接进行a=5这样不是更快吗?是的这样也是可以的,这样对a的修改,就多了⼀种的途径,写代码就会更加灵活, 后期慢慢就能理解了
3、指针变量的大小
指针变量在32/64位地址线的情况下所占的大小是不同的
32位:就是32位比特位,一个字节8个比特位,也就是4个字节
64位:就是64位比特位,一个字节8个比特位,也就是8个字节
目前常用的指针变量为
1、char *
2 、 int *
3 、float *
4、double *
接下来我们来分析在不同位数下,指针变量的大小
总结:
•
32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
•
64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
X64环境输出结果
•
注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。
三、指针变量的意义
既然每个指针变量的大小在相同平台下的大小都是相同的,那么为什么还要区分呢? 当然是因为其有特殊的意义的
3.2
指针+ - 整数
int main()
{
int n = 5;
int * pr = &n;
char * pr2 = (char*)&n; //把5的整数类型的地址强制转化为字符类型的地址
printf("%p\n", &n);
printf("%p\n", pr);
printf("%p\n", pr+1);
printf("%p\n", pr2);
printf("%p\n", pr2+1);
return 0;
}
这里我们发现 int *类型可能会跳过4个字节,char *类型的会跳过1个字节。
结论:指针类型决定了向前或者向后走的一步有多大。
3.3 void*指针
在指针类型中有一种的类型是 void * 类型的,可以理解无具体类型的指针(可以理解位垃圾桶),就是可以接收任意类型的指针,但是有利就有弊,void *类型不可以进行指针的+ -整数和解引用的运算。(因为类型无法确定,不知道怎么来进行运算)
int main()
{
int a = 10;
int* pr = &a;
char* pr1 = &a; //用char * 类型来接受int 类型数据的地址类型会不兼容的
return 0;
}
那么我们来换成 void * 类型来接收呢?
int main()
{
int a = 10;
int* pr = &a;
void* pr1 = &a; //那么就会正常运行,不会报错但是无法使用
return 0;
}
四、const 修饰变量
变量是可以被修改的,那么我们如果不想让变量修改呢?有什么方法吗?当然const就是来限制变量的,让其不能被修改。
我们来举例子来证实其中的玄机
#include<stdio.h>
void text1()
{
int n = 0;
int m = 0;
int *pr = &n;
*pr = 5;
printf("%d \n", *pr);
}
void text2()
{
int n = 0;
int m = 0;
const int* pr = &n;
// int const* pr = &n; //这两种方法const的都有相同的作用只要是在*pr的左边
//*pr = 5; //加上const,就不能通过解引用符号来改变n的值了,但是pr依然可以用来接收其它数据的地址
pr = &m;
printf("%p\n", pr);
}
void text3()
{
int n = 0;
int m = 0;
int* const pr = &n; //这里放在*右边,限制的是pr,*pr没有被限制
*pr = 5; //所以可以通过解引用操作符来改变n的值
// pr = &m; //但是pr中不可以重新放其它的地址(否则会报错)
printf("%d \n", *pr);
}
void text4()
{
int n = 0;
int m = 0;
const int* const pr = &n; //当*的左边和右边都有const ,pr和*pr都会被限制,如果我们要强行改变,就会报错
*pr = 5; //报错
pr = &m; //报错
}
int main()
{
text1();
text2();
text3();
text4();
return 0;
}
总结:const 修饰指针变量的时候
•
const如果放在”*“的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。
但是指针变量本⾝的内容可变。
•
const如果放在’'*“的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指
向的内容,可以通过指针改变。
五、指针运算
指针的基本运算有三种,分别是
1.指针+ 指针
2.指针- 指针
3.指针的关系运算
5.1指针+整数
从上边我们可以知道,数组在内存中是连续储存的,并且地址是由低到高的,所以只要我们知道了第一个数组元素的地址就可以顺藤摸瓜得到数组其它元素
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", *(arr + i)); //*(arr+i) == arr[i]
} //原因:arr表示的是数组收元素的地址,通过+i我们可以拿到数组中任意元素的地址然后打印出来
printf("\n");
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]); //arr[i]的本质也是会转化位*(arr+i),通过拿到元素的地址,然后解引用拿到数组对应的元素再来打印出来
}
return 0;
}
两种方式打印的结果都是一样的,说明两者的都可以实现打印数组
5.2 指针-指针
指针-指针得到是字符的长度,我们此时可以想到,strlen也是得到字符的长度的,那么我们是否可以通过次运算来写出自己的strlen函数呢?
int my_strlen(char * s)
{
char* sz = s; //保存一下首元素的地址
while (*s != '\0')
{
s++; //s的地址在改变
}
return s - sz;
}
int main1()
{
char arr[] = "abcdef";
int ret = my_strlen(arr);//自创函数
printf("%d", ret);
return 0;
}
//两者都可以计算出我们字符串的长度
int main()
{
char arr[] = "abcdef";
int ret = strlen(arr); //库函数
printf("%d", ret);
return 0;
}
5.3 指针的关系运算
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int * pr = &arr[0];
int ret = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < ret; i++)
{
if (pr <= &arr[0 + i]) //让其指针作比较
{
printf("%d ", *pr);
pr++;
}
}
return 0;
}
六、野指针
野指针:顾名思义就是指向的方向是不明的(随机,无限制的,错误的),理解成为野狗(流浪的,可恶的)。
野指针的成因
1.指针为初始化
2.指针越界访问
3.指针指向的空间释放
我们来分别举例子来说明这三种情况
6.1
int main()
{
int* pr;
*pr = 50;
printf("%d", *pr);
return 0;
}
2.
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int* pr = arr;
int ret = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i <= ret; i++)
{
printf("%d ", *(arr + i)); //这里指针在i=10的时候已经越界访问了数组中的元素,所以打印出来会报错
}
return 0;
}
3.指针指向的空间释放
int* pr()
{
int a = 10;
int* pr1 = &a; //由于这里的a是局部变量,再出函数的时候就已经销毁了,这时如果把a的地址拿出来在使用可能就会造成野指针
return pr1;
}
int main()
{
int *prr =pr();
printf("%d ", *prr);
return 0;
}
既然出现了野指针那么我们是否可以有一些方法来避免野指针的出现呢?如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。
(野狗咬人我们如何避免呢?就是要限制它,把它绑在树上让它吧,限制它的行动)
指针的初始化如下:
int main()
{
int a = 5;
int *pr = &a; //这里我们的a是有地址的,所以可以用指针变量pr来接收
int* prr = NULL; // prr没有可以能接收的具体变量的地址,所以我们就给其赋为 空指针(NULL)
return 0;
}
6.2
小心指针越界访问:
我们在使用指针的时候一定需要注意指针访问了那些空间,只有指针能够访问到的时候我们才可以使用,不能超出访问的范围,超出了就变成了越界访问。
6.3
指针变量不再使用时,及时置为NULL,指针使用之前检查其有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。
int main()
{
int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
int *pr = arr;
int j = 0;
int ret = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < ret; i++)
{
*(pr++) = i; //这里循环4此后第五次就越界访问了,pr已经成为野指针了
}
pr = NULL; //把pr置为空指针
pr = arr;//重新给pr赋值数组的起始地址
if (pr != NULL) //只有当pr不再为空指针的时候才重新使用
{
}
return 0;
}
七、assert 断言
assert.h
头⽂件定义了宏assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”
我们也需要注意的时assert()宏接收一个表达式,如果表达为真则就返回一个非零值,如果表达式为假则返回一个0,这是assert()就会报错。
例子:
#include<assert.h>
int main()
{
int a = 5;
int* pr = &a;
int* prr = NULL;
assert(pr != 0); //使用库函数要包含头文件
assert(prr != 0);
return 0;
}
这里不仅报错了,而且还给了报错的行数来方便我们来修改。这就时assert()断言的好处。
我们还可以使用一种方式来开启或者关闭assert断言,我们只需要在就在
#include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。
例如:
#define NDEBUG
#include <assert.h>
在我们通过asseert 断言来修改代码以后,确保代码没有问题,就可以用#define NDEBUG来关闭断言,,如果程序又出现了问题,我们就可以把#define NDEBUG注释掉(或者删除),再次编译,重新启动assert断言来找寻问题。
八、指针的使用和传址调用
8.1
strlen的模拟实现
上边们利用指针减去指针获得了求字符串的长度的一种方法,那么我们今天来介绍另一种方法。
1. 参数str接收⼀个字符串的起始地址,然后开始统计字符串中\0 之前的字符个数,最终返回⻓度。
2.如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是到\0,就把计数器加1,直到遇见\0停止。
#include<stdio.h>
#include<assert.h>
int my_strlen(const char* str) //这里我们要求的时统计长度不改变指针指向内容的值,加上一个const会更好
{
int count = 0;//计数器
assert(str); //确保str不是空指针,要包头文件
while (*str)
{
count++; // 只要*str为真 就开始统计
str++; //依次从后访问1
}
return count;
}
int main()
{
char arr[] = "asdfghjkl";
int ret = my_strlen(arr);
printf("该字符个数有%d个", ret);
return 0;
}
8.2
学习指针是为了什么呢?当然是为了解决一些只有指针来才能解决的问题
例子:写一个函数,交换两个整形变量的值
1.传值调用
void swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int num = 0;
int num2 = 0;
scanf("%d%d", &num, &num2);
printf("交换前:num=%d ,num2=%d\n", num, num2);
swap(num,num2); //我们来写一个函数来交换 num和num2的值
printf("交换后:num=%d ,num2=%d", num, num2);
return 0;
}
这里我们发现,两个值并没有进行交换这是为什么呢?
结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参,所以上边的代码失败了
2.传址调用
那么只要我们把地址传过去再进行修改,就可以完成该交换
void swap(int *x, int *y)
{
int tmp = 0;
tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int num = 0;
int num2 = 0;
scanf("%d%d", &num, &num2);
printf("交换前:num=%d ,num2=%d\n", num, num2);
swap(&num, &num2); //我们来写一个函数来交换 num和num2的值
printf("交换后:num=%d ,num2=%d", num, num2);
return 0;
}
我们可以看到实现成Swap的⽅式,顺利完成了数据的交换,这⾥调⽤Swap函数的时候是将变量的地址传
递给了函数,这种函数调⽤⽅式叫:传址调⽤。
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中
只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。**
如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。