一、内存和地址
1.1内存
在讲内存和地址之前,我们想一个生活案例:假设有一栋没有门牌的房子,你想在这栋房子里找一个东西,由于没有门牌号,你并不知道这个东西在哪个房间,于是就要挨个房间去找,这种查找方式会导致效率低下,但是,如果我们在每个房间编上号,那么你就可以快速找到你想取的房间。
计算机中的内存是同理,计算机在CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那么这些内存空间如何高效管理呢?其实就是把内存划分为一个个内存单元,每个内存单元的大小为一个字节。一个字节等于8个比特位,每个比特位存储二进制的0或1,每个内存单元都有一个编号,这个编号我们称为指针。
1.2.地址
计算机中的编制不是把每个字节的内存单元的地址记录下来,而是通过硬件设计完成的。首先我们必须理解,计算机内有很多的硬件单元,而硬件单元是要相互协作的,如何将每个独立的硬件联系起来呢?就是通过线连接起来,不过跟指针有关的是地址总线。32位机器有32根地址总线,每根线只有两态,表示0或1(电脉冲的有无),那么一根线就能表示两种含义,32根线就能表示2^32种含义,每种含义表示一个地址。
二、与指针相关的操作符和关键字
2.1 取地址操作符(&)
知道了C语言中内存和地址的关系,那么我们想知道数据在内存中的地址该怎么做呢?C语言提供了取地址操作符&,使用方法如下:
int main()
{
int a = 10;
printf("%p\n", &a);
return 0;
}
2.2 解引用操作符(*)
我们通过取地址操作符得到了地址,那么地址储存在哪里呢?答案是指针变量int*p中,其中的p表示指针的名字,后续我们可以通过p找到该指针变量,因此,指针变量是用来存放地址的。
当我们知道了变量在内存中的地址之后,我们可与不可以通过地址找到该变量呢?答案依旧是肯定的,C语言提供了解引用操作符*,后期我们可以通过*找到该变量。
2.3 指针变量的大小及意义
32位机器下,指针变量的大小是4个字节;64位机器下,指针变量的大小是8个字节。指针变量可以加减整数,解引用操作等,其类型决定了指针变量解引用一次能操作多少个字节,以及向前向后走一步距离是多大。
2.4 const修饰指针
变量可以通过直接修改以及通过指针变量修改,为了不让变量被修改,我们引入了const。
int main()
{
int n = 10;//n可以修改
const int m = 10;
m = n;//m不可以修改
return 0;
}
我们对上述代码进行分析,m本质上是变量,之所以不能修改,是因为我们用const在语法上加了限制,使其变为常变量,但我们可以绕过const从而将m修改
int main()
{
int n = 10;
const int m = 20;
int* p = &m;
*p = n;
printf("%d\n", m);
return 0;
}
2.5 const修饰指针变量
void test1()
{
int n = 10;
int m = 20;
int* p = &n;
*p = m;
printf("n=%d m=%d\n", n, m);
}
void test2()
{
int n = 10;
int m = 20;
const int* p = &n;
*p = m;
printf("n=%d m=%d\n", n, m);
}
void test3()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = m;
printf("n=%d m=%d\n", n, m);
}
void test4()
{
int n = 10;
int m = 20;
const int*const p = &n;
*p = m;
printf("n=%d m=%d\n", n, m);
}
int main()
{
test1();
test2();
test3();
test4();
return 0;
}
const修饰指针变量的时候(左定值右定向)
如果放在*的左边:限制*p,即不能通过指针变量p修改p指向空间的内容,但p是不受限制的,
如果放在*的右边:限制p变量,即p变量无法修改,但*p不受限制,还是可以通过p修改p所指对象的内容。
2.6 野指针
野指针是指指针所指向的位置是不可知的。
野指针的成因:
1.指针未初始化;2.指针越界访问;3.指针指向空间已释放
如何规避野指针:
1. 指针初始化:
如果明确知道指针指向哪里就明确赋值,如果不知道就制空NULL。
2.小心指针越界:
一个程序向内存申请了哪些空间通过指针就只能访问哪些空间,不能超出范围访问。
3.指针变量不再使用时及时制NULL,使用前检查指针的有效性。
4.避免返回局部变量的地址。
2.7 assert断言
assert.h头文件定义了宏assert(),用在运行时确保程序满足指定条件,如果不符合,就报错终止运行。
assert对于程序员是很友好的,使用assert()有几个好处:不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。如果已经确认程序没有问腿,不需要再做断言,就在#include<assert.h>前定义一个宏NDEBUG。然后重新编译程序,编译器就会禁用文件中所有的assert()语句。如果程序出现问题,可以移除#define NDEBUG这条指令。
三、指针的使用和传址调用
3.1 传值调用和传址调用
学习指针的目的是使用指针解决问题,那么是什么问题?为什么非指针不可呢?下面我们将讨论传值调用和传址调用,从而更加深入理解指针的作用,我们观察下面的代码:
我们发现a和b的值并没有像我们想象的那样相互交换,这是为什么呢?事实上,实参传递给形参的时候,形参会单独创建一份临时空间来接受实参,对形参的修改不会影响实参。
那么我们该怎么办呢?重新审视指针的定义,我们不难发现,指针是内存单元的编号,为了达到我们想要的效果,我们只需要让形参和实参所指向的地址是同一块空间即可,根据这个原理,我们在传参的时候需要把实参的地址传给形参,那么我们改进上述代码:
将变量的地址传给函数,这种函数调用方式叫:传址调用
传址调用可以让函数和主函数之间建立真正的联系,在函数内部可以修改主函数的变量,所以未来函数中只是需要主函数中的变量值来实现计算,就可以采用传值调用,如果函数内部要修改主调函数中的变量值,就需要传址调用。
3.2 strlen的模拟实现
库函数strlen是统计\0之前的字符个数,参数str是字符串首元素的地址,将其传给my_strlen函数时开始遍历统计\0之前的字符个数,只要不是\0计数器count就+1,这样我们就可以模拟实现一个strlen函数。
四、数组名的理解以及一维数组传参的本质
4.1 数组名的理解
在介绍数组名之前我们先看以下代码:
我们发现打印出来的首元素地址和arr数组名的地址是一样的,那么是不是可以说:数组名就是首元素的地址呢?如果是,那么下面代码该怎么解释?
但事实上,数组名就是首元素的地址,但存在两个特例:
(1.)sizeof(数组名):sizeof中单独放数组名计算的是整个数组的大小。
(2.)&数组名:这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和首元素的地址是有区别的)。为此我们再看以下代码:
我们发现在给&arr+1之后跳过了80个字节,这是整个数组的长度。
那么在我们知道数组名代表首元素的地址之后,我们是否可以这样写代码:
int main()
{
int arr[10] = { 0 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
for (i = 0;i < sz; i++)
{
scanf("%d", p+i);
}
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
4.2 一维数组传参的本质
我们通过上述论证知道了:数组名是首元素的地址,那么在数组传参的时候传递的是数组名,也就是说数组传参的本质是首元素的地址,因此,一维数组传参,形参的部分可以写成数组的形式也可以写成指针的形式。
五、冒泡排序
5.1 冒泡排序
其核心在于:两两相邻元素进行比较,代码如下:
void bubble_sort(int* arr, int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
for (j = 0; j < sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 2,5,9,7,6,3,4,1,8,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
for (int i = 0; i < sz; i++)
{
printf("%d ", *(arr + i));
}
return 0;
}
5.2 二级指针
指针变量也是变量,存放指针变量的地址的指针叫做二级指针,对二级指针解引用找到的是一级指针存放的地址。
六、数组和指针
6.1 指针数组
什么是指针数组?顾名思义,就是存放指针的数组。形如:int*parr[ ] 。那么指针数组有什么作用呢?我们可以通过它来模拟二维数组。
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* parr[3] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
6.2 数组指针变量
我们知道,指针数组是存放指针的数组,那么数组指针变量是指针变量还是数组?答案是指针变量,是存放数组的地址,能够指向数组的指针变量,形如:int(*p)[ ]
由于数组指针变量是存放指针的数组,那么它应该指向地址,因此数组指针变量的初始化应该为:
int(*parr)[10]=&arr;
了解了数组指针变量后,我们就可以知道二维数组传参的本质:传递的是第一行这个一维数组的地址,代码如下:
void test(int(*p)[5], int x, int y)
{
for (int i = 0; i < x; i++)
{
for (int j = 0; j < y; j++)
{
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
6.3 函数指针变量
函数指针变量是用来存放函数的地址的,未来通过地址可以调用函数的。形如:int (*p) (int x,int y)
那么,函数是否有地址呢?我们观察以下代码:
我们看到,函数是有地址的,而且函数名就是地址,也可以通过&函数名得到函数的地址,如果我们要把函数的地址存放起来就要有函数指针变量.
函数指针变量的使用:
6.4 函数指针数组
函数指针数组就是存放函数的数组,形如:int (*p[ ]) ()。函数指针数组的一个用途是转移表,即:将类型相同的几个函数存放在一个数组中,从而实现函数的连续调用。
计算器的一般实现
void menu()
{
printf("******************************\n");
printf("****** 1.加法 2.减法 *****\n");
printf("****** 3.乘法 4.除法 *****\n");
printf("****** 5.退出程序 *****\n");
printf("******************************\n");
printf("请选择:");
}
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int a, b;
int input = 0;
int ret = 0;
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
printf("输入两个操作数\n");
scanf("%d %d", &a, &b);
ret = Add(a, b);
printf("ret=%d\n", ret);
break;
case 2:
printf("输入两个操作数\n");
scanf("%d %d", &a, &b);
ret = Sub(a, b);
printf("ret=%d\n", ret);
break;
case 3:
printf("输入两个操作数\n");
scanf("%d %d", &a, &b);
ret = Mul(a, b);
printf("ret=%d\n", ret);
break;
case 4:
printf("输入两个操作数\n");
scanf("%d %d", &a, &b);
ret = Div(a, b);
printf("ret=%d\n", ret);
break;
case 5:
printf("退出程序\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input);
return 0;
}
使用函数指针数组实现:
void menu()
{
printf("******************************\n");
printf("****** 1.加法 2.减法 *****\n");
printf("****** 3.乘法 4.除法 *****\n");
printf("****** 5.退出程序 *****\n");
printf("******************************\n");
printf("请选择:");
}
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int a, b;
int input = 0;
int ret = 0;
int(*p[5])(int x, int y) = { NULL,Add,Sub,Mul,Div };//转移表
do
{
menu();
scanf("%d", &input);
if (input <= 4 && input >= 1)
{
printf("请输入两个操作数:\n");
scanf("%d %d", &a, &b);
ret = (*p[input])(a, b);
printf("ret=%d\n", ret);
}
else if (input = 5)
{
printf("退出计算器\n");
}
else
printf("输入错误,请重新输入");
} while (input);
return 0;
}