C语言中的指针

鉴于以后可能会用到 C 语言进行开发,故复习一下 C 语言中的指针

0x00 指针

指针的定义是存储地址值的一类变量,用 * 来声明

int *p; // 定义了一个 int 类型的指针变量,这时候 p 指针还未被显式赋值,他随机指向系统中的任何地址,这时候被称作“野”指针
		// 这个时候不应对其进行任何的存取操作,否则轻则程序崩溃,重则系统崩溃

指针的用法说简单也简单,说难也难,难就难在怎么去理解地址与变量之间的转换,举个简单的例子

int *pa,pb; // C 语言中,指针变量的定义必须用 * 来显式定义,这句语句其实定义了两个类型的变量:
			// 一个 int 类型的指针变量 pa 和一个 int 类型的变量 pb
pa = &pb; // 让指针指向 pb
*pa = 6; // 等于 pb = 6 ;
char *pc,*pd,pe,pf; // 这里定义了两个 char 类型的指针变量,但是是“野”指针
pa = pc // 赋值失败,虽然同为指针变量,但是指针类型不一样
pc=&pe; // 赋值成功,指针 pc 变量指向 pe
*pd=&pf; // 赋值失败,指针变量是 pd ,而不是 *pd 
char *ph=&pb; // 赋值成功,这种赋值方式只能在指针变量被定义的时候进行赋值

0x01 数组和指针

int *pa,*pb,a[10],b[10][10];
pa = a; // 相当于 p = &a[0],是因为数组名其实是数组的首地址
// 此时访问数组元素的方式
// 直接访问
a[2];
// 间接访问
*(a + 1); // 等于 a[1] , 在 C 语言中指针加一代表着内存空间中加上一个该指针变量类型所占字节大小的偏移量
*(pa + 1); // 如上

pb = b;
b[1][2];
*(*(a + 1) + 2);
*(*(pb + 1) + 2);

实例:

#include <stdio.h>
#define N 10

int main(void){
    int *p,a[N],i;
    p = a; // p 指向 a[0]
    
    printf("Input the array:\n");
    
    for (i = 0;i < N;i++)
        scanf("%d",p++); // 循环录入
    
    printf("the array is:\n");
    
    for (p = a;p<a+N;p++)
        printf("%d\t",*p); // 利用 *(p + i) 的方式间接访问数组元素
    
    return 0;
}

二维数组类似

0x02 指针数组

顾名思义,数组的元素是同类型的指针,他的主要用途是拿来处理字符串,在 C 语言中,一个字符串变量代表返回的是字符串首字母的地址,即指向该字符串首字符的指针常量,将若干个字符串常量作为指针数组的各个元素,就可以通过操作数组元素来操作字符串常量,例如:

char *a[] = {"if","not","null"};
int i;
for (i = 0;i<3;i++){
    puts(a[i]); // 输出指针变量中存放地址指向的字符串
}

上面的这种方式需要事先知道数组元素的数量。更加简单的做法是在数组的最后一个元素过后加入一个 NULL 值, NULL 在 C 语言中不是关键字,而是一个宏定义:

#define NULL ((void*) 0)

NULL 在多个头文件中有着定义:stdlib.h、stdio.h、string.h、stddef.h等,只要包含这些头文件,都可以直接使用 NULL 值,那么上面的访问代码可以写成:

#include <stdio.h>
#include <stdlib.h>

int main(void){
    char *a[] = {"if","not","null",NULL};    
    int i;
    char *b = "sdfafs";
    printf("%s\n",b + 1);
    puts(*a);
    for(i = 0;a[i]!=NULL;i++){
       puts((a[i] + 1));
    }
}

// dfafs
// if
// f
// ot
// ull

0x03 字符指针

字符串和字符数组名均表示指针常量,其本身的值不能修改。如下语句均是错误的:

char c = "xyz";
c++; //错误。字符数组名c为常量
"xyz"++; //错误。字符串表示指针常量,其值不能修改
*("xyz"+1)='d'; //运行时错误。试图把y,变为d

字符串变量的指针只能进行读取操作,不能进行修改指针所指向的字符串的值

char *pc; //正确,未初始化,随机指向
pc="abcd"; //正确,让 pc 指向串 "abcd"
pc="hello"; //正确,修改pc指向,让其指向串"hello"
*(pc+4) = 'p'; //运行时错误。试图把'o'字符改变为'p'
strcpy(pc,"xyz");//运行时错误。企图把另一个串复制到pc空间

字符数组和字符指针使用示例:

char str[]='I Like C Language!";
char *ps=str;//初始指向字符串首字符"I"
*(ps+3)='o';//'i'->'o'
*(ps+4)='v';//'k'->'v'
ps=str+2;//ps指向'L'字符
puts (ps);//输出 Love C Language!并换行

0x04 字符数组的使用注意事项

数组访问越界的问题

char c[]="Nan Jing", *pc=c+4; //c 大小:9,pc 指向'J'
c[10] ='!'; //错误。没有c[10]元素,越界存储,编译器不检查数组是否越界
*(pc+6)='!'; //错误。越界存,pc+6 等价于 c[10]
putchar (c[9]) ; //错误,越界取,值不确定

数组结束符 "/0"

一般把字符串存放于字符数组中时,一定要存储字符串结束符 '\0',因为 C 库函数中,对字符串处理的函数,几乎都是把 '\0' 作为字符串结束标志的。如果字符数组中没有存储结束符,却使用了字符串处理函数,因为这些函数会寻找结束符 '\0',可能会产生意想不到的结果,甚至程序崩溃。例如:

char s1[5]="hello"; //s1不含'\0'
char s2[] = {'w','o','r','l','d'}; //s2大小:5,不含'\0'
char s3[5]; //未初始化,5个空间全为不确定值

s3[0] ='g';
s3[1]='o'; // 此时 s3 = {'g','o',?,?,?}
puts(s2); //s2中不含'\0',输出不确定值,甚至程序崩溃.
strcpy (s1, s3) ; //运行时错误。s3中找不到结束符'\0'

char s4[5] = {'g','o'}; // s4 = {'g','o','\0','\0','\0'}
puts (s4); //输出go并换行
strcpy (s1,s4); //把 s4 中的串 go 和一个'\o'复制到 s1 中。
				// 此时 s4 = {'g','o','\0',l,o}
				// 这时当字符串处理函数进行处理时,遇到第一个 \0 就会停止
int len=strlen (s1) ; // 运行,len 为 2
puts (s1); //运行,输出go并换行

通过字符指针修改变量字符串

通过字符指针变量可以访问所指字符数组中保存的串,不仅可以读取该数组中保存的字符串,还可以修改该串的内容。原因从数组的本质上理解:数组是一系列相同类型变量的集合,故其中保存的字符串,可以理解为是由若干个字符变量组成的。每个字符变量当然可以改变。

#include<stdio.h>
#include<string.h>
int main (void)
{
    char str[30]="Learn and live."
    *p=str;
    *(p+6)='A';
    *(p+10)='L';
    puts(str);
    return 0;
}

// Learn And Live.

0x05 指针与函数

在 C 语言中,函数调用有两种形式:传值调用和传址调用,其中,传址调用介绍的是数组类型作函数形参,数组名作实参的形式。

传址调用:指针作函教形参

现在介绍传址调用的另外一种形式,即指针变量作函数形参,地址(或其他指针变量)作实参的形式。函数调用时,在函数体内可以通过实参地址间接地对该实参地址对应的空间进行操作,从而实现可以在函数体内改变外部变量值的功能。

传值调用与传址调用的区别如下:

  • 传值调用:实参为要处理的数据,函数调用时,把要处理数据(实参)的一个副本复制到对应形参变量中,函数中对形参的所有操作均是对原实参数据副本的操作,无法影响原实参数据。且当要处理的数据量较大时,复制和传输实参的副本可能浪费较多的空间和时间。
  • 传址调用:顾名思义,实参为要处理数据的地址,形参为能够接受地址值的“地址箱”即指针变量。函数调用时,仅是把该地址传递给对应的形参变量,在函数体内,可通过该地址(形参变量的值)间接地访问要处理的数据,由于并没有复制要处理数据的副本,故此种方式可以大大节省程序执行的时间和空间。

指针函教:指针作函教返回类型

有时函数调用结束后,需要函数返回给调用者某个地址即指针类型,以便于后续操作,这种函数返回类型为指针类型的函数,通常称为指针函数。

指针函数的定义格式为:

类型*函数名(形参列表)
{
  ... /*函数体*/
}

指针函数,在字符串处理函数中尤为常见。

例如,编程实现把一个源字符串 src 连接到目标字符串 dest 之后的函数,两串之间用空格隔开,并返回目标串 dest 的地址。

#include<stdio.h>
char * str_cat (char * dest, char * src);
int main (void)
{
    char s1[20]="Chinese"; //目标串
    char s2[10]="Dream";
    char *p=str_cat(s1, s2); //返回地址赋给 p
    puts (p);
    return 0;
}
//str_cat的参数也可为字符数组形式
char * str_cat(char * dest, char * src)
{
    char *p1=dest,*p2=src;
    while (*p1!='\0') //寻找dest串的结尾,循环结束时,p1指向'\0'字符
        p1++;
    *p1++=' '; //加空格,等价于*p1='';p1++;
    while (*p2!='\0')
        *p1++=*p2++;
    return dest;
}


// Chinese Dream

指向函教的指针——函教指针

在 C 语言中,整型变量在内存中占一块内存空间,该空间的起始地址称为整型指针,可把整型指针保存到整型指针变量中。函数像其他变量一样,在内存中也占用一块连续的空 间,把该空间的起始地址称为函数指针。而函数名就是该空间的首地址,故函数名是常量指针。可把函数指针保存到函数指针变量中。

函数指针的定义

函数指针变量的定义格式为:

返回类型(*指针变量名)(函数参数表);

说明:上述定义中,指针变量名定义括号不能省略,否则,则为返回指针类型的函数原型声明,即指针函数的声明。

例如:

int *pf (int,int);//该语句声明了一个函数原型,该函数名为pf,该函数含两个int型参数,且该函数返回类型为整型指针类型,即int*。int (*pf) (int,int);//该语句定义了一个函数指针变量pf,该指针变量pf可以指向任意含有两个整型参数,且返回值为整型的函数。

如下定义了一个func函数。

int func (int a, int b){    //...}

该函数含有两个整型参数,且返回类型为整型。与 pf 要求指向的函数类型一致,可让 pf 指向该函数,可以采用如下两种方式。

pf=&func; //正确pf=func; //正确。也可省略&

在给函数指针变量赋值时,函数名前面的取地址操作符 & 可以省略。因为在编译时,C 语言编译器会隐含完成把函数名转换成对应指针形式的操作,故加 & 只是为了显式说明编译器隐含执行该转换操作。

有如下三个函数的原型声明:

void f1(int);int f2(int,float);char f3(int,int);

可能有些编译器对类型检查不严格,但严格意义上来说,如下对函数指针的赋值语句均认为是错误的。

pf=f1; //错误。参数个数不一致、返回类型不一致pf=f2; //错误。参数2的类型不一致pf=f3; //错误。返回类型不一致

通过函数指针调用函数

例如,如下 f() 函数原型及函数指针变量 pf:

int f (int a);int (*pf) (int)=&f; //正确。pf 初始指向 f()函数

当函数指针变量 pf 被初始化指向函数f()后,调用函数 f() 有如下三种形式。

int result;result=f(2); //正确。编译器会把函数名转换成对应指针result=pf(2); //正确。直接使用函数指针result=(*pf)(2); //正确。先把函数指针转换成对应函数名

函数调用时,编译器把函数名转换为对应指针形式,故前两种调用方式含义一样,而第三种调用方式,*pf 转换成对应的函数名 f(),编译时,编译器还会把函数名转换成对应指针形式,从这个角度来理解,第三种调用方式走了些弯路。

函数指针通常主要用于作为函数参数的情形。

假如实现一个简易计算器,函数名为 cal(),假设该计算器有加减乘除等基本操作,每个操作均对应一个函数实现,即有 add()、sub ()、mult()、div() 等,这 4 个函数具有相同的参数及返回值类型。即:

int add (int a, int b); //加操作int sub (int a, int b) ; //减操作int mult (int a, int b) ; //乘操作int div (int a, int b); //除操作

定义函数指针变量 int (*pf) (int,int);,该函数指针变量 pf 可分别指向这 4 个函数。

如果用户调用该计算器函数 cal(),希望在不同的时刻调用其不同的功能(加减乘除),较通用的方法,是把该函数指针变量作为计算器函数 cal() 的参数。即:

//计算器函数void cal (int (*pf) (int, int) , int op1, int op2){    pf (op1,op2) ;//或者(*pf) (op1,op2);}

假如当用户希望调用 cal() 函数实现加操作时,只需把加操作函数名 add() 及加数和被加数作为实参传给 cal () 函数即可;此时 pf 指针指向 add() 函数,在 cal () 函数内通过该函数指针变量 pf 调用其所执行的函数 add ()。可采用如下两种调用方式。

pf (op1,op2);

或者

(*pf)(op1,op2);

例如,使用函数指针,编程实现一个简单计算器程序。实现代码为:

#include<stdio.h>
void cal(void (*pf) (int,int),int opl,int op2);
void add (int a, int b) ; //加操作
void sub (int a, int b) ; //减操作
void mult (int a, int b) ; //乘操作
int main (void){    
    int sel,x1,x2;    
    printf ("Select the operator:");    
    scanf("%d",&sel);    
    printf("Input two numbers:");    
    scanf ("%d%d",&x1, &x2);    
    switch(sel)    {        
        case 1:            
            cal(add,x1,x2);            
            break;        
        case 2 :            
            cal(sub,x1,x2);            
            break;        
        case 3:            
            cal(mult,x1,x2) ;            
            break;        
        default:            
            printf ("Input error!\n");    
    }    
    return 0;
}

void cal (void (*pf) (int, int) , int opl, int op2){    
    pf (opl,op2) ; //或者(*pf)(opl,op2);
}

void add (int a, int b){    
    int result=(a + b);    
    printf("%d + %d = %d\n",a,b,result);
}

void sub (int a, int b){    
    int result= (a - b);    
    printf ("%d - %d = %d\n", a,b, result);
}

void mult (int a, int b){    
    int result= (a * b);    
    printf ("%d * %d = %d\n",a,b,result);
}

// Select the operator:1
// Input two numbers:2 5
// 2 + 5 = 7
posted @ 2021-03-24 20:06  绯狱丸丶  阅读(260)  评论(0编辑  收藏  举报