本次讲解重点:
1、字符指针
2、数组指针
3、指针数组
4、数组传参和指针传参
5、函数指针
关于指针这个知识点的主题,我们在前面已经初级阶段已经对指针有了大致的理解和应用了。我们知道了指针的概念:
1、指针就是地址,但口语中说的指针通常指的是指针变量,指针变量用来存放地址,地址唯一标识一块内存空间。
2、指针的大小是固定的4/8字节(32位平台/64位平台)。
3、指针是有类型的,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。
4、指针的运算。
5、二级指针:存放一级指针变量的地址的变量。
理解:指针变量前的第一个*与变量结合表示它是指针,再往前面所有东西表示这个指针所指向对象的类型。
关于初阶指针有什么不太了解,大家可以点击下方链接学习一遍之后,再来学习进阶。
S6---初阶指针_wangjiushun的博客-CSDN博客
接下来,我们继续探讨指针的高级主题。
1. 字符指针
在指针的类型中我们知道有一种指针类型为字符指针:char*
一般我们使用:让一个字符指针指向一个字符变量
int main()
{
char ch = 'a';
char* pc = &ch;
return 0;
}
还有一种使用方式:让一个字符指针指向一个常量字符串的首字符地址
#include<stdio.h>
int main()
{
const char* pstr = "abcdef";
printf("%s\n", pstr);
return 0;
}
代码const char *pstr="abcdef";
很容易让我们误以为是把常量字符串存储到字符指针pstr里了,但是想一想一个字符指针怎么可能放得下7个字节的字符串呢?所以其本质是把字符串abcdef首字符的地址放到了pstr中。
图解:
知识点总结:
1. 常量字符串:
①常量字符串不能被修改:用const修饰(如果修改,程序崩溃);
②常量字符串出现在表达式中时,这个常量字符串的值是首字符的地址。
2. const修饰的常变量:变量pstr不能被修改,但是pstr本质上还是一个变量,所以叫常变量
3. %s-打印字符串,直到遇到'\0'才停止。
有这么一道面试题:
#include<stdio.h>
int main()
{
char str1[] = "hello bit";
char str2[] = "hello bit";
const char* str3 = "hello bit";
const char* str4 = "hello bit";
if (str1 == str2)
{
printf("str1 and str2 are same\n");
}
else
{
printf("str1 and str2 are not same\n");
}
if (str3 == str4)
{
printf("str3 and str4 are same\n");
}
else
{
printf("str3 and str4 are not same\n");
}
return 0;
}
大家猜猜运行的结果。
运行结果:
为什么会这样了?
知识点:
总结:
①(常量字符串,不能被修改)C/C++会把常量字符串存储到单独的一个内存区域,当有几个指针,指向同一个常量字符串的时候,它们实际会指向同一块内存。
②但是用相同的常量字符串去初始化不同的数组的时候,就会开辟不同的内存块。③一个变量对应着一个唯一的空间。
图解:
所以str1和str2不同,str3和str4相同。
2. 指针数组
在初阶指针里我们已经学习一次指针数组,指针数组是一个存放指针的数组。
这里,我们在回顾一下:
整形数组-存放整形的数组;
字符数组-存放字符的数组
指针数组-存放指针(地址)的数组
指针数组的使用:使用一维数组模拟二维数组
代码1:存放字符指针的数组
#include<stdio.h>
int main()
{
//存放字符指针的数组
const char* arr[4] = { "abcd","hello","hehe","wang" };
//打印
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%s\n", arr[i]);
}
return 0;
}
图解:
代码2:存放整形指针的数组
#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* arr[3] = { arr1,arr2,arr3 };
//打印
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("%d ", *(arr[i] + j));//等价于arr[i][j]
}
printf("\n");
}
return 0;
}
图解:
总结:这就是和二维数组一样的,只不过我们是使用指针数组来模拟的。
3. 数组指针
3.1 数组指针的定义
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针-存放整形地址的指针-指向整形的指针 int*
字符指针-存放字符(字符串)地址的指针-指向字符(字符串)的指针 char*
那数组指针是:存放数组地址的指针-指向数组的指针
代码实例:
int main()
{
//整形指针
int a = 10;
int* pi = &a;
//字符指针
char ch = 'w';
char* pc = &ch;
//数组指针
int arr[10] = { 0 };
//int* pa[10]是这样的吗,但是这不是指针数组,
//pa先与[]结合了就是数组,
int(*pa)[10] = &arr;
return 0;
}
对数组指针的理解:①pa先和*结合,说明pa是一个指针变量,然后指向的是一个大小为10个整形的数组。(即pa是一个指针,指向一个数组的指针,叫做数组指针)
②数组指针后面的[]表示所指向的数组有几个元素
知识点:
①注意:[]的优先级要高于*的,所以必须加上()来保证pa先和*结合。
②指针变量前的第一个*与变量结合表示它是指针,再往前面所有的东西表示这个指针所指向对象的类型。
接下来,我们深入了解数组名和&数组名
3.2 &数组名VS数组名
代码1:打印&数组名和数组名的地址观察
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//打印数组名地址
printf(" arr-->%p\n", arr);
//打印&数组名地址
printf("&arr-->%p\n", &arr);
return 0;
}
运行结果:
运行之后我们发现:&数组名和数组名的地址是一样的。
难道两个是一样的吗?
代码2:&数组名和数组名+1
//代码2:
//数组名-数组首元素的地址
// &数组名-是数组的地址
// 数组首元素的地址和数组的地址从值的角度来看是一样的,但是意义不一样
// 指针的类型决定了指针+-整数的步长,指针解引用操作的时候的权限
//
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf(" arr---->%p\n", arr);//指针的类型:int*
printf(" arr+1-->%p\n", arr + 1);//+1,向后跳4个字节
printf("&arr---->%p\n", &arr);//指针的类型:int(*)[10]
printf("&arr+1-->%p\n", &arr + 1);//+1,向后跳40个字节
return 0;
}
运行结果:
图解:
根据上面的代码,我们发现:
数组名-数组首元素的地址
&数组名-是数组的地址
数组首元素的地址和数组的地址从值的角度来看是一样的,但是意义不一样。
知识点:
1. 指针的类型决定了指针+-整数的步长,指针解引用操作的时候的权限。
2. 存储一个数组所需的内存字节数:
①一维数组:总字节数=sizeof(类型)*元素个数
②二维数组:总字节数=sizeof(类型)*行数*列数
3、对二维数组数组名的加深理解:数组名就是二维数组首元素的地址
对于二维数组来说,它的第一行就是它的第一个元素,第二行就是它的第二个元素……即一行是一个元素,二维数组是一个一维数组的数组。
//代码:二维数组的数组名 //二维数组的数组名是首元素地址-->第一行的地址 // 对于二维数组一行是一个元素 #include<stdio.h> int main() { int arr[3][4] = { 0 }; printf("%p\n", arr); printf("%p\n", arr + 1);//跳过了16个字节 return 0; }
运行结果:
上机运行实践,也正是如此:arr+1跳40个字节,刚好是一行。
注意区分:二维数组的第一行第一个元素地址是&arr[0][0],
3.3 数组指针的使用
代码1:一维数组数组指针的使用,但是我们很少这样写代码
//代码1:一维数组数组指针的使用,但是我们很少这样写代码
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//数组指针
int(*p)[10] = &arr;
//打印数组
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", (*p)[i]);//*p[i]这种写法的话,p先与[i]结合再解引用
}
return 0;
}
代码2:二维数组数组指针的使用
#include<stdio.h>
void print1(int arr[3][4], int r, int c)
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print2(int(*p)[4], int r, int c)
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ", *(*(p + i) + j));//等价于(*(p+i))[j],也等价p[i][j]
}
printf("\n");
}
}
int main()
{
int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
//调用函数打印数组
print1(arr, 3, 4);
print2(arr, 3, 4);
return 0;
}
图解:
学了指针数组和数组指针我们一起回顾并看看下面代码的意思:
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
4. 数组参数、指针参数
我们写代码的时候难免要把数组或者指针传给函数,那么函数的参数该如何设计呢?
4.1 一维数组传参
void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int* arr)//ok?
{}
void test2(int* arr[20])//ok?
{}void test2(int* arr[])//ok?
{}
void test2(int** arr)//ok?
{}
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
4.2 二维数组传参
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
void test(int* arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int(*arr)[5])//ok?
{}
void test(int** arr)//ok?
{}
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
4.3 一级指针传参
思考:当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
答案是:①一级指针变量;②一维数组数组名;③变量地址。
比如:
void test1(int *p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?
void test1(int* p)
{}
void test2(char* p)
{}
int main()
{
//test1函数能接收什么参数?
int a = 10;
int* pi = &a;
int arr[5] = { 0 };
test1(pi);//一级指针变量
test1(arr);//一位数组数组名
test1(&a);//变量地址
//test2函数能接收什么参数?
char ch = 'w';
char* pc = &ch;
char arr1[6] = { '\0' };
test2(pc);
test2(arr1);
test2(&ch);
return 0;
}
4.4 二级指针传参
思考:当函数的参数为二级指针的时候,函数能接收什么参数?
答案是:①二级指针变量;②指针数组数组名;③一级指针变量地址。
代码实例:
void test(char** p)
{ }
int main()
{
char c = 'b';
char* pc = &c;
char** ppc = &pc;
char* arr[10];
test(&pc);//一级指针变量地址
test(ppc);//二级指针变量
test(arr);//指针数组数组名
return 0;
}
5、函数指针
我们先打印一下函数的地址:
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
//打印函数地址
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
运行结果:
从结果我们发现:取函数名和函数名,打印的地址一样。
结论:①取函数名和函数名,都是函数的地址。
那函数的地址是怎么保存的呢?
看下面代码:
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
//数组指针-指向数组的指针
int arr[10] = { 0 };
int(*pa)[10] = &arr;
//函数指针-指向函数的指针
int (*pf)(int, int) = &Add;//pf 是一个存放函数地址的指针变量-函数指针
int ret1 = pf(2, 3);//pf是函数地址,Add也是函数地址,所以不用解引用也可以
int ret2 = (*pf)(2, 3);//写上*也可以,符合语法,但是需注意带()
printf("%d %d\n", ret1, ret2);
return 0;
}
类比数组指针,函数指针也是如此要注意带()保证*和变量pf先结合。
使用函数指针找到所指向的函数:
①直接写函数指针:因为函数指针变量存储的就是函数的地址,所以在使用的时候可以不解引用。
②函数指针解引用:解引用其实没必要,这样写就是方便理解。因为pf是指针,指针要找到他所指向的对象(对象函数名就是函数的地址),解引用更容易理解,符合语法的理解。(注意:优先级缘故,如果要解引用,一定要使用()把*和函数指针变量括起来。)
现在我们来看两段有趣的代码:
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
代码1的解读:
int main()
{
( *( void(*)() ) 0 )();
//分析:
// ①void(*)()-->函数指针类型,这个函数是无参的
// ②()里面放类型-->强制类型转换
// ③( void(*)() ) 0 -->把0强转成这种函数指针类型,
// 就是0被当成了函数的地址
// ④(*指针变量)-->解引用操作,找到0地址的函数
// ⑤最后的()-->调用函数不用传参(因为强转的0函数类型是无参的)
//
//总结:该代码是一次函数调用,调用0地址处的一个函数
// 首先代码中将0强转为类型为( void(*)() )的函数指针
// 然后去调用0地址处的函数
return 0;
}
破题点:从0出发,再开始分析。
代码2的解读:
//函数返回类型是函数指针
int main()
{
void (*signal(int, void(*)(int))) (int);
//分析:①signal没有与*结合,这不是指针
// ②signal后有括号()-->signal是函数名
// ③函数名括号()里-->函数参数
// 第一个参数是int类型,第二个参数是函数指针类型
// ④语句最后有;没有函数体-->函数是声明
// ⑤函数的声明-->形式:函数返回类型 函数名 (形参列表);
// 那剩下的void(*)(int)就是函数返回类型了
//
//总结:该代码是一次函数的声明
// 声明的函数名字叫signal
// signal函数的参数有2个,第一个是int类型,第二个是函数指针类型,
// 该函数指针能够指向的那个函数的参数是int,返回类型是void。
// signal函数的返回类型是一个函数指针,
// 该函数指针能够指向的那个函数的参数是int,返回类型是void。
//我们能不能这么写?
//void(*)(int) signal(int,void(*)(int));
//答案是不能这么写,帮助理解可以这么写,但是语法上是错的
//函数的返回类型是函数指针是,只能把函数名 (形参列表)移到*的后面。
//优化:类型简化--->typedef
//函数指针的重命名
typedef void(*pf_t)(int);
pf_t signal(int, pf_t);
return 0;
}
破题点:从signal出发,再开始分析。
知识点:
1、函数返回类型是指针函数时的写法:当函数的返回类型是函数指针时,只能把函数名 (参数列表)移到返回类型这个函数指针的*的后面。
2、指针类型简化(重命名)--->typedef
指针类型重命名,别名写在*的后面并且还要注意操作符的优先级(即*先与别名结合)
指针进阶的主题并未结束,在下一次文章终结。
有什么不足希望大家指出,我会更加努力写出更好的文章。