C 语言笔记:指针(下)

一、数组指针

1. 二维数组

二维数组像一个表格,元素按行和列排列。
例:
int a[3][5]; 定义了一个 3 行 5 列的一个二维数组。
可以认为二维数组 a 由 3 个元素组成,其中每个元素都是一个包含 5 个整数的一维数组。
**回顾:**数组的名字是数组的首地址,是第 0 个元素的地址,是个常量,数组名字加 1 指向下个元素。

二维数组 a 中 ,a+1 指向下个元素,即下一个一维数组,即下一行。

2. 数组指针

数组指针是个指针,指向一个数组,+1 跳一个数组,即指向下个数组。
数组指针的作用就是可以保存二维数组的首地址。

3. 数组指针的定义方法

指向的数组的类型(*指针变量名)[指向的数组的元素个数]
int (*p)[5];//定义了一个数组指针变量 p,p 指向的是整型的有 5 个元素的数组

p+1 往下指 5 个整型,跳过一个有 5 个整型元素的数组。

例子:

#include <stdio.h>
// 定义数组指针
void test1()
{
    int a[3][5];
    int (*p)[5];

    printf("a = %p\n", a);
    printf("a + 1 = %p\n", a+1);

    p = a;

    printf("p = %p\n", p);
    printf("p + 1 = %p\n", p + 1);
}
int main()
{
    test1();
    return 0;
}

执行结果:

a = 00000033553ff900
a + 1 = 00000033553ff914
p = 00000033553ff900
p + 1 = 00000033553ff914

例子:数组指针的用法 1

// 数组指针的用法
// 可以将二维数组的首地址传递到另一个函数里面,
// 此时函数的形参就需要定义为数组指针
void fun(int (*p)[5], int x, int y)
{
    p[0][1] = 666;
}

void test2()
{
    int i, j;
    int a[3][5] = {0};
    fun(a, 3, 5);
    for(i=0;i<3;i++)
    {
        for(j=0;j<5;j++)
            printf("%d ", a[i][j]);
        printf("\n");
    }
}
int main()
{
    test2();
    return 0;
}

执行结果:

0 666 0 0 0 
0 0 0 0 0 
0 0 0 0 0 

4. 各种数组指针的定义

一维数组指针,加 1 后指向下个一维数组
int(*p)[5];
配合每行有 5 个 int 型元素的二维数组来用

int a[3][5];
int b[4][5];
int c[5][5];
int d[6][5]; 

p=a;p=b;p=c;p=d; 都是可以的

5. 注意

指针数组:是个数组,有若干个相同类型的指针构成的集合
int *p[10];
数组 p 有 10 个 int *类型的指针变量构成,分别是 p[0] ~p[9]

数组指针:是个指针,指向一个数组,加 1 跳一个数组
int (*p)[10];
P 是个指针,p 是个数组指针,p 加 1 指向下个数组,跳 10 个整型。

指针的指针:\

int **p;//p 是指针的指针
int *q;
p=&q;

6. 数组名字取地址:变成数组指针

a+1 跳一个整型元素,是 a[1]的地址
a 和 a+1 相差一个元素,4 个字节

&a 就变成了一个一维数组指针,是 int(*p)[10]类型的。
(&a) +1 和&a 相差一个数组即 10 个元素即 40 个字节。

例子:

void test3()
{
    int a[10];
    printf("a = %p\n", a);
    printf("a + 1 = %p\n", a+1);

    printf("&a = %p\n", &a);
    printf("&a + 1 = %p\n", &a+1);
}
int main()
{
    test3();
    return 0;
}

执行结果

a = 000000688f7ff5e0
a + 1 = 000000688f7ff5e4
&a = 000000688f7ff5e0
&a + 1 = 000000688f7ff608

7. 数组名字和指针变量的区别

int a[10];
int *p;
p=a;

相同点:
a 是数组的名字,是 a[0]的地址,p=a 即 p 也保存了 a[0] 的地址,即 a 和 p 都指向 a[0],所以在引用数组元素的时候,a 和 p 等价
引用数组元素回顾:
a[2]、*(a+2)、p[2]、*(p+2) 都是对数组 a 中 a[2] 元素的引用。

不同点:
(1) a 是常量、p 是变量
可以用等号 = 给 p 赋值,但是不能用等号给 a 赋值
(2) 对 a 取地址,和对 p 取地址结果不同
因为 a 是数组的名字,所以对 a 取地址结果为数组指针。
p 是个指针变量,所以对 p 取地址(&p)结果为指针的指针。

void test4()
{
    int a[10];
    int *p;
    p = a;

    printf("a = %p\n", a); // a 是一维数组
    printf("&a = %p\n", &a); // &a 是一维数组指针
    printf("&a + 1 = %p\n", &a+1); // &a+1 跳 40 个字节

    printf("p = %p\n", p); // p 是一个数组指针
    printf("&p = %p\n", &p); // &p 是指针的指针
    printf("&p + 1 = %p\n", &p+1); // &p+1 跳一个int*类型的指针,8个字节
}
int main()
{
    test4();
    return 0;
}

8. 多维数组中指针的转换

在二维数组中,行地址取 * 不是取值的意思,而是指针降级的意思,由行地址(数组指针)变成这一行第 0 个元素的地址。取 * 前后还是指向同一个地方,但是指针的类型不一样了。

//二维数组的数组名降级问题
void test5()
{
    int a[3][5];
    printf("a = %p\n", a);// a 表示第0行第0个元素的地址
    printf("a + 1 = %p\n", a+1);// a+1 指向下一行第0个元素,跳过 5×4 个字节

    printf("*a = %p\n", *a);// *a 表示第0行第0个元素的地址
    printf("(*a) + 1 = %p\n", (*a)+1);// (*a)+1 跳过 4 个字节,指向第0行第1个元素
}
int main()
{
    test5();
    return 0;
}

执行结果:

a = 000000b5587ffc50
a + 1 = 000000b5587ffc64
*a = 000000b5587ffc50
(*a) + 1 = 000000b5587ffc54

二维数组的数组名默认是一个行指针,加 1 指向下一行的首地址。
二维数组名取 * ,表示地址的降级,意味着行指针降级为列指针,加 1 保存下一个元素的地址。

一维数组的数组名默认是一个列指针,加 1 保存下一个元素的地址。
一维数组的数组名取 & ,则是地址的升级,将列指针升级为行指针,加 1 保存下一行元素的首地址。

二、指针和函数的关系

可以给函数传一个整型、字符型、浮点型的数据,也可以给函数传一个地址。

函数的传参方式:复制传参、地址传参、全局传参(几乎用不到)

1. 复制传参

将实参的值传给形参,不管形参怎么变化,跟实参都没有关系

#include <stdio.h>
// 函数的传参方式之复制传参
void myfun1(int a, int b)// 这里的 a 和 b 是形参
{
    int temp;
    temp = a;
    a = b;
    b = temp;

    printf("in fun: a = %d, b = %d\n", a, b);
    printf("&a = %p, &b = %p\n", &a, &b);
}

void test1()
{
    int a = 100;
    int b = 20;

    printf("before fun: a = %d, b = %d\n", a, b);
    printf("&a = %p, &b = %p\n", &a, &b);

    myfun1(a, b);// 这里 a 和 b 是实参

    printf("after fun: a = %d, b = %d\n", a, b);// 无论形参怎么变化,实参都不变
    printf("&a = %p, &b = %p\n", &a, &b);
}
int main()
{
    // printf("Hello World!\n");
    test1();
    return 0;
}

执行结果:

before fun: a = 100, b = 20
&a = 0000001c90fffd4c, &b = 0000001c90fffd48
in fun: a = 20, b = 100
&a = 0000001c90fffd20, &b = 0000001c90fffd28
after fun: a = 100, b = 20
&a = 0000001c90fffd4c, &b = 0000001c90fffd48

2. 地址传参

将实参的地址传递给形参,函数体对形参进行变换,实参的值会随之变换。

例:

// 函数的传参方式之地址传参
void myfun2(int *p, int *q)
{
    int temp;
    temp = *p;
    *p = *q;
    *q = temp;

    printf("in fun: *p = %d, *q = %d\n", *p, *q);
    printf("p = %p, q = %p\n", p, q);
}

void test1()
{
    int a = 100;
    int b = 20;

    printf("before fun: a = %d, b = %d\n", a, b);
    printf("&a = %p, &b = %p\n", &a, &b);

    myfun2(&a, &b);// 把 a 和 b 的地址传给形参

    printf("after fun: a = %d, b = %d\n", a, b);// 无论形参怎么变化,实参都不变
    printf("&a = %p, &b = %p\n", &a, &b);
}
int main()
{
    // printf("Hello World!\n");
    test1();
    return 0;
}

执行结果:

before fun: a = 100, b = 20
&a = 0000006a51dff72c, &b = 0000006a51dff728
in fun: *p = 20, *q = 100
p = 0000006a51dff72c, q = 0000006a51dff728
after fun: a = 20, b = 100
&a = 0000006a51dff72c, &b = 0000006a51dff728

例:看似是地址传参,实则复制传参

void test2(char *p)// 形参是一级指针
{
    p = "hello kitty";
    printf("in fun: %s\n", p);
}
int main()
{
    char *p = "hello world";// 实参是一级指针
    test2(p);// 传入的是一级指针,本质上是复制传参,而不是地址传参
    printf("after fun: %s\n", p);
    return 0;
}

执行结果:

in fun: hello kitty
after fun: hello world

把上例改为地址传参,要传给形参一个一级指针的地址,形参应该是一个二级指针

void test2(char **p)// 形参是二级指针
{
    *p = "hello kitty";
    // printf("in fun: %s\n", p);
}
int main()
{
    char *p = "hello world";// 实参是一级指针
    test2(&p);// 传入的是一级指针的地址,是地址传参
    printf("after fun: %s\n", p);
    return 0;
}

总结:要想改变主调函数中变量的值,必须传变量的地址,而且还得通过 *地址 去赋值,无论这个变量是什么类型的。

在地址传参中,如果实参为普通变量,那么形参得是一级指针;如果实参为一级指针,那么形参得是二级指针。以此类推……

3. 传递数组

传递数组理论上都是地址传参。

例:传递一维数组

void fun1(int *p)
{
    printf("%d\n", p[2]);
    printf("%d\n", *(p+3));
}
void test3()
{
    int a[10]={1, 2, 3, 4, 5, 6, 7, 8};
    fun1(a);
}
int main()
{
    test3();
    return 0;
}

执行结果:

3
4

例:传递二维数组

// void fun2(p[][4]) // 形式1
void fun2(int (*p)[4]) // 形式2:通过数组指针
{
    printf("%d\n", p[0][2]);
    printf("%d\n", *(*(p + 1) + 2));
}
void test4()
{
    int a[2][4] = {1, 2, 3, 4,
                   5, 6, 7, 8};
    fun2(a);
}
int main()
{
    test4();
    return 0;
}

执行结果:

3
7

为什么 *(*(p + 1) + 2) 会打印出 7 呢?

(1) (*p)[4] :指针 p 指向的是一个一维数组,这个数组有 4 个整数。
(2) p + 1 :指针算数。将指针向前移动足够的字节,以指向下一个包含4个整数的数组,即第二行。
(3) *(p + 1) :对 p + 1 的解引用操作。它告诉编译器,“给我这个地址所指向的内容”,在这个上下文中,它就是第二行的数组。所以,*(p + 1) 实际上就是 a[1],即第二行的数组。
(4) *(p + 1) + 2:这是在第二行数组上加上 2 ,指向第二行的第三个元素。
(5) *(*(p + 1) + 2):解引用,得到第二行第三列的元素值,即 7 。

4. 指针函数:返回值为指针的函数

一个函数可以返回整型数据、字符数据、浮点型的数据,也可以返回一个指针。

例:

// 指针函数
char *fun3()
{
    char str[100]="hello world"; // 函数内部定义,存储在栈区,随着当前代码段结束而释放
    return str; // 数组名是一个地址常量
}
void test5()
{
    char *p; // 定义一个字符指针
    p = fun3();
    printf("p = %s\n", p);
}
int main()
{
    test5();
    return 0;
}

执行结果:

p = (null)

为什么会出现null呢?

因为 str 是在 *fun3 函数内部定义的,保存在内存的栈区,它会随着当前代码段结束而释放。

那怎么办呢?

char str[100]="hello world"; 改成 static char str[100]="hello world";
静态区的空间不会随着当前代码段的结束而释放。

// 指针函数
char *fun3()
{
    static char str[100]="hello world";
    return str; // 数组名是一个地址常量
}
void test5()
{
    char *p; // 定义一个字符指针
    p = fun3();
    printf("p = %s\n", p);
}
int main()
{
    test5();
    return 0;
}

执行结果:

p = hello world

5. 函数指针:指向函数的指针

在运行程序的时候,会将函数的指令加载到内存的代码段。所以函数也有起始地址。
C 语言规定:函数的名字就是函数的首地址,即函数的入口地址。
定义一个指针变量,去存储函数的地址,这个指针变量就是函数指针。\

(1) 函数指针针变量的定义方法
返回值类型 (*函数指针变量名)(形参列表);
int (*p)(int,int); //定义了一个函数指针变量 p,p 指向的函数必须有一个整型的返回值,且有两个整型参数。
int max(int x,int y)
{
    // 函数体
}
int min(int x,int y)
{
    // 函数体
}

可以用这个 p 存放这类函数的地址。

p=max;
p=min;
(2) 调用函数的方法

a. 通过函数的名字去调函数(最常用的)

int max(int x,int y)
{
    int temp;
    if(x<y)
    {
        temp = y;
    }
    else if(x>y)
    {
        temp = x;
    }
    else
    {
        temp = x;
    }
    printf("max = %d\n", temp);
}
void test6()
{
    max(3,5);
}
int main()
{
    test6();
    return 0;
}

执行结果:

max = 5

b. 通过函数指针变量去调用

int max(int x,int y)
{
    int temp;
    if(x<y)
    {
        temp = y;
    }
    else if(x>y)
    {
        temp = x;
    }
    else
    {
        temp = x;
    }
    printf("max = %d\n", temp);
}
void test6()
{
    int (*p)(int, int);
    p = max;
    p(3, 5);
}
int main()
{
    test6();
    return 0;
}

执行结果:

max = 5
(3) 函数指针数组:每个元素都是函数指针的数组

返回值类型 (*函数指针变量名[函数指针个数])(形参列表)
int(*p[10])(int,int); 定义了一个函数指针数组,有 10 个元素 p[0] ~ p[9],每个元素都是函数指针,指向的函数,必须有整型的返回值,两个整型参数。

(4) 函数指针最常用的地方

回调函数:通过函数指针调用的函数

#include <stdio.h>
int add(int x, int y)
{
    return x + y;
}
int sub(int x, int y)
{
    return x - y;
}
int multi(int x, int y)
{
    return x * y;
}
int div(int x, int y)
{
    return x / y;
}
int operate(int (*p)(int, int), int a, int b)
{
    return p(a, b);
}
int main()
{
    int a, b, c, d;
    a = operate(add, 3, 4);
    b = operate(sub, 8, 12);
    c = operate(multi, 2, 9);
    d = operate(div, 9, 3);
    printf("a, b, c, d = %d, %d, %d, %d\n", a, b, c, d);
    return 0;
}

执行结果:

a, b, c, d = 7, -4, 18, 3

三、经常容易混淆的指针

  1. 指针数组

int *a[10]; 数组 a 中有 10 个整型的指针变量 a[0] ~ a[9]

  1. 数组指针

int (*a)[10]; 指向一个数组,加 1 指向下一个数组。

  1. 指针的指针

int **p; 用来保存指针的地址。

  1. 指针函数:返回值为指针的函数。

  2. 函数指针:指向函数的指针。

四、特殊指针

  1. 空类型的指针(void *)

char * 类型的指针指向 char 型的数据。
int * 类型的指针指向 int 型的数据。
float * 类型的指针指向 float 型的数据。
void * 通用指针,任何类型的指针都可以给 void * 类型的指针变量赋值。

举例子:
有个函数叫 memset

void * memset(void *s,int c,size_t n);

这个函数的功能是将 s 指向的内存前 n 个字节,全部赋值为 c。
Memset 可以设置字符数组、整型数组、浮点型数组的内容,所以第一个参数,就必须是个通用指针。
它的返回值是 s 指向的内存的首地址,可能是不同类型的地址,所以返回值也得是通用指针。

注意:void* 类型的指针变量,也是个指针变量,在 32 位系统下,占 4 个字节。

  1. 空指针 NULL

char *p=NULL;
可以认为 p 哪里都不指向,也可以认为 p 指向内存编号为 0 的存储单位。
在 p 的四个字节中,存放的是 0x00 00 00 00 一般 NULL 用在给指针初始化。

五、main 函数传参

int main(int argc, char *argv[])

argc:是一个 int 类型的变量,表示命令终端传入的参数的个数
argv:是一个指针数组,用于保存每一个命令终端传入的参数。

#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
printf("argc=%d\n",argc);
for(i=0;i<argc;i++)
{
printf("argv[%d]=%s\n",i,argv[i]);
}
return 0;
}

此例暂且不表,后续学习 Linux 时再说。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值