一 内存与地址与指针
在计算机中处理数据时,需要调用内存中的数据,数据处理后的数据也会返回内存中。
对于内存空间,被划分为一个个内存单元,每个内存单元的大小为一个字节,即1Byte-8bit(一个字节内能放八个比特位)。当计算机需要处理内存中的数据时,通过内存单元的地址来找到它,这里的地址我们可以理解为指针。即内存单元被指针指向着,计算机通过指针可以找到该内存单元。我们可以理解为内存单元的地址==指针。
二 指针变量
我们知道,内存的地址被指针指向。那么我们可以取出某个数据的地址将其赋予给一个变量,那么这个变量被称作指针变量。
下列代码中的变量num被指针变量p指向,那么我们通过对p解引用可以得到变量num的值
图解如下:
三 指针变量的大小
32位的机器有32根地址线,每根地址线转换成数字信号有两种结果:0和1,那么32根地址线产生的二进制序列当作一个地址,就是32个bit位,即4个字节。同理,64 位机器的一个地址就是8个字节。
在X86环境下,即32位机器,每个指针变量的大小是4个字节
在X64环境下,即64位机器,每个指针变量的大小是8个字节
指针变量的大小与1其指针类型无关,只有平台相关,在相同平台下,指针变量的大小是相同的。
四 指针变量类型的意义
在X86平台下,指针的大小为4个字节,在X64平台下,指针的大小为8个字节。
4.1 指针的解引用
下列代码在调试中观察内存可知,int*指针的解引用一次操作四个字节
但是char*类型的指针只修改了一个字节(char*)&num表示将num的地址强制转换为char类型的指针。
所以我们可以得到:指针的类型决定了指针解引用的时候的权限。
4.2指针+-整数
在指针的+-运算中,是根据指针的类型来决定跳过的字节数。如:int类型的指针每次跳过4个字节,char类型的指针每次跳过1个字节。
4.3 void*指针
void*指针也称作“泛型指针”,可以用来接收任何类型的地址,但不能直接进行+-整数运算与解引用操作。
例如下列代码,用char类型的指针来接收int类型的变量,VS就会报错,“int*类型无法转换成char*类型”。
但是,我们使用void*类型的指针来接收就不会显示这个错误,但是后续就无法进行指针的操作。
4.4 const修饰
被const修饰的变量属性变成常属性,不能被更改。在我们对数据进行调用时,如果不想数据在调用过程中被修改或者覆盖,我们可以使用const将其变成常量。
在下面的代码中,变量a被const修饰,当我们想对a进行修改时,VS报错。
但是,a的本质终究是变量,只不过被const修饰后有了限制。知道原因后,我们可以绕过这个限制,不直接对变量a进行修改,而是通过a的地址进行修改。
但是一般来讲,const修饰指针的时候,放在*的左边与右边意义是不一样的。
void test1() {
int n = 10;
int m = 20;
int* p = &n;
*p = 10;
p = &m;
}
//const放在左边
void test2() {
int n = 10;
int m = 20;
const int* p = &n;
*p = 10;//报错
p = &m;
}
//const放在右边
void test3() {
int n = 10;
int m = 20;
int* const p = &n;
*p = 10;
p = &m;//报错
}
//两边都有const
void test4() {
int n = 10;
int m = 20;
const int* const p = &n;
*p = 10;//报错
p = &m;//报错
}
结论:
如果const放在*的左边,那么修改的是指针指向的内容,指针指向的内容不能改变,但是指针本身可以被改变。
如果const放在*的右边,那么修饰的是指针本身,指针变量的内容不能修改,但是指针指向的内容可以通过指针改变。
4.5 指针运算
指针有三种运算:指针+-整数,指针-指针,指针的运算关系。
下面让我们来依次分析
*指针+-整数
以数组为例,因为数组在内存中是连续存放的,知道数组首元素的地址就知道数组剩余的元素。
通过指针的+-运算可以得到数组中的元素:
*指针-指针
指针-指针运算得到的是指针之间的元素个数,但是有一个前提:指针指向的是同一块空间。
在库函数中,我们可以通过strlen函数来获取字符串中的字符个数,我们可以通过指针-指针操作来复现库函数中的strlen函数。
注:strlen统计的是‘\0’之前的个数。
*指针的关系运算
通过指针间的预算关系我们也可以实现对数组的打印
五 野指针
野指针就是无法确定其指向的指针。
下列就是野指针,因为定义整型指针时没有确定其指向的位置,后面当我们为p指向的位置赋值时,就造成了非法访问(因为p的指向根本不知道,是随机的)。所以当我们要使用指针时,一定要对指针进行初始化操作或者置为NULL,避免野指针的出现。
int main(){
int*p;
*p=1;
return 0;
}
除此之外,还有两中比较常见的野指针:越界访问和内存是释放。
越界访问顾名思义就是指针的指向范围超过了原先的范围,当超出原先的范围时,指针就无法找到准确的位置,这时指针是野指针 。
指针指向的内存释放:一个函数返回了一个地址,指针指向该地址,但函数调用后立马销毁,地址也不存在了,这时候指针就是野指针。
六 assert断言
assert是宏定义,被定义在assert.h头文件中。
当使用assert时,程序运行时如果不符合指定条件,就报错终止运行。如果assert()表达式为真,assert()就不会产生任何作用。
而对于程序中的assert(),我们也可以控制其是否进行断言,只需在assert.h头文件前定义宏NDEBUG即可。
#define NDEBUG
#include<assert.h>
当使用指针作为调用函数时,我们需要判断这个指针是否为空指针,这时候assert()就可以派上用场。
void test(char*arr){
assert(arr);//判断传过来的指针是否为空指针
}
七 传值调用与传址调用
通过值的调用与通过地址的调用是不一样的。
//下面的代码不会实现两个数的更改,因为在函数内是开辟新的空间来存储,函数调用后立马销毁
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 10;
int b = 20;
printf("%d %d\n", a, b);
swap(a, b);
printf("%d %d", a, b);
}
传址调用可以让函数与主函数的建立真正的关系,在函数内部可以实现修改主函数的变量。
//通过调用两个数的地址来更改,那么在函数调用销毁后,地址指向的数已经改变,可以实现
void swap(int*a, int*b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int a = 10;
int b = 20;
printf("%d %d\n", a, b);
swap(&a,&b);
printf("%d %d", a, b);
}
当我们需要用主函数中的变量来实现计算时,可以使用传值调用。当我们需要修改主函数内部的变量数据时,应当使用传址调用。
八 数组名的理解
我们发现数组名和数组首元素的地址打印结果一致,数组名就是数组首元素的地址。
int main() {
int arr[] = { 1,2,3,4,5,6 };
printf("%p\n", arr);
printf("%p", &arr[0]);
}
但是有两个例外:sizeof(数组名)和&数组名。
sizeof(数组名):表示整个数组的大小,单位是字节。如果是数组首地址的话,那么输出应该是4或者8.
&数组名:取出的是整个数组的地址,与数组首元素地址有区别。
当我们对数组的地址进行+-操作时,取数组首元素的地址与数组的地址实现的功能有区别。
我们发现,&arr[0]与arr都是首元素的地址,+1操作后跳过数组中的一个元素,即四个字节。
而对于&arr,取的是数组的地址,+1操作跳过的是整个数组。
九 通过指针访问数组
以下是通过指针对数组进行赋值与输出
int main() {
int arr[10] = {0};
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++) {
scanf_s("%d", p + i);
}
for (int i = 0; i < sz; i++) {
printf("%d", p[i]);//p[i]与*(p+i)本质上是等价的
}
return 0;
}
十 一维数组的传参本质
我们知道,数组可以作为参数传递给函数。
对于计算数组内的元素个数,我们一般用到:
int sz=sizeof(arr)/sizeof(arr[0]);
如果我们在函数的内部使用这串代码,那么还能不能实现计算数组元素的功能?
可以看到,两者的结果并不一致。
在本质上,数组传参传递的是数组首元素的地址,那么在函数内部,sizeof(arr)计算的是一个地址的大小而不是如同主函数内整个数组的大小,在X86环境下,传递数组首元素大小为4个字节,数组首元素的大小也为4个字节,那么输出的结果为1。那么我们可以得知,数组传参的本质可以说传递的是指针,而不是整个数组,所以在函数内部无法求得整个数组的元素个数。
如下面的代码,将形参改成指针形式,结果也一致。
十一 二级指针
指针存放着地址,但是指针变量也是变量,指针也有地址, 那么我们将存放一级指针变量的地址的变量称作二级指针。
通过对二级指针pp解引用*pp找到一级指针p,再对一级指针p解引用*p找到a。三级指针四级指针与二级指针一致,每一级指针存放的是上一级指针的地址。
十二 指针数组
整型数组存放的是整型变量,字符数组存放的是字符变量,同理,指针数组存放的是指针变量(每个元素存放的都是指针的)。
通过指针数组,我们可以模拟二维数组的实现:
//指针数组模拟二维数组
int main() {
int arr1[] = { 1,2,3 };
int arr2[] = { 3,4,5 };
int arr3[] = { 4,5,6 };
int* arr[] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", arr[i][j]);
}printf("\n");
}
}
上述代码只能模拟二维数组的效果,但并非是真实的二维数组,因为每一行并非是连续的。
十三 字符指针
对于字符指针的一般使用:
int main() {
char ch = 'a';
char* p = &ch;
*p = 'A';
return 0;
}
除此之外还有一种使用方式:
int main() {
const char* p = "hellow.com";
printf("%s", p);
}
但在这里并 不是将字符串“hellow.com”存放到指针p中,而是将字符串的首字符地址放进指针p中。
下面我们来看一下有关字符串与指针开辟空间的问题,在下述代码中的结果是怎样的呢?
int main() {
char str1[] = "hellow";
char str2[] = "hellow";
const char* p1= "hellow";
const char* p2 = "hellow";
if (str1 == str2) {
printf("str1=str2\n");
}
else {
printf("str1!=str2\n");
}
if (p1 == p2) {
printf("p1=p2\n");
}
else {
printf("p1!=p2");
}
}
str1与str2字符串的内容虽然一致,但是这是两个不同的变量,常量字符串区初始化不同数组的时候会开辟出不同的内存块,所以str1与str2不同。
在C/C++里,常量字符串会存储到一个单独的内存区域,当几个指针指向同一个字符串时,他们会指向同一块内存。所以p1=p2。
十四 数组指针
指针数组与数组指针有上面区别呢?
指针数组是一种数组,里面的每个元素都是指针。
数组指针是指针变量,存放的是数组的地址,是能够指向数组的指针变量
//指针数组
int*arr[10];
//数组指针
int(*p)[10];
在数组指针中,*先与p结合,表示这是个指针,这个指针指向大小为10的数组,所以这是个数组指针。
数组指针的初始化
在前面我们了解到,通过&arr[0]获取到的是数组首元素的地址,&arr则是数组的地址。
将数组的地址存放在指针中,如下:
int(*p)[10]=&arr;
十五 二维数组的传参本质
如二维数组arr[3][3]={{1,2,3},{2,3,4},{3,4,5}},我们一般会将其画成下面的样子:
但是在内存中,这些元素是连续存放的:
对于一个三行三列的二维数组arr[3][3],可以看成是三个一维数组,每个一维数组有三个元素组成。前面我们学到,对于数组来说,指针指向的是数组首元素的地址,那么对于二维数组来说,首元素地址是第一个一维数组的地址。
//二维数组的传参本质
void test(int(*p)[5], int x, int y) {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 5; 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);
}
对于形参,我们可以写出数组int arr[3][5](行数可以忽略不写),也可以写成指针int(*p)[5]的形式。
十六 函数指针
对于函数来说,函数也是有地址的:
void test() {
printf("hehe\n");
}
int main(){
printf("%p\n", test);
printf("%p", &test);
}
函数名就是函数的地址,也可以通过&函数名的方法获得函数的地址。
函数既然有地址,那么我们就可以使用指针将其储存起来,这样的指针我们将其称之为函数指针。
函数指针的写法与数组指针十分类似:
//数组指针
int (*p)[10];
//函数指针
int (*p)(形参)=函数名;
如:
int (*p)(int x,int y)=Add;
我们可以通过函数指针调用指针指向的函数:
int add(int x, int y) {
return x + y;
}
int main() {
int a = 10;
int b = 20;
//先找到函数的位置:通过指针p找到函数,进而通过指针调用函数
int(*p)(int, int) = add;
printf("%d\n", (*p)(a, b));
printf("%d", (*p)(a, 10));
}
输出结果:
十七 typedef关键字
typedef关键字是用来类型重命名的,可以将复杂的类型简单化。
如将unsigned int 重命名为uint。
typedef unsigned int uint;
对于指针类型,我们也可以将其重命名,如下代码将int*的指针重命名为ptr_t。
typedef int* ptr_t;
但对于数组指针与函数种子则有些不同 :
数组指针需要将新的类型名放到*的右边,此时数组指针重命名为par_t。
typedef int(*par_t)[10];
函数指针也需要将新的类型名放到*的右边,此时函数指针重命名为pbr_t。
tydepef void(*pbr_t)(int);
十八 函数指针数组
将多个函数的地址可以存放到数组中,这个数组被称为函数指针数组。
函数指针数组定义:
int (*p[3])();
p先与[]结合,说明p是数组,是int(*)()类型的函数指针。
利用函数指针函数,我们可以实现一个简单的计数器:
#include<stdio.h>
//函数指针数组实现转移表
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(*p[5])(int x, int y) = { 0,add,sub,mul,div };//函数名就是函数的地址
int input = 1;
int x = 0;
int y = 0;
do {
printf("0.退出 1.add 2.sub 3.mul 4.div\n");
printf("请输入:");
scanf_s("%d", &input);
//判断输入数是否合理
if (input >= 1 && input <= 4) {
printf("请输入两个操作数:");
scanf_s("%d %d", &x, &y);
int ret = (*p[input])(x, y);
printf("结果是:%d\n", ret);
}
else if (input == 0) {
printf("退出成功");
}
else {
printf("输入有误,重新输入\n");
}
} while (input);
}
十九 回调函数
将函数的地址作为参数传递给另一个函数,这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数可以很好的解决代码臃肿的问题。
下述代码通过switch语句实现简单的计数器功能,但是输入输出语句是冗余的。
#include<stdio.h>
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 input = 1;
int x = 0;
int y = 0;
do {
printf("0.退出 1.add 2.sub 3.mul 4.div\n");
printf("请输入:");
scanf_s("%d", &input);
int ret = 0;
switch (input) {
case 0:
printf("退出成功");
case 1:
scanf_s("%d %d", &x, &y);
ret = add(x, y);
printf("%d\n", ret);
break;
case 2:
scanf_s("%d %d", &x, &y);
ret = sub(x, y);
printf("%d\n", ret);
break;
case 3:
scanf_s("%d %d", &x, &y);
ret = mul(x, y);
printf("%d\n", ret);
break;
case 4:
scanf_s("%d %d", &x, &y);
ret = div(x, y);
printf("%d\n", ret);
break;
default:
printf("输入错误\n");
break;
}
} while (input);
}
但是通过回调函数我们可以解决这个问题,只是改变了调用函数的逻辑,从直接调用变成用函数调用。
#include<stdio.h>
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;
}
void cald(int(*pt)(int x, int y)) {//函数的形参就是函数指针
int ret = 0;
int x = 0;
int y = 0;
scanf_s("%d %d", &x, &y);
ret = pt(x, y);
printf("结果是:%d\n", ret);
}
int main() {
int input = 1;
int x = 0;
int y = 0;
do {
printf("0.退出 1.add 2.sub 3.mul 4.div\n");
printf("请输入:");
scanf_s("%d", &input);
int ret = 0;
switch (input) {
case 0:
printf("退出成功");
case 1:
cald(add);
break;
case 2:
cald(sub);
break;
case 3:
cald(mul);
break;
case 4:
cald(div);
break;
default:
printf("输入错误\n");
break;
}
} while (input);
}