前言
本文介绍数据类型和变数据,运算符和输入输出相关内容。
(【由浅入深】是一个系列文章,它记录了我个人作为一个小白,在学习c++技术开发方向计相关知识过程中的笔记,欢迎各位彭于晏刘亦菲从中指出我的错误并且与我共同学习进步,作为该系列的第一部曲-c语言,大部分知识会根据本人所学和我的助手——通义,DeepSeek,腾讯元宝等以及合并网络上所找到的相关资料进行核实誊抄,每一篇文章都可能会因为一些错误在后续时间增删改查,因为该系列按照我的网络课程学习笔记形式编写,我会使用绝大多数人使用的讲解顺序编写,所以基础框架和大部分内容案例会与他人一样,基础知识不会过于详细讲述)
一.变量与常量数据
1.1 变量概述
-
变量是在程序执行过程中其值可以改变的量。在C语言中,变量代表可变的存储位置,需要在程序中先定义后使用。
-
变量的定义格式:
数据类型 变量名;
如:
int age; // 定义整型变量
float salary; // 定义浮点型变量
char grade; // 定义字符型变量 -
变量的初始化:变量可以在定义时初始化,也可以在定义后赋值:
int count = 0; // 定义并初始化
float pi = 3.14; // 定义并初始化
char ch; // 仅定义
ch = ‘A’; // 后续赋值 -
变量的作用域:变量的作用域决定了它在程序中可以被访问的范围:
局部变量:在函数内部定义的变量,只在该函数内有效
全局变量:在函数外部定义的变量,整个程序中都有效
1.2 常量概述
- 常量是在程序执行过程中其值不能被改变的量。C语言中常量的值在初始化后不能被修改。
- 在C语言中,常变量使用关键字 const 定义,
基本语法:const 数据类型 常量名 = 初始化值;
示例:
const int age = 10; // 定义并初始化整型常变量
const char ch = ‘C’; // 定义并初始化字符型常变量
const float pi = 3.14159; // 定义并初始化浮点型常变量 - const 变量必须在定义时初始化(显式或隐式),初始化后不能再赋值,否则编译报错。
- 全局/静态const变量:可以不显式初始化,会被隐式初始化为0(或对应类型的零值),局部const变量:必须在定义时初始化,不能不初始化
注意:
- const定义的是"常变量"/只读变量,不是真正的常量
- const定义的变量会分配内存(在静态区),而真正的常量(如#define定义的)只是在预处理阶段进行文本替换,不分配内存,即没有在哪一个区的说法
- 在C语言中,const定义的变量不能用于数组大小(需要编译期常量),但#define定义的常量可以用于数组大小、case标签等需要常量表达式的场景。
#define SIZE 10
int arr[SIZE]; // 没问题 - const 关键字只是限制了这个变量的值不能被修改,但它仍然占用内存,在编译时被当作一个变量处理
- 真正的常量在C语言中通常是通过#define定义的(如#define PI 3.14),它只是在预处理阶段进行文本替换
- C语言中const定义的变量必须在声明时初始化,否则会导致编译错误。
1.3 全局常量与局部常量
- 全局常量:在整个程序中都可以访问的常量
#include <stdio.h>
const int num = 20052454; // 全局常量定义
int main() {
printf("该学校的招生代码是:%d\n", num); // 输出全局常量
return 0;
}
- 局部常量:只在其定义的代码块中有效的常量
#include <stdio.h>
const int num = 20052454; // 全局常量
int main() {
{
const int num1 = 19980235; // 局部常量
printf("该学校的招生代码是:%d\n", num); // 输出全局常量
printf("该专业的招生代码是:%d\n", num1); // 输出局部常量
} // 局部常量num1的作用域结束
printf("该学校的招生代码是:%d\n", num); // 仍然可以访问全局常量
return 0;
}
1.4 常量与变量的区别
| 特性 | 变量 | 常量 |
|---|---|---|
| 值是否可变 | 可以在程序执行过程中修改 | 一旦初始化后不能修改 |
| 定义方式 | 直接定义 | 使用const关键字定义 |
| 作用域 | 有局部和全局之分 | 有局部和全局之分 |
| 示例 | int count = 0; | const int MAX = 100; |
1.5 常量的类型
C语言中的常量主要包括:
- 整型常量:如10, -5, 0x1F(十六进制)
- 浮点型常量:如3.14, -0.5, 2.5e-3
- 字符常量:如’A’, ‘a’, ‘\n’
- 字符串常量:如"Hello", “C语言”
1.6 符号常量
除了使用const关键字定义常量外,C语言还允许使用#define定义符号常量:
#include<stdio.h>
#define PI 3.14159
#define MAX_SIZE 100
int main(){
// 使用符号常量
printf("圆周率是:%f\n", PI);
int array[MAX_SIZE]={0};
printf("%d", array[3]);
}
符号常量在预处理阶段被替换,不占用内存空间,而const常量是真正的变量,会占用内存。
1.7 注意事项
- 常量必须在定义时进行初始化,不能先定义后初始化
- const关键字定义的常量虽然值不能修改,但其本质仍是变量,只是值被保护不能修改
- 符号常量(#define)是预处理指令,不占用内存空间,但不如const常量安全
- 在C语言中,常量的类型在定义时就确定了,与变量一样遵循C语言的数据类型规则
补充:const图形化描述所在区域
高地址 0xFFFFFFFF
---------------------------------
| 内核空间 (Kernel Space) |
| (操作系统内核使用,用户程序无法访问) |
---------------------------------
| 命令行参数和环境变量 | <-- 栈的顶部附近
---------------------------------
| 栈区 (Stack) |
| 由高地址向低地址增长 |
| · 局部变量 |
| · 函数参数 |
| · 返回地址 |
| · 寄存器保存区 |
| (函数调用时自动分配,返回时自动释放)|
---------------------------------
| ↓ | (栈向下增长)
| | |
| | |
| | |
---------------------------------
| ↑ | (堆向上增长)
| | |
| | |
| | |
---------------------------------
| 堆区 (Heap) |
| 由低地址向高地址增长 |
| · malloc/free动态分配的内存 |
| · new/delete分配的对象 (C++) |
| (需程序员手动管理,易产生内存泄漏) |
---------------------------------
| 全局/静态存储区 (Global/Static) |
| (程序启动时分配,结束时释放) |
| · .data段: 已初始化的全局变量和静态变量|
| · .bss段: 未初始化的全局变量和静态变量|
| (程序加载时由OS初始化为0) |
---------------------------------
| 常量区 (Constant) |
| (只读,修改会导致段错误) |
| · 字符串常量 (如 "hello") |
| · const修饰的全局常量 |
| · 数字常量 (如 100, 3.14) |
---------------------------------
| 代码区 (Text/Code) |
| (只读,存储可执行指令) |
| · 函数体的二进制代码 |
| · 程序指令 |
---------------------------------
低地址 0x00000000
二. c语言基本数据类型
2.1 基本数据类型概述
C语言的基本数据类型可以分为以下几类:
- 整型
- 浮点型
- 字符型
- 布尔型
- 复数和虚数类型
- 空类型
2.2 整型
整型用于存储整数(没有小数部分的数值),可以是有符号的(signed)或无符号的(unsigned)。
- 基本整型
| 类型 | 通常大小(字节) | 有符号范围 | 无符号范围 | 说明 |
|---|---|---|---|---|
int | 4 | -2,147,483,648 ~ 2,147,483,647 | 0 ~ 4,294,967,295 | 标准整型,最常用的整型 |
short 或 short int | 2 | -32,768 ~ 32,767 | 0 ~ 65,535 | 短整型,节省内存 |
long 或 long int | 4或8 | -2,147,483,648 ~ 2,147,483,647(32位)或-9,223,372,036,854,775,808~9,223,372,036,854,775,807(64位) | 0 ~ 4,294,967,295(32位)或0 ~ 18,446,744,073,709,551,615(64位) | 长整型,更大范围 |
long long 或 long long int | 8 | -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 | 0 ~ 18,446,744,073,709,551,615 | C99标准引入,更长的整型 |
- 无符号整型
| 类型 | 通常大小(字节) | 范围 | 说明 |
|---|---|---|---|
unsigned int | 4 | 0 ~ 4,294,967,295 | 无符号标准整型 |
unsigned short 或 unsigned short int | 2 | 0 ~ 65,535 | 无符号短整型 |
unsigned long 或 unsigned long int | 4或8 | 0 ~ 4,294,967,295(32位)或0 ~ 18,446,744,073,709,551,615(64位) | 无符号长整型 |
unsigned long long 或 unsigned long long int | 8 | 0 ~ 18,446,744,073,709,551,615 | 无符号更长整型 |
注意:无符号整型比有符号整型可以表示更大的正数范围,因为没有符号位。
2.3 字符型
字符型用于存储单个字符,本质上是一种特殊的整型。
| 类型 | 通常大小(字节) | 有符号范围 | 无符号范围 | 说明 |
|---|---|---|---|---|
char | 1 | -128 ~ 127 | 0 ~ 255 | 标准字符型,通常有符号 |
signed char | 1 | -128 ~ 127 | - | 有符号字符型 |
unsigned char | 1 | - | 0 ~ 255 | 无符号字符型 |
字符型的特性
- ASCII编码:字符在计算机中存储的是ASCII码值
例如:‘A’ 对应 ASCII 65,‘a’ 对应 ASCII 97
代码示例:
char c = 'A';
printf("%d\n", c); // 输出65
编码规律:(数大小)
数字:48(0x30) - 57(0x39) → “0"到"9”
大写字母:65(0x41) - 90(0x5A) → “A"到"Z”
小写字母:97(0x61) - 122(0x7A) → “a"到"z”
空格:32(0x20)
-
字符与整数的转换:字符型可以当作整型使用
char c = 65; // 等同于 ‘A’
printf(“%c\n”, c); // 输出’A’ -
转义字符:转义字符是C语言中一种特殊的字符序列,以反斜杠\开头,后跟一个字符或数字,用于表示一些无法直接输入的字符或具有特殊功能的字符。
| 序列格式 | 名称 | ASCII值 | ASCII(十六进制) | 分类 | 说明 | 实际使用示例 | 注意事项 |
|---|---|---|---|---|---|---|---|
\0 | 空字符 | 0 | 0x00 | 控制字符 | 字符串终止符,表示字符串结束 | char str[] = "Hello\0"; | 不要将\0用于字符串中间,否则字符串会提前结束 |
\n | 换行符 | 10 | 0x0A | 控制字符 | 将光标移到下一行开头,用于文本换行 | printf("Line 1\nLine 2"); | 在Windows系统中,换行通常需要\r\n,Unix/Linux/macOS (现代)则只需要\n |
\t | 水平制表符 | 9 | 0x09 | 控制字符 | 插入水平制表符(通常4或8个空格),用于对齐输出 | printf("Name:\tJohn\nAge:\t25"); | \t的宽度在不同系统上可能不同,通常为4或8个空格 |
\r | 回车符 | 13 | 0x0D | 控制字符 | 将光标移到当前行开头,不换行,用于覆盖输出 | printf("Processing 50%\rProcessing 100%"); | 通常与\n一起使用,形成\r\n,是 Windows 系统中表示换行的标准方式。 |
\b | 退格符 | 8 | 0x08 | 控制字符 | 将光标向后移动一个位置,用于删除前一个字符 | printf("Hello\bWorld"); 输出:HellWorld | 不能真正删除字符,只移动光标 |
\f | 换页符 | 12 | 0x0C | 控制字符 | 将光标移到下一页开头,主要用于打印机分页 | printf("Page 1\fPage 2"); | 对屏幕显示无影响 |
\v | 垂直制表符 | 11 | 0x0B | 控制字符 | 用于打印机控制垂直位置,对屏幕显示无影响 | printf("Line 1\vLine 2"); | 对屏幕显示无影响 |
\a | 响铃符 | 7 | 0x07 | 控制字符 | 触发系统响铃(beep),用于提示用户 | printf("Error!\a"); | 有些系统可能不支持响铃功能 |
\\ | 反斜杠 | 92 | 0x5C | 特殊字符 | 表示反斜杠本身,用于避免转义歧义 | printf("C:\\Windows\\System32"); | 反斜杠是转义字符,所以需要双写表示单个反斜杠 |
\' | 单引号 | 39 | 0x27 | 特殊字符 | 表示单引号字符,用于字符常量中 | char c = '\''; | 用于字符常量中,不能用于字符串 |
\" | 双引号 | 34 | 0x22 | 特殊字符 | 表示双引号字符,用于字符串常量中,字符常量可以直接写,不转义 | printf("He said: \"Hello!\""); | 用于字符串中表示双引号 |
\? | 问号 | 63 | 0x3F | 特殊字符 | 表示问号字符,通常直接使用?即可 | printf("Is this a question?\?"); | 通常不需要使用,直接使用?即可 |
\ooo | 八进制转义 | 可变 | 可变 | 特殊字符 | 表示三位八进制数对应的字符(如\101表示’A’) | printf("%c", '\101'); // 输出 'A' 也可以用%d直接输出 | 八进制转义最多3位,后跟1-3位八进制数字(0-7),如\101 |
\xhh | 十六进制转义 | 可变 | 可变 | 特殊字符 | 表示两位十六进制数对应的字符(如\x41表示’A’) | printf("%c", '\x41'); // 输出 'A' | 十六进制转义最多2位,以x开头,如\x41 |
注意:
\n在Windows系统中通常表示为\r\n(回车+换行),而\r单独使用表示回车。- 八进制转义最多3位,十六进制转义最多2位。
- 续行符是反斜杠\,用于将一行代码分成多行,它不是用于输出的转义字符,而是用于代码编写时的格式化工具。(续行符后的行必须是代码,\后面必须直接跟换行符,中间不能有空格或其他字符,在C语言中,函数调用的参数列表可以自由换行,不需要使用续行符(\)。)
- 字符串字面量(用双引号 " 包围)中是否可以直接写单引号 ’ 或双引号 ",取决于你要写的是哪一种引号。单引号 ’ 在双引号字符串中:可以直接写,无需转义;双引号 ’ 在单引号字符串中:可以直接写,无需转义
2.4 布尔型
C语言在C99标准中引入了布尔类型:
| 类型 | 通常大小(字节) | 说明 |
|---|---|---|
_Bool | 1 | 用于表示布尔值(真或假) |
布尔类型特性
-
_Bool类型是一种无符号整数类型。任何非零值赋给_Bool变量时,会被隐式转换为1;零值则保持不变。
-
_Bool类型严格限制存储0(表示false)或1(表示true)_Bool b = 5; // b的值为1 _Bool c = 0; // c的值为0 -
C99引入了
stdbool.h头文件,其中定义了bool、true和false,使代码更易读#include <stdbool.h> bool is_valid = true;// 实际存储为1 if (is_valid) { printf("Valid!\n"); }
2.5 浮点型
- 浮点型用于存储带有小数部分的数值,是近似值表示,不是准确值。
| 类型 | 通常大小(字节) | 有效数字 | 数值范围 | 说明 |
|---|---|---|---|---|
float | 4 | 6-7位 | ±3.4e-38 ~ ±3.4e38 | 单精度浮点型 |
double | 8 | 15-16位 | ±1.7e-308 ~ ±1.7e308 | 双精度浮点型 |
long double | 10或16 | 18-19位 | ±1.2e-4932 ~ ±1.2e4932 | 扩展精度浮点型(部分编译器支持) |
- 由于浮点数是近似表示,多次计算可能导致真值偏差:
float a = 0.1;
float b = 0.2;
float c = a + b; // c可能不是精确的0.3
2.6 复数和虚数类型
-
注意:在C语言中,_Complex 和_Imaginary不是一个独立的类型,而是一个类型修饰符。它需要与基本数据类型一起使用。C99 提供了三种复数类型:float _Complex/_Imaginary,double _Complex/_Imaginary,和 long double _Complex/_Imaginary。对于 float _Complex类型的变量来说,它包含两个 float类型的值,一个用于表示复数的实部,另一个用于表示虚部,_Complex 和 _Imaginary 是 C99 关键字,语法上不需要头文件;
-
复数的初始化
- 方法1:使用复数单位I【I 是虚数单位(sqrt(-1)),在 <complex.h> 中定义】
double _Complex c1 = 3.0 + 4.0 * I; // 3 + 4i,需要头文件 - 方法2:结构体初始化方式(等价于方法1)
double _Complex c2 = {5.0, 6.0}; // 实部5.0,虚部6.0,是合法的 C99 语法,且不需要包含 <complex.h> 头文件。
- 方法1:使用复数单位I【I 是虚数单位(sqrt(-1)),在 <complex.h> 中定义】
-
C99标准引入了复数和虚数类型:
| 类型 | 通常大小(字节) | 说明 |
|---|---|---|
_Complex | (float)8或16(double) | 用于表示复数(实部和虚部) |
_Imaginary | (float)8或16(double) | 用于表示虚数(只有虚部) |
-
creal() - 获取实部
double creal(double complex z); // 双精度版本
float crealf(float complex z); // 单精度版本
long double creall(long double complex z); // 长双精度版本 -
cimag() - 获取虚部
double cimag(double complex z); // 双精度版本
float cimagf(float complex z); // 单精度版本
long double cimagl(long double complex z); // 长双精度版本 -
通用宏(C99+)
#include <tgmath.h>
#define creal(z) // 自动选择合适版本
#define cimag(z) // 自动选择合适版本
C 语言中默认使用双精度版本(creal/cimag)
必须包含头文件:#include <complex.h> // 标准复数头文件
注意:C99标准中,_Complex可以简写为complex,所以通常写成float complex、double complex。_Imaginary可以简写为imaginary,所以通常写成float imaginary、double imaginary。
- 复数类型使用示例
#include <complex.h>
#include<stdio.h>
int main() {
double complex z = 3.0 + 4.0*I; // 复数 3+4i
double real = creal(z); // 实部 3.0
double imag = cimag(z); // 虚部 4.0
double _Imaginary z1 = 4.0 * I; // 纯虚数 4i
float _Imaginary zf = 2.5 * I; // 纯虚数 2.5i
return 0;
}
2.7 空类型
| 类型 | 通常大小(字节) | 说明 |
|---|---|---|
void | 0 | 不表示任何特定的数据类型 |
-
void的使用场景-
函数没有返回值:
void print_message() {
printf(“Hello, World!\n”);
} -
函数没有参数:
void function(void) {
// 函数体
} -
通用指针类型:
void *ptr; // 通用指针,可以指向任何类型的数据
-
2.8 类型大小和范围
C语言中类型大小和范围依赖于编译器和硬件平台,可以通过sizeof运算符确定:
#include <stdio.h>
#include <limits.h>
#include <float.h>
int main() {
printf("char: %zu bytes, range: %d to %d\n",
sizeof(char), CHAR_MIN, CHAR_MAX);
printf("int: %zu bytes, range: %d to %d\n",
sizeof(int), INT_MIN, INT_MAX);
printf("float: %zu bytes, range: %e to %e\n",
sizeof(float), FLT_MIN, FLT_MAX);
printf("double: %zu bytes, range: %e to %e\n",
sizeof(double), DBL_MIN, DBL_MAX);
printf("long double: %zu bytes, range: %Le to %Le\n",
sizeof(long double), LDBL_MIN, LDBL_MAX);
//在C语言中,函数调用的参数列表可以自由换行,不需要使用续行符(\)。
return 0;
}
2.9 类型修饰符
C语言提供以下类型修饰符来改变基本数据类型的属性:
| 修饰符 | 说明 |
|---|---|
signed | 指定有符号整型(默认) |
unsigned | 指定无符号整型,只能表示非负数 |
short | 指定短整型 |
long | 指定长整型 |
long long | 指定更长整型(C99标准) |
2.10 数据类型选择建议
-
整型:
- 需要存储小整数:
char或short - 需要存储一般整数:
int - 需要存储大整数:
long或long long - 只需要非负整数:
unsigned int、unsigned long
- 需要存储小整数:
-
浮点型:
- 一般精度要求:
float - 高精度要求:
double - 极高精度要求:
long double
- 一般精度要求:
-
字符型:
- 一般字符处理:
char - 需要精确控制字符值:
signed char或unsigned char
- 一般字符处理:
-
布尔型:
- 表示真/假条件:
_Bool(或bool,通过stdbool.h)
- 表示真/假条件:
2.11 重要注意事项
-
C语言没有字符串类型:字符串是字符数组的特殊形式,以
\0结束。 -
const定义的是"常变量",不是常量:const int a = 10;中的a仍然是变量,只是其值不能被修改。 -
类型大小依赖于平台:
int在32位系统上通常为4字节,在64位系统上可能为4或8字节。 -
浮点数的精度问题:浮点数是近似值表示,多次计算可能导致真值偏差。
-
整型的溢出:当整型值超出其表示范围时,会发生溢出,结果是未定义行为。
-
字符型与整型的互换:字符型本质上是整型,可以与整型进行转换和运算。
2.12 C语言数据类型总结
| 类型 | 说明 | 典型大小(字节) | 代表值 |
|---|---|---|---|
char | 字符型 | 1 | ‘A’ |
int | 整型 | 4 | 10 |
float | 单精度浮点型 | 4 | 3.14f |
double | 双精度浮点型 | 8 | 3.1415926535 |
long double | 扩展精度浮点型 | 10或16 | 3.14159265358979323846 |
_Bool | 布尔型 | 1 | true, false |
_Complex | 复数类型 | 8或16 | 3.0+4.0i |
_Imaginary | 虚数类型 | 8或16 | 4.0i |
void | 空类型 | 0 | 无 |
理解C语言的基本数据类型是编程的基石,它们决定了变量的存储方式、数值范围和程序效率。选择合适的数据类型可以优化内存使用、提高程序效率,并避免因超出数据类型范围而引发的错误。
三.字符串
3.1 字符串的定义与存储
在C语言中,没有专门的字符串数据类型,字符串本质上是字符数组,以’\0’(空字符)作为结束标志。
-
字符串的存储方式,C语言中字符串存储在连续的内存空间中,每个字符占用1字节,末尾必须以’\0’结束:
char str[] = "Hello";在内存中表示为:
H e l l o \0 0 1 2 3 4 5 -
字符串的声明与初始化
声明字符数组:
char str[20]; // 声明一个长度为20的字符数组
初始化字符串:
char str[] = “Hello”; // 自动添加’\0’结束符
char str[6] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘\0’};注意:数组大小应比实际字符数多1,用于存放'\0'结束符。
3.2 字符串与字符数组的区别
字符数组:可以包含任何字符,不以’\0’结束,不能自动识别字符串结束。
字符串:必须以’\0’结束,C语言函数依赖此结束符进行操作。
3.3 null字符(\0)
-
null字符(
\0)是C语言中表示字符串结束的特殊字符,其ASCII值为0,也称为零终止符。它不是可见字符,而是用于标记字符串的结束位置。 -
重要特性:
- 字符串结束标志:C语言中的字符串必须以
\0结尾,这是C语言字符串处理机制的核心 - ASCII值:0(0x00)
- 存储:在内存中占用1字节
- 长度计算:字符串长度不包括
\0本身
- 字符串结束标志:C语言中的字符串必须以
-
C语言中没有内置的字符串类型,字符串本质上是字符数组。没有结束符,程序无法知道字符串在哪里结束,可能导致:
- 读取超出字符串范围的内存
- 程序崩溃
- 无法正确处理字符串
-
字符串长度计算
#include <stdio.h> #include <string.h> int main() { char str[] = "Hello"; int len = strlen(str); // len = 5(不包括\0) printf("Length: %d\n", len); return 0; } -
重要注意事项
-
多个null字符:字符串中可以有多个
\0,但只有第一个被当作字符串结束符,后面的\0会被视为普通字符 -
忘记添加\0的后果:如果字符串没有以
\0结尾,程序可能会继续读取内存直到遇到\0,导致:- 不正确的字符串长度
- 程序崩溃
- 安全漏洞(如缓冲区溢出)
-
字符串字面量:字符串字面量(如
"Hello")会自动添加\0结束符
-
-
与NULL的区别
项目 null字符( \0)NULL 类型 字符 指针 用途 标记字符串结束 表示空指针 ASCII值 0 0(但作为指针) 示例 char s[] = "A\0B";int *p = NULL;
补充:字符串什么时候能修改,什么时候不能
| 声明方式 | 存储位置 | 是否可修改 | 说明 | 代码示例 |
|---|---|---|---|---|
char s[] = "abc"; | 数组s在栈区,字符串内容被复制到栈区 | ✅ 可修改 | 数组内容是拷贝的(从常量区复制到栈区) | char s[] = "abc"; s[0] = 'x'; printf("%s\n", s); // ✅ 修改成功,输出: xbc |
static char s[] = "abc"; | 数组s在全局/静态区,字符串内容被复制到全局/静态区 | ✅ 可修改 | 静态数组,可写(存储在全局/静态区) | static char s[] = "abc"; s[0] = 'x'; printf("%s\n", s); // ✅ 修改成功,输出: xbc |
char *s = "abc"; | 指针s在栈区,字符串"abc"在常量区 | ❌ 不可修改 | 指向常量区的字符串字面量,不能修改 | char *s = "abc"; s[0] = 'x'; // ❌ 未定义行为,程序崩溃 |
const char *s = "abc"; | 指针s在栈区,字符串"abc"在常量区 | ❌ 不可修改 | 显式声明为只读,编译器会报错 | const char *s = "abc"; s[0] = 'x'; // ❌ 编译错误: assignment of read-only location |
char *s = malloc(10); strcpy(s, "abc"); | 指针s在栈区,字符串内容在堆区 | ✅ 可修改 | 动态分配内存,内容可写 | char *s = malloc(10); strcpy(s, "abc"); s[0] = 'x'; printf("%s\n", s); // ✅ 修改成功,输出: xbc |
char s[10] = {'a','b','c'}; | 数组s在栈区,字符串内容被复制到栈区 | ✅ 可修改 | 手动初始化数组,可修改 | char s[10] = {'a','b','c'}; s[0] = 'x'; printf("%s\n", s); // ✅ 修改成功,输出: xbc |
char strs[4][10] = {"tom", "jack", "rose", "lily"}; | 二维数组strs在栈区,字符串内容被复制到栈区 | ✅ 可修改 | 二维字符数组,可修改内部字符 | char strs[4][10] = {"tom", "jack", "rose", "lily"}; strs[1][1] = 'X'; printf("%s\n", strs[1]); // ✅ 修改成功,输出: jXck |
char *strs[4] = {"tom", "jack", "rose", "lily"}; | 指针数组strs在栈区,字符串"tom"、"jack"等在常量区 | ❌ 不可修改 | 指针数组中的字符串指向常量区,不能修改 | char *strs[4] = {"tom", "jack", "rose", "lily"}; strs[1][1] = 'X'; // ❌ 未定义行为,程序崩溃 |
重要说明
-
字符串字面量的本质:
"abc"本身存储在常量区(只读内存)- 用
char s[] = "abc";时,C 会将常量区的字符串复制到栈上的数组 - 用
char *s = "abc";时,s 指向的是常量区的字符串,不能修改
-
二维字符数组 vs 一维字符指针数组:
- 二维字符数组:
char strs[4][10] = {"tom", "jack", ...};- 可修改 - 一维字符指针数组:
char *strs[4] = {"tom", "jack", ...};- 不可修改
- 二维字符数组:
-
为什么不能修改字符串字面量:
char *s = "hello"; s[0] = 'H'; // 会导致段错误(Segmentation Fault)这是因为字符串字面量存储在只读内存区,试图修改会导致程序崩溃。
-
安全修改字符串的正确做法:
// 正确:使用字符数组 char s[] = "hello"; s[0] = 'H'; // ✅ 安全修改 // 正确:使用动态内存 char *s = malloc(6); strcpy(s, "hello"); s[0] = 'H'; // ✅ 安全修改 free(s);
四. 字符串输入
补充:size_t
在C语言中,size_t 是一个无符号整数类型,用于表示对象的大小(如内存大小、数组长度、字符串长度等)。它是C标准库定义的类型,通常在 <stddef.h> 头文件中声明。
- 核心特性
| 特性 | 说明 |
|---|---|
| 类型 | 无符号整数(unsigned) |
| 用途 | 表示大小/数量(字节数、元素个数等) |
| 定义位置 | <stddef.h>(标准头文件) |
| 为什么设计 | 与系统架构匹配,避免负数问题,确保跨平台兼容性 |
- 为什么需要
size_t?(关键原因)
- 与系统架构匹配
size_t的大小(位数)由编译器根据目标平台自动确定:- 32位系统:
size_t通常是unsigned int(32位) - 64位系统:
size_t通常是unsigned long或unsigned long long(64位)
- 32位系统:
- 为什么重要?
64位系统可以处理超过 4GB 的内存,而unsigned int(32位)无法表示这么大的值。size_t保证了能表示任何可能的内存大小。
- 无符号类型,避免负数
- 大小不能为负数,所以必须用无符号整数。
- 如果用
int表示大小:int len = strlen("hello"); // len = 5 if (len < 0) { ... } // 逻辑错误:len 不可能为负
- 标准库统一接口
- 所有标准库函数(
malloc,strlen,sizeof等)都使用size_t,确保代码一致性。
4.1 scanf()
-
函数定义:
int scanf(const char* format, ...); -
功能:从标准输入(
stdin)读取格式化数据,并根据格式字符串解析后存储到指定变量中。 -
参数说明
-
format格式字符串-
空白字符(如空格、换行、制表符):
scanf会跳过输入中的空白字符。 -
非空白字符(非
%开头):直接与输入字符匹配。例如示例:scanf(“年龄:%d”, &age);
输入必须是 年龄:25,如果输入 Age:25 会失败。 -
格式说明符(以
%开头):控制如何解析输入数据,并将结果存储到对应参数中。
-
-
可变参数(变量指针)
- 所有参数必须是指针,用于存储解析后的数据。
示例:scanf("%d", &num);中,&num是num的地址。
- 所有参数必须是指针,用于存储解析后的数据。
-
-
格式说明符详解
通用格式:%[*][width][length]specifier
| 子说明符 | 作用 |
|---|---|
* | 抑制赋值(读取但不存储)。例如 %*d 会跳过一个整数。 |
width | 最大读取字符数。例如 %3s 最多读取3个字符。 |
length | 指定目标变量的类型(见下文)。 |
specifier | 数据类型(见下表)。 |
- 格式说明符与对应数据类型
| 说明符 | 数据类型 | 说明 |
|---|---|---|
%d | int | 读取十进制整数(可带符号)。 |
%u | unsigned int | 读取无符号十进制整数。 |
%o | unsigned int | 读取八进制整数(自动忽略前导 0)。 |
%x / %X | unsigned int | 读取十六进制整数(自动忽略前导 0x)。 |
%f、%e、%g | float 或 double | 读取浮点数(支持科学计数法)。 |
%c | char | 读取单个字符(包括空白字符)。 |
%s | char* | 读取非空白字符序列(自动添加 \0 结尾)。 |
%[...] | char* | 读取指定字符集的字符串(如 %[a-z])。 |
%[^...] | char* | 读取排除指定字符集的字符串(如 %[^,])。scanf(“%[^…]”) 会读取字符,直到遇到指定字符集中的任意一个字符为止,且不会读取那个字符。 |
%p | 指针类型 | 读取指针地址(格式与 printf 的 %p 一致)。 |
%n | int* | 存储已读取字符数(不读取数据,只记录已读字符数)eg:scanf(“%d%n”, &a, &count);读取数字后,count记录已读字符数(如输入123,count=3) |
%% | 无 | 读取输入中的%符号,匹配实际的百分号 |
- 长度修饰符与数据类型对应表
| 修饰符 | 有符号类型 | 无符号类型 | 说明 | 示例 |
|---|---|---|---|---|
hh | signed char | unsigned char | 读取单字节的有符号/无符号字符 | %hhd, %hhu |
h | short | unsigned short | 读取2字节的有符号/无符号短整型 | %hd, %hu |
l | long | unsigned long | 读取4字节(32位)或8字节(64位)的有符号/无符号长整型 | %ld, %lu |
ll | long long | unsigned long long | 读取8字节的有符号/无符号长整型 | %lld, %llu |
j | intmax_t | uintmax_t | 读取系统支持的最大整型(C99引入) | %jd, %ju |
z | 无 | size_t | 用于%u、%o、%x,表示size_t类型 | %zu |
t | 无 | ptrdiff_t | 用于%d,表示指针差值类型,返回的是两指针之间相隔的元素个数。 | %td |
L | long double | 无 | 读取长双精度浮点数 | %Lf |
- 注意float和double的输入输出格式
| 操作 | float | double |
|---|---|---|
| 输入 | %f | %lf |
| 输出 | %f(推荐) | %lf(推荐) |
- 返回值
- 成功赋值的参数个数:例如
scanf("%d %d", &a, &b)成功读取两个整数时返回2。 - EOF:scanf 只有在输入流到达文件结尾 或发生 I/O 错误时才会返回 EOF(-1),其他所有情况(包括格式错误)都会返回 0 或成功读取的项数(正整数)。
- 匹配失败:例如输入非数字字符时,返回已成功赋值的个数。
- 成功赋值的参数个数:例如
注意:
scanf的宽度规范设计为不包括\0;
scanf 函数使用 %s 格式说明符读取字符串时,会自动在字符串末尾添加终止符 \0
-
使用示例
#include <stdio.h> int main() { char name[32]; int age; printf("请输入姓名和年龄(格式:姓名 年龄):"); scanf("%s %d", name, &age); // 输入:Alice 25 printf("姓名:%s,年龄:%d\n", name, age); return 0; } -
抑制赋值(
%*d)int a, b; // 输入:10 20 30 scanf("%d %*d %d", &a, &b); // a=10, b=30(跳过中间的20) -
指定宽度(
%5s)char str[10]; scanf("%5s", str); // 输入:HelloWorld → str="Hello"(最多读取5字符) -
字符集匹配(
%[a-z])char str[32]; scanf("%[a-z]", str); // 仅读取小写字母(如输入 "abc123" → str="abc") -
常见问题与解决方案
- 缓冲区溢出风险
- 问题:使用
%s时未限制宽度可能导致缓冲区溢出。 - 解决方案:指定宽度,如
%31s(目标数组大小为32时)。
- 问题:使用
char name[32]; scanf("%31s", name); // 避免溢出- 输入残留问题
- 问题:
scanf不会自动清除输入缓冲区中的换行符。 - 解决方案:手动清理缓冲区。
- 问题:
int c; while ((c = getchar()) != '\n' && c != EOF); // 清空缓冲区- 无法读取含空格的字符串
- 问题:
%s会跳过空白字符并以空格结束。 - 解决方案:使用字符集匹配或
fgets。
- 问题:
char str[100]; scanf("%[^\n]", str); // 读取整行(含空格),直到遇到换行符 - 缓冲区溢出风险
注意:scanf()中的%s:只能读取一个单词(遇空格结束)
printf()中的%s:输出字符串直到遇到’\0’(不是文件结尾或eof)
-
最佳实践
1. 始终检查返回值:if (scanf("%d", &num) != 1) { printf("输入无效!\n"); }- 优先使用
fgets+sscanf(更安全):
char line[100]; if (fgets(line, sizeof(line), stdin)) { sscanf(line, "%d %f", &int_var, &float_var); }-
避免
gets和scanf直接读取字符串:gets存在缓冲区溢出风险(C11 已废弃)。scanf的%s无法处理含空格的字符串。
-
明确指定宽度:
char buf[10]; scanf("%9s", buf); // 确保不会溢出 - 优先使用
4.2 gets()
注意:
此函数在 C11 标准后被移除,C++14 标准中也被弃用,禁止 在现代代码中使用。始终使用 fgets 替代,确保输入长度安全。
- 函数原型
char * gets ( char * str );
-
功能
从标准输入(stdin)读取字符串并存储到str中,直到遇到换行符(\n)或文件结束符(EOF)为止。- 换行符处理:如果遇到换行符,不会复制到
str中。 - 自动添加空字符:读取结束后,会自动在
str末尾追加字符串结束符\0。
- 换行符处理:如果遇到换行符,不会复制到
-
注意事项
- 与
fgets的区别:gets仅从stdin读取,而fgets可以从任意文件流读取。gets不包含换行符,而fgets会包含。- 最关键区别:
gets不支持指定缓冲区大小,可能导致缓冲区溢出(Buffer Overflow),这是严重安全隐患。
- 与
-
参数
str
指向内存块的指针(字符数组),用于存储读取到的字符串。
-
返回值
- 成功时:返回
str(指向输入字符串的指针)。 - 遇到文件结尾(EOF):如果在读取任何字符前遇到 EOF,会设置
feof标志,返回NULL,且str内容不变。 - 读取错误:设置
ferror标志,返回NULL,且str内容可能已改变。
- 成功时:返回
char str[10]; // 在栈上分配10字节的字符数组
char *a = gets(str); // gets() 会将输入写入 str,返回 str 的地址
-
兼容性
- C语言:自 C11 标准起,该函数被正式移除。
- C++语言:自 C++14 标准起,该函数被弃用(Deprecated)。
-
安全警告
gets函数无法限制输入长度,因此极易导致缓冲区溢出。例如:
char str[10];
gets(str); // 如果输入超过9个字符(不含结束符),将覆盖内存,导致未定义行为!
- ✅ 替代方案(推荐使用)
使用fgets函数替代gets,以避免缓冲区溢出:
char str[100];
if (fgets(str, sizeof(str), stdin) != NULL) {
// 移除换行符(如果存在)
size_t len = strlen(str);
if (len > 0 && str[len-1] == '\n') {
str[len-1] = '\0';
}
}
4.3 fgets()
- 函数定义:
char *fgets(char *str, int num, FILE *stream); - 功能:从指定输入流(如标准输入或文件)中读取一行字符串,存储到字符数组
str中,最多读取num-1个字符。读取在以下情况结束:
1. 已读取num-1个字符;
2. 遇到换行符(\n);
3. 达到文件末尾(EOF)。 - 特点:
- 保留换行符(
\n)并存储到str中; - 自动在字符串末尾添加空字符(
\0)。
- 保留换行符(
- 参数说明
| 参数 | 说明 |
|---|---|
str | 指向字符数组的指针,用于存储读取的字符串。 |
num | 最大读取字符数(包括终止符 \0)。例如 num=100 时,最多读取 99 个字符。 |
stream | 输入流指针,可以是文件指针(如 FILE*)或标准输入 stdin。 |
-
返回值
- 成功:返回
str(指向读取字符串的指针)。 - 失败:
若读取前已到达文件末尾(EOF),返回NULL,且str内容不变。
若读取过程中发生错误,返回NULL,且str内容可能被修改。
- 成功:返回
-
与
gets的区别
| 特性 | fgets | gets |
|---|---|---|
| 输入源 | 支持任意流(如文件、标准输入) | 仅支持标准输入 |
| 缓冲区安全 | 安全(可指定最大读取长度) | 不安全(无长度限制,易导致溢出) |
| 换行符处理 | 保留换行符 | 丢弃换行符 |
| 标准支持 | C89/C99/C11/C17 标准支持 | C11 标准中被移除 |
- 示例代码
#include <stdio.h>
#include <string.h>
int main() {
char buffer[100];
printf("请输入一行内容(最多99字符):");
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
// 移除末尾换行符(如果存在)
buffer[strcspn(buffer, "\n")] = '\0';
printf("您输入的内容为:%s\n", buffer);
} else {
printf("读取失败或输入为空。\n");
}
return 0;
}
输入示例:
Hello, World!
输出:
您输入的内容为:Hello, World!
- 使用场景与注意事项
- 读取文件内容
FILE *fp = fopen("example.txt", "r");
if (fp != NULL) {
char line[256];
while (fgets(line, sizeof(line), fp)) {
printf("%s", line); // 逐行输出文件内容
}
fclose(fp);
}
- 处理换行符:
fgets会保留换行符,可通过以下方式移除:
buffer[strcspn(buffer, "\n")] = '\0'; // 替换换行符为空字符
- 输入缓冲区残留问题:若输入内容超过
num-1字符,剩余字符会留在缓冲区,需手动清理:
int c;
while ((c = getchar()) != '\n' && c != EOF); // 清空缓冲区
-
常见问题与解决方案
- 输入截断问题
- 问题:输入内容过长时,
fgets会截断输入。 - 解决方案:检查输入末尾是否包含换行符,若无则清空缓冲区。
- 问题:输入内容过长时,
if (buffer[strlen(buffer)-1] != '\n') { // 输入被截断,清空剩余字符 while (getchar() != '\n'); }- 处理二进制文件
- 问题:
fgets用于二进制文件时可能提前终止(遇到\0)。 - 解决方案:使用
fread读取二进制文件。
- 问题:
- 输入截断问题
-
最佳实践
- 始终指定
num为sizeof(str):
char str[128]; fgets(str, sizeof(str), stdin); // 安全限制读取长度- 验证返回值:
if (fgets(str, sizeof(str), stdin) == NULL) { // 处理输入错误或空输入 }- 清理换行符:
str[strcspn(str, "\n")] = '\0'; // 安全移除换行符 - 始终指定
- 优先使用
fgets替代gets:- 避免缓冲区溢出风险。
补充 :strcspn
strcspn 是 string complementary span 的缩写,表示"字符串互补跨度"。这个函数用于计算字符串中开头连续不包含指定字符集的字符数量。
函数原型
#include <string.h>
size_t strcspn(const char *str1, const char *str2);
功能说明
- 从
str1的开头开始,逐个检查字符 - 找到第一个在
str2中出现的字符时停止 - 返回从开头到这个字符的长度(不包括这个字符)
- 如果
str1中没有任何字符在str2中出现,返回str1的长度
返回值
- 0:如果
str1开头就包含str2中的字符 - str1的长度:如果
str1中没有任何字符在str2中出现 - 中间值:从开头到第一个在
str2中出现的字符的长度
与strspn的区别
strcspn:计算不包含指定字符集的起始长度strspn:计算只包含指定字符集的起始长度
典型应用场景
- 去除字符串末尾的换行符
char str[100];
fgets(str, sizeof(str), stdin); // 读取一行
str[strcspn(str, "\n")] = '\0'; // 将换行符替换为字符串结束符
printf("处理后的字符串: %s", str);
- 提取不含特定字符的前缀
char *str = "Hello, world!";
char *reject = "!,";
size_t n = strcspn(str, reject); // 返回6("Hello"的长度)
printf("前缀: %.*s", (int)n, str); // 输出"Hello"
- 字符串过滤
char *str = "123456";
char *reject = "234";
size_t n = strcspn(str, reject); // 返回1(整个字符串都不包含"234"中的字符)
重要提示
strcspn不修改原字符串,只返回长度- 通常用于配合字符串截断使用
- 与
fgets结合使用时,可有效去除输入字符串末尾的换行符 - 如果
str2包含多个字符,函数会查找第一个在str2中出现的字符
为什么需要strcspn?
在处理字符串时,经常需要知道字符串中第一个特定字符(如空格、换行符、标点符号)的位置。strcspn提供了一种高效的方法来获取这个位置,避免了手动遍历字符串的麻烦。
总结
strcspn是C标准库中用于字符串处理的实用函数- 用于获取字符串开头连续不包含指定字符集的长度
- 常用于去除字符串末尾的换行符、解析字符串等场景
- 与
strspn配合使用,可以实现更复杂的字符串处理逻辑
4.4 getchar()
- 函数定义:
int getchar(void); - 功能:从标准输入(stdin)读取下一个字符,并将其作为 int 类型返回。
- 等价性:等效于 getc(stdin)。
- 无参数:直接从标准输入读取字符。
- 返回值
成功:返回读取的字符(类型为 int,实际为 unsigned char 转换为 int)。
失败/结束:返回常量 EOF(通常定义为 -1)。
若输入流已到达文件末尾(EOF),则设置 stdin 的 EOF 指示器(feof(stdin))。
若发生读取错误,则设置 错误指示器(ferror(stdin))。 - 示例代码
#include <stdio.h>
int main() {
int c;
puts("输入文本。输入句号(.)后按回车结束:");
do {
c = getchar(); // 读取单个字符
putchar(c); // 立即输出字符
} while (c != '.'); // 遇到句号时终止
return 0;
}
运行示例:
输入文本。输入句号(.)后按回车结束:
Hello, World.↵
Hello, World.
-
核心特性
- 逐字符处理
即时性:每次调用读取一个字符,适合实时输入处理(如密码掩码、游戏控制)。
缓冲区行为:输入通常以行为单位缓冲(行缓冲),按回车键后数据才会传递给程序。 - 返回值类型为 int
原因:必须能表示 EOF(-1),而 char 类型无法区分 EOF 和合法字符(如 0xFF)。
正确用法:使用 int 类型变量接收返回值,避免错误。 - 换行符处理
保留换行符:用户按下回车时,‘\n’ 会被读取为一个独立字符。
示例:输入 A↵ 时,getchar() 会依次返回 ‘A’ 和 ‘\n’。
- 逐字符处理
-
常见问题与解决方案
1. 输入缓冲区残留问题
问题现象:若之前使用 scanf 或 fgets 读取输入,缓冲区中可能残留换行符(‘\n’),导致 getchar() 直接读取该字符。
解决方案:在调用 getchar() 前清空缓冲区:
int c;
while ((c = getchar()) != ‘\n’ && c != EOF); // 清空缓冲区
2. 误用 char 类型接收返回值
错误代码:
char c = getchar(); // 可能导致无法正确判断 EOF
正确代码:
int c;
while ((c = getchar()) != EOF) { /* 处理字符 */ }
3. 无法立即响应单个字符输入
问题:默认情况下,输入以行为单位缓冲(需按回车后才会处理)。
解决方案:在支持的系统中使用非标准函数(如 conio.h 的 _getch()),但需注意可移植性。 -
与 scanf/fgets 的对比
| 特性 | getchar | scanf(“%c”) | fgets |
|---|---|---|---|
| 读取单位 | 单个字符 | 单个字符 | 整行字符串 |
| 换行符处理 | 保留为独立字符 | 保留为独立字符 | 包含在字符串中(可选择移除) |
| 缓冲区问题 | 易受残留字符影响 | 同左 | 一次性读取整行,减少问题 |
| 适用场景 | 实时字符处理(如游戏控制) | 简单字符读取 | 安全读取用户输入 |
-
使用场景
- 读取单个字符决策
int confirm;
printf("确定执行此操作吗?(y/n): ");
confirm = getchar();
if (confirm == 'y' || confirm == 'Y') {
// 执行操作
}
- 输入过滤
int c;
printf("请输入小写字母:");
while ((c = getchar()) != EOF) {
if (c >= 'a' && c <= 'z') {
putchar(c);
} else {
break;
}
}
- 密码输入掩码(不推荐)
#include <stdio.h>
#include <conio.h> // 非标准库,仅限Windows
int main() {
int c;
printf("输入密码:");
while ((c = _getch()) != '\r') { // 按回车结束
putchar('*');
}
return 0;
}
- 最佳实践
- 始终用 int 接收返回值:避免无法检测 EOF。
- 处理换行符残留:在 getchar() 前清空缓冲区。
- 避免死循环:确保 getchar() 有明确退出条件。
- 替代方案:
读取整行输入:优先使用 fgets。
跨平台单字符读取:使用库函数(如 ncurses 或 SDL)。
4.5 常见问题与解决方案
-
缓冲区溢出问题
问题:使用
scanf或gets时,输入过长字符串导致覆盖内存解决方案:使用
fgets并指定最大长度
char str[100];
fgets(str, sizeof(str), stdin);
-
未处理换行符
问题:
fgets会将换行符读入字符串解决方案:
// 读取后移除换行符
size_t len = strlen(str);
if (len > 0 && str[len-1] == '\n') {
str[len-1] = '\0';
}
- 未初始化字符数组
问题:未初始化的字符数组可能包含垃圾值
解决方案:
char str[100] = {0}; // 初始化为全0
- 未添加结束符
\0
问题:字符串没有以\0结束,导致字符串处理函数行为异常
解决方案:确保字符串以\0结束
str[99] = '\0'; // 确保字符串结束
4.6 最佳实践
- 优先使用
fgets()
char input[100];
printf("请输入: ");
fgets(input, sizeof(input), stdin);
// 移除换行符
input[strcspn(input, "\n")] = '\0';
- 输入验证
char input[100];
printf("请输入最多99个字符: ");
if (fgets(input, sizeof(input), stdin) != NULL) {
// 移除换行符
input[strcspn(input, "\n")] = '\0';
// 验证长度
if (strlen(input) >= sizeof(input)-1) {
printf("输入过长!\n");
} else {
printf("您输入的是: %s\n", input);
}
}
- 避免使用
scanf处理字符串
// 错误示例
char str[10];
scanf("%s", str); // 不能处理空格
// 正确示例
char str[10];
fgets(str, sizeof(str), stdin);
// 移除换行符
str[strcspn(str, "\n")] = '\0';
4.7 代码示例
示例1:安全的字符串输入
#include <stdio.h>
#include <string.h>
int main() {
char input[100];
printf("请输入您的名字: ");
if (fgets(input, sizeof(input), stdin) != NULL) {
// 移除换行符
size_t len = strlen(input);
if (len > 0 && input[len-1] == '\n') {
input[len-1] = '\0';
}
printf("您输入的名字是: %s\n", input);
}
return 0;
}
示例2:处理包含空格的字符串
#include <stdio.h>
#include <string.h>
int main() {
char sentence[100];
printf("请输入一句话: ");
if (fgets(sentence, sizeof(sentence), stdin) != NULL) {
// 移除换行符
size_t len = strlen(sentence);
if (len > 0 && sentence[len-1] == '\n') {
sentence[len-1] = '\0';
}
printf("您输入的是: %s\n", sentence);
}
return 0;
}
五.字符串输出
5.1 C语言字符串的基本概念
在C语言中,没有内置的字符串类型,字符串处理依赖于标准库函数和字符数组。字符串是以空字符 \0 结尾的字符数组。例如,字符串 "Hello" 在内存中存储为:
{'H', 'e', 'l', 'l', 'o', '\0'}
5.2 printf 函数(最常用)
- 函数原型:
int printf ( const char * format, ... ); - 功能:将格式化的数据输出到标准输出(stdout)。如果格式字符串 format 中包含格式说明符(以 % 开头的子序列),后续的附加参数会按照指定的格式被格式化并替换对应的说明符。
- 参数说明
- format(格式字符串)
类型:const char*
作用:包含要输出的文本,可能包含嵌入的格式说明符。
格式说明符原型:%[flags][width][.precision][length]specifier - …(可变参数)
作用:根据 format 中的格式说明符,提供对应的值或指针。
注意:参数数量必须与格式说明符匹配,多余的参数会被忽略。
- format(格式字符串)
- 格式说明符详解
- 基础说明符(specifier)
| 说明符 | 输出类型 | 示例 |
|---|---|---|
d 或 i | 有符号十进制整数 | 392 |
u | 无符号十进制整数 | 7235 |
o | 无符号八进制整数 | 610 |
x | 无符号十六进制整数(小写) | 7fa |
X | 无符号十六进制整数(大写) | 7FA |
f | 十进制浮点数(小写) | 392.65 |
F | 十进制浮点数(大写) | 392.65 |
e | 科学计数法(小写) | 3.9265e+2 |
E | 科学计数法(大写) | 3.9265E+2 |
g | 自动选择最短表示(%e 或 %f) | 392.65 |
G | 自动选择最短表示(%E 或 %F) | 392.65 |
a | 十六进制浮点数(小写) | -0xc.90fep-2 |
A | 十六进制浮点数(大写) | -0XC.90FEP-2 |
c | 单个字符 | 'a' |
s | 字符串(%s在用于输出字符串时,从指定的字符串指针开始,一直打印字符直到遇到空字符\0,不会因为遇到空格、制表符或换行符而停止) | "sample" |
p | 指针地址 | b8000000 |
n | 不输出内容,将已写字符数存储到指针指向的变量中 | 无输出 |
%% | 输出单个 % 符号 | % |
- 子说明符(Sub-specifiers)
(1) 标志(Flags)
| 标志 | 作用 | 示例 |
|---|---|---|
- | 左对齐(默认右对齐) | %-10d |
+ | 强制显示正负号(默认仅负数显示负号) | +3.14 |
(空格) | 正数前加空格 | " 3.14" |
# | 强制显示前缀(如 0x、0) | #x → 0x64 |
0 | 用 0 填充左侧空白(默认用空格) | %010d → 0000001977 |
(2) 宽度(Width)
- 作用:指定最小输出宽度。
- 示例:
printf("%10d", 1977); // 输出: 1977(右侧填充空格) printf("%*d", 10, 1977); // 同上(宽度由参数指定)
(3) 精度(Precision) (%.nf格式化浮点数时,printf会进行四舍五入)
- 作用:控制输出精度。
- 示例:
printf("%.2f", 3.1416); // 输出:3.14(保留两位小数) printf("%.5s", "Hello, World!"); // 输出:Hello(截断字符串) printf("%.*f", 3, 3.14159265); // 输出:3.142(精度由参数指定)
(4) 长度修饰符(Length Modifiers)
| 修饰符 | 对应类型(有符号) | 对应类型(无符号) | 说明 |
|---|---|---|---|
hh | signed char | unsigned char | 用于 %d、%i、%u、%o、%x |
h | short | unsigned short | 用于 %d、%i、%u、%o、%x |
l | long | unsigned long | 用于 %d、%i、%u、%o、%x、%f、%s |
ll | long long | unsigned long long | 用于 %d、%i、%u、%o、%x |
j | intmax_t | uintmax_t | 用于 %d、%i、%u、%o、%x |
z | 无(用于 %u、%o、%x) | size_t | 用于 %u、%o、%x |
t | 无(用于 %d) | ptrdiff_t | 用于 %d |
L | long double | 无 | 用于 %f、%e、%E、%g、%G、%a、%A |
-
返回值
成功:返回实际输出的字符数(不包括末尾的空字符 \0)。
失败:返回负数,并设置 ferror 错误标志。(通常为-1,具体看系统)
多字节字符编码错误:设置 errno 为 EILSEQ,并返回负数。 -
常见错误与解决方案
- 参数类型不匹配
// 错误:用 %d 输出 long long
long long x = 9223372036854775807LL;
printf(“%d\n”, x); // ❌ 未定义行为(输出随机值)
解决方案:使用 %lld:
printf(“%lld\n”, x); // ✅ 正确 - 未初始化变量
int x;
printf(“%d\n”, x); // ❌ 未定义行为(输出随机值)
解决方案:初始化变量:
int x = 0;
printf(“%d\n”, x); // ✅ 正确 - 字符串未以 \0 结尾
char str[5] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’};
printf(“%s\n”, str); // ❌ 未定义行为(未正确终止字符串)
解决方案:确保字符串以 \0 结尾:
char str[6] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘\0’};
printf(“%s\n”, str); // ✅ 正确
- 参数类型不匹配
-
printf 中的 * 用法
在 C 语言的 printf 函数中,* 是一个非常有用的格式修饰符,它允许我们在运行时动态指定:- 输出宽度(%*d)
- 字符串长度(%.*s)
- 精度(%*.*f)
- 宽度和精度(%*.*f)
基本语法 %[*][width][.precision]type
将格式说明符中的数字替换为一个动态的整数值,这个整数值需要在参数列表中提供。
| 格式 | 代码示例 | 输出结果 |
|---|---|---|
%*d | printf("%*d", 5, 123); | 123 |
%.*s | printf("%.*s", 3, "Hello"); | Hel |
%*.*f | printf("%*.*f", 8, 2, 3.14159); | 3.14 |
说明
%*d:动态设置字段宽度(右对齐补空格)。%.*s:动态截取字符串长度。%*.*f:动态设置字段宽度和小数位数(右对齐补空格)。- *表示使用一个额外的参数来指定最小字段宽度。这个额外的参数可以是任何整数类型的变量,也可以是整数常数
5.3 puts 函数(简洁方便)
-
函数原型:
int puts(const char *str); -
功能说明:将指定的 C语言字符串 输出到标准输出(stdout),并在末尾自动添加一个 换行符 \n。函数从 str 指向的地址开始复制字符,直到遇到 空字符 \0(字符串终止符)为止。空字符不会被输出,但会自动添加换行符。
注意:puts 与 fputs 的区别在于:puts 自动添加换行符,而 fputs 不会。puts 仅输出字符串,而 fputs 可以指定输出流(如文件)。
-
参数说明
| 参数 | 类型 | 描述 |
|---|---|---|
str | const char* | 要输出的 以 \0 结尾的 C语言字符串 |
- 返回值
| 情况 | 返回值 | 说明 |
|---|---|---|
| 成功 | 非负整数 | puts函数的返回值是实际输出的字符总数,包括字符串中的所有字符(包括空格)和它自动添加的换行符。(反正是 一定是非负整数) |
| 失败 | EOF(通常定义为 -1) | 并设置错误指示器 ferror |
- 示例代码
#include <stdio.h>
int main() {
char str[] = "Hello, World!";
int result = puts(str); // 输出字符串并自动换行
if (result == EOF) {
printf("Error writing to stdout\n");
}
return 0;
}
输出:
Hello, World!
-
使用注意事项
- 字符串必须以 \0 结尾
如果 str 未正确以 \0 结尾,puts 会持续读取内存直到遇到 \0,可能导致 缓冲区溢出 或 输出垃圾数据。 - 自动添加换行符
puts 会自动在字符串末尾添加换行符 \n,无需手动添加:
char str[] = “Hello”;
puts(str); // 输出 “Hello\n” - 避免输出非字符串数据
puts 仅接受字符串参数,不能格式化输出其他类型(如整数、浮点数):
int num = 42;
puts(num); // ❌ 错误!参数类型不匹配 - 检查返回值
在关键操作中建议检查返回值,防止输出失败导致程序异常:
if (puts(“Critical message”) == EOF) {
// 处理错误
}
- 字符串必须以 \0 结尾
-
与 printf 和 fputs 的对比
| 方法 | 语法 | 自动换行 | 格式化输出 | 适用场景 |
|---|---|---|---|---|
puts | puts(str); | ✅ 是(自动添加 \n) | ❌ 否 | 简单字符串输出 |
printf | printf("%s", str); | ❌ 否(需手动添加 \n) | ✅ 是 | 格式化输出(如拼接变量) |
fputs | fputs(str, stdout); | ❌ 否(需手动添加 \n) | ❌ 否 | 自定义输出流(如文件) |
5.4 putchar 函数(逐个字符输出)
- 函数原型:
int putchar(int character); - 功能说明:将 单个字符 输出到标准输出(stdout)。等效于调用 putc(character, stdout)。内部会将 character 强制转换为 unsigned char 后输出。
- 参数说明
| 参数 | 类型 | 描述 |
|---|---|---|
| character | int | 要输出的字符(以 int 类型传递,实际为字符的 ASCII 值) |
-
返回值
- 成功时:putchar 返回写入的字符(转换为 unsigned char 后再转为 int 类型)。
例如,putchar(‘A’) 成功时会返回 65(即 ‘A’ 的 ASCII 值)。 - 失败时:返回 EOF(通常定义为 -1),表示发生了错误(如写入到已关闭的文件流)。
- 成功时:putchar 返回写入的字符(转换为 unsigned char 后再转为 int 类型)。
-
示例代码
#include <stdio.h>
int main() {
// 输出大写字母 A-Z
for (char c = 'A'; c <= 'Z'; c++) {
putchar(c); // 逐个字符输出
}
return 0;
}
输出结果:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
-
使用注意事项
- 参数类型为 int,但实际输出为 unsigned char
putchar 接受 int 类型参数,但会将其转换为 unsigned char 后输出。
允许传入 char 类型(会自动提升为 int):
char c = ‘A’;
putchar(c); // ✅ 正确 - 不会自动添加换行符
putchar 仅输出单个字符,需手动添加换行符 \n:
putchar(‘A’);
putchar(‘\n’); // ✅ 输出 A 并换行 - 错误处理
建议检查返回值以捕获输出错误:
if (putchar(‘A’) == EOF) {
printf(“Error writing to stdout\n”);
}
- 参数类型为 int,但实际输出为 unsigned char
-
常见错误与解决方案
- 传递无效字符值
putchar(256); // ❌ 无效:ASCII 范围为 0~255
解决方案:确保字符值在 0~255 范围内:
putchar(65); // ✅ 正确(输出 ‘A’) - 忽略返回值
putchar(‘A’); // ❌ 忽略可能的错误
解决方案:检查返回值:
if (putchar(‘A’) == EOF) {
perror(“Output error”);
} - 未手动添加换行符
putchar(‘A’);
putchar(‘B’); // ❌ 输出 AB,无换行
解决方案:需要时手动添加 \n:
putchar(‘A’);
putchar(‘\n’); // ✅ 输出 A 并换行
- 传递无效字符值
💡 提示:putchar 内部直接操作输出缓冲区,连续调用时效率高于 printf(无格式解析)
5.5 fputs 函数说明
- 函数原型
int fputs ( const char * str, FILE * stream );
-
功能:将
str指向的 C 语言字符串写入指定的文件流(stream)。- 写入规则:从
str的起始地址开始复制字符,直到遇到字符串结束符\0为止。 - 终止符处理:字符串结束符
\0不会被写入文件流。
- 写入规则:从
-
与
puts的区别fputs:- 需要指定目标文件流(如
stdout或文件指针)。 - 不会自动添加换行符。
- 需要指定目标文件流(如
puts:- 默认写入
stdout。 - 自动在字符串末尾添加换行符。
- 默认写入
-
参数说明
str
需要写入文件流的 C 语言字符串。stream
指向文件流对象的指针(例如通过fopen打开的文件指针)。
-
返回值
- 成功时:返回一个非负整数值(通常为写入的字符数)。
- 失败时:返回
EOF,并设置错误标志(可通过ferror检查)。
-
示例代码说明
#include <stdio.h>
int main () {
FILE * pFile; // 文件指针
char sentence[256]; // 缓冲区用于存储用户输入
// 提示用户输入
printf("Enter sentence to append: ");
// 从标准输入读取一行(最多255字符)
fgets(sentence, 256, stdin);
// 以追加模式("a")打开文件 mylog.txt
pFile = fopen("mylog.txt", "a");
if (pFile != NULL) {
// 将用户输入写入文件(不包含换行符)
fputs(sentence, pFile);
// 关闭文件
fclose(pFile);
} else {
printf("Failed to open file.\n");
}
return 0;
}
代码功能
- 允许用户输入一行文本。
- 将输入内容追加到文件
mylog.txt中(文件不存在时会自动创建)。 - 不会自动添加换行符(需用户输入时手动输入换行,或程序中添加)。
总结
✅ fputs 的核心特性
| 特性 | 说明 |
|---|---|
| 写入目标 | 任意文件流(如文件、标准输出) |
| 换行符处理 | 不自动添加换行符(需手动添加) |
| 终止符处理 | 不写入字符串结束符 \0 |
| 返回值 | 成功:非负值;失败:EOF |
| 错误检查 | 使用 ferror(stream) 检查错误原因 |
⚠️ 常见注意事项
- 手动添加换行符(如
fputs("hello\n", fp))。 - 检查文件是否成功打开(避免空指针操作)。
- 避免缓冲区溢出(使用
fgets时限制长度)。 - 区分
fputs和puts:fputs:更灵活(可指定流),但需手动换行。puts:默认输出到stdout,自动换行。
5.6 使用注意事项
-
字符串结尾的
\0- C语言字符串必须以
\0结尾 - 如果字符串未正确以
\0结尾,printf和puts可能会继续读取内存直到遇到\0,导致意外输出
- C语言字符串必须以
-
避免缓冲区溢出
- 使用
printf时,避免直接输出未限制长度的字符串 - 推荐使用格式说明符限制输出长度:
- 使用
printf("%.100s", str); // 限制最多输出100个字符
puts与printf的选择- 简单字符串输出:使用
puts更简洁 - 需要格式化(如添加前缀、控制宽度):使用
printf - 需要逐字符处理:使用
putchar
- 简单字符串输出:使用
5.7 实际应用示例
示例1:使用 puts 输出简单字符串
#include <stdio.h>
int main() {
char greeting[] = "Welcome to C Programming!";
puts(greeting); // 自动换行
return 0;
}
示例2:使用 printf 进行格式化输出
#include <stdio.h>
#include <string.h>
int main() {
char name[] = "Alice";
int age = 25;
printf("Name: %s\n", name);
printf("Age: %d\n", age);
printf("Full info: %s, %d years old\n", name, age);
return 0;
}
示例3:逐字符输出字符串
#include <stdio.h>
int main() {
char message[] = "C is powerful!";
for (int i = 0; message[i] != '\0'; i++) {
putchar(message[i]);
}
putchar('\n'); // 添加换行符
return 0;
}
六. 基本运算符和其他运算符
6.1 基本运算符(算术运算符)
6.1.1 算术运算符
| 运算符 | 作用 | 示例 | 说明 |
|---|---|---|---|
+ | 加法/正号 | a + b | 二元运算符(加法),单目运算符(正号) |
- | 减法/负号 | a - b | 二元运算符(减法),单目运算符(负号) |
* | 乘法 | a * b | 二元运算符 |
/ | 除法 | a / b | 二元运算符(整数除法结果为整数,整数除法总是截断小数部分(向零取整),不会进行四舍五入。) |
% | 取余(模) | a % b | 二元运算符(仅适用于整数) |
++ | 自增 | a++ 或 ++a | 单目运算符(变量值+1) |
-- | 自减 | a-- 或 --a | 单目运算符(变量值-1) |
6.1.2 自增/自减运算符详解
| 用法 | 说明 | 示例 |
|---|---|---|
++a (前缀) | 先自增,后使用 | int a = 5; int b = ++a; → a=6, b=6 |
a++ (后缀) | 先使用,后自增 | int a = 5; int b = a++; → a=6, b=5 |
--a (前缀) | 先自减,后使用 | int a = 5; int b = --a; → a=4, b=4 |
a-- (后缀) | 先使用,后自减 | int a = 5; int b = a--; → a=4, b=5 |
注意:C语言中自增自减运算符只能用于变量,不能用于常量、表达式或任何不能作为左值的表达式。
6.1.3 算术运算符优先级
- 优先级从高到低:
*、/、%>+、- - 例如:
a + b * c→ 先计算b * c,再计算a + (b * c) - 例如:
a * b + c / d→ 先计算a * b和c / d,再计算加法
6.2 关系运算符
| 运算符 | 作用 | 示例 | 说明 |
|---|---|---|---|
> | 大于 | a > b | 比较大小 |
>= | 大于等于 | a >= b | 比较大小 |
< | 小于 | a < b | 比较大小 |
<= | 小于等于 | a <= b | 比较大小 |
== | 等于 | a == b | 判断相等(注意:是两个等号) |
!= | 不等于 | a != b | 判断不相等 |
注意:关系运算符的返回值是布尔值(0表示假,非0表示真)。
6.3 逻辑运算符
C语言的逻辑运算符只有三种:
运算符 作用 示例 说明
! 逻辑非 !a 逻辑取反(真→假,假→真)
&& 逻辑与 a && b 两个条件同时为真,结果才为真
|| 逻辑或 a || b 两个条件有一个为真,结果就为真
短路特性:
a && b:如果a为假,b不会被计算a || b:如果a为真,b不会被计算
补充: C语言中逻辑运算符的返回值
C语言中逻辑运算符(&&、||、!)的返回值不是bool类型,而是int类型(0或1)。
详细解释
-
返回值类型:
- 在C语言中,逻辑运算符的返回结果是
int类型,值为0(表示假)或1(表示真)。 - 逻辑运算符不执行通常的算术转换。相反,它们根据每个作数与0的等效性来计算每个作数。逻辑作的结果为0或1。结果的类型为int。
- 在C语言中,逻辑运算符的返回结果是
-
C99及更高版本的改进:
- 在C99标准中,引入了
_Bool类型和stdbool.h头文件,提供bool、true、false等宏。 - 但即使使用
stdbool.h,逻辑运算符本身的返回值仍然是int(0或1),只是可以将这些值视为布尔值。 - 逻辑运算符的结果类型是int,但在布尔上下文中通常被视为true或false。在C99及更高版本中,可以使用stdbool.h头文件中的bool类型来更明确地表示布尔值
- 在C99标准中,引入了
-
C语言的布尔处理方式:
- C语言编译系统在给出逻辑运算结果时,以数字1表示’真’,以数字0表示’假’,但在判断一个量是否为’真’时,以0表示’假’,以非0表示’真’。
- 在C语言中,逻辑运算符用于对表达式进行布尔逻辑运算,返回结果为0(假)或1(真)
实际使用示例
#include <stdio.h>
int main() {
int a = 5, b = 0;
// 逻辑与,返回int类型(0或1)
printf("a && b = %d\n", a && b); // 输出 0
// 逻辑或,返回int类型(0或1)
printf("a || b = %d\n", a || b); // 输出 1
// 逻辑非,返回int类型(0或1)
printf("!a = %d, !b = %d\n", !a, !b); // 输出 0, 1
// 在C99及更高版本中,可以使用bool类型
#include <stdbool.h>
bool result = (a > 0) && (b < 10); // result为true(1)
printf("result = %d\n", result); // 输出 1
return 0;
}
加粗样式关键区别
| 特性 | 逻辑运算符返回值 | 说明 |
|---|---|---|
| 类型 | int | 始终返回整数类型(0或1) |
| 值 | 0 = 假,1 = 真 | 但"真"的判断标准是非0值 |
| C99+ | 可用bool类型存储 | 但运算符本身仍返回int |
重要提示
- 在C语言中,任何非0值都被视为"真",只有0被视为"假"。
- 逻辑运算符的短路特性(如
a && b中如果a为假,则不计算b)是C语言的重要特性,也是实际编程中需要考虑的。
总结:C语言中逻辑运算符的返回值是int类型(0或1),不是bool类型。C99及更高版本通过stdbool.h提供了bool类型,但逻辑运算符本身返回的是整数。
6.4 赋值运算符
| 运算符 | 作用 | 示例 | 说明 |
|---|---|---|---|
= | 简单赋值 | a = b | 将 b 的值赋给 a |
+= | 加后赋值 | a += b | 等价于 a = a + b |
-= | 减后赋值 | a -= b | 等价于 a = a - b |
*= | 乘后赋值 | a *= b | 等价于 a = a * b |
/= | 除后赋值 | a /= b | 等价于 a = a / b |
%= | 取余后赋值 | a %= b | 等价于 a = a % b |
<<= | 左移后赋值 | a <<= b | 等价于 a = a << b |
>>= | 右移后赋值 | a >>= b | 等价于 a = a >> b |
&= | 按位与后赋值 | a &= b | 等价于 a = a & b |
^= | 按位异或后赋值 | a ^= b | 等价于 a = a ^ b |
|= | 按位或后赋值 | a|= b | 等价于 a = a | b |
赋值运算符的结合性:从右到左
a = b = c = 5; // 等价于 a = (b = (c = 5));
注意:整个赋值表达式的值是赋值后的值,即赋给左操作数的值
6.5 条件运算符(三元运算符)
条件 ? 表达式1 : 表达式2
- 如果条件为真,返回表达式1的值
- 如果条件为假,返回表达式2的值
示例:
int max = (a > b) ? a : b; // 如果a > b,max = a,否则max = b
6.6 逗号运算符
表达式1, 表达式2,表达式3, 表达式4...
- 返回值:整个逗号表达式的值是最后一个表达式的值
- 从左到右计算每个表达式
- 返回最后一个表达式的值
示例:
int a = 5, b = 10;
int c = (a++, b + a); // 先计算a++(a=6),再计算b+a(16),c=16
6.7 指针运算符
| 运算符 | 作用 | 示例 | 说明 |
|---|---|---|---|
* | 解引用 | *p | 获取指针p指向的值 |
& | 取地址 | &a | 获取变量a的内存地址 |
6.8 成员运算符
| 运算符 | 作用 | 示例 | 说明 |
|---|---|---|---|
. | 结构体成员访问 | struct s.name | 访问结构体成员 |
-> | 指针指向的结构体成员访问 | struct_ptr->name | 访问指针指向的结构体成员 |
6.9 求字节数运算符
| 运算符 | 作用 | 示例 | 说明 |
|---|---|---|---|
sizeof | 计算数据类型或变量的字节数 | sizeof(int) | 返回int类型占用的字节数 |
6.10 强制类型转换运算符
(类型)表达式
示例:
int a = 5;
double b = (double)a / 2; // 将a转换为double,避免整数除法
6.11 运算符优先级总结
优先级从高到低(从上到下优先级递减)
- 括号运算符:
()、[]、->、. - 单目运算符:
++、--、!、~、-(取反)、*(指针)、&(取地址)、sizeof - 乘除运算符:
*、/、% - 加减运算符:
+、- - 移位运算符:
<<、>> - 关系运算符:
>、>=、<、<= - 相等运算符:
==、!= - 按位与运算符:
& - 按位异或运算符:
^ - 按位或运算符:
| - 逻辑与运算符:
&& - 逻辑或运算符:
|| - 条件运算符:
? : - 赋值运算符:
=,+=,-=,*=,/=,%=等 - 逗号运算符:
,
结合性规律
- 大多数运算符:从左到右
- 赋值运算符:从右到左
- 单目运算符:从右到左
- 三目运算符:从右到左
简单记忆:
!> 算术运算符 > 关系运算符 >&&>||> 赋值运算符
6.12 常见错误与解决
错误1:混淆 = 和 ==
if (a = 5) { ... } // 错误!应为 if (a == 5)
解决:在条件判断中,将常量放在左边,避免误写:
if (5 == a) { ... } // 如果写成 if (5 = a),编译器会报错
错误2:忘记括号导致优先级错误
int a = 5, b = 3, c = 2;
int result = a + b * c; // 11,而非 20
解决:明确使用括号:
int result = a + (b * c); // 11
int result = (a + b) * c; // 16
错误3:自增/自减在表达式中的歧义
int a = 5;
int b = a++ + ++a; // 不确定结果(编译器可能返回 11、12 或其他)
解决:避免在单个表达式中多次使用自增/自减:
int a = 5;
int b = a + a + 1; // 11
a++; // 6
a++; // 7
七.类型转换详解
7.1 类型转换的定义与分类
C语言中的类型转换是指将一个数据类型的值转换为另一个数据类型的值。C语言提供两种类型转换方式:
- 隐式类型转换(自动转换):由编译器自动完成,无需程序员显式指定
- 显式类型转换(强制转换):由程序员通过强制类型转换运算符手动指定
7.2 自动类型转换规则
7.2.1 整型提升
-
定义
整型提升是指在表达式中,当char、short等较小的整数类型作为操作数时,自动转换为int或unsigned int类型的过程。这是C语言表达式求值中的隐式类型转换。
关键点: 整型提升是C语言中保证表达式计算一致性和效率的机制。 -
整型提升的原因
- CPU硬件限制:表达式的整型运算要在CPU的运算器(ALU)内执行,而ALU操作数的字节长度一般就是int的字节长度(32位),同时也是CPU通用寄存器的长度。
- 计算效率:CPU通常更高效地处理int长度的整数,而非更小的类型。
- 表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
-
整型提升的规则
整形提升是在符号位前面补位
| 类型 | 规则 | 示例 |
|---|---|---|
| 有符号整数 | 高位补充符号位(最高位为0补0,为1补1) | char c =-1; → 整型提升为 0xFFFFFFFF(-1) |
| 无符号整数 | 高位补0 | unsigned char c = 255; → 整型提升为 0x000000FF(255) |
- 整型提升的注意事项: 整型提升不影响原始变量的类型,只影响表达式求值时的操作数类型
7.2. 2 算术转换
-
定义:算术转换是指当表达式中存在不同类型的操作数时,C语言自动进行的类型转换,目的是将操作数转换为同一类型,以便进行运算。
-
算术转换的规则
2.1 浮点类型优先级:- 如果任一操作数是long double,另一个转换为long double
- 如果任一操作数是double,另一个转换为double
- 如果任一操作数是float,另一个转换为float
2.2 整型类型转换规则,如果所有操作数都不是浮点型,按以下顺序转换:
- 如果任一操作数是unsigned long,另一个转换为unsigned long
- 如果任一操作数是long且另一个是unsigned int,两个都转换为unsigned long
- 如果任一操作数是long,另一个转换为long
- 如果任一操作数是unsigned int,另一个转换为unsigned int
- 否则,两个操作数转换为int
注意
转换等级:char, short → int → unsigned int → long → unsigned long → long long → unsigned long long → float → double → long double
- 算术转换的典型示例
#include <stdio.h>
int main() {
int i = -1; // 有符号int
if (i > sizeof(i)) {
printf("大于\n"); // 输出"大于"
} else {
printf("小于\n");
}
return 0;
}
解释:
i是-1,二进制为11111111 11111111 11111111 11111111(补码)
sizeof(i)是4(无符号整数)
算术转换:i被转换为无符号int,解释为0xFFFFFFFF(4294967295)
比较:4294967295 > 4,所以输出"大于"
- 算术转换的注意事项
- 保无符号规则:当操作符两边是signed和unsigned类型时,signed类型会被转换为unsigned类型
int i = -1;
unsigned int u = 1;
if (i > u) // i被转换为unsigned int,结果为true
- 类型转换可能丢失信息:
float f = 3.9;
int i = f; // i = 3(截断,不是四舍五入)
- 算术转换优先于运算符:
float f = (float)3 / 2; // 先类型转换,再除法
- 整型提升与算术转换的关系
| 特性 | 整型提升 | 算术转换 |
|---|---|---|
| 发生场景 | 表达式中使用char、short等小整数类型 | 表达式中存在不同类型的操作数 |
| 转换目的 | 使小整数类型能参与int级运算 | 使不同类型的操作数能进行相同运算 |
| 转换规则 | 有符号数符号扩展,无符号数零扩展 | 按类型等级从低到高转换 |
| 是否影响原始变量 | 不影响 | 不影响 |
| 与CPU的关系 | 为适应CPU整型运算器(ALU) | 为适应表达式求值的一致性 |
关键区别:整型提升是算术转换的一部分,是算术转换中处理小整数类型的一种特殊形式。
7.2.3 赋值过程中的类型转换
- 赋值过程中,右侧表达式的值会被转换为目标变量的类型
- 将浮点数值赋予整型变量时,小数部分会被截断(不是四舍五入)
- 例如:
int a = 3.9;→a的值为3
7.2.4 函数调用和返回值的类型转换
- 函数调用时,参数会转换为相应参数的类型
- 函数返回时,返回表达式的值会转换为函数的返回类型
7.3 强制类型转换
- 语法
(type_name)expression
type_name:目标数据类型expression:要转换的表达式
- 示例
int a = 5;
float b = (float)a; // 显式将整型转换为浮点型
float c = 3.14;
int d = (int)c; // 显式将浮点型转换为整型(截断小数部分)
char e = 'A';
float f = (float)e; // 字符的ASCII码值转换为浮点型
- 强制类型转换的注意事项
- 强制类型转换优先级较高
- 强制类型转换会得到所需类型的中间值,而操作数的类型不会发生变化
- 将浮点型强制转换为整型时,不会进行四舍五入,而是直接截断
7.4 类型转换的典型示例
- 整型转浮点型
int num = 10;
float f = (float)num; // f = 10.0
- 浮点型转整型
float f = 10.75;
int num = (int)f; // num = 10(截断小数部分,不是四舍五入)
- 字符型转整型
char c = 'A';
int ascii = (int)c; // ascii = 65('A'的ASCII码值)
- 整型转字符型
int ascii = 65;
char c = (char)ascii; // c = 'A'
- 混合类型运算
int a = 5;
float b = 2.5;
float c = a + b; // a自动转换为float,结果为7.5
7.5 类型转换的注意事项与常见错误
- 精度丢失
- 从高精度类型(如
double)向低精度类型(如float)转换时,可能存在精度丢失 - 例如:
double d = 0.1234567890123456789;转换为float时,精度会降低
- 赋值时的截断
- 浮点数赋值给整型时,小数部分会被直接截断,而非四舍五入
- 例如:
int i = 3.9;→i的值为3,不是4
- 转换范围溢出
- 当转换值超出目标类型的表示范围时,可能导致未定义行为
- 例如:将
int的值2147483647转换为char时,可能会溢出
- 混合类型运算的优先级
- 类型转换的优先级高于算术运算
- 例如:
float result = (float)a * b;先进行类型转换,再进行乘法
7.6 类型转换的常见应用场景
- 数值计算
当不同数据类型的数值需要进行运算时,确保计算精度:
int a = 7;
int b = 2;
float result = (float)a / b; // 结果为3.5,而非3(整数除法)
- 函数参数传递
当函数参数类型与实际传递的类型不一致时:
void printFloat(float x) {
printf("Value: %f", x);
}
int main() {
int a = 5;
printFloat((float)a); // 显式转换为float类型
return 0;
}
- 指针类型转换
在处理指针时,可能需要将指针类型转换为其他类型:
int num = 10;
void *ptr = #
int *intPtr = (int *)ptr; // 指针类型转换
7.7 总结
| 类型 | 转换方式 | 说明 | 注意事项 |
|---|---|---|---|
| 自动转换 | 编译器自动完成 | 算术运算、赋值、函数调用时 | 低精度转高精度,小数部分截断 |
| 强制转换 | (type)expression | 程序员手动指定 | 优先级高,可能造成精度丢失 |
| 长→短 | 截断 | 如int转short | 直接截取低位,可能丢失数据 |
| 短→长 | 符号扩展/零扩展 | 如char转int | 有符号数符号扩展,无符号数零扩展 |
重要提示:在进行类型转换时,要特别注意可能带来的精度丢失和数据截断问题。当需要保留精度时,应使用高精度类型进行计算,避免不必要的类型转换。
2715

被折叠的 条评论
为什么被折叠?



