C语言-第11章-指针

11.0 导入例子

void swap(int x, int y){
  int temp;
  temp = x;
  x = y;
  y = temp;
}
int main(){
  int x = 10, y = 20;
  swap(x, y);
  printf("x = %d, y = %d\n", x, y);
  getchar();
  return 0;
}

在这里插入图片描述
函数章节中已经指出上述代码无法交换main函数中x和y的值,因为swap函数中的x和y与main函数中的x和y不一对不同的变量,swap函数中的x和y交换了值,但是对main函数中的x和y没有影响。怎样定义一个可以交换main函数中x和y的值的函数呢?这就需要用到指针变量,让它们指向x和y,这样这些指针变量就可以直接访问x和y了。

void swap(int *p, int *q){  // p指向x,q指向y,*p就是x的别名,*q就是y的别名
  int temp;
  temp = *p;
  *p = *q;
  *q = temp;
}
int main(){
  int x = 10, y = 20;
  swap(&x, &y); // 将x和y的地址传给函数
  printf("x = %d, y = %d\n", x, y);
  getchar();
  return 0;
}

在这里插入图片描述

11.1 指针基本概念

指针变量用来存储变量的地址,用指针变量可以对变量进行间接访问,相当于给变量起了别名。

11.1.1 变量的地址

每个变量都有一个内存位置,各变量的内存可能有多个字节组成,首字节(地址值最小的字节)的地址称为变量的地址。比如,下图中变量i占的地址为从2000到2003共4个字节,所以变量i的地址是2000。在这里插入图片描述
&称为取地址运算符,可以得到变量的地址:

&变量名

printf输出地址值需要使用转换说明符%p:

#include <stdio.h>

int main ()
{
   int  var1;
   float var2;

   printf("var1 变量的地址: %p\n", &var1  );
   printf("var2 变量的地址: %p\n", &var2  );

   getchar();
   getchar();
   return 0;
}

在这里插入图片描述
注意,每次运行程序变量的地址可能会变化。

注意,不能对表达式取地址,比如

int x = 0;
printf("%p", &(x + 1));  // 编译报错

11.1.2 指针变量

指针变量是专门存储变量地址值的一类变量。需要根据预计存储的地址值是属于哪种类型的变量,而声明该类型的指针变量。比如,声明存储整型变量地址的int指针变量,

int *p;

声明存储双精度浮点类型变量地址的double指针变量,

double *q;

指针变量可以和其它变量一起出现在声明中:

int i, j, a[10], *p;

int类型的指针变量可以也只能保存某个int变量的地址:

int i, *p;
...
p = &i;

p存储了i的地址,也可以说p指向了i。输出指针变量的值同样使用转换说明%p。

...
printf("%p\n", p);

在这里插入图片描述
也可以在声明指针变量的同时对它初始化

int i;
int *p = &i;

甚至可以把i的声明和p的声明合并,但是需要先声明i:

int i, *p = &i;

一旦p指向了某个变量,比如i,那么就可以使用间接寻址(引用)运算符*来访问那个变量(存储在变量中的值)。

int i, *p = &i;
i = 10;
printf("%d\n", *p);  // 显示10

实际上,只要p指向i,那么*p就是i的别名(小名),*p和i是等价的“变量名”。
在这里插入图片描述

int i, *p = &i; // p指向了i,*p是i的别名
i = 10;
printf("%d\n", *p);  // 显示10
i = 20;
printf("%d\n", *p);  // 显示20
*p = 30;
printf("%d\n", i);  // 显示30

注意,C语言要求每个指针变量只能指向一种特定类型的变量:

int *p;  // 只能指向int类型变量
double *q;  // 只能指向double类型变量
char *r;  // 只能指向char类型变量

可能有人会说不都是变量的地址吗,这些地址值都是整数,为什么不能用int整型变量来保存各种类型变量的地址呢?首先地址范围和整型范围可能是不同的,其次,更重要的是指针变量最重要的作用是通过地址访问它指向的变量,因为不同类型的变量有不同的处理机制,所以指针变量要知道它指向的变量的类型,故指针变量本身是有类型的。

下面代码是在声明指针变量p的同时给它赋初值

int i;
int *p = &i;

相当于

int i, *p;
p = &i;

在p已经被声明过后,并且p已经指向了某个变量后,*p就是那个变量的别名。再出现类似的赋值就是一种含义上的错误。

int i, j;
int *p = &i;
*p = &j;  // 含义上的错误
*p = 10;  // 等价于将10赋值给i

注意,下面声明

int *p, q;

声明了指向整型变量的指针变量p和普通整型变量q。

int* p, q;

如果把*和int放在一起,声明的两个变量p和q,仍然p是指针变量,q是整型变量。因为本质上*是用来修饰p的,指针变量的声明

int *p;

不论*是紧邻nt还是p,都应该被解读为,*说明p是指针变量,而前面的int说明p是用来指向int变量的。这与数组的声明有点类似,

int a[10];

如果要声明两个指针变量,需要在每个指针变量前放置*

int *p, *q;

11.1.3 指针变量的赋值

如果两个指针变量类型相同,可以相互赋值。假设声明如下,

int i, j, *p = &i, *q;

赋值语句

q = p;

将p的值(i的地址)赋值给q,效果是q和p同指向i
在这里插入图片描述
由于p和q都指向i,可以通过p和q来访问i

*p = 1;

// 图

*q = 2;

在这里插入图片描述
不要把

q = p;

*q = *p;

混淆了。第一句话是指针赋值,第二句话不是,它是指针引用的变量之间的赋值。比如,

p = &i;
q = &j;
i = 1;

在这里插入图片描述

*q = *p;

在这里插入图片描述
上述赋值相当于

j = i;

11.1.4 用NULL地址来安全的使用指针变量

就像普通变量没有赋初值的时候不能访问(读取它的值),以及访问数组元素时下标不能越界(访问了未申请的地方),指针变量也是这样,

int *p;
printf("%d\n", *p);

因为p还没有初始化,它的值可能是乱七八糟的值,或者说它指向了某个未知的地方,对这个地方的访问会导致未定义的行为。
如果未初始化的p的值恰好是有效的内存地址,下面的赋值会试图修改存储在该地址的数据

int *p;
*p = 1;  // 错误

对内存中未知位置的修改是危险的,可能会导致未定义的行为或者系统崩溃。只有让指针指向某个声明了的变量后,对p的间接引用才是安全的,因为此时实际上是对那个变量的引用。
为了安全的访问指针变量,在没有明确p应该指向哪个变量之前(对p进行实际的初始化),可以用地址值NULL来初始化指针变量

int *p = NULL;

NULL 指针是定义在头文件stdio.h中的值为零的常量。

#define NULL    0  // 在stdio.h头文件中

在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。如果指针变量的值为0则明确的表明该指针不指向一个有效的(可访问的)内存位置,或者说它还没有指向某个变量,不能对它进行访问(使用间接寻址运算符*),如果对它访问,则系统一定检测出异常。

#include <stdio.h>

int main ()
{
   int  *ptr = NULL;
   printf("ptr 的地址是 %p\n", ptr  );
   
   *ptr = 1;  // 导致程序检测出异常
   
   getchar();
   return 0;
}

在这里插入图片描述
这比只声明指针变量p,但是没有对它初始化,即让它的值是完全未知的好,因为万一未初始化的p的值恰好指向有效的内存地址,上面的赋值语句会试图修改存储在该地址的数据。而此时系统可能检查不出来这样的问题。这是程序很大的隐患。

为了进一步提高程序的安全性,还可以在使用指针变量前,判断它的值是否为空:

int *p = NULL;
...  // 在使用p之前,应该让p指向某个整型变量
if (p != NULL){
  // p已经指向了某个变量,可以使用*p了
}else{
  // p没有指向某个变量,不能使用*p
}

11.1.4 常见问题

问题1. 只能给变量或者说“左值”取地址,表达式不可以。

int i = 0, *p;
p = &i;  // 正确,i是变量,即左值,有明确的地址
p = &(i + 10);  // 错误,i+10是表达式,不属于“左值”,不能对它取地址

问题2. 指针变量还未初始化就访问

int *p;
*p = 10;  // 错误,p还没有指向变量之前,不能对它间接引用

问题3. 指针是有类型的,一种类型的指针变量只能指向同种类型的变量

int i, *p;
double j;
p = &i;  // 正确
p = &j;  // 错误

11.2 传递指针给函数

传递实际参数变量的地址给函数的指针形式参数,在函数中可以修改实际参数变量的值。


C语言调用函数时,传的是实际参数变量的值(如果传的是变量而不是表达式的话),而形式参数变量和实际参数变量实际上是不同的变量,所以在函数内对形式参数的修改对实际参数没有影响。那怎样在函数内修改实际参数变量的值呢?一种方法是将实际参数的地址传给函数,比如setZero函数

void setZero(int *p){
  printf("p == %p\n", p);
  *p = 0;
}
int main(){
  int x = 10;
  printf("&x == %p\n", &x);
  setZero(&x);
  printf("%d\n", x);
  getchar();
  return 0;
}

通过setZero函数的处理,main函数中实际参数x的值修改为0了。函数调用时也仍然是传值,但是传的是实际参数x的地址值。形式参数p的存储期限仍然是自动的,即setZero调用时p被创建,调用结束p被销毁,但是由于把x的地址值传给了p,让p指向了x,那么*p就是x的别名,*p赋值为0,相当于把x赋值为0。
在这里插入图片描述
这也是调用scanf函数需要在变量名前面加取地址运算符的原因

scanf("%d", &x);

scanf函数需要修改实际参数变量x的值,所以它必须得到它的地址。否则,它无法修改x的值。

如果指针变量p指向了x,那么将p传给scanf函数和传x的地址是一样的。

int x, *p = &x;
scanf("%d", p);

类似的,可以定义交换实际参数变量值的函数swap,

void swap(int *p, int *q){
  int temp;
  temp = *p;
  *p = *q;
  *q = temp;
}

int main(){
  int x = 10, y = 20;
  swap(&x, &y);
  printf("x = %d, y = %d\n", x, y);
  getchar();
  return 0;
}

因为函数从语法来说最多有一个返回值,如果函数需要返回多个结果就可以利用指针参数。

比如,找两个数最大值和最小值都是分成两个函数的可以定义成一个函数同时找到最大和最小值,

#include <stdio.h>

void max_min(int x, int y, int *p_max, int *p_min);

int main(){
  int x = 10, y = 20, max_value, min_value;
  max_min(x, y, &max_value, &min_value);
  printf("max = %d, min = %d\n", max_value, min_value);
  getchar();
  return 0;
}

void max_min(int x, int y, int *p_max, int *p_min){
  if (x > y){
	  *p_max = x;
	  *p_min = y;
  }else{
	  *p_max = y;
	  *p_min = x;
  }
}

比如,函数decompose可以返回输入浮点数的整数和小数部分,由于返回两个结果,所以该函数声明了两个指针形式参数。

#include <stdio.h>

void decompose(double x, long long *p_int_part, double *p_frac_part);

int main(){
  double x, frac_part;
  long long int_part;
  scanf("%lf", &x);
  decompose(x, &int_part, &frac_part);
  printf("int part = %lld, frac part = %f\n", int_part, frac_part);
  getchar();
  getchar();
  return 0;
}

void decompose(double x, long long *p_int_part, double *p_frac_part){
  *p_int_part = (long long)x;
  *p_frac_part = x - *p_int_part;
}

11.3 从函数返回指针

函数可以返回变量的地址值,即指针


不仅可以为向函数传递指针,还可以编写返回指针的函数。比如,找两个数最大值的函数,可以直接返回指针,指向大的实际参数

#include <stdio.h>

int *max(int *a, int *b){
  if (*a > *b){
    return a;
  }else{
    return b;
  }
}

int main(){
  int x, y, *p_max;

  scanf("%d%d", &x, &y);

  p_max = max(&x, &y);

  printf("%d和%d中大的那个是%d\n", x, y, *p_max);

  getchar();
  getchar();
  return 0;
}

调用函数时,将x和y的地址分别传给a和b指针,那么*a是x的别名,*b是y的别名,调用结束,返回a和b中指向值大的那个,即或者是x的地址,或者是y的地址。因为返回地址,所以要以指针变量接收。

由于函数调用结束,其中的局部变量会被回收,所以注意不要返回指向自动局部变量的指针:

int *f(void){
  int i;
  ...
  return &i;
}

随着f的调用,局部变量i会被创建出来,但是随着f的结束,i会被回收,换句说话,i就不存在了,所以返回i的地址是无效的地址。

11.4 指针与数组

数组的各元素在内存中是连续存储的,可以让指针指向数组的首元素,然后利用指针的算术运算来遍历数组的各个元素。

int a[5] = {1,2,3,4,5};
int *p = &a[0];
for(i = 0; i < 5; i++){
  printf("%d ", *(p + i));
}

11.4.1 指针的算术运算

指针的算术运算有3种:

  • 指针加上整数
  • 指针减去整数
  • 两个指针相减

指针还支持关系运算和比较运算

  • 关系运算符<、<=、>、>=
  • 判等运算符==、!=

对指针变量进行算术运算需要不超出数组范围才有意义,对两个指针变量进行关系和比较运算需要它们指向同一个数组才有意义。


数组元素实质上就是变量,可以让指针变量指向数组元素。

int a[5], *p;

可以让p指向a[0]

p = &a[0];

由于数组名表示数组的首元素地址,上式可以写为

p = a;

在这里插入图片描述
可以借助于p,修改数组首元素的值

*p = 5;

在这里插入图片描述
指针的算术运算有3种:

  • 指针加上整数
  • 指针减去整数
  • 两个指针相减

指针的这3种算术运算一般都是与数组相关的,即对指向数组某个元素的指针进行偏移操作、以及计算指向数组元素指针的相对位置的运算。
指针p加上整数j,会得到指针值p原先指向的元素的后的j个位置。也可以从下标方面来理解。如果p指向数组元素a[i],那么p+j指向数组元素a[i + j]。例如,

p = &a[2];
q = p + 3;

在这里插入图片描述
实际上,假设p是指向类型为int的指针,比如地址值是1000,那么p+1的地址并不是1001,而是1004,即p + sizeof(int),p+j的地址是p + j * sizeof(int)。再比如,假设q指向double类型数组d的首元素,输出q和q+1的地址值

double d[10], *q = &d[0];
printf("q == %p, q+1 == %p\n", q, q + 1);
printf("&d[0] == %p, &d[1] == %p\n", &d[0], &d[1]);

在这里插入图片描述
类似的,p减去整数j,得到p前面j个元素的位置。也可以理解为,假设p指向下标为i的数组元素,那么p - j,指向下标为i - j的数组元素。

p = &a[7];
q = p - 4;

在这里插入图片描述
两个指针变量还可以进行相减和比较大小的运算,不过只有在它们指向同一个数组这些运算才有意义。

两个指针变量相减得到它们之间间隔几个数组元素,或者理解为下标的相减。比如p指向a[i],q指向a[j],那么p-q等于i - j。

p = &a[6];
q = &a[3];
i = p - q;  // i是3
j = q - p;  // j是-3

可以理解为,p - q 等于 (p的值-q的值) / sizeof(数组指向的元素)。
在这里插入图片描述
关系运算符<、<=、>、>=,判等运算符==、!=在两个指针变量指向同一个数组才有意义,关系运算符比较它们的相对位置。比如,如果,

p = &a[5];
q = &a[1];

那么p < q是假,p > q是真。

11.4.2 通过指针变量处理数组

借助于指针变量算术运算的特性,可以用指针变量来处理数组,比如

#define N 10
...
int a[N], sum, *p;
...
sum = 0;
for(p = &a[0]; p < &a[N]; p++){
  sum += *p;
}

在这里插入图片描述
这里需要注意的是a[N]不是合法的数组元素,但是对它使用取地址运算符是允许的。上述代码或者写成

#define N 10
...
int a[N], sum, *p;
...
sum = 0;
for(p = &a[0]; p <= &a[N - 1]; p++){
  sum += *p;
}

由于数组名表示数组的首元素地址,用指针变量遍历数组还可以写成

for(p = a; p < a + N; p++){
  sum += *p;
}

或者,还可以写成

for(i = 0; i < N; i++){
  sum += *(a + i);
}

实际上,a + i等同于&a[i],*(a + i)等同于a[i]。换句话说,可以把数组的取下标操作看成是先对数组首元素地址进行算术运算再进行间接寻址运算,即a[i]~*(a + i)。所以,还可以写成

for(p = a, i = 0; i < N; i++){
  sum += p[i];  // p[i]相当于*(p + i)
}

注意,数组名是地址常量,所以不能改变,不能像指针变量那样出现

a++;

对数组的输入和反向输出可以写为

#include <stdio.h>

#define N 10

int main(){
  int a[N], *p;
  
  printf("请输入%d个整数:", N);
  for(p = a; p < a + N; p++){
    scanf("%d", p);
  }

  printf("反向输出数组各元素:");
  for(p = a + N - 1; p >= a; p--){
    printf(" %d", *p);
  }
  printf("\n");
  
  getchar();
  return 0;
}

11.4.3 数组形式参数实际上是指针参数

借助于数组运算,我们可以用指针变量作为形式参数来处理数组实际参数

int sum_array(int *a, int n){
  int i, sum = 0;
  for(i = 0; i < n; i++){
    sum += *(a + i); // 或者写为 sum += a[i];
  }
  return sum;
}
int main(){
  int a[] = {1,2,3,4,5,6,7,8,9,10};
  printf("前%d个元素的和是%d\n", 5, sum_array(a, 5));      
  getchar();
  return 0;
}

实际上,函数的数组形式参数本身不是数组类型,就是当做指针类型来处理的

int sum_array(int a[], int n){  // a表面上是数组类型,实际上是指针类型int *a
  int i, sum = 0;
  for(i = 0; i < n; i++){
    sum += a[i];  // 或者写为sum += *(a + i);
  }
  return sum;
}

在这里插入图片描述
实际上,在C语言中,函数调用普通变量传递时,函数的形式参数类似于实际参数的“副本”,拷贝了实际参数的值。如果是数组参数,如果像普通变量传递的那样,那应该建立实际数组的“副本”-形式参数数组,并把整个数组的各个元素值复制过去。但是,C语言的形式参数的数组本质上不是数组类型,而是指针类型。函数调用时仅仅传过去数组首元素的地址值,函数中利用数组的算术运算的特性来访问实际参数数组。这样处理的好处是函数调用时不用建立数组实际参数的“副本”,在速度和内存上都更高效。这也解释了之前遇到过的两个现象:

现象1. 用sizeof计算形式参数数组的大小并不是数组大小。因为它实际上是指针类型。

#include <stdio.h>
void f(int a[], int n){
  printf("in function, sizeof(a) = %d\n", sizeof(a));
}
int main(){
  int a[10];
  printf("in main, sizeof(a) = %d\n", sizeof(a));
  f(a, 10);
  getchar();
  return 0;
}

在这里插入图片描述
现象2. 对形式参数数组的修改实际上就是对实际数组参数的修改。

#include <stdio.h>
void set_zeros(int a[], int n){
  int i;
  for(i = 0; i < n; i++){
    a[i] = 0;
  }
}
int main(){
  int array[] = {1,2,3,4,5,6,7,8,9,10},i;
  set_zeros(array, 5);
  for(i = 0; i < 10; i++){
	  printf("%d ", array[i]);
  }
  getchar();
  return 0;
}

在这里插入图片描述
注意,正因为数组参数实际上是指针参数,那么要处理的数组元素数量需要用另一个参数传递到函数中,否则,函数内部不知道要处理的数组元素个数。这是向函数传数组还会传一个整数的原因。

int sum_array(int *a, int n);

这个情况有特例,假如需要传的是字符数组,由于字符数组中存储字符串以空字符作为结束字符,故函数中只要在遍历字符数组各元素时判断当前数组元素是否为空字符就知道数组中存储的有效元素是否结束。这也是为什么处理字符数组的函数只需要一个数组参数。

/*查找字符数组中字符c出现的次数*/
int find_char(char *p, char c){
  int i, count;
  for(i = 0, count = 0; p[i] != '\0'; i++){
    if (p[i] == c){
      count++;
    }
  }
  return count;
}

11.4.4 const修饰符

const修饰符用来修饰变量,表示这些变量是“只读”的,即常量。程序可以访问const型对象的值,但无法改变它的值。比如,下面声明为const的整型变量x是“只读”的,

const int x = 10;
x = 20; // 编译报错

声明const数组,

const int arr[] = {1,2,3};
arr[0] = 15;  // 编译报错

编译器可以帮助我们检查const修饰符修饰的变量有没有被试图修改,如果有就会报错,这避免了我们无意中修改const变量。由于const修饰的变量在声明之后是无法修改值的,故必须在声明的同时给它指定初值,即必须初始化它。

const修饰符的作用和宏定义#define比较类似,但是它们也有一定的区别,#define为数值、字符字面量或者字符串字面量创建名字。const可用于产生任何类型的只读对象,包括数组、指针等等。

const修饰的本质上还是变量,需要符合和变量相同的作用域规则。比如,在一个函数中的const常量不能在别的函数中访问。而用#definec创建的常量不受这些规则的限制。

const修饰的整型变量还是不可以用作声明数组时的长度

const int n = 10;
int a[n];  // 错误

没有绝对的原则说明何时使用#define以及何时使用const。这里建议对表示数或字符的常量使用#define。这样就可以把这些常量作为数组维数,并且在switch语句或其它要求常量表达式的地方使用它们。

const可以用来修饰指针变量,由于指针变量的特殊性,有两种不同的写法,表示不同的含义。

含义1. 常量指针

const int *p;

const修饰p指向的变量,表示p指向的对象是常量,是“只读”的,如果编译器检测到有修改的代码,就会报错。同时,p本身不是常量,可以指向相同类型的其它变量。比如,

int a, b;
const int *p;
p = &a;
*p = 20;  // 错误
p = &b; // 允许 

含义2. 指针常量

int *const p = ...;

const修饰符写在*和指针变量名之间,表示p本身是常量,p存了某个变量的地址值后就不能修改了,p会始终指向同一个变量。但是那个变量本身的值是可以修改的。

int a, b;
int *const p = &a;
*p = 20;  // 允许
p = &b; // 错误 

常量指针的一个作用是用来修饰数组参数。上一节提到数组形式参数本质上是指针参数,而对数组形式参数的修改就是对数组实际参数的修改。但是并不希望接收数组参数的函数修改数组实际参数,比如,求数组元素和的函数,

int sum_array(int a[], int n){
  int i, sum = 0;
  for(i = 0; i < n; i++){
    sum += *(a + i); // 或者写为 sum += a[i];
  }
  return sum;
}

或者,直接声明指针形式参数

int sum_array(int *a, int n){
  int i, sum = 0;
  for(i = 0; i < n; i++){
    sum += *(a + i); // 或者写为 sum += a[i];
  }
  return sum;
}

显然,sum_array函数的功能是求数组中若干元素的和,它不应该修改数组元素,为了确保函数中不会误修改,可以在定义函数参数变量的时候加上const修饰符

int sum_array(const int a[], int n){
  int i, sum = 0;
  for(i = 0; i < n; i++){
    sum += *(a + i); // 或者写为 sum += a[i];
  }
  return sum;
}

或者

int sum_array(const int *a, int n){
  int i, sum = 0;
  for(i = 0; i < n; i++){
    sum += *(a + i); // 或者写为 sum += a[i];
  }
  return sum;
}

11.5 指针和字符串

来看下比较类似的声明。

char date[]="October 1";
char *p_date = "October 1";

在数组章节,我们知道字符数组可以被视为存储字符串的“变量”。而字符指针也可以指向字符串,第二条语句的含义是为字符串字面量“October 1”分配空间,并让字符指针变量p_date指向该字符串字面量的首元素。字符串字面量被以字符数组的形式存储起来(最后含有空字符)。比如,"October 1"字符串字面量被存储为
在这里插入图片描述
只是它被存储在了常量区域,它里面的内容不能修改。
数组名本质上就是表示数组首元素的地址常量,而p_date现在又存储了字符串常量数组的首元素地址,故它可以被当成字符数组来处理。比如,

char date[]="October 1";
char *p_date = "October 1";
printf("%s\n", date);
printf("%s\n", p_date);

但是,这两个“变量”还是有所区别的,

  • date是字符数组,是真正的存储字符串的“变量”,里面存储的字符串的内容可以修改。但是date本身却是地址常量,不能修改它的值。
    char date[]="October 1";
    char *p_date = "October 1";
    date[2] = 't'; // 允许
    date = p_date; // 不允许,date是表示数组首元素地址的常量,值不能被修改
    
  • p_date是字符指针,它可以指向不同的字符数组的首元素地址,借此在需要字符串常量的地方就可以使用它。但是,它既可能指向字符数组,也可能指向字符串字面量,如果指向字符串字面量,那么由于不能修改字符串字面量中的内容,故也不能通过字符指针来修改。
    char date[]="October 1";
    char *p_date = "October 1";
    p_data[0] = 'a';  // 不允许,p_date指向的是字符串字面量,字符串字母量里面的内容不能修改
    p_date = date;  // 允许
    p_date[0] = 'a';  // 允许,现在p_date指向了可以修改内容的字符数组date。date数组的内容变为了"actober 1"
    

这样就造成字符指针使用的困扰,不过可以在指向字符串字面量的字符指针前加const修饰符,表示不能修改字符指针指向的内容,指针是常量指针,即是指向常量的指针。这样如果编译器检查到了修改的代码就会报错。

const char *p_date = "October 1";
p_data[0] = 'a';  // 编译就报错

使用字符指针另一个常见的错误是在字符指针尚未指向字符数组就直接使用它

char *p;

p[0] = 'a';
p[1] = 'b';
p[2] = 'c';
p[3] = '\0';

上面这段代码中,程序只是为指针变量p本身分配了空间,还没有为字符数组分配空间,故不能这样写,否则程序运行时会崩溃。正确的写法如下,

char *p;
char str[10];
p = str;
p[0] = 'a';
p[1] = 'b';
p[2] = 'c';
p[3] = '\0';

11.6 字符串数组

存储多个字符串用什么呢?一种方案是创建二维字符数组,然后按照每行一个字符串的方式把字符串存储到数组中。比如,

char planets[][8] = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"};

上面声明了二维字符数组,省略了行数(从初始化式子元素的数量可以求出),必须指明列数。其在内存中如下所示,
在这里插入图片描述
可见,用二维字符数组存储字符串常量,当常量的内容大小不一致时还是比较浪费内存的。此时可以考虑用指向字符串的指针数组来存储:

char *planets[] = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"};

此时,planets数组变成了一维数组,每个元素指向了字符串常量
在这里插入图片描述
虽然为planets数组每个指针元素分配空间,但是字符串中不再浪费字符。
为了访问某个字符串,只需要对数组取下标。由于指针和数组之间的紧密关系,访问行星名字中的字符的方式和访问二维数组元素的方式相同。例如,为了在planets数组中搜寻以字母M开头的字符串,可以使用下面的循环,

for(i = 0; i < 9; i++){
  if (planets[i][0] == 'M'){
    printf("%s\n", planets[i]);
  }
}

11.7 指针和二维数组(不考)

11.7.1 用指针遍历二维数组的各元素

假设已经定义了宏NUM_ROWS和NUM_COLS

#define NUM_ROWS 3
#define NUM_COLS 5

以及二维数组声明如下

int m[NUM_ROWS][NUM_COLS];

当然可以用二重循环来处理二维数组的各元素

int row, col;
...
for(row = 0; row < NUM_ROWS; row++){
  for(col = 0; i col < NUM_COLS; col++){
    m[row][col] = 0;
  }
}

由于多维数组的各元素是按照行连续分布在内存中的,即先存储第0行的全部元素,接着第1行的,以此类推:
在这里插入图片描述
所以可以使用一个指针变量遍历二维数组的各元素

int *p;
for(p = &m[0][0]; p <= &m[NUM_ROWS - 1][NUM_COLS - 1]; p++){
  *p  = 0;
}

显然,这样的处理方式基本上就是把二维数组当成一维数组来处理了。

11.7.2 处理二维数组的行

由于二维数组的每一行都可以看成是一维数组,故处理二维数组的行比较简单,比如,将二维数组m的下标为i的行的各元素置为0,

int *p;
for(p = &m[i][0]; p < &m[i][0] + NUM_COLS; p++){
  *p = 0;
}

由于在C语言中,二维数组被看成是元素是一维数组的一维数组,所以二维数组m,被视为由一维数组m组成,m的各元素是m[0],m[1],…,m[NUM_ROWS - 1],而这些数组元素又是包含了NUM_COLS个元素的一维数组。对于下标为i的行来说,m[i]表示这一行的一维数组名,它包含了m[i][0],m[i][1], …,m[i][NUM_COLS - 1]个元素。

在这里插入图片描述
之前说明过,数组名又表示数组首元素的地址,故m[i]等价于&m[i][0]。

int *p;
for(p = m[i]; p < m[i] + NUM_COLS; p++){
  *p = 0;
}

11.7.3 处理二维数组的列(数组指针)

由于二维数组是以行存储的,所以处理二维数组的列比较麻烦。需要声明数组指针,即指向二维数组中构成行的一维数组的指针变量。

int (*p)[NUM_COLS];
for(p = &m[0]; p < &m[NUM_ROWS]; p++){
  (*p)[i] = 0;
}

比如,下面的代码输出二维数组的前3列

#include <stdio.h>

#define NUM_ROWS 3
#define NUM_COLS 5

int main(){
	int m[NUM_ROWS][NUM_COLS] = {{1,0,0,1,1},{0,1,0,2,2},{0,0,1,3,3}};

	int (*p)[NUM_COLS], col = 0;

	for(p = &m[0] ;p < &m[NUM_ROWS] ; p++){
		printf("%d ", (*p)[col]);
	}
	printf("\n");

	col++;
	for(p = &m[0] ;p < &m[NUM_ROWS] ; p++){
		printf("%d ", (*p)[col]);
	}
	printf("\n");

	col++;
	for(p = &m[0] ;p < &m[NUM_ROWS] ; p++){
		printf("%d ", (*p)[col]);
	}
	printf("\n");

	getchar();
	return 0;
}

这里把p声明为指向长度为NUM_COLS的整型数组的指针。在(*p)[NUM_COLS]中,*p是需要使用括号的,如果没有括号,编译器将认为p是指针数组,而不是指向数组的指针。表达式p++把p移到下一行的开始位置。在表达式(*p)[i]中,*p代表m的一整行,因此(*p)[i]选中了该行第i列的那个元素。

for语句第一部分p = &m[0]可能是让人迷惑的地方,p是指向长度为NUM_COLS的整型数组的指针,而m[0]就是这样一个整型数组(C将二维数组看成是一维数组,由m[0],m[1],…,m[NUM_ROWS-1]组成,只是这些数组元素本身还是一维数组),按照指针指向变量的规律,需要将变量的地址赋值给相应的指针变量

int x, *t;
t = &x;

故,这里应该写为p = &m[0]。
注意,这里不能用p = m[0]替代p = &m[0],否则编译器会给出警告。

int (*p)[NUM_COLS];
for(p = m[0] /*错误*/; p < &m[NUM_ROWS]; p++){  
  (*p)[i] = 0;
}

在这里插入图片描述
尽管m[0]和&m[0]的值相等(都是m[0][0]这个元素在内存中的地址),但是它们的含义却不相同。
因为m[0]本身等价于&m[0][0],它表示m[0]这一行的一维数组的首元素地址,数组的各元素是m[0][0],m[0][1],…,m[0][NUM_COLS-1]。由于数组元素是整型,故它应该被赋值给指向整型变量的指针。

int *t;
t = m[0]; // 或者写为 t = &m[0][0];
t++; // t自增1后指向m[0][1]

而&m[0]表示m作为数组名的数组的首元素地址,即数组元素是m[0],m[1],…,m[NUM_ROWS-1]的数组,这个数组的各元素本身是长度为NUM_COLS的一维数组,故&m[0]应该赋值给指向长度为NUM_COLS的数组的指针变量,这样类型才匹配。

int (*p)[NUM_COLS];
p = &m[0];  // 或者写为p = m;
p++;  // p自增1后指向m[1],即下一行的行一维数组

在C语言中,数组名始终等价于数组首元素的地址,所以m等价于&m[0]。(尽管m是二维数组),故遍历二维数组列的代码还可以写成

int (*p)[NUM_COLS];
for(p = m; p < m + NUM_ROWS; p++){
  (*p)[i] = 0;
}

由于*(p + i)相当于p[i],故甚至可以将p当成二维数组名那样使用

int m[NUM_ROWS][NUM_COLS], (*p)[NUM_COLS];
p = &m[0]; // 或者写为p = m;
for(i = 0; i < NUM_ROWS; i++){
  for(j = 0; j < NUM_COLS; j++){
    p[i][j] = 0; // 或者写为 (*(p + i))[j] = 0;
  }
}

实际上,作为二维数组形式参数的类型本质上就是数组指针,

#include <stdio.h>

void test(int m[][5], int n){ // 实际上m的类型是:int (*m)[5]
	printf("sizeof(m) in test:%d\n", sizeof(m));
}

int main(){
  int m[3][5];

  printf("sizeof(m) in main:%d\n", sizeof(m));

  test(m, 3);

  getchar();
  return 0;
}

在这里插入图片描述

11.8 指针的高级应用 (不考)

11.8.1 命令行参数

命令行参数

11.8.2 动态内存分配

C语言的动态内存分配

11.8.3 指向指针的指针

指向指针的指针

11.8.4 指向函数的指针

函数指针

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值