指针实际上就是内存地址。一个指针变量的值就是某个内存单元的地址(或指针)。“指针”是指地址,是常量,”指针变量“是指取值为地址的变量。

9.1 指针变量的定义和使用

9.1.1 变量值的存取方法

  • 直接引用:通过变量名来引用变量的内存单元值。
  • 间接引用:通过内存地址引用内存单元值。

9.1.2 指针变量的定义

[存储类型] 数据类型符 *变量名;

  • 存储类型是指指针变量本身的存储类型
  • 数据类型符是指针变量所指向的内存单元的数据类型
    1. int *p1; //单个指针变量的定义
    2. static int *p2; //静态指针变量
    3. float *p3, a; //一条语句中同时定义普通变量和指针变量
    4. char *p4, *p5; //一条语句中同时定义多个指针变量
    注意:
  1. * 与变量名之间可加若干 空格,如 int * p; 也是正确的定义。
  2. int *p; 定义的指针变量是p,而不是*p。

9.1.3 指针变量的赋值

设有指向整型变量的指针变量p,如果要把整型变量a的地址赋予p,可以有以下两种方式:

  1. 指针变量初始化方法

    1. int a;
    2. int *p=&a;
    3. static int *p = &a; //错误!标准C下不能用auto变量的地址去初始化static型指针
  2. 赋值语句方法

    1. int a=20;
    2. int *p, *q;
    3. p = &a;
    4. q = p;
  3. 给指针赋值时, = 右边的指针类型与左边不同,需进行强制类型转换

    1. int a;
    2. int *pi;
    3. char *pc;
    4. pi = &a; //pi指向a
    5. pc = (char *)pi; //pc也指向了a

    虽然pi和pc都指向了a,但引用变量a所对应的内存单元的值是不相同的。pc只能访问a的字节单元,但pi可以访问a的整个内存单元

9.1.4 零指针与空类型指针

  1. 零指针(空指针)

指针变量的值为0的指针称为零指针。

  1. int *p=0; //表示p指向地址为0的单元,系统保证该单元不作他用,表示指针变量值没有意义
  2. int *p=NULL; //也可以写成这种形式。NULL在C语言中被定义为值为0的符号常量

注意:p=NULL与未对p赋值不同。

  1. void*类型指针

表示形式为 void *p; 并不指定p是指向哪一种类型数据的指针变量,使用时要进行强制类型转换

  1. char *p1;
  2. void *p2;
  3. p1 = (char *)p2;
  4. p2 = (void *)p1;

9.1.5 指针变量的引用

在指针变量说明中, * 表示其后的变量是指针类型。而表达式中出现的 * 是一个运算符,表示指针变量所指的变量。

  1. int a;
  2. int *p=&a; //p指向a
  3. *p=10; //相当于a=10

9.1.6 指针变量的关系运算

假如p1和p2是两个指针变量:

  • p1==p2表示p1和p2指向同一内存单元
  • p1>p2表示p1处于高地址位置
  • p1<p2表示p1处于低地址位置

9.2 指针和数组

9.2.1 指向数组的指针变量

  1. int a[10], *p=a; //或*p=&a[0]; 数组名即为数组的首地址,也即数组第一个元素的地址
  2. //或者
  3. int a[10], *p;
  4. p=a; //或p=&a[0];

通过上述指针变量,可以如下引用数组元素:

  • p+ia+i 都是数组元素 a[i] 的地址。
  • *(p+i)*(a+i) 就是数组元素 a[i]
  • 指向数组的指针变量,也可看做数组名,因而可以使用下标。如 p[i] 等价于 *(p+i) ,也等价于 a[i]

9.2.2 利用行指针变量访问多维数组

  1. 假设有一个m行n列的二维数组,指针访问如下:
  • 当二维数组的首地址赋给指针变量p后, a[i][j] 等价于 *(p+i*n+j)、p[i*n+j]、*(a[0]+i*n+j) 。二维数组的地址其实也是连续的,只是形体上看似分了行,所以只要计算出对应元素的位序,指针变量的访问方式与一维数组一样。
  • 二维数组名不可赋值给一般指针变量p,只能赋值给指向二维数组的行指针变量。int a[2][3],*p; ,不可写成 p=a;
  1. 通过二维数组的行指针来访问二维数组

在C语言中,行指针是一种特殊的指针变量。它专门用于指向一维数组,定义一个行指针变量的一般格式为:
数据类型符 (*行指针变量名) [常量表达式];

  • 行指针变量名和 * 一定要用小括号括起来。
  • 常量表达式规定了行指针所指一维数组的长度,也就是二维数组的第二维的大小,它是不可省略的。
  • 数据类型符代表行指针所指一维数组的元素类型 ```c int a[3][4]; //定义一个3行4列的二维数组 int (p)[4]; //不可写成int p[4]; 更不可写成int (*p)[2]

//上述语句定义了一个可指向含有4个整型元素的一维数组的指针p,其初始化方法如下 p=a; // 或 p=&a[0]

  1. **以下四种表示元素a[i][j]的形式是等价的,即通过行指针p引用二维数组a的元素a[i][j]:**
  2. - `p[i][j]`
  3. - `*(p[i]+j)`
  4. - `*(*(p+i)+j)`
  5. - `(*(p+i))[j]` //注意这里,必须讲*(p+i)整体括起来才能生效!!否则是错误的!
  6. **注意:**
  7. - 对二维数组的行指针变量进行赋值的一般形式为:
  8. - 二维数组名+整型常数n,例如: `p=a+1;`
  9. - &二维数组名[整型常量n],例如: `p=&a[1];`
  10. - 不可用数组列地址对其赋值
  11. - 例如, `p=a[0];` `p=&a[0][0];` 都是错
  12. <a name="5NABI"></a>
  13. ### 9.2.3 指针数组
  14. 当某个数组单元都是指针型数据时,这个数组被称为指针数组。其定义的一般格式为:<br />`数据类型符 *变量名[常量表达式];`
  15. ```c
  16. char c[3]={'a', 'b', 'c'};
  17. char *p[3];
  18. p[0]=&c[0]; //p[i]是一个指针,*p[i]才是指针所指向的值
  19. p[1]=&c[1];
  20. p[2]=&c[2];
指针数组 二维数组指针
变量定义 int *p[20]; int (*p)[20];
变量性质 p是数组名,不是指针变量,不可对p赋值 p是指针变量,不是数组名,可对p赋值

image.png

9.3 指针与字符串

字符串在内存中的起始地址(即第一个字符的地址)称为字符串的指针,可以定义一个字符指针变量指向一个字符串。

9.3.1 字符串的表示

  1. 字符数组

    char str[]="i love China";
    
  2. 字符指针变量

  • 边定义边赋值 char *字符指针变量名=字符串常量;

    char *pstr="i love China";
    
  • 先定义后赋值 char *字符指针变量名; 字符指针变量名=字符串常量;

    char *pstr;
    pstr="i love China"; //将字符串的首地址赋给pstr
    

9.3.2 字符串的引用

  1. 逐个字符引用

    char *pstr="i love China";
    for(; *pstr!='\0'; pstr++)
     printf("%c", *pstr);
    

    输出: i love China

  2. 整体引用

    char *pstr="i love China";
    printf("%s", pstr);
    

    输出: i love China

    字符串说白了就是字符数组。而字符指针变量pstr中,仅存储字符串常量的首地址

9.4 指针与动态内存分配

9.4.1 静态内存分配

当程序中定义变量或数组后,系统就会给变量或数组按照其数据类型及大小来分配相应的内存单元,这种内存分配方式称为静态内存分配。

int k;  //分配2个字节(VC下4个字节)
char ch[10];  //分配10个字节的内存块,首地址就是ch的值

9.4.2 动态内存分配

所谓动态内存分配,是指在程序运行过程中,根据程序的实际需要来分配一块大小合适的连续的内存单元。动态分配的内存需要有一个指针变量记录内存的起始地址。

9.4.3 ANSI C中常用的几个动态内存分配函数

调用下列函数时源程序中要包含stdlib.h或malloc.h。malloc或calloc或realloc与free一般成对出现。

9.4.3.1 函数malloc()

void *malloc(unsigned int size);

  • size这个参数的含义是分配的内存大小(以字节为单位)
  • 用malloc分配内存不一定成功,如果分配内存失败,则返回值是NULL(空指针);若成功,返回一个指向空类型(void)的指针,即所分配内存块的首地址,也即返回的指针所指向的内存块可以是任何类型。
    int n, *pscore;
    scanf("%d", &n);
    pscore=(int *)malloc(n*sizeof(int));  //分配n个连续的整型单元,首地址赋给pscore
    if(pscore==NULL) {  //分配内存失败
      printf("error!");
      exit(0);
    }
    
    注意:
  1. malloc前必须加指针类型转换符。
  2. malloc所带的一个参数是指需分配的内存单元字节数,尽管可以直接用数字表示,但一般写成 **分配数量*sizeof(内存单元类型符)**
  3. malloc可能返回NULL,因此一定要进行非空判断。

9.4.3.2 函数calloc()

void *calloc(unsigned int num, unsigned int size);

  • num表示向系统申请的内存空间的数量,size表示申请的每个内存空间的字节数。
  • 同malloc,分配内存不一定成功。成功返回一个void类型的连续存储空间的首地址;失败返回空指针(NULL)。
    int n, *pscore;
    scanf("%d", &n);
    pscore=(int *)calloc(n, sizeof(int));  //分配n个连续的整型单元,首地址赋给pscore
    if(pscore==NULL) {  //分配内存失败
      printf("error!");
      exit(0);
    }
    

    不难发现,malloc和calloc作用相似,而它们之间的区别就是calloc在动态分配完内存后,自动初始化该内存空间为零,而malloc不初始化,里边数据是随机的垃圾数据。

9.4.3.3 函数realloc()

void *realloc(void *p, unsigned int size); 用于改变原来分配的存储空间的大小。

  • 将p所指向的存储空间的大小改为size个字节,如果分配内存失败,返回空指针;成功则返回新分配的存储空间的首地址(void类型),该首地址与原来分配的首地址不一定相同
  • realloc的主要应用场合是:当先前通过动态内存分配的存储空间因实际情况需要进行扩充或缩小时,就可以使用realloc( )解决,其好处是原存储空间中的数据保持不变
    char i, *p;
    p=(char *)malloc(4*sizeof(char)); //动态申请一个4字节的存储空间用于存放char类型数据
    for(i=0; i<4; i++) //分别存放1,2,3,4
      *(p+i)=i+1;
    p=(char *)realloc(p, 6*sizeof(char));  //执行后,p所指向的存储空间由4字节变为6字节,且原先存放的1,2,3,4仍保留
    

9.4.3.4 动态内存释放

在C语言中,函数内部定义的非静态存储类型的局部变量当函数运行结束时,系统会自动回收这些变量所占内存。
但函数内部动态分配的内存,系统是不会自动释放的,必须使用释放内存的函数free来释放
void free(void *block);

  • block是分配的动态内存的首地址,一般为指向该内存块首地址的指针变量。
    int i;
    p=(int **)malloc(n*sizeof(int *)); //此处构建一个二级指针p,其n个单元内,每个单元存放一个int型指针
    for(i=0; i<n; i++)  //每个int型指针,指向一个存放n个int型数据的存储单元,总体上就构成了一个相当于n行n列的二维数组
      p[i]=(int *)malloc(n*sizeof(int));
    //那么释放分配的存储空间时,需要先释放指针数组中各单元指针所指向的内存,然后再释放指针数组所占的内存
    for(i=0; i<n; i++)
      free(p[i]);
    free(p);
    

9.5 多级指针

存放目标变量地址的指针成为一级指针。如果指针中存放的不是变量的地址,而是存放一级指针变量的地址,这种指针成为二级指针
一般来说,对于一个n(n>1)级指针变量,其内容是存放一个n-1级指针变量的地址。

9.5.1 二级指针的定义和引用

[存储类型] 数据类型符 **变量名;

  • 存储类型是指二级指针变量本身的存储类型
  • 数据类型符是指针变量所指向的最终目标变量的数据类型
  • * 的个数表示指针的级数 ```c int a=3; int p1; int *p2;

p1=&a; p2=&p1; **p2=5;

上述语句使最终变量a的值为5。
> 对二级指针不能用变量的地址对其直接赋值。如int a,**p;  p=&a;


**注意**:二级指针与指针数组的关系。例如,int **p; 与 int *q[10]; 之间的关系。

- 指针数组名是二级指针常量
- p=q;    p+i是q[i]的地址
- 指针数组作为形参时,int *q[ ] 与 int **q 完全等价;但作为变量定义两者不同
- **系统只给p分配能保存一个指针值的内存区;而给q分配10块内存区,每块可保存一个指针值**

<br />
<a name="WlSUp"></a>
## 9.6 指针作为函数参数
```c
//传值调用
void func(int a) {
    a=5;
}

//传址调用
void func2(int *p) {
    *p=5;
}

void main() {
    int b=0;
    func(b); //b值仍为0
    func2(&b);  //b值变为5
}

从上述程序可知,指针型参数是实现函数调用者和被调用者之间双向数据传送的手段之一。
在定义函数时,形参前面有一个 * ,就表明是指针型参数,有多个就是多级指针。

9.6.1 常态指针型参数

  1. 参数指针所指向的内存单元是常态的,不能赋值: ```c

void func(const int p) { int a[10]; p=a; //正确 p=5; //编译错误 }


2. 参数指针本身是常态的,不能赋值:
```c
void func(int * const p) {
    int a[10];
    p=a; //编译错误
    *p=5;  //正确
}

9.6.2 通过函数为指针变量分配内存

顾名思义,需传递一个指针变量到函数中,也就是将指针变量的地址传递进函数,因此形参必须为指针的指针,即二级指针

void getm(int *p, int num) {  //错误的方式
    p=(int *)malloc(num*sizeof(int));
    return;
} //该方法并不会对p的指向产生影响,因为传进函数的为p的形参,函数结束后形参消失

void getmRight(int **p, int num) { //正确的方式
    *p=(int *)malloc(num*sizeof(int));
    return;
}

9.7 指针函数

返回值为指针的函数即为指针函数,定义格式如下:
函数类型 *函数名([形参1,形参2,...,形参n])
指针函数所返回的指针,不能为auto型局部变量的地址,但可以返回static型变量的地址。

int *getdata(int num) { //错误,因为随着函数的返回,auto型局部变量生存周期结束,其对应的指针将变为野指针
    int a[100];
    int k;
    if(num>100) return NULL;
    for(k=0; k<num; k++)
        scanf("%d", &a[k]);
    return a;
}

int *getdata(int num) { //正确,static型局部变量生命周期为整个程序
    static int a[100];
    int k;
    if(num>100) return NULL;
    for(k=0; k<num; k++)
        scanf("%d", &a[k]);
    return a;
}

编写指针函数的总体原则为:返回指针所对应的内存空间不能因为该指针函数的返回而被释放。返回的指针通常有以下几种:

  1. 函数中动态分配的内存的首地址(通过malloc等实现,不主动free就会一直存在);
  2. 函数中的静态变量(static)或全局变量所对应的存储单元的首地址;
  3. 通过指针形参所获得的实参的有效地址,即别人的地址本来就在函数外,当然不会被你函数影响到。

9.8 函数指针

一个函数在编译时,被分配了一个入口地址,用函数名表示,这个地址就称为该函数的指针。可以用一个指针变量指向一个函数,然后通过该指针变量调用此函数。

9.8.1 函数指针变量

函数类型 (*指针变量)([形参类型1,形参类型2,...,形参类型n])

  1. 定义
  • 函数类型为 函数指针所指向的 函数的 返回值类型
  • 指针变量专门存放函数入口地址,可指向返回值类型相同的不同函数
  • *指针变量外的括号不能缺少,否则变成了指针函数;
  • 形参类型指函数指针所指向的函数的参数列表,有则写,没有可省略。
    int (*p)(int, int); //定义了一个可指向  带两个int型的形参,返回值为int型的函数指针
    
  1. 赋值

函数指针=[&]函数名;

  • 函数名后不能带括号和参数;
  • 函数名前的 & 是可选的。
    int man(int a, int b) {
      return (a>b?a:b);
    }
    int (*p)(int, int); //定义函数指针p
    p=max;  //将函数所对应的内存单元首地址(函数名max)赋给函数指针p
    
  1. 调用格式

函数指针变量([实参1,实参2,...,实参n]);

(*函数指针变量)([实参1,实参2,...,实参n]);

//上面定义的函数指针p经赋值后
p(2,3);  或  (*p)(2,3);    //等价于max(2,3)

9.9 带参数的main函数

在操作系统状态下,为执行某个程序而键入的一行字符称为命令行
命令名 参数1 参数2 ... 参数n
带参数的main函数形式为:

void mian(int argc, char *argv[]) {
    ...
}
  • argc用于存放命令行中参数的个数(字符串的个数);
  • argv是字符型指针数组,数组中的元素(即字符串指针)指向命令行参数中各字符串的首地址;
  • argv和argc这两个变量名可以由程序员随意命名;
  • 第一个参数是main所在的可执行文件名。