这一章在干嘛?

OOP 的精髓是”设计自己的数据类型”,但造新类型之前得先认识 C++ 给你的原材料——内置类型。这一章把整型(从 char 到 long long)、字符型、布尔型、浮点型一次讲透,再配上变量命名规则、初始化写法、五种算术运算符和”类型之间怎么自动转换、怎么强制转换”。这是全书的地基章,宁可慢也要吃透。

3.1 简单变量与命名规则

3.2 整型基础

3.3 无符号类型与溢出

3.4 整型字面值与进制

3.5 char 类型与字符

3.6 bool 类型

3.7 const 限定符

3.8 浮点数

3.9 算术运算符

3.10 类型转换

3.11 C++11 的 auto 声明

3.1 简单变量与命名规则

程序要存信息(比如股价、湿度、人数),就得追踪三件事:信息存在哪、存了什么值、存的是什么类型。变量的策略是:声明时用类型描述信息种类,用变量名代指那个值:

int braincount;
braincount = 5;

程序会找一块足够装下整数的内存,把 5 拷进去。你不需要知道它在内存里的具体地址——以后还能用 & 运算符取地址(第 4 章指针的内容)。

命名规则(编译器强制的):

  • 只能用字母、数字、下划线 _
  • 首字符不能是数字;
  • 大小写视为不同字符(poodlePoodle 是两个变量);
  • 不能用 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_MINchar 的最大 / 最小值
SCHAR_MAX / SCHAR_MINsigned char 的最大 / 最小值
UCHAR_MAXunsigned char 的最大值
SHRT_MAX / SHRT_MINshort 的最大 / 最小值
USHRT_MAXunsigned short 的最大值
INT_MAX / INT_MINint 的最大 / 最小值
UINT_MAXunsigned int 的最大值
LONG_MAX / LONG_MINlong 的最大 / 最小值
ULONG_MAXunsigned long 的最大值
LLONG_MAX / LLONG_MINlong long 的最大 / 最小值
ULLONG_MAXunsigned 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——像汽车里程表转到头又从头开始:

ch03_01

▲ 图 3.1 整数的典型溢出行为

C++ 只保证无符号类型的这种回绕行为;带符号类型越界属于未定义,虽然现有实现大多也是这样转的。

怎么选整型?三条经验:

  1. 没有特殊理由就用 int——它被设为计算机处理起来最”自然”(最快)的宽度;
  2. 变量永不为负(如文档字数)→ 用 unsigned,范围更大;
  3. 数值可能超过 16 位 int 的范围 → 就算你的系统 int 是 32 位也坚持用 long,换到 16 位 int 的系统程序照常工作:

ch03_02

▲ 图 3.2 为了可移植性,大整数请用 long

反过来,要省内存且 int 和 short 同为 16 位时也可考虑 short——不过只有存大数组时才真正省得出东西。只需一个字节就上 char。

3.4 整型字面值与进制

字面值(常量)就是你直接写出来的 212、1776。C++ 支持三种进制,看开头就能分辨:

开头进制例子十进制值
1~9十进制9393
0 后跟 1~7八进制04234
0x 或 0X十六进制0x4266

十六进制里 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)

显示成十六进制或八进制,得用操纵器 hexoct(还有默认的 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 十六进制
换行符\n100xA
水平制表符\t90x9
垂直制表符\v110xB
退格\b80x8
回车\r130xD
响铃\a70x7
反斜杠\920x5C
问号?630x3F
单引号390x27
双引号340x22

用法和普通字符一样——单独成字符常量就套单引号(‘\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)或 signed char(典型 −128127)。只存 ASCII 字符的话,char 本身就够。
  • 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(布尔逻辑的创立者)命名,只有两个值:truefalse。字面值可提升为 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 的幂缩放),你不必深究。

两种写法

  1. 标准小数点写法:12.34、8.0(小数部分为 0 但有小数点,照样按浮点存);
  2. E 记数法:3.45E6 表示 3.45 × 10⁶,E6 就是”10 的 6 次方”。E 或 e 都行,指数可正可负,但数字中间不能有空格(7.2 E6 非法):

ch03_03

▲ 图 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.333333

tub 在第七位就开始跑偏(正确值 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 = 0

2.34E+22 有 23 位整数,加 1 是在改第 23 位数字,而 float 只记得住前 6~7 位——这次加法石沉大海。需要精度就上 double 或 long double。

3.9 算术运算符

五种基本算术运算符,每个都是”操作数 运算符 操作数”构成表达式:

运算符作用示例结果
+4 + 2024
12 − 39
*28 * 4112
/17 / 35(整数除法,丢弃小数)
%取模(余数)19 % 61

取模运算符的注意事项:两个操作数都必须是整数,对浮点数用 % 会直接编译错误;若操作数有负数,结果符号满足 (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.498657

50.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 除法),靠操作数类型区分——这又是运算符重载

ch03_04

▲ 图 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 90

auks 先按 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 的结果并解释为什么。