- 注:本页使用的是
.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
,和第一次不同。
注意:
- 地址是连续的
- 在同一台机器上,地址编号是唯一的
- 每次运行程序,地址都不一定一样,是由系统随机分配的
首地址
一个整型变量(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没有指向一个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
这分别是变量num
和ch
分别的地址。
输出万能指针指向存储区域的数据
但是如果要用*
去获得这个地址指向的存储区域的数据,就会遇到问题了。因为存储地址本身没有类型,这是我们为了方便描述,所以说有类型,比如int
,char
。
比如在下面的代码中,就会编译报错。
#include<stdio.h>
int main(){
int num = 114514;
void* p = #
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
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;
。
Comments NOTHING