第1章 变量和基本类型
第1章 变量和基本类型
1.1 基本内置类型
C++定义了一套包括算术类型和空类型在内的基本数据类型,其中算术类型包含了字符型、整数型、布尔值和浮点数,空类型不对应具体的值,仅用于一些特殊的场合(如函数不返回任何值时使用空类型作为返回类型)。
1.1.1 算术类型
算术类型分为两类——整型和浮点型。
算术类型的数据大小(该类型的数据所占的比特数或字节数)在不同的机器上有所差别,C++标准规定了数据大小的最小值,允许不同的编译器赋予这些类型更大的尺寸。下表是GCC编译器下数据类型所占字节数和取值范围:
| 数据类型 | 32位(Bytes) | 64位(Bytes) | 取值范围 |
|---|---|---|---|
| bool | 1 | 1 | true,false |
| char | 1 | 1 | -128 ~ 127 |
| unsigned char | 1 | 1 | 0 ~ 255 |
| wchar_t(宽字符) | 2 | 2 | 0 ~ 65535 |
| char16_t(Unicode字符) | 2 | 2 | 0 ~ 65535 |
| char32_t(Unicode字符) | 4 | 4 | 0 ~ 4294967295 |
| short int | 2 | 2 | -32768 ~ 32767 |
| unsigned short int | 2 | 2 | 0 ~ 65535 |
| int | 4 | 4 | -2147483648 ~ 2147483647 |
| unsigned int | 4 | 4 | 0 ~ 4294967295 |
| long int | 4 |
8 |
- |
| unsigned long int | 4 |
8 |
- |
| long long int | 8 | 8 | -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 |
| unsigned long long int | 8 | 8 | -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 |
| float | 4 | 4 | 3.4E +/- 38(6~7位有效数字) |
| double | 8 | 8 | 1.7E +/- 308 (14~15位有效数字) |
| long double | 16 | 16 | 1.7E +/- 308 (18~19位有效数字) |
如何选择数据类型?以下是一些经验准则:
- 当明确知晓数值不可能为负时,选用无符号类型;
- 使用int执行整数运算;
- 在算术表达式中不用使用char或bool;
- 执行浮点数运算时选用double;
1.1.2 类型转换
当把一种算术类型的值赋给另外一种类型时,类型所能表示的值的范围决定了转换的过程:
bool b = 42; // b = true
int i = b; // i = 1
i = 3.14; // i = 3
double pi = i; // pi = 3.0
unsigned char c = -1; // 假设char占8bit,c = 255;
signed char c2 = 256; // 假设char占8bit,c2的值是未定义的
- 当把一个非布尔类型的算术值赋给布尔类型时,算术值为0则结果为
false,否则结果为true; - 当把一个布尔值赋给非布尔类型时,布尔值为
false则结果为0,否则结果为1; - 当把一个浮点数赋给整数类型时,进行了近似处理,结果值将仅保留浮点数中的小数点之前的部分;
- 当把一个整数值赋给浮点类型时,小数部分记为0,若该整数所占的空间超过了浮点类型的容量,精度有可能损失;
- 当赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数组总数取模后的余数;例如:8bit大小的unsigned char数据范围是0~255,若赋给了一个区间以外的值,则实际结果是该值对256取模后的余数,-1赋给8bit的unsigned char时,实际上是赋给了255,对256取模之后还是255;
- 当赋给带符号的类型一个超出它数据范围的值时,结果是未定义(undifined)的,此时程序可能继续工作,可能崩溃,也可能生成垃圾数据!
程序应当尽量避免依赖于实现环境的行为,这样的程序是不可移植的!
当程序的某处使用了一种算术类型的值而实际上所需要的是另一种类型的值时,编译器同样会执行上述的转换:
int i = 42;
if(i) // if语句的条件将为true
i = 0;
含有无符号类型的表达式
当一个算术表达式中既有无符号数又有int值时,那个int值将会转换成无符号数:
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // 输出-84
std::cout << u + i << std::endl; // 若int占4字节,则输出4294967264(即2^32 - 32)
当从无符号数中减去一个值时,不管这个值是不是无符号数,都必须确保结果不能是一个负值:
unsigned u1 = 42, u2 = 10;
std::cout << u1 - u2 << std::endl; // 正确:输出32
std::cout << u2 - u1 << std::endl; // 正确:输出
无符号数不会小于0,因此在循环中要注意循环条件:
for(unsigned u = 10; u >= 0; u++) // 错误,变量u永远也不会小于0,陷入死循环
std::cout << u << std::endl;
切勿混用带符号类型和无符号类型!
若表达式里既含有带符号类型又含有无符号类型,带符号数会自动转换成无符号数,当带符号类型取值为负值时,会出现异常结果!
1.1.3 字面值常量
一个形如42的值被称为字面值常量(literal)。每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型;
整型和浮点型字面值
可以将整型字面值写作十进制数、八进制数或十六进制数的形式——以0开头的整数表示八进制数,以0x或0X开头的表示十六进制数:
20 /* 十进制 */ 024 /* 八进制 */ 0x14 /* 十六进制 */
整型字面值具体的数据类型由它的值和符号决定,默认情况下,十进制字面值是带符号数,八进制和十六进制字面值可能是带符号数也可能是无符号数。十进制字面值的类型是int、long和long long中字节数最小的那个(int),前提是该类型能容纳下当前值;八进制和十六进制字面值的类型是能容纳其数值的int、unsigned int、long、unsigned long、long long和unsigned long long中的字节数最小的那个。类型short没有对应的字面值!
浮点型字面值是一个小数或以科学计数法表示的指数,其中指数部分用E或e表示:
3.14159 3.14159E0 0. 0e0 .001
浮点型字面值默认是一个double类型。
字符和字符串字面值
由单引号括起来的一个字符称为char型字面值,由双括号括起来的0个或多个字符则被称为字符串型字面值:
'a' // 字符字面值
"Hello World!" // 字符串字面值
字符串字面值的类型实际上是由常量字符构成的数组,编译器为每个字符串的结尾加上一个空字符'\0',因此字符串字面值的实际长度要比它的实际字符个数多1。
若两个字符串字面值位置紧邻并且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。当熟悉的字符串字面值比较长,写在一行里不合适时,就可以采取分开书写的方式:
std::cout << "a really, really long string literal "
"that spans two lines" << std::endl;
转义序列
有两类字符程序员不能直接使用:
- 不可打印的字符,如退格或其他控制字符,因为它们没有可视的图符;
- 在C++中有特殊含义的字符(单引号、双引号、问号、反斜线)
在这种情况下需要使用转义序列(escape sequence),转义序列均以反斜线作为开始:
| 转义序列 | 表示 |
|---|---|
| 换行符 | \n |
| 回车符 | \r |
| 横向制表符 | \t |
| 纵向制表符 | \v |
| 退格符 | \b |
| 报警(响铃)符(标准输出中输出该符时系统自带的扬声器会发出"叮"的一声) | \a |
| 进纸符 (将光标移动到下一页开头) | \f |
| 反斜线 (将光标回退回前一个字符) | \\ |
| 双引号 | \" |
| 单引号 | \' |
| 问号 | ? |
在程序中,转义序列被当做一个字符使用:
std::cout << '\n'; // 转到新的一行
std::cout << "\tHi!\n"; // 输出一个制表符,输出"Hi!",转到新的一行
也可以使用泛化的转义序列,其形式是\x后跟一个或多个十六进制数字或者\后紧跟1个、2个或3个八进制数字,其中数字部分表示的是字符对应的数值:
\7 (响铃) \12 (换行符) \40(空格)
\0 (空字符) \115(字符M) \x4d(字符M)
注意:如果反斜线后面跟着的八进制数字超过3个,则只有前3个数字与反斜线\构成转义序列,例如\1234表示两个字符——123对应的字符和字符4。
而\x要用到后面跟着的所有字符,例如\x1234表示1234这个十六进制数对应的字符
指定字面值的类型
通过添加前缀和后缀可以改变整型、浮点型和字符型字面值的默认类型:
| 前缀 | 含义 | 类型 |
|---|---|---|
| u | Unicode16字符 | char16_t |
| U | Unicode32字符 | char32_t |
| L | 宽字符 | wchar_t |
| u8 | UTF-8(仅用于字符串字面常量) | char |
| 后缀 | 含义 | 类型 |
|---|---|---|
| u or U | 无符号整型 | unsigned |
| l or L | 长整型 | long |
| ll or LL | 长整型 | long |
| f or F | 单精度浮点型 | float |
| l or L | 扩展精度浮点型 | long double |
L'a' // 宽字符字面值,类型是wchar_t
u8"Hi" // utf-8字符串字面值
42ULL // 无符号长整型字面值,类型是unsigned long long
1E-3F // 单精度浮点型字面值,类型是float
3.14159L // 扩展精度浮点型字面值,类型是long double
布尔字面值和指针字面值
true 和 false是布尔类型的字面值,nullptr是指针字面值
1.2 变量
变量(variable)提供一个具名的、可供程序操作的存储空间,C++中的每个变量都具有其数据类型,数据类型决定着变量所占内存空间大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。对C++程序员来说,“变量(variable)”和“对象(object)”一般可以互换使用。
什么是对象?
对象实际上是指一块能存储数据并具有某种类型的内存空间。
1.2.1 变量定义
变量定义的基本形式是:首先是类型说明符,随后紧跟着一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。列表中每个变量名的类型都由类型说明符指定,定义时还可以为一个或多个变量赋初值:
int sum = 0, value,
units_sold = 0; // sum、value和units_sold都是int,sum和units_sold初值为0
Sales_item item; // item的类型是Sales_item
std::string book("0-201-78345-X"); // book通过一个string字面值初始化
初始值
当对象在创建时获得了一个特定的值,则说这个对象被初始化(initialized)了,用于初始化变量的值可以是任意表达式:
double price = 109.99, discount = price * 0.6; // price用字面量初始化,discount用表达式的值初始化
double salePrice = applyDiscount(price, discount); // salePrice用函数applyDiscount返回值初始化
初始化和赋值的区别:
初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,然后以一个新的值代替
列表初始化
C++定义了初始化的好几种不同形式:
int units_sold = 0;
int units_sold = {0};
int units_sold{0};
int units_sold(0);
从C++11新标准开始,用花括号来初始化变量得到了全面应用,这种初始化的形式被称为列表初始化(list initialization)
,无论是初始化对象,还是为对象赋新值,都可以使用列表初始化。
当用于内置类型的变量时,若使用列表初始化并且初始值存在丢失信息的风险时,编译器将会报错:
long double ld = 3.1415926536;
int a{ld}, b = {ld}; // 错误:转换未执行,因为存在丢失信息的风险
int c(ld), b = ld; // 正确:转换执行了,且确实丢失了部分值
默认初始化
如果定义变量时没有指定初值,那么变量将被默认初始化(default initialization),此时变量被赋予了“默认值”,默认值是什么取决于变量类型:
-
如果是内置类型的变量未被显式地初始化,它的值将由定义的位置决定:
- 定义于
任何函数体之外的变量将被初始化为0; - 定义于
函数体内部的内置类型的变量将不被初始化(uninitialized),一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误!
- 定义于
-
每个类各自决定其初始化对象的方式,是否允许不经初始化就定义对象也由类自己决定,若类允许这种行为,它将决定对象的初始值到底是什么,绝大多数类都支持无需显式初始化而定义对象,这样的类提供了一个合适的默认值:
std::string empty; // empty非显式化地初始化为一个空串
Sales_item item; // 被默认初始化的Sales_item对象
一些类则要求每个对象都显式地初始化,此时如果创建了一个该类的对象而未对其做明确的初始化操作的话,将会引发错误。
未初始化变量引发运行时故障
未初始化的变量含有一个不确定的值,编译器并未要求检查使用未初始化的变量这一行为,使用未初始化的变量将会带来无法预计的后果!建议初始化每一个内置类型的变量。
1.2.2 变量声明和变量定义
为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(separate compilation)机制,该机制允许程序分割为若干个文件,每个文件可独立编译。
为了支持分离式编译,C++将变量声明和定义区分开来,声明(declaration)使得名字为程序所知,一个文件如果想使用别的文件定义的变量的名字则必须包含对那个名字的声明,而定义(definition)则负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,但定义除了定义变量的类型和名字之外,还申请了存储空间,还可能会为变量赋一个初始值。
若想声明一个变量而非定义它,可以在变量名前添加关键字extern,并且不要显式地初始化变量,任何包含了显式初始化的声明将成为定义,这样也就抵消了extern的作用:
extern int i; // 声明i而非定义i
int j; // 声明并定义j
extern double pi = 3.1416; // 定义
在函数体内部,如果初始化一个由extern关键字标记的变量将引发错误。
int main()
{
extern int i = 5; // 错误
}
变量只能被定义一次,但是可以被多次声明。
静态类型
C++是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型,其中检查类型的过程被称为类型检查(type checking),对象的类型决定了对象能参与的运算,在C++语言中,编译器负责检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将会报错。
1.2.3 标识符
C++ 标识符(indentifier)由字母、数字、下划线组成,其中必须以字母或下划线开头,不能以数字开头,标识符长度没有限制,但对大小写敏感。C++ 保留了一些名字作为关键字,这些名字不能被用作标识符。
1.2.4 名字的作用域
同一个名字在程序的不同位置,可能指向不同的实体。
作用域是程序的一部分,在其中名字有特殊的含义,C++中大多数的作用域都以花括号分隔。同一个名字在不同的作用域中可能指向不同的实体,名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。
#include <iostream>
int main() {
int sum = 0;
for (int val = 1; val <= 10; i++) {
sum += val;
}
std::cout << sum << std::endl;
}
名字main定义于所有花括号之外,它和其他大多数定义在函数体之外的名字一样都具有全局作用域(global scope),一旦声明之后,全局作用域内的名字在整个程序范围内都可使用。名字sum定义于main函数所限定的作用域内,从声明sum到main函数结束为止都可以访问它,但是出了main函数所在的快就无法访问了,因此变量sum拥有块作用域(block scope)。名字val定义于for语句内,在for语句内可以访问,但是在main函数中的其他部分就不能访问它了。
在第一次使用变量时再定义它
一般来说,在对象第一次使用的地方附近定义它是一种好的选择,因为这样做有助于更容易找到变量的定义,同时也能赋给一个合理的初始值。
作用域的嵌套
作用域能彼此包含,被包含的作用域被称为内层作用域(inner scope),包含着别的作用域的作用域被称为外层作用域(outer scope)。
作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字,同时允许在内层作用域中重新定义外层作用域已有的名字(不建议)
#include <iostream>
int reused = 42; // reused拥有全局作用域
int main() {
int unique = 0; // unique拥有块作用域
std::cout << reused << std::endl; // 输出42,使用全局变量reused
int reused = 0; // 新建局部变量reused,覆盖了全局变量reused
std::cout << reused << std::endl; // 输出0,使用局部变量reused
std::cout << ::reused << std::endl; // 输出42,显式地指定使用全局变量reused
}
::是作用域操作符,因为全局作用域本身并没有名字,所以当作用域操作符左侧为空时,将向全局作用域发出请求获取作用域操作符右侧名字对应的变量。
1.3 复合类型
复合类型是基于其他类型定义的类型,C++有多种复合类型——引用和指针等。
一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成,每个声明符命名了一个变量并指定该变量为与基本类型有关的某种类型。
1.3.1 引用
C++11中新增了一种引用即所谓的右值引用(rvalue reference),这种引用主要用于内置类中,一般说的引用(reference)实际上指的是左值引用(lvalue reference)。
引用为对象起了一个别名,引用类型引用(refer to)另外一种类型,通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名:
int ival = 1024;
int &refVal = ival; // refVal指向ival(refVal是ival的一个别名)
int &refVal1; // 报错,引用必须被初始化