物联网工程师技术之C语言指针
本章重点
• 指针的概念
• 使用指针
• 指针运算
• 指针与数组
C语言的自由性很大部分体现在其灵活的指针运用上。指针可以用来有效地表示复杂的数据结构,可以用于函数参数传递并达到更加灵活使用函数的目的,可以使C语言程序的设计具有灵活、实用、高效的特点。指针是C语言的灵魂,运用得好更是事半功倍,可以让大家写出的程序更简洁!
本章将系统介绍指针的用法。在介绍指针之前,本章首先介绍程序在内存中的存放方式,随后介绍变量地址并引入指针的概念。本章还会介绍如何在程序中正确使用指针、指针的运算、指针和数组的关系等内容。
8.1 内存管理
程序员编写的C语言程序在经历过预处理,编译,汇编和链接之后成为硬盘上的可执行文件。在运行程序时,可执行文件载入到内存开始执行。程序在内存中一般会占用如下几段区域:
1. 代码段:这一段内存用来存放程序编译之后得到的机器指令。在程序执行时,CPU从代码段中逐条读取程序的指令,在CPU中解码执行。
2. 数据段:数据段用于存放程序中的全局变量,静态变量。在数据段中,未初始化的全局变量和静态变量在一段里,已初始化的全局变量和静态变量在另一段里。
3. 栈:用于函数调用和存放局部变量,栈由编辑器自动管理。
4. 堆:当程序在运行过程中有时需要动态分配内存,这一段用于提供动态分配内存的区域就是堆。程序通过malloc和free函数实现在堆中动态申请和释放内存的功能。
下面是例程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函数申请了一段长度为125个int大小的内存,这一段内存被分配在堆上。随后的free函数释放了这段内存。本章随后将要介绍malloc和free函数。
8.2 什么是指针
本书此前的章节中介绍过内存和变量的概念。内存可以被抽象为一个巨大的一维数组,这个数组的每个元素是一个字节,字节在这个数组中的下标是该字节在内存中的地址。在32位操作系统中,字节的地址用一个32位二进制数表示,其范围是0x00000000到0xffffffff,简单计算可以知道内存的大小是4GB。
每一个变量在内存中占据一段连续的字节,字节的数量由变量的类型决定。以int为例,32位的int类型整数在内存中占据连续的4个字节。变量的第一个字节在内存中的地址被称为这个变量的地址。
指针也是一个变量,也有自己的名字,并在内存中占据一段字节保存指针的内容。和普通的变量不同的是,指针的内容是内存中的地址。以32位程序为例,内存的地址用32位二进制数表示,那么指针的大小就是32位4个字节。
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-2中printf里%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中定义了一个3行4列的二维数组,随后利用printf和for循环打印出了所有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看上去像是一种全新的数据类型,但是它的本质只是内存中的一个地址而已,它的值也不过是一个普通的整数。%p在printf中将这个整数解读为指针指向的内存中的地址,以十六进制输出,如果对十六进制不太习惯,也可以在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类型的整数i和double类型的浮点数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类型的变量num1和num2,随后定义了一个指向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 = # // 指针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中定义了两个指针p1和p2,其中p1被初始化指向一个int类型的变量num,p2暂时不使用,初始化为NULL。由于p1的值必定非0,因此在if判断中为真;相反,p2的值为0,在if判断中为假。程序最后会利用printf打印出p1的值。
NULL指针被广泛地使用在多种数据结构中。有一种数据结构叫做链表,在链表中每一个节点都是一段内存。这一段内存中包含一个指针指向下一个节点的地址。这样,从第一个节点开始就可以顺着指针的地址遍历链表中的所有节点。如何判断是否到达了链表的最后一个节点呢?答案就是在链表的最后一个节点中,指针的值被设为了NULL。这样当检查到某个节点包含的指针为NULL的时候就可以确定整个链表已经被遍历完了。
8.3 指针的使用
定义了指针之后,下一步就是如何使用已经定义的指针。通过指针,可以对指针指向的变量进行读写,可以用来访问数组,也可以向堆中申请新的内存,还可以用来作为函数的参数和返回值。
8.3.1 用指针读写数据
通过“*指针变量”可以读取指针指向的变量的值。下面是例程8-10:
例程 810 用指针读数据
#include <stdio.h>
int main()
{
int num = 12;
int* p = #
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 = #
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 = #
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函数分别打印p和p+1的值;类似地,程序中定义了指向double的指针q,并将q初始化为d的地址,并利用printf打印q和q-1的值。程序的运行结果如图8-25所示:
图 825 指针加减整数
从图8-25中可以看出,p+1和p之间相差了4个字节,而q-1和q之间相差了8个字节。它们相差的字节数正好是所指向数据类型的大小,即int的4个字节和double的8个字节。这个例程进一步验证了指针加减整数是以自己指向的数据类型的大小为单位进行的。
指针可以加减整数,自然也可以利用自增和自减运算符,因为自增或自减相当于对原来的值加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行将指针p的值写为NULL。如果暂时不使用某个指针,将它的值设为NULL是一种好习惯。
第7行将指针q的值设为数组num的第一个元素的地址,并用printf打印q的值和*q的值。
第9行利用指针的加法将p的值赋值为q+2,根据前面的介绍,q+2应该指向数组num的第3个元素,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 指针与指针相减
程序中初始化了两个指针p和q,分别指向数组num的第1个和第3个元素。由于数组在内存中是连续存储的,p和q的地址相差了两个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类型的数据大小),因此输出的结果显示p和q的差是-2,而p也小于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-22中char*类型的指针p被初始化为数组str的地址,此时p指向了数组中的第一个元素H。接下来的while循环中利用p++将p的值每次后移一个char的大小,恰好相当于访问char数组中的下一个元素,因此整个while循环实现了遍历str数组的作用。
之前的例程揭示了对于整数k,p+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 指向指针的指针
指针可以指向内存中的变量,在前面的例子中,这个变量可以是double,int,char等基本数据类型。由于指针自己也可以作为一个变量存放在内存中,因此可以定义一个特殊类型的指针——指向指针的指针。
8.5.1 指针的地址
指针也是变量,也有自己的类型和地址,因此可以将指针变量的地址复制给另外一个新指针,这个新指针就是指向指针的指针。请看下面的例程8-26:
例程 8 指向指针的指针
1 #include <stdio.h>
2 int main()
3 {
4 int num = 3;
5 int* p = #
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的结果应该就是num的值3。
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之后地址增加了一个double,num[0]可以被看做一个一维的double数组。类似地num[1]也可以被看成是一个新的一维数组。
然而,尽管num[0]和num[1]可以用来给double*类型的指针初始化,num的类型并不是像想象中那样的double**,观察num+1的值可以发现,num的地址增加了24个字节,即3个double的长度,这恰好是二维数组中一行的长度,这也从侧面证明了num的类型并不是double**,事实上,num的类型是double(*)[3],表示num指向的元素是一个长度为3的double数组。
8.5.3 malloc函数与free函数
本章前面的示例中多次提到了malloc和free两个函数,这两个函数用于向堆中申请和释放内存。malloc函数的使用方式是:
指针类型 指针名 = (指针类型)malloc(要分配的字节数);
free函数的使用方式是:
free(指针名);
在使用这两个函数之前需要加上头文件stdlib.h。下面例子的注释演示了malloc和free的用法,请看例程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*,它可以被强制转换为任意类型的指针,上述程序中针对不同类型的指针进行了不同类型的强制转换。随后利用pi和pd指针访问分配好的堆空间,将其中某些内存位置的值进行修改。程序的运行结果如图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分配了两段内存,并将内存的地址分别赋值给两个指针pi和pd。pi申请了一段长度为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 = #
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 本章小结
本章介绍了程序在内存中不同段的内容,介绍了指针的基本概念和用法,也涉及到了指针的运算和指向指针的指针,最后介绍了malloc和free两个函数的使用方法,当对C越来越熟悉时,大家会发现,把与指针搅和在一起的“类型”这个概念分成“指针的类型”和“指针所指向的类型”两个概念,是精通指针的关键点之一。
- 点赞
- 收藏
- 关注作者
评论(0)