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;
}

&aryary表示整个数组,因此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;
}

&aryary表示整个数组,因此ptr1指向数组ary的最后一个元素的后面,也就是跳过数组ary

数组名ary表示首元素的地址,也就是ary[0]的地址,*(ary + 1) = ary[1],因此ptr2指向ary[1]

1.4 总结

  1. 除了sizeof(arr)&arr中的数组名arr,其它地方出现的数组名arr,都是数组首元素的地址。
  2. *(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更好理解。

Javaint[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个元素为1int型,所以应该是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;
}

数组名也是变量,数组变量名取地址得到数组指针,一维数组ary1int[4]类型,因此&ary1int[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]类型,因此&nAryint[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个元素的指针常量

某类型变量&运算得到同类型的指针

七、练习

C语言数组与指针练习题

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shlyyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值