C语言中的数据结构和变量

1. 数据类型

C语言提供了丰富的数据类型,这些数据类型可以用来描述很多数据:

整数,浮点数(小数),字符,字符串。

描述整数使用整型类型 ,描述浮点数(小数)使用浮点型类型 ,描述字符使用字符类型


使用类型可以归纳相似的数据,编译器提前知晓数据的类型,才能操作数据。

在C语言中,数据类型有两大类,如图结构:


复制代码
数据类型
├── 内置类型
│   ├── 字符型
│   │   └── char
│   ├── 整型
│   │   ├── short
│   │   ├── int
│   │   ├── long
│   │   └── long long
│   ├── 浮点型
│   │   ├── float
│   │   ├── double
│   │   └── long double
│   └── 布尔类型
│       └── _Bool
└── 自定义类型
    ├── 数组
    ├── 结构体-struct
    ├── 枚举-enum
    └── 联合体-union数据类型

笔者现在只学习到内置类型,因此本章主要梳理内置类型

下面会先引入了unsignedsigned这两个关键字以节省篇幅

这两个关键字用来修饰字符型和整型类型,会在后面详细介绍

下文中的[ ],表示可省略。


1.1 字符型

字符:character,在C语言中取char表示字符类型。

c 复制代码
[signed] char //有符号的char
[unsigned] char // 无符号的char

1.2 整型

整数:integer,在C语言中取int表示整型类型。

c 复制代码
//短整型
short [int]
[signed] short [int]
unsigned short [int]

//整型
int 
[signed] int
unsigned int

//长整型
long [int]
[signed] long [int]
unsigned long [int]

//更长整型
long long [int]
[signed] long long [int]
unsigned long long [int]

1.3 浮点型
c 复制代码
float
double
long double

1.4 布尔类型

C语言中使用整数0表示假,非0值表示真。

在C99中特别加入了布尔类型,用来专门区分真假。

这样,布尔值就有单独的一个类型了

c 复制代码
_Bool

使用布尔类型需要包含头文件<stdbool.h>

其变量取值是:true or false

在<stdbool.h>头文件里存放着:

c 复制代码
#define bool	_Bool
#define true	1
#define false	0

_Bool可以拿来使用:

c 复制代码
_Bool flag = true;
if (flag)
    printf("its true");

由于_Bool敲写起来有点麻烦,因此现在也可以用bool:

c 复制代码
bool flag = true;
if (flag)
    printf("its true");

1.5 数据类型的长度

不同的数据类型,能够创造出长度不同的变量,而变量长度则影响变量能存储的数据多少。

在本文1.7会详细阐述。


1.6 sizeof操作符

sizeof是关键字,同时也是操作符,可以用来计算sizeof中包含的操作数的类型长度,其单位是字节(1 字节 = 8 比特)
sizeof操作符的操作数可以是类型,变量,也可以是表达式。

c 复制代码
sizeof (/*这里存放类型*/)
sizeof 表达式 //表达式的括号可以省略
  • 要注意sizeof其中的表达式不参与真实运算,根据表达式的类型来得出大小,这么说可能有点混乱,
    别担心,会在下文1.8详细解释。
  • sizeof的计算结果的类型是size_t,想打印这个类型,需要使用占位符%zu
c 复制代码
#include <stdio.h>

  int main(void)
{
    int i = 100;
    printf("%zu\n",sizeof(i));
    printf("%zu\n",sizeof i);//因为 i 是变量,所以可以省略括号
    printf("%zu\n",sizeof (int));
    printf("%zu\n",sizeof (2 + 1));
    
    return 0;
}

那么sizeof的返回值是什么类型呢?由于这里还没有涉及符号的概念,会在下文2讲到符号的时候详细阐述。


1.7 数据类型长度
c 复制代码
#include <stdio.h>

  int main(void)
{
     printf("%zu\n",sizeof(char));
     printf("%zu\n",sizeof(_Bool));
     printf("%zu\n",sizeof(short));
     printf("%zu\n",sizeof(int));
     printf("%zu\n",sizeof(long));
     printf("%zu\n",sizeof(long long));
     printf("%zu\n",sizeof(float));
     printf("%zu\n",sizeof(double));
     printf("%zu\n",sizeof(long double));

     return 0;

}

这里重申:
sizeof操作符,可以用来计算sizeof中包含的操作数的类型长度,其单位是字节(1 字节 = 8 比特)


在Debian12.0 + gcc x86_64环境下会输出:

bash 复制代码
1
1
2
4
8
8
4
8
16

在Windows下MinGw-w64环境会输出:

bash 复制代码
1
1
2
4
4
8
4
8
16

在Windows下使用VS2026 x64即MSVC环境下则会输出:

bash 复制代码
1
1
2
4
4
8
4
8
8

这三组输出分别来自三种主流 C 编译环境,差异来源于操作系统 ABI(应用二进制接口)和编译器实现策略的不同:

1.long 大小不同:8 vs 4

复制代码
原因:数据模型(Data Model)不同
    Linux / macOS中使用LP64 模型:
        Long = 64 位(8 字节)
        Pointer = 64 位
        Int = 32 位
        → 称为 LP64
    Windows中使用LLP64 模型:
        Long = 32 位(4 字节)
        Long Long = 64 位
        Pointer = 64 位
        → 称为 LLP64

所有 Windows 编译器(包括 MinGW 和 MSVC)都遵循 LLP64,因此 sizeof(long) == 4。

而 Linux/GCC 遵循 LP64,故 sizeof(long) == 8。

这些就是跨平台整数类型不一致的最常见来源。

2.long double 大小不同:16 vs 8

复制代码
    GCC(含 MinGW)行为:
    在 x86/x86_64 上使用 x87 FPU 的 80 位扩展精度浮点格式
    实际有效数据:10 字节(80 位)
    但为满足 16 字节对齐要求(x86_64 ABI 规定),sizeof(long double) = 16
    MSVC 行为:
    不支持 80 位扩展精度
    将 long double 直接等同于 double
    因此 sizeof(long double) = sizeof(double) = 8

这意味着:在 MSVC 下,long double 不会提供比 double 更高的精度,而 GCC 下会(约 19 位十进制精度 vs 15~17)。

因此跨平台开发要注意这些差异

  1. 其他类型为何一致?

    复制代码
     char:C 标准规定 sizeof(char) == 1
     _Bool:C99 标准要求最小可寻址单位,通常为 1 字节(所有现代编译器遵守)
     short/int/long long/float/double:
         在 32/64 位主流平台上,这些类型的大小已高度统一
         尤其 int 几乎总是 32 位(即使在 64 位系统)
         long long 自 C99 起保证 ≥64 位,各编译器均实现为 8 字节

总结一下:

差异点 根本原因
long 大小 操作系统 ABI 不同:Linux 使用 LP64,Windows 使用 LLP64
long double 大小 编译器实现策略不同:GCC 支持 x87 扩展精度,MSVC 不支持并降级为 double

而根据 C 标准(C11 §5.2.4.2.1 / C17 同理):

复制代码
    int 至少 16 位
    long 至少 32 位
    并且必须满足:
    c

    sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)

因此:

复制代码
sizeof(long) >= sizeof(int) 永远成立
实际常见情况:
    Linux x86_64:int=4, long=8 → 满足
    Windows x64:int=4, long=4 → 相等,也满足 ≥

又根据 C 标准(C11 §5.2.4.2.2 / Floating-point types):

复制代码
The values of the floating types are specified in terms of precision and range, and:

    float ≤ double ≤ long double in both precision and storage size.

虽然标准没有直接写 sizeof(long double) >= sizeof(double),但它规定了:

复制代码
double 的精度 不低于 float
long double 的精度 不低于 double
而在所有现实实现中,更高的精度必须通过更大的存储空间实现

因此,所有主流编译器和平台都保证:

sizeof(float) <= sizeof(double) <= sizeof(long double)

实际常见情况:

平台 sizeof(double) sizeof(long double)
所有平台 8 ---
Linux (GCC/x86_64) 8 16(含填充)或 12(有效 80 位)
Windows (MSVC) 8 8(long double 等同于 double
Windows (MinGW-w64) 8 16

即使 MSVC 中两者相等(8 == 8),仍然满足 >=
故:
sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long);
sizeof(float) <= sizeof(double) <= sizeof(long double),是始终成立的
也可简单记忆差异项:
sizeof(long)>=sizeof(int);
sizeof(long double)>=sizeof(double)


一些跨平台开发建议:

  • 避免依赖long的大小

  • 使用 stdint.h 中的固定宽度类型:

    c 复制代码
      #include <stdint.h>
      int32_t a;   // 明确 32 位
      int64_t b;   // 明确 64 位
  • 不要假设 long double 有更高精度
    → 若需高精度,使用第三方库(如 MPFR、Boost.Multiprecision)
    → 或明确使用 double 并接受其限制
    size_t 表示对象大小,intptr_t 表示指针整数转换


1.8sizeof操作符中的表达式不计算

可以通过以下验证:

c 复制代码
#include <stdio.h>
  int main(void)
{
    short a = 10;
    int b = 100;
    printf("%zu\n", sizeof(a = b + 1));
    printf("s = %d\n", a);
    
return 0;

终端会输出:

bash 复制代码
2
10

2. signed 和 unsigned以及数据类型的取值范围

signedunsigned是两个关键字,用来修饰字符型和整型

signed表示此类型带有正负号
unsigned表示此类型不带有正负号,只能表示0或正整数。

例如,对int类型,默认携带正负号,前面的unsigned是可以省略不写的,默认携带正负号,即使加上去变成signed int也不会有什么错误,只是稍显繁杂。即:

c 复制代码
signed int a == int a;

对于不携带正负号的int,就必须加上unsigned声明修饰变量:

c 复制代码
unsigned int a;

修饰unsigned可以让相同长度的内存存储更大的数字,实际增加了一倍,下面会对不同的编译环境进行比较:

不同编译器(MSVC、MinGW、GCC x86_64)下的行为:

2.1整数变量

将整数变量声明为 unsigned,可以在相同内存长度下使最大可表示值翻倍。例如:

  • signed short int(16 位):范围为 -32768 ~ 32767,最大值为 32767
  • unsigned short int(16 位):范围为 0 ~ 65535,最大值为 65535

这一特性在 MSVC(Visual Studio)、MinGW、GCC(x86_64 平台)下完全一致 ,因为这些编译器在 x86_64 架构上对 shortint 的大小定义是统一的。


2.1.1各编译器 <limits.h> 中整数定义对比(x86_64)
1. Microsoft Visual Studio (MSVC)
c 复制代码
#define SHRT_MIN    (-32768)
#define SHRT_MAX      32767
#define USHRT_MAX     0xFFFF        // 65535

#define INT_MIN     (-2147483647 - 1)  // -2147483648
#define INT_MAX       2147483647
#define UINT_MAX      0xFFFFFFFFU      // 4294967295
2. MinGW-w64(Windows 上的 GCC)
c 复制代码
#define SHRT_MIN    (-32768)
#define SHRT_MAX      32767
#define USHRT_MAX     65535          // 或 0xFFFF

#define INT_MIN     (-2147483647 - 1)
#define INT_MAX       2147483647
#define UINT_MAX      4294967295U    // 或 0xFFFFFFFFU
3. GCC(Linux / macOS, x86_64)
c 复制代码
#define SHRT_MAX    32767
#define USHRT_MAX   65535
#define INT_MAX     2147483647
#define UINT_MAX    4294967295U

因此所有主流 x86_64 编译器均规定:

复制代码
sizeof(short) == 2
sizeof(int) == 4
因此,unsigned short 最大值恒为 65535,unsigned int 恒为 4294967295

差异在于long,在MSVC(LLP64)中long是四个字节,而在MinGW/GCC(LP64)中long是八个字节,这个差异在本文1.7 已经详细讨论过。

但是我们此处在讨论"unsigned 使最大值翻倍",此差异不影响 short 和 int,因此忽略long即可,但我们要知道差异。


我们小结一下:

  • unsigned 确实让相同位宽的整型最大值翻倍

    (从 2\^{n-1} - 12\^n - 1)。

  • 在 x86_64 平台上,MSVC、MinGW、GCC 对 shortint 的定义完全一致。

  • 差异仅存在于 long 类型。

    所以:我们可安全跨平台使用 unsigned short / unsigned int 获取更大的非负整数范围。


2.2字符型变量

字符类型char也可以被signed,unsigned修饰,

也可以显式声明为:

c 复制代码
signed char//范围在-128-127
unsigned char//0-255

要注意,C语言规定char的符号性由实现定义,由系统实现决定。

因此,char不等同于signed char,他可能是signed char,也可能是unsigned char。它在某些平台上等价于 signed char,在另一些平台上则等价于 unsigned char。


1. Microsoft Visual Studio (MSVC)
c 复制代码
#define SCHAR_MIN   (-128)  // signed char 最小值
#define SCHAR_MAX   127     // signed char 最大值
#define UCHAR_MAX   0xFF    // unsigned char 最大值(255)
2. MinGW-w64(Windows 上的 GCC)
c 复制代码
#define SCHAR_MIN   (-128)
#define SCHAR_MAX   127
#define UCHAR_MAX   255     // 或 0xFF
3. GCC(Linux / macOS, x86_64)
c 复制代码
#define SCHAR_MIN   (-128)
#define SCHAR_MAX   127
#define UCHAR_MAX   255     // 或 0xFF

这一点与 int 不同:int 始终等同于 signed int,其符号性是明确且不可变的。

这时候可能会有疑问:
为什么 char 也有正负之分?

char 本质是 1 字节(8比特位)的整数类型,并非仅用于字符。因此它可以像其他整数一样分为:

  • signed char:范围 -128 ~ 127
  • unsigned char:范围 0 ~ 255

C 标准规定:char 的默认符号性由编译器决定 ,它可能是 signed,也可能是 unsigned不等同于 signed char

因此处理字节或数值时,应显式使用 signed char / unsigned char,或更清晰的 int8_t / uint8_t

读者若还有困惑,可以想象 char 是一个 8格的小盒子(1字节 = 8位):

  • signed char:有1格当"符号牌"(最高位),剩下7格存数字 → 能装 -128 ~ 127。
  • unsigned char:8格全用来存数 → 能装 0 ~ 255。

同一个盒子,用法不同。

再简化来说:我有存放的能力,但我用不用是另一回事。

希望可以让读者清晰理解。


2.3 数据类型的取值范围

C 提供 shortintlonglong long 等整型,是因为它们取值范围不同,可在不同场景选择最合适的类型,兼顾内存和性能。

要查看各类型的极限值,请使用标准头文件中的常量:

  • 整型范围 :定义在 <limits.h>
    • SCHAR_MIN / SCHAR_MAXsigned char
    • SHRT_MIN / SHRT_MAXshort
    • INT_MIN / INT_MAXint
    • LONG_MIN / LONG_MAXlong
    • LLONG_MIN / LLONG_MAXlong long
    • UCHAR_MAXUSHRT_MAXUINT_MAXULONG_MAXULLONG_MAX:对应无符号类型的最大值

为保证可移植性,应使用这些常量,而非硬编码数值。

笔者在2.12.2 广泛使用了limits.h里的数据。

limits.h 文件中说明了整型类型的取值范围。
float.h 文件中说明浮点型类型的取值范围。

由于浮点数类型取值范围稍微复杂,但原理类似,因此不再本文进行讨论。

这里已经讲清楚了符号的概念,对于运算符sizeof的返回值,其实C语言中值规定了它是无符号的整数,没有指定具体的类型,因此这个返回值的类型是系统自主决定的,有可能是unsigned int,unsigned long,unsigned long long,无法确切知晓,其printf()中对应的占位符分别是:%u,%lu,%llu。我们很难选择,也不利于程序的可以移植性。
因此C语言提供了一个类型别名size_t来统一表示sizeof的返回值类型,让其对应当前系统的sizeof的返回值类型,这个返回值的类型是什么?我们不需要知道也不需要不关心,交给编译器去做,这是它的事情。我们只需要知道size_t,和%zu,并且了解size_t是可以单独使用的就可以了。
就像这样:

c 复制代码
#include <stdio.h>
    int main(void)
{
      int a = 10;
      printf("%zu\n", sizeof(a));
      printf("%zu\n", sizeof a);
      //a是变量的名字,可以省略掉sizeof后边的()
      printf("%zu\n", sizeof(int));
      printf("%zu\n", sizeof(3 + 3.5));
  
    return 0;
}

3. 变量

我们已经搞清楚了类型,类型是用来创建变量 的。

C语言中把经常变化的值成为变量 ,不变的量成为常量

3.1 变量的创建

变量命名的一般规则

  1. 首字符:必须是字母(a--zA--Z)或下划线 _不能以数字开头
  2. 组成字符:只能包含字母、数字(0--9)和下划线。
  3. 区分大小写:countCount 是两个不同的变量。
  4. 不能使用关键字:如 intiffor 等保留字不可作变量名。
  5. 建议
    • 使用有意义的英文单词或缩写(如 studentCountmax_value)。
    • 避免单个字母(除非在简单循环中,如 ij)。
    • 保持风格一致(推荐小写字母 + 下划线,或驼峰命名法)。
c 复制代码
int age;
char name;
double weight;

创建变量的同时给予一个初始值,叫做初始化:

c 复制代码
int age = 18;
char name = 'X';
double weight = 66.6;
unsigned int height = 180;
3.2 变量的分类
  • 全局变量:在大括号外定义的变量

    全局变量使用范围很广,一般是整个项目都想使用的变量。

  • 局部变量:在大括号内定义的变量就是局部变量

    局部变量的适用范围比较局限,仅能在其所处的局部范围内使用。

c 复制代码
#include <stdio.h>
     int global = 2026; //全局变量
  int main (void)  
  {
      int local = 2000;//局部变量
      printf("%d\n",local);
      printf("%d\n",global);
      return 0;

如果局部变量与全局名称相同,则优先使用局部变量。

这点与git的全局:--global user.name ,局部:user.name类似,都是优先使用局部的。

全局变量和局部变量在内存中存储的位置存在差异:

  • 局部变量存放在内存的栈区
  • 全局变量存放在内存的静态区
  • 堆区则用来动态管理内存

这里只是粗略划分,就是简单提及。

4. 算术操作符

在书写代码的时候难免会涉及计算,因此C语言提供了一系列操作符用来方便运算,其中有一组操作符叫做算术操作符:+ - * / %

这些操作符也叫做双目操作符,因为会有两个操作数。有时候也会把算术操作符叫做算术运算符,这只是翻译差异。

4.1 + 和 -

+-用来加减运算
+-的两端就是他们的操作数

c 复制代码
#include <stdio.h>
    int main(void)
{
      int x = 1 + 1;
      int y = 2 - 1;
      printf("%d\n", x);
      printf("%d\n", y);
      
    return 0;
}
4.2 *

*用来乘法运算

c 复制代码
#include <stdio.h>
    int main(void)
{
      int num = 5;
      printf("%d\n", num * num); // 输出 25
    
    return 0;
4.3 /

/用来除法运算

其两端如果是整数就会执行整数除法,结果为整数

c 复制代码
#include <stdio.h>
    int main(void)
{
      float x = 6 / 4;
      int y = 6 / 4;
      printf("%f\n", x); // 输出 1.000000
      printf("%d\n", y); // 输出 1
    
     return 0;
}

上面代码中,尽管变量 x 的类型是float (浮点数),但是6 / 4 得到的结果是1.0 ,而不是1.5 。

原因就在于 C 语言里面的整数除法是整除,只返回整数部分,会直接丢弃小数部分。

如果想要得到浮点数的结果,两个运算数里至少需要有一个浮点数。

c 复制代码
#include <stdio.h>
    int main(void)
{
      float x = 6.0 / 4; // 或者写成 6 / 4.0
      printf("%f\n", x); // 输出 1.500000
  
    return 0;
}  

保留6位小数是默认设置。

要重视整数除法会丢弃小数部分:

c 复制代码
#include <stdio.h>
    int main(void)
{
      int i = 5;
      i = ( i / 20) * 100;

    return 0;
}

经过运算,i会变成 0而不是25,因为i/20是整除,会直接丢弃小数得到:0

要想得到预期的结果,把20改成20.0就可以了,将整数除法编程浮点数除法。

4.4 %

%用来取模(余)运算,会返回两个整数相除的余数值。

取模运算%只可以用整数,不可是浮点数。

c 复制代码
#include <stdio.h>
    int main(void)
{
      int x = 6 % 4; // 2
    
    return 0;
}

对负数取模,其结果的符号取决于第一个运算数的符号:

c 复制代码
#include <stdio.h>
    int main(void)
{
      printf("%d\n", 11 % -5); // 1
      printf("%d\n",-11 % -5); // -1
      printf("%d\n",-11 % 5); // -1

    return 0;
}

5. 赋值操作符 =

在本文3.1 里讲了创建变量的同时给予一个初始值,叫做初始化。

在在变量创建好后,再给一个值,就叫赋值。

赋值操作符:=是一个可以随时给变量赋值的操作符

5.1 连续赋值

赋值操作符支持连续赋值:

c 复制代码
    int a = 1;
    int b = 2;
    int c = 0;
    c = b = a+3;//连续赋值

虽然C语言支持这种格式,但这种格式不便于理解,因此还是建议拆开来写:

c 复制代码
    int a = 1;
    int b = 2;
    int c = 0;
    b = a+3; 
    c = b;

这样书写在调试的时候,每次赋值的细节就很方便观察了。


5.2 复合赋值符

书写代码的过程中,我们可能经常对一个数进行自增减操作:

c 复制代码
    int a = 10;
    a = a+3;  
    a = a-2;

更方便的写法:

复制代码
    int a = 10;
    a += 3;  
    a -= 2;

复合赋值符可以方便程序员编写代码:

复制代码
+=      -=
*=      /=
//下面这些以后学习

>>=     <<=
&=  |=  ^=

6. 单目操作符:++,--,+,-

有两个操作数的操作符是双目操作符,单目操作符很显然就是只有一个操作数。++,--,+(正),-(负)就是单目操作符。

6.1 ++ 和 --

++是自增的操作符,分为前置 ++ 和后置 ++,--是自减的操作符,也分为前置 -- 和后置 --。

6.1.1 前置 ++
c 复制代码
    int a = 10;
    int b = ++a;
    printf("a=%d b=%d",a,b);

先+1,后使用

6.1.2 后置 ++
c 复制代码
    int a = 10;
    int b = a++;
    printf("a=%d b=%d",a,b);

先使用,后+1

对于自增变量本身而言,前后置++没有区别,但对于赋值的变量来讲,就有先后之分了。6.1.2的代码也等价于:

c 复制代码
int a = 10;
int b = a;
a = a + 1;
printf("a=%d b=%d\n",a,b);
6.1.3 前置--

如果搞懂了前置++,那么理解前置--就毫无困难,只是把+1改成-1

c 复制代码
int a = 10;
int b = --a;
printf("a=%d b=%d\n",a,b);//输出9 9

先-1,后使用

6.1.4 后置--
c 复制代码
int a = 10;
int b = a--;
printf("a=%d b=%d\n",a,b);//输出 9 10

先使用,后-1

6.2 +和-

要注意这里的+和-是正负号,是单目操作符

运算符+对于正负号没有影响,完全可以忽略不写:

c 复制代码
int a = +10;等价于==>int a = 10;

运算符-则用来改变值的正负号,负数前加入-得到正数,正数前加入-得到负数

c 复制代码
int a = 10;
int b = -a;
int c = -10;
printf("b=%d c=%d\n",b,c);//b和c都是-10

int a = -10;
int b = -a;
printf("b=%d\n",b);b是10

7. 强制类型转换

强制类型转换是特殊的操作符,其语法很简单:

c 复制代码
(这里写入类型)

例如:

c 复制代码
int a = 1.11;
//a是int类型,1.11是double类型,因此编译器会警告
c 复制代码
int a = (int)1.11;
//强制将double的类型转换为int类型,这样强制转换只取整数部分

强制转换类型要在特定情况,或者尽量避免使用。否则会丢失数据


8. scanf 和 printf 函数

8.1 printf函数

8.1.1 printf函数的基本用法

printf()可以将参数文本输出到屏幕上,其f表示format格式化,定制输出文本的格式。

c 复制代码
#include <stdio.h>
int main (void)
{
    printf("Hello World!");
    return 0;
}

运行上述代码会在屏幕输出"Hello World!"。
printf()的换行符会在运行结束后停留在输出结束的地方,不会自动换行,需要使用\n来移动光标到下一行。

c 复制代码
#include <stdio.h>
int main (void)
{
    printf("Hello World!\n");
    return 0;

想要在文本内部换行也可以,就像这样:

c 复制代码
#include <stdio.h>
  int main(void)
{
    printf("Hello\nWorld\n");
    //等价
    printf("Hello\n");
    printf("World\n");
    
    return 0;
}

printf()是标准库函数,定义在标准库的头函数stdio.h中,因此使用此函数就必须在头源码文件头部引用这个头文件,其中i是input,o是output。


8.1.2 占位符

printf()里面可以在输出文本里指定占位符

占位符就是占位符的位置可以输入其他值代替,占个位置。

c 复制代码
#include <stdio.h>
int main(void)
{
    printf("There are %d pens\n",10);
    return 0;
}

输出文本是There are %d pens\n,%d是占位符,这个位置会被其他值代替。这个占位符的第一个字符一律是%,第二个字符表示占位符的类型,%d表示这个代替的值必须是一个整数。


printf()函数的第二个参数就是替换占位符的值,上文代码的例子就是整数10替换%d,代码执行的输出结果就是There are 10 pens


%d是比较常用的占位符,还有%s,其表示带入的是一个字符串

c 复制代码
#include <stdio.h>
  int main(void)
{
    printf("%s is coming now\n", "Wang");
    return 0;
}

这里的%s表示代入的是一个字符串,因此printf()的第二个参数就必须是字符串。


输出文本里也可以使用多个占位符。

c 复制代码
#include <stdio.h>
  int main(void)
{
    printf("%s says it is %d o'clock\n", "Li", 10);
    return 0;
}

这里的输出文本%s says it is %d o'clock 有两个占位符,第一个是字符串占位符%s ,第二个是整数占位符%d ,分别对应printf() 的第二个参数Li和第三个参数10。执行后的输出就是Li says it is 10 o'clock

printf()函数的参数与占位符是一一对应的关系,有n个占位符,printf()会有n+1个参数,如果参数个数少于对应的占位符,printf()就会输出内存中的任意值。


8.1.3 占位符列举
  • %a:十六进制浮点数,字母输出为小写。
  • %A:十六进制浮点数,字母输出为大写。
  • %c:字符。//char
  • %d:十进制整数(有符号的10进制整数)。// int
  • %e:使用科学计数法的浮点数,指数部分的 e 为小写。
  • %E:使用科学计数法的浮点数,指数部分的 E 为大写。
  • %i:整数,基本等同于 %d 。
  • %f:小数(包含 float 类型和 double 类型)。
    //float: %f double : %lf
  • %g:6个有效数字的浮点数。整数部分一旦超过6位,就会自动转为科学计数法,指数部分的 e 为小写。
  • %G:等同于 %g ,唯一的区别是指数部分的 E 为大写。
  • %hd:十进制 short int 类型。
  • %ho:八进制 short int 类型。
  • %hx:十六进制 short int 类型。
  • %hu:unsigned short int 类型。
  • %ld:十进制 long int 类型。
  • %lo:八进制 long int 类型。
  • %lx:十六进制 long int 类型。
  • %lu:unsigned long int 类型。
  • %lld:十进制 long long int 类型。
  • %llo:八进制 long long int 类型。
  • %llx:十六进制long long int类型。
  • %llu:unsigned long long int类型。
  • %Le:科学计数法表示的long double类型浮点数。
  • %Lf:long double类型浮点数。
  • %n:已输出的字符串数量。该占位符本身不输出,只将值存储在指定变量之中。
  • %o:八进制整数。
  • %p:指针(用来打印地址)。
  • %s:字符串。
  • %u:无符号整数(unsigned int)。
  • %x:十六进制整数。
  • %zu:size_t类型。
  • %%:输出一个百分号。

比较重要的已经加粗,会查表即可,用多了自然就熟悉了。

8.1.4 输出格式

printf()可以定制占位符的输出格式。

8.1.4.1 限定格式

printf()允许限定占位符的最小宽度:

c 复制代码
#include <stdio.h>
  int main(void)
{
    printf("%5d\n", 123); // 输出为 " 123"
    return 0;
}

%5d表示这个占位符的宽度至少为5位。如果不满5位,对应的值前面会添加空格。

输出的值默认是右对齐,因此输出内容前面会有空格;

如果希望改成左对齐,在输出内容后面添加空格,可以在占位符的% 的后面插入一个-号。

c 复制代码
#include <stdio.h>
  int main(void)
{
  printf("%-5d\n", 123); // 输出为 "123 "
  return 0;
}

对于小数而言,上述的限定符会限制所有数字的最小显示宽度

c 复制代码
#include <stdio.h>
  int main(void)
{
    printf("%12f\n", 12.345);
    return 0;
}

%12f会限制输出的浮点数最少要维持12位的形式,而由于小数的默认显示精度是小数点后6位,因此输出的结果12.345前会添加2个空格。


8.1.4.2 输出显示正负号

printf()在一般情况下不会在正数的前方显示+,仅对负数显示-,如果想要正数也输出+,就需要在占位符%后添加一个+

c 复制代码
#include <stdio.h>
  int main(void)
{
    printf("%+d\n", 12); // 输出 +12
    printf("%+d\n", -12); // 输出 -12
    return 0;
}

这样就可以让输出的数值始终携带正负号了。


8.1.4.3 限定小数位数

输出小数的时候,有时候也想要限定小数点后的位数,就比如只想保留四位小数,占位符就可以写成%.4f

c 复制代码
#include <stdio.h>
  int main(void)
{
    printf("The number is %.4f\n", 0.5);
    return 0;//输出0.5000
}

这种写法可以和限定宽度占位符结合使用:

c 复制代码
#include <stdio.h>
  int main(void)
{
    printf("The number is %8.4f\n", 0.5);
    return 0;//输出  0.5000,前面会有两个空格
}

宽度和小数位数这两个限定值可以用*代替,转而通过printf()的参数传入:

c 复制代码
#include <stdio.h>
  int main(void)
{
    printf("The number is %*.*f\n",8,4,0.5);
    return 0;//输出  0.5000,前面会有两个空格
}

这样写法就便于改变参数了,方便了不少。

8.1.4.4 输出部分字符串

输出字符串的占位符是:%s,默认全部输出。若想要输出部分,可以使用%.[m]s指定输出的长度,这里的[m]是一个数字,表示想要输出的长度,这个数字是从前向后计算的,因此这个部分就是开头的部分。

c 复制代码
#include <stdio.h>
  int main(void)
{
    printf("%.5s\n", "Hello world");
    return 0;
}

输出结果就是Hello,前5位。


8.2 scanf函数

前面介绍了printf()函数用过输出结果,而scanf()函数就是输入值给变量的:

c 复制代码
#include <stdio.h>
  int main(void)
{
    int a = 1;
    printf("请输入您的身高:");
    scanf("%d",&a);
    printf("您的身高是:%d\n",a);
    return 0;
}

8.2.1 scanf()的基本用法

scanf() 函数用于读取键盘输入。

当程序运行到这条语句时,会暂停执行,等待用户从键盘输入数据。

当用户输入数据并按下回车键后,scanf() 会处理这些输入,并将它们存入指定的变量中。

它的函数原型定义在 stdio.h 头文件内,语法结构与 printf() 相似。

c 复制代码
int i = 0;
scanf("%d",&i);

scanf() 的第一个参数是格式字符串,其中包含占位符,用法和 printf() 的占位符大致相同。

C语言中的数据都有特定的类型,scanf() 必须提前知道输入的数据类型才能进行正确处理。

scanf() 函数的其他参数是用来存放用户输入的变量,格式字符串里有多少个占位符,后面就需要对应多少个变量。

对于上面的代码块:

  • scanf()的第一个参数 "%d" 提前规定了输入的应该是一个整数
  • "%d" 就是占位符,其中%是占位符的标志,d 代表整数
  • 第二个参数 "&i" 表示将用户输入的整数存入变量 i

变量前方必须添加 & 符号(指针变量除外)来获取地址。因为 scanf() 传递的是内存地址,程序需要通过变量 i 的地址,将输入的值存入该地址对应的内存空间中。


从键盘读取多个变量就像如此:

c 复制代码
scanf("%d%d%f%f", &a, &b, &c, &d);

格式字符串%d%d%f%f表示用户输入的前两个是整数,后两个是浮点数,比如10 -10 3.14 -1.0e4 。这四个值依次放入abcd 四个变量。

scanf() 处理数值占位符时,会自动过滤空白字符,包括空格、制表符、换行符等等。

因此,用户输入的数据之间,有一个或多个空格不影响scanf() 解读数据。即使用户使用回车键,将输入分成几行,同样不影响解读。

c 复制代码
10
-10
3.14
1.0e4

即使分成四行输入,得到的结果与一行输入是完全一样的。每次按下回车键后,scanf() 就会开始读取。如果第一行的内容匹配了第一个占位符,那么下次按下回车键时,就会按顺序从第二个占位符开始读取。

scanf() 处理用户输入的原理是:先将用户的输入存入输入缓冲区,等到按下回车键后,再按照占位符的要求对缓冲区内的内容进行读取。

读取数据时,会从上一次读取遗留的第一个字符开始,直到读完缓冲区,或者遇到第一个不符合条件的字符为止。

c 复制代码
#include <stdio.h>
  int main(void)
{
    int x;
    float y;
    // 输入 " -13.45e12# 0"
    scanf("%d", &x);
    printf("%d\n", x);
    scanf("%f", &y);
    printf("%f\n", y);
  
    return 0;
}

在上述示例中,scanf() 读取用户输入时,%d 占位符会忽略开头的空格,从 - 开始读取,到 . 处停止(因为 . 不属于整数的有效字符),因此读取到的值为 -13

第二次调用 scanf() 时,会从上一次停止的位置继续读取。

此时首字符是.,由于对应的占位符是 %f,程序会读取到 .45e12(符合科学计数法的浮点数格式),遇到 # 时停止(# 不属于浮点数的有效字符)。

由于 scanf() 支持连续处理多个占位符,上述示例也可以简写为以下形式:

c 复制代码
#include <stdio.h>
  int main(void)
{
    int x;
    float y;
    // 输入 " -13.45e12# 0"
    scanf("%d%f", &x, &y);
    return 0;
}

相信你应该可以掌握了scanf()函数的基本用法了。

8.2.2 scanf()函数的返回值

scanf()函数的返回值是一个整数,表示成功读取到的变量的个数。

如果没有读取任何项,或者匹配失败,则返回0

如果在成功读取任何数据之前,发生了读取错误或者遇到读取到文件结尾,则返回常量 EOF (-1)。

EOF - end of file 文件结束标志

c 复制代码
#include <stdio.h>
  int main(void)
{
    int a = 0;
    int b = 0;
    float f = 0.0f;
    int r = scanf("%d %d %f", &a, &b, &f);
    printf("a=%d b=%d f=%f\n", a, b, f);
    printf("r = %d\n", r);
    
    return 0;
}

输入输出测试:

bash 复制代码
1 2 3.14
a=1 b=2 f=3.140000
r=3

如果在输入2个数后,按ctrl+z提前介入输入的话:

bash 复制代码
1 2
^Z
^Z
^Z
a=1 b=2 f=0.000000
r=2

在VS环境中按3次ctrl+z ,才结束了输入,我们可以看到r是2,表示正确读取了2个数值。

如果一个数字都不输入,直接按3次ctrl+z ,输出的r是-1,也就是EOF

bash 复制代码
^Z
^Z
^Z
a=0 b=0 f=0.000000
r=-1

8.2.3 占位符

scanf() 常用的占位符如下,与 printf() 的占位符基本一致。

  • %c:字符。
  • %d:整数。
  • %ffloat 类型浮点数。
  • %lfdouble 类型浮点数。
  • %Lflong double 类型浮点数。
  • %s:字符串。
  • %[]:在方括号中指定一组匹配的字符(比如 %[0-9]),遇到不在集合之中的字符,匹配将会停止。

scanf() 的所有占位符中,除 %c 外,其余都会自动忽略开头的空白字符。%c 不忽略空白字符,总是读取当前的第一个字符,无论它是否为空格。

若要强制跳过空白字符再读取,可写成 scanf(" %c", &ch),即在 %c 前加一个空格,表示跳过零个或多个空白字符。

占位符 %s 的规则是从第一个非空白字符开始读取,直到遇到空白字符(空格、换行符、制表符等)为止。

由于 %s 不包含空白字符,无法读取多个单词,除非连续使用多个 %s。这意味着 scanf() 不适合读取含空格的字符串(如书名)。此外,scanf() 会在字符串变量末尾自动存储空字符 \0

scanf() 读取字符串到数组时,不会检测长度是否越界,可能导致溢出。为防止这种情况,应指定最大读取长度,写成 %[m]s,其中 [m] 为整数,表示最大长度,超出部分将被丢弃。

c 复制代码
#include <stdio.h>

int main()
{
    char name[11];
    scanf("%10s", name);

    return 0;
}

上例中,name 是长度为 11 的字符数组,%10s 表示最多读取 10 个字符,剩余字符被丢弃,从而避免数组溢出。


由于上述安全性问题(如缓冲区溢出风险),在 Visual Studio (VS) 开发环境中编译时,编译器会标记 scanf() 函数为"不安全",并推荐使用微软特有的安全函数(如 scanf_s)。

然而,scanf_s 并非标准 C 语言的一部分,使用它会导致代码无法在其他编译器(如 GCC、Clang)或操作系统(如 Linux、macOS)上编译,这对跨平台开发是非常不利的。

为了在 VS 中继续使用标准的 scanf() 函数并保持代码的跨平台兼容性,通常的做法是在代码的最开头定义一个宏,以关闭 VS 的这种安全警告。

在源文件的第一行添加以下代码:

c 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

每次都要手动添加还是太繁琐了,有没有自动的方法?

一个小知识点:

Visual Studio 在创建新源文件时,实际上是复制一个预定义的模板文件。

要实现自动添加,只需找到安装目录下的 newc++file.cpp(通常位于 VC++ 模板文件夹内)。

将 define _CRT_SECURE_NO_WARNINGS 1 写入并保存。此后每次新建文件,该定义便会自动包含在内。

笔者的注记:
鉴于笔者个人并不习惯使用 VS 开发环境,后续文章中不再详细补充此方法的具体操作步骤。


8.2.4 赋值忽略符

有时候,笨蛋用户输入的格式可能不符合预期

c 复制代码
#include <stdio.h>
  int main(void)
{
    int year = 0;
    int month = 0;
    int day = 0;
    scanf("%d-%d-%d", &year, &month, &day);
    printf("%d %d %d\n", year, month, day);
    
    return 0;
}

上例中,若用户输入 2020-01-01,程序能正确解析。但若输入格式改变(如 2020/01/01),scanf() 解析便会失败。

为解决此问题,scanf() 提供了赋值忽略符 (assignment suppression character)*。将其加在占位符的 % 之后(如 %*c),该占位符读取的数据将被丢弃,不会赋值给任何变量。

c 复制代码
#include <stdio.h>

  int main(void)
{
    int year = 0;
    int month = 0;
    int day = 0;

    // 使用 %*c 忽略任意分隔符
    scanf("%d%*c%d%*c%d", &year, &month, &day);

    return 0;
}

上例中,%*c 表示读取一个字符但不赋值给任何变量(即忽略分隔符),从而能够兼容不同的日期输入格式。

代码中写成int main(void)是笔者本人的习惯,可以直接写成int main()

由于现实繁忙没有及时更新博客,实属抱歉,感谢为我评论鼓励的伙伴,也为我增添了很多动力。
欢迎交流指正!