第十一章 指针和数组
一旦给出数组的定义,编译系统就会为其在内存中分配固定的存储单元,相应的,数组的首地址也就确定了
C语言中的数组名有特殊的含义,它代表存放数组元素的连续存储空间的首地址
//L11-1
#include <stdio.h>
int main()
{
int a[5], i;
printf("Input five numbers:");
for (i=0; i<5; i++)
{
scanf("%d", &a[i]); /* 用下标法引用数组元素 */
}
for (i=0; i<5; i++)
{
printf("%4d", a[i]); /* 用下标法引用数组元素 */
}
printf("\n");
return 0;
}
```c
//运行结果
Input five numbers:1 2 3 4 5
1 2 3 4 5
表达式a+i代表数组中下标为i的元素a[i]的地址(&a[i])
因此可以通过间接寻址来引用数组中的元素
*(a+i)表示取出首地址后第i个元素的内容,即a[i]
指针的算术运算和关系运算常常是针对数组元素而言的
如果定义了一个指向整型数据的指针变量p,并使其值为数组a的首地址
那么可以通过这个指针变量p来访问数组a中的元素
p+1和p++是不同的操作
p+1并不改变当前指针的指向
p++相当于将指针变量向前移动一个元素位置
p++是为指针变量加上1*sizeof(基类型)个字节
用数组名和用指向一维数组的指针变量作函数实参,向被调函数传递的都是数组的起始地址,都是模拟按引用调用
对一维数组而言,用数组作函数形参和用指针变量作函数形参本质相同,因为他们接收的都是数组的起始地址,都需按此地址对主调函数中的实参数组进行间接寻址
//L11-2
#include <stdio.h>
void InputArray(int a[], int n);
void OutputArray(int a[], int n);
int main()
{
int a[5];
int *p = a;
printf("Input five numbers:");
InputArray(p, 5); /* 用指向一维数组的指针变量作为函数实参 */
OutputArray(p, 5); /* 用指向一维数组的指针变量作为函数实参 */
return 0;
}
void InputArray(int a[], int n) /* 形参声明为数组,输入数组元素值 */
{
int i;
for (i=0; i<n; i++)
{
scanf("%d", &a[i]); /* 用下标法访问数组元素 */
}
}
void OutputArray(int a[], int n) /* 形参声明为数组,输出数组元素值 */
{
int i;
for (i=0; i<n; i++)
{
printf("%4d", a[i]); /* 用下标法访问数组元素 */
}
printf("\n");
}
指针与二维数组的关系:
int a[3][4];
a[i]可以看成是a[i][0]、a[i][1]、a[i][2]、a[i][3]这4个元素组成的一维整型数组的数组名,代表这个一维数组的第1个元素a[i][0]的地址(&a[i][0])
a[i]+j即*(a+i)+j代表这个数组中下标为j的元素的地址(&a[i][j])
*(a[i]+j)即*(*(a+i)+j)代表a[i][j]
二维数组的行指针和列指针:
int a[3][4];
行指针
int (*p)[4];
定义了一个可以指向含有4个元素的一维整型数组的指针变量,也可作为一个指向二维数组的指针变量,它所指向的二维数组的每一行有4个元素
初始化
p = a;
p = &a[0];
引用
p[i][j];
*(p[i]+j);
*(*(p+i)+j);
(*(p+i))[j];
列指针
int *p;
初始化
p = a[0];
p = *a;
p = &a[0][0];
引用
p+i*n+j代表&a[i][j]
p[i*n+j]或*(p+i*n+j)代表a[i][j]
//L11-3
#include <stdio.h>
void InputArray(int *p, int m, int n);
void OutputArray(int *p, int m, int n);
int main()
{
int a[3][4];
printf("Input 3*4 numbers:\n");
InputArray(*a, 3, 4); /* 向函数传递二维数组的第0行第0列的地址 */
OutputArray(*a, 3, 4); /* 向函数传递二维数组的第0行第0列的地址 */
return 0;
}
/* 形参声明为指向二维数组的列指针,输入数组元素值 */
void InputArray(int *p, int m, int n)
{
int i, j;
for(i = 0; i<m; i++)
{
for(j = 0; j<n; j++)
{
scanf("%d", &p[i*n+j]);
}
}
}
/* 形参声明为指向二维数组的列指针,输出数组元素值 */
void OutputArray(int *p, int m, int n)
{
int i, j;
for(i = 0; i<m; i++)
{
for(j = 0; j<n; j++)
{
printf("%4d", p[i*n+j]);
}
printf("\n");
}
}
```c
//运行结果
Input 3*4 numbers:
1 2 3 4 5 6 7 8 9 10 11 12
1 2 3 4
5 6 7 8
9 10 11 12
指针数组:
由若干基类型相同的指针构成的数组,称为指针数组
指针数组中的每个元素都是一个指针,且这些指针指向相同数据类型的变量
因指针数组中的元素是一个指针,所以在使用指针数组之前必须对指针数组元素进行初始化
//L11-4
#include <stdio.h>
#include <string.h>
#define MAX_LEN 10 /* 字符串最大长度 */
#define N 150 /* 字符串个数 */
void SortString(char *ptr[], int n);
int main()
{
int i, n;
char name[N][MAX_LEN]; /* 定义二维字符数组 */
char *pStr[N]; /* 定义字符指针数组 */
printf("How many countries?");
scanf("%d", &n);
getchar(); /* 读走输入缓冲区中的回车符 */
printf("Input their names:\n");
for (i=0; i<n; i++)
{
pStr[i] = name[i]; /* 让pStr[i]指向二维字符数组name的第i行 */
gets(pStr[i]); /* 输入第i个字符串到pStr[i]指向的内存 */
}
SortString(pStr, n); /* 字符串按字典顺序排序 */
printf("Sorted results:\n");
for (i=0; i<n; i++)
{
puts(pStr[i]); /* 输出排序后的n个字符串 */
}
return 0;
}
/*函数功能:用指针数组作函数参数,采用交换法实现字符串按字典顺序排序 */
void SortString(char *ptr[], int n)
{
int i, j;
char *temp = NULL; /* 因交换的是字符串的地址值,故temp定义为指针变量 */
for (i=0; i<n-1; i++)
{
for (j = i+1; j<n; j++)
{
if (strcmp(ptr[j], ptr[i]) < 0) /* 交换指向字符串的指针 */
{
temp = ptr[i];
ptr[i] = ptr[j];
ptr[j] = temp;
}
}
}
}
上述程序中,保存在二维字符数组name中的字符串并没有发生任何变化,即执行SortString()后字符串在其实际物理存储空间中的存放位置并没有改变,但每个指针数组元素所指向的字符串在排序前后发生了变化,即这种排序仅改变了指针数组中各元素的指向
物理排序:通过移动字符串在实际物理存储空间中的存放位置而实现的排序
索引排序:通过移动字符串的索引地址(指针的指向)而实现的排序
相对于使用二维数组实现物理排序的方法而言,使用指针数组实现索引排序的程序执行效率更高一些
指针数组用于表示命令行参数
//L11-5
#include <stdio.h>//该程序的文件名是echo.c,程序编译连接后的可执行文件名为echo.exe
int main(int argc, char *argv[])//带参数的main()函数的定义
//第1个形参argc被声明为整型变量,用于存放命令行中参数的个数
//第2个形参argv被声明为指针数组,用于接收命令行参数
//由于所有的命令行参数都被当做字符串来处理,所以在这里字符指针数组argv的各元素依次指向命令行中的参数
{
int i;
printf("The number of command line arguments is:%d\n", argc);
printf("The program name is:%s\n", argv[0]);
if (argc > 1)
{
printf("The other arguments are following:\n");
for (i=1; i<argc; i++)
{
printf("%s\n", argv[i]);
}
}
return 0;
}
```c
//运行结果
Windows PowerShell
版权所有 (C) Microsoft Corporation。保留所有权利。
PS C:\Users\lenovo> Desktop\echo.exe programming is fun
The number of command line arguments is:4
The program name is:C:\Users\lenovo\Desktop\echo.exe
The other arguments are following:
programming
is
fun
命令行参数很有用,尤其是在批处理命令中使用较为广泛
例如,可通过命令行参数向一个程序传递这个程序所要处理的文件的名字,还可以用来指定命令的选项等
当程序中不需要命令行参数时,一般就在main后面的括号里使用关键字void将函数main()声明为无参数
C程序的内存映像:
- 只读存储区:存放程序的机器代码和字符串常量等只读数据
- 静态存储区:存放程序中的全局变量和静态变量等
- 堆:保存函数调用时的返回地址、函数的形参、局部变量及CPU的当前状态等程序的运行信息
- 栈:自由存储区,程序可利用C的动态内存分配函数来使用它
C语言程序中变量的内存分配方式:
- 从静态存储区分配
- 在栈上分配
- 从堆上分配
指针之所以重要:
- 指针为函数提供修改变量值的手段
- 指针为C的动态内存分配系统提供支持
- 指针为动态数据结构提供支持
- 指针可以改善某些子程序的效率
使用指针定义动态数组是指针除作为函数形参之外的另一个重要应用
动态内存分配是指在程序运行时为变量分配内存的一种方法,适用于在程序运行中需要数量可变的内存空间,即在运行时才能确定要使用多少个字节的内存来存放数据的情况
动态内存分配函数(使用时需要在程序开头将头文件
malloc()
用于分配若干字节的内存空间,返回一个指向该内存首地址的指针
若系统不能提供足够的内存单元,函数将返回空指针NULL
void *malloc(unsigned int size);
void *指针为通用指针或无类型的指针,即声明了一个指针变量,但未指定它可以指向哪一种基类型的数据
因此若要将函数调用的返回值赋予某个指针,应先根据该指针的基类型,用强转的方式将返回的指针值强转为所需的类型
calloc()
用于给若干同一类型的数据项分配连续的存储空间并赋值为0,返回首地址
void *calloc(unsigned int num, unsigned int size);
free()
释放向系统动态申请的由指针p指向的存储空间,无返回值
void free(void *p);
唯一的形参p给出的地址只能是由malloc()何calloc()申请内存时返回的地址
realloc()
用于改变原来分配的存储空间的大小
void *realloc(void *p, unsigned int size);
由于由动态内存分配得到的存储单元是无名的,只能通过指针变量来引用它
所以一旦改变了指针的指向,原来分配的内存及数据也就随之丢失了
因此不要轻易改变该指针变量的值
//L11-6
#include <stdio.h>
#include <stdlib.h>
void InputArray(int *p, int n);
double Average(int *p, int n);
int main()
{
int *p = NULL, n;
double aver;
printf("How many students?");
scanf("%d", &n); /* 输入学生人数 */
p = (int *) malloc(n * sizeof(int)); /* 向系统申请内存 */
if (p == NULL) /* 确保指针使用前是非空指针,当p为空指针时结束程序运行 */
{
printf("No enough memory!\n");
exit(1);
}
printf("Input %d score:", n);
InputArray(p, n); /* 输入学生成绩 */
aver = Average(p, n); /* 计算平均分 */
printf("aver = %.1f\n", aver); /* 输出平均分 */
free(p); /* 释放向系统申请的内存 */
return 0;
}
/* 形参声明为指针变量,输入数组元素值 */
void InputArray(int *p, int n)
{
int i;
for (i=0; i<n; i++)
{
scanf("%d", &p[i]);
}
}
/* 形参声明为指针变量,计算数组元素的平均值 */
double Average(int *p, int n)
{
int i, sum = 0;
for (i=0; i<n; i++)
{
sum = sum + p[i];
}
return (double)sum / n;
}
//L11-7
#include <stdio.h>
#include <stdlib.h>
void InputArray(int *p, int m, int n);
double Average(int *p, int m, int n);
int main()
{
int *p = NULL, m, n;
double aver;
printf("How many classes?");
scanf("%d", &m); /* 输入班级数 */
printf("How many students in a class?");
scanf("%d", &n); /* 输入每班学生人数 */
p = (int *)calloc(m*n, sizeof(int)); /* 向系统申请内存 */
if (p == NULL) /* 确保指针使用前是非空指针,当p为空指针时结束程序运行 */
{
printf("No enough memory!\n");
exit(1);
}
InputArray(p, m, n); /* 输入学生成绩 */
aver = Average(p, m, n); /* 计算平均分 */
printf("aver = %.1f\n", aver); /* 输出平均分 */
free(p); /* 释放向系统申请的内存 */
return 0;
}
/* 形参声明为指向二维数组的列指针,输入数组元素值 */
void InputArray(int *p, int m, int n)
{
int i, j;
for(i = 0; i<m; i++) /* m个班 */
{
printf("Please enter scores of class %d:\n", i+1);
for(j = 0; j<n; j++) /* 每班n个学生 */
{
scanf("%d", &p[i*n+j]);
}
}
}
/* 形参声明为指针变量,计算数组元素的平均值 */
double Average(int *p, int m, int n)
{
int i, j, sum = 0;
for(i = 0; i<m; i++) /* m个班 */
{
for(j = 0; j<n; j++) /* 每班n个学生 */
{
sum = sum + p[i*n+j];
}
}
return (double)sum / (m*n);
}
常见的内存错误及其对策:
非法内存访问
持续的内存泄露导致系统内存不足
内存分配未成功就使用
内存分配成功了,但是未初始化就使用
内存分配成功了,也初始化了,但是发生了越界使用
忘记了释放内存,造成了内存泄露
释放内存后仍然继续使用
野指针:
- 指针操作超越了变量的作用范围
- 指针变量未被初始化
- 指针变量所指向的动态内存被free后未置为NULL
解决对策:
- 不要把局部变量的地址作为函数的返回值返回
- 在定义指针变量的同时对其进行初始化
- 尽量把malloc()集中在函数的入口处,free()集中在函数的出口处
缓冲区溢出攻击:
使用strncpy()、strncat()等“n族”字符串处理函数,通过增加一个参数来限制字符串处理的最大长度,可防止发生缓冲区溢出
只要在使用从函数外部传入的参数之前对其进行检查
- 小心地防止指针越界
- 防止代码访问不该访问的内存
那么抵抗缓冲区溢出攻击并非难事