
1. 数据类型到底在讲什么先说个直觉判断数据类型这件事几乎决定了你之后写的每一行C代码长什么样。我这段时间带几个刚接触编程的朋友入门发现大家卡住的地方往往不是语法本身而是搞不清楚为什么一个变量还要分这么多种类型。其实道理特别简单计算机里存数据就像去快递柜存东西大格子、小格子、冷藏格子都是给不同类型的东西准备的你用错格子轻则浪费空间重则东西根本放不进去。C是一门静态类型语言这句话翻译成人话就是每个变量在出生那一刻就必须说清楚自己是什么类型。好处是编译器能帮你提前抓出一大批错误坏处是入门阶段确实多了一道门槛。所以这篇启蒙笔记第二篇咱们就把数据类型这件事彻底聊透从最基础的整型、浮点型开始一直到类型转换的坑我尽量用实际能跑通的代码和踩过的坑来讲。这篇文章适合谁看呢你已经会写最简单的int main()和cout输出但对类型还处于一知半解的阶段。看完之后你至少能回答三个问题一个数到底该用哪种类型int和long long差别有多大为什么两个double相减会得出一个看着很奇怪的结果数据类型听着像是理论课其实它就是纯粹的实操课。你在键盘上敲下的每一个变量声明都是在和内存打交道。所以我们先建立一个基本共识类型决定了两件事——这块内存有多大以及这块内存里的二进制怎么解释。2. 整型家族int背后的一堆亲戚2.1 基础整型有哪些C里的整型不是一个int打天下而是一个家族。常用的是short、int、long、long long它们主要的区别就是能表示的范围不一样。如果你去查资料会看到各种至少16位至少32位之类的描述这其实是标准给编译器的最低要求不是精确值。我在实际使用中更在意的是当前平台下真实的大小。绝大多数情况下你在自己的电脑上会遇到这种情况short通常2个字节范围约-32768到32767int通常4个字节范围约-21亿到21亿long在Windows上是4个字节在很多Linux环境里是8个字节long long通常8个字节范围极大基本不用担心溢出这段代码可以让你在自己机器上验证#include iostream using namespace std; int main() { cout short: sizeof(short) 字节 endl; cout int: sizeof(int) 字节 endl; cout long: sizeof(long) 字节 endl; cout long long: sizeof(long long) 字节 endl; return 0; }sizeof是C里一个非常实用的运算符它直接告诉你某个类型占了几个字节。我建议你亲手跑一下这段代码因为网上说的都不如自己机器上看到的结果可信。2.2 int到底够不够用这是一个特别经典的问题什么时候用int什么时候必须换更大的我自己的判断标准很简单只要你预感这个数可能超过20亿就直接上long long。举个例子计算从1加到100万结果大概是5000亿这已经远超int的范围了。你要是用int去存结果会变成负数而且你根本不知道它什么时候错的。这种bug特别坑人因为编译不会报错运行也不一定崩溃就是结果不对。初学者最容易犯的一个错是用int去接收一个long long的计算结果。比如int result 1LL * 1000000 * 1000000;这段代码虽然能编译通过但result里存的是溢出的垃圾值。编译器可能给你一个警告也可能什么都不说。所以我后来养成了一个习惯**凡是做乘法或者累加先估算一下量级拿不准就直接用long long。**在绝大多数竞赛、刷题和日常开发场景里long long都不会错付出的代价只是多占4个字节完全值得。注意int的溢出是一个静默错误它不像数组越界那样会直接崩溃而是给你一个看起来还挺正常、实际上完全不对的数字。在C里有符号整型溢出是未定义行为也就是说编译器可以干任何事情不要指望它给你一个确定的回绕结果。3. 浮点型小数世界的精度陷阱3.1 float和double的区别浮点型是用来存小数的主要就是float和double。从名字上就能看出一点端倪double是双精度float是单精度。它们的区别在于有效数字的位数和能表示的指数范围。float一般4个字节有效数字大约7位double一般8个字节有效数字大约15到16位。什么叫有效数字就是一个数从左边第一个非零数字开始一共能信赖几位。我举个例子你立刻就懂了#include iostream #include iomanip using namespace std; int main() { float f 1234567.89; double d 1234567.89; cout fixed setprecision(6); cout float: f endl; cout double: d endl; return 0; }运行结果大概率会让你吃惊float打印出来的可能是1234567.875000而double是1234567.890000。你明明存的是.89float却给你存成了.875。这就是精度问题float只有大约7位有效数字后面那些位数它根本管不过来。3.2 为什么0.1加0.2不等于0.3这是几乎所有学编程的人都会遇到一次的经典问题。在C里写0.1 0.2结果不是0.3而是一个很长的、带一屁股9的数字。原因要从二进制说起。十进制里的有限小数转成二进制后很多都变成了无限循环小数。计算机内存有限存不下无限循环只能在某一位截断。这就导致浮点数在内存里其实是一个近似值而不是精确值。我说一个特别直观的类比你觉得1/3用十进制怎么写0.33333...永远写不完你只能写到某一位就停了。计算机存浮点数也是这个道理只不过它是在二进制里遇到了写不完的数。这个事实带来的直接教训是**永远不要直接用去判断两个浮点数是否相等。**你想判断a和b是否相等时应该判断它们的差的绝对值是否小于一个很小的数比如#include cmath double a 0.1 0.2; double b 0.3; if (fabs(a - b) 1e-9) { cout a 和 b 可以视为相等 endl; }这段代码里的1e-9就是你自己定的一个精度阈值意思是在这个误差范围内我们认为它们相等。实际工作中具体取多少取决于你的业务场景但思路是固定的。3.3 double在竞赛和项目里的使用心得如果你刷过算法题应该会有印象涉及小数的题基本条件反射就是double没人用float因为精度不够带来的误差会在中间计算中被放大。在项目里我踩过一个印象很深的坑算金额相关的数据时用了double结果累计到一定规模后对不上账。后来才意识到金额这种对精度要求极高的场景应该用整数去表示分或者使用专门处理十进制的库而不是直接用浮点数做加减乘除。这不是C特有的问题是所有用二进制浮点数表示小数的语言共同的特点。注意浮点数比较的坑初期你可能体会不深但越往后越容易踩。我的建议是从第一天开始就养成习惯——凡是浮点数一律用容差比较不要用省得以后回头改一大堆代码。4. 字符型与布尔型两个容易被小看的类型4.1 char不只是字符还是小号整型char类型用来存单个字符比如A、b、7。单引号里只能放一个字符这是写代码时最容易犯的低级错误用双引号包一个字符类型就变成了字符串完全不是一个东西。char在内存里占用1个字节。这里有一个关键认知char本质上是一个小号整型。比如说A在ASCII编码里对应数字65所以char变量既可以用字符赋值也可以用整数赋值。char c1 A; char c2 65; cout c1 c2 endl;这两行代码输出的都是大写字母A。理解这一点特别重要因为很多字符串处理的底层逻辑就是在操作这些数字。比如判断一个字符是不是数字可以直接看它的ASCII码在不在48到57这个区间或者用isdigit函数。字符和整数之间的转换还有一个很实用的场景把大写字母转成小写字母只需要加上32。因为大写A是65小写a是97差32char upper B; char lower upper 32; cout lower endl;这段代码会输出小写的b。当然实际项目里更推荐直接用tolower函数但理解这个原理对你看懂别人写的代码非常有帮助。4.2 bool的实际用法不止是真假bool类型只有两个值true和false。在内存里通常占1个字节。很多初学者觉得这个类型太简单没什么好学的但实际上它在代码逻辑里的作用非常大。你写条件判断、循环控制、函数返回值到处都会用到bool。我自己的经验是**如果一个函数是在回答一个是或否的问题返回值就应该用bool不要用int用0和1代替。**这样代码读起来清晰得多别人看到返回类型就能猜到函数意图。还有一种常见用法是作为标志位。比如你要处理一个数据只有在某种条件下才处理就可以用bool来记录状态bool found false; for (int i 0; i n; i) { if (arr[i] target) { found true; break; } } if (found) { cout 找到了 endl; }这里found就是一个标志位记录了循环过程中是否遇到过目标值。这种模式在写代码时遇到得非常多。另外提一句bool在参与整数运算时会隐式转换true变成了1false变成了0。比如true true的结果是2。这在某些情况下能简化代码但也容易让人迷惑。我的建议是除非你有十足的把握否则不要让bool参与算术运算这属于典型的看着很酷实际坑人的写法。5. 类型修饰符与隐式转换5.1 signed还是unsigned这是个问题整型前面还能加修饰符常见的是signed和unsigned。int默认是signed表示有符号能表示正数和负数。加上unsigned之后只能表示非负数但正数的范围大了一倍。原因很简单本来用来表示正负符号的那一个二进制位被释放出来用来表示数值了。1个字节的charsigned char范围是-128到127unsigned char范围是0到255。那是不是正数范围更大就更厉害不一定。使用unsigned最大的风险在于你一不小心就可能让计算结果变成很大的正数而不是预期的负数。这在做减法的时候特别容易中招unsigned int a 1; unsigned int b 2; cout a - b endl;直觉上结果是-1但实际上输出的是4294967295因为unsigned类型不允许出现负数它回绕到了类型能表示的最大值附近。这个坑在循环和数组下标场景里很经典。如果你的循环变量是unsigned int却又在循环体里做了减法逼近0的操作最后很容易陷入死循环。5.2 类型转换的规则到底是什么C里类型转换有两条路线隐式转换和强制转换。隐式转换是编译器自动做的你不需要写任何代码。它的核心规则就是**把小的类型往大的类型转把精度低的往精度高的转。**比如int和double做运算int会先被转成double所以3 / 2的结果是1而3.0 / 2的结果是1.5。这个差异在入门阶段极其容易踩坑。3 / 2是两个整数相除结果还是整数小数部分直接被丢弃。你如果写成float result 3 / 2;结果依然是1因为整除发生在前赋值在后。正确的做法是让至少一个操作数变成浮点数double result1 3.0 / 2; // 1.5 double result2 3 / 2.0; // 1.5 double result3 3 / 2; // 1注意这是一个很多新手都会犯的错我在带人入门时几乎每次都强调。写C的时候时刻问自己一句这个计算里的数据都是什么类型结果会是什么类型5.3 强制转换用哪种风格如果需要你自己手动控制转换C提供了几种方式。最原始的是C风格的(int)x这种写法在C里也能编译但不推荐。C风格的static_castint(x)更明确一看就知道这里发生了类型转换。实际写法double pi 3.14159; int intPi static_castint(pi);这里intPi的值是3小数部分被直接截断不是四舍五入。如果你需要四舍五入应该用round函数而不是依赖强制转换。注意强制转换在你明确知道自己在做什么的时候用它是对编译器的一种我比你更懂的表态。如果只是因为编译报错就随便加个强转把错误压下去这是在给自己埋雷。6. 如何选类型我的一套实用判断流程讲了这么多基础来点能直接落地的。我给自己建立了一套选类型的心法每次写代码前过一遍基本不会出大问题。先看这个数据是整数还是小数小数一律double除非有明确的性能或内存要求才考虑float。再看整数的量级能确定不超过32767可以用short但说实话日常开发里用short的场景很少不超过20亿用int再往上或者不确定直接用long long。然后看有没有负数参与有就用signed不写就是默认signed如果数据本身就不可能是负数比如数组下标、对象数量、内存大小用unsigned也没有问题但要时刻警惕减法运算的坑。char只用来存单个字符bool只用来表达逻辑真假不要试图用int的0和1代替bool代码可读性会差很多。什么时候需要稍微浪费一点我自己的原则是**优先保证代码行为和意图的正确性其次才考虑那点内存开销。**一个int和一个long long就差4个字节一件微不足道的小事但你因为溢出排查一个晚上的代价可就大了。下面这个表格是我平时给初学者做参考的你可以存一份类型常见字节典型范围适用场景int4约±21亿一般计数、循环变量、常规整数long long8范围极大大数计算、累计求和、乘法结果double8约15位有效数字小数、科学计算、统计数值char1ASCII字符字符处理、字符串底层操作bool1true/false标志位、条件判断结果这个表里的字节数是我在常见平台上的经验值不同环境下可能有差异但你写代码时按照这个思路去选基本是稳的。7. 新手必须提前知道的几个坑7.1 整型溢出和除以0int能表示的最大值大约是21亿超过这个数就会出现溢出。初学者最容易遇到的就是阶乘、幂运算、斐波那契这类会快速增大的计算场景。写这类算法题时我建议你从一开始就估算结果会到多大只要超过10亿就别犹豫直接long long。除以0的问题更基础但更致命。整数除以0会导致程序直接崩溃这是未定义行为。浮点数除以0倒不会崩溃结果是inf或nan但nan是个特别麻烦的东西任何涉及nan的比较都是false包括nan nan都是false。所以在处理浮点数时如果涉及除法最好先判断一下除数是否为0。7.2 输出格式的细节初学者用cout输出小数时默认不一定会显示足够多的小数位。比如你输出double类型可能出现1.5正常显示但1.0000000001被简化成1的情况。这是因为cout默认的精度只有6位有效数字。想要完整输出需要引入iomanip头文件并结合fixed和setprecision一起用#include iomanip double x 1.0 / 3.0; cout fixed setprecision(10) x endl;这段代码会输出0.3333333333保留10位小数。fixed表示使用定点表示法setprecision(10)表示小数点后保留10位。这个组合在写算法题和做报表输出时特别好用。7.3 不要盲目背诵亲手试一遍数据类型这部分内容只靠看是记不牢的。我见过太多人把规则背得滚瓜烂熟一写代码还是错。最简单有效的方法是开一个空工程把今天讲到的每一种类型都声明一遍用sizeof输出字节数用limits头文件里的max()和min()输出边界值自己观察现象。C标准库提供了一个工具头文件limits可以帮你查看类型的极限#include limits cout int max: numeric_limitsint::max() endl; cout long long max: numeric_limitslong long::max() endl;numeric_limits是一个模板类通过它你能拿到某个类型在当前编译环境下的最大值、最小值、精度等信息。这个比你在网上查到的任何资料都更贴合你自己的环境毕竟C在不同平台上确实会有差异信自己机器上的结果才最稳妥。我自己在实际带人入门时发现只要愿意亲手把这几个类型跑一遍的人后面遇到类型相关的问题直觉都会比纯看书的强。数据的本质是内存里的二进制类型是解释二进制的一副眼镜多试几次你就知道什么时候该换哪副眼镜了。下次写代码前不妨花一分钟想想我这个变量的取值范围到底是多少用哪个类型最合适想完再动手效率反而更高。