• C primer plus学习笔记 —— 6、数组和指针


    概述

    数组和指针的关系非常紧密,所以这本书将他们放到一章来学习。下面我们一起来看一下。

    数组

    我们通常将只存储一个值的变量叫做标量scalar,将存储多个值的叫做vector

    初始化方式

    int fix = 1; //标量
    int power[8] = {1, 2, 3, 4, 5, 6, 7, 8};//vector
    const int power2[8] = {1, 2, 3, 4, 5, 6, 7, 8};//只读数组,内容不能被修改
    int power[] = {1, 2, 3, 4, 5, 6, 7};//使用初始化列表,可以不用指定元素数量
    int power[6] = {[5]=22}; //部分初始化,其余值默认初始化为0,(C99)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    使用未初始化的数组

    #include 
    #define SIZE 4
    int main(void)
    {
        int no_data[SIZE];  //只声明未初始化的数组
        int i;
        
        printf("%2s%14s\n",
               "i", "no_data[i]");
        for (i = 0; i < SIZE; i++)
            printf("%2d%14d\n", i, no_data[i]); //会打印随机值
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    使用未初始化值的数组,会打印内存上的现有垃圾值。

    使用部分初始化的数组

    #include 
    #define SIZE 4
    int main(void)
    {
        int no_data[SIZE]={3,5};  //初始化部分值
        int i;
        
        printf("%2s%14s\n",
               "i", "no_data[i]");
        for (i = 0; i < SIZE; i++)
            printf("%2d%14d\n", i, no_data[i]); //只有前两个数打印输入,后面会被默认赋为0
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    如果部分初始化,剩余值初始化为0.

    数组赋值

    int no_data[5]={3,5,4,5,6};
    int data[5];
    data = no_data;//不允许
    data[5] = {3,4,5,7,7}; //不起作用,因为已经声明了,不能再用初始化方式赋值
    
    • 1
    • 2
    • 3
    • 4

    数组边界

    编译器不会检查数组下标是否使用得当,所以数组下标越界使用,会导致程序改变其他变量的值,从而导致程序异常终止。
    C为何不检查数组越界?
    在程序运行之前,数组下标值尚未确定,编译器如果需要再运行时添加额外代码检查数组每个下标,就会降低程序运行速度。因此编译器不检查下标越界,而信任程序员。

    多维数组

    float rain[5][10]; //声明5行10列的数组,第一个参数代表元素所在行,第二个代表所在列。
    
    • 1

    数组与指针

    数组表示法其实实在变相的使用指针。

    数组名是数组首元素的地址

    int power[4]; 
    
    • 1

    power&power[0]都表示数组首元素的地址
    两个都是常量,不会改变。但是可以把他赋值给指针变量。

    #include 
    #define SIZE 4
    int main(void)
    {
        short dates [SIZE];
        short * pti;
        short index;
        double bills[SIZE];
        double * ptf;
        
        // 数组首元素地址
        pti = dates;   
        ptf = bills;
        printf("%23s %15s\n", "short", "double");
        //打印poiter+index的地址
        for (index = 0; index < SIZE; index ++)
            printf("pointers + %d: %10p %10p\n",
                   index, pti + index, ptf + index);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述
    上面程序是打印数组中每个元素的地址。
    第一行是打印每个数组开始的地址,下一行是指针+1后的地址。
    可以看到C中指针+1,增加的是一个存储单元,也就是+1后的地址是数组中下一个元素的地址,而不是下一个字节的地址。

    • 比如short数组,指针+1,则增加地址十六进制c到e也就是2字节。因为short占2字节。
    • 比如double数组,指针+1,则增加地址十六进制0到8也就是8字节。因为double占8字节。

    在这里插入图片描述
    dates+2 == &date[2] 元素首元素地址后移两位,也就是第三个元素的地址
    所以可以推导出某个元素下标的值也可以表示为arr[i] == *(arr+i)
    下面看一个区别:

    *(dates + 2) //dates数组中第三个元素的值,他与dates[2]等价
    *dates + 2 //dates数组中第一个元素的值+2
    
    • 1
    • 2

    指针类型

    所以,我们在声明指针时,必须要指定指针的类型。这样指针+1移动的实际上是该指针类型的存储单元。比如int指针,每+1,指针实际移动是4个字节。

    指针的定义

    1. 指针的值是他所指向对象的内存地址。
    2. 指针前面使用*运算符可以得到指针所指向的对象的值。
    3. 指针+1,是他对应存储单元+1.

    函数,数组, 指针

    我们想把一个数组传入函数,如何做?
    可以这样,使用数组形参

    数组名作为形参

    #include 
    #define SIZE 10
    int sum(int ar[], int n);
    int main(void)
    {
        int marbles[SIZE] = {20,10,5,39,4,16,19,26,31,20};
        long answer;
        
        answer = sum(marbles, SIZE);
        printf("The total number of marbles is %ld.\n", answer);
        printf("The size of marbles is %zd bytes.\n",
               sizeof marbles);
        
        return 0;
    }
    
    int sum(int ar[], int n)     // how big an array?
    {
        int i;
        int total = 0;
        
        for( i = 0; i < n; i++)
            total += ar[i];
        printf("The size of ar is %zd bytes.\n", sizeof ar);
        
        return total;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    我们把一个函数

    使用指针作为形参

    #include 
    #
    define SIZE 10
    int sum(int ar[], int n);
    int main(void)
    {
        int marbles[SIZE] = {20,10,5,39,4,16,19,26,31,20};
        long answer;
        
        answer = sum(marbles, SIZE);
        printf("The total number of marbles is %ld.\n", answer);
        printf("The size of marbles is %zd bytes.\n",
               sizeof marbles);
        
        return 0;
    }
    int sum(int *arr, int n)    
    {
        int i;
        int total = 0;
        
        for( i = 0; i < n; i++)
            total += ar[i]; //也写成total += *(ar++);
        printf("The size of ar is %zd bytes.\n", sizeof ar);
        
        return total;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    记住:数组名是该数组首元素的地址
    我们的数组marbles就是数组名,也就是该数组首元素的地址。
    而sum函数要接收的形参是一个int型指针,也就是一个地址。那么我们将该数组首元素地址传入到函数中。这时,arr就表示的marbles这个数组的数组名了,所以在函数中我们直接使用arr[4]

    所以使用函数接收数组的时候下面两种形式声明等价

    int sum(int ar[], int n);
    int sum(int *arr, int n); 
    
    • 1
    • 2

    指针运算符优先级

    *和++ 运算符优先级相同,但是结合律是从左往右

    #include 
    int data[2] = {100, 200};
    int moredata[2] = {300, 400};
    int main(void)
    {
        int * p1, * p2, * p3;
        
        p1 = p2 = data;
        p3 = moredata;
        printf("  *p1 = %d,   *p2 = %d,     *p3 = %d\n",
               *p1     ,   *p2     ,     *p3); // 100 100 300
        printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n",
               *p1++     , *++p2     , (*p3)++); // 100 200 300
        printf("  *p1 = %d,   *p2 = %d,     *p3 = %d\n",
               *p1     ,   *p2     ,     *p3); // 200 200 301
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    指针的使用

    指针的初始化

    指针初始化时,“=”的右操作数必须为内存中数据的地址,不可以是变量。
    指针的初始化主要有两大类方法:
    一是给指针变量初始化一个在内存中已经存在的地址;二是通过指针变量申请一块新的内存并赋初值。

    一是给指针变量初始化一个在内存中已经存在的地址,常用的有以下几种方法:

    1. 变量的地址:
      int a = 10;
      int * p = NULL;
      p = &a;
      这两句等价于 int * p = &a;
    2. 地址常量
      将指针常量赋给一个指针
      如:long p = (long)(unsigned long)0xfffffff0;
    3. 数组的地址
      char ary[100];
      char *cp = ary;
    4. 字符串常量
      char *cp = “abcdefg”;

    对于一个不确定要指向何种类型的指针,在定义它之后最好把它初始化为NULL,并在解引用这个指针时对它进行检验,防止解引用空指针。另外,为程序中任何新创建的变量提供一个合法的初始值是一个好习惯,它可以帮你避免一些不必要的麻烦。

    二是通过指针变量申请一块新的内存即动态内存分配

    1. 采用malloc函数与free函数,这时需要包含
      int *a =(int )malloc(nsizeof(int));

      free (a);此处释放指针
    2. 采用new/malloc与delete/free:
      (1)变量
      int * p = new int(10);表示指针指向的内存中存放的数据是10

      delete p;
      (2)数组
      int *arr = new int[10];表示数组中可以存放10个元素

      delete[] arr;
      (3)结构体
      struct test{
      int a;
      int b;
      };
      int c = 0;
      test * t = new test();
      c = t->a;

    使用未初始化的指针

    在使用指针之前,必须要用已分配的地址来初始化他
    int *pt;
    在声明创建了一个指针时,系统只分配了存储指针本身的内存,而并未分配存储数据的内存。
    比如

    int *pt;
    *pt = 5; //严重错误
    
    • 1
    • 2

    严重错误,因为该句意思是把5存储在pt指向的位置。而此时pt只声明了,还没有指向一个位置,所以不知道将5存在何处,程序可能擦除其他数据,或者导致程序崩溃。

    指针操作

    // ptr_ops.c -- pointer operations
    #include 
    int main(void)
    {
        int urn[5] = {100,200,300,400,500};
        int * ptr1, * ptr2, *ptr3; //声明指针
        
        ptr1 = urn;         // 把一个地址赋值给指针,等价于ptr1 = &urn[0]; 
        ptr2 = &urn[2];     // 把一个地址赋值给指针
        // 解引用指针以及获得地址
        printf("pointer value指针的值, dereferenced pointer解引用指针, pointer address指针的地址:\n");
        printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n",
               ptr1, *ptr1, &ptr1);//ptr1 = 0x7ffd7c103ad0, *ptr1 =100, &ptr1 = 0x7ffd7c103ac8
        
        // pointer addition指针加法
        ptr3 = ptr1 + 4;
        printf("\nadding an int to a pointer:\n");
        printf("ptr1 + 4 = %p, *(ptr4 + 3) = %d\n",
               ptr1 + 4, *(ptr1 + 3));//ptr1 + 4 = 0x7ffd7c103ae0, *(ptr4 + 3) = 400
        // increment a pointer 指针++
        ptr1++;            
        printf("\nvalues after ptr1++:\n");
        printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n",
               ptr1, *ptr1, &ptr1); //ptr1 = 0x7ffd7c103ad4, *ptr1 =200, &ptr1 = 0x7ffd7c103ac8 指针本身会移动至下一个元素
        // decrement a pointer 指针--
        ptr2--;           
        printf("\nvalues after --ptr2:\n");
        printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n",
               ptr2, *ptr2, &ptr2); //ptr2 = 0x7ffd7c103ad4, *ptr2 = 200, &ptr2 = 0x7ffd7c103ac0
        --ptr1;            // restore to original value
        ++ptr2;            // restore to original value
        printf("\nPointers reset to original values:\n");
        printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2); //ptr1 = 0x7ffd7c103ad0, ptr2 = 0x7ffd7c103ad8
        // subtract one pointer from another 指针减指针
        printf("\nsubtracting one pointer from another:\n");
        printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n",
               ptr2, ptr1, ptr2 - ptr1);//ptr2 = 0x7ffd7c103ad8, ptr1 = 0x7ffd7c103ad0, ptr2 - ptr1 = 2 (表示的是元素之间下标位置距离
        // subtract an integer from a pointer 指针减一个数
        printf("\nsubtracting an int from a pointer:\n");
        printf("ptr3 = %p, ptr3 - 2 = %p\n",
               ptr3,  ptr3 - 2); //ptr3 = 0x7ffd7c103ae0, ptr3 - 2 = 0x7ffd7c103ad8
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    使用const保护数据

    函数传参两种方式,一种是按值传递,一种是指针传递。如果传递的是数组,则我们都要使用按指针传递。因为值传递数组函数栈必须有足够空间来接收原数组的副本,而且效率低下。所以我们一般都使用指针传递。
    但是按指针传递有一个安全隐患就是可能会修改原数组中的数据,但有时我们不想让函数修改我们数组中的数据,以保证数据的完整性。我们可以使用const关键字。

    /* displays array contents */
    void show_array(const double ar[], int n)
    {
        int i;
        
        for (i = 0; i < n; i++)
            printf("%8.3f ", ar[i]);
        putchar('\n');
    }
    /* multiplies each array member by the same multiplier */
    // 如果这里使用const修饰,那么编译就会报错
    void mult_array(double ar[], int n, double mult)
    {
        int i;
        
        for (i = 0; i < n; i++)
            ar[i] *= mult;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    const修饰的几种用法

    //修饰数组
    const double dip[SIZE] = {20.0, 17.66, 8.2, 15.3, 22.22};
    dip[2] = 3.4; //编译报错
    double *pt = dip; //不允许,因为dip是const数组,同理不要把const数组传给普通形参指针
    
    //修饰指针类型:不允许改变指向的值
    double rates[4] = {20.0, 17.66, 8.2, 15.3, 22.22};
    const double *pd  = rates;// 指向元素首地址
    *pd = 4.5; //不允许
    pd[2] = 4; // 不允许
    double rates2[2] = {23,4};
    pd = rates2;//允许,pd指针可以指向其他位置
    
    //修饰单独指针:指针不能指向其他位置
    double arr[4] = {20.0, 17.66, 8.2, 15.3, 22.22};
    double * const pt2 = arr;
    pt2 = &arr[2]; //不允许,不能指向其他位置
    *pt = 33.1; //允许,可以修改指向的值arr[0]
    
    //修饰指针类型+单独指针:不能指向其他位置也不能修改值
    double arr2[4] = {20.0, 17.66, 8.2, 15.3, 22.22};
    const double* const pc = arr2;
    pc = &arr2[2]; //不允许,不能指向其他位置
    *pc = 33.1; //不允许
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    指针的兼容性

    指针之间的赋值比数值之间的类型要严格。
    也就是不同类型指针之间不能赋值,不然会报编译错误。
    比如int和double之间不能赋值,int** 和int*之间也不能相互赋值。
    在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    中国智能音箱市场销量下降,百度稳居第一 /中国即评出10个大模型创新案例 |魔法半周报
    MySQL 增删改 insert delete update 实例
    工程制图试题
    SAP CO系统配置-产品成本控制
    QT 通用算法可以在任何提供STL风格迭代器的容器类上使用
    [Python] reverse()函数 VS reversed()函数
    LeetCode Algorithm 2326. 螺旋矩阵 IV
    “座驾改造” VoxEdit 创作大赛
    Linux安装Nacos集群
    kafka如何保证消息不丢失?半分钟的答案和半个小时的答案有点不一样。
  • 原文地址:https://blog.csdn.net/chongbin007/article/details/125833298