内存
内存是计算机的重要存储器,计算机的程序都是在内存中运行的,内存只能暂时性的存储数据,断电就会丢失数据。内存是一块很大的空间,将内存划分为一个个内存单元,内存单元的大小是一个字节,给每个内存单元都编号,编号就是内存单元的地址,可以通过地址访问内存单元的数据。内存就像生活中的城市,内存单元就像城市里的房子,内存单元的地址就像房子的地址,有了房子的地址,就能找到房子,访问房子里面的人。
32位平台下,内存编号是32根地址线或者32根数据线通电后,通过高电平或者低电平的电信号转换成数字信号1或0,有2的三十二次方个编号,从0x00000000到0xFFFFFFFF,内存大小为4GB。
注:本编博客在32位平台下编译的。
指针
指针就是内存地址,指针变量是用来存储内存地址的变量。指针变量的类型要对应存储变量的类型再加一个*,口头上的指针一般都是指针变量。
变量的地址是存储数据最小的那一个地址,a的地址是0x0078FE90,从低地址开始存储数据,访问数据的时候从低地址开始往后面找。a是int类型的,所以需要int类型的指针来存储a的地址。
解引用操作符
*:解引用操作符,指针解引用,访问指针指向的数据。
将int类型的变量a的地址存储到int类型的指针变量pa中,对pa解引用,访问的数据a,再将a修改为10。
指针大小
指针大小跟存储的数据类型没有关系,因为存储的是地址,地址多大是跟多少位平台有关。
32位情况下,地址是32个bit,需要四个字节才能保存该地址;64位情况下,地址是64个bit,需要八个字节才能保存改地址。
指针类型
指针类型决定了指针向前或向后走一步多少字节,跳过指针指向的类型大小字节 。
指针的类型决定了指针解引用的时候,能够访问多少字节空间的数据。
指针运算
指针+-整数
指针+-整数是跳过整数个指针指向的对象大小。
字符指针+-整数,跳过整数乘字符大小个字节。整型指针+-整数,跳过整数*整型大小个字节。
指针++:pch++是pch = pch + 1,跳过一个字符,来到该地址。pa++是pa = pa + 1,跳过一个整型,来到该地址。
指针-指针
指针-指针的绝对值是指针与指针之间的元素个数。注:指针-指针,这两个指针必须指向同一块空间。
int arr[5] = { 1,2,3,4,5 };
int* start = &arr[0];
int* end = &arr[5];
int n = end - start; // n等于5
指针比较
往后
int arr[5] = { 1,2,3,4,5 };
for (int* start = &arr[0]; start < &arr[5]; ++start)
{
pritnf("%d ", *start);
}
往前
int arr[5] = { 1,2,3,4,5 };
for (int* start = &arr[4]; start >= &arr[0]; --start)
{
pritnf("%d ", *start);
}
标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
字符指针
存储char类型变量的地址,解引用访问一个字节的空间。
char ch = 'a';
// 存储变量ch的地址
char* pc = &ch;
// 字符数组arr存储字符串"abc",
char arr[] = "abc";
// 存储字符数组首元素的地址
char* parr = arr;
// 常量字符串,具有常属性,不能修改值,使用const修饰指针
// 存储字符串第一个字符的地址
const char* p = "abc";
用字符串"abc"初始化字符数组内容,字符指针parr指向的是数组首元素的地址;常量字符串是存储在内存的常量区的,直接指向该地址。
虽然都用字符串"abc"初始化了arr1和arr2数组,但是arr1和arr2是不同数组,在不同的空间上,所以它们的地址并不一样。字符指针p1和p2都是指向常量字符串"abc",常量字符串只在常量区存储一份,所以它们指向的都是同一块空间。
整型指针
存储int类型变量的地址,解引用访问四个字节的空间。
pa指向的地址是a地址的第一个地址0x00B8FE88。
因为pa是整型指针,指针解引用的时候访问四个字节的空间,*pa = 1,将该地址往后四个字节的空间重新赋值为1。
如果将整型数据的地址用字符指针存储,修改该指针的时候值修改一个字节的空间。
指针遍历数组
#include <stdio.h>
void print(int* p, int n)
{
for (int i = 0; i < n; ++i)
{
printf("%d ", *(p + i));
}
}
int main()
{
int arr[5] = { 1,2,3,4,5 };
int size = sizeof(arr) / sizeof(arr[0]);
print(arr, size);
return 0;
}
二级指针
存放一级指针的指针。
int a = 1;
int* pa = &a;
int** ppa = &pa;
**ppa = 2;
ppa的第二个*表示ppa是一个指针,这个指针指向的对象的int*类型的。*ppa找到pa,**ppa相当于*pa,*pa找到a,将2赋值给a。
指针数组
存放指针的数组
char* arr2[5] // 存放字符指针的数组,数组的元素个数是5
int* arr1[10] // 存放整形指针的数组,数组的元素个数是10
通过指针数组访问指针数据
arr[i]找到指针数组的元素,再通过*解引用访问数据。
模拟二维数组
通过指针数组模拟二维数组。
*(parr+i)访问指针数组中的元素,再通过*(*(parr+i)+j)访问数组arr1、arr2、arr3的元素。
数组指针
讲数组指针之前先温习一下数组名。
数组名
数组名是数组首元素的地址,但是有两个列外。
1.sizeof(数组名) 这里的数组名表示的是整个数组,计算的是整个数组的大小。
2.&数组名 这里的数组名表示的是整个数组,取出的是整个数组的地址,这个地址和首元素的地址是一样的,但是意义不一样,.0对该地址加1,跳过的是整个数组大小。
打印的地址是16进制数。
arr和arr+1相差一个整型大小,4个字节;&arr[0]和&arr[0]+1相差一个整型大小,4个字节。
arr和&arr[0]的地址都是int*类型的,+1跳过一个int类型大小4个字节。
&arr和&arr+1相差五个整型大小,20个字节,&arr+1跳过的整个数组。
&arr的地址+1跳过整个数组大小,那么&arr的地址是什么类型的?&arr是取出真个数组的地址,需要存储在一个数组指针中。
数组指针是一个指向整个数组的指针。
int* arr[10] //指针数组
int (*p)[10] //数组指针
指针数组,[] 的优先级比 * 高,arr先和[10]结合,说明arr是数组,数组元素类型是int*类型。
数组指针,先用()将 * 和 p 结合,说明p是指针,(*p)再和[10]结合,表示指针p指向的是整个数组,数组有十个元素,数组元素的int类型的。
&arr的地址+1跳过整个数组大小,跳过4*10个字节;p指针指向的类型是int [10]的数组,p+1跳过整个数组大小4*10个字节。指针类型对应,+1的效果一样。
char arr[5];
用什么来存放&arr?
char (*p)[5] = &arr;
指针数组当形参接收二维数组。
将二维数组的数组名传参,实际传的首元素的地址,二维数组首元素的地址是&arr[0],取出的是第一行的地址,第一行三个整型元素,需要一个数组指针int (*p)[3]接收。
函数指针
指向函数的指针。函数名就是函数的地址,&函数名跟函数名意义一样,都是函数的地址。
返回类型 (*指针变量名)(参数类型) = 函数名;
函数指针的类型要和该函数的类型一一对应。
#include <stdio.h>
int get_max(int a, int b)
{
return a > b ? a : b;
}
int main()
{
int (*pf)(int, int) = get_max;
// int (*p)(int, int) = &get_max;
int max = pf(3, 10);
printf("%d", max);
return 0;
}
通过函数指针调用函数
int max = pf(3, 10);
int max = (*pf)(3, 10);
这两种形式都可以,使用第一种比较好, 第二种*号要在()内。
int max = (***pf)(3, 10);
对函数指针解引用没什么意义,多少个*都可以,只是语法支持。
理解以下代码:
(* ( void (*) () ) 0) ();
void (* signal(int , void(*) (int) ) ) (int);
void (*) ()是函数指针类型。( void (*) () )是强制类型转换。( void (*) () ) 0是对0进行强制类型转换。(* ( void (*) () ) 0) ()是通过0调用该函数。
先把0强制类型转换为一个函数指针类型,那么0地址处就放着一个返回类型是void,无参数的一个函数,再调用0地址处的函数。
signal先和()结合,表示signal是函数,signal的参数是int和函数指针,形参只有类型,没有变量名,表示signal(int , void(*) (int)是函数声明,剩下的就是函数指针的返回类型是void(*) (int),是一个函数指针。
signal是一个函数声明,函数的参数,一个是int类型,一个是void(*)(int)的函数指针的类型,函数的返回类型也是void(*)(int)的函数指针。
返回类型是函数指针的类型的函数,必须写成:返回类型(* 函数名(参数类型)) (参数类型)
函数指针数组
数组的元素都是相同的,函数指针数组的元素类型是一样的,即函数指针指向的函数的返回类型、函数参数类型和个数是一样的。
#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;
}
int main()
{
int (*pf[4])(int, int) = { add, sub, mul, div };
int i = 0;
for (i = 0; i < 4; i++)
{
int a = pf[i](10, 2);
printf("%d\n", a);
}
return 0;
}
pf先和[4]结合,pf[4]是数组,剩下的是数组的元素类型int (*)(int, int),是函数指针类型。函数指针数组存储了加、减、乘、除四个函数,通过访问函数指针数组找到函数的地址调用函数。
回调函数
回调函数是通过函数指针调用的函数。把函数的指针(地址)作为参数传递给另一个函数,这个函数使用了函数指针,函数指针指向的函数就是回调函数。回调函数不是由该函数的实现直接调用,而是在特定的事件或条件发生时由另外的一个函数调用。
#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 menu()
{
printf("*** 1.add 2.sub ***\n");
printf("*** 3.mul 4.div ***\n");
printf("*** 0.exit ***\n");
}
void cal(int (*pf)(int, int))
{
int x = 0;
int y = 0;
printf("请输入两个操作数\n");
scanf("%d %d", &x, &y);
int ret = pf(x, y);
printf("%d\n", ret);
}
int main()
{
int input = 0;
do
{
menu();
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");
default:
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
add、sub、mul、div这四个函数都是回调函数。这里回调函数的好处,把差异提取出来,大量的重复代码,只保留一份,去掉代码的冗余,调用函数的同时,传需要的功能函数,再通过函数指针调用该功能。后面添加功能的时候,switch-case语句中只需要传函数的地址,再通过函数指针调用函数即可。
不使用回调函数,代码冗余,后续添加功能,重复代码不断累加。
#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 menu()
{
printf("*** 1.add 2.sub ***\n");
printf("*** 3.mul 4.div ***\n");
printf("*** 0.exit ***\n");
}
int main()
{
int input = 0;
do
{
menu();
int x = 0;
int y = 0;
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个操作数\n");
scanf("%d %d", &x, &y);
int ret = add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入两个操作数\n");
scanf("%d %d", &x, &y);
int ret = sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入两个操作数\n");
scanf("%d %d", &x, &y);
int ret = mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入两个操作数\n");
scanf("%d %d", &x, &y);
int ret = div(x, y);
printf("%d\n", ret);
break;
case 0:
printf("退出计算器\n");
default:
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
野指针
野指针是指针指向的位置是不可知的,是一个随机值,这些空间是不允许用户访问的,访问这些空间是非法访问,程序会出错。
产生的原因
局部变量的指针变量未初始化。指针越界访问(内存中未开辟使用的空间,但是却访问了)。
规避野指针
指针初始化。指针不初始化是随机值,对随机值的地址访问,出错。
int* pa;
*pa = 1;
小心指针越界。对数组的遍历要注意边界问题。
int arr[4] = { 1,2,3,4 };
for (int i = 0; i <= 4; ++i)
{
printf("%d ", arr[i]); // i等于4的时候越界
}
指针指向的空间释放置为NULL定义。指针不用的时候值为空指针。
int a = 1
int* p = &a;
p = NULL;
避免返回局部变量的地址(局部变量出了作用域,自动销毁)。
int* f()
{
int a = 1;
return &a;
}
指针使用之前检查有效性(assert断言)。