目录
前面我们已经了解了指针的基本概念以及简单的使用,那么什么问题一定要使用指针解决呢?
我们来接着往下学习:
传值调用和传址调用
问题:写一个函数,交换两个变量的值。
传值调用
#include<stdio.h>
void Swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("a=%d b=%d", &a, &b);
Swap(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
我们发现a和b的值并没有发生交换,这是为什么呢?
我们可以调试来看看
在进入函数内部后,把a的值给x,b的值给y
函数完成交换后,x和y的值发生了交换,但是a和b的值没有发生交换。
我们可以看到在main函数内部,创建了a和b,a的地址是0x009af998,b的地址是0x009af98c,在调用Swap函数时,将a和b传递给了Swap函数,在Swap函数内部创建了形参x和y接收a和b的值,但是x的地址是0x009af8b4,y的地址是0x009af8b8,x和y确实接收到了a和b的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap函数内部交换x和y的值,不会影响a和b,当Swap函数调⽤结束后回到main函数,a和b的没法交换。Swap函数在使⽤的时候,是把变量本⾝直接传递给了函数,这就是传值调⽤。
结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实 参。 (单向值传递)
传址调用
//传址调用
#include<stdio.h>
void Swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("a=%d b=%d", &a, &b);
Swap(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
常见错误
在写这个函数内部的代码的时候可能会出现一些错误。
例1:
#include<stdio.h>
void Swap(int* x, int* y)
{
int* temp = x;
x = y;
y = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("a=%d b=%d", &a, &b);
Swap(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
//err2
#include<stdio.h>
void Swap(int* x, int* y)
{
int* temp;
*temp = *x;
*x = *y;
*y = *temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("a=%d b=%d", &a, &b);
Swap(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
这里就是我们之前提到的野指针的问题,temp没有进行初始化,temp的值是随机的,对*temp赋值就是向一个未知的存储单元赋值,而这个未知的存储单元可能存放着有用的数据,这样就有可能影响系统的正常工作情况,编译器会进行报错。
使用指针变量的好处
函数调用使用return语句只可以得到一个返回值,而使用指针变量就可以得到多个变化了的值。传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量。
二级指针
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;//指向a的指针变量pa
int** ppa = &pa;//二级指针ppa存放pa的地址
printf("%p\n", pa);
printf("%p\n", *ppa);//*ppa 通过对ppa中的地址进⾏解引用,找到的是pa
printf("%d\n", **ppa);
//**ppa==*(*(ppa))==*(pa)==a
//**ppa先通过*ppa找到 pa,然后对 pa 进⾏解引用操作:* pa,最后找到a
return 0;
}
数组名的理解
在前面我们知道一维数组名事实上是首元素地址,那是不是数组名都是代表首元素地址呢?
先说结论:
数组名是数组首元素(第⼀个元素)的地址是对的但是有两个例外:• sizeof(数组名) ,sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的大小, 单位是字节• &数组名 ,这⾥的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素 的地址是有区别的)
sizeof(数组名)
使用
#include<stdio.h>
int main()
{
int arrt[] = { 1,2,3,4,5,6 };
printf("sizeof(arrt)=%d\n", sizeof(arrt));
// 数组元素为整型,一个元素4个字节,整个数组24个字节
char arrc[] = "abcdef";
printf("sizeof(arrc)=%d\n", sizeof(arrc));
// 数组元素为字符,一个元素1个字节,整个数组7个字节(包括'\0')
return 0;
}
sizeof和strlen的对比
sizeof1. sizeof是 操作符2. sizeof 计算操作数所占内存的大小,单位是字节3. 不关注内存中存放什么数据
strlen1. strlen是 库函数 ,使⽤需要包含头⽂件 string.h2. srtlen是 求字符串⻓度的,统计的是 '\0'之前字符的个数3. 关注内存中是否有'\0' , 如果没有'\0',就会持续往后找 ,可能会越界
#include<stdio.h>
#include<string.h>
int main()
{
char arrc[] = { 'a','b','c' };
printf("sizeof(arrc)=%d\n", sizeof(arrc));
printf("strlen(arrc)=%d\n", strlen(arrc));
return 0;
}
#include<stdio.h>
#include<string.h>
int main()
{
char arrc[] = { 'a','b','c','\0'};
printf("sizeof(arrc)=%d\n", sizeof(arrc));
printf("strlen(arrc)=%d\n", strlen(arrc));
return 0;
}
#include<stdio.h>
#include<string.h>//strlen头文件
int main()
{
char arrc[] = "abc";
printf("sizeof(arrc)=%d\n", sizeof(arrc));
printf("strlen(arrc)=%d\n", strlen(arrc));
return 0;
}
计算数组元素个数
sizeof(数组名)既然代表整个数组的字节数的话,那么我们怎么用sizeof来计算数组元素个数呢?我们可以将整个数组的字节数 / 一个数组元素的字节数就可以计算出数组元素个数了。
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
int sz2 = sizeof(arr) / sizeof(int);
//数组元素为整型,一个元素4个字节
printf("sz1=%d\n", sz1);
printf("sz2=%d\n", sz2);
return 0;
}
&数组名
&数组名的时候数组名是代表整个数组地址。
我们来看看一段代码:
#include <stdio.h>
int main()
{
int arr[6] = { 1,2,3,4,5,6 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
return 0;
}
我们可以看到他们输出的结果是一样的,有人会好奇不是说&数组名的时候数组名是代表整个数组地址吗,为什么会输出一样的地址呢?
别着急,我们再来看看一段代码
#include <stdio.h>
int main()
{
int arr[6] = { 1,2,3,4,5,6 };
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0] + 1);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr + 1);
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr + 1);
return 0;
}
当它们都加1的时候就发生了区别,我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是首元素地址+1就是跳过⼀个元素。 但是&arr 和 &arr+1相差24个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。
所以我们可以得出结论:&数组名,这⾥的数组名表示整个数组,取出的是整个数组的地址。
使用指针访问数组
#include <stdio.h>
int main()
{
int arr[6] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* pa = arr;//一维数组名是数组首元素地址
int i = 0;
for (i = 0; i < sz; i++)
{
scanf("%d", pa+i);
//scanf("%d", arr+i);
}
printf("\n");
for (i = 0; i < sz; i++)
{
printf("%d ", *(pa + i));
}
return 0;
}
⼀维数组传参的本质
#include <stdio.h>
void test(int arr[])
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("sz2=%d\n", sz2);
}
int main()
{
int arr[6] = { 0 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("sz1=%d\n", sz1);
test(arr);
return 0;
}
//计算指针变量的大小 #include <stdio.h> //使用数组传参的两种形式 void test1(int arr[]) { printf("test1:%zd\n", sizeof(arr)); } void test2(int* arr) { printf("test2:%zd\n", sizeof(arr)); } int main() { int arr[6] = { 0 }; int sz1 = sizeof(arr) / sizeof(arr[0]); test1(arr); test2(arr); return 0; }
如果对指针在VS不同环境是不同大小不清楚的,可以看看上一篇博客哦!
总结: ⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
字符指针
使用方式
这里单独讲解一下字符指针。
从名字上来看,字符指针就是指向字符的指针,存放的是字符变量的地址,类型为char*
例:
//字符指针
#include<stdio.h>
int main()
{
char c = 'd';
char* pc = &c;//字符指针
printf("%c\n", *pc);//字符指针解引用
return 0;
}
字符指针只有一种使用方式吗?
当然不是
我们来看看另外一种字符指针使用的方式:
#include<stdio.h>
int main()
{
char* pc = "abcdef";
return 0;
}
字符指针变量被一个常量字符串赋值,那么这里的字符指针存放的是一整个字符串吗?
我们来简单的验证一下:
#include<stdio.h>
int main()
{
char* pc = "abcdef";
printf("%c\n", *pc);
printf("%s\n", *pc);
return 0;
}
我们分别以单个字符和字符串的形式来打印字符指针pc解引用的内容。
我们可以看到以字符串形式打印的时候并没有得到我们想要的结果,虽然编译器没有报错,但是它给出了警告。
事实上,这里本质是把字符串 "abcdef\0" , 首字符的地址放到了pc中,如果打印字符串,需要首元素(字符)地址来进行打印。
正确形式:
#include<stdio.h>
int main()
{
char* pc = "abcdef";
printf("%c\n", *pc);
printf("%c\n", *(pc + 1));
printf("%s\n", pc);
return 0;
}
所以这种形式是把⼀个常量字符串的 首字符的地址 存放到 字符指针变量 中,而 常量字符串不可以修改 ,我们可以使用 const进行修饰 来提醒我们。
趣味代码
我们来看看一个有趣的代码:
#include <stdio.h>
int main()
{
char str1[] = "hello world.";
char str2[] = "hello world.";
const char* str3 = "hello world.";
const char* str4 = "hello world.";
if (str1 == str2)
printf("str1 and str2 are same\n");//1
else
printf("str1 and str2 are not same\n");//2
if (str3 == str4)
printf("str3 and str4 are same\n");//3
else
printf("str3 and str4 are not same\n");//4
return 0;
}
你知道这一段代码答案是什么吗?
答案:2,3(不知道你做对了吗?)
我们一起来看看:
C/C++会把常量字符串存储到单独的⼀个内存区域,我们知道常量字符串是不可以被修改的,既然不可以被修改,那么内容相同的常量字符串只需要保存一次就好了,当⼏个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存,所以str3和str4得到的是相同的首元素(字符)地址;而数组是可以被修改的,如果⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存,所以str1和str2得到的是不同的首元素(字符)地址。
指针数组
使用
指针数组?是指针?还是数组呢?我们来类比一下
整型数组——存放整型的数组,数组元素类型是整型。
字符数组——存放字符的数组,数组元素类型是字符。
所以:
指针数组——存放指针的数组,数组元素类型是指针。
指针数组事实上是数组,那么它应该如何使用呢?
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* pa = &a;
int* pb = &b;
int* pc = &c;
return 0;
}
我们来看看这一段代码,创建了3个指针变量来存放3个整型变量的地址,这样看起来是不是显得十分臃肿多余,如果需要存放更多的指针呢?我们以前知道存放更多的数据可以使用数组,那么在这里我们就可以使用指针数组。
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
/*int* pa = &a;
int* pb = &b;
int* pc = &c;*/
int* p[3] = { &a,&b,&c };
//下标 0 1 2
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d\n", *p[i]);
}
return 0;
}
解释 int* p[3]
p先与[3]结合 ([ ]的优先级要高于*号的),说明p是数组
3 ——p数组里面有三个元素
int* ——每个数组元素是int* 类型的,也就是整型指针
结论:指针数组的每个元素都是用来存放地址(指针)的。
模拟二维数组
#include <stdio.h>
int main()
{
int arr1[4] = { 1,2,3,4 };
int arr2[4] = { 2,3,4,5 };
int arr3[4] = { 3,4,5,6 };
int* p[3] = { arr1,arr2,arr3 };
//数组名是首元素地址
//p先与[3]结合,说明p是数组,有三个元素
//int* 说明每个数组元素是int* 类型的,也就是整型指针
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
//printf("%-3d", p[i][j]);
printf("%-3d", *(*(p + i) + j));
//*(p+i)== p[i]
//先对p+i解引用,得到一个地址(指针)
}
printf("\n");
}
return 0;
}
数组指针
使用
类比:
整型指针——指向整型的指针,存放的是整型变量的地址。
字符指针——指向字符的指针,存放的是字符变量的地址。
所以:
数组指针——指向数组的指针,存放的是数组的地址。
想要得到数组的地址,通过前面我们知道应该是 &(数组名)
#include <stdio.h>
int main()
{
int arr[6] = { 1,2,3,4,5,6 };
int(*p)[6] = &arr;//数组指针
return 0;
}
解释 int (*p)[6]
(*p)—— p先和*结合,说明p是⼀个指针变量
【[ ]的优先级要⾼于*号的,加上()来保证p先和*结合】
6 ——p指向数组的元素个数
int —— p 指向的数组的元素类型
所以p是⼀个指针,指向⼀个数组,这就是数组指针。
类型
数组指针是什么类型呢?
相信大家都有经验了吧!
去掉名字就是类型,那么上面代码中数组指针的类型是 int(*)[6] 。
二维数组传参的本质
前面我们了解到一维数组传参的本质 ,那我们接下来一起来了解下二维数组传参的本质 。
前面我们把二维数组做函数参数,会写出下面的代码:
#include <stdio.h>
void print_arr(int arr[][4])
{
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 4; j++)
printf("%d ", arr[i][j]);
printf("\n");
}
}
int main()
{
int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
print_arr(arr);
return 0;
}
在前面,我们写二维数组做函数参数的时候,形参也写成⼆维数组的形式(可以省略行,但是不可以省略列),那么有没有其他的形式呢?
深度理解二维数组
⼆维数组可以看做是每个元素是⼀维数组的数组,也就可以理解为 ⼆维数组的每个元素是⼀个⼀维数组 ,⼆维数组的⾸元素就是第⼀行,是一个⼀维数组。因为数组名是首元素地址,所以 二维数组的数组名表⽰的就是第⼀⾏的地址 ,是 ⼀维数组的地址 。 第⼀⾏的⼀维数组的类型就是 int [4] ,所以第⼀⾏的地址的类 型就是数组指针类型 int(*)[4] 。
#include<stdio.h>
int main()
{
int arr[3][4] = { 0 };
printf("arr == %p\n", arr);
printf("arr + 1 == %p\n", arr + 1);
printf("arr + 2 == %p\n", arr + 2);
return 0;
}
⼆维数组传参本质上也是传递了地址,传递的是第⼀行 ⼀维数组的地址,那么形参也是可以写成指针形式的。
形参写成指针形式
#include <stdio.h>
void print_arr(int (*p)[4])
{
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 4; j++)
//printf("%d ", p[i][j]);
printf("%d ", *(*(p + i) + j));
printf("\n");
}
}
int main()
{
int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
print_arr(arr);
return 0;
}
有人可能会好奇为什么打印可以写成 *(*(p+i)+ j)的形式呢?
我们知道⼆维数组的每个元素是⼀个⼀维数组,那么这个一维数组的数组名是什么呢?
事实上,在上面的代码中,一维数组的数组名就是arr[i] , 数组名arr[i]又是一维数组的首元素地址,再进行加 j 解引用就可以得到相应的元素了。
#include<stdio.h>
int main()
{
int arr[3][4] = { 0 };
for (int i = 0; i < 3; i++)
{
printf("%p\n", arr[i]);
}
return 0;
}
函数指针
相信大家已经有经验了
函数指针——指向一个函数的指针,存放的是函数的地址。
那么函数是不是也有地址呢?我们来简单测试一下
函数的地址
#include<stdio.h>
void test(int n)
{
printf("n == %d\n", n);
}
int main()
{
int a = 10;
test(a);
printf("test == %p\n", test);
printf("&test == %p\n", &test);
return 0;
}
我们可以发现函数也是有地址的,并且无论是以函数名还是以&函数名用取地址的形式打印,得到的是一样的地址。
结论:函数是有地址的,函数名就是函数的地址,函数名和&函数名都是代表函数地址,没有区别
函数指针变量
那么如果存放函数地址就需要创建一个函数指针变量,那么应该如何创建一个函数指针变量呢?我们可以类比于数组指针的写法
#include<stdio.h>
int Max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
//数组指针
int arr[6] = { 1,2,3,4,5,6 };
int(*p1)[6] = &arr;
//函数指针
int a = 10;
int b = 20;
int(*p2)(int, int) = &Max;
int(*p3)(int x, int y) = &Max;
return 0;
}
p2,p3先与*结合说明是一个指针变量
(int ,int) ——指向函数的参数类型和个数的交代 【也可以写成(int x,int y)】
p2,p3 ——函数指针变量名int ——指向函数的返回类型
使用函数指针变量
那么应该如何使用函数指针变量呢?
#include<stdio.h>
int Max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
//函数指针
int a = 10;
int b = 20;
int(*p)(int, int) = &Max;
int ret1 = Max(a, b);//函数调用
//函数名其实就是函数的地址
int ret2 = (*p)(a, b);//使用函数指针变量
//对p解引用找到这个函数
int ret3 = p(a, b);
//p里面存放的是函数的地址
//与int ret1 = Max(a, b);效果相同
printf("ret1 == %d\n", ret1);
printf("ret2 == %d\n", ret2);
printf("ret3 == %d\n", ret3);
return 0;
}
这里呢,*也可以不写,因为p得到的就是函数Max的地址,与int ret1 = Max(a, b); 效果相同。
当然写上更加方便我们理解,首先对p解引用找到这个函数,再进行运算,记得带上( )让p先与*结合。
函数指针数组
定义
正确形式: int (*parr[ 3 ]) ();
parr1 先和 [ ] 结合,说明 parr是数组数组的内容 是 int (*)() 类型的函数指针
计算器的实现
如果我们想要写一个代码来实现加减乘除的计算,我们很容易写出下面的代码:
#include<stdio.h>
void menu()
{
printf("*****请选择计算方式*****\n");
printf("*****1.Add 2.Sub*****\n");
printf("*****3.Mul 4.Div*****\n");
printf("***** 0.exit *****\n");
}
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 = 0;
int b = 0;
int input = 0;
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入操作数:\n");
scanf("%d %d", &a, &b);
printf("结果为:%d\n", Add(a, b));
break;
case 2:
printf("请输入操作数:\n");
scanf("%d %d", &a, &b);
printf("结果为:%d\n", Sub(a, b));
break;
case 3:
printf("请输入操作数:\n");
scanf("%d %d", &a, &b);
printf("结果为:%d\n", Mul(a, b));
break;
case 4:
printf("请输入操作数:\n");
scanf("%d %d", &a, &b);
printf("结果为:%d\n", Div(a, b));
break;
case 0:
printf("退出计算器!\n");
break;
default:
printf("选择有误!请重新选择!\n");
break;
}
printf("\n");
} while (input);
return 0;
}
我们可以看到这个代码中有许多重复的部分,显得有点冗长,那么有没有什么办法可以进行优化呢?
我们来看看下面的代码:
#include<stdio.h>
void menu()
{
printf("*****请选择计算方式*****\n");
printf("*****1.Add 2.Sub*****\n");
printf("*****3.Mul 4.Div*****\n");
printf("***** 0.exit *****\n");
}
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 = 0;
int b = 0;
int input = 1;
int(*p[5])(int x, int y) = { 0, Add, Sub, Mul, Div };//函数指针数组
//函数名就是函数的地址
do
{
menu();
scanf("%d", &input);
if (input > 0 && input <= 4)//满足条件才输入操作数
{
printf("请输入操作数:\n");
scanf("%d %d", &a, &b);
int ret = (*p[input])(a, b);//使用
printf("结果为:%d\n", ret);
}
else if (input == 0)
printf("退出计算器!\n");
else
printf("选择有误!请重新选择!\n");
printf("\n");
} while (input);
return 0;
}
我们可以看到这一段代码进行了进一步的优化,使用了一个函数指针数组,来存放不同函数的地址,在下面通过下标来访问使用函数,这种间接的使用函数的方式我们叫转移表。