C | 指针

发布时间 2023-03-22 19:08:07作者: 就良同学

1.什么是指针

指针是一种变量,也称指针变量,它的值不是整数、浮点数和字符,而是内存地址。指针的值就是变量的地址,而变量又拥有一个具体值。因此,可以理解为变量直接引用了一个值,指针间接地引用了一个值。一个存放变量地址的类型称为该变量的“指针”。

指针变量的大小?

32位系统为例,每个字节(即一个内存单元)都拥有一个地址编号,地址范围为0x00000000~0xffffffff。当指针变量占4个字节(即32bit)时,刚好能够表示所有地地址编号。

不管什么类型的指针,其大小只和系统编译器有关系。

image

2.指针的定义与使用

2.1 指针的定义

在C语言中,所有变量在使用前都需要声明。例如,声明一个指针变量的语句如下:

int *qPtr, q;

q是整型变量,表示要存放一个整型类型的值;qPtr是一个整形指针变量,表示要存放一个变量的地址,而这个变量是整数类型。qPtr叫做一个指向整型的指针。

在声明指针变量时,“*”只是一个指针类型标识符,指针变量的声明也可以写成 int* qPtr。

定义指针三步骤(来自传智播客):

  1. *与符号相结合代表是一个指针变量,比如*p;
  2. 要保存谁的地址,就写出它的声明语句,比如int a, int a[10];
  3. 用*p替换掉变量名称,即int a→int *p,int a[10]→int (*p)[10](数组指针);

指针变量可以在声明时赋值,也可以在声明后赋值。例如,在声明时为指针变量赋值的语句如下:

int q = 12;
int *qPtr = &q;

也可以在声明后为指针变量赋值:

int q = 12, *qPtr;
qPtr = &q;

2.2 指针的使用

指针变量主要通过取地址运算符&和指针运算符*来存取数据。例如,&a指的是变量a的地址(取址),*ptr表示ptr所指向的内存单元存放的内容(取值)。

#include<stdio.h>

int main(){
    int q=12;
    int *qptr;
    qptr = &q;
    printf("q的地址是:%p\nqptr中的内容是:%p\n", &q, qptr);
    printf("q的值是:%d\n*qptr的值是:%d\n", q, *qptr);
    // 运算符'&'和'*'是互逆的
    printf("&*qptr=%p, *&qptr=%p\n因此有&*qptr=*&qptr\n", &*qptr, *&qptr);
    return 0;
}

image

3.指针的宽度(步长)

#include<stdio.h>

int main(){
    int num = 0x01020304;
    char *p1 = (char *)&num;
    short *p2 = (short *)&num;
    int *p3 = &num;

    printf("%#x\n", *p1);
    printf("%#x\n", *p2);
    printf("%#x\n", *p3);
    return 0;
}

image

通过*取指针变量所指向那块内存空间内容时,取得内存的宽度和指针变量本身指向变量的类型有关。

image

image

题目:

int a[5] = {1, 2, 3,4 , 5};
int *ptr = (int *)(&a+1);
printf("%d,%d", *(a+1), *(ptr-1));

输出结果为:A.2,5 B.2,4 C.1,5 D.1,4

分析:

&a+1:跨过的是整个数组的宽度

&a[0]+1:跨过的是数组内单个元素的宽度

所以&a+1指向的内存地址已经不属于数组了,然后int *ptr = (int *)(&a+1)将&a+1强转为int*型,所以ptr-1将后退一个4个字节即一个数组元素大小,即指向a[4]=5

4.野指针和空指针和万能指针

4.1 野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

#include<stdio.h>

int main(){
    int *p ;

    *p = 200;
    printf("%d\n", *p);
    return 0;
}

image

上述代码出现问题的原因:指针变量未初始化

任何指针变量刚被创建时不会自动成为 NULL 指针,它的缺省值是随机的。

所以,指针变量在创建的同时应当被初始化,要么将指针设置为 NULL ,要么让它指向合法的内存。

如果没有初始化,编译器会报错‘point’ may be uninitializedin the function。

4.2 空指针

#include<stdio.h>

int main(){
     // 将0号地址编号0x00000000号内存赋予指针内部值,等价于赋予NULL
    int *p = NULL;

    *p = 200; // 因为p保存了0号内存的地址,这个地址为内存的起始地址,是不可使用的,非法
    printf("%d\n", *p);
    return 0;
}

image

NULL是C语言标准定义的一个值,这个值其实就是0,只不过为了使得看起来更加具有意义,才定义了这样的一个宏,中文的意思是空,表明不指向任何东西。

任何程序数据都不会存储在地址为0的内存块中,它是被操作系统预留的内存块。

空指针的作用:

如果指针使用完毕,需将指针赋予NULL;在使用指针前需判断指针是否为NULL

4.3 万能指针

void *p,可以保存任意的地址。

void p:不可以定义void类型的变量,因为编译器不知道给该变量分配多大的内存空间;

void *p:可以定义void *变量,因为指针都是4个字节(32位系统)。

#include<stdio.h>

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

程序将报错,无法编译。因为虽然p内存内确实存储的是变量a的地址,但是由于p指向void型变量,导致根据p指针内部存储地址去取相应位置值时不知道取多大的内存大小。

#include<stdio.h>

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

image

*(int *)p:将指针p强转为int *型,此时根据p指针内部存储地址去取相应位置值时,将读取4个字节大小。

5.const修饰的指针变量

引子:const int a = 10;const修饰变量a,表示不能再通过a修改a内存里面的内容。

image

5.1 指向常量的指针

const修饰*,表示不能通过该指针修改指针所指内存的数值,但是指针指向可以变。

image

image

5.2 指针常量

修饰p,指针指向不能变,指针指向的内存可以被修改。

image

注:const int * const p =&a;表示p指针指向内存区域不能被修改,同时p的指向也不能被改变。

6.多级指针

#include<stdio.h>

int main(){
    int a = 10;
    int *p = &a;
    int **q = &p;
    // 通过q获取a的值
    printf("%d\n", **q);
    return 0;
}

image

7.指针数组与数组指针

7.1 指向数组元素的指针

例如定义一个整型数组和一个指针变量,语句如下。

int a[5]={10,20,30,40,50};
int *aPtr;

这里的a是一个数组,它包含了5个整型数据。变量名a就是数组a的首地址,它与&a[0]等价。如果令aPtr=&a[0]或者
aPtr=a,则aPtr也指向了数组a的首地址。

也可以在定义指针变量时直接赋值,如以下语句是等价的。

int *aPtr=&a[0];
int *aPtr;
aPtr =&a[0];

与整型、浮点型数据一样,指针也可以进行算术运算,但含义却不同。当一个指针加1(或减)1并不是指针值增加(或减少)1,而是使指针指向的位置向后(或向前)移动了一个位置,即加上(或减去)该整数与指针指向对象的大小的乘积。例如对于aPtr+=3,如果一个整数占用4个字节,则相加后aPtr=2000+4*3=2012(这里假设指针的初值是2000)。同样指针也可以进行自增(++)运算和自减(--)运算。

也可以用一个指针变量减去另一个指针变量。例如,指向数组元素的指针aPtr的地址是2008,另一个指向数组元素的指针bPtr的地址是2000,则a=aPtr-bPtr的运算结果就是把从aPtr到bPtr之间的元素个数赋给a,元素个数为(2008-2000)/4=2(假设整数占用4个字节)

我们也可以通过指针来引用数组元素。例如以下语句。

*(aPtr+2);

如果aPtr是指向a[0],即数组a的首地址,则aPtr+2就是数组a[2]的地址,*(aPtr+2)就是30。

注意:指向数组的指针可以进行自增或自减运算,但是数组名则不能进行自增或自减运算,这是因为数组名是一个常量指针,它是一个常量,常量值是不能改变的。

#include<stdio.h>

int main(){
    int a[5] = {10, 20, 30, 40, 50};
    int *aPtr, i;
    aPtr = &a[0];
    for(i=0;i<5;i++){ //通过数组下标引用元素的方式输出数组元素
        printf("a[%d]=%d\n", i, a[i]);
    }
    for(i=0;i<5;i++){ //通过数组名引用元素的方式是输出数组元素
        printf("*(a+%d)=%d\n", i, *(a+i));
    }
    for(i=0;i<5;i++){ //通过指针变量下标引用元素的方式输出数组元素
        printf("aPtr[%d]=%d\n", i, aPtr[i]);
    }
    for(aPtr=a, i=0; aPtr<a+5; aPtr++, i++){ //通过指针变量偏移的方式输出数组元素
        printf("*(aPtr+%d)=%d\n", i, *aPtr);
    }
    return 0;
}

image

7.2 指针数组

定义:指针数组其实也是一个数组,只是数组中的元素是指针类型的数据。换句话说,指针数组中的每一个元素都是一个指针变量。

定义指针数组的方式如下:

int *p[4]

例1:使用指针数组保存字符串并将字符串打印输出。

#include<stdio.h>

int main(){
    // 定义指针数组
    const char *s[4] = {"ABC", "DEF", "GHI", "JKL"};
    int n = 4;
    int i;
    const char *aPtr;
    // 方法1:通过数组名输出字符串
    for(i=0;i<n;i++){
        printf("第%d个字符串:%s\n", i+1, s[i]);
    }
    // 方法2:通过指向数组的指针输出字符串
    for(aPtr=s[0],i=0;i<n;aPtr=s[i]){
        printf("第%d个字符串:%s\n", i+1, aPtr);
        i++;
    }
    return 0;
}

运行结果图示:

result

注:常量与指针间的转换 warning: ISO C++ forbids converting a string constant to 'char*'

Q:为什么s[i]打印的是值而不是地址?

A:%s占位符的特点就是只要告诉他字符串的首地址,就可以读取整个字符串

例2:利用指针数组实现对一组变量的值按照从小到大排序,排序时交换变量的指针值。

/* 利用指针数组实现对一组变量的值按照从小到大排序,排序时交换变量的指针值。 */
#include<stdio.h>

int main(){
    int a, b, c, d, e;
    int *s[5] = {&a, &b, &c, &d, &e};
    int i,j;
    int n=5;
    int *p;
    // 用户输入5个数(大小任意)
    printf("请输入5个任意正整数(空格分隔):\n");
    scanf("%d%d%d%d%d", &a, &b, &c, &d, &e);
    printf("排序前:\n");
    for(i=0;i<n;i++){
        printf("%d\n",*s[i]);
    }
    // 排序:冒泡排序
    for(i=0;i<n-1;i++){ // 执行n-1趟
        for(j=0;j<n-i-1;j++){ // 每一趟需要执行n-1-i次比较操作
            if(*s[j] > *s[j+1]){
                p = s[j];
                s[j] = s[j+1];
                s[j+1] = p;
            }
        }
    }
    printf("排序后:\n");
    for(i=0;i<n;i++){
        printf("%d\n",*s[i]);
    }
    return 0;
}

运行结果图示:

image

7.3 数组指针

定义:数组指针是指向数组的一个指针。如下定义:

int (*p)[4]

其中,p是指向一个拥有4个元素的数组的指针,数组中每个元素都为整型。与前面刚刚介绍过的指针数组做比较,这里定义的数组指针多了一对括号,*p两边的括号不可以省略。这里定义的p仅仅是一个指针,不过这个指针有点特殊,这个p指向的是包含4个元素的一维数组。

数组指针p与它指向的数组之间的关系可以用下图来表示。

image

如果有如下语句:

int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
p=a;

数组指针p与数组a中元素之间的关系如图所示。其中,(*p)[0]、(*p)[1]、(*p)[2]、(*p)[3]分别保存的是元素值为1、2、3、4的值。p、p+1和p+2分别指向二维数组的第一行、第二行和第三行,p+1表示将指针p移动到下一行。

image

*(p+1)+2表示数组a第1行第2列的元素的地址,即&a[1][2],*(*(p+1)+2)表示a[1][2]的值即7,其中1表示行,2表示列。

image

Q:为什么(*p)[0]、(*p)[1]、(*p)[2]、(*p)[3]分别保存的是元素值为1、2、3、4的值而不是它们的地址呢?

A:指针[i] == *(指针+i)

(*p)[0] == *(*p+0)→p本来表示的是第0行一整行的数据,出现在表达式中将自动转为指向a[0][0]的指针→*p+0还是指向a[0][0]的指针→*(*p+0)即对地址取值。

下面编程输出以上数组指针的值和数组的内容。

#include<stdio.h>

int main(){
    int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
    int (*p)[4] = a; // 声明数组指针p(p是一个指向'内含4个整型元素的数组'的指针)
    int row, col;
    // 输出数组的内容
    for(row=0;row<3;row++){
        for(col=0;col<4;col++){
            printf("a[%d][%d]=%-4d", row, col, *(*(p+row)+col));
        }
        printf("\n");
    }
    // 输出数组指针的值
    for(row=0;row<3;row++, p++){
        for(col=0;col<4;col++){
            printf("(*p[%d])[%d]=%p\t", row, col, (*p+col));
        }
        printf("\n");
    }
    return 0;
}

运行结果图示:

image

注释:[] == *()→如,p[0] 等价于 *(p+0)

8.指针函数与函数指针

8.1 指针函数

指针函数是指函数的返回值是指针类型的函数。例如,以下是一个指针函数的声明:

float *func(int a, int b);

func是函数名,前面的'*'表明返回值的类型是指针类型,因为前面的类型标识符是float,所以返回的指针是指向浮点型的。

例:假设若干个学生的成绩存放在二维数组中,要求输入学生编号,利用指针函数实现其成绩的输出。

#include<stdio.h>

int *FindAddress(int (*ptrScore)[4], int index);
void Display(int *, int n);

int main(){
    int score[3][4] = {{83, 78, 79, 88}, {71, 88, 92, 63}, {99, 92, 87, 80}};
    int n = 4;
    int row;
    int *p;
    while(1){
        printf("请输入学生编号(1 or 2 or 3),输入0退出程序:\n");
        scanf("%d", &row);
        if(row == 0){
            break;
        }else if(row == 1 || row == 2 || row == 3){
            printf("第%d名学生的各科成绩分别为:\n", row);
            p = FindAddress(score, row-1);
            Display(p,n);
        }else{
            printf("输入不合法,请重新输入!\n");
        }
    }
    return 0;
}

int *FindAddress(int (*ptrScore)[4], int index){
    /*查找某条学生成绩记录地址函数。通过传递的行地址找到要查找学生成绩所在行,并返回该行的首元素地址*/
    int *ptr;
    ptr = *(ptrScore+index);
    return ptr;
}

void Display(int *ptr, int n){
    /*输出学生成绩的实现函数。利用传递过来的指针输出每门课的成绩*/
    int col;
    for(col=0; col<n; col++){
        printf("%4d", *(ptr+col));
    }
    printf("\n");
}

image

注:p = FindAddress(score, row-1);二维数组的数组名表示啥?

若a是一维数组,则a指向的是第一个元素。

若a是二维数组,也可以将a看成一个一维数组,那么其元素是其行向量。则a指向的是第一个行向量。

8.2 函数指针

指针可以指向变量、数组,也可以指向函数,指向函数的指针就是函数指针。

1)函数指针的调用

例1:通过一个函数求两个数的乘积,并通过函数指针调用该函数。

#include<stdio.h>

int Mult(int a, int b);

int main(){
    int a, b;
    int (*func)(int, int);
    printf("请输入2个数:\n");
    scanf("%d%d", &a, &b);
    /*方法1:函数名调用*/
    printf("%d * %d = %d\n", a, b, Mult(a, b));
    /*方法2:函数指针调用*/
    func = &Mult; // 因为函数名本身就是地址,所以&可以省略
    printf("%d * %d = %d\n", a, b, func(a, b));
    return 0;
}

int Mult(int x, int y){
    return x*y;
}

image

2)函数指针作为函数参数的使用

例2:利用函数指针作为函数参数,实现选择排序算法的升序排列和降序排列。

#include<stdio.h>
void SelectSort(int *, int, int (*)(int, int)); //选择排序,函数指针作为参数调用
int Ascending(int, int); // 是否进行升序排列
int Descending(int, int); // 是否进行降序排列
void swap(int *, int *);
void Display(int a[], int n);

int main(){
    int a[10] = {13, 23, 11, 4, 9, 16, 22, 23, 9, 10};
    printf("排序前数列:\n");
    Display(a, 10);
    printf("升序排列:\n");
    SelectSort(a, 10, Ascending);
    Display(a, 10);
    printf("降序排列:\n");
    SelectSort(a, 10, Descending);
    Display(a, 10);
    return 0;
}

void swap(int *a, int *b){
    int temp = *a;
    *a = *b;
    *b = temp;
}

void Display(int a[], int n){
    int i;
    for(i=0;i<n;i++){
        printf("%4d", a[i]);
    }
    printf("\n");
}

int Ascending(int a, int b){
    if(a>b){
        return 1;
    }else{
        return 0;
    }
}
int Descending(int a, int b){
    if(a<b){
        return 1;
    }else{
        return 0;
    }
}

void SelectSort(int *ptr, int n, int (*compare)(int, int)){
    /*选择排序基本思想(升序):每一趟排序从n-i个元素中选取关键字最小的元素作为有序序列的第i个元素*/
    int i, j, k;
    // 将第i个元素与后面n-i个元素进行比较,将关键字最小的元素放在第i个位置
    for(i=0;i<n;i++){
        j=i; // 初始时,关键字最小的元素下标为i
        for(k=j+1;k<n;k++){
            if(compare(*(ptr+j), *(ptr+k))){
                j=k;
            }
        }
        if(j!=i){
            swap(ptr+j, ptr+i);
        }
    }
}

image

其中,函数SelectSort(a,N,Ascending)中的参数Asscending是一个函数名,传递给函数定义void SelectSort(int *p,int n,int(*compare)(int,int))中的函数指针compare,这样指针就指向了Asscending。从而可以在执行语句(*compare)(a[j], a[j+1])时调用函数Ascending(int a,int b)判断是否需要交换数组中两个相邻的元素,然后调用swap(&a[j],&a[j+1])进行交换。

8.3 函数指针数组

假设有3个函数f1、f2和f3,可以把这3个函数作为数组元素存放在一个数组中,需要定义一个指向函数的指针数组指向这
三个函数,代码如下:

void (*f[3])(int)={f1,f2,f3};

f是包含3个指向函数指针元素的数组,f[0]、f[1]和f[2]分别指向函数f1、f2和f3。通过函数指针f调用函数的形式如下。

f[n](m); /*n和m都是正整数*/

例:声明一个指向函数的指针数组,并通过指针调用函数。

#include<stdio.h>

void f1(int n); /*函数f1声明*/
void f2(int n); /*函数f2声明*/
void f3(int n); /*函数f3声明*/

int main(){
    void (*f[3])(int)={f1,f2,f3}; /*声明指向函数的指针数组*/
    int flag;
    printf("调用函数请输入1、2或者3,结束程序请输入0。\n");
    scanf("%d",&flag);
    while(flag){
        if(flag==1||flag==2||flag==3){
            f[flag-1](flag); /*通过函数指针调用数组中的函数*/
            printf("请输入1、2或者3,输入0结束程序.\n");
            scanf("%d",&flag);
        }else{
            printf("请输入一个合法的数(1~3),输入0结束程序.\n");
            scanf("%d",&flag);
        }
    }
    printf("程序结束.\n");
    return 0;
}
void f1(int n) /*函数f1的定义*/
{
    printf("函数f%d:调用第%d个函数!\n",n,n);
}
void f2(int n) /*函数f2的定义*/
{
    printf("函数f%d:调用第%d个函数!\n",n,n);
}
void f3(int n) /*函数f3的定义*/
{
    printf("函数f%d:调用第%d个函数!\n",n,n);
}

image

函数指针不能执行像f+1、f++、f--等运算。