大家好,我是努力学习游泳的鱼,今天我们来学习C语言的重头戏:指针。指针时C语言里的重难点,是很多初学者的拦路虎,有些同学就是被C语言的指针劝退的,我在初学指针时也走了不少弯路。但是,不要怕!指针真的没有那么难!觉得指针难,是因为学习的方法不对。这篇文章里,我会尽可能用最通俗易懂的语言,来详细讲解指针的方方面面,希望能帮助到有需要的朋友。文章较长,建议先收藏,防止迷路。如果你觉得这篇文章帮助到了你,麻烦点个免费的赞支持一下博主。感谢大家的支持!
提到指针,首先要认识内存。
组装电脑时需要插入内存条,这是电脑必不可少的硬件。内存的作用是存储数据,我们在编程中创建的变量都是放在内存中的。
常见的内存条,大小有8G
,16G
等等。那么大的一块空间是如何管理的呢?内存被划分为一个个很小的内存单元,每个内存单元大小是一个字节,并且对应一个编号。
那么编号是怎么产生的呢?现在电脑常见的配置有64
位机器和32
位机器。这里的64
和32
指的是地址线的条数,每条地址线可以产生高电势和低电势,对应着二进制中的1
和0
。以32
位机器为例,32
位机器可以产生的编号包括32
位全0
到32
位全1
的总共232个二进制数。64
位机器同理。
这里的编号就是传说中的地址!
总结:内存是一个存储器,分为一个个很小的内存单元,每个内存单元大小是一个字节,并且唯一对应一个由32
(64
)根地址线产生的二进制编号,这个编号就是地址。
这就很容易理解后面会讲到的指针变量的大小:32
位机器的地址(也就是刚刚讲到的编号)是32位
的0/1
序列,比如10000100101011110100001010111101
,每个0
或者1
是一个比特位(bit)。比特位是计算机单位中最小的,一个二进制位(0/1
)就是一个比特位。所以,32
位机器的地址大小就是32
bit,也就是4
个字节。同理64
位机器的地址就是64
比特,也就是8
个字节。
&
是C语言提供的操作符,用于取出操作数的地址。其实,我们已经见过这个操作符了,scanf
函数里就会用到scanf("%d", &num);
。用法非常简单,在它后面直接跟你想取地址的对象。比方说:
int a = 0;
&a;
这就取出了a
的地址。
这里需要说明一下,a
是int
类型,大小是4
个字节,也就是需要占用4
个内存单元(前面说了一个内存单元大小是1
个字节),用&
取出来的只是第一个内存单元的地址。
我们还可以把地址打印出来,地址的打印格式是%p
。
#include
int main()
{
int a = 0;
printf("%p\n", &a);
return 0;
}
我们拿到了a
的地址后,会想要把它存起来,这就需要定义一个变量。用来存放地址的变量叫做指针变量,也叫指针。所以,我们可以这样理解:指针就是地址!
没错,指针变量,也就是指针,等价于地址,等价于内存的编号,这只是不同的叫法而已,意思是完全一样的。所以不要把指针想的太高大上,它只是一个普普通通的编号而已。
那么指针变量如何定义呢?这样写int* pa = &a;
这一行代码蕴含着很多的信息。定义了一个指针变量,名字是pa
,并且初始化为变量a
的地址。这里的pa
的类型是int*
。其中这个*
表示pa
是指针变量,而int
表示pa
指向的对象(即a
)是int
类型的。
举一反三:
char ch = 'w';
char* pch = &ch;
这里的*
表示pch
是一个指针变量。char
表示pch
指向的对象(即ch
)是char
类型的。
我们拿到了一个变量的地址,就可以通过这个地址来访问这个变量。这里就要介绍另外一个重要的操作符*
。*
是解引用操作符,又称间接访问操作符。在*
后面跟指针,就能找到指针指向的空间。
#include
int main()
{
int a = 0;
int* pa = &a;
*pa = 1;
printf("%d\n", a);
return 0;
}
这里就直接把pa
指向的对象a
改成了1
。
那么指针变量的大小是多大呢?
其实前面已经剧透过了。32
位机器是4
个字节,而64
位机器是8
个字节,这是由于32
位机器产生的地址是32
个0/1
组成的二进制序列,每个0/1
是一个比特位,总共32
个比特位,即4
个字节。同理64
位机器产生的地址是64
个比特位,即8
个字节。
注意:指针变量的大小跟指针指向的变量的大小无关,只跟机器是32
位还是64
位有关。
下面来验证一下这一点。
#include
struct Stu
{
char name[20];
int age;
float score;
};
void test()
{
printf("hehe\n");
}
int main()
{
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(short*));
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(long*));
printf("%d\n", sizeof(long long*));
printf("%d\n", sizeof(float*));
printf("%d\n", sizeof(double*));
printf("%d\n", sizeof(long double*));
//结构体指针
printf("%d\n", sizeof(struct Stu*));
//数组指针
int arr[10] = { 0 };
printf("%d\n", sizeof(&arr));
//函数指针
printf("%d\n", sizeof(&test));
return 0;
}
测试结果:在32
位(X86
)环境下,全是4
;在64
位环境下(X64
)全是8
。
注:如未特殊声明,以下环境均为X64
。
指针的大小跟指针的类型无关,只和环境(32
位还是64
位虚拟地址空间)有关。也就是说,相同的环境下,不同类型的指针的大小是相同的,比如X86
环境下,char*
和int*
的大小都是4
个字节,那么为什么还要区分不同的指针类型呢?不同的指针类型有什么区别呢?
一般来说,如果我们会用一个整型指针来存储一个整型变量的地址,再对这个整型指针解引用,就能够访问这个整型变量。比如:
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0;
return 0;
}
0x11223344
是一个十六进制数字。这里补充一个知识:十六进制数字都是以0x
开头的。一个十六进制位的大小是4
个比特位,所以两个十六进制位的大小是8
个比特位,即一个字节。对于0x11223344
,11
是一个字节,22
是一个字节,33
是一个字节,44
是一个字节,总共是4
个字节,刚好能够存放在一个int
类型的变量中。
执行int a = 0x11223344;
后,通过调试看,我们把0x11223344
放到了变量a
中,并且在内存中也找到了a
的位置。
接着执行int* pa = &a;
,就把a
的地址存储在pa
中。
最后*pa = 0;
,由于pa
是int*
类型的指针,对它解引用就能够访问一个int
,会把变量a
改成0
。即,对int*
类型的指针解引用,能访问4
个字节。
如果是char*
类型的指针呢?结果又会如何呢?
int main()
{
int a = 0x11223344;
char* pa = &a;
*pa = 0;
return 0;
}
先执行int a = 0x11223344;
。
接着执行char* pa = &a;
。由于char*
的指针也是8
个字节(X64
),存储a
的地址不成问题。
最后,重头戏来了!*pa = 0;
我们对于pa
这个char*
的指针解引用,编译器会认为,我们想要找一个char
类型的变量。而char
类型的变量只有1
个字节,所以只会访问1
个字节,把这1
个字节的空间存储的数据改成0
。
综上,对一个int*
的指针解引用,能访问4
个字节。对一个char*
的指针解引用,能访问1
个字节。
这就是指针类型的第一个作用:
指针类型决定了,指针在被解引用的时候,访问的权限。
指针类型是一种看待内存空间的角度。对一个字符指针来说,内存空间存储的都是字符,对这个指针解引用,会访问1
个字符,即1
个字节的空间。对一个整型指针来说,内存空间存储的都是整型,对这个指针解引用,会访问1
个整型,即4
个字节的空间。
阅读下面的代码:
#include
int main()
{
int a = 0;
int* pa = &a;
char* pc = &a;
printf("%p\n", pa);
printf("%p\n", pc);
printf("%p\n", pa + 1);
printf("%p\n", pc + 1);
return 0;
}
pa
是一个整型指针,pc
是一个字符指针,对它们分别+1
的结果相同吗?
pa
和pc
存储的地址是相同的,都是&a
,但是pa+1
跳过了4
个字节,pc+1
跳过了1
个字节。
这就是指针类型的第二个作用:
指针类型决定了,指针向前或者向后走一步,走多大距离。
对于一个整型指针,向前走一步会跳过一个整型,即跳过4
个字节。对于字符指针,向前走一步会跳过一个字符,即跳过1
个字节。
本质上,对一个int*
指针+1
会在地址上+1*sizeof(int)
,即跳过4
个字节, 对一个char*
指针+1
会在地址上+1*sizeof(char)
,即跳过1
个字节。后面会讲,如果对int*
指针+n
会在地址上+n*sizeof(int)
,对char*
指针+n
会在地址上+n*sizeof(char)
,以此类推。
明白了指针类型的作用后,我们就可以在不同的场景下选择合适的指针类型来解决问题了。比如:
我们有一个数组int arr[10] = {0};
,这个数组有10
个int
,总共40
个字节,如何以字节为单位访问,把这40
个字节的数据都改成'x'
的ASCII码值呢?
首先我们需要一个char*
的指针,才能一次访问1
个字节,+1
后也会跳过1
个字节。char* p = (int*)arr;
,数组名arr
表示数组首元素的地址,是int*
类型的,需要强制类型转换成char*
。一开始让p
指向这个地址,对p
指针解引用能访问1
个字节,从而把这个字节改成'x'
的ASCII码值,接着对p
指针+1
都会跳过1
个字节,就可以以字节为单位访问arr
数组的40
字节的空间了。
int main()
{
int arr[10] = {0};
char* p = (int*)arr;
int i = 0;
for (; i<40; ++i)
{
*p = 'x';
++p;
}
return 0;
}
同理,如果我们想按照整型的方式来访问arr
,每次把4
个字节的数据改成0x11223344
,就应该使用int*
类型的指针。
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for (; i<10; ++i)
{
*p = 0x11223344;
++p;
}
return 0;
}
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
int main()
{
int* p; // 局部变量未初始化,默认是随机值
*p = 20; // 不能这样访问
return 0;
}
由于指针p
是个局部变量,而且没有初始化,存放的是随机值。如果我们把这个随机值当做一个地址,这个地址对应的内存空间不属于我们,是不能访问的,如果强行对这个地址解引用,就会造成非法访问。此时的p
就是野指针。
#include
int main()
{
int arr[5] = {1,2,3,4,5};
int *p = arr;
int i = 0;
for (; i<10; ++i)
{
printf("%d ", *p);
++p;
}
return 0;
}
由于数组arr
里只有5个元素,p
指针以整型为单位向后访问时,只能访问5
次,第6
次访问(i==5
)时,p
指针已经超出了数组的范围,形成了越界访问。此时的p
就是野指针。
int* test()
{
int a = 10;
return &a;
}
int main()
{
int *p = test();
*p = 100;
return 0;
}
局部变量a
在进入test
函数是创建,test
函数调用完毕后就销毁了。如果把a
的地址放到p
指针里,p
指针就指向了一块已经销毁的空间,这块空间的使用权限不属于我们。如果强行对p
解引用,就形成了非法访问。此时的p
就是野指针。
我们一定要小心,不要在代码中出现野指针。规避野指针有以下几点经验。
当我们知道应该如何对指针初始化时,应对其初始化。如:int* p = &a;
当我们不知道应该如何对指针初始化时,应初始化成NULL
。如:int *q = NULL;
而使用前需检查,不是NULL
时才能使用if (NULL != q)
。
尤其是使用指针访问数组时,一定要检查是否越界。
假设我们已经对指针p
进行了各种操作,已经不想使用这个指针了,则应该置空p = NULL
。
局部变量的作用域是变量所在的局部范围,如果出了作用域就销毁了。如果一个局部变量在销毁之后,仍然有指针指向它,这个指针就是野指针。我们应避免出现这种情况。
使用一个指针之前,要检查其是否为空,非空才可使用。我们应避免对空指针解引用。
一个int*
指针±n
,会向后(前)跳n*sizeof(int)
字节。其他类型的指针同理。
假设p
指针是int*
类型的,则p+5
就会向后跳5
个int
,即向后跳5*sizeof(int)=20
字节。
相同类型且指向同一块空间的指针可以相减。指针-指针的绝对值是指针和指针之间元素的个数。
假设创建一个数组int arr[10] = {0};
,则&arr[9]-&arr[0]
计算的是两个指针之间元素的个数,由于arr[9]
和arr[0]
之间差9
个int
,所以相减的结果是9
。
当然,如果反过来,&arr[0]-&arr[9]
得到的结果就是-9
,因为随着数组下标的增长,地址是由低到高变化的。&arr[0]-&arr[9]
是低地址-高地址,得到的结果是负数。
我们可以使用指针-指针求字符串的长度。比如对于字符串"abcdef"
,内存空间实际存储的是[a b c d e f \0]
,那么\0
的地址减a
的地址就是中间字符的个数,也就是字符串的长度。
#include
int my_strlen(char* str)
{
char* start = str;
// 找\0
while (*str)
{
++str;
}
return str - start;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("len = %d\n", len);
return 0;
}
两个指针是可以比较大小的。
举个例子:随着数组下标的增长,地址是由低到高变化的。创建一个数组int arr[10] = {0};
那么就有:&arr[0]<&arr[1]
,&arr[9]>&arr[8]
等等。由于数组名是数组首元素的地址,所以有arr==&arr[0]
。
有了以上三种指针之间的运算,我们来分析下面的代码。
#define N_VALUES 5
int main()
{
float values[N_VALUES];
float *vp;
// 写法1
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
}
return 0;
}
这段代码使用指针vp
来遍历数组values
。vp
被初始化为首元素地址,把改地址对应的元素置成0后,访问下一个位置。当vp
指向values[N_VALUES]
(即数组最后一个元素的下一个位置)时就越界了,不再继续访问,跳出循环。
注意!有朋友可能会认为这段代码有问题,因为访问了values[N_VALUES]
,似乎越界了。事实上,这段代码是没有问题的,因为虽然越界了,但是没有修改该处的值。如果我们把values[N_VALUES]
的值修改了,那vp
就是野指针了,造成了非法访问。
如果我们想用指针从后往前遍历数组,就可以这么写:
// 写法2
for (vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
一开始指针vp
指向了最后一个元素的下一个位置,进入循环后立刻自减,访问最后一个元素,以此类推。最后一次进入循环后,自减后指向values[0]
,不再满足vp > &values[0]
,跳出循环。
但是有朋友可能会认为这么写有点别扭,于是对这段代码简化如下:
// 写法3
for (vp = &values[N_VALUES-1]; vp >= &values[0]; vp--)
{
*vp = 0;
}
一开始让vp
指向values[N_VALUES-1]
(最后一个元素),访问后再往前走,直到vp
指向values[0]
后,把values[0]
置成0,再往前走,指向values[-1]
(第一个元素的前一个位置),不满足vp >= &values[0]
后跳出循环。
这种写法,实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
在写法3中,最后一次执行循环的判断部分时,是拿&values[-1]
(第一个元素的前一个位置的地址)与&values[0]
比较,是不被允许的。
对于数组的详细讲解,请阅读【C语言】数组。
先来复习一个问题:数组名是什么?
数组名表示数组首元素的地址,但是有两个例外。
sizeof(数组名)
,数组名表示整个数组,计算的是整个数组的大小,单位是字节。&数组名
,数组名表示整个数组,取出的是整个数组的地址。先创建一个数组int arr[10] = {1,2,3,4,5,6,7,8,9,10};
,计算数组元素个数:int sz = sizeof(arr) / sizeof(arr[0]);
。
我们用指针来访问数组,首先需要一个指针p
。数组名arr
表示首元素的地址(类型是int*
),我们就用这个地址来初始化指针p
:int* p = arr;
。又因为数组在内存中是连续存放的,我们有了数组首元素的地址,就能够找到后面所有元素的地址。
我们可以在for
循环内,用循环变量i
产生0~sz-1
的数。那么p+i
就跳过了i
个int
类型的数据,就指向了数组中下标为i
的元素。再对其解引用,*(p+i)
就能访问数组中下标为i
的元素了。
#include
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
int i = 0;
for (; i < sz; ++i)
{
printf("%d ", *(p + i));
}
return 0;
}
当我们有一个变量int a = 10;
时,我们取出它的地址并存放在一个指针变量中int *pa = &a;
,此时pa
是一个一级指针。pa
也是一个变量,也有地址,我们取出pa
的地址,存放在另一个指针中int** ppa = &pa;
,此时ppa
就是一个二级指针,类型是int**
。
我们如何理解int**
类型呢?可以拆分成int*
*
。后面这个单独的*
表示ppa
是一个指针变量,前面的int*
表示ppa
指向的对象(即pa
)是int*
类型的。
同理,我们还可以取出ppa
的地址,存放在一个三级指针里:int*** pppa = &pa;
。这样就能无限套娃了。
我们对二级指针ppa
解引用,由于ppa
存放的是pa
的地址,我们就能访问pa
了。比如*ppa = NULL;
就等价于pa = NULL;
。
如果写int arr[5];
,则arr
是存放整型的数组,简称整型数组;如果写char ch[6];
,则ch
是存放字符的数组,简称字符数组。
那什么是指针数组呢?就是存放指针的数组。
比如写:int* arr[10];
,arr
就是一个整型指针数组,有10
个元素,每个元素是int*
类型的。
我们可以用一个指针数组来模拟二维数组。
假设有三个整型数组,分别是data1
,data2
和data3
,由于数组名表示首元素地址,所以data1
,data2
和data3
就分别表示对应的数组首元素地址,我们把它们都存放在一个数组指针arr
里。那么,arr[i]
就可以访问到数组data1
,data2
和data3
,arr[i][j]
就可以访问到data1
,data2
和data3
的元素。
#include
int main()
{
int data1[] = { 1,2,3,4,5 };
int data2[] = { 2,3,4,5,6 };
int data3[] = { 3,4,5,6,7 };
int* arr[] = { data1, data2, data3 };
int i = 0;
for (; i < sizeof(arr) / sizeof(arr[0]); ++i)
{
int j = 0;
for (; j < sizeof(data1) / sizeof(data1[0]); ++j)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
对比一下二维数组的访问,是不是非常像?
#include
int main()
{
int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7} };
int i = 0;
for (; i < sizeof(arr) / sizeof(arr[0]); ++i)
{
int j = 0;
for (; j < sizeof(arr[0]) / sizeof(arr[0][0]); ++j)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
假设我们有一个整数,int a = 10;
由于a
是个变量,可以直接修改a = 20;
,如果我们不想修改这个变量,可以加const
来修饰,如const int a = 10;
,此时如果强行修改a
,如a = 20;
就会报编译错误。对于const
修饰的变量a
,我们称为常变量。
此时a
真的不能被修改了吗?也不见得。我们再用一个指针来存储它的地址:int *pa = &a;
,接着对它解引用来间接地修改:*pa = 20;
这么写就强行把常变量a
给修改了。
如果我们想用一个指针来存储变量a
的地址,又不想通过解引用指针的方式来修改变量a
,就可以使用const
修饰指针。正确的写法是const int *pa = &a;
或者int const *pa = &a
。这两种写法const
都放在*
左边,修饰的是*pa
,就不能通过解引用pa
来修改a
了,但是仍然可以改变pa
的值。比如:int b = 20; pa = &b;
这样pa
就指向b
了。
如果把const
放在*
右边,即int* const pa = &a;
,那么const
修饰的就是pa
,此时pa
的值就不能修改了,比如不能写:int b = 20; pa = &b;
,但是我们可以通过解引用pa
的方式来修改a
,如:*pa = 100;
,就把a
改成了100
。
如果在*
的左右两边都加上const
,如:const int* const pa = &a;
,那么我们既不能修改pa
的值,也不能通过解引用pa
的方式来修改a
。既不能写int b = 20; pa = &b;
,也不能写*pa = 100;
。
char*
类型的指针可以存储一个字符的地址。根据我们所学的知识,我们已经可以看懂下面的代码。
char ch = 'w';
char* pch = &ch;
*pch = 'e';
字符指针还有一个更加常见的用法。当我们直接写出一个常量字符串,比如"abcdef"
时,它的值是这个字符串的首字符(即'a'
)的地址。如果我们想存储这个地址,就需要用到字符指针。如:char* p = "abcdef";
。此时指针p
就指向了字符'a'
,相当于指向了字符串"abcdef"
。
当我们用%s
的格式打印字符串时,只需要字符串的起始地址,即字符串首字符的地址,程序就会从这个起始地址指向的字符开始,一直向后打印字符,直到遇到\0
停止打印。如:当字符指针p
指向了字符串"abcdef"
的首字符(即a
),我们打印字符串就写printf("%s\n", p);
。
对于char* p = "abcdef";
,我们把一个常量字符串首字符的地址存储在一个字符指针中,由于常量字符串时不能修改的,如果写*p = 'w';
,强行修改常量字符串,程序就会崩溃。为了防止这种危险的行为,我们一般会使用const
来修饰这个指针,即const char *p = "abcdef";
。
下面代码输出的结果是什么呢?
#include
int main()
{
const char* p1 = "abcdef";
const char* p2 = "abcdef";
if (p1 == p2)
{
printf("p1 == p2\n");
}
else
{
printf("p1 != p2\n");
}
char arr1[] = "abcdef";
char arr2[] = "abcdef";
if (arr1 == arr2)
{
printf("arr1 == arr2\n");
}
else
{
printf("arr1 != arr2\n");
}
return 0;
}
由于p1
和p2
指向的都是常量字符串"abcdef"
,这个常量字符串是不能修改的,所以没必要存在两份,只需保存一份就行了,p1
和p2
指向的是内存中同一块空间,这块空间存放"abcdef"
这个字符串。
反观arr1
和arr2
,是两个数组,必然是两块不同的空间,数组名表示首元素地址,所以arr1
和arr2
不相等。
数组指针,即存放数组地址的指针。
我们直接对数组名取地址,取出的是数组的地址。如:创建数组int arr[10] = {0};
,对数组取地址&arr;
即为数组的地址。
如果我们想把数组的地址存起来,就需要数组指针。对于上面的例子,正确的写法:int (*p)[10] = &arr;
。括号里的*p
,p
先和*
结合,说明p
是一个指针。向外一看,看到了[10]
,这里的方括号说明p
是一个数组指针,指向了一个数组,方括号里的10
说明p
指向的数组有10
个元素。再往前一看,看到了int
,说明数组元素的类型是int
。
对于int (*p)[10]
,我们把指针变量的名字p
去掉,就能得到数组指针类型是int (*)[10]
。这个类型中,括号里的*
表示这是一个指针类型,向外一看[10]
,方括号表示这个类型创建的指针变量可以存放一个数组的地址,数组有10
个元素。再向前一看,这个int
表示数组的元素类型是int
。
由于指针类型决定了指针+1
跳过几个字节,数组指针类型+1
跳过整个数组。比如int (*)[10]
类型,+1
跳过整个数组,即跳过10
个int
,即40
个字节。
我们可以用数组指针来访问数组的元素。先举个一维数组的例子。
假设有个一维数组int arr[10] = {1,2,3,4,5,6,7,8,9,10};
,我们来写一个函数,打印出这个数组的元素。假设用数组指针的方式,那应该这样调用这个函数:print(&arr, sizeof(arr)/sizeof(arr[0]));
。接下来实现print
函数。
我们需要一个数组指针来接收数组的地址int (*parr)[10]
。对parr
解引用,即*parr
就能找到整个数组,相当于数组名,而数组名表示首元素地址。根据以上分析,*parr
就相当于&arr[0]
,对首元素地址+i
就能找到下标为i
的元素的地址,再对其解引用就能找到下标为i
的元素。
void print(int (*parr)[10], int sz)
{
int i = 0;
for (; i<sz; ++i)
{
printf("%d ", *((*parr)+i));
}
printf("\n");
}
接下来我们用数组指针访问二维数组。
假设有一个二维数组int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7}};
,接下来我们要写一个函数打印这个数组,这个函数应这么调用:print(arr, sizeof(arr)/sizeof(arr[0]), sizeof(arr[0])/sizeof(arr[0][0]));
。其中,sizeof(arr)/sizeof(arr[0])
(数组的总大小/数组第一行的大小
)是数组的行数,sizeof(arr[0])/sizeof(arr[0][0])
(数组第一行的大小/数组第一行第一个元素的大小
)是数组的列数。
arr
是数组名,表示数组首元素的地址。二维数组的首元素就是它的第一行,而第一行是5
个int
的一维数组。所以arr
是5
个int
的一维数组的地址,用数组指针parr
来接收,加上类型应该这样写:int (*parr)[5]
。
那如何使用parr
来访问arr
呢?parr
是数组第一行的地址,parr+i
就跳过了i
行,即parr+i
是数组第i
行的地址。对其解引用,即*(parr+i)
就能找到第i
行,相当于第i
行的数组名,就是第i
行首元素的地址。*(parr+i)+j
就是第i
行首元素的地址跳过j
个元素,即*(parr+i)+j
是第i
行第j
个元素的地址。再对其解引用*(*(parr+i)+j)
就能访问第i
行第j
个元素。
void print(int (*parr)[5], int r, int c)
{
int i = 0;
for (; i<r; ++i)
{
int j = 0;
for (; j<c; ++j)
{
printf("%d ", *(*(parr+i)+j));
}
printf("\n");
}
}
假设我们有个一维数组int arr[10];
,我们想调用一个test
函数,把数组名arr
传过去,即test(arr);
,试问,test
函数应该用什么类型的形参来接收呢?
数组传参,可以数组接收,所以可以写成void test(int arr[10]) {}
。又因为,使用数组名传参时,实际传递的是数组首元素的地址,并不会再函数内部创建一个新的数组,所以可以省略数组的大小,即void test(int arr[]) {}
。甚至可以乱写数组的大小,但是不建议,比如void test(int arr[100]) {}
。
由于使用数组名传参时,实际传递的是首元素的地址,我们可以直接使用指针来接收void test(int* p) {}
。
除了上面提到的写法,其余写法都是错误的。
假设我们有个二维数组int arr[3][5]
,我们想调用一个test
函数,把数组名arr
传过去,即test(arr);
,试问,test
函数应该用什么类型的形参来接收呢?
数组传参,可以数组接收,所以可以写成void test(int arr[3][5]) {}
。对于二维数组,行可以省略,列不能省略,所以也可以写成void test(int arr[][5]) {}
。对于省略的行,也可以乱写,但是不建议,比如void test(int arr[100][5]) {}
。
由于使用数组名传参时,实际传递的是首元素的地址,二维数组的首元素就是第一行的地址,我们可以直接使用数组指针来接收void test(int (*p)[5]) {}
。
除了上面提到的写法,其余写法都是错误的。
假设我们有一个函数,新参是一个一级指针void test(int* ptr) {}
,试问,实参部分可以怎么写呢?
可以直接传一个一级指针过去。如int a = 10; test(&a);
,或int* p = &a; test(p);
当然也可以传一个数组过去,由于数组名表示首元素地址,只需要首元素是int
类型就行了,如int arr[10]; test(arr);
。
假设我们有一个函数,新参是一个二级指针void test(int** ptr) {}
,试问,实参部分可以怎么写呢?
可以直接传一个二级指针过去。如
int a = 10;
int* pa = &a;
int** ppa = &pa;
test(ppa);
当然,对于以上代码,也可以直接传递一级指针的地址,即test(&pa)
。
除此之外,还可以传一个数组过去。数组名表示首元素的地址,只需要首元素是int*
类型,首元素地址就是int**
类型。所以我们需要一个指针数组int* arr[10];
,然后传过去就行了test(arr);
。
假设我们有一个函数Add
:
int Add(int x, int y)
{
return x + y;
}
我们如何拿到函数的地址呢?只需要对函数名取地址即可。&Add
就是函数的地址。除此之外,函数名也表示函数的地址,也就是说,直接写出函数名Add
也表示函数的地址。
如果我们想把函数的地址存起来,就需要函数指针变量。假设我们用变量pf
来存放Add
的地址,应该如何书写它的类型呢?首先,我们要确保pf
是个指针,就用括号把这玩意和*
括起来,即(*pf)
,接着向外一看,这是一个函数指针,就需要一个圆括号(对比数组指针的方括号):(*pf)()
,圆括号内写函数的形参:(*pf)(int, int)
。再往前一看,是函数的返回类型int
:int (*pf)(int, int)
。所以完整的写法是:int (*pf)(int, int) = Add;
。
对于函数指针,如果去掉变量名,剩下的就是函数指针类型,如以上的pf
的类型就是int (*)(int, int)
。
对函数指针解引用,就可以找到对应的函数。如上面的例子中,写*pf
就可以调用这个函数了,即int ret = (*pf)(2, 3);
,此时ret
就是5
。需要注意的是,使用函数指针调用函数,是可以省略*
的,也就是说,直接写int ret = pf(2, 3);
也可以调用Add
函数。事实上,这个*
就是摆设,你甚至可以写很多个*
,比如int ret = (********pf)(2, 3);
,当然这是开个玩笑,建议别这么写。
函数指针数组,就是存放函数指针的数组。比如,假设我们有几个函数,它们的参数和返回类型都是一样的。
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;
}
函数名表示函数的地址,要想把这些函数的地址都存起来,就需要一个函数指针数组。这个函数指针数组的类型应该怎么写呢?假设数组名是pf
,由于是一个数组,就要先和方括号结合,即pf[4]
,方括号里的4
表示数组有4
个元素,当然我们如果要对这个数组初始化,就可以省略数组元素个数,即pf[]
。接下来写数组的元素类型,由于数组的元素是函数指针,所以先与*
结合,说明它是个指针(*pf[])
。向外一看,是个函数指针,所以需要圆括号,圆括号里写函数的形参类型(*pf[])(int, int)
。再向前一看,是函数的返回类型int (*pf[])(int, int)
。
访问这个数组的元素也很简单,使用for
循环访问就行了。由于访问的元素都是函数指针,直接在后面加圆括号就可以调用对应的函数。
#include
int main()
{
int (*pf[])(int, int) = { Add, Sub, Mul, Div };
int sz = sizeof(pf) / sizeof(pf[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
int ret = pf[i](8, 2);
printf("%d\n", ret);
}
return 0;
}
当然,我们可以把上面的代码改造成一个计算器程序,此时函数指针数组就被称作转移表。
#include
// 使用函数指针数组实现计算器
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 menu()
{
printf("******************************\n");
printf("***** 1. add 2. sub *****\n");
printf("***** 3. mul 4. div *****\n");
printf("***** 0. exit *****\n");
printf("******************************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
// 转移表
int (*pfArr[])(int, int) = { 0, Add, Sub, Mul, Div };
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
break;
}
else
{
printf("选择错误,重新选择\n");
}
} while (input);
return 0;
}
假设还是上面的4
个函数,Add
、Sub
、Mul
和Div
。我们把这4
个函数的地址存起来,可以用函数指针数组int (*pfArr[])(int, int) = { Add, Sub, Mul, Div };
。如果再取出这个函数指针数组的地址,即&pfArr
,就需要存放到指向函数指针数组的指针。
假设这个指向函数指针数组的指针变量名是p
,那么应该如何写它的类型呢?首先,它是个指针,所以用括号把它和*
括起来(*p)
。向外一看,它指向一个数组,所以需要一个方括号,里面放数组的元素个数(*p)[4]
。每个元素的类型是什么呢?是函数指针类型,即int (*)(int, int)
,我们假设这个函数指针类型创建一个变量,名字叫pf
,即int (*pf)(int, int)
,再把其中的pf
替换成前面写出来的(*p)[4]
就得到了int (*(*p)[4])(int, int)
。完整的写法是int (*(*p)[4])(int, int) = &pfArr;
。
如何使用这个指向函数指针的数组来访问前面的4
个函数呢?对数组指针解引用,相当于数组名,由数组名可以访问这个数组的元素,而这个数组的元素都是函数指针,就可以调用这些函数。
再详细一点,p
是指向函数指针数组pfArr
的指针,那么*p
就相当于数组名pfArr
,而(*p)[i]
就相当于pfArr[i]
,即函数指针数组的元素,再用这些函数指针调用函数即可。
#include
int main()
{
int (*pfArr[])(int, int) = { Add, Sub, Mul, Div };
int (*(*p)[4])(int, int) = &pfArr; // p是指向函数指针数组的指针
int i = 0;
for (i = 0; i < 4; i++)
{
int ret = (*p)[i](8, 2);
printf("ret = %d\n", ret);
}
return 0;
}
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用,用于对该事件或条件进行响应。
如以下的test
函数就是回调函数。
#include
// 回调函数
void test()
{
printf("hehe\n");
}
void print_hehe(void (*p)())
{
if (1)
{
p();
}
}
int main()
{
print_hehe(test);
return 0;
}
我们可以使用回调函数实现计算器。
#include
// 使用回调函数实现计算器
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 menu()
{
printf("******************************\n");
printf("***** 1. add 2. sub *****\n");
printf("***** 3. mul 4. div *****\n");
printf("***** 0. exit *****\n");
printf("******************************\n");
}
void calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
calc(Add);
break;
case 2:
calc(Sub);
break;
case 3:
calc(Mul);
break;
case 4:
calc(Div);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
接下来我们来研究以下库函数qsort
,并且自己用实现一个类似的。
qsort
是一个库函数,是基于快速排序算法的排序函数。以下是该函数的声明:void qsort(void* base, size_t num, size_t width, int (*cmp)(const void* e1, const void* e2));
。
四个参数,从左到右依次是:
void* base
:待排序数据的起始位置。size_t num
:数据的元素个数。size_t width
:一个元素的字节大小。int (*cmp)(const void* e1, const void* e2)
:一个函数指针,指向了比较函数。e1
和e2
分别指向一个元素,假设e1
指向data1
,e2
指向data2
,若data1>data2
,则该比较函数返回值为正数;若data1,则该比较函数返回值为负数;若data1=data2
,则该比较函数返回值为0
。
对于最后一个参数,要求qsort
函数的使用者自定义一个比较函数,调用qsort
函数时,需要把比较函数的地址作为参数传递给qsort
,此时这个比较函数就是回调函数。
我们已经会使用冒泡排序来排序一个整型数组了,代码如下:
void bubble_sort(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int flag = 1; // 假设已经有序
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
flag = 0;
// 交换
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if (flag == 1)
{
// 已经有序了
break;
}
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 10,9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
print_arr(arr, sz);
return 0;
}
接下来我们参考qsort
,把冒泡排序也改造得通用一点。有以下几点需要改进:
4
个,来应对各种情况,简单起见,使用以下参数:void* base, int num, int width, int (*cmp)(const void* e1, const void* e2)
。><=
来比较,而是使用回调函数cmp
。代码如下:
void Swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *(buf1 + i);
*(buf1 + i) = *(buf2 + i);
*(buf2 + i) = tmp;
}
}
void bubble_sort(void* base, int num, int width, int (*cmp)(const void* e1, const void* e2))
{
int i = 0;
for (i = 0; i < num - 1; i++)
{
int flag = 1; // 假设已经有序
int j = 0;
for (j = 0; j < num - 1 - i; j++)
{
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
flag = 0;
// 交换
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
if (flag == 1) // 已经有序了
{
break;
}
}
}
其中Swap
函数还有另一种写法。
void Swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
接下来我们来调用这个改进后的bubble_sort
函数。
排序整型数组:
#include
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 10,9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);
return 0;
}
排序结构体:
#include
#include
typedef struct Stu
{
char name[20]; // 名字
int age; // 年龄
double score; // 成绩
}Stu;
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((Stu*)e1)->age - ((Stu*)e2)->age;
}
int cmp_stu_by_name(const void* e1, const void* e2)
{
return strcmp(((Stu*)e1)->name, ((Stu*)e2)->name);
}
void print_stu_info(Stu arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("arr[%d] name:%s age:%d score:%lf\n", i, arr[i].name, arr[i].age, arr[i].score);
}
}
int main()
{
Stu arr[] = { {"zhangsan", 20, 30.5}, {"lisi", 30, 90.0}, {"wangwu", 25, 70.5} };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
printf("按照年龄排序\n");
print_stu_info(arr, sz);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
printf("按照名字排序\n");
print_stu_info(arr, sz);
return 0;
}
假设有一个数组int a[] = {1,2,3,4};
,试问以下表达式的结果是多少?以下论述大小时,均省略单位(字节)。
sizeof(a)
:数组名直接放到sizeof
内部,表示整个数组,计算的是整个数组的大小,即16
。sizeof(a+0)
:数组名表示首元素地址,+0
相当于没加,还是首元素地址,大小就是4/8
。sizeof(*a)
:数组名表示首元素地址,对其解引用就是首元素,计算的是首元素的大小,即4
。sizeof(a+1)
:数组名表示首元素地址,+1
后跳过一个元素,即第二个元素的地址,大小是4/8
。sizeof(a[1])
:计算的是第二个元素的大小,即4
。sizeof(&a)
:对数组名取地址,取出的是数组的地址,计算数组的地址大小是4/8
。sizeof(*&a)
:取出数组的地址,再解引用,可以拿到整个数组,计算整个数组的大小是16
。sizeof(&a+1)
:&a
取出数组的地址,+1
后跳过整个数组,指向了数组后面,仍然是地址,大小是4/8
。sizeof(&a[0])
:取出第一个元素的地址,大小是4/8
。sizeof(&a[0]+1)
:取出第一个元素的地址,+1
后跳过一个元素,指向了第二个元素,仍然是地址,大小是4/8
。假设我们有一个字符数组char arr[] = {'a', 'b', 'c', 'd', 'e', 'f'};
,试问下面表达式的结果是多少?以下论述大小时,均省略单位(字节)。
sizeof(arr)
:计算数组的大小,即6
。sizeof(arr+0)
:数组首元素的地址,大小是4/8
。sizeof(*arr)
:对首元素地址解引用,得到首元素,大小是1
。sizeof(arr[1])
:计算下标为1
的元素大小,即1
。sizeof(&arr)
:计算数组的地址的大小,即4/8
。sizeof(&arr+1)
:数组的地址+1
后跳过整个数组,仍是地址,大小是4/8
。sizeof(&arr[0]+1)
:首元素地址+1
跳过1
个元素,指向第2
个元素,仍是地址,大小是4/8
。strlen(arr)
:arr
是数组名,表示首元素地址,strlen
会从这个地址开始向后数字符,直到遇到\0
才停止。由于我们不知道数组后面内存空间存放的数据是什么,最终的结果是随机值。strlen(arr+0)
:arr+0
也是首元素地址,同上,结果是随机值。strlen(*arr)
:对首元素地址解引用,得到首元素'a'
,即把'a'
的ASCII码值97
传递给strlen
函数,strlen
会把97
当做地址,向后数字符,直到数到\0
,这会造成内存的非法访问。strlen(arr[1])
:同上,把'b'
的ASCII码值98
传给了strlen
,也会造成内存的非法访问。strlen(&arr)
:取出数组的地址,值和数组首元素地址相同,结果和strlen(arr)
相同。strlen(&arr+1)
:取出数组的地址,+1
后跳过了整个数组,从该位置向后找\0
,结果是随机值。strlen(&arr[0]+1)
:取出首元素地址,+1
后跳过了一个元素,指向了第二个元素,会从该位置向后找\0
,结果是随机值。假设我们有一个字符数组char arr[] = "abcdef";
,试问下面表达式的结果是多少?以下论述大小时,均省略单位(字节)。
sizeof(arr)
:由于字符串结尾默认隐藏一个\0
,故大小是7
。sizeof(arr+0)
:arr+0
是数组首元素的地址,故大小是4/8
。sizeof(*arr)
:*arr
是数组首元素,故大小是1
。sizeof(arr[1])
:arr[1]
是数组的第二个元素,故大小是1
。sizeof(&arr)
:&arr
是数组的地址,故大小是4/8
。sizeof(&arr+1)
:&arr+1
是\0
后面的地址,故大小是4/8
。sizeof(&arr[0]+1)
:&arr[0]+1
是数组第二个元素的地址,故大小是4/8
。strlen(arr)
:从首元素地址开始向后找\0
,结果是6
。strlen(arr+0)
:同上,结果是6
。strlen(*arr)
:把a
的ASCII码值当做地址向后找\0
,造成内存的非法访问。strlen(arr[1])
:把b
的ASCII码值当做地址向后找\0
,造成内存的非法访问。strlen(&arr)
:数组的地址和首元素地址值相同,故结果相同,为6
。strlen(&arr+1)
:数组的地址+1
跳过整个数组,从\0
后面开始向后找\0
,结果是随机值。strlen(&arr[0]+1)
:数组第二个元素向后找\0
,结果是5
。假设我们有一个字符指针char* p = "abcdef";
,试问下面表达式的结果是多少?以下论述大小时,均省略单位(字节)。
sizeof(p)
:计算指针变量的大小为4/8
。sizeof(p+1)
:p
是a
的地址,+1
后跳过一个字符,指向了b
,仍是指针,故大小是4/8
。sizeof(*p)
:即a
,大小是1
。sizeof(p[0])
:转换为*(p+0)
,即*p
,同上,大小是1
。sizeof(&p)
:二级指针,大小是4/8
。sizeof(&p+1)
:跳过了p
,仍是二级指针,大小是4/8
。sizeof(&p[0]+1)
:p[0]
是*(p+0)
,即*p
,即a
,再取地址,是a
的地址,+1
后跳过一个字符,指向了b
,仍是地址,故大小是4/8
。strlen(p)
:从a
的位置向后数字符,直到遇到\0
,结果是6
。strlen(p+1)
:从b
的位置向后数字符,直到遇到\0
,结果是5
。strlen(*p)
:把a
的ASCII码值作为地址传递给strlen
,造成内存的非法访问。strlen(p[0])
:同上,把a
的ASCII码值作为地址传递给strlen
,造成内存的非法访问。strlen(&p)
:从指针p
的位置向后找\0
,结果是随机值。strlen(&p+1)
:p
向后跳过一个char*
的大小,指向p
后面,向后找\0
,结果是随机值。strlen(&p[0]+1)
:从b
的位置向后找\0
,结果是5
。假设我们有一个二维数组int a[3][4] = {0};
,试问下面表达式的结果是多少?以下论述大小时,均省略单位(字节)。
sizeof(a)
:计算整个数组大小,即48
。sizeof(a[0][0])
:计算第一行第一个元素的大小,即4
。sizeof(a[0])
:a[0]
是第一行的数组名,sizeof(a[0])
就是第一行的数组名单独放在sizeof
内部,计算的是第一行的大小,即16
。sizeof(a[0]+1)
:a[0]
是第一行的数组名,表示第一行首元素的地址,+1
后指向了第一行第二个元素,仍是地址,故大小是4/8
。sizeof(*(a[0]+1))
:由上一条分析,*(a[0]+1)
就是第一行第二个元素,故大小是4
。sizeof(a+1)
:a
表示二维数组第一行的地址,a+1
表示二维数组第二行的地址,故大小是4/8
。sizeof(*(a+1))
:由上一条分析,*(a+1)
就能拿到第二行,故大小是第二行的大小,即16
。sizeof(&a[0]+1)
:a[0]
是第一行的数组名,对第一行的数组名取地址就能拿到第一行的地址,再+1
后就是第二行的地址,故大小是4/8
。sizeof(*(&a[0]+1))
:由上一条分析,*(&a[0]+1)
就能拿到第二行,故大小是第二行的大小,即16
。sizeof(*a)
:a
是第一行的地址,*a
就能拿到第一行,故大小是第一行的大小,即16
。sizeof(a[3])
:看似越界了,但也可以计算大小。相当于第四行的数组名(虽然第四行不存在),计算的是第四行的大小,即16
。