C语言指针
前言
本人在学习C语言指针相关内容时,包括指针数组、数组指针、函数指针,总是一头雾水,而且学了忘,忘了学,反复循环。现将本人对这部分内容的理解总结如下文。
如果能够理解下列代码写法的原因,就不需要浪费时间看本文了。
int main(int argc, char* argv[])
{
int ary1[4] = { 1,2,3,4 };
int *p1 = ary1;
int(*p2)[4] = &ary1;
int nAry[2][4] = {
{10,20,30,40},
{60,70,80,90}
};
int(*p3)[4] = nAry;
int (*p4)[2][4] = &nAry;
return 0;
}
(*(void(*)())0)();
void (*signal(int, void(*)(int n)))(int n);
int main(int argc, char* argv[])
{
int(*pfun[2])(int a, int b);
int(**pPfun)(int a, int b) = pfun;
return 0;
}
int main(int argc, char* argv[])
{
int(*pfun[2])(int a, int b);
int(*(*pPfun)[2])(int a, int b) = &pfun;
return 0;
}
一、数组再认识
1.1 一维数组下标运算([])
下标运算的结果得到其元素的引用。
下标运算需要一个操作数为整型,另一个操作数为地址。
int main(int argc, char* argv[])
{
int ary[5] = { 1, 2 };
// &2[ary] = &ary[2];
printf("%p\r\n", &ary[2]); // 0019FED0
printf("%p\r\n", &2[ary]); // 0019FED0
system("pause");
return 0;
}
*(arr + n) = arr[n]
int main(int argc, char* argv[])
{
int ary[5] = { 5,4,3,2,1 };
int* ptr = (int*)(&ary + 1);
printf("%d\n", *(ptr - 1)); // 1
return 0;
}
&ary
中ary
表示整个数组,因此ptr
指向数组ary
的最后一个元素的后面,也就是跳过数组ary
。
1.2 一维数组寻址方式
一维数组的寻址方式:
type ary[M] = …; ary[n] address is:
(int)ary + sizeof(type) * n
int main(int argc, char* argv[])
{
int ary[2] = { 0 };
// 0x400000处的MZ标识
printf("%p\r\n", (void*)ary[(0x400000 - (int)ary) / sizeof(int)]); // 00905A4D
printf("%s\r\n", (char*)&ary[(0x400000 - (int)ary) / sizeof(int)]); // MZ?
system("pause");
return 0;
}
1.3 二维数组
二维数组是特殊的一维数组,数组元素是一维数组的数组
int ary[2][3] = {
{1,2,3}, // ary[0]
{4,5,6} // ary[1]
};
// ary有两个元素,每个元素是int[3]类型
int[3] ary[2] = {
{1,2,3},
{4,5,6}
};
// 两次下标运算,第一次下标运算得到ary[1],为一维数组里面3个元素
// 第二次下标运算从ary[1]的一维数组中取第2个元素得到数据
ary[1][2];
二维数组寻址方式
type ary[N][M] = ...;
int x,y = ...;
ary[x][y] address is:
(int)ary + sizeof(type[M])*x + sizeof(type)*y
==> (int)ary + sizeof(type)*M*x + sizeof(type)*y
==> (int)ary + sizeof(type)*(M*x + y)
ary[0][M*x + y] <==> ary[x][y]
*(arr + n) = arr[n]
int main(int argc, char* argv[])
{
int ary[2][5] = { 10,9,8,7,6,5,4,3,2,1 };
int* ptr1 = (int*)(&ary + 1);
int* ptr2 = (int*)(*(ary + 1));
printf("%d,%d\n", *(ptr1 - 1), *(ptr2 - 1)); // 1 6
return 0;
}
&ary
中ary
表示整个数组,因此ptr1
指向数组ary
的最后一个元素的后面,也就是跳过数组ary
。
数组名ary
表示首元素的地址,也就是ary[0]
的地址,*(ary + 1) = ary[1]
,因此ptr2
指向ary[1]
。
1.4 总结
- 除了
sizeof(arr)
和&arr
中的数组名arr
,其它地方出现的数组名arr
,都是数组首元素的地址。- *(arr + n) = arr[n]
二、指针
指针是个变量,里面存放内存单元的地址。通过指针对内存单元进行间接操作,指针的类型决定了指针向前和向后走一步(指针加减1)有多大,并且指针类型决定了指针的解释方式(对指针解引用操作*)。
总结来说,指针 = 地址 + 解释方式
2.1 int* pa, p;
int main(int argc, char* argv[])
{
int ary[4] = { 1,2,3,4 };
int* pa, p; // pa为指针,p为整型。
pa = ary;
p = ary; // “=”:“int”与“int *”的间接级别不同
return 0;
}
2.2 指针运算
1.指针加整型得到同类型的指针常量
2.对指针作下标运算,得到对指针类型的变量引用
3.同类型指针可以相减,结果为整型常量
type *p = ...;
int n = ...;
// 1.指针加整型得到同类型的指针常量
p + n = (int)p + n*sizeof(type);
// 2.对指针作下标运算,得到对指针类型的变量引用
p[n] = *(type*)((int)p + n*sizeof(type));
type *p1 = ...;
type *p2 = ...;
// 3.同类型指针可以相减,结果为整型常量
p1 - p2 = ((int)p1 - (int)p2) / sizeof(type);
2.3 *p++ 与 *++p
*的优先级与++的优先级相同,优先级都为2,且都是从右到左的结合方向。
*p++: 首先执行p++,但后置++是整条语句执行完以后p再++。然后执行 *p取值。最后p+=1。
等同于*p;p+=1;
void mystrcpy(char* szDst, char* szSrc)
{
while(*szDst++ = *szSrc++);
}
*++p:从右向左的结合方向,++p先执行,执行完以后p更新为p+1,然后再取值。
等同于:p+=1;*p;
2.4 二维指针的作用
二维指针作为指针的指针,还可以用来作为形参,在函数内对该二维指针做间接访问,来修改实参(一维指针&运算),进而达到修改一维指针的目的。
int g_nTest = 0x123;
// 通过参数传出指针,要传入二级指针
// 传递pn的地址保存在ppn参数变量中
void SetPoint(int** ppn)
{
// 发生间接访问 将pn地址的内容修改
*ppn = &g_nTest;
}
int main(int argc, char* argv[])
{
int n = 0x888;
int* pn = &n;
// 修改pn的指向 设置pn保存全局变量的地址
SetPoint(&pn);
system("pause");
return 0;
}
二维指针还有另一个作用就是接收指针数组,详见下文。
三、指针数组
本质上是数组,是存放指针的数组。比如main函数的参数中:
int main(int argc, char* argv[], char* envp[])
{
// 命令行以argc个参数作为结尾标志
for (int i = 0; i < argc; i++)
{
puts(argv[i]);
}
// 环境变量以NULL作为结尾标志
while (*envp != NULL)
{
puts(*envp);
envp++;
}
system("pause");
return 0;
}
指针数组可以用二维指针来接收:
int main(int argc, char* argv[])
{
char** p = argv;
system("pause");
return 0;
}
这里的argv作为数组名,数组名是第0个元素(char * 类型)的指针常量,也就是 char**类型。因此要接收argv需要定义一个二维指针。
指针数组的优缺点:
优点:综合了变长存储和定长存储的优点。有着变长存储的数据大小可变的优点,也有着定长存储的随机访问的优点。排序时只需要交换数组中的指针即可,没有元素的拷贝。变长存储的查找和排序问题得到了解决。
缺点:数据量很大时,插入和删除的开销很大。
四、数组指针
4.1 一维数组的数组指针
4.1.1 数组名的类型
为了说明,我们定义下面两个变量,整型
变量a
,数组
变量ary1
。
int a = 10;
int ary1[4] = { 1,2,3,4 };
如果按照整型变量的定义方式,如果要定义包含4个int
类型数据的数组ary1
,则数组ary1
的定义本应该是int[4] ary1;
,但语法上并不支持这种写法,而是写成了int ary1[4];
。
虽然许多书上在谈数组时一直说数组是非基本数据类型,这话本身没有问题,因为int[4]
不是基本数据类型。但我们总是会把上面的数组ary1
说成整型数组ary1
,这种说法容易被误导数组ary1
是整型的,实则不然,应该说数组ary1的成员是整型的
,或者说包含整型数据的一维数组ary1
。虽然数组不是基本类型,但我们把ary1
说成整型数组类型变量ary1
更好理解。
Java
中int[4] ary1;
与int ary1[4];
这两种写法都支持。C/C++仅支持int ary1[4];
这一种写法,学过编译原理我们可以推测可能C/C++编译器在语法或语义分析中int[4] ary1;
这种写法与其它有冲突。
总之,我想说的是要把数组ary1
理解成与普通变量一样,本质上就是一个变量,该变量的类型为数组类型
,并且是整型数组类型
,即int[4]
。这样不管是一维数组变量,还是二维数组变量都相同。
实际上,数组变量就是数组名,引入了指针以后,数组变量的类型就需要重新说明。
为了方便说明,我们依然使用一维数组ary1
。
int ary1[4] = { 1,2,3,4 };
这里我们直接抛出结论:
数组名的类型是数组第0个元素的指针常量
一维数组的数组名ary1
的类型是数组第0个元素的指针常量,数组第0个元素为1
是int
型,所以应该是int
型的指针常量,即是int* const
类型。
int main(int argc, char* argv[])
{
int ary1[4] = { 1,2,3,4 };
int* const p1 = ary1; // ary1是 int* const 类型
return 0;
}
4.1.2 &数组名的类型
某类型变量&运算得到同类型的指针
整型变量名取地址得到整型指针,即int
型变量&运算得到int类型的指针也就是int*
。
int main(int argc, char* argv[])
{
int a = 10;
int* p = &a; // a为int类型 &a为int*类型
return 0;
}
数组名也是变量,数组变量名取地址得到数组指针,一维数组ary1
是int[4]
类型,因此&ary1
是int[4]*
类型,由于数组指针语法上不支持这种写法,因此应该写成int (*)[4]
,这也便是数组指针。
4.1.3 数组名的值
上面是数组变量类型的问题。下面讨论数组变量值的问题。
对于基本数据类型的变量,变量有内存空间大小,变量在其内存空间中二进制的值就是该变量的值,通过&变量
得到变量的地址。
对于数组变量来说来说,数组变量在编译时就确定其内存空间大小,但数组变量的值不同于变量的值,因为数组中包含很多同类型的元素,实际上数组变量的值是数组在内存空间的地址,也就是数组第0个元素在内存空间的地址,数组首元素的地址。而&数组变量
依然是数组在内存空间的地址。
总之,数组名、&数组名的值都是数组首元素的地址。
4.2 二维数组的数组指针
int nAry[2][4] = {
{10,20,30,40},
{60,70,80,90}
};
// 等价于下面的写法,但下面的写法语法上不支持
int[4] nAry[2] = { // 写法1
{10,20,30,40},
{60,70,80,90}
};
int[2][4] nAry = { // 写法2
{10,20,30,40},
{60,70,80,90}
};
从写法2来看,二维数组nAry
的类型是int[2][4]
。
从写法1来看,二维数组nAry
有两个元素,每个元素是一个int[4]
也就是int类型的数组。
如果要接收数组名:
数组名是第0个元素的指针常量。数组名nAry
的第0个元素nAry[0]
为int[4]
类型,所以nAry是int[4]*
类型。因此可以这样定义变量int[4] *p = nAry;
,同样由于语法不支持,正确写法应该是int (*p)[4] = nAry;
。
如果要接收&数组名:
某类型变量&运算得到同类型的指针。数组是int[2][4]
类型,因此&nAry
是int[2][4]*
类型,语法上不支持这种写法,应该写成int (*p)[2][4] = &nAry
。
4.3 本节总结
int main(int argc, char* argv[])
{
int ary1[4] = { 1,2,3,4 };
int *p1 = ary1;
int(*p2)[4] = &ary1;
int nAry[2][4] = {
{10,20,30,40},
{60,70,80,90}
};
int(*p3)[4] = nAry;
int (*p4)[2][4] = &nAry;
return 0;
}
五、函数指针
函数指针存储函数的地址。函数名就是函数的地址,&函数名的值也是函数的地址。
void test(int n)
{
printf("test:%d\r\n", n);
}
int main(int argc, char* argv[])
{
// 函数名 = &函数名 = 函数地址
printf("%p\r\n", test);
printf("%p\r\n", &test);
return 0;
}
5.1 函数指针定义与调用
void test(int n)
{
printf("test:%d\r\n", n);
}
int main(int argc, char* argv[])
{
// 定义函数指针变量pfun
void (*pfun)(int n);
// 函数指针变量赋值
pfun = test;
pfun = &test;
// 调用函数指针保存的函数
pfun(1);
(*pfun)(100);
return 0;
}
定义的函数指针变量本应该是这种写法void(*)(int n) pfun;
,带有返回值、参数的指针,只不过语法上不支持这种写法。
但可以使用typedef改写上面的定义,typedef可以将函数指针定义为一种类型。
typedef void(*PFUN)(int n);
int main(int argc, char* argv[])
{
PFUN pfun = test;
(*pfun)(100);
return 0;
}
typedef相当于把void(*)(int n)
命名为PFUN
。
看下面的代码:
(*(void(*)())0)();
这段代码的含义是,调用0地址处的代码。首先将0转换为函数函数指针void(*)()
无参无返回值类型,然后调用该函数。代码本质上是一个函数调用。
值得说的是函数指针的两种调用方式中,推荐使用(*pfun)();
这种调用方式,另一种方式pfun()
有些情况下可能不能使用,如上面调用0地址处的代码。
5.2 返回值是函数指针的函数
看下面的代码:
void (*signal(int, void(*)(int n)))(int n);
这段代码的含义是定义一个返回值是void(*)(int n)
类型,参数其中一个为int
型,另一个为void(*)(int n)
类型的函数。其本质上是一个函数定义。
既然上面的代码本质上是函数定义,那么如何写一个该函数的函数指针?
void (*(*signal)(int, void(*)(int n)))(int n);
对于上面定义的函数指针,首先(*signal)
是一个函数指针,void(*)(int n)
,是该函数的返回值也是一个函数指针,该函数的参数是(int, void(*)(int n))
。
使用typedef可以改写代码为:
typedef void(*PFUN)(int n);
int main(int argc, char* argv[])
{
//void (*signal(int, void(*)(int n)))(int n);
PFUN signal(int, PFUN);
return 0;
}
这也说明了,当定义一个返回值是函数指针的函数时,在写法上,需要将函数的参数紧跟在函数名写在后面,如上面的
(int, void(*)(int n))
。将函数名和参数括起来的括号外面才是函数的返回值–函数指针。
5.3 函数指针数组
数组存放相同类型的数据,当该数据类型是函数指针时,就变成了函数指针数组,定义如下:
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int main(int argc, char* argv[])
{
int (*pfun[2])(int a, int b);
pfun[0] = add;
pfun[1] = sub;
return 0;
}
使用typedef进行改写:
typedef int(*PFUN)(int a, int b);
int main(int argc, char* argv[])
{
PFUN pfun[2] = { add, sub };
return 0;
}
两种函数调用方式:
int sum1 = pfun[0](1, 2);
int sum2 = (*pfun[0])(1, 2);
5.4 函数指针数组的数组指针
int (*pfun[2])(int a, int b);
typedef int(*PFUN)(int a, int b);
PFUN pfun[2] = { add, sub };
对于函数指针数组,数组名的类型是数组第0个元素的指针常量,函数指针数组中第0个元素的类型为PFUN
,即int(*)(int a, int b)
。类比一维数组及其函数指针,数组可以用指针接收,于是有下面代码:
int main(int argc, char* argv[])
{
PFUN pfun[2] = { add, sub };
PFUN* pPfun = pfun;
return 0;
}
下面是另一种写法:
int main(int argc, char* argv[])
{
int(*pfun[2])(int a, int b);
int(**pPfun)(int a, int b) = pfun;
return 0;
}
首先函数指针数组中的元素类型是int(*)(int a, int b)
,其指针类型是int(**)(int a, int b)
对于&函数指针数组,某类型变量&运算得到同类型的指针,函数指针数组的类型为PFUN[2]
,也就是int(*[2])(int a, int b);
int main(int argc, char* argv[])
{
PFUN pfun[2] = { add, sub };
PFUN(*ppp2)[2] = &pfun;
return 0;
}
另一种写法:
int main(int argc, char* argv[])
{
int(*pfun[2])(int a, int b);
int(*(*pPfun)[2])(int a, int b) = &pfun;
return 0;
}
首先int(*[2])(int a, int b)
是函数指针数组的类型,其指针int(*(*pPfun)[2])(int a, int b)
。
六、总结
*(arr + n) = arr[n]
数组名的类型是数组第0个元素的指针常量
某类型变量&运算得到同类型的指针