1.内存与地址
计算机上CPU在处理数据时,需要的数据都是在内存中读取的,处理后的得数据也会放回内存中。
内存其实是由一个个内存单元组成,每个内存单元占1字节(Byte)
1个字节空间可以放8个比特位(bit)
一个比特位可以存放一个2进制的0/1.
为了计算机能够快速找到对应的内存空间,每个内存单元都有一个编号,我们把内存单元的编号称为地址,C语言给地址了一个新名字:指针
即:
内存单元的编号=地址=指针
1.1内存编址
CPU访问内存中的某个字节空间,要知道他在内存中的位置,然而内存中的字节空间十分多,这就要给内存进行编址
CPU与内存之间存在大量数据交互,这时候就需要用(地址总线)将其联系起来。
这里可以简单理解,32位机器有32根地址总线,每根线有两态,表示为0/1,(电脉冲有无),这样一根线就表示2种含义,32根线就有2^32种含义,每种含义都代表一个地址。
当地址信息被下达给内存,在内存上,找到该地址对应的数据,通过数据总线传入CPU寄存器
(仅了解)
2.指针变量与地址
2.1取地址操作符&
在创建变量时,会向内存申请储存空间,
比如一个整型变量,向内存申请4个字节,而每个字节都有对应的地址
#include<stdio.h>
int main()
{
int a = 10;
printf("%p", &a);
return 0;
}
对于此代码,当我们调试后,能够得到整型a所占的4个字节的地址
1.0x006FFD70
2.0x006FFD71
3.0x006FFD72
4.0x006FFD73
如果我们想要得到a的地址,就要使用&取地址操作符。
&a取出的地址是a所占字节的地址中较小的
即;x006FFD70
通常只要我们找到一个数据的第一个字节地址,就可以顺藤摸瓜访问到所有的地址
2.2指针变量与解引用
指针变量
指针变量说白了就是用来存放地址的。
#include<stdio.h>
int main()
{
int a = 0;
int* pa = &a;//取出a的地址,将他放在指针变量pa中
return 0;
}
我们知道了pa是指针变量,那么int *是什么东西呢?
其实很容易理解,在这幅图中,a作为一个整型变量,他存放的内容是整数10。
在变量pa他存放的内容是整型a的地址,地址=指针,我们就可以理解,*是为了说明pa是一个存放指针的指针变量,而他储存的内容指向整型变量a,这就是int(pa指向的内容为int类型)
类比如果是一个字符的地址就要放在字符指针变量中
char c='b';
char *ch=&c;//将字符变量的地址放在字符指针变量中
解引用
在上面的例子中,我们知道pa中存放的是a的地址,那如果我们又想获取a的数值呢?
这里可以用到解引用操作符()
pa的意思是通过pa存放的地址。找到它所指向的空间
简单来说pa=a,所以pa=0;,就把a的值改成了0;
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;
printf("%d\n", *p);
printf("%d\n", a);
*p = 0;
printf("%d\n", a);
return 0;
}
assert()断言
当提到了解引用,我们就不得不提到assert()断言
assert.h头文件定义了宏assert,这个宏就叫“断言”
assert(p!= NULL);
当指针变量p不是空指针时,程序继续运行,如果是空指针,就会报错
assert()接受一个表达式作为参数,表达式为真返回非零值,否则,返回0,assert就会报错,在标准错误流stderr中就会1.写入一个错误信息,显示未通过的表达式,以及表达式的文件名和行号。
那么回到最初,为什么我们说提到解引用就不得不提assert呢?
当我们在对一个指针变量进行解引用时,首先要确保这个指针变量不是空指针,因此为了增加的代码的可靠性,我们在解引用是可以在前面加上断言
#include<stdio.h>
#include<assert.h>
int main()
{
int a = 10;
int* p = &a;
assert(p != NULL);//断言,确保p不是空指针
printf("%d\n", *p);
printf("%d\n", a);
*p = 0;
printf("%d\n", a);
return 0;
}
当然assert的用处不仅如此
它不仅能完成上边1的功能,还有一个2.无需更改代码就可以实现开启或关闭assert的超绝机制。
#define NDEBUG
#include<assert.h>
那就是在头文件前面定义一个宏NDEBUG
如果确认程序没有问题,不必要在做断言,就定义这个宏,再重新编译的时候,编译器就会禁用掉文件中的assert()语句;相反如果程序又出现问题,那就注释掉这个宏,assert语句重新启用。
2.3const修饰指针变量
一般const修饰指针变量,可以放在 * 的左边,或者 * 的右边,两者意义不同。
const在 * 左边 | const在 * 的右边 |
---|---|
修饰指针变量指向的内容,确保指针变量指向的内容不能通过指针改变,但指针变量本身可以更改 | 修饰指针变量本身,确保指针变量本身的内容不能被更改,而他指向的内容可以通过指针更改 |
#include<stdio.h>
void test1()
{
int n = 9;
int m = 7;
int * p = &n;
printf("%p\n", p);
*p = 7;//ok?--yes
p = &m;//ok?--yes
printf("%p\n", p);
printf("%d\n", *p);
}
void test2()
{
int n = 9;
int m = 7;
int const* p = &n;
printf("%p\n", p);
*p = 7;//ok?--no
p = &m;//ok?--yes
printf("%p\n", p);
printf("%d\n", *p);
}
void test3()
{
int n = 9;
int m = 7;
int*const p = &n;
printf("%p\n", p);
*p = 7;//ok?--yes
p = &m;//ok?--no
printf("%p\n", p);
printf("%d\n", *p);
}
int main()
{
test1();//无const
test2();//在*左边
test3();//在*右边
return 0;
}
3.指针运算
1.指针±整数
2.指针-指针
3.指针的运算关系
4.void*
3.1指针±整数
数组在内存中是连续存放的,因此只要知道首元素的地址,就可以知道后面的所有元素
指针±的时候跳过的字节数,取决于指针指向内容的数据类型。
那么对于整型数组±跳过4个字节那就是跳过一个整型元素
,字符类型也是如此。
下边给一个int类型的数组
但对于*int p=&arr;取了整个数组的地址赋给指针变量,当他加1的时候,跳过的是整个数组
#include<stdio.h>
//遍历整个数组
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));//指针+-整数
}
return 0;
}
3.2指针-指针
指针-指针计算的是两个指针之间的元素个数
#include<stdio.h>
//模拟实现strlen
int my_strlen(char* p)
{
char* m = p;
while (*p!='\0')
{
p++;
}
return p - m;
}
int main()
{
char p[] = "abcd";
int ret=my_strlen(p);
printf("%d\n", ret);
return 0;
}
3.3指针的运算关系
#include<stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
while (p <( arr + sz))
{
printf("%d ", *p);
p++;
}
return 0;
}
3.4void*类型
在指针类型中有一种特殊的就是void类型,可以理解成无具体类型的指针,它可以接受任意类型的地址,但是因为它的类型是不确定的,所以他也无法进行解引用操作和指针±整数运算*
4.野指针
野指针就是指针指向的位置是不确定的,不可知的,随机的,无限制的
野指针的成因
有3点
1.指针未初始化
char *p;//未初始化,默认为随机值
2.指针越界访问
#include<stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int* p = arr;
int i = 0;
for (i = 0; i <= 11; i++)
{
printf("%d ", *(p + i));//指针越界访问
}
return 0;
}
3.指针指向空间的释放
#include<stdio.h>
int* test()
{
int n = 10;
return &n;
}
int main()
{
int* p = test();
return 0;
}
这个代码就是在test函数中定义了局部变量n,当执行完test函数后,n所占用的空间就会被释放掉,,但是test却返回了n的地址,并由指针变量p来接受。明明n所在的内存空间已经被释放掉了呀,这样p就成为一个空指针了,这显然是不行的。
因此我们要避免返回局部变量的指针
如果需要返回指针指向的数据,应该通过动态分配内存(例如在test函数中使用malloc分配内存),就可以确保在函数返回后,数据仍然有效。
野指针的规避
1.对指针初始化
2.避免出现指针越界
3.在指针不用时,及时置为NULL,指针使用之前检查有效性(哈哈哈,这里就可以用到断言啦!)
4.避免返回局部变量的地址
5.指针的使用和传址调用
5.1传值调用和传址调用
我们写一个交换两个值的代码
#include<stdio.h>
void exchange(int x, int y)
{
int tem = 0;
tem=x;
x = y;
y = tem;
}
int main()
{
int a = 5;
int b = 0;
printf("交换前a=%d b=%d\n", a, b);
exchange(a, b);
printf("交换后a=%d b=%d\n", a, b);
return 0;
}
运行后,我们发现,a与b的值并没有被交换
为什么呢?
我们调试看看吧
我们会发现在exchange函数中,x和y确实交换了数值呀,可为什么main函数中不交换呢?
仔细看我们会发现,exchange在接受参数时,内部创建了形参x,y但他们的地址与a,b的地址不同,相当于x,y是独立的空间,那他们变不变和a,b有什莫关系呢?a,b是不受影响的。自然当exchange函数调用结束后,a,b的值仍没被交换
在这个代码中,我们把a,b两个变量本身传给了函数。这就叫做传值调用
结论;实参传递给形参时,形参会单独创建一份临时空间来接受实参,对形参的修改不影响实参。
那么,要如何交换a,b呢,?
既然a,b 与x,y的地址不同,我们不妨试试把a,b的地址传给x,y,通过地址间接交换
#include<stdio.h>
void exchange(int* x, int* y)
{
int tem = 0;
tem = *x;
*x = *y;
*y = tem;
}
int main()
{
int a = 5;
int b = 0;
printf("交换前a=%d b=%d\n", a, b);
exchange(&a, &b);
printf("交换后a=%d b=%d\n", a, b);
return 0;
}
看!果然交换了,我们把变量地址传给函数的的调用方法就叫“传址调用”
6.数组名和一维数组传参本质
6.1数组名
当我们想要获得一个数组的第一个元素地址时,可以用&arr[0];也可以直接写数组名
#include<stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
printf("&arr[0]=%p\n", &arr[0]);
printf("arr =%p\n", arr);
return 0;
}
通过运行结果,我们可以看到数组名的本质就是首元素的地址。
#include<stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
printf("%zd", sizeof(arr));
return 0;
}
嗯?如果说arr是首元素地址的话,在32位/64位的环境下,地*址的字节数应该是4/8(这里作者实在X64环境下运行的),怎么输出的结果是40?
这明明是整个数组的字节个数啊?
事实上,数组名是数组首元素的地址这句话是非常正确的,可万事都不是绝对的,数组名也不例外。
两个例外;
sizeof(数组名),sizeof中单独放数组名,表示的就是整个数组,计算整个数组的大小,单位字节
&(数组名):这的数组名也是指整个数组,取出的是整个数组的地址(整个数组的地址和首元素地址是不一样的)
除此之外,数组名就是首元素的地址
6.2一维数组传参的本质
#include<stdio.h>
void test1(int arr[])
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz2);
}
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz1);
test1(arr);
return 0;
}
我们发现当把arr传过去的时候,并不能正确计算出数组元素的个数
原因是,表面上我们的test函数接受的是数组,实际上接受的是首元素的地址,是指针
sizeof计算的是地址所占的字节数,而不是数组的大小,真因为形参的本质是指针,所以在这个函数内部是无法计算元素个数的。作者这里是在X64环境下运行的,地址的字节数就是8,计算的结果sz2=2;(各位宝宝们,如果是在x86环境下的话,计算出来就是1)
#include<stdio.h>
void test(int* arr)//参数携程指针形式
{
printf("%zd\n", sizeof(arr));
//计算一个指针变量的大小,sizeof(数组名)计算的是整个数组的地址
}
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
test(arr);
return 0;
}
我们传过去的arr是个指针,是地址,地址的字节数在64位环境下为8
结论:一维数组传参的本质上数组传参传递的是首元素的地址
7.二级指针
二级指针说白了就是为了存放一级指针变量
ppa存放的是pa的地址,pa中存放的是a的地址
*ppa,实质是对ppa内存放的pa的地址解引用,得到pa的内容,pa的内容是a的地址,*pa是对pa的内容即a的地址解引用,得到a的内容10;
8.指针数组和数组指针
这部分内容作者在之前的博客已经介绍过了
直通车:https://blog.csdn.net/2301_80151382/article/details/140967510?spm=1001.2014.3001.5501
9.函数指针变量
这部分内容作者在之前的博客已经介绍过了
直通车:https://blog.csdn.net/2301_80151382/article/details/141001124?spm=1001.2014.3001.5501
10.回调函数
回调函数就是通过函数指针调用的函数
如果你把函数指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数的时候,被调用的函数就是回调函数
在作者之前函数指针变量的博客里边,就有提到一个转移表,那么用回调函数去实现它是一个很不错的选择
#include<stdio.h>
//回调函数实现转移表
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
void cal(int(*pf)(int x, int y))//回调函数
{
int a = 0;
int b = 0;
printf("请输入你的操作数:");
scanf("%d %d", &a, &b);
int ret = 0;
ret = pf(a, b);
printf("%d\n", ret);
}
int main()
{
int input = 0;
do
{
printf("************************\n");
printf("**1.add******2.sub******\n");
printf("**3.mul******4.div******\n");
printf("*****0.退出程序*********\n");
printf("************************\n");
printf("请输入你的选择:");
scanf("%d", &input);
switch (input)
{
case 1:
cal(add);
break;
case 2:
cal(sub);
break;
case 3:
cal(mul);
break;
case 4:
cal(div);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("输入错误,请重新选择\n");
}
} while (input);
return 0;
}
作者有话说:作者只是一只初学小白,以上的解释分析均是作者自己对已学知识的理解,如有错误,感谢指出;如感到有帮助。请留下一个👍