这一章在干嘛?
OOP 的精髓是”设计自己的数据类型”,但造新类型之前得先认识 C++ 给你的原材料——内置类型。这一章把整型(从 char 到 long long)、字符型、布尔型、浮点型一次讲透,再配上变量命名规则、初始化写法、五种算术运算符和”类型之间怎么自动转换、怎么强制转换”。这是全书的地基章,宁可慢也要吃透。
3.1 简单变量与命名规则
程序要存信息(比如股价、湿度、人数),就得追踪三件事:信息存在哪、存了什么值、存的是什么类型。变量的策略是:声明时用类型描述信息种类,用变量名代指那个值:
int braincount;
braincount = 5;程序会找一块足够装下整数的内存,把 5 拷进去。你不需要知道它在内存里的具体地址——以后还能用 & 运算符取地址(第 4 章指针的内容)。
命名规则(编译器强制的):
- 只能用字母、数字、下划线
_; - 首字符不能是数字;
- 大小写视为不同字符(
poodle和Poodle是两个变量); - 不能用 C++ 关键词当名字;
- 以两个下划线开头、或下划线+大写字母开头的名字被保留给实现使用——用了不报错,但后果无法预料(未定义行为);
- 名字长度没有上限,所有字符都有效(ANSI C 只保证前 63 个字符)。
int poodle; // 合法
int Poodle; // 合法,且与 poodle 是两个变量
int my_stars3; // 合法
int _Mystars3; // 合法但被保留——下划线开头
int 4ever; // 非法——数字开头
int double; // 非法——double 是关键词
int begin; // 合法——begin 是 Pascal 的关键词,C++ 不管
int __fools; // 合法但被保留——两个下划线开头
int honky-tonk; // 非法——不允许连字符多个单词组成名字时,惯用 my_onions(下划线分隔)或 myEyeTooth(后词首字母大写)两种风格,选一种坚持用即可。有人还喜欢加类型前缀(如 nMyWeight 的 n 表示整数、sz 表示字符串、p 表示指针),团队有规范就跟着团队走。
3.2 整型基础
整数没有小数部分,如 2、98、−5286。整数有无穷多个,而内存有限,所以语言只能表示其中一部分。有的语言只给一种整型,C++ 却给了好几档——按占用内存从窄到宽排:char、short、int、long、long long(C++11 新增),每档还有带符号/无符号两个版本,共 10 种整型可选。这种”按需选型”的思想正是 OOP 定制数据类型的预演。
标准只保证最小宽度(单位是位 bit):
- short 至少 16 位;
- int 至少和 short 一样大;
- long 至少 32 位、至少和 int 一样大;
- long long(C++11)至少 64 位、至少和 long 一样大。
位与字节:内存的基本单位是位(bit),可以想象成一个电子开关,0 是关、1 是开。8 位能表达 2⁸ = 256 种组合(0255 或 −128127),每加一位翻一倍:16 位 65536 种,32 位约 42.9 亿种,64 位约 1844 亿亿种——unsigned long 装不下地球人口或银河系恒星数,long long 可以。**字节(byte)**通常指 8 位内存单元(有些场合叫 octet),但 C++ 对 byte 的定义是”至少能装下基本字符集的相邻位数”,所以个别实现可能是 16 位甚至 32 位的字节。
默认都是带符号的:16 位 int 的范围大约是 −32768 ~ +32767,正负各占一半。
两个查自家系统底细的工具:sizeof 运算符(返回类型或变量的字节数)和 climits 头文件(定义了各类型的极限符号常量)。程序清单 3.1 limits.cpp:
// limits.cpp -- some integer limits
#include <iostream>
#include <climits> // use limits.h for older systems
int main()
{
using namespace std;
int n_int = INT_MAX; // initialize n_int to max int value
short n_short = SHRT_MAX; // symbols defined in climits file
long n_long = LONG_MAX;
long long n_llong = LLONG_MAX;
// sizeof operator yields size of type or of variable
cout << "int is " << sizeof (int) << " bytes." << endl;
cout << "short is " << sizeof n_short << " bytes." << endl;
cout << "long is " << sizeof n_long << " bytes." << endl;
cout << "long long is " << sizeof n_llong << " bytes." << endl;
cout << endl;
cout << "Maximum values:" << endl;
cout << "int: " << n_int << endl;
cout << "short: " << n_short << endl;
cout << "long: " << n_long << endl;
cout << "long long: " << n_llong << endl << endl;
cout << "Minimum int value = " << INT_MIN << endl;
cout << "Bits per byte = " << CHAR_BIT << endl;
return 0;
}在 64 位 Windows 7 上的输出:
int is 4 bytes.
short is 2 bytes.
long is 4 bytes.
long long is 8 bytes.
Maximum values:
int: 2147483647
short: 32767
long: 2147483647
long long: 9223372036854775807
Minimum int value = -2147483648
Bits per byte = 8注意 sizeof 的两种用法:对类型名要加括号 sizeof (int);对变量名括号可省 sizeof n_short。climits 里定义的符号常量一览:
| 符号常量 | 含义 |
|---|---|
| CHAR_BIT | 一个 char 的位数 |
| CHAR_MAX / CHAR_MIN | char 的最大 / 最小值 |
| SCHAR_MAX / SCHAR_MIN | signed char 的最大 / 最小值 |
| UCHAR_MAX | unsigned char 的最大值 |
| SHRT_MAX / SHRT_MIN | short 的最大 / 最小值 |
| USHRT_MAX | unsigned short 的最大值 |
| INT_MAX / INT_MIN | int 的最大 / 最小值 |
| UINT_MAX | unsigned int 的最大值 |
| LONG_MAX / LONG_MIN | long 的最大 / 最小值 |
| ULONG_MAX | unsigned long 的最大值 |
| LLONG_MAX / LLONG_MIN | long long 的最大 / 最小值 |
| ULLONG_MAX | unsigned long long 的最大值 |
climits 内部其实是 #define INT_MAX 32767 这样的预处理器宏——编译前把源码里的 INT_MAX 全部替换成 32767,像编辑器的全局查找替换。C++ 有更好的办法(const,见 3.7 节),但许多 C/C++ 兼容头文件仍在用 define。
初始化:把声明和赋值合并成一步。可以用常量、已定义的变量、甚至表达式来初始化:
int uncles = 5; // 用常量
int aunts = uncles; // 用变量
int chairs = aunts + uncles + 4; // 用表达式,得 14把 uncles 的声明挪到后面,后两条就失效了——初始化时右边的值必须已经已知。C++ 还有两种自家语法:
int owls = 101; // 传统 C 式
int wrens(432); // C++ 式,像函数调用C++11 又推广了大括号初始化(可用于一切类型,将来会成为统一写法):
int emus{7}; // 不带 =
int rheas = {12}; // 带 =
int rocs = {}; // 空大括号,初始化为 0
int psychics{}; // 同上,初始化为 0大括号写法还多一层保护:禁止”收窄转换”(narrowing),详见 3.10 节。
3.3 无符号类型与溢出
四种整型各有无符号版本——不能存负数,但正数范围翻倍:short 是 −32768~+32767,unsigned short 就是 0~65535。只用于永不为负的量(人口、计数)时用它。写法上 unsigned 单独用就等于 unsigned int:
unsigned short change; // unsigned short 类型
unsigned int rovert; // unsigned int 类型
unsigned quarterback; // 同样是 unsigned int
unsigned long gone; // unsigned long 类型
unsigned long long lang_lang; // unsigned long long 类型越过极限会发生什么?程序清单 3.2 exceed.cpp 做了个实验——把 short 和 unsigned short 都设为最大值 32767,然后加一、减一:
// exceed.cpp -- exceeding some integer limits
#include <iostream>
#define ZERO 0 // makes ZERO symbol for 0 value
#include <climits> // defines INT_MAX as largest int value
int main()
{
using namespace std;
short sam = SHRT_MAX; // initialize a variable to max value
unsigned short sue = sam; // okay if variable sam already defined
cout << "Sam has " << sam << " dollars and Sue has " << sue;
cout << " dollars deposited." << endl
<< "Add $1 to each account." << endl << "Now ";
sam = sam + 1;
sue = sue + 1;
cout << "Sam has " << sam << " dollars and Sue has " << sue;
cout << " dollars deposited.\nPoor Sam!" << endl;
sam = ZERO;
sue = ZERO;
cout << "Sam has " << sam << " dollars and Sue has " << sue;
cout << " dollars deposited." << endl;
cout << "Take $1 from each account." << endl << "Now ";
sam = sam - 1;
sue = sue - 1;
cout << "Sam has " << sam << " dollars and Sue has " << sue;
cout << " dollars deposited." << endl << "Lucky Sue!" << endl;
return 0;
}输出:
Sam has 32767 dollars and Sue has 32767 dollars deposited.
Add $1 to each account.
Now Sam has -32768 dollars and Sue has 32768 dollars deposited.
Poor Sam!
Sam has 0 dollars and Sue has 0 dollars deposited.
Take $1 from each account.
Now Sam has -1 dollars and Sue has 65535 dollars deposited.
Lucky Sue!Sam(short)越过上限后从 32767 跳到 −32768;Sue(unsigned short)越过下限后从 0 跳到 65535——像汽车里程表转到头又从头开始:

▲ 图 3.1 整数的典型溢出行为
C++ 只保证无符号类型的这种回绕行为;带符号类型越界属于未定义,虽然现有实现大多也是这样转的。
怎么选整型?三条经验:
- 没有特殊理由就用 int——它被设为计算机处理起来最”自然”(最快)的宽度;
- 变量永不为负(如文档字数)→ 用 unsigned,范围更大;
- 数值可能超过 16 位 int 的范围 → 就算你的系统 int 是 32 位也坚持用 long,换到 16 位 int 的系统程序照常工作:

▲ 图 3.2 为了可移植性,大整数请用 long
反过来,要省内存且 int 和 short 同为 16 位时也可考虑 short——不过只有存大数组时才真正省得出东西。只需一个字节就上 char。
3.4 整型字面值与进制
字面值(常量)就是你直接写出来的 212、1776。C++ 支持三种进制,看开头就能分辨:
| 开头 | 进制 | 例子 | 十进制值 |
|---|---|---|---|
| 1~9 | 十进制 | 93 | 93 |
| 0 后跟 1~7 | 八进制 | 042 | 34 |
| 0x 或 0X | 十六进制 | 0x42 | 66 |
十六进制里 af(不分大小写)表示 1015:0xF 是 15,0xA5 是 165。程序清单 3.3 hexoct1.cpp:
// hexoct1.cpp -- shows hex and octal literals
#include <iostream>
int main()
{
using namespace std;
int chest = 42; // decimal integer literal
int waist = 0x42; // hexadecimal integer literal
int inseam = 042; // octal integer literal
cout << "Monsieur cuts a striking figure!\n";
cout << "chest = " << chest << " (42 in decimal)\n";
cout << "waist = " << waist << " (0x42 in hex)\n";
cout << "inseam = " << inseam << " (042 in octal)\n";
return 0;
}输出(cout 默认总是按十进制显示,不管你写的是哪个进制):
Monsieur cuts a striking figure!
chest = 42 (42 in decimal)
waist = 66 (0x42 in hex)
inseam = 34 (042 in octal)要显示成十六进制或八进制,得用操纵器 hex、oct(还有默认的 dec),改一次一直生效直到再改:
// hexoct2.cpp -- display values in hex and octal
#include <iostream>
using namespace std;
int main()
{
using namespace std;
int chest = 42;
int waist = 42;
int inseam = 42;
cout << "Monsieur cuts a striking figure!" << endl;
cout << "chest = " << chest << " (decimal for 42)" << endl;
cout << hex; // manipulator for changing number base
cout << "waist = " << waist << " (hexadecimal for 42)" << endl;
cout << oct; // manipulator for changing number base
cout << "inseam = " << inseam << " (octal for 42)" << endl;
return 0;
}输出:
Monsieur cuts a striking figure!
chest = 42 (decimal for 42)
waist = 2a (hexadecimal for 42)
inseam = 52 (octal for 42)cout << hex; 自己什么都不显示——它只是给 cout 下达”以后按十六进制显示”的指令。另外因为 hex 属于 std 且程序用了该命名空间,就不能再用 hex 当变量名了;若改用 std::cout 风格,hex 仍可作变量名。
编译器如何判定常量的类型:没有后缀时,十进制常量存为能装下它的最小类型(int → long → long long);十六进制/八进制常量的候选清单多了无符号类型(int → unsigned int → long → unsigned long → long long → unsigned long long),因为十六进制常用来表达内存地址,地址天然是无符号的。也可以用后缀指定:l/L 表示 long,u/U 表示 unsigned int,ul(顺序大小写随意)表示 unsigned long,C++11 加了 ll/LL(long long)和 ull/ULL。建议用大写 L——小写 l 太像数字 1。
3.5 char 类型与字符
char 是为存字符设计的最小的整型。计算机存字母的办法很直接:给每个字符编个号。美国最常用的是 ASCII 码:65 是 ‘A’,77 是 ‘M’。char 保证能装下目标系统的全部基本字符,实践中常是 8 位(可表示少于 128 种字符),所以它也能兼当比 short 还小的整型用。
程序清单 3.5 chartype.cpp:
// chartype.cpp -- the char type
#include <iostream>
int main( )
{
using namespace std;
char ch; // declare a char variable
cout << "Enter a character: " << endl;
cin >> ch;
cout << "Hola! ";
cout << "Thank you for the " << ch << " character." << endl;
return 0;
}运行示例:
Enter a character:
M
Hola! Thank you for the M character.有趣的地方在于:你敲的是 M 不是数字 77,屏幕显示的也是 M——但内存里存的确实是 77。翻译工作由 cin 和 cout 替你做了:输入时把键击 M 转成 77,输出时把 77 转回 M,依据就是变量的类型。同样的 77 放进 int 变量,cout 就显示 77。程序清单 3.6 morechar.cpp 把这事挑明:
// morechar.cpp -- the char type and int type contrasted
#include <iostream>
int main()
{
using namespace std;
char ch = 'M'; // assign ASCII code for M to ch
int i = ch; // store same code in an int
cout << "The ASCII code for " << ch << " is " << i << endl;
cout << "Add one to the character code:" << endl;
ch = ch + 1; // change character code in ch
i = ch; // save new character code in i
cout << "The ASCII code for " << ch << " is " << i << endl;
// using the cout.put() member function to display a char
cout << "Displaying char ch using cout.put(ch): ";
cout.put(ch);
// using cout.put() to display a char constant
cout.put('!');
cout << endl << "Done" << endl;
return 0;
}输出:
The ASCII code for M is 77
Add one to the character code:
The ASCII code for N is 78
Displaying char ch using cout.put(ch): N!
Done三个要点:①字符字面值用单引号(‘M’),双引号是给字符串的(“M”),cout 都能处理但两者本质不同(第 4 章细讲);②char 本质是整数,ch = ch + 1 完全合法,M 就变成了 N——把字符当整数处理省去了繁琐的转换函数;③键盘输入的数字也是字符:cin >> ch 时敲 5 存进去的是字符码 53;cin >> n(n 是 int)才是转换成数值 5。
成员函数 cout.put():名字里的句点叫成员运算符——put() 是 ostream 类的成员函数,只能通过类对象(如 cout)调用。为什么需要它?历史原因:C++ 2.0 之前,字符常量按 int 存储,cout << 'M' 会显示 77 而不是 M,cout.put('M') 才能正确显示字符。2.0 之后单字符常量改存为 char,<< 就够用了,但 put() 作为传统保留了下来。
转义序列:有些字符没法直接从键盘敲进程序(比如换行),或者已被语言征用(比如双引号是字符串的分隔符),就用转义序列表示:
| 字符名称 | C++ 代码 | ASCII 十进制 | ASCII 十六进制 |
|---|---|---|---|
| 换行符 | \n | 10 | 0xA |
| 水平制表符 | \t | 9 | 0x9 |
| 垂直制表符 | \v | 11 | 0xB |
| 退格 | \b | 8 | 0x8 |
| 回车 | \r | 13 | 0xD |
| 响铃 | \a | 7 | 0x7 |
| 反斜杠 | \ | 92 | 0x5C |
| 问号 | ? | 63 | 0x3F |
| 单引号 | ’ | 39 | 0x27 |
| 双引号 | ” | 34 | 0x22 |
用法和普通字符一样——单独成字符常量就套单引号(‘\a’),放在字符串里就直接写:
char alarm = '\a';
cout << alarm << "Don't do that again!\a\n";
cout << "Ben \"Buggsie\" Hacker\nwas here!\n";最后一行输出:
Ben "Buggsie" Hacker
was here!换行的三种等价写法:
cout << endl; // endl 操纵器
cout << '\n'; // 字符常量
cout << "\n"; // 字符串显示数字时 endl 好打,显示字符串时把 \n 嵌在字符串末尾更省事。还可以用八进制或十六进制码表示字符:Ctrl+Z 的 ASCII 码是 26,写成 '\032' 或 '\x1a'。有选择时优先用符号形式(如 \b)而非数字形式——数字写法绑定特定编码,符号写法在任何编码下都通用、也更可读。程序清单 3.7 bondini.cpp 演示了响铃和退格:
// bondini.cpp -- using escape sequences
#include <iostream>
int main()
{
using namespace std;
cout << "\aOperation \"HyperHype\" is now activated!\n";
cout << "Enter your agent code:____\b\b\b\b\b\b\b\b";
long code;
cin >> code;
cout << "\aYou entered " << code << "...\n";
cout << "\aCode verified! Proceed with Plan Z3!\n";
return 0;
}运行效果:先响铃并显示标题;打印 4 个下划线后用 8 个退格把光标退回到行内,等你输入密码(42007007),随后再响铃两次确认。注意现代系统不一定支持全部转义序列——有的对 \a 保持沉默,有的把 \b 显示成小方块。
通用字符名与宽字符:源码字符集之外的国际字符可以用 \u 后跟 8 个十六进制位(如 \u00F6 表示 ö)或 \U 后跟 16 个十六进制位来引用,它们对应 ISO 10646 / Unicode 的码点。Unicode 是给全世界 10 万多个字符(含希腊、西里尔、阿拉伯、中文日文表意文字等)统一编号的标准,ASCII 是它的子集。此外:
- signed char / unsigned char:char 默认不指定带不带符号,由实现自行决定。要把 char 当小整数用且范围很关键时,明确写出
unsigned char(典型 0255)或127)。只存 ASCII 字符的话,char 本身就够。signed char(典型 −128 - wchar_t:字符集装不进一个字节(如日文汉字)时的”宽字符”类型,宽度与符号性随实现而定。配套用
wcin/wcout输入输出,字面值前加 L:wchar_t bob = L'P';。 - char16_t / char32_t(C++11):无符号、定宽 16/32 位的字符类型,字面值前缀分别为
u'C'和U'R',与\u、\U通用字符名天然匹配,弥补了 wchar_t 宽度不确定的缺陷。
3.6 bool 类型
bool 以英国数学家 George Boole(布尔逻辑的创立者)命名,只有两个值:true 和 false。字面值可提升为 int:true 是 1,false 是 0;反过来,任何数值或指针值也能隐式转成 bool——非零即 true,零即 false:
bool is_ready = true; // true
int ans = true; // ans 得 1
int promise = false; // promise 得 0
bool start = -100; // start 得 true
bool stop = 0; // stop 得 false在第 6 章学了 if 语句之后,bool 会成为出镜率极高的类型。
3.7 const 限定符
给常量起个有意义的名字,比满篇光秃秃的 12 好读得多。C++ 的做法是用 const 修饰声明:
const int Months = 12; // Months is symbolic constant for 12初始化之后就不许再改——试图给 Months 赋新值,g++ 会报”赋值给只读变量”的错误。const 必须在声明时初始化,下面的写法不行:
const int toes; // 此时 toes 的值未定义
toes = 10; // 太晚了!常用惯例:首字母大写(Months)、全大写(跟随 define 传统)或 k 开头(kmonths),选定一种贯彻始终。
为什么比 define 好?①能指明类型;②能用 C++ 的作用域规则把名字限制在特定函数或文件里;③能用于数组、结构等更复杂的类型。从 C 转来的朋友:想用 define 定义符号常量时,请改用 const。ANSI C 也借用了 const,但 C++ 版本作用域规则不同,而且 C++ 里可以用 const 值来指定数组长度(C 不行)。
3.8 浮点数
整数装不下 3.14159,也装不下人体细菌总数(超过 10¹⁴)。浮点数的存储思想是”值 × 缩放因子”:34.1245 可以表示为 0.341245(值)× 100(缩放因子),34124.5 则是同一个 0.341245 × 100000——缩放因子负责挪动小数点,这就是”浮点”二字的由来。C++ 内部用二进制做同样的事(按 2 的幂缩放),你不必深究。
两种写法:
- 标准小数点写法:12.34、8.0(小数部分为 0 但有小数点,照样按浮点存);
- E 记数法:3.45E6 表示 3.45 × 10⁶,E6 就是”10 的 6 次方”。E 或 e 都行,指数可正可负,但数字中间不能有空格(7.2 E6 非法):

▲ 图 3.3 E 记数法
2.52e+8 // E 或 e 均可,+ 可省略
8.33E-4 // 指数可为负
7E5 // 等价于 7.0E+05
-18.32e13 // 前面可有正负号
5.98E24 // 地球质量,单位千克
9.11e-31 // 电子质量,单位千克负指数是除以 10 的幂:8.33E-4 即 0.000833。注意 −8.33E4 是 −83300——数前的符号作用于数值本身,指数上的符号作用于缩放。口诀:E+n 小数点右移 n 位,E−n 左移 n 位。
三种浮点类型:float、double、long double,按有效数字位数(significand,数字中有意义的位数——14179 有 5 位,“约 14000” 只有 2 位)和指数范围来规定:float 至少 32 位,double 至少 48 位且不小于 float,long double 不小于 double;典型实现是 float 32 位、double 64 位、long double 80~128 位。指数范围至少 −37 到 +37,具体极限查 cfloat(即 C 的 float.h)头文件。
float 只有约 6~7 位有效数字,double 约 15 位,差距有多要命?程序清单 3.8 floatnum.cpp(其中 cout.setf(...) 是强迫输出保持定点表示的格式控制,细节在第 17 章):
// floatnum.cpp -- floating-point types
#include <iostream>
int main()
{
using namespace std;
cout.setf(ios_base::fixed, ios_base::floatfield); // fixed-point
float tub = 10.0 / 3.0; // good to about 6 places
double mint = 10.0 / 3.0; // good to about 15 places
const float million = 1.0e6;
cout << "tub = " << tub;
cout << ", a million tubs = " << million * tub;
cout << ",\nand ten million tubs = ";
cout << 10 * million * tub << endl;
cout << "mint = " << mint << " and a million mints = ";
cout << million * mint << endl;
return 0;
}输出:
tub = 3.333333, a million tubs = 3333333.250000,
and ten million tubs = 33333332.000000
mint = 3.333333 and a million mints = 3333333.333333tub 在第七位就开始跑偏(正确值 3333333.333333…),mint 稳稳地准到小数点后六位——float 和 double 的精度差异一目了然。
浮点常量的类型:默认是 double!要 float 加 f/F 后缀,要 long double 加 l/L 后缀(同样推荐大写 L):
1.234f // float 常量
2.45E20F // float 常量
2.345324E28 // double 常量
2.2L // long double 常量浮点的代价:运算通常比整数稍慢,而且精度有限。清单 3.9 的极端例子——一个大数加 1 再减回去,结果不是 1 而是 0:
// fltadd.cpp -- precision problems with float
#include <iostream>
int main()
{
using namespace std;
float a = 2.34E+22f;
float b = a + 1.0f;
cout << "a = " << a << endl;
cout << "b - a = " << b - a << endl;
return 0;
}输出:
a = 2.34e+022
b - a = 02.34E+22 有 23 位整数,加 1 是在改第 23 位数字,而 float 只记得住前 6~7 位——这次加法石沉大海。需要精度就上 double 或 long double。
3.9 算术运算符
五种基本算术运算符,每个都是”操作数 运算符 操作数”构成表达式:
| 运算符 | 作用 | 示例 | 结果 |
|---|---|---|---|
| + | 加 | 4 + 20 | 24 |
| − | 减 | 12 − 3 | 9 |
| * | 乘 | 28 * 4 | 112 |
| / | 除 | 17 / 3 | 5(整数除法,丢弃小数) |
| % | 取模(余数) | 19 % 6 | 1 |
取模运算符的注意事项:两个操作数都必须是整数,对浮点数用 % 会直接编译错误;若操作数有负数,结果符号满足 (a/b)*b + a%b 等于 a。
程序清单 3.10 arith.cpp:
// arith.cpp -- some C++ arithmetic
#include <iostream>
int main()
{
using namespace std;
float hats, heads;
cout.setf(ios_base::fixed, ios_base::floatfield); // fixed-point
cout << "Enter a number: ";
cin >> hats;
cout << "Enter another number: ";
cin >> heads;
cout << "hats = " << hats << "; heads = " << heads << endl;
cout << "hats + heads = " << hats + heads << endl;
cout << "hats - heads = " << hats - heads << endl;
cout << "hats * heads = " << hats * heads << endl;
cout << "hats / heads = " << hats / heads << endl;
return 0;
}运行示例:
Enter a number: 50.25
Enter another number: 11.17
hats = 50.250000; heads = 11.170000
hats + heads = 61.419998
hats - heads = 39.080002
hats * heads = 561.292480
hats / heads = 4.49865750.25 + 11.17 应该是 61.42,程序却显示 61.419998——这不是算错了,是 float 只有 6 位有效数字的锅(61.419998 四舍五入到 6 位就是 61.4200)。要精度,用 double。
优先级与结合性:3 + 4 * 5 是 23 不是 35——乘、除、取模优先于加、减;同优先级看结合性:120 / 4 * 5 中 / 和 * 同级且左到右结合,先算 120/4=30 再乘 5 得 150。拿不准就加括号。注意:优先级和结合性只在两个运算符争夺同一个操作数时起作用;20 * 5 + 24 * 6 里两个乘号没有公共操作数,谁先算由实现自便(结果一样)。
除法的类型敏感:两个整数相除 → 整数除法(小数部分直接扔掉);有浮点数参与 → 浮点除法。程序清单 3.11 divide.cpp:
// divide.cpp -- integer and floating-point division
#include <iostream>
int main()
{
using namespace std;
cout.setf(ios_base::fixed, ios_base::floatfield);
cout << "Integer division: 9/5 = " << 9 / 5 << endl;
cout << "Floating-point division: 9.0/5.0 = ";
cout << 9.0 / 5.0 << endl;
cout << "Mixed division: 9.0/5 = " << 9.0 / 5 << endl;
cout << "double constants: 1e7/9.0 = ";
cout << 1.e7 / 9.0 << endl;
cout << "float constants: 1e7f/9.0f = ";
cout << 1.e7f / 9.0f << endl;
return 0;
}输出:
Integer division: 9/5 = 1
Floating-point division: 9.0/5.0 = 1.800000
Mixed division: 9.0/5 = 1.800000
double constants: 1e7/9.0 = 1111111.111111
float constants: 1e7f/9.0f = 1111111.125000混合类型运算时,C++ 自动把小的类型转成大的再算(所以 9.0/5 也得 1.8);最后两行还显示了 double 和 float 的精度差。同一个 / 能干三种活(int 除法、float 除法、double 除法),靠操作数类型区分——这又是运算符重载:

▲ 图 3.4 不同的除法
取模的实战:整数除法 + 取模是”按整单位拆分”问题的黄金搭档。程序清单 3.12 modulus.cpp 把磅换算成英石(1 英石 = 14 磅):
// modulus.cpp -- uses % operator to convert lbs to stone
#include <iostream>
int main()
{
using namespace std;
const int Lbs_per_stn = 14;
int lbs;
cout << "Enter your weight in pounds: ";
cin >> lbs;
int stone = lbs / Lbs_per_stn; // whole stone
int pounds = lbs % Lbs_per_stn; // remainder in pounds
cout << lbs << " pounds are " << stone
<< " stone, " << pounds << " pound(s).\n";
return 0;
}运行示例:
Enter your weight in pounds: 181
181 pounds are 12 stone, 13 pound(s).181 / 14 = 12(整除),181 % 14 = 13(余数),一个除法一个取模,拆分完成。
3.10 类型转换
11 种整型加 3 种浮点型混着用,C++ 在三种场合自动转换:赋值时、表达式里混合类型时、传参给函数时。不理解规则,有些结果会让你一脸问号。
赋值/初始化时的转换:往”更大范围”的类型转通常安全(short → long 只是多了几个字节躺平);往”更小”或异构的类型转就有坑:
| 转换类型 | 潜在问题 |
|---|---|
| 大浮点 → 小浮点(如 double → float) | 精度(有效数字)损失;值可能超出目标类型范围,结果未定义 |
| 浮点 → 整数 | 丢掉小数部分;原值可能超出目标范围,结果未定义 |
| 大整数 → 小整数(如 long → short) | 原值可能超范围;通常只复制低位字节 |
零赋给 bool 变 false,非零赋给 bool 变 true。程序清单 3.13 init.cpp 演示初始化时的转换:
// init.cpp -- type changes on initialization
#include <iostream>
int main()
{
using namespace std;
cout.setf(ios_base::fixed, ios_base::floatfield);
float tree = 3; // int converted to float
int guess(3.9832); // double converted to int
int debt = 7.2E12; // result not defined in C++
cout << "tree = " << tree << endl;
cout << "guess = " << guess << endl;
cout << "debt = " << debt << endl;
return 0;
}某系统输出:
tree = 3.000000
guess = 3
debt = 1634811904三个教训:3.9832 转 int 是截断(truncation,直接扔掉小数)不是四舍五入;7.2E12 远超 int 的范围,结果未定义——这个系统给出 1634811904,另一个系统给出 2147483647,编译器换一家答案就变。C++11 的大括号初始化正好堵这个漏洞:它禁止”收窄”(narrowing,即目标类型可能装不下):
const int code = 66;
int x = 66;
char c1 {31325}; // 收窄,不允许
char c2 = {66}; // 允许,char 装得下 66
char c3 {code}; // 同上,允许
char c4 = {x}; // 不允许——x 是变量,编译器不看它的运行值
x = 31325;
char c5 = x; // 传统写法放行c4 被拒是因为编译器只认”变量可能取任何值”,不替你追踪 x 的来龙去脉。
表达式里的转换:先做整型提升(integral promotion)——bool、char、unsigned char、signed char、short 在计算时统统升格为 int(true→1,false→0)。因为 int 通常是计算机算得最快的”自然宽度”,绕这一下反而更快。然后混合运算时小的向大的看齐,C++11 的检查清单按序执行:有 long double 就全转 long double;否则有 double 转 double;否则有 float 转 float;否则整数做整型提升后,同号比大小转成高等级的;异号时 unsigned 等级高则 signed 就范,依此类推。整数等级从高到低:long long > long > int > short > signed char;unsigned 类型与对应 signed 同级;bool 最低。例:short chickens = 20; short ducks = 35; short fowl = chickens + ducks; 实际是先把两个 short 都升成 int 相加,再把结果转回 short 存进 fowl。
强制类型转换(type cast):自动转换不听你指挥时,可以手动强制。两种传统写法:
(long) thorn // C 风格
long (thorn) // C++ 风格,像函数调用强制转换不改变原变量,只是造出一个目标类型的新值。C++11 还有更严格的 static_cast<typeName>(value) 形式。程序清单 3.14 typecast.cpp:
// typecast.cpp -- forcing type changes
#include <iostream>
int main()
{
using namespace std;
int auks, bats, coots;
// the following statement adds the values as double,
// then converts the result to int
auks = 19.99 + 11.99;
// these statements add values as int
bats = (int) 19.99 + (int) 11.99; // old C syntax
coots = int (19.99) + int (11.99); // new C++ syntax
cout << "auks = " << auks << ", bats = " << bats;
cout << ", coots = " << coots << endl;
char ch = 'Z';
cout << "The code for " << ch << " is "; // print as char
cout << int(ch) << endl; // print as int
cout << "Yes, the code is ";
cout << static_cast<int>(ch) << endl; // using static_cast
return 0;
}输出:
auks = 31, bats = 30, coots = 30
The code for Z is 90
Yes, the code is 90auks 先按 double 相加得 31.98,赋给 int 时截断成 31;bats、coots 先各自截断成 19 和 11 再加,得 30——先加后转与先转后加结果不同。最常见的转换用途在第 4~6 行:ch 是 char,cout 显示 Z;强转成 int 后 cout 切换到整数模式,显示 ASCII 码 90。“让数据换个形态去满足另一种期望”,正是类型转换的头号使命。
3.11 C++11 的 auto 声明
C++11 给古董关键词 auto 注入了新生命:初始化声明中用 auto 代替类型名,编译器自动推断变量类型与初始值一致:
auto n = 100; // n 是 int
auto x = 1.5; // x 是 double
auto y = 1.3e12L; // y 是 long double简单场合用它反而容易踩坑:想让 x、y、z 都是 double,auto z = 0; 会把 z 定成 int——0 是 int 字面值。auto 的真正用武之地是 STL 那种长得吓人的类型:
std::vector<double> scores;
std::vector<double>::iterator pv = scores.begin(); // C++98 写法
auto pv = scores.begin(); // C++11 写法现在先混个脸熟,后面遇到复杂类型时它会大显身手。
常见坑:整数溢出像里程表
short 越过 32767 会直接跳到 −32768,unsigned short 从 0 减 1 会跳到 65535——编译器不会报错,错误会潜伏到程序深处才爆发。计数器、累加器要选够宽的类型;C++ 只保证无符号类型的回绕行为,带符号类型溢出属未定义行为,别依赖。
常见坑:整数除法与浮点精度
17 / 3得 5 不是 5.67——两个整数相除会丢弃小数部分;想得到小数,至少让一个操作数带小数点(17.0 / 3)。另一个方向:float 只有约 6 位有效数字,50.25 + 11.17会显示 61.419998;对精度有要求一律用 double,别忘了浮点常量默认就是 double 类型。
通关标准:
你能说出 10 种整型的宽度排序和 signed/unsigned 区别,并用 sizeof 和 climits 查出自家系统的真实数值;能解释 char 存的其实是整数编码、bool 的零/非零规则;能默写 const 常量的正确写法(声明时必须初始化);能预测 17/3、17%3、9.0/5 的结果并解释为什么。
自测:
cout << 0x42;和cout << 42;分别输出什么?为什么?都输出 66?不——第一条输出 66,第二条输出 42。0x42 的十进制值是 66,所以
cout << 0x42;显示 66,而cout << 42;显示 42。cout 默认按十进制显示,字面值写哪个进制只影响源代码里的写法,存储的都是同一个二进制值;想让 cout 显示十六进制得用 hex 操纵器。
自测:'M'、"M"、77 三者在内存里有什么区别?
‘M’ 是 char 字面值(早期编译器按 int 存 77,C++ 2.0 起按 char 存);“M” 是字符串,由字符 ‘M’ 和结尾的空字符组成;77 是 int 常量。char 变量 ch = ‘M’ 时内存里存的就是数值 77,用 cout << ch 显示 M,用 cout << int(ch) 显示 77——类型决定显示方式。
自测:3.9832 赋给 int 变量得多少?7.2E12 呢?这两个结果各暴露了什么规则?
前者得 3——浮点转整数是截断(丢弃小数部分)而非四舍五入;后者的值远超 int 范围,结果未定义,不同编译器给出不同答案(如 1634811904 或 2147483647)——超范围的大整数转小类型不可依赖。用 C++11 大括号初始化(如
int debt{7.2E12};)会直接禁止这种收窄转换,编译不过,把错误拦在源头。
自测:181 磅怎么用两个运算符拆成"12 英石 13 磅"?
181 / 14 得 12(整数除法取商),181 % 14 得 13(取模取余数)。整数除法和取模是”按整单位拆分”类问题的标准搭档:前者给整单位数,后者给余数。
自测:为什么
auto z = 0;会让本想用 double 的 z 变成 int?怎么避免?auto 让变量类型与初始值类型一致,而 0 是 int 字面值,所以 z 被推断为 int。想用 double 就写
auto z = 0.0;,或者干脆显式声明double z = 0;(0 会自动转换为 0.0)。auto 的强项是 STL 迭代器这类冗长类型,简单数值场合显式写类型更保险。