C中的指针

KaedeharaLu 发布于 2024-08-16 603 次阅读


  • 注:本页使用的是.c扩展名的C,而不是.cpp结尾的C++

printf()的用法

使用printf()需要引用头文件stdio.h

#include <stdio.h>

printf()的基本格式为printf("格式化字符串",输出表列)
格式化字符串包含三种对象,分别为:
(1)字符串常量;
(2)格式控制字符串;
(3)转义字符。
字符串常量原样输出,在显示中起提示作用。输出表列中给出了各个输出项,要求格式控制字符串和各输出项在数量和类型上应该一一对应。其中格式控制字符串是以%开头的字符串,在%后面跟有各种格式控制符,以说明输出数据的类型、宽度、精度等。

格式控制字符的类型如下:

字符 对应数据类型 含义 示例
d/i int 输出十进制有符号32bits整数,i是老式写法,推荐用d printf("%i",123);
输出123
o unsigned int 无符号8进制(octal)整数(不输出前缀0) printf("0%o",123);
输出0173
u unsigned int 无符号10进制整数 printf("%u",123);
输出123
x/X unsigned int 无符号16进制整数,x对应的是abcdef,X对应的是ABCDEF(不输出前缀0x) printf("0x%x 0x%X",123,123);
输出0x7b 0x7B
f/lf float(double) 单精度浮点数用f,双精度浮点数用lf(printf可混用,但scanf不能混用) printf("%.9f %.9lf",0.000000123,0.000000123);
输出0.000000123 0.000000123。
注意指定精度,否则printf默认精确到小数点后六位
F float(double) 与f格式相同,只不过 infinity 和 nan 输出为大写形式。 例如printf("%f %F %f %F\n",INFINITY,INFINITY,NAN,NAN);
输出结果为inf INF nan NAN
e/E float(double) 科学计数法,使用指数(Exponent)表示浮点数,此处”e”的大小写代表在输出时“e”的大小写 printf("%e %E",0.000000123,0.000000123);
输出1.230000e-07 1.230000E-07
g float(double) 根据数值的长度,选择以最短的方式输出,%f或%e printf("%g %g",0.000000123,0.123);
输出1.23e-07 0.123
G float(double) 根据数值的长度,选择以最短的方式输出,%f或%E printf("%G %G",0.000000123,0.123);
输出1.23E-07 0.123
c char 字符型。可以把输入的数字按照ASCII码相应转换为对应的字符 printf("%c\n",64);
输出A
s char* 字符串。输出字符串中的字符直至字符串中的空字符(字符串以空字符’\0‘结尾) printf("%s","测试test");
输出:测试test
S wchar_t* 宽字符串。输出字符串中的字符直至字符串中的空字符(宽字符串以两个空字符’\0‘结尾) setlocale(LC_ALL,"zh_CN.UTF-8"); setlocale(LC_ALL,"zh_CN.UTF-8");
wchar_t wtest[]=L"测试Test";
printf("%S\n",wtest);
输出:测试test
p void* 以16进制形式输出指针 printf("%010p","lvlv");
输出:0x004007e6
n int* 什么也不输出。%n对应的参数是一个指向signed int的指针,在此之前输出的字符数将存储到指针所指的位置 int num=0;
printf("lvlv%n",&num);
printf("num:%d",num);
输出:lvlvnum:4
% 字符% 输出字符‘%’(百分号)本身 printf("%%");
输出:%
m 输出errno值对应的出错内容 printf("%m\n");
a/A float(double) 十六进制p计数法输出浮点数,a为小写,A为大写 printf("%a %A",15.15,15.15);
输出:0x1.e4ccccccccccdp+3 0X1.E4CCCCCCCCCCDP+3

但是printf()函数还有很多内容,文章的主题不在此,所以不展开讲述。这里放一篇文章链接,推荐参考。printf()用法详解

指针基础

地址

什么是地址

内存有很多内存单元(字节)组成,每一个单元都有一个唯一的16进制的编号,这个编号就是地址。
如:0x1001 0x1002 0x1003

  • 一般的变量我们使用变量名调用,实际使用时,系统也是通过地址去访问,变量名只是方便程序编写。

获得地址

取址符:&,在scanf()函数也有用到。
scanf()的用法可以参考这篇文章:【C语言】scanf函数详解
例如:

#include<stdio.h>

int main() {
    int age = 18;
    printf("%d %p\n", age,&age);
    return 0;
}

在我这里输出了18 00000088D5EFF624。每次运行输出的地址一般是不同的,因为内存状态、占用不同,每次都会存储在不同的地址。比如我第二次运行输出的就是18 00000099815AF794,和第一次不同。
指针1

注意:

  • 地址是连续的
  • 在同一台机器上,地址编号是唯一的
  • 每次运行程序,地址都不一定一样,是由系统随机分配的

首地址

一个整型变量(int)需要4个字节存储,那么每个字节都会有一个地址。但是程序只返回了一个地址,这就是首地址。
但是在实际使用过程中,不会额外提及这个概念,所说的地址就是首地址。
内存地址
可以看到啊,我将程序显示的地址在内存中查询到,将绿框的四个地址反着拼接,就得到了00000012的值,这也是一个16进制数,按照16进制转10进制,就是2×(16^0)+1×(16^1)=18,就是定义的变量age的值。而这4个字节被称作一个“存储区域”。

基础指针/一级指针

指针是一种特殊的数据类型,可以用来声明一个指针变量,用来存储地址。

  • 指针≠地址,但是在使用过程和讲述过程中,一般也不会区分,习惯性把指针变量叫做指针。
    指针与地址

指针的优势:

  • 1.直接访问硬件,快速传递数据
  • 2.返回一个以上的值,如返回一个数组或结构体
  • 3.方便处理字符串

指针的定义与使用

定义指针变量

和普通变量定义类似,类型+变量名。但是有一点不同,需要再类型后面再跟上一个*。
如,int* age
如果我写了这样一个代码,编译,肯定是运行不了的了。

#include<stdio.h>

int main() {
    //定义指针变量:类型* 变量名
    int* ptr; 
    //ptr是一个指针 指针(变量存储谁的地址,就说指向了谁)指向的是一个int类型的空间
    printf("%p\n", ptr);
    return 0;
}

未初始化ptr
可以看到编译器报错说“ptr没有初始化”,因为这时候ptr没有指向一个int类型的空间。那么浅浅修改一下代码。

#include<stdio.h>

int main(){
    int age = 18;
    int* ptr=&age; //取址变量age
    printf("%p\n", ptr);
    return 0;
}

这样,控制台就输出出了变量age的地址。如0000008B404FFCC4
注意:指针变量的类型要尽量和指向的变量的类型保持一致,虽然说程序可以正常运行,但是会看到调试时编译器(Visual Studio)有这样的提示warning C4133: “初始化”: 从“char *”到“int *”的类型不兼容

输出指针变量指向的变量

以上面的代码为例,除了可以通过printf("%d\n",age)输出变量age的内容,还可以通过*ptr的方式输出。
示例代码:

#include<stdio.h>

int main(){
    int age = 18;
    int* ptr=&age;
    printf("%d  %d\n",age, *ptr);
    return 0;
}

运行后,就输出了18 18

&与*

  • & 取址符=>获得变量的地址。当然,也可以通过&获取到一个指针变量的地址。
  • * 根据指针变量获取指向的空间的数据
  • (a是一个int型变量)printf("%d",a)printf("%d",*&a)是完全一样的,第二个输出相当于就是取址过后再根据这个地址去获得数据。

一句话

&像一个锁,*像一把钥匙,用*可以解开&的锁并获取到这个存储区域的数据。
这就有一句话了: 一把钥匙解一把锁。到这里,暂时不理解是什么意思,没关系,还有二级指针、三级指针,到时候估计就懂了。

野指针,空指针,万能指针

野指针

没有被初始化的指针,不允许出现的,可能导致编译不通过等等其它后果。

空指针

被赋值为NULL的指针,不指向任何函数或对象。

  • 空指针不能使用,会导致程序崩溃
    代码如下:

    #include<stdio.h>
    
    int main(){
      int* ptr=NULL;
      printf("%p\n",ptr);
      return 0;
    }

    最后输出了0000000000000000,这是什么地址呢?是操作系统占用掉的内存地址,当然是不能访问的。

现在把printf("%p\n",ptr);修改为printf("%d\n",*ptr);再运行,欸,就出现问题了。
输出空指针错误

那空指针可以干嘛呢?可以用来判断指针有没有指向。
现在来推测一下,下面的代码输出什么。

#include<stdio.h>

int main(){
    int *ptr = NULL;
    //printf("%p\n",ptr);
    //printf("%d\n",*ptr);

    if (ptr != NULL) {
        printf("%d\n",*ptr);
    }else {
        printf("ptr指向NULL\n");
    }

    return 0;
}

是的,最后输出了ptr指向NULL。这就是和野指针不同的,野指针没有指向,所以是没有办法比较的。

万能指针

什么是万能指针

就是一个void类型的指针。
万能指针可以指向任意地址,可以指向int、double、float、char都可以。
比如,代码如下:

#include<stdio.h>

int main(){
    int num = 114514; //定义一个int
    char ch = 'A'; //定义一个char
    void* p1 = &num, * p2 = &ch; //分别取址
    printf("%p\n%p\n", p1, p2); //输出
    return 0;
}

最后输出示例:

0000007376DFF904
0000007376DFF924

这分别是变量numch分别的地址。

输出万能指针指向存储区域的数据

但是如果要用*去获得这个地址指向的存储区域的数据,就会遇到问题了。因为存储地址本身没有类型,这是我们为了方便描述,所以说有类型,比如intchar
比如在下面的代码中,就会编译报错。

#include<stdio.h>

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

万能指针不指定类型报错
因此,在根据地址取数据时,需要强制指定一个类型,才可以正常的获得数据。
比如在上面的代码中,将上面的printf("%d\n", *p);修改为printf("%d\n", *(int*)p);,就可以正常输出114514了。

同理,下面这个代码也可以正常输出A

#include<stdio.h>

int main(){
    char ch = 'A';
    void* p = &ch;
    printf("%c\n", *(char*)p);
    return 0;
}

万能指针的隐式转化

  • void*类型的指针可以自动隐式转化为其它类型的指针。

在下面的代码中,int*就直接获得了指针p指向的地址,并且认定为int*类型,所以在最后输出的时候就不需要强转了,直接输出了114514

#include<stdio.h>

int main(){
    int num = 114514;
    void* p = &num; //取址num
    int* pi = p; //隐式转化为int*类型
    printf("%d\n", *pi);
    return 0;
}
  • 到这里,指针基础就算结束了。

指针进阶

二级指针

  • 什么叫二级指针?
    指针除了可以指向一个普通类型的数据,如int char float,也可以指向另一个指针,如int* char* float*,如果一个指针指向另一个指针,就称之为二级指针,即指向指针的指针。
    二级指针示例
    上面这张图中,指针page指向了普通类型的变量age,而指针ppage指向了指针page。因为page本身就是一个指针了,所以ppage就被称作 二级指针

转化到代码中就是这样。

#include<stdio.h>

int main(){
    //定义一个一级指针
    int age = 28;
    int* page = &age; //page指向了age,并存储age的地址
    printf("%p %d\n", page, *page);

    //定义二级指针
    int** ppage = &page; //几级指针变量名前面就有几个*
    printf("%d\n", **ppage); //一个*得到page存储的地址,再加个*根据这个地址获得数据

    return 0;
}

输出样例:

000000DE6152F814 28
28

第一行输出了page的指向地址,和根据地址得到的数据。第二行就是从ppage开始寻址,先得到page存储的地址,然后得到了数据。
也可以自己printf("%p %p\n",ppage,&page);验证一下是不是一样的。
这是二级指针的用法,三级指针、四级指针等等都同理。几级指针定义就有几个*

  • 还有一个到这里,还记得前面说的一句话吗?想一想那句话再来回顾一下呢? 一把钥匙解一把锁,所以二级指针(ppage)要获得age的数据,那么就需要两把钥匙(*)来解开这两把锁(&)。
    多级指针的理解

虽然说,C#/C++中并没有限制指针级数的问题,但是一般只会用一级二级指针,三级及以后基本不用了。一级二级中,二级用的都算少的。

const与指针

const是constant的简写。被const修饰的变量只可读,不可修改,即只读变量。但是const是通过编译器实现的,也就是说const被修改了是编译错误而不是程序错误,所以如果能欺骗过编译器,就可以修改const定义的变量。

而const和指针搭配,一共有三种类型。

常量指针

  • 先从普通的指针看起。
#include<stdio.h>

int main(){
    int age = 18;
    int* ptr = &age; //取址
    *ptr = 19; //操作
    printf("%d %d\n",age, *ptr);
    return 0;
}

在这个代码中,*ptr=19;操作了存储age的内存区域,将数据修改为了19,所以最后输出的时候就输出的是19 19

  • 再来说常量指针。
    常量指针的意思是指向的存储区域的数据不能通过指针进行修改。比如看到下面的代码。
#include<stdio.h>

int main(){
    int age = 18;
    const int* ptr = &age; //定义一个常量指针

    *ptr = 20;
    printf("%d %d\n", age, *ptr);
    return 0;
}
  • const int* ptr = &age;const int* ptr = &age;完全相等,都是定义一个常量指针,没有区别。
    上面这个代码编译就会报错。因为第七行尝试通过指针对数据进行修改,那肯定是不行的。
    常量指针报错
    而直接操作age则可以修改。比如将上面的第7行修改为下面的就没有问题了,就可以输出20 20

    age = 19;

常量指针是可以改变指针指向的,所以尝试运行一下下面的代码。

#include<stdio.h>

int main() {
    int age1 = 18, age2 = 100;
    const int* ptr = &age1;
    printf("%d\n",*ptr);
    ptr = &age2;
    printf("%d\n", *ptr);
    return 0;
}

输出:

18
200

指针常量

指针常量就是说指针的指向是不可以改变的。
怎么定义一个指针常量呢?以int型为例:int* const 变量名

下面看两个代码。

  • 代码1:

    #include<stdio.h>
    
    int main() {
      int age1 = 18, age2 = 100;
      int* const ptr = &age1;
      printf("%d\n",*ptr);
      age1 = 20;
      printf("%d\n", *ptr);
      return 0;
    }

    这个代码运行的很正常,因为我并没有尝试修改ptr的指向,只是通过修改age1的内容,达到了修改输出的效果。输出:

    18
    20
  • 代码2:
#include<stdio.h>

int main() {
    int age1 = 18, age2 = 100;
    int* const ptr = &age1;
    printf("%d\n",*ptr);
    ptr = &age2;
    printf("%d\n", *ptr);
    return 0;
}

这个就报错了,因为我在尝试修改ptr的指向,但是这种定义方式是不允许修改指向的。
指针常量报错

特殊的

有一种指针很特殊,既不能修改指向,指向的内存也不可以通过指针修改。
这种指针的定义方式是:const int* const ptr
(个人不理解其存在的意义)

小结

从上面也可以看出来,const是修饰右侧最近的关键字。

写法 解释
const int ptr
int const
ptr
看第一个的标准写法,const修饰int*,所以说可以理解为指针指向的数据不能(通过指针)更改
int* const ptr const修饰了ptr,所以说ptr的内容不能更改,而ptr存储的是一个地址,所以说ptr的指向就是不可以改变的
const int* const ptr 两个const,一个修饰int*,一个修饰ptr,所以不仅指向的数据不能 (通过指针)更改,ptr的指向还不可以改变

指针妙用

  • 修改被const修饰的普通只读变量
    先上代码:
#include<stdio.h>

int main() {
    const int age = 18;
    printf("%d\n", age);
    int* ptr = &age;
    *ptr = 100;
    printf("%d\n", age);
    return 0;
}

age是一个被const修饰的int型只读变量,前面提到过,const是通过编译器实现的,也就是说const被修改了是编译错误而不是程序错误,所以如果能欺骗过编译器,就可以修改const定义的变量。那么在这个代码中,明面上没有对age变量做直接修改,但是通过指针对存储区域的数据进行了修改,达到了修改只读变量的作用。
编译过程中,有一个warning,warning C4090: “初始化”: 不同的“const”限定符,这是因为age前面的修饰是const int,指针最好要保证指针类型和指针指向类型一致,所以说理论上应该使用const int* ptr定义这个指针。
如果想去掉这个警告,只需要把int* ptr = &age;改成int* ptr = (int*)&age;