回顾
首先我们来大概回顾一下指针初阶的知识
内存会划分为一个个的内存单元,每个内存单元都有一个独立的编号—编号也被称为地址,地址在C语言中也被称为指针,指针(地址)需要存储起来—存储到变量中,这个变量就被称为指针变量
例如:
int a = 10;
int* pa = &a;
指针的大小固定是4/8个字节(32位/64位平台)
地址是物理的电线上产生
电信号转化为数字信号
32位机器—32根地址线—1/0
32个0/1组成的二进制序列,把这个二进制序列就作为地址,32个bit位才能存储这个地址
也就是需要4个字节才能存储
所以指针变量的大小就是4个字节
同理在64位机器上,地址的大小是64个0/1组成的二进制序列,需要64个bit位存储,也就是8个字节,所以指针变量的大小就是8个字节
1 字符指针
在指针的类型中我们知道有一种指针类型为字符指针char*
一般使用
int main()
{
char ch = 'w';
//ch = 'a'//这里可以理解成办事的方法有很多种,这只是其中一种
char *pc = &ch;//pc就是字符指针
*pc = 'w';
return 0;
}
现在有以下代码
int main()
{
char arr[] = "abcdef";
const char* p = "abcdef";//本质上是把字符串首字符的a地址放在p当中,相当于p指向了字符串首元素a的地址//这个字符串是一个常量字符串,常量字符串不能被修改,所以我们需要加上一个const防止被修改
return 0;
}
这里本质上是把字符串首字符的a地址放在p当中,相当于p指向了字符串首元素a的地址//这个字符串是一个常量字符串,常量字符串不能被修改,所以我们需要加上一个const防止被修改,另外注意一下const的用法,const放在的左边限制的是p,const放在*右边限制的p
我们来将上面的代码打印出来看一看效果
int main()
{
char arr[] = "abcdef";
const char* p = "abcdef";//本质上是把字符串首字符的a地址放在p当中,相当于p指向了字符串首元素a的地址//这个字符串是一个常量字符串,常量字符串不能被修改,所以我们需要加上一个const防止被修改
printf("%s\n", p);
printf("%c\n", *p);
return 0;
}
大家思考一个问题(注意结合上述字符指针的知识),看下面代码
int main()
{
const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", pstr);
return 0;
}
代码 const char* pstr = “hello bit.”,本质是把字符串 hello bit. 首字符的地址放到了pstr中
上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量pstr中
掌握了字符指针的基本知识过后我们来看一道笔试题,看下面代码
#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;
}
2 指针数组
整型数组—存放整型的数组
字符数组—存放字符的数组
指针数组—存放指针的数组
这里我们再复习一下,下面的指针数组分别是什么意思
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
我们来使用指针数组模拟实现一个二维数组
int main(int argc, char* argv[], char* envy[])
{
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };
int* arr[3] = { arr1,arr2,arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", *(*(arr + i) + j));
Sleep(1000);
}
printf("\n");
}
system("pause");
return 0;
}
这里的*(*(arr + i) + j)意思是,arr这个指针变量里面存放的是首元素的地址,也就是arr1数组名第一个元素的地址,然后+依次遍历二维数组,我这里加的一个Sleep(1000)的意思是停留1秒钟打印一个数组
这不是真正的二维数组,真正的二维数组在内存中是连续存放的
3 数组指针
3.1 数组指针的定义
数组指针
类比:
整型指针—指向整型变量的指针,存放整型变量的地址的指针变量
字符指针—指向字符变量的指针,存放字符变量的地址的指针变量
数组指针—指向数组的指针,存放的是数组的地址的指针变量
我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针
浮点型指针: float * pf; 能够指向浮点型数据的指针
那数组指针应该是:能够指向数组的指针
下面代码哪个是数组指针?
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
解释
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指
向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
3.2 &数组名VS数组名
这里我们把数组名的理解讲一下
数组名是数组首元素的地址
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
但是数组名表示首元素的地址有两个例外
1.sizeof(数组名),这里的数组名表示的不是数组首元素的地址,数组名表示整个数组,sizeof(数组名)计算的是整个数组的大小,单位是字节
2.&数组名,这里的数组名表示整个数组,&数组名取出的是整个数组的地址
除此之外,所有的其他地方的数组名都是数组的首元素的地址
这里有一个现象,大家看下面代码
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%p\n", &arr);
return 0;
}
这里打印的地址都是一样的,但是本质上真的是一样的吗?
我们将代码调试起来看一看
这里虽然我们看到的地址是一样的,但是他们的类型有所差距
这里取地址arr的类型应该是int(*)【10】
正因为他们的类型不同
现在我们来看下面代码
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", arr + 1);
printf("%p\n", &arr[0]);
printf("%p\n", &arr[0] + 1);
printf("%p\n", &arr);
printf("%p\n", &arr + 1);
return 0;
}
你拷贝这串代码编译运行会发现各自加1的效果产生了不同的效果
原因是指针类型的不同带来的不同效果
对于数组名来说
&数组名得到的是数组的地址
3.3 数组指针的使用
int main()
{
int arr[10] = { 0 };
//数组的地址存放到数组指针变量
//int[10]*p=&arr;//error
int(*p)[10] = &arr;
//int*p1=&arr;
return 0;
}
这里的int*p1=&arr可能会报警告,因为指针类型不符合,上面我们给大家介绍了&arr的指针类型是int(*p1)[10]
那么数组指针怎么使用呢?一般放在二维数组上才方便
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int (*arr)[5], int row, int col)
{
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
return 0;
}
看下面代码
//二维数组传参,形参是指针的形式
void Print(int (*p)[5], 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));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7 };
//arr 是数组名,数组名表示数组首元素的地址
Print(arr, 3, 5);
return 0;
}
这里给大家解释一下*(*p+i)+j),加i相当于拿到某一行的地址,然后加j在进行遍历
二维数组的每一行可以理解为二维数组的一个元素,每一行又是一个一维数组,所以二维数组其实是一维数组的数组
二维数组的数组名也是数组名,数组名就是首元素的地址,arr—首元素的地址—第一行的地址—一维数组的地址—数组的地址
一维数组传参,形参的部分可以是数组,也可以是指针
我们看下面代码
//void test1(int arr[5], int sz)
//{}
//void test2(int* p, int sz)
//{}
//
//int main()
//{
// int arr[5] = { 0 };
// test1(arr, 5);
// test2(arr, 5);
// return 0;
//}
形参部分可以写成数组,但是本质上是一个指针,所以建议用一个指针来接收
二维数组传参。形参部分可以是数组,也可以是指针
//void test3(char arr[3][5], int r, int c)
//{}
//
//void test4(char (*p)[5], int r, int c)
//{}
//int main()
//{
// char arr[3][5] = {0};
// test3(arr, 3, 5);
// test4(arr, 3, 5);
//
// return 0;
//}
//
数组指针—是指针—是指向数组的指针
指针数组—是数组—是存放指针的数组
大家可以用好孩子这个例子去理解一下
4数组传参 指针传参
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
4.1 一维数组传参
#include <stdio.h>
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?
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}
第一个OK,数组传参用一个数组接收没毛病
第二个OK,只是把元素个数写出来了
第三种OK,用一个指针接收
第四种OK,一个数组20个元素,每个元素为int*,用int*arr[20]接收没毛病
第五种OK,一级指针的地址用一个二级指针来接收,所以也没毛病
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);
}
第一种OK
形参部分,行可以省略,但是列不能省略
总结:二维数组传参,函数形参的设计只能省略第一个[]的数字
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素
这样才方便运算
所以第二种不OK,第三种OK
第四种,数组名是首元素的地址—二维数组是一行的地址,绝对不是某一个的地址,所以错误
第五种不行,因为我传过去的是数组名,这里的形参只是个指针数组,那肯定不行,要么写成数组指针或者二维数组
第六种OK,是一个数组指针,可以指向一行
第七种错误,数组名表示首元素的地址,第一行的地址怎么可能会用二级指针来接收呢,肯定不对!
4.3 一级指针传参
#include <stdio.h>
void print(int *p, int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d\n", *(p+i));
}
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9};
int *p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
大家自行理解一下,比较简单
这里大家思考一个问题
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
比如
void test1(int *p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?
4.4 二级指针传参
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}
思考:
当函数的参数为二级指针的时候,可以接收什么参数?
void test(char **p)
{
}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);//Ok?
return 0;
}
5函数指针
函数指针—指向函数的指针
数组指针—指向数组的指针
举个例子,看代码
//int Add(int x, int y)
//{
// return x + y;
//}
//
//int main()
//{
// int arr[10] = {0};
// int (*pa)[10] = &arr;
// //printf("%p\n", &Add);
// //printf("%p\n", Add);
结果是一模一样的,你们可以在自己的编译器上面去试一试
函数名是函数的地址
&函数名也是函数的地址
//int Add(int x, int y)
//{
// return x + y;
//}
// int (*pf)(int, int) = &Add;//pf是函数指针变量
// //int (*)(int, int) 是函数指针类型
// return 0;
//}
来看一段代码
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出的结果
看下面代码
//int Add(int x, int y)
//{
// return x + y;
//}
//
//int main()
//{
// //int (*pf)(int, int) = &Add;
// int (*pf)(int, int) = Add;
//
// int r = Add(3, 5);
// printf("%d\n", r);
//
// int m = (*pf)(4, 5);
//
// printf("%d\n", m);
//
// return 0;
//}
注意*要先和pf结合称为一个指针,再和参数类型结合组合称为函数指针
来看看下面两段有趣的代码
我们先看第一个
//代码1
(* (void (*)())0 )();
很多人看第一眼我去直接懵逼了,这个0是个什么东西,一个函数指针的相关知识搞这么复杂,其实这行代码是有他的特定意思的,我给大家解释一下
0可以默认为是整型
我可以把0认为是int类型,和x0012ff40没有区别各位
也可以0x0012ff40—是一个以16进制表示的整数
你也可以认为这个东西是地址—地址是编号—可以是整型的地址—Int
举个例子
//void(*p)()—p是函数指针
//void(*)()是函数指针类型
我在0前面加了有个括号
这里是把0强制类型转化为下面这种
void(*)()这种指针类型
所以这行代码的意思是调用0地址处的函数,将0强制类型转化为
void(*)()类型的函数指针
然后调用0地址处的这个函数
这里面就运用到了函数指针的知识,尝试着去理解一下,这行代码出自于C陷阱与缺陷
来看第二个代码
//代码2
void (*signal(int , void(*)(int)))(int);
signal是一个函数声明
signal函数有两个参数,第一个参数的类型是Int,第二个参数类型是void(*)(int)函数指针类型
该函数指针指向的函数有一个Int类型的参数,返回类型为void
signal函数的返回类型也是void(*)(int)函数指针类型,该函数指针指向的函数有一个int类型的参数,返回类型为void
这个代码有点复杂,我们稍作修改
我们需要了解一下typedef这个函数—类型重命名
// typedef void(*pf_t)(int);
// pf_t signal(int, pf_t);
// void (* signal(int, void(*)(int) ) )(int);
//