ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C语言数据类型深度解析:从内存布局到工程实践

C语言数据类型深度解析:从内存布局到工程实践 1. 项目概述为什么数据类型是C语言的基石如果你刚开始接触C语言可能会觉得数据类型这个概念既基础又有点枯燥。不就是int、float、char这些吗书上讲老师也讲。但在我十多年的嵌入式开发和系统编程经历里见过太多因为对数据类型理解不透彻而引发的“血案”程序在32位机器上跑得好好的一到64位环境就内存溢出网络传输的数据接收端解析出来全是乱码一个简单的数值计算结果却偏差了十万八千里。这些问题追根溯源十有八九都和数据类型的底层表示、内存布局、运算规则有关。C语言被称为“高级语言中的汇编语言”其强大和灵活很大程度上源于它对数据类型的精细控制。它不像Python或JavaScript那样一个变量可以随意存放任何类型的数据。在C语言里每一个变量在诞生时就必须明确宣告它的“身份”——也就是数据类型。这个宣告决定了三件至关重要的事第一这个变量要在内存中占据多大的“地盘”存储空间第二这块“地盘”里的0和1应该如何被解读编码格式第三能对这块“地盘”进行哪些“操作”允许的运算。所以把C语言的数据类型学透绝不是死记硬背几个关键字和字节数。它是在理解计算机如何存储和处理信息是在搭建一座连接高级逻辑思维与底层硬件实现的桥梁。无论是你想深入操作系统内核、编写高性能的嵌入式固件还是为学习C、Rust等系统级语言打下坚实基础这一关都必须过得扎扎实实。接下来我们就抛开那些笼统的概述深入到每一个数据类型的“五脏六腑”里去看个明白。2. 核心需求解析我们到底需要掌握什么面对“数据类型”这个主题很多初学者容易陷入两个误区一是只记名字和大概范围二是过早地去死记各种编译器下的具体字节数。我们的核心需求是建立一个系统且深刻的理解框架。这个框架应该能回答以下问题2.1 理解“类型系统”的哲学C语言的类型系统是“静态的”、“弱类型的”。静态意味着类型检查主要发生在编译期变量一旦声明其类型通常就不能改变不像Python的变量可以随时指向不同类型的对象。弱类型意味着它提供了相当灵活有时甚至是危险的类型转换机制比如整数和字符、整数和指针之间常常可以隐式转换。理解这一点就能明白为什么C语言效率高编译时确定很多信息但也需要程序员格外小心编译器不会帮你严格把关所有类型错误。2.2 掌握“内存视角”的分析方法这是最关键的一环。我们不能只停留在“int是4个字节”这样的结论上。必须建立起“变量 - 类型 - 内存大小 - 内存布局 - 值范围”这一连串的思维链条。例如当你说“int a 10;”时你应该能在脑海里勾勒出在内存的某个地址比如0x7ffd2c6a4b34开始连续4个字节假设是4字节int被分配里面存储着二进制补码形式的10。2.3 厘清“可移植性”与“确定性”的平衡C语言标准如C99、C11只规定了每种基本类型的最小范围并没有规定精确的字节数。int可以是2字节、4字节甚至是8字节这取决于编译器、操作系统和硬件架构这就是“数据模型”如LP32, ILP32, LP64。我们的需求不是记住所有情况而是学会如何编写可移植的代码。这就需要我们掌握sizeof运算符、limits.h和stdint.h头文件的使用让程序在编译时自我适配而不是写死一个数值。2.4 打通“类型”与“运算”的关联不同的数据类型支持的运算是不同的。整数类型可以进行位运算,|,~,,而浮点数不行。指针类型支持加减运算以实现偏移但相乘、相除没有意义。char类型虽然本质是整数但通常用来参与字符处理相关的逻辑。理解类型如何影响运算行为是写出正确、高效代码的前提。基于以上需求我们将从最基础的分类开始逐一拆解并注入大量实际开发中积累的经验和避坑指南。3. 基本数据类型深度剖析C语言的数据类型大厦建立在几块最基本的基石之上。我们常说“基本数据类型”通常指的是算术类型包括整数类型、浮点数类型和字符类型。下面我们抛开简单的定义深入其内存和运算细节。3.1 整数类型不只是数字更是位模式整数类型是C语言中使用最频繁的类型。其核心在于理解它们在内存中以“二进制补码”形式存储。3.1.1 有符号与无符号的本质区别这是第一个关键分水岭。以int和unsigned int为例。有符号 (int): 最高位Most Significant Bit, MSB是符号位。0表示正数或零1表示负数。数值范围是对称的对于n位范围是 -2^(n-1) 到 2^(n-1)-1。例如32位int的范围是 -2,147,483,648 到 2,147,483,647。无符号 (unsigned int): 所有位都用于表示数值。范围是 0 到 2^n - 1。例如32位unsigned int的范围是 0 到 4,294,967,295。注意混用有符号和无符号类型是常见的错误源头。当它们在一起运算时C语言会进行“通常的算术转换”将有符号数转换为无符号数这可能导致意想不到的结果。例如int a -1; unsigned int b 10;判断(a b)结果会是0假因为-1被转换成了一个巨大的无符号数。3.1.2 不同长度的整数类型C语言提供了不同“宽度”的整数类型以适应不同需求short (int): 通常为2字节。用于节省空间存储较小的整数。int: 机器的“自然大小”通常与CPU字长相关32位系统常为4字节64位系统也常为4字节。是最通用的整数类型。long (int): 长度不小于int。在Linux 64位系统下通常是8字节。long long (int): C99引入至少8字节用于处理非常大的整数。实操心得如何确定具体大小永远不要死记硬背。在编写代码时尤其是涉及网络协议、文件格式等需要精确字节定义的场景使用sizeof运算符。#include stdio.h int main() { printf(Size of short: %zu bytes\n, sizeof(short)); printf(Size of int: %zu bytes\n, sizeof(int)); printf(Size of long: %zu bytes\n, sizeof(long)); printf(Size of long long: %zu bytes\n, sizeof(long long)); return 0; }在自己的开发环境中运行这段代码是了解平台特性的第一步。3.1.3 可移植性利器stdint.h为了避免“int到底是2字节还是4字节”的困惑C99标准引入了stdint.h头文件定义了确定宽度的整数类型。这是编写跨平台代码的黄金标准。int8_t,int16_t,int32_t,int64_t: 确定位宽的有符号整数。uint8_t,uint16_t,uint32_t,uint64_t: 确定位宽的无符号整数。还有int_leastN_t至少N位和int_fastN_t处理最快的至少N位等类型。在嵌入式、网络编程中应优先使用uint32_t、int16_t这类明确类型而不是模糊的int或long。3.2 字符类型披着字符外衣的整数char类型非常特殊它本质上是1个字节的整数。char: 标准没有明确规定它是有符号还是无符号。这由编译器实现定义。你可以通过CHAR_MIN在limits.h中来判断如果CHAR_MIN是0则是unsigned char如果是负数如-128则是signed char。signed char: 明确表示有符号的1字节整数。unsigned char: 明确表示无符号的1字节整数。核心应用场景存储字符使用char并遵循ASCII、UTF-8等编码规范。‘A’在内存中就是整数65。处理原始内存当需要逐字节处理一块内存如图像数据、网络数据包时unsigned char*是最常用的指针类型因为它能确保每个字节都被解释为0-255的正数避免符号扩展问题。小范围整数如果需要一个非常小的整数范围且明确知道数值不会溢出可以使用char来节省空间在内存极度受限的嵌入式系统中很常见。踩坑记录将char类型用于数值计算时要格外小心。例如char c 0xFF;如果char是有符号的那么c的值是-1而不是255。后续如果进行if(c 0xFF)判断结果会是假。安全的做法是在需要数值时明确使用unsigned char。3.3 浮点类型近似艺术与IEEE 754标准浮点数是用来近似表示实数的。理解其“近似性”至关重要。C语言主要提供两种浮点类型float: 单精度浮点数。通常为4字节遵循IEEE 754标准。提供约6-7位有效十进制数字。double: 双精度浮点数。通常为8字节。提供约15-16位有效十进制数字。是默认的浮点数字面值类型如3.14默认为double。long double: 扩展精度浮点数长度和精度由实现定义通常不少于double。3.3.1 内存布局与精度陷阱以最常见的IEEE 754单精度float为例4字节1位符号位 (S)8位指数位 (E)23位尾数位 (M) 数值 (-1)^S * 1.M * 2^(E-127)这种表示法意味着很多十进制小数无法被精确表示如0.1就像十进制无法精确表示1/3一样。因此永远不要用来直接比较两个浮点数是否相等。正确做法是比较两者差的绝对值是否小于一个极小的误差范围epsilon#include math.h // 错误的做法if (a b) ... // 正确的做法 #define EPSILON 1e-6 if (fabs(a - b) EPSILON) { // 认为 a 和 b 相等 }3.3.2 特殊值浮点数有特殊的位模式来表示一些特殊值无穷大正无穷 (INFINITY)、负无穷 (-INFINITY)由除以0.0等操作产生。非数NaN(Not a Number)由无效操作产生如sqrt(-1)、0.0/0.0。一个关键特性是NaN与任何值包括它自己比较结果都是假。即NaN NaN返回0。需要使用isnan()函数来判断。实操心得默认使用double在现代计算机上double的精度和速度通常都不是问题甚至有些硬件架构下double的运算速度与float相差无几。除非有明确的存储空间或带宽压力如大规模数组否则优先使用double以获得更好的精度。警惕累积误差在循环中进行大量浮点运算时误差会累积。对于金融等需要精确计算的领域应考虑使用定点数库或十进制浮点数库如decimalin C# C语言中需第三方库。查看内存表示调试时如果想看一个float在内存中的实际十六进制表示可以将其地址强制转换为unsigned int*注意字节序来查看。这有助于理解其底层编码。4. 派生数据类型构建与应用基本类型像砖块而派生类型则像用这些砖块搭建起来的墙、柱子乃至整个建筑。它们是C语言构建复杂数据结构的工具。4.1 数组线性序列的容器数组是在内存中连续排列的、同一类型元素的集合。声明type name[length];如int scores[100];关键特性数组名在大多数表达式中会“退化”为指向其首元素的指针。即scores等价于scores[0]。4.1.1 一维数组的内存布局对于int arr[5] {1,2,3,4,5};假设int为4字节arr的起始地址为0x1000则arr[0]位于0x1000 - 0x1003arr[1]位于0x1004 - 0x1007... 以此类推地址连续递增。4.1.2 多维数组的本质C语言只有一维数组多维数组是“数组的数组”。例如int matrix[3][4];是一个包含3个元素的数组每个元素又是一个包含4个int的数组。它在内存中是按行优先连续存储的。matrix[0][0],matrix[0][1], ...,matrix[0][3],matrix[1][0], ...当作为函数参数传递时只有第一维的大小可以省略其他维必须指明void func(int arr[][4], int rows);常见问题与排查数组越界这是最经典、最危险的错误。C语言不会检查数组索引是否有效。越界写可能破坏栈上的其他变量导致程序行为诡异甚至覆盖函数返回地址导致崩溃或安全漏洞。务必手动确保索引在[0, length-1]范围内。数组大小传递数组作为函数参数传递时会丢失其长度信息。通常需要额外传递一个长度参数void processArray(int arr[], int size);。sizeof的妙用在定义数组的同一作用域内sizeof(array)返回的是整个数组占用的字节数。sizeof(array) / sizeof(array[0])是计算数组元素个数的经典且安全的方法。4.2 结构体异质数据的聚合结构体struct允许你将多个不同类型的变量组合成一个逻辑整体。4.2.1 定义与初始化struct Person { char name[50]; int age; float height; }; // 初始化方式 struct Person p1 {Alice, 30, 165.5}; // 顺序初始化 struct Person p2 {.age 25, .name Bob, .height 180.0}; // 指定成员初始化C994.2.2 内存对齐性能与空间的权衡这是结构体最核心、也最容易出问题的地方。为了CPU高效访问内存编译器会对结构体成员进行“内存对齐”。规则大致是每个成员的起始地址必须是其自身类型大小或编译器对齐模数alignment的整数倍。struct Example { char a; // 1字节地址偏移0 // 编译器插入3字节填充padding以满足下一个int的对齐要求 int b; // 4字节地址偏移必须是4的倍数所以从偏移4开始 char c; // 1字节地址偏移8 // 编译器在末尾插入3字节填充使得整个结构体大小是最大成员(int)的整数倍4的倍数 }; // sizeof(struct Example) 很可能是12而不是 1416。影响对齐影响了结构体的sizeof大小也影响了其在内存中的布局。控制对齐可以使用编译器指令如GCC的__attribute__((packed))来取消对齐但这可能导致性能下降甚至在某些架构上引发硬件异常。通常不建议随意打包除非有明确的协议对齐要求如网络数据包、硬件寄存器映射。4.2.3 结构体作为函数参数直接传递大型结构体会导致整个结构体被复制到栈上开销较大。通常的做法是传递指向结构体的指针。void printPerson(const struct Person *p) { // 使用const指针避免意外修改 printf(Name: %s, Age: %d\n, p-name, p-age); }4.3 共用体共享内存的魔法共用体union的所有成员共享同一块内存空间。其大小足以容纳最大的成员。union Data { int i; float f; char str[20]; } data;当你给data.i赋值后再读取data.f得到的是一个由int的位模式解释出来的float值通常是无意义的。这常用于“类型双关”type punning但需注意严格别名规则Strict Aliasing Rule可能引发的未定义行为。核心应用节省空间在多个数据成员不会同时使用的场景下如协议消息可以是命令码或数据负载。位操作与数据解析方便地提取一个整数的各个字节或将多个字节组合成一个整数。模拟多态结合一个类型标识符type tag和共用体可以模拟简单的变体类型。4.4 枚举赋予整数意义的名字枚举enum提供了一种定义命名整数常量的方式使代码更易读。enum Weekday {MON, TUE, WED, THU, FRI, SAT, SUN}; enum Weekday today WED;默认情况下第一个枚举符值为0后续依次加1。可以显式指定值enum State {OK 0, ERROR -1, TIMEOUT 1};枚举类型在C中本质上就是整数类型通常是int可以参与整数运算但这会削弱其类型安全性。C中的enum class在这方面更严格。5. 指针类型内存的导航员指针是C语言的灵魂也是难点所在。它存储的是内存地址。5.1 指针基础与运算声明type *ptr_name;如int *p;取地址与解引用运算符获取变量地址*运算符解引用指针获取所指位置的值。int var 42; int *p var; // p指向var printf(%d\n, *p); // 输出42 *p 100; // 通过p修改var的值为100指针运算指针的加减运算以所指向类型的大小为单位进行移动。int arr[5] {10, 20, 30, 40, 50}; int *ptr arr[1]; // ptr指向20 ptr; // 现在ptr指向30因为int是4字节地址实际增加了4 printf(%d\n, *(ptr 2)); // 输出50ptr2指向第4个元素索引3即50ptr n等价于(char*)ptr n * sizeof(*ptr)。5.2 指针与数组的紧密关系如前所述数组名在表达式中退化为指针。这使得指针可以像数组一样使用下标[]运算符。int arr[3] {1,2,3}; int *p arr; // 等价于 p arr[0] printf(%d %d\n, p[1], *(p1)); // 都输出2反过来数组访问arr[i]在编译器内部也被解释为*(arr i)。5.3 多级指针与指针数组指针数组数组的元素是指针。char *str_array[10];这是一个包含10个char*指针的数组常用于存储多个字符串。数组指针指向整个数组的指针。int (*ptr_to_array)[10];这是一个指向“包含10个int的数组”的指针。它与int *ptr[10];指针数组有本质区别。多级指针int **pp;指向int*的指针。常用于动态二维数组、在函数中修改指针参数本身等场景。5.4void指针泛型指针void*是一种通用指针类型可以指向任何类型的数据但不能直接进行解引用和算术运算因为不知道步长。主要用途泛型函数参数如qsort、memcpy等标准库函数的参数。动态内存分配malloc、calloc返回void*需要强制类型转换。面向底层的内存操作。void *generic_ptr; int n 10; generic_ptr n; // 可以指向int // int val *generic_ptr; // 错误不能直接解引用 int val *(int*)generic_ptr; // 正确先强制转换类型重要提示使用void*时要非常清楚它当前实际指向的类型错误的类型转换会导致未定义行为。6. 类型限定符与修饰符这些关键字修饰在类型之前或之后改变了类型的某些属性。6.1const不变性的承诺const表示“只读”。它承诺程序不会通过这个标识符去修改它所绑定的对象。指向常量的指针const int *p;或int const *p;两者等价。指针指向的内容是常量不能通过p修改但p本身可以指向别的变量。常量指针int *const p var;指针本身是常量初始化后不能再指向别处但可以通过它修改所指内容。指向常量的常量指针const int *const p var;两者都不可变。经验之谈在函数参数中尽可能使用const指针来传递不希望被修改的数据。这既是良好的接口文档告诉调用者函数不会修改你的数据也能让编译器进行更好的优化和错误检查。6.2volatile阻止编译器优化volatile告诉编译器这个变量的值可能会被程序之外的代理如硬件寄存器、另一个线程改变因此编译器不应对其读写操作进行激进的优化如缓存到寄存器、删除“冗余”读取。典型应用场景内存映射硬件寄存器嵌入式开发中读取一个硬件状态寄存器。多线程共享变量在C语言标准中volatile不能完全保证线程安全需要配合原子操作或内存屏障但这是一个常见用法。信号处理函数中修改的全局变量。volatile uint32_t *status_reg (volatile uint32_t*)0x40021000; while (*status_reg 0x01) { // 每次循环都必须从内存读取status_reg不能优化掉 // 等待某个标志位被硬件置位 }6.3static链接与持久性static的含义取决于它所处的上下文在函数内部局部变量将变量的存储期从“自动”变为“静态”。变量在程序生命周期内只初始化一次在多次函数调用间保持其值。同时它改变了变量的链接属性从“无链接”变为“内部链接”不对于局部static它没有链接性只是存储期改变。在全局作用域变量或函数将标识符的链接属性从“外部链接”改为“内部链接”。这意味着该变量或函数仅在定义它的源文件内可见其他源文件无法通过extern声明来访问它。这是实现信息隐藏、避免命名冲突的重要手段。6.4register过时的建议register关键字建议编译器将变量存储在CPU寄存器中以加快访问速度。但现代编译器的优化器远比程序员聪明能够自动决定哪些变量放入寄存器。因此register在C99之后基本已弃用不再具有实际意义它只是作为一个提示且不能对register变量取地址。7. 类型转换显式与隐式的规则C语言中类型转换无处不在理解其规则是避免诡异Bug的关键。7.1 隐式类型转换自动转换发生在表达式求值、赋值、函数调用传参等过程中编译器自动进行。整型提升在表达式中char、short等小于int的类型会先被提升为int或unsigned int再参与运算。通常的算术转换当一个运算符两边的操作数类型不同时会按照一个复杂的层次结构进行转换目标是转换为“更宽”或“更精确”的类型。大致顺序是int-unsigned int-long-unsigned long-long long-unsigned long long-float-double-long double。赋值转换等号右边的值会被转换为左边变量的类型。隐式转换的陷阱有符号与无符号混合如前所述可能导致负数被解释为大正数。整数与浮点数混合整数转换为浮点数可能损失精度当整数超出浮点数有效位数时。窄化转换将宽类型赋给窄类型如long long赋给int高位被截断可能丢失信息。7.2 显式类型转换强制转换使用(type_name) expression语法进行。double pi 3.14159; int int_pi (int)pi; // int_pi 3直接截断小数部分 float f (float)10 / 3; // 将整数10转换为float再运算得到浮点数结果用途消除编译器警告。进行有意识的类型转换如将void*转换为具体指针类型。执行特定的数值处理如取整。风险强制转换会覆盖编译器的类型检查。如果转换不合理如将指针转换为不相关的整数类型会导致未定义行为。务必确保转换在逻辑上是安全的。8. 自定义类型typedef的妙用typedef用于为已有的类型创建一个新的别名而不是创建新类型。它极大地提高了代码的可读性和可维护性。typedef unsigned char uint8_t; // 标准库的做法 typedef struct { char name[50]; int age; } Person; // 现在可以直接用 Person 代替 struct Person typedef int (*Comparator)(const void*, const void*); // 定义函数指针类型好处简化复杂声明特别是对于函数指针和结构体。提高可移植性通过typedef将平台相关的类型如int32_t抽象出来只需修改typedef定义即可适配不同平台。增强代码表达力DistanceInMeters比double更能表达变量的含义。9. 常见问题与排查技巧实录在实际开发中数据类型相关的问题往往表现为一些令人困惑的现象。下面是一些典型场景和排查思路。9.1 数值溢出与回绕现象计算结果与预期不符特别是涉及大数运算或循环边界时。unsigned char counter 255; counter counter 1; printf(%u\n, counter); // 输出 0发生了回绕排查检查涉及变量的类型范围。使用limits.h中的宏如INT_MAX,UINT_MAX来了解边界。对于可能溢出的运算在运算前进行范围检查。或者考虑使用范围更大的类型如用long long做中间计算。对于无符号整数明确意识到回绕是定义良好的行为模运算对于有符号整数溢出是未定义行为结果不可预测。9.2 浮点数比较失败现象两个理论上应该相等的浮点数用比较却返回false。float a 0.1 0.2; float b 0.3; if (a b) { // 很可能为假 printf(Equal\n); }排查与解决永远不要直接比较浮点数相等。使用绝对误差或相对误差进行比较。#include math.h #include float.h int float_equal(float a, float b) { float abs_diff fabsf(a - b); // 绝对误差法适用于数值接近0的情况 if (abs_diff 1e-6) return 1; // 相对误差法更通用 float rel_diff abs_diff / (fabsf(a) fabsf(b) FLT_MIN); return rel_diff 1e-6; }对于货币等精确计算考虑使用整数以分为单位或专门的十进制库。9.3 指针错误野指针、空指针、悬垂指针现象程序崩溃段错误、数据损坏、行为不可预测。野指针指针变量未初始化。其值是随机的垃圾地址。空指针指向NULL或(void*)0的指针。对其解引用会导致崩溃。悬垂指针指针指向的内存已被释放free但指针本身仍保留着原来的地址。排查与预防初始化定义指针时立即初始化为NULL。int *p NULL;检查后再用在解引用指针前检查是否为NULL。释放后置空free(ptr); ptr NULL;这是一个好习惯。注意作用域避免返回指向局部变量的指针。使用工具Valgrind、AddressSanitizer等内存调试工具是发现这类问题的利器。9.4 结构体字节对齐导致的序列化问题现象将一个结构体直接写入文件或通过网络发送在另一端读取时成员值错乱或解析失败。struct Packet { char type; int id; short length; }; // 直接 fwrite(packet, sizeof(struct Packet), 1, file); 可能出错。原因结构体内部和末尾可能有填充字节不同平台、不同编译器设置下的对齐方式可能不同导致sizeof(struct Packet)不一致内存布局也不同。解决方案手动序列化/反序列化不要直接读写整个结构体。而是逐个成员进行读写使用确定大小的类型如uint32_t。void write_packet(FILE *fp, const struct Packet *p) { fputc(p-type, fp); uint32_t net_id htonl(p-id); // 转换字节序 fwrite(net_id, sizeof(uint32_t), 1, fp); uint16_t net_len htons(p-length); fwrite(net_len, sizeof(uint16_t), 1, fp); }使用编译器指令打包结构体谨慎使用#pragma pack(1)或__attribute__((packed))。但这会牺牲性能且可能引发非对齐内存访问错误在某些架构如ARM上。设计协议时考虑对齐合理安排结构体成员的顺序从大到小或从小到大排列可以减少填充字节。例如将上面的结构体改为{ int id; short length; char type; }填充可能会减少。9.5sizeof在数组参数中的“陷阱”现象在函数内部对数组参数使用sizeof得到的是指针的大小而不是数组的大小。void printSize(int arr[10]) { printf(Array size in function: %zu\n, sizeof(arr)); // 输出864位系统指针大小而不是40 }原因数组作为函数参数时会退化为指针。函数内部并不知道数组的实际长度。解决方案始终将数组长度作为另一个参数传递给函数。void processArray(int arr[], size_t size) { for (size_t i 0; i size; i) { // 处理 arr[i] } }理解C语言的数据类型是一个从“是什么”到“为什么”再到“怎么用”和“怎么避坑”的完整过程。它远不止是语法手册上的几个表格。我个人的体会是每当在代码中定义一个变量时都应该在脑海里过一遍它需要多少内存它的值范围够用吗它会被如何解释它和其他类型运算时会怎样养成这样的思维习惯就能从根源上避免大量隐蔽的错误写出更健壮、更高效的C语言程序。最后再分享一个小技巧多读优秀的开源C项目代码如Linux内核、Redis观察它们是如何严谨地使用数据类型的特别是stdint.h中的类型和const限定符的使用这是提升对数据类型理解的最佳途径之一。
返回列表