物联网工程师技术之C语言指针

举报
tea_year 发表于 2024/01/19 22:18:57 2024/01/19
【摘要】 本章重点• 指针的概念• 使用指针• 指针运算 • 指针与数组C语言的自由性很大部分体现在其灵活的指针运用上。指针可以用来有效地表示复杂的数据结构,可以用于函数参数传递并达到更加灵活使用函数的目的,可以使C语言程序的设计具有灵活、实用、高效的特点。指针是C语言的灵魂,运用得好更是事半功倍,可以让大家写出的程序更简洁! 本章将系统介绍指针的用法。在介绍指针之前,本章首先介绍程序在内存中的存放方...

本章重点

指针的概念

使用指针

指针运算

指针与数组

          C语言的自由性很大部分体现在其灵活的指针运用上。指针可以用来有效地表示复杂的数据结构可以用于函数参数传递并达到更加灵活使用函数的目的可以使C语言程序的设计具有灵活、实用、高效的特点。指针C语言的灵魂运用得好更是事半功倍,可以大家写出的程序更简洁!

本章系统介绍指针用法。介绍指针之前,本章首先介绍程序在内存中的存放方式,随后介绍变量地址并引入指针的概念。本章还会介绍如何在程序中正确使用指针指针的运算、指针和数组的关系等内容。

8.1 内存管理

程序员编写的C语言程序在经历过预处理,编译,汇编和链接之后成为硬盘上的可执行文件。在运行程序时,可执行文件载入到内存开始执行。程序在内存中一般占用如下几段区域:

1. 代码段这一段内存用来存放程序编译之后得到的机器指令。在程序执行时,CPU代码段中逐条读取程序的指令,在CPU中解码执行

2. 数据段:数据段用于存放程序中的全局变量静态变量在数据段中,未初始化的全局变量和静态变量在一段里初始化的全局变量和静态变量在另一里。

3. 栈:用于函数调用和存放局部变量,栈编辑器自动管理。

4. 堆:当程序在运行过程中有时需要动态分配内存一段用于提供动态分配内存的区域就是堆。程序通过mallocfree函数实现堆中动态申请和释放内存功能。

    下面是例程8-1,在注释中揭示了各个变量在内存中的位置。

例程 81内存管理

 #include <stdio.h>
 #include <stdlib.h>
 int globalVariable;                    //    未初始化的全局变量,在数据段中
 int initializedGlobalVariable = 1; //    初始化的全局变量,在数据段中
 static int staticGlobalVariable;    //    未初始化的静态全局变量,在数据段中
 void foo()
 {
     int localVariable = 2;    //    局部变量,在栈中
 }
 int main()
 {
     static int staticLocalVariable = 2;    //    初始化的静态局部变量,在数据段中
     int localVariable = 3;    //    局部变量,在栈中
     foo();    //    调用函数foo,会使用到栈
     int* p = (int*)malloc(sizeof(int) * 125);    
     //    125个int大小的一段内存,在堆中
     free(p);
     return 0;
 }

程序的运行结果8-1所示

81 内存管理

例程8-1有几个值得注意的地方

1. static关键字修饰的静态变量,无论是全局的还是局部的保存数据段中。被static修饰的局部变量不会保存在栈中

2. main函数和foo函数都有名为localVariable的局部变量,foo函数被调用时,它们在内存中是两个变量,存放在栈中的两个不同位置。

3. malloc函数申请了一段长度为125int大小的内存,这一段内存被分配在堆上。随后的free函数释放了这段内存。本章随后要介绍mallocfree函数。

8.2 什么是指针

本书此前的章节中介绍过内存和变量的概念。内存可以抽象为一个巨大一维数组,这个数组的每个元素一个字节,字节在这个数组下标该字节在内存中的地址32位操作系统中字节地址用一个32位二进制表示,其范围是0x000000000xffffffff,简单计算可以知道内存的大小是4GB

每一个变量在内存中占据一连续的字节字节的数量由变量的类型决定。以int为例,32int类型整数在内存中占据连续的4字节。变量第一个字节内存中的地址被称为这个变量的地址。

指针也是一个变量,也有自己的名字,并在内存中占据一段字节保存指针的内容。和普通的变量不同的是,指针的内容是内存中的地址32程序为例,内存的地址32位二进制数表示,那么指针大小就是324字节。

8.2.1 变量的地址

本书在前面介绍scanf时曾经介绍过&符号。&称为取地址运算,在&之后跟一个变量名就可以取得这个变量在内存中的地址。请看下面的例程8-2

例程 82 变量的地址

 #include <stdio.h>
 int main()
 {
     int num = 0x12345678;        //    int类型变量num
     printf("num = %x\n", num);
     printf("&num = %p\n", &num);
     return 0;
 }

程序的运行结果如8-2所示

82 变量的地址

例程8-2printf%x表示输出十六进制数,%p表示输出一个指针,在这里可以理解为以十六进制形式输出一个地址

8-2为例指针&num的值,也就是变量num地址0032F8B8,这意味着num占据了内存0032F8B8开始的4字节。4字节存储的值0x12345678

此前的章节中介绍过C语言的数组在内存中是连续保存的,也可以用取地址运算符验证数组在内存中的保存方式是否真的是连续的。例程8-3

例程 83 数组的地址

 #include <stdio.h>
 int main()
 {
     int num[4] = {1, 2, 3, 4};    //    定义一个长度为4的数组
     int i;
     for (i = 0; i < 4; i++)
     {    
         printf("&num[%d] = %p\n", i, &num[i]);
     }
     return 0;
 }

程序运行结果如8-3所示:

83 数组的地址

例程8-3首先定义了一个int类型的数组num,包含四个整数,接下来利用printf打印num[0]num[3]四个元素内存中的地址。可以看到,数组中四个元素在内存确实是连续存放的。它们在内存中的位置构成一个等差数列,公差为4个字节,刚好是一个int数据的大小。

类似地,还可以用&验证二维数组在内存中是行优先存储的。在下面的例程8-4中定义了一个34的二维数组,随后利用printffor循环打印出了所有12元素的地址,如例程8-4所示

例程 84 二维数组的地址

 #include <stdio.h>
 int main()
 {
     int num[3][4] = {{0, 1, 2, 3},
                     {4, 5, 6, 7},
                     {8, 9, 10, 11}};    //    二维数组
     int i, j;
     for (i = 0; i < 3; i++)
     {
         for (j = 0; j < 4; j++)
         {
             printf("&num[%d][%d] = %p\n", i, j, &num[i][j]);
         }
     }
     return 0;
 }

程序的运行结果8-4所示

84 二维数组的地址

可以看出,二维数组元素确实按照行优先的顺序保存。具体地说,num[0][0]num[0][3]这四个元素地址紧挨在一起,接下来是num[1][0]num[1][3]num[2][0]num[2][3]等等。

尽管&num看上去像是一种全的数据类型,但是它的本质只是内存中的一个地址而已,的值也不是一个普通的整数。%pprintf中将这个整数解读指针指向的内存中的地址,以十六进制输出,如果对十六进制不太习惯,也可以printf中使用%d%i输出十进制的地址如例程8-5所示

例程 85 输出十进制地址

1 #include <stdio.h>
2 int main()
3 {
4     int num = 0x12345678;    //    int类型变量num
5     printf("&num = %i, %p\n", &num, &num);
6     return 0;
7 }

程序的运行结果如8-5所示

85 输出十进制地址

可以利用计算器验证这两个地址是一样的。尽管如此,由于C语言中的数据类型大小多数都是4个字节的整数倍,因此还是建议使用十六进制查看指针地址。

8.2.2 定义一个指针

本章开头曾经介绍过指针是一种特殊的变量,因此定义一个指针的方法和定义一个变量的方法是类似也需要知道变量的类型,为变量起一个合适的变量名,如果可以的话再给变量赋一个初值。指针的类型它指向的变量类型加上*组成。下面是例程8-6

例程 86 定义一个指针

1

#include <stdio.h>
 int main()
 {
     int i = 12;                //    int类型变量i
     double d = 34.0;        //    double类型变量d
     int* pi = &i;            //    指针pi,指向i
     double* pd = &d;        //    指针pd,指向d
     printf("pi = %p\n", pi);
     printf("pd = %p\n", pd);
     return 0;
 }

程序运行结果如8-6所示:

86 定义一个指针

例程8-6首先定义了两个变量:int类型的整数idouble类型浮点数d。接下来在第7行定义了一个指针:

    int* pi = &i;

其中int*是这个指针变量的类型,它表示该指针保存的值必须一个int类型整数的地址,更常用的一种等价说法是:这个指针指向一个int类型的变量pi是这个指针变量的变量名,&i这个指针的初值由于i是一个int类型变量符合pi类型,因此i的地址赋值给pi合法

类似地,在第8行定义了另一个指向double指针pd

    double* pd = &d;

该指针类型double*,表示pd必须指向一个double类型的变量,的初值是变量d地址,由于d是一个double类型的浮点数,d地址pd的类型一致。

由于指针变量也是变量,因此指针变量的值(指向的内存中的地址)也是可以修改的。修改指针变量的值相当于将原来的指针指向了的一段内存。下面一个修改指针变量的例子例程8-7

例程 87 修改指针变量

 #include <stdio.h>
 int main()
 {
     int num1 = 3, num2 = 5;        //    定义两个变量num1和num2
     int* p = &num1;        
     printf("p = %p\n", p);  
    printf("&num1 = %p\n", &num1);
     p = &num2;
     printf("p = %p\n", p);
     printf("&num2 = %p\n", &num2);
     return 0;
 }

8-7所示

87修改指针变量

例程8-7中,首先定义了两个int类型变量num1num2,随后定义了一个指向num1指针p此时p中的值是num1内存中的地址。main函数中利用printf查看了此时p的值和num1地址,它们应该是一致的

接下来p的值修改为num2地址,利用printf输出p的值和num2的地址此时它们应该是一致的。

有的时候刚刚声明了一个指针但是并不准备立即使用,这个时候可以NULL去初始化它。NULL值是0一般被称为空指针。例程8-8查看了NULL指针的值:

例程 88 空指针

 #include <stdio.h>
 int main()
 {
     int* p = NULL;
     printf("p = %p\n", p);
     return 0;
 }

程序的运行结果如图8-8所示

88 空指针

可以看到空指针值确实NULL

利用空指针有以下几个好处:首先,空指针可以用来初始化当前不立即使用指针。如果不进行初始化,这些指针指向的内存地址是不确定的,很有可能是不合法的地址如果接下来忘记初始化就使用这些指针就会出现问题

其次如果每次对暂时使用的指针都赋值为NULL的话,按照C语言计算逻辑表达式真值的规则(非零即为真就可以用指针的值来判断这个指针是否有效,请看例程8-9

例程 89 使用NULL指针

 #include <stdio.h>
 int main()
 {
     int num = 1234;        //    定义一个变量num
     int* p1 = &num;        //    指针p1指向num
     int* p2 = NULL;        //    指针p2是空指针
     if (p1)
     {
         printf("p1 = %p\n", p1);
     }
     if (p2)
     {
         printf("p2 = %p\n", p2);
     }
     return 0;
 }

程序的运行结果如图8-9所示

89 使用NULL指针

例程8-9定义了两个指针p1p2,其中p1初始化指向一个int类型的变量nump2暂时不使用,初始化为NULL。由于p1值必定0,因此在if判断中为真;相反p20,在if判断中程序最后会利用printf打印p1

NULL指针被广泛地使用在多种数据结构中。有一种数据结构叫做链表,在链表中每一个节点都是一段内存。这一段内存中包含一个指针指向下一个节点地址。这样第一个节点开始可以顺着指针的地址遍历链表中的所有节点如何判断是否到达了链表的最后一个节点?答案就是在链表的最后一个节点中,指针的值被设为了NULL。这样检查到某个节点包含的指针为NULL的时候就可以确定整个链表已经被遍历完了。

8.3 指针的使用

定义了指针之后,下一步就是如何使用已经定义的指针。通过指针,可以对指针指向的变量进行读写可以用来访问数组,也可以向堆中申请的内存还可以用来作为函数的参数和返回值。

8.3.1 用指针读写数据

通过*指针变量可以读取指针指向的变量的值。下面例程8-10

例程 810 指针读数据

 #include <stdio.h>
 int main()
 {
     int num = 12;
     int* p = &num;
     printf("*p = %d\n", *p);
     num = 14;
     printf("*p = %d\n", *p);
     return 0;
 }

程序的运行结果如图8-10所示:

810 用指针读数据

例程8-10中首先声明了一个int类型的变量num,接下来利用&num初始化一个指向int的指针p。可以这样理解*p的计算过程:首先通过变量p得到num在内存中的地址,再根据num的地址在内存中找到num的值,这就是*p的结果。

    第一次调用printf时因此*p的输出是12。随后程序修改了num的值为14,因此第二次调用printf时*p的值应该为14。

,如例程8-11所示程序的运行如图8-11所示*符号不仅可以用来读取指针指向的数据,还可以作为左值用来修改指针指向的数据。下面一个*p写数据的例子例程8-12

例程 8 用指针写数据

1 #include<stdio.h>
2 int main()
3 {
4     int num = 12;
5     int* p = &num;
6     printf("*p = %d\n", *p);
7     *p = 14;
8     printf("num = %d\n", num);
9     return 0;
10 }

程序的运行结果如8-12所示

8 指针写数据

8.3.2 在例程8-11*p=14修改了num的值,通过第二次调用printf可以看到num的值确实被修改为了14,这揭示了*p不仅可以用来读num,还可以用来改写num,如例程8-13所示指针作为函数参数

通过前面的讲解可知,整型变量、字符变量、数组名、数组元素等都可以作为函数的参数,此外,指针也可以作为函数的参数但是由于指针的本质上是内存中的地址,实际参数的指针进行值传递,相当于这个地址拷贝给形式参数指针,从而形式参数指针指向内存同样的地址。下面的例程8-14体现了使用指针作为函数参数的效果:

例程 8 指针作为函数参数

1 #include <stdio.h>
2 void swap1(int x, int y)
3 {
4     int temp = x;
5     x = y;
6     y = temp;
7 }
8 void swap2(int* px, int* py)
9 {
10     int temp = *px;
11     *px = *py;
12     *py = temp;
13 }
14 int main()
15 {
16     int a = 3, b = 5;
17     printf("a = %d, b = %d\n", a, b);
18     swap1(a, b);
19     printf("a = %d, b = %d\n", a, b);
20     swap2(&a, &b);
21     printf("a = %d, b = %d\n", a, b);
22     return 0;
23 }

程序的运行结果如8-15所示

8.3.3 指针作为函数返回值

除了作为函数参数指针可以函数的返回值和指针作为形式参数类似用指针作为返回值时返回一个内存中地址。比如下面例程8-15

例程 8 指针作为函数返回值

 #include <stdio.h>
 #include <stdlib.h>
 int* array1()
 {
     int* p = (int*)malloc(sizeof(int) * 3);
     p[0] = 15;
     return p;
 }
 int* array2()
 {
     int a[3];
     a[0] = 15;
     return a;
 }
 int main()
 {
     int* q1 = array1();
     int* q2 = array2();
     printf("*q1 = %d\n", *q1);
     printf("*q2 = %d\n", *q2);
     return 0;
 }

    程序的运行结果如图8-23所示:

8 指针作为函数返回值

    例程8-15中再次揭示了栈和堆的不同。array1中利用malloc函数在堆上分配了一段长度为3个int的内存,一个int*类型的指针指向了这段内存,它的值就是这段内存的地址。第7行将这段内存中第一个int的值改成了15,最后将这段内存的地址返回。在main函数的第20行里,返回的地址被拷贝给了int*的指针q1,于是*q1就会访问到15。

    然而在array2中,由于a是分配在array2函数的栈里的,在函数调用之后栈上的局部变量被自动释放,因此q2指向了一段被释放后内容未知的内存,从运行结果中可以看出这一点。

8.4 指针运算

除了利用指针访问指向的内存,指针还可以参与简单的运算本节中主要介绍指针加减整数,指针与指针相减两种运算。除此之外,本节还将要介绍指针进行赋值,以及指针一维数组之间的密切关系。

8.4.1     指针加减整数

指针中的值实际上内存里的地址,因此,一个指针加减整数相当于内存地址进行加减,其结果依然是一个指针。然而尽管内存地址是以字节为单位增长的,指针加减整数的单位却不是字节,而是指针指向数据的大小请看下面的例8-16

例程 8 指针加减整数

 #include <stdio.h>
 int main()
 {
     int num = 3;
     int* p = &num;
     printf("p = %p\n", p);
     printf("p + 1 = %p\n", p + 1);
     double d = 3.5;
     double* q = &d;
     printf("q = %p\n", q);
     printf("q - 1 = %p\n", q - 1);
     return 0;
 }

程序的运行结果如8-24所示:

8 指针加减整数

例程8-16首先定义了一个指向int指针p,并将p初始化为num的地址,随后利用printf函数分别打印pp+1;类似地程序中定义了指向double指针q并将q初始化为d的地址,并利用printf打印qq-1的值。程序的运行结果如8-25所示

825 指针加减整数

从图8-25可以看出p+1p之间相差了4个字节,而q-1q之间相差8个字节它们相差的字节数正好是所指向数据类型的大小,int4字节double8个字节。这个例程进一步验证指针加减整数是自己指向数据类型的大小单位进行的。

指针可以加减整数,自然也可以利用自增和自减运算符因为自增或自减相当于对原来的值加1或者减1下面使用自增和自减运算符的例子如例程8-17所示

例程 817 指针自增和自减

 #include <stdio.h>
 int main()
 {
     int num[4] = {0, 1, 2, 3};    //    一维int数组
     int* p = &num[1];
     printf("*p = %d p = %p\n", *p, p);
    p++;
     printf("*p = %d p = %p\n", *p, p);
    p--;
    printf("*p = %d p = %p\n", *p, p);
    return 0;
 }

程序的运行结果如图8-26所示

826 指针自增和自减

从程序的输出中可以看到利用指针自增和自减之后,指针改变量并不是正负1而是以指针指向的int类型的大小单位进行改变。当p++p的值增加了4字节(一个int的大小)。由于p一开始指向了数组中下标1数,数组在内存中又是连续存储的,因此自增之后p恰好指向了数组中的下一个元素。自减过程也类似。

8.4.2     指针赋值

指针值本质上是内存中的地址,因此只要是内存地址都可以用来给指针赋值。下面例程展示几种不同指针赋值的方法如例程8-18所示

例程 8 指针赋值

 #include <stdio.h>
 int main()
 {
    int* p = NULL;
    int num[3] = { -1, -3, -2 };
    int* q = &num[0];
    printf("q = %p, *q = %d\n", q, *q);
    p = q + 2;
    printf("p = %p, *p = %d\n", p, *p);
    unsigned int* r = (unsigned int*)q;
    printf("r = %p, *r = %u\n", r, *r);
   return 0;
 }

程序的运行结果如图8-27所示

827 指针赋值

例程8-18中演示了为指针赋值的3方法:

5行将指针pNULL。如果暂时不使用某个指针将它的值设为NULL是一种好习惯

7行将指针q的值设为数组num的第一个元素的地址,并printf打印q的值和*q

9行利用指针加法将p值赋值为q+2,根据前面介绍q+2应该指向数组num3个元素,printf输出p的值*p值。

11行展示不同类型指针的强制转换。当指针进行强制转换时指针的指向内存不变,只是将内存中的数据进行不同类型的解读。具体地说,q指针被强制类型转换为unsigned int*之后q指针依然指向num[0]这个元素,但是num[0]4字节原本按照int类型解读-1现在被解读为unsigned int可以预见这是一个特别大的整数。printf打印了强制类型转换之后的指针r*r

8.4.3     指针与指针相减

前面提到一个指针加上整数之后会得到新的指针相应的,两个同类型指针相减的结果一个整数。和指针加减整数的单位一样,两个同类型指针相减的结果也不是字节为单位的,而是以指针指向的数据类型大小单位的。下面是一个结合了指针和数组元素的例子例程8-19所示

例程 819 指针与指针相减

 #include <stdio.h>
 int main()
 {
     int num[3] = { 0, 1, 2 };
     int* p = &num[0];
     int* q = &num[2];
     printf("q - p = %d\n", q - p);
     return 0;
 }

程序的运行结果如8-28所示

828 指针指针相减

程序中初始化了两个指针pq,分别指向数组num1个和第3个元素。由于数组内存中是连续存储的,pq的地址相差两个int也就是8字节程序的输出结果如8-29所示

829 指针指针相减

可以看到q-p结果并不是两者地址直接相减的结果8而是两个指针之间相差的int的数量2。上述输出结果进一步验证了同类型指针做减法,结果的单位是数据类型的大小而不是字节。

既然指针和指针是可以相减的,相同类型的指针之间也是可以比较大小的:如果两个同类型指针相减的结果大于0那么前者比后者大;小于0则是后者比前者大。指针之间的大小关系实际上揭示了指针指向的地址在内存的位置先后例程8-20所示

例程 820 指针比较大小

 #include <stdio.h>
 int main()
 {
     int num[4] = {0, 1, 2, 3};    //    一维int数组
     int* p = &num[0];
     int* q = &num[2];
     printf("p - q = %d\n", p - q);
     if (p > q)
     {
        printf("p > q\n");
     }
    else if (p < q)
    {
         printf("p < q\n");
     }
    else
    {
        printf("p == q\n");
    }
    return 0;
 }

程序的运行结果如8-30

830 指针比较大小

可以看到由于一维数组num在内存中是连续存放的,指针p指向了数组中下标0元素,指针q指向了数组2元素,指针q值应该比指针p要大2(相差两个int类型的数据大小)因此输出的结果显示pq的差是-2p也小于q

8.4.4 指针与数组

前面的数组一中介绍过数组内存中存储方式是连续存储的,一个数组占据的字节数等于数组的元素个数乘以每个元素占用的字节数。本节中的例程将要揭示一个数组的数组名也是一个地址——整个数组的起始地址,或者说是这个数组第一个元素的地址请看例程8-21

例程 8 指针与数组

 #include <stdio.h>
int main()
{
     int array[3] = {0, 1, 2};
     printf("array = %p\n", array);    
     int* p = &array[0];
     printf("p = %p\n", p);
      return 0;
}

程序的运行结果如图8-31所示

831 指针与数组

例程8-21首先输出array的值,随后初始化一个指向int变量的array[0]指针,根据数组存储方式,array[0]地址也就是整个数组第一个字节的地址。

8-31可以看出,两次printf的结果完全一样。说明数组名array可以看做数组中第一个元素的地址。结合指针加减以及指针和数组的关系可以利用指针遍历字符串数组例程8-22所示

例程 8 指针遍历数组

 #include <stdio.h>
 int main()
 {
     char str[] = "Hello world!";
     char* p = str;
     while (*p)
     {
         printf("%c", *p);
         p++;
     }
     printf("\n");
     return 0;
 }

程序运行结果如8-32所示

832 指针遍历数组

例程8-22char*类型的指针p被初始化为数组str地址此时p指向了数组中的第一个元素H。接下来while循环中利用p++p的值每次后移一个char大小恰好相当于访问char数组中的下一个元素,因此整个while循环实现遍历str数组的作用。

之前的例程揭示对于整数kp+k指向的是数组str[k]的元素。指针和数组的这一紧密联系还允许程序员通过下标来访问指针指向的内存。具体地说,对于一个指针p使用p[k]访问得到的元素使用*(p+k)得到结果是一样的,如例程8-23所示

例程 823 利用下标访问指针

 #include <stdio.h>
 int main()
 {
     int num[3] = { 20, 40, 60 };
     int* p = num;
     for (int i = 0; i < 3; i++)
     {
         printf("p[%d] = %d, *(p + %d) = %d, num[%d] = %d\n",
             i, p[i], i, *(p + i), i, num[i]);
     }
     return 0;
 }

程序运行结果如8-33所示

833 利用下标访问指针

从例程8-23可以看到数组名和下标指针加下标以及用指针加法运算可以访问数组中的元素。

除了一维数组,指针也可以用来灵活访问二维数组例程8-24所示例程 824 利用双重for循环遍历二维数组

 #include <stdio.h>
 int main()
 {
     int num[3][4] = {{0, 1, 2, 3},
                     {4, 5, 6, 7},
                     {8, 9, 10, 11}};    //    二维数组
     int i, j;
     int sum = 0;
     for (i = 0; i < 3; i++)
    {
         for (j = 0; j < 4; j++)
         {
             printf("%d\n", num[i][j]);
             sum += num[i][j];
        }
     }
     printf("sum = %d\n", sum);
     return 0;
 }

程序的运行结果如8-34所示

834利用双重for循环遍历二维数组

例程8-24首先定义了一个二维数组,接着利用两重for循环去遍历二维数组中的所有元素,并将它们累加求和并输出。程序的运行结果如8-35所示

835利用双重for循环遍历二维数组

由于二维数组在内存中是按行连续存储的,下面对例程8-24进行修改,利用二维数组的这一特点,仅用一个for循环就可以实现遍历整个数组并求和的过程如例程8-25所示

例程 825 利用一重for循环遍历二维数组

 #include <stdio.h>
 int main()
 {
     int num[3][4] = {{0, 1, 2, 3},
                     {4, 5, 6, 7},
                     {8, 9, 10, 11}};    //    二维数组
     int i;
     int sum = 0;
     int* p = &num[0][0];
    for (i = 0; i < 12; i++)
    {
         printf("%d\n", *p);
         sum += *p;
         p++;
     }
     printf("sum = %d\n", sum);
     return 0;
 }

程序的运行结果如图8-36所示

836 利用一重for循环遍历二维数组

通过8-35和图8-36的比较发现,使用一重for循环和此前二重for循环遍历的结果完全一样,对于更高的数组,也可以直接用一重for循环去遍历。这里需要注意的是,当使用一重for循环和指针遍历二维数组时,遍历的顺序是行优先的:即先访问的是第一行中的所有元素,接着访问第二行中的所有元素,接着访问第三行中的所有元素,等等。

8.5 指向指针的指针

指针可以指向内存中的变量,在前面的例子中,这个变量可以是doubleintchar等基本数据类型。由于指针自己也可以作为一个变量存放在内存中,因此可以定义一个特殊类型的指针——指向指针的指针。

8.5.1 指针的地址

指针也是变量,也有自己的类型和地址,因此可以指针变量的地址复制给另外一个新指针,这个新指针就是指向指针的指针。请看下面的例程8-26

例程 8 指向指针的指针

1 #include <stdio.h>
2 int main()
3 {
4     int num = 3;
5     int* p = &num;
6     int** q = &p;
7     printf("p = %p, *p = %d\n", p, *p);
8     printf("q = %p, *q = %p, **q = %d\n", q, *q, **q);
9     return 0;
10 }

程序的运行结果如图8-37所示

831 指向指针的指针

例程8-26首先定义了一个int类型变量num,随后定义了一个指向num的指针p接下来定义了一个指向p的指针q普通指针的定义方式类似q指针类型等于就是它指向数据的类型int*加上一个*,也就是int**q指针的初值被定为p指针的地址。

程序的运行结果如8-38所示

838 指向指针的指针

程序第一行输出的是p指针的值和p指针指向的num的值;程序第二行首先输出了q指针的值,接下来用*访问q指针指向的内容。由于q指向是指针p,因此*q访问就是p的值由于*q也是一个指针,可以再次用*访问*q这个指针(实际上就是p指向内存也就是num的值可以预见**q结果应该就是num3

8.5.2 指针与二维数组

维数组的数组名里包含了数组信息,二维数组的数组名更加复杂一些。下面的例子以一个double类型的二维数组为例,揭示二维数组和指针之间的关系,如例程8-27所示

例程 8 指针与二维数组

 #include <stdio.h>
 int main()
 {
     double num[2][3] = { {0.0, 1.0, 2.0}, {3.0, 4.0, 5.0} };
     printf("num = %p, num + 1 = %p\n", num, num + 1);
     printf("num[0] = %p, num[0] + 1 = %p\n", num[0], num[0] + 1);
     printf("num[1] = %p\n", num[1]);
     return 0;
 }

程序的运行结果如图8-39所示:

839 指针与二维数组

由于C语言中二维数组是按照行优先的顺序在内存中排列的,因此num[2][3]的在内存中的顺序是num[0][0]num[0][1]num[0][2]num[1][0]num[1][1] num[1][2]。观察num[0]值可以发现num[0]就是num值,也就是数组的第一个元素num[0][0]的位置num[0]+1之后地址增加一个doublenum[0]可以看做一个一维double数组。类似地num[1]也可以被看成是一个新的一维数组。

然而尽管num[0]num[1]可以用来给double*类型的指针初始化,num的类型并不是像想象中那样的double**观察num+1值可以发现,num的地址增加了24字节,即3double的长度,这恰好是二维数组中一行的长度这也侧面证明了num的类型并不是double**事实上,num的类型是double(*)[3]表示num指向的元素一个长度为3double数组

8.5.3 malloc函数与free函数

    本章前面的示例中多次提到了mallocfree两个函数,这两个函数用于向堆中申请和释放内存。malloc函数的使用方式是:

指针类型 指针 = (指针类型)malloc(分配的字节数);

free函数的使用方式是:

free(指针);

使用这两个函数之前需要加上头文件stdlib.h。下面例子的注释演示mallocfree的用法请看例程8-28

例程 8 malloc与free

 #include <stdio.h>
 #include <stdlib.h>
int main()
 {
     int* pi = (int*)malloc(sizeof(int) * 15);    //    在堆上申请内存
     double* pd = (double*)malloc(sizeof(double) * 2);    //    在堆上申请内存
     pi[2] = 4;
     pd[7] = 13.5;
     printf("pi[2] = %d\n", pi[2]);
     printf("pd[7] = %f\n", pd[7]);
     free(pi);    //    释放pi指向的内存
     free(pd);    //    释放pd指向的内存
    return 0;
 }

程序运行结果8-40所示

840 malloc与free

malloc函数的返回值是void*它可以被强制转换为任意类型的指针,上述程序中针对不同类型的指针进行了不同类型的强制转换。随后利用pipd指针访问分配好的堆空间,将其中某些内存位置的值进行修改。程序运行结果8-41所示

841 malloc与free

    需要强调的是,堆中申请的内存不像栈上自动释放,程序员必须通过free手动释放。一旦失去了指向已分配内存的指针没有利用free函数释放,就会造成内存泄露。下面是一个内存泄露的例子例程8-29所示

例程 8 内存泄露

 #include <stdio.h>
 #include <stdlib.h>
 int main()
 {
     int* pi = (int*)malloc(sizeof(int) * 100000000);
     double* pd = (double*)malloc(sizeof(double) * 100000000);
     free(pi);
     double d = 1234.0;
     pd = &d;
     return 0;
 }

程序运行结果如8-42所示

842 内存泄露

例程8-29中首先通过malloc分配了两段内存,并将内存的地址分别赋值给两个指针pipdpi申请了一段长度为400MB的内存,pd申请了一段长度为800MB的内存。申请完成后,堆中分配出了1.2GB的内存。可以利用windows的任务管理器的进程选项卡查看程序当前使用的内存如图8-43所示


843malloc之后内存使用情况

可以看到例程8-29目前占据了大约1.2GB的内存。

接下来程序中释放了pi指针指向的内存,这样有大约400MB的内存从堆空间中被释放,还有800MB的内存正在使用

关于free函数还有点需要强调的地方:

首先free函数是用来释放堆空间中malloc分配的内存,它的参数是必须指向堆空间中的一个指针,因此不要将指向栈空间中的指针传递给free否则程序会报错如例程8-30所示

例程 8 free释放栈指针

 #include <stdio.h>
 #include <stdlib.h>
 int main()
 {
     int num = 234;
     int* p = &num;
     free(p);
    return 0;
 }

Visual Studio中运行上述程序,程序的运行结果如图8-46所示

846 free释放指针

这个错误意味着程序在内存的分配和回收上出现了一些问题。检查之后发现错误的原因是在main函数中利用释放了一个栈指针。

第二,即使是对于指向堆上的指针,free函数只能释放这些指针一次。如果重复释放同样的指针程序也会报错请看例程8-31

例程 8 free重复释放指针

 #include <stdio.h>
 #include <stdlib.h>
 int main()
 {
     int* p = (int*)malloc(20);
     free(p);
     printf("1 free\n");
     free(p);
     printf("2 free\n");
    return 0;
 }

如果Visual Studio中运行上述程序,可以看到行时错误如图8-47所示


847 free重复释放指针

再观察程序在控制台的输出,如图8-48所示

848 free重复释放指针

这说明第一次用free释放指针是成功第二次调用free释放同一个指针的时候出现了错误。

第三如何避免出现使用free函数多次释放指针的错误呢?在使用free函数释放完指针之后,很有可能在后面的程序中再次误用已经释放的指针。为了避免这一点,可以在free函数之后立刻将指针设为NULL。由于free函数规定参数为NULL时函数不做操作,这样就可以避免误用free函数多次释放指针带来的问题例程8-32所示

例程 8 free释放NULL指针

 #include <stdio.h>
 #include <stdlib.h>
 int main()
 {
    int* p = (int*)malloc(15);
    free(p);
     p = NULL;
     printf("1 free\n");
     free(p);
     printf("2 free\n");
     return 0;
 }

程序的运行结果如8-49所示

849 free释放NULL指针

可以看到,将free释放完的指针设为NULL是一个好习惯,程序这次没有崩溃,可以正常执行两次free虽然第二次free实际上没有释放内存)当然,更好的做法是不要误用free函数多次释放同样的指针。

8.5.4 动态构造二维数组

本章要介绍的最后一个关于malloc应用是动态地构造二维数组。有些时候程序中需要定义一个二维数组,但是二维数组的大小知道程序运行时才能确定,比如以两个参数的形式传递给某一个函数或者通过用户的输入来确定。这个时候简单地定义一个二维数组是不起作用比如例程8-33

例程 8 动态分配二维数组

 #include <stdio.h>
 #include <stdlib.h>
 int main()
 {
     int u, v;            //    定义二维数组的长和宽
     printf("u = \n");
     scanf("%d", &u);
     printf("v = \n");
     scanf("%d", &v);
     int array[u][v];    //    根据输入的长宽定义一个二维数组
     return 0;
 }

上面的程序试图从用户的输入中获得二维数组的大小然后定义一个二维数组。

既然直接定义二维数组行不通了,就可以考虑利用malloc在堆中动态地分配一个二维数组。malloc可以在堆中分配一段连续的内二维数组在内存中也应该是连续存储的,看上去这是一件很轻松的事情如例程8-34所示

例程 8 动态分配二维数组

 #include <stdio.h>
#include <stdlib.h>
 int main()
 {
     int u, v;            //    定义二维数组的长和宽
     printf("u = \n");
     scanf("%d", &u);
     printf("v = \n");
     scanf("%d", &v);
     int** array = (int*)malloc(sizeof(int)* u * v);
     int i, j;
    for (i = 0; i < u; i++)
    {
        for (j = 0; j < v; j++)
        {
            array[i][j] = 0;
         }
     }
     return 0;
 }

如果在Visual Studio运行上面的程序,它依然出现运行时错误假设用户的输入如8-51所示

8.6 本章小结

本章介绍了程序内存中不同段内容介绍了指针的基本概念用法,涉及到了指针运算和指向指针的指针,最后介绍了mallocfree两个函数的使用方法当对C越来越熟悉时,大家会发现,把与指针搅和在一起的类型这个概念分成指针的类型指针所指向的类型两个概念,是精通指针的关键点之一。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。