七、指针

一、指针前述

指针是一种数据类型,就像int和float,int装整型数据,float装浮点型数据,指针装地址型数据

可以简单的理解指针就是地址。比如一个人买了一间房子,这个房子可以是故宫,可以是中南海,可以是白宫,可以是唐宁街,可以是别墅,猪圈等。而指针式可以理解为房产证,房产证里面写着房子的具体地址。根据房产证的地址,我们就能找到房子了。所以操作指针的目的,就是为了通过房产证里面的地址来找到房子进而来操作房子。直接把房产证中个地址改了,并不会修改房子本身的地址,只是修改了指向的房子而已。

二、基本数据类型指针

2.1 种类

char,  short,  int,  long,  long long,  float,  double

2.2 指针的声明与定义

int *p;

// 形式:类型+*+变量名;
// int 表示p装的地址对应的空间的数据类型
// *表示p是一个指针变量
// p是指针的名字

指针就是装地址的变量,变量就要赋值,即一定要装一块空间的地址,或者说指向一块空间,才能被使用。就像int a;,如果没有被初始化,没有赋值,这东西啥也不能干。指针变量也是同理,不装地址的情况下,啥都不能干,也叫野指针

#include<stdio.h>
int main(void)
{
    int a = 12;
    int b = 24;
    char c = 'a';

    int *p;
    p = &a; // 指向某个地址,就是内容装哪块地址,就指向哪一块空间
    printf("%p, %p, %p\n", p, &a, &p); // 010FF73C, 010FF73C, 010FF718
    p = &b; // 重指向 
    printf("%p, %p, %p\n", p, &b, &p); // 010FF730, 010FF730, 010FF718
   
    p = &c;
    /*
    重指向 警告: 
    Visual Studio: warning C4133: “=”: 从“char *”到“int *”的类型不兼容 
    DevC++: [Error] cannot convert 'char*' to 'int*' in assignment
    所以类型一定要对应上,因为类型会决定指定的读写方式
    */
    printf("%p, %p, %p\n", p, &c, &p); // 010FF727, 010FF727, 010FF718
    
    return 0;
}

2.3 通过指针操作所指向的空间

指针里面存放的是地址,直接操作地址意义不大,就是把房子地址改了,但是本身的房子还在那里。所以指针的核心作用是通过地址来操作地址所指向的数据。

操作数据主要分为对数据的读写操作。

/*
    读数据
    读指针的数据主要分为两种数据。一种是指针指向的数据,一种是指针变量自身的地址。
*/

/* 
    1. 读取指针指向的数据 用 *+地址。这个地址一定是合法地址,
    即我们申请了的地址,如定义的变量、数组等 2. 此时的 * 是一个内存操作符,还有人叫解引用运算符。是获取指针指向的
    数据内容,和定义一个指针类型的 * 是不一样,定义时候属于一种标记。 3. 总结: 一个指针指向一个变量,*这个指针,就是变量本身。
*/ #include<stdio.h> int main(void) { int a = 12; int *p1 = &a; int *p2; p2 = &a; *p2 = 23; // 1. 一种是指针指向的数据 printf("%d\n", *p1); // 12 printf("%d\n", *p2); // 12 printf("%d\n", *(&a)); // 12 // 2. 读取指针变量本身地址 printf("%p\n", &p1); // 000000000062FE38 printf("%d\n", &p1); // 6487608 return 0; }

2.4 类型决定内存操作

2.4.1 指针的基本运算

#include<stdio.h>
int main(void)
{
    int a = 10;
    double b = 9.9;
    char c = '@';
    
    int *pa = &a;
    double *pb = &b;
    char *pc = &c;
    
    // 初始值
    printf("&a=%p, &b=%p, &c=%p\n", &a, &b, &c);// &a=000000000062FE34, &b=000000000062FE28, &c=000000000062FE27
    printf("pa=%p, pb=%p, pc=%p\n", pa, pb, pc);// &a=000000000062FE34, &b=000000000062FE28, &c=000000000062FE27
    
    // 加法运算
    pa++;
    pb++;
    pc++;
    printf("pa=%p, pb=%p, pc=%p\n", pa, pb, pc);// pa=000000000062FE38, pb=000000000062FE30, pc=000000000062FE28
    
    //减法运算
    pa -= 2; pb -= 2; pc -= 2;
    printf("pa=%p, pb=%p, pc=%p\n", pa, pb, pc);// pa=000000000062FE30, pb=000000000062FE20, pc=000000000062FE26
    
    return 0;
}
/*
    1. 从运算结果可以看出:pa、pb、pc 每次加 1,它们的地址分别增加 4、8、1,正好是 
    int、double、char 类型的长度;减 2 时,地址分别减少 8、16、2,正好是 int、double、char 类型长度的 2 倍。 2. 以int类型为例。int占据4个字节,当指针pa指向int类型的时候,int占据4个字节的地址,当pa+1的时候,
    如果内存往后移动1个字节的话,那么pa+1所占据的内存就是 a内存的后面3个字节长度加上 1 个新的空间,
    这样组成的新的内存空间意义不大,不清楚数据具体会成为什么,所以pa+1所指向的就是增加了一个 int类型所占据的长度。 3. 由此我们可以引申出来,如果指针操作的内存大小与所指向的数据的内存大小不一样,那么很有可能会出现问题的。
*/

2.4.2 指针的内存操作

所指向的空间是什么类型,那么*p 就一次操作多大的内存空间

一个小范围的指针可以指向大范围的空间,并且操作不异常,虽然结果不太对

一个大范围的指针不可以指向小范围的空间,否则操作异常,结果更不对

#include<stdio.h>
int main(void)
{
    double a = 123.456;
    int *p = &a; // Dev-C++中编译就会报错,VS中编译警告,可以执行
    printf("%lf, %lf\n", a, *p);
    *p = 789;
    printf("%lf, %lf\n", a, *p);

    char c = 'a';
    printf("%c\n", c);
    p = &c;
    /*
    *p = 'b';  // 运行直接报错了
    printf("%c, %c\n", c, *p);
    */

    return 0;
}
/*
    double类型一次性占据8个字节,int类型一次性操作4个字节。当int类型的指针指向8个字节的double类型时候,
   每次操作double类型的4个字节(一般的字节),重新给double赋值的时候,也只是操作double的4个字节。所以
   从现象看,double类型的前面部分数据是对的,后面是错误的(数据跟大小端存储有关系)。 同理,操作char类型的时候。char类型的字节是1个字节,但是int类型的指针操作了4个字节,超过了char的大小,所以就会直接会报错。
*/

三、 二级指针

3.1 二级指针的定义

 

 二级指针就是指向指针的指针,也就是装地址的地址。 普通变量的地址用一级指针来装。一级指针变量的地址,用二级指针来装,同理二级指针地址用三级指针装

#include<stdio.h>
int main(void)
{
    int a = 12;
    int *p = &a;
    
    // 想要存放p的地址,该怎么处理?
    int *p1 = &p; // warning C4047: “初始化”:“int *”与“int **”的间接级别不同
    
    int *p2 = p; // 直接用p来也是不行的。因为p是变量名,直接用p代表着是值传递,传递的是p的值,也就是 p=&a, 所以等价于 int *p2 = &a;
    
    /*
        因为 a是普通变量, 装普通变量的时候 用 p来存储,又因为是地址,所以用int *来定义,所以  int *p。 
     p为指针变量,要装指针变量,要用 *p3来表示,又因为是装地址 int *,所以就是 int * *p3, 即 int **p3 一个*表示一级指针,指向的是变量的地址 两个*表示的是二级指针,指向的是相应指针变量的地址,如果要取到变量的值,就需要两个*进行取值
*/ int **p3 = &p; return 0; }

3.2 二级指针的使用

#include<stdio.h>
int main(void)
{
    int a = 12;
    int *p = &a;
    int **q = &p; 
    
    printf("a = %d\n",a);     // a = 12
    printf("&a = %p\n",&a);      // &a = 000000000062FE4C
    printf("p = %p\n",p);     // p = 000000000062FE4C
    printf("&p = %p\n",&p);      // &p = 000000000062FE40
    printf("*p = %d\n",*p);      // *p = 12
    printf("q = %p\n",q);      // q = 000000000062FE40
    printf("&q = %d\n",&q);   // &q = 6487608
    printf("*q = %d\n",*q);   // *q = 6487628
    printf("**q = %d\n",**q); // **q = 12
}
// 从上面的例子可以得出如下结论
q = &p;
*q = p = &a;
**q = *p = a;

https://blog.csdn.net/weixin_42616791/article/details/106737245

 

四、一维数组与指针

4.1 指针遍历数组

4.1.1 指针遍历数组方法

为什么普通类型指针可以遍历数组?
  1、数组空间是连续的
  2、地址加减是加一个类型的大小

#include <stdio.h>
int main()
{
    int arr[] = { 99, 15, 100, 888, 252 };
    int len = sizeof(arr) / sizeof(int);  //求数组长度
    int i;
    int *p;
    p = &arr[0];
    
    // 1. 遍历数组场景一 p+1
    printf("%p, %p ,%p, %p, %p\n", arr, arr+1, arr+2, arr+3, arr+4);
    printf("%p, %p, %p, %p, %p\n", p, p+1, p+2, p+3, p+4);
    printf("%p, %p, %p, %p, %p\n", &arr[0], &arr[1], &arr[2], &arr[3], &arr[4]);
    /* 
        上述三个打印中,打印出来的结果是一样的。说明   数组名+1 == 指针+1 == 对应下标数组地址   是一样的,改变的都是一个数的字节大小。
        根据 地址前面加上 *,即 *(&arr[1]) 我们就能获得1号元素的数据,同理 即 *(arr+1), *(p+1)就能获取下标1的数据。加上()是因为*的优先级高于+
    */
    
    
     // 2. 遍历数组场景二 *(p+i)
    for(i=0; i<len; i++)
    {
        printf("%d  ", *(arr+i) );  // 99  15  100  888  252  *(arr+i)等价于arr[i]
    }
    printf("\n");
    for(i=0; i<len; i++)
    {
        printf("%d  ", *(p+i));// 99  15  100  888  252   *(p+i) == *(arr+i) == *(&arr[0]+i)  
    }
    printf("\n");
    /* 
        arr 数组名代表数组的首地址。arr+1代表增加了4个字节的长度,因为数组的元素是连续的, 所以arr+1就是第一个元素的地址。
     *(arr+1)就是这个地址的值了。同样,p存放的是是数组首地址,所以 *(p+1) == *(&arr[0]+1) == *(arr+1)
*/ // 3. 遍历数组场景三 (*p)++ for(i=0; i<len; i++) { printf("%d ", (*p)++); // 99 100 101 102 103 } printf("\n"); /* 使用(*p)++得出的结果是连续的数字。因为第一次循环先计算 *p == 99, 然后 ++,第二次循环将 ++ 的值输出。 */ // 4. 遍历数组场景四 *p++ for(i=0; i<len; i++) { printf("%d ", *p++); // 104 15 100 888 252 *p++ == *(p++) } /* 使用*p++在输出的结果和*(p+i)是一样的,但是运算结束以后,p指向的地址发生了改变,因为p++以后,
     p的值发生了改变,所以指向的地址也发生了改变
*/ // 5. 指针赋值 p = &arr[0]; // 重新初始化,因为步骤4已经改了p最后的指向了 *(p+2) = 13; for(i=0; i<len; i++) { printf("%d ", *(p+i)); // 104 15 13 888 252 } return 0; }

4.1.2 深入理解下标运算

#include <stdio.h>
int main()
{
    int arr[] = { 99, 15, 100, 888, 252 };
    int len = sizeof(arr) / sizeof(int);
    int *p = &arr[0];
    int i;
    for(i=0; i<len; i++)
    {
        printf("%d  ", arr[i]);
        printf("%d  ", p[i]);
         printf("%d  ", *(p+i));
         printf("%d  ", *(i+p));
         printf("%d\n", i[p]);
        /*
             99  99  99  99  99
            15  15  15  15  15
            100  100  100  100  100
            888  888  888  888  888
            252  252  252  252  252
        */
    }
    return 0;
}

/*
    1. 从上面的运算结果中我们看出来 arr[i]与p[i], *(p+i), *(i+p), i[p]的结果是一样的。
    2. arr[i]取值没有什么疑问,是数组的正常下标取值。
    3. 在 4.1.1 的步骤二中,我们可以隐约感觉出来  arr和p有某种相等的关系。其实arr数组名
    代表数组的首地址,p=&arr[0]也是数组的首地址,所以我们就感觉可以这样获取数据 4. arr[i] 本质就是 地址+[偏移量]。推理出 p[i]是指针的下标运算。 准确地说,下标运算
    不是数组专属,只是一个运算符。数组可以用这个运算符是因为它符合了下标运算符的使用条件,地址+[偏移量],指针也是完全可以的 5. *(p+i)的值与 p[i]的值是相等的.执行的原理都是 :地址+偏移量找到目标地址,然后操作目标地址空间(读、写、取地址)。
    但是意义不一样: *(p+i)混合运算表达式,包含三个运算符, p[2]只有一个下标运算符 6. 同理: arr[i] == *(arr+i)。所以在某种程度上 arr==p, 但是 arr是不能被赋值,
    只能参与运算, p既可以参与运算,也可以被赋值,指向新的地址。 7. *(p+i) == p[i] == *(i+p) == i[p] 8. 将 *(p+i) == p[i] == A, 所以 *(A+j)==A[j]==p[i][j] 所以 *(*(p+i) + j) == p[i][j]
*/

4.2 指针数组 (装地址的数组)

4.2.1 指针数组的概念

#include<stdio.h>

int main()
{
    int b = 4,
        c = 3,
        d = 7,
        e = 8,
        f = 1;

    int* a[5] = { &b,&c,&d,&e,&f };
    printf("%d\n",d); // 7
    *a[2] = 13;
    printf("%d\n", d); // 13
    return 0;
}

4.2.2 指针数组的拉链结构

 

//  指针数组的拉链结构(数组中的每一个元素放置的另一个数组的地址
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
    int b[3] = {1,2,3};
    int c[2] = {4,5};
    int d[4] = {6,7,8,9};
    int e[5] = {10,11,12,13,14};
    int f[2] = {15,16};
    
    int* a[5] = {&b[0],c,d,e,f};   // 数组名代表数组的首地址
    
    // 查找 d 的 8    d[2]
    printf("%d\n", a[2][2]);   // a[2] 代表 d的地址 , a[2][2] == d[2] 
    system("pause");
    return 0;
 } 

// 【注意】
// 1.访问格式跟二维数组一样,意义也是一样的
// 2.但是内存结构是不一样的,或者说本质不一样
// 3.每个小数组大小可以不同,但是元素类型必须相同

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
    int arr1[] = { 1, 2, 3, 4, 5 };
    int arr2[] = { 2, 3, 4, 5, 6 };
    int arr3[] = { 3, 4, 5, 6, 7 };
    int * arr[] = { 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));
        }
        printf("\n");
        for (j = 0; j < 5; j++)
        {
            printf("%d  ", arr[i][j]);
        }
        printf("\n");
    }
 
    return 0;
}

 

 

4.3 数组指针(指向数组的指针)

4.3.1 地址与指针的关系

#include<stdio.h>

int main(void)
{
    int a = 15;
    printf("%p, %p\n", &a, &a+1); // 000000000062FE14, 000000000062FE18
    
    int b[5] = {3,4,5,6,7};
    printf("%p, %p\n", &b,&b+1);   // 000000000062FE30, 000000000062FE44
    
    return 0;    
} 
/*
1. int a; int类型的地址 &a+1 的时候增加的是 一个 int 类型的字节的大小。 
2. int b[5]; int 数组类型的 &b+1 的大小根据 &a+1 的理论 应该是加了整个数组的大小20各字节
说明&b这个地址类型是其所表示的数组那么大(字节数)。
3. 存储int类型的地址 我们用 int *即 int指针来装。
同理存储 int[] 类型的地址就叫做数组类型的地址,简称数组地址,用数组指针来装, 即 int []*
*/

4.3.2 数组指针的使用

int a[5];
int (*p)[5] = &a;

/*
(*p): p是一个指针,一定要加这个小括号,否则p就先跟[ ]结合,就是数组了

int [5]: p所指向的类型,数组类型, int [5]与int [7] ,数组元素个数不同,那就是不同类型的数组。一定要记住,不是所有的数组指针都是一样的

合在一起: int [5](*p)  这应该是通常的组合模型,但是C语言不让这样写,得int (*p)[5]这样写
*/
#include<stdio.h>

int main(void)
{    
    int a[5] = {3,4,5,6,7};
    int (*p1)[5] = &a;  // 没问题
//    int (*p2)[6] = &a; //报错 
    
    /*
    此时 *p1 == a;
    *p1[2] == 5 ??? 这样是不对的。p1是数组指针, p1[2]==*(p1+2), p1加上两个数组的大小,此时已经越界了。
    */
    
    // 数组指针
    // *p1 指针    int[5]  组类型 --->   int [5]*p1 ---> int *p1[5] (这样就成了指针数组) --->  int (*p1)[5]   int (*p1)[5]  中括号的优先级高于指针 
    printf("%d\n",(*p1)[0]);  // 3
    return 0;    
} 

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
    char* arr[5];
    char* (*pa)[5] = &arr; // (*pa)的*说明pa是指针, [5]:pa指向的数组是5个元素, char * 代表pa指向的数组元素是 char*类型
    return 0;
}

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
    int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
    //但是我们一般很少这样写代码
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d\n", (*p)[i]);  // *pa == arr
        printf("%d\n", *((*p) + i));
    }
    return 0;
}

 

4.4 指针数组与数组指针的区别

 指针数组数组指针
含义 装地址的数组(指针的数组 ) 指向数组的指针(数组的指针)
表示方式 int *p[5] int (*p)[5]
用处 用的不多 函数参数

五、 二维数组与指针

5.1 二维数组与指针分析

int a[2][3] = {{1,2,3}, {4,5,6}}

二维数组主要有三个地址: &a, &a[0], &a[0][0], 对于(&a[1]) 、(&a[0][1])也是地址,暂不考虑。
1. &a[0][0]: 二维数组第一个小数组的第一个元素的地址。对应的数据类型是 int类型。
所以存储这个数据的地址应该为 int *p = &a[0][0];
2. &a[0]: 二维数组的首元素的地址, 等价于 数组名 a;对应的是二维数组的第一个元素,这个是一个元素是3个的一 维数组,所以对应的指针应该是一维数组的数组指针 int (*p)[3] = &a[0];
2. &a: 根据上面的可以推倒,这个是二维数组的地址, 对应的指针应该是 int(*p)[2][3] = &a;

5.2 利用指针遍历二维数组

#include<stdio.h>

int main(void)
{
    int a[2][3] = {{1,2,3}, {4,5,6}};
    int *p1 = &a[0][0];  // p1指向这个元素地址, *p1就是这个元素本身。取元素的地址  *p1 = a[0][0]
    for(int i=0; i<6; i++) 
    { 
        printf("%d   ",*(p1+i)); 
    }
    printf("\n");
    
    
    int (*p2)[3] = &a[0];  // 取一维数组的地址  *p2 = a[0]; *(p2+i) = p2[i] = a[i]   
    for (int j=0; j<2; j++)
    {
        for(int k = 0; k<3; k++)
        {
            printf("%d   ",(*(p2+j))[k]);  // (*(p2+j))[k] == *(*(p+i) + j) == p[i][j]
             // 参见:4.1.2 深入理解下标运算
            //printf("%d   ",*(*(p2+j) + k)); // 和上面一样   
        }
    }
    printf("\n");
 
    
    int (*p3)[2][3] = &a;  // 取二维数组的地址  *p3 = a
    for (int m=0; m<2; m++)
    {
        for(int n=0; n<3; n++)
        {
               printf("%d   ", (*p3)[m][n]);
          }
    } 
    return 0; 
} 

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

void print1(int arr[3][5], int x, int y)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < x; i++)
    {
        for (j = 0; j < y; j++)
        {
            printf("%d  ", arr[i][j]);
        }
        printf("\n");
    }
}


void print2(int(*parr)[5], int x, int y)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < x; i++)
    {
        for (j = 0; j < y; j++)
        {
            printf("%d  ", *(*(parr + i) + j));
            // parr + i:指向第i行地址 ,*(parr+i)指向该行首元素的地址, *(parr+i)+j指向该行第j列的元素地址
            // *(parr+i) == parr[i] -- > *(*(parr + i) + j) == *(parr+i)[j] == parr[i][j] == arr[i][j]
        }
        printf("\n");
    }
}

int main()
{
    int arr[3][5] = { { 1, 2, 3, 4, 5 }, { 3, 4, 5, 6, 7 }, { 5, 6, 7, 8, 9 } };
    // print1(arr, 3, 5);   // print1(&arr[0][0], 3, 5); 也是一个意思
    print2(arr, 3, 5); // arr 是数组名,就是首元素地址,首元素就是第一行的元素,所以arr是数组指针

    return 0;
}

 

 

int arr1[5];  // arr1是一个5个元素的整形数组

int *arr2[10];// arr2是一个数组,数组中有10个元素,每个元素的类型是 int *类型, arr2是指针数组

int (*arr3)[10];// arr3是一个指针, 该指针指向了一个数组,数组有10个元素,每个元素的类型是 int类型, arr3是一个数组指针

int (*arr4[10])[5];// arr4是一个数组,该数组有10个元素,每个元素是一个数组指针, 该数组指针指向的数组有5个元素,每个元素是 int类型

六、数组参数,指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

6.1 一维数组传参

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

void test1(int arr[])       // true  传数组可以直接用数组接收
{}
void test1(int arr[10])     // true  传数组可以直接用数组接收
{}
void test1(int *arr)        // true  数组名代表数组首元素地址,用地址接收没问题
{}
void test2(int *arr[20])   // true  数组指针当参数,可以用数组指针接收
{}
void test2(int *arr[])     // true  数组指针当参数,可以用数组指针接收
{}
void test2(int **arr)      // true  指针数组的首元素地址是一级指针,一级指针的地址传过来,用二级指针接收,
{}
int main()
{
     int arr[10] = {0};
     int *arr2[20] = {0};
     test1(arr);      // 传数组名可以直接用数组,或者首元素地址接收
     test2(arr2);     // 传指针数组可以用数组指针,数组指针首元素地址接收
    return 0;
}

6.2 二维数组传参

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

void test(int arr[3][5])   // true, 二维数组传参,用二维数组接收
{}
void test(int arr[][])     // false, 列必须要有
{}
void test(int arr[][5])    // true, 行可以没有,列一定要有
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。

void test(int *arr)       // false, 二维数组的数组名,表示首元素,也就是第一行的地址
{}
void test(int* arr[5])    // false, 传过来的是地址,接收的参数时数组,数组元素是 int *
{}
void test(int(*arr)[5])   // ture,  二维数组首元素地市,接收第一行元素也就是数组的地址,需要用数组指针
{}
void test(int **arr)      // false, 数组的地址没办法放在二级指针,二级指针存放的是一级指针的地址
{}

int main()
{

    int arr[3][5] = { 0 };
    test(arr);
    return 0;
}

6.3 一级指针传参

#define _CRT_SECURE_NO_WARNINGS 1
#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);  // true
    return 0;
}

6.4 二级指针传参

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

void test(int** ptr)  // 二级指针传参,参数直接用二级指针
{
    printf("num = %d\n", **ptr);
}

int main()
{
    int n = 10;
    int*p = &n;
    int **pp = &p;
    int *arr[10];
    
    test(pp);  // true
    test(&p);  // true
    test(arr); // true
    return 0;
}

七、函数指针

7.1 函数指针表示方式

7.1.1 函数指针的产生

// 数组指针; 指向数组的指针
// 函数指针: 指向函数的指针
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h> 

int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}

int main()
{
    int a = 10;
    int b = 20;
    int ret = Add(a, b);
    printf("rett = %d\n", ret);
    printf("%p\n", Add);  // 函数名和 &函数名是一样的
    printf("%p\n", &Add);
    return 0;
}

// 输出的是两个地址,这两个地址是 test 函数的地址。
// 那我们的函数的地址要想保存起来,怎么保存?

7.1.2 函数指针的使用

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h> 

int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}

int main()
{
    int a = 10;
    int b = 20;
    int ret = Add(a, b);
    printf("rett = %d\n", ret); // ret = 30
    
    int (*pa)(int, int) = Add;   // 存放函数的地址
    // *pa == Add;
    int ret2 = (*pa)(30, 40);
    printf("rett = %d\n", ret2); // ret2 = 70
    
    return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h> 

void Print(char *str)
{
    printf("%s\n", str);
}

int main(void)
{
    void(*p)(char *) = Print;
    (*p)("Hello World");  // Hello World
    return 0;
}
(*(void (*)())0)();  
// void (*)()          : 是一个函数指针类型
// (void (*)())0       : 将0强制转换成函数指针类型 (void (*)()), 0就是一个函数的地址
// *(void (*)())0      : 解引用
// (*(void (*)())0)()  : 调用函数


void (*signal(int , void(*)(int)))(int); 
// void(*)(int)                 : 函数指针作为第二个参数
// signal(int , void(*)(int))   : signal函数名, 参书有两个 (int, void(*)(int))
// void (*)(int)                : 这个是上面一行函数的返回类型, 是一个函数指针类型,说明 signal(int , void(*)(int)) 的返回类型是函数指针类型

// 总结: signal 是一个函数声明, signal函数的参数有两个,第一个是 int, 第二个是函数指针,该函数指针指向的函数的参数时int,返回类型是 void。 signal函数的返回类型也是一个函数指针,该函数指针指向的函数参数时 int, 返回类型是 void

// 简单写如下:
/*
typedef void(* pfun_t)(int);   将函数指针类型 void (*)(int)  重命名为 pfun_t
pfun_t signal(int, pfun_t);    转换之后即为这个样子
*/
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h> 
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}

int main()
{
    int(*pa)(int, int) = Add;   // 存放函数的地址
    printf("%d\n", (pa)(30, 40));    // 70
    printf("%d\n", (*pa)(30, 40));   // 70
    printf("%d\n", (**pa)(30, 40));  // 70
    printf("%d\n", (***pa)(30, 40)); // 70
    // pa是一个函数指针,解引用和不解应用得到最后的结果都是一样, **, ***意义不大,不建议用, 所以对于函数指针来说, *只是一个摆设

    return 0;
}

7.2 函数指针数组

7.2.1 函数指针数组的引入

把函数的地址存到一个数组中,那这个数组就叫函数指针数组
    
int (*parr1[10])();
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h> 

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


int main()
{
    int(*pa)(int, int) = Add; // pa 存放一个函数的函数指针
    // 需要一个数组来存放4个函数的地址,这个就是函数指针数组
    int(*parr[4])(int, int) = { Add, Sub, Mul, Div };  //  parr1 先和 [] 结合,说明 parr1是数组,parr就是一个函数指针数组,
    int i = 0;
    for (i = 0; i < 4; i++)
    {
        printf("%d  ", parr[i](2,3));  // 5  -1  6  0
    }
    
    return 0;
}

7.2.2 函数指针数组的举例使用

// 1. 函数指针数组的指向
// 一个函数声明是 char * my_strcpy(char * dest, const char* src);
// 1). 写一个函数指针pf,能够指向 my_stecpy
// 2). 写一个函数指针数组 pfArr, 能够存放 4 个 my_strcpy 函数的地址

// 1). char * (*pf)(char* , const char* )
// 2). char * (*pfArr[])(char* , const char* )
// 2. 转移表, 计算器
// 2.1) 普通的switch case 方法
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h> 

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("*** 0.exit  1. add ***\n");
    printf("*** 2.sub   3. mul ***\n");
    printf("*** 4.div   **********\n");
    printf("**********************\n");
}
int main()
{
    int x = 0;
    int y = 0;
    int input = 0;
    int ret =  0;
    do
    {
        menu();
        printf("请输入选项:>>>");
        scanf("%d", &input);

        
        switch (input)
        {
        case 0:
            printf("退出\n");
            break;
        case 1:
            printf("请输入两个操作数:>>>");
            scanf("%d%d", &x, &y);
            ret = Add(x, y);
            printf("ret = %d\n", ret);
            break;
        case 2:
            printf("请输入两个操作数:>>>");
            scanf("%d%d", &x, &y);
            ret = Sub(x, y);
            printf("ret = %d\n", ret);
            break;
        case 3:
            printf("请输入两个操作数:>>>");
            scanf("%d%d", &x, &y);
            ret = Mul(x, y);
            printf("ret = %d\n", ret);
            break;
        case 4:
            printf("请输入两个操作数:>>>");
            scanf("%d%d", &x, &y);
            ret = Div(x, y);
            printf("ret = %d\n", ret);
            break;
        default:
            printf("输入错误,请重新输入\n");
            break;
        }
    } while (input);


    return 0;
}

 

// 2.2) 用函数指针数组解决, 使用数组指针数组能大量减少代码的冗余, 函数指针数组又称为转移表
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h> 

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("*** 0.exit  1. add ***\n");
    printf("*** 2.sub   3. mul ***\n");
    printf("*** 4.div   **********\n");
    printf("**********************\n");
}
int main()
{
    int x = 0;
    int y = 0;
    int input = 0;
    int ret =  0;
    do
    {
        menu();
        int(*pfArr[])(int, int) = { 0, Add, Sub, Mul, Div };
        printf("请输入选项:>>>");
        scanf("%d", &input);
        if (input >= 1 && input <= 4)
        {
            printf("请输入两个操作数:>>>");
            scanf("%d%d", &x, &y);
            ret = pfArr[input](x, y);
            printf("ret = %d\n", ret);
        }
        else if (input == 0)
        {
            printf("退出\n");
        }
        else
        {
            printf("请输入合理范围内的数字\n");
        }
    } while (input);
    
    return 0;
}

7.3 指向函数指针数组的指针

// 指向函数指针数组的指针是一个 指针

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h> 

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;
}
int main()
{
    int arr[10] = { 0 };   // 数组
    int(*parr)[10] = &arr;   // 数组指针, 就是将 数组 arr前面加个 *
    int(*pf)(int, int);  // 函数指针
    int(*pArr[5])(int, int) = { 0, Add, Sub, Mul, Div };// pArr 是一个函数指针数组
    
    int(*(*pfArr)[5])(int, int) = &pArr;   
    // pfArr 是一个数组指针,指针指向的数组有4个元素, 
    // 指向的数组的每个元素的类型是函数指针 int (*)(int, int), 相当于在上面一行的 pArr前面加上 *
    return 0;
}

7.4 回调函数

7.4.1 什么是回调函数

        回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

        可以理解为将函数名(函数地址)当成参数传给其他函数,在其他函数中使用这个地址调用这个函数,实现这个函数的功能

7.4.2 回调函数简单示例

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h> 

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 Calc(int(*pf)(int, int))
{
    int x = 0;
    int y = 0;
    int ret = 0;
    printf("请输入两个操作数:>>>");
    scanf("%d%d", &x, &y);
    ret = pf(x, y);
    printf("ret = %d\n", ret);
}

void menu()
{
    printf("**********************\n");
    printf("*** 0.exit  1. add ***\n");
    printf("*** 2.sub   3. mul ***\n");
    printf("*** 4.div   **********\n");
    printf("**********************\n");
}
int main()
{
    int x = 0;
    int y = 0;
    int input = 0;
    int ret = 0;
    do
    {
        menu();
        printf("请输入选项:>>>");
        scanf("%d", &input);


        switch (input)
        {
        case 0:
            printf("退出\n");
            break;
        case 1:
            Calc(Add);
            break;
        case 2:
            Calc(Sub);
            break;
        case 3:
            Calc(Mul);
            break;
        case 4:
            Calc(Div);
            break;
        default:
            printf("输入错误,请重新输入\n");
            break;
        }
    } while (input);
    
    return 0;
}

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h> 

void Print(char *str)
{
    printf("%s\n", str);
}

void test(void(*p)(char *))
{
    printf("test\n");
    p("wangyong");
}
int main()
{
    test(Print);
    return 0;
}

7.4.3 库函数 qsort使用

// qsort --- 库函数  使用的底层是 快速排序   #include<stdlib.h>
// void qsort(void *base, size_t num, size_t width, int (* cmp)(const void *e1,const void *e2));
// void* 参见 空指针内容,代表可以接受任意类型的地址


#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void bubble_sort(int arr[], int sz)
{
    int i = 0;
    int j = 0;
    int tmp = 0;
    for (i = 0; i < sz - 1; i++)  // 趟数
    {
        for (j = 0; j < sz - 1 - i; j++)  // 两个比较元素 的对数
        {
            if (arr[j]>arr[j + 1])
            {
                tmp = arr[j + 1];
                arr[j + 1] = arr[j];
                arr[j] = tmp;
            }
        }
    }
}


struct Stu
{
    char name[20];
    int age;
};

// 比较两个整形元素
int cmp_int(const void* e1, const void* e2)  
{
    // 比较两个整形的值
    // e1, e2都是 void,不能直接 *e1, *e2解引用操作,强制类型转换为 int *类型之后进行比较
    return *(int*)e1 - *(int*)e2;
}

// 排序整型
void sort_int()
{
    int i = 0;
    int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), cmp_int);
    for (i = 0; i < sz; i++)
    {
        printf("%d   ", arr[i]);
    }
    printf("\n");
}

// 比较两个浮点型规则
int cmp_float(const void* e1, const void* e2)
{
    // return *(float*)e1 - *(float*)e2;  // 这里会警告,因为返回的是浮点型,函数应该返回整型
    if (*(float*)e1 == *(float*)e2)
    {
        return 0;
    }
    else if (*(float*)e1 > *(float*)e2)
    {
        return 1;
    }
    else
    {
        return -1;
    }
}


// 排序浮点型
void sort_float()
{
    int i = 0;
    float f[] = { 9.0, 8.0, 7.0, 6.0, 5.0, 4.0 };
    int sz = sizeof(f) / sizeof(f[0]);
    qsort(f, sz, sizeof(f[0]), cmp_float);
    for (i = 0; i < sz; i++)
    {
        printf("%f ", f[i]);
    }
}


// 结构体按照年龄排序规则
int cmp_stu_by_age(const void* e1, const void* e2)
{
    return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

// 结构体按照姓名排序规则
int cmp_stu_by_name(const void* e1, const void* e2)
{
    return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

// 排序结构体
void sort_struct()
{
    struct Stu s[3] = { { "zhangsan", 20 }, { "lisi", 30 }, { "wangwu", 25 } };
    int sz = sizeof(s) / sizeof(s[0]);
    qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
    qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}


int main()
{
    int i = 0;
    int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    // 曾经的冒泡排序, 原本的冒泡排序只能排序整形数字,没办法比较浮点型以及字符等其他的数据,所以可以使用 qsort
    bubble_sort(arr, sz);
    /*for (i = 0; i < sz; i++)
    {
        printf("%d   ", arr[i]);
    }*/

    // void qsort(void *base, size_t num, size_t width, int (* cmp)(const void *e1,const void *e2));
    //int (* cmp)(const void *e1,const void *e2) 当e1<e2的时候,返回<0.e1==e2,返回==0, e1>e2,返回>0的数字
        /*
    qsort:
        第一个参数: 待排序数据的首元素地址
        第二个参数: 待排序数据的元素个数
        第三个参数: 待排序数据的每个元素的大小,单位是字节
        第四个参数: 是函数指针,比较两个元素的所用函数的地址,这个函数使用者自己实现
                    函数指针的两个参数是: 待比较两个元素的地址
    */
    sort_int();
    sort_float();
    sort_struct();
    return 0;
}

 

7.4.4 根据qsort,修改冒泡排序适用所有数据类型

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<string.h>


struct Stu
{
    char name[20];
    int age;
};

// 比较两个整形元素
int cmp_int(const void* e1, const void* e2)  
{
    // 比较两个整形的值
    // e1, e2都是 void,不能直接 *e1, *e2解引用操作,强制类型转换为 int *类型之后进行比较
    return *(int*)e1 - *(int*)e2;
}

// 排序整型
void sort_int()
{
    int i = 0;
    int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), cmp_int);
    for (i = 0; i < sz; i++)
    {
        printf("%d   ", arr[i]);
    }
    printf("\n");
}

// 比较两个浮点型规则
int cmp_float(const void* e1, const void* e2)
{
    // return *(float*)e1 - *(float*)e2;  // 这里会警告,因为返回的是浮点型,函数应该返回整型
    if (*(float*)e1 == *(float*)e2)
    {
        return 0;
    }
    else if (*(float*)e1 > *(float*)e2)
    {
        return 1;
    }
    else
    {
        return -1;
    }
}


// 排序浮点型
void sort_float()
{
    int i = 0;
    float f[] = { 9.0, 8.0, 7.0, 6.0, 5.0, 4.0 };
    int sz = sizeof(f) / sizeof(f[0]);
    qsort(f, sz, sizeof(f[0]), cmp_float);
    for (i = 0; i < sz; i++)
    {
        printf("%f ", f[i]);
    }
}

// 结构体按照年龄排序规则
int cmp_stu_by_age(const void* e1, const void* e2)
{
    return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}


// 结构体按照姓名排序规则
int cmp_stu_by_name(const void* e1, const void* e2)
{
    return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}


// 排序结构体
void sort_struct()
{
    struct Stu s[3] = { { "zhangsan", 20 }, { "lisi", 30 }, { "wangwu", 25 } };
    int sz = sizeof(s) / sizeof(s[0]);
    qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
    qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);

}


void Swap(char * buff1, char* buff2, int width) // 如果没有width参数,char* 每次只能交换一个字节,需要交换整个数据所在的所有字节
{
    int i = 0;
    for (i = 0; i < width; i++)
    {
        char tmp = *buff1;
        *buff1 = *buff2;
        *buff2 = tmp;
        buff1++;
        buff2++;
    }
}

// 模仿 qsort 来修改冒泡排序
// 排序各种类型的冒泡排序,实现bubble_sort2函数的程序员, 他是否知道未来排序的数据类型?
// 如果不知道待比较的两个元素的数据类型
void bubble_sort2(void *base, int sz, int width, int (*cmp)(void *e1, void *e2))
{
    int i = 0;
    int j = 0;
    for (i = 0; i < sz - 1; i++)
    {
        // 每一层比较趟数
        for (j = 0; j < sz - 1 - i; j++)
        {
            // 两个元素的比较
            // cmp 函数接收用 void*来接收低地址, 因为不清楚传过来的具体的数据类型,但是我们在比较两个数的时候
            // 不清楚是具体的 char类型,int类型,float类型, struct类型等的时候,没办法将元素直接类型转换。
            // 此时,我们将每个数据类型都转换成 char*类型,因为函数传过来的时候将 width传过来了, char *之后再加上 width
            // 这样就能跳转到下一个操作数据进行比较
            if (cmp((char *)base+j*width, (char*)base+(j+1)*width) > 0)     
            {
                // 交换
                Swap((char *)base + j*width, (char*)base + (j + 1)*width, width);
            }
        }
    }
}


// 数组使用 bubble_sort2 排序
void array_bubble_sort2()
{
    int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    // 使用bubble_sort2的程序员一定知道自己排序的是什么数据
    // 就应该知道如何比较待排序数据中的元素
    bubble_sort2(arr, sz, sizeof(arr[0]), cmp_int);

    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d   ",arr[i]);
    }
}

// 结构体使用 bubble_sort2 排序
void struct_bubble_sort2()
{
    struct Stu s[3] = { { "zhangsan", 20 }, { "lisi", 30 }, { "wangwu", 25 } };
    int sz = sizeof(s) / sizeof(s[0]);
    // 使用bubble_sort2的程序员一定知道自己排序的是什么数据
    // 就应该知道如何比较待排序数据中的元素
    bubble_sort2(s, sz, sizeof(s[0]), cmp_stu_by_age);
    bubble_sort2(s, sz, sizeof(s[0]), cmp_stu_by_name);
}

int main()
{
    array_bubble_sort2();
    struct_bubble_sort2();
    return 0;
}

 

 

 

 

 

 

八、指针的大小

8.1 占用字节大小的确定

1、32位的系统支持32位的软件 64位系统支持32 64位的软件

2、32位的程序/软件是4字节指针,64位的程序是8字节指针。
所以,32位系统最大支持4字节指针,64位系统最大支持8字节指针

3、程序的位数由什么决定呢?
  由我们的开发环境(编译器)决定

8.2 如何查看占用字节

sizeof(指针变量名字)
64bit程序(8字节)
32bit程序(4字节)

32位的地址,最大就是4字节装,就够了,你给分个5字节,没用。用不上,有一个字节空间浪费,64位同理。 4字节 == 4*8 == 32位; 最大数 == 2^32-1

#include<stdio.h>
int main(void)
{

int *p1;
    short *p2;
    float* p3;
    double* p4;
    int(*p5)[2];
    int(*p6)[2][3];
    printf(" %d, %d, %d, %d, %d, %d\n", sizeof(p1), sizeof(p2), sizeof(p3), sizeof(p4), sizeof(p5), sizeof(p6));
    printf(" %d, %d, %d, %d, %d, %d\n", sizeof(int *), sizeof(short *), sizeof(float *), sizeof(double *), sizeof(int (*)[2]), sizeof(int (*)[2][3]));
    /*
        32位: 4, 4, 4, 4, 4, 4 
        64位: 8, 8, 8, 8, 8, 8
    */
    return 0;
}

九、字符串与指针

C语言中没有专门特定的定义字符串类型,前面的知识我们知道,字符串就是以'\n'结尾的字符数组,字符数组本质上还是数组。数组可以与指针进行整合,所以字符串也是可以与指针进行整合的。

9.1 数组与字符串

#include <stdio.h>
#include <string.h>

int main(){
    char str[] = "https://www.cnblogs.com/wangyong123/articles/13689673.html";
    int len = strlen(str), i; // == int len = strlen(str); int i;   strlen的长度不包括'\0'
    printf("%s\n", str); // %s获取字符串
    for(i=0; i<len; i++){ // for 循环输出字符串
        printf("%c", str[i]);
    }
    printf("\n");
    return 0;
}

9.2 指针与数组

#include <stdio.h>
#include <string.h>

int main(){
    char str[] = "https://www.cnblogs.com/wangyong123/articles/13689673.html";
    char *p = str;  // char *p = &str[0];     
    /*
    char *p = &str; 错误。一个是char指针,一个是数组地址。
    char (*p)[] = &str; 错误。  char (*p)[] 与char (*p)[59]不是一个类型
    char (*p)[59] = &str; 完美 
    */
    int i;
    int len = strlen(str);
    
    // 方式一: %s
    printf("%s\n", str); // "https://www.cnblogs.com/wangyong123/articles/13689673.html"
    printf("\n");
    
    // 方式二: p[i]
    for(i=0; i<len; i++)
    {
        printf("%c",p[i]); // "https://www.cnblogs.com/wangyong123/articles/13689673.html"
    }
    printf("\n");
    
    // 方式三: *(p+i)
    for(i=0; i<len; i++)
    {
        printf("%c",*(p+i)); // "https://www.cnblogs.com/wangyong123/articles/13689673.html"
    }
    printf("\n"); 
    
    return 0;
}

9.3 指针与字符串

除了字符数组以外,c语言还支持另外一种表示字符的方法,就是直接使用一个指针指向字符串
1. char *str = "https://www.cnblogs.com/wangyong123/articles/13689673.html";

2. char *str;
   str =  "https://www.cnblogs.com/wangyong123/articles/13689673.html";
#include <stdio.h>
#include <string.h>

int main(){
    
    char *str =  "https://www.cnblogs.com/wangyong123/articles/13689673.html";
    int i;
    int len =strlen(str);
    
    // 方式一: %s直接输出
    printf("%s\n", str); // "https://www.cnblogs.com/wangyong123/articles/13689673.html"
    printf("\n");
    
    // 方式二: str[i]
    for(i=0; i<len; i++)
    {
        printf("%c", str[i]); //"https://www.cnblogs.com/wangyong123/articles/13689673.html"
    }
    printf("\n");
    
    // 方式三: *(str+i)
    for(i=0; i<len; i++)
    {
        printf("%c", *(str+i)); // "https://www.cnblogs.com/wangyong123/articles/13689673.html"
    }

    return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>


int main()
{
    char arr1[] = "Wangyong";
    char arr2[] = "Wangyong";

    char *p1 = "Wangyong";
    char *p2 = "Wangyong";

    if (arr1 == arr2)
    {
        printf("arr1 == arr2\n");
    }
    else
    {
        printf("arr1 != arr2\n"); // 用相同的常量字符串去初始化
        // 不同的数组的时候就会开辟出不同的内存块。所以arr1和arr2不同
    }

    if (p1 == p2)  // p1 == p2 因为 p1 和 p2 指向的都是常量字符串,常量字符串在内存中不能修改,
        // 所以 p1 与 p2指向相同的地址,所以是 p1 == p2
    {
        printf("p1 == p2\n");
    }
    else
    {
        printf("p1 != p2\n");
    }
      return 0;
}

9.4 字符数组与指针表示字符串区别

字符数组和使用一个指针指向字符串是非常相似的。那么这两种表示字符串的区别是什么?

1. 字符数组和指针字符串的区别是内存中的存储区域不一样,字符数组存储在全局数据区或栈区,指针指向的字符串存储在字符常量区。

  全局区和栈区的字符串(包括其它数据)有读取和写入的权限,而常量区的字符串只有读取权限,没有写入权限。

2. 内存权限不同导致一个明显的结果就是,字符数组在定义后可以读取和修改每个字符,而对于指针字符串来说,一旦被定义

  后就只能读取而不能修改,任何对它的赋值都是错误的。

#include <stdio.h>
int main()
{
    char * str = "Hello World!";
    str = "Wang Yong";  //正确,可以更改指针变量本身的指向
    str[3] = 'N';  //错误,不能修改指向的字符串中的字符,指针字符串指向的数据存储在字符常量区,不能修改。

    return 0;
}

在编程过程中,如果只涉及到对字符串的读取,那么字符数组和字符串常量都可以满足要求;如果有写入(修改)的操作,
那么只能使用字符数组,不能使用字符串常量。

// 获取用户输入的字符串就是一个典型的写入操作,只能使用字符数组,不能使用字符串常量,请看下面的代码:
#include <stdio.h>
int main() {
    char str[30];
    gets(str);// 正确
    printf("%s\n", str); 
    char  *ptr;
    gets(ptr); //错误,指针指向的字符串存储在常量字符区,不能写入,键盘输入是写入(输入)操作。
    printf("%s\n", str);

    return 0;
}

十、空指针与野指针

10.1 空指针 void *

 

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
    int a = -10;
    char c = 'w';
    int *pa = &a;  // 数据类型完美吻合
    char *pc = &a; // 可以存储但是会报警告
    // 有没有一种数据类型可以接受任意类型的地址?
    void *po = &a; // 没有报错,没有警告, 没有具体类型的指针
    // *po = 0;    // 报错, 因为void*类型的指针解引用的时候不知道访问几个字节,所以不能进行解引用操作
    po = &c;       // 没毛病
    return 0;
}

10.2 野指针

10.2.1 什么是野指针

 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

10.2.2 野指针形成原因

10.2.2.1 指针未初始化

#include <stdio.h>
int main()
{ 
     int *p;//局部变量指针未初始化,默认为随机值
    *p = 20;
     return 0; 
}

10.2.2.2 指针越界访问

#include <stdio.h>
int main()
{
    int arr[10] = {0};
    int *p = arr;
    int i = 0;
    for(i=0; i<=11; i++)
    {
        //当指针指向的范围超出数组arr的范围时,p就是野指针
        *(p++) = i;
    }
    return 0; 
}

10.2.3 如何规避野指针

1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放即使置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性

 

#include <stdio.h>
int main()
{
    int *p = NULL;
    int a = 10;
    p = &a;
    if(p != NULL)
    {
        *p = 20;
    }
    return 0; 
}

 

 

十一、* 的四种作用

1. 声明的时候有*,表示指针变量。 int * p;
2. *+地址,表示地址操作符。 *(p+i)
3. 数字*数字,表示乘法。 *2
4. 注释。 /* */

 十二、指针题目练习

12.1 sizeof与strlen的计算

1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3. 除此之外所有的数组名都表示首元素的地址。

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
    // 数组名是数组首地址,但有两处是例外
    // 1. sizeof(数组名)   
    // 2. &数组名  
    // 这两处代表整个数组
    int a[] = { 1, 2, 3, 4 };
    printf("%d\n", sizeof(a));          // 16  sizeof(数组名),计算数组的总大小,单位是字节 -- 16
    printf("%d\n", sizeof(a + 0));      // 4   a是数组名,是首元素地址, a+0 还是首元素地址,地址韩式 4个字节
    printf("%d\n", sizeof(*a));         // 4   a是数组名,是首元素地址, *a == 1就是首元素 是整形, sizeof(*a) == 4
    printf("%d\n", sizeof(a + 1));      // 4   a是数组名,是首元素地址,a + 1和a+0意义是一样的,a+1代表第二个元素的地址
    printf("%d\n", sizeof(a[1]));       // 4   第二个元素大小
    printf("%d\n", sizeof(&a));         // 4   此时 &a取出的是数组的地址,但是数组的地址也是地址,地址的大小就是 4 个字节
    printf("%d\n", sizeof(*&a));        // 16  &a是数组的地址,*&a取数组地址之后又解引用访问数组,sizeof 计算的就是数组的阿晓,相当于 sizeof(a),所以是 16
    printf("%d\n", sizeof(&a + 1));     // 4   &a是数组的地址, &a+1虽然地址跳过整个数组,但是还是地址
    printf("%d\n", sizeof(&a[0]));      // 4   第一个元素的地址
    printf("%d\n", sizeof(&a[0] + 1));  // 4   第二个元素的地址
    return 0;
}

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
    char arr[] = { 'a', 'b', 'c', 'd', 'e', 'f' };
    printf("%d\n", sizeof(arr));          // 6  sizeof(数组名),计算的是数组的总大小  6个字节
    printf("%d\n", sizeof(arr + 0));      // 4  arr是首元素的地址, arr + 0 还是首元素的地址
    printf("%d\n", sizeof(*arr));         // 1  arr是首元素的地址, *arr == 'a' 占一个字节
    printf("%d\n", sizeof(arr[1]));       // 1  arr[1]是第二个字符
    printf("%d\n", sizeof(&arr));         // 4  &arr虽然是数组的地址,但是还是地址
      printf("%d\n", sizeof(&arr + 1));     // 4  &arr + 1 就是跳过整个数组,但是还是地址
    printf("%d\n", sizeof(&arr[0] + 1));  // 4  &arr[0] + 1就是第二个元素的地址
    printf("%d\n", strlen(arr));          // 随机值, 找不到 '\0'
    printf("%d\n", strlen(arr + 0));      // 随机值, arr+0与 arr是一样的
    //printf("%d\n", strlen(*arr));         // 代码有问题  *arr == 'a'
    //printf("%d\n", strlen(arr[1]));       // 代码有问题
    printf("%d\n", strlen(&arr));         // 随机值, &arr 取得是数组的地址,也还是数组的首地址,和 strlen(arr) 和strlen(arr+0)的随机值应该是一样的
    printf("%d\n", strlen(&arr + 1));     // 随机值, 比上面一行的随机值小 6
    printf("%d\n", strlen(&arr[0] + 1));  // 随机值, 比上上面那个随机值小 1
    
    return 0;
}

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
    char arr[] = "abcdef";                  // 自动添加 计算sizeof 添加'\0', strlen计算的时候查找'\0'截止,所以计算的时候不添加
    printf("%d\n", sizeof(arr));            // 7 sizeof(arr) 计算的是数组的大小, 单位是 7 
    printf("%d\n", sizeof(arr + 0));        // 4 arr 代表首元素地址  + 0 还是首元素地址,所以计算的还是地址的大小
    printf("%d\n", sizeof(*arr));            // 1 arr 是首元素地址, *arr == 'a'
    printf("%d\n", sizeof(arr[1]));         // 1 arr[1] == 'b'
    printf("%d\n", sizeof(&arr));           // 4 &arr 虽然是数组的地址,还是还是地址, sizeof(&arr) == 4
    printf("%d\n", sizeof(&arr + 1));       // 4 原理同上
    printf("%d\n", sizeof(&arr[0] + 1));    // 4 &arr[0] + 1 也就是 &arr[1],还是地址
    printf("%d\n", strlen(arr));            // 6 计算 strlen的时候遇到 '\0' 的时候停止, '\0'不计算
    printf("%d\n", strlen(arr + 0));        // 6 arr+0 == arr
    // printf("%d\n", strlen(*arr));           // 代码有问题 *arr == 'a' 在ascii中是 97, 访问内存地址为 97的位置,非法访问,报错
    // printf("%d\n", strlen(arr[1]));         // 代码有问题, 原理同上
    printf("%d\n", strlen(&arr));           // 6 &数组名 代表整个数组  &arr是数组的指针存放在数组指针,  char(*p)[7] = &arr; strlen的参数 是 const char *,强行赋值没关系。
    printf("%d\n", strlen(&arr + 1));       // 随机值, 因为跳过了整个数组,再往后查找,就随机找到 '\0' 才停止
    printf("%d\n", strlen(&arr[0] + 1));    // 5 &arr[0] + 1 == &a[1] 往后数一共5个字符
    
    return 0;
}

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
    char *p = "abcdef";
    printf("%d\n", sizeof(p));               // 4 p是一个地址,计算指针变量的大小
    printf("%d\n", sizeof(p + 1));           // 4 p+1计算的是字符 'b' 的地址
    printf("%d\n", sizeof(*p));              // 1 *p == 'a'
    printf("%d\n", sizeof(p[0]));            // 1 p[0] == *(p + 0) == *p == 'a'
    printf("%d\n", sizeof(&p));              // 4 &p是地址,地址占据4个字节
    printf("%d\n", sizeof(&p + 1));          // 4 还是地址
    printf("%d\n", sizeof(&p[0] + 1));       // 4 &p[0] + 1 == p + 1  == 'b'
    printf("%d\n", strlen(p));               // 6 求 strlen 不算 '\0'
    printf("%d\n", strlen(p + 1));           // 5 p+1从'b'开始往后找'\0'
    // printf("%d\n", strlen(*p));              // 报错
    // printf("%d\n", strlen(p[0]));           // 报错
    printf("%d\n", strlen(&p));              // 随机值
    printf("%d\n", strlen(&p + 1));          // 随机值
    printf("%d\n", strlen(&p[0] + 1));       // 5 &p[0] + 1 == 'b'
    
    return 0;
}

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int main()
{
    int a[3][4] = { 0 };
    printf("%d\n", sizeof(a));                 // 48   3 * 4 * 4 == 48
    printf("%d\n", sizeof(a[0][0]));           // 4
    printf("%d\n", sizeof(a[0]));              // 16  a[0]相当于第一行的作为一维数组的数组名, sizeof(arr[0])把数组名单独放在sizeof内,计算的是第一行的大小
    printf("%d\n", sizeof(a[0] + 1));          // 4   a[0]代表第一行首元素地址,其实就是就是第一行第一个元素的地址,所以 +1以后是第一行第二个元素的地址
    printf("%d\n", sizeof(*(a[0] + 1)));       // 4  *(a[0]+1)是第一行第二个元素的值,是int类型,所以是 4
    printf("%d\n", sizeof(a + 1));             // 4  a是二维数组数组名,没有sizeof(数组名),也没有&(数组名),所以a是首元素地址, 而把二维数组看成一维护数组是时,二维数组的首元素是他的第一行。a+1 == &a[0]+1 == &a[1], 计算的是地址的大小
    printf("%d\n", sizeof(*(a + 1)));          // 16  *(a+1) == *(&a[1]) == a[1] 计算的是二维数组第二行第的大小
    printf("%d\n", sizeof(&a[0] + 1));         // 4   &a[0]+1 == &a[1]  计算的是第二行的地址
    printf("%d\n", sizeof(*(&a[0] + 1)));      // 16  *(&a[0] + 1)) 是解引用第二行的一维数组,计算第二行的大小
    printf("%d\n", sizeof(*a));                // 16  *a == *(&a[0]) a是首元素地址-第一行的地址, *a就是第一行,所以计算的就是第一行一维数组的大小, 
    printf("%d\n", sizeof(a[3]));              // 16  计算sizeof的时候内部的表达式并不会参与计算。 所以 a[3]也就是第四行数组的大小。

    return 0;
}

 

posted on 2021-04-13 19:35  软饭攻城狮  阅读(211)  评论(0编辑  收藏  举报

导航