《C++ Primer》第2章 变量和基本类型(上)
2.1 基本内置类型
C++基本内置类型包括:算数类型、空类型(void)。
空类型仅用于一些特殊场合,如函数不返回任何值时使用void
作为返回类型。
2.1.1 算数类型
算数类型包括整型(包括字符和布尔型在内)和浮点型。
C++标准只规定了每种算数类型尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。所以算数类型的尺寸在不同机器上有所差别。
布尔类型(
bool
)的取值是真(true
)或假(false
)。一个
char
的大小和一个机器字节一样
//在本机上用以下程序测试各类型所占字节
#include <iostream>
int main(){
std::cout <<sizeof(int)<<"," //int占4个字节
<<sizeof(char)<<","//char占1个字节
<<sizeof(double)<<","//double占8个字节
<<sizeof(bool)<<","//bool占1个字节
<<sizeof(wchar_t)<<"," //wchar_t占4个字节
<<sizeof(char16_t)<<"," //char16_t占2个字节
<<sizeof(char32_t)<<"," //char32_t占4个字节
<<sizeof(short)<<"," //short占2个字节
<<sizeof(long)<<"," //long占8个字节
<<sizeof(long long)<<"," //long long占8个字节
<<sizeof(float)<<","//float占4个字节
<<sizeof(long double) //long double占16个字节
<<std::endl;
return 0;
}
/*编译运行此程序,输出为: *4,1,8,1,4,2,4,2,8,8,4,16 *各数字依次对应为程序中各数据类型所占字节数 */
计算机可寻址的最小内存块称为字节。
大多数机器,1个字节=8个二进制位,即1 byte=8 bit。
为了赋予内存中某个地址明确的含义,必须知道存储在该地址的数据的类型。
带符号类型和无符号类型
除去布尔型和扩展字符型(除char以外的其他字符型,如wchar_t
等),其他整型可以划分为带符号的(signed)和无符号的(unsigned)。
除char
外,其他默认都是带符号的,前边加上unsigned就变成无符号的。如int
为带符号的,unsigned int
为无符号的。另外unsigned int
可以缩写为unsigned
。
单独提一下字符型,char
、signed char
、unsigned char
是不同的。但表现形式只有带符号的和无符号的两种,其中的char
有没有符号由编译器决定。
double和float尽量选double。因为float通常精度不够,且与double计算代价相差无几。
2.1.2 类型转换
-
当我们把非布尔类型的算数值赋给布尔类型时,初始值为0则结果为false,否则结果为true。
如:
bool b = 42;
,则b的值为true。 -
当我们把布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。
如,接上:
int i = b;
,则i的值为1。 -
当把浮点数赋给整数类型时,会进行近似处理,结果值仅保留小数点之前的部分。
如:
double pi = 3.14;
,则pi的值为3.0。 -
当把整数值赋给浮点型时,小数部分记为0。如果该整数所占空间超过浮点型容量,精度可能有损失。
-
当赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。
如:8 bit的
unsigned char
可以表示0至255内的值。若给它赋值-1,则结果为-1对256取模后所得的余数,即255。取模计算过程:
1.求整数商c(向负无穷方向舍入)
c=a/b=-1/256=-0.00390605,向负无穷方向舍入,则结果为-1。
2.计算模或余数r
r=a-c*b=-1-(-1)*256=255。
-
当赋给带符号类型一个超出它表示范围的值时,结果是未定义的。此时,程序可能继续工作、可能崩溃,也可能产生垃圾数据。
含有无符号类型的表达式
-
当一个算数表达式中既有无符号数又有
int
值时,那个int
值就会转换成无符号数。如:
unsigned u =10; int i = -42;
,若计算u+i
,其结果会是4294967264(前提int占4个字节)。计算u+i时,i转换为无符号数,转换过程就是上述取模计算过程。
1.c=-42/(2^32),向负无穷方向舍入后得c=-1。
2.r=-42-(-1)*2^32=4294967254
3.u+i=u+r=4294967264
-
当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不能使一个负值。
如:
unsigned u1=42,u2=10;
,若计算u2-u1
,其结果又会是4294967264。
提示:表达式里既有带符号类型又有无符号类型,带符号数会自动转换成无符号数。
2.1.3 字面值常量
整型、浮点型、字符型、字符串型字面值
一个形如42的值被称为字面值常量,即一望而知的值。
如:42是一个整型字面值。
3.14159是一个浮点型字面值。
'a’是一个字符字面值
“A”是一个字符串字面值。(实际长度为2,因为它以’\0’结尾)。
如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,那它们实际是一个整体。如:
std::cout<<"a really,really long string literal "
"that spans two lines."<<std::endl;
转义序列
有两类字符程序员不能直接使用:一类是不可打印的字符,如退格等;另一类是C++中有特殊含义的字符(单引号、双引号、问号、反斜线)。
在这些情况下需要用到转义序列。
如\n
代表换行符,\r
代表回车符,等等。
在程序中,转义序列被当作一个字符使用。
泛化转义序列:形式为
\x
后紧跟1个或多个十六进制数字,或\
后紧跟1~3个八进制数字。其中数字部分表示字符对应的数值。
指定字面值的类型
通过添加前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型。
如L'a'
为宽字符型字面值,类型为wchar_t
;
3.14159L
为扩展精度浮点型字面值,类型为long double
;
42ULL
为无符号整型字面值,类型为unsigned long long
。
布尔字面值和指针字面值
true
和false
是布尔类型的字面值,nullptr
是指针字面值。
2.2 变量
变量和对象两个词一般可以互换使用。
2.2.1 变量定义
基本形式:类型说明符,随后紧跟一个或多个变量名,其中变量名以逗号分隔,最后以分号结束。定义时还可以为一个或多个变量赋初值。如:
int sum=0,value;
Sales_item item;
std::string book("0-201-78345-X");
通常,对象指具有某种数据类型的存储空间。我们在使用对象这个词时,并不严格区分是类还是内置类型。
初始值
-
用于初始化变量的值可以使任意复杂的表达式
-
当一次定义两个或多个变量时,对象名字随着定义也就马上可以使用了。因此,在同一条定义语句中,可以用先定义的变量值去初始化后定义的其他变量。如:
double price = 10.99 , discount=price*0.2;
在C++中,初始化和赋值是两个完全不同的操作。
初始化:创建变量时赋予其一个初始值。
赋值:把对象的当前值擦除,而以一个新值来替代。
列表初始化
列表初始化:无论初始化对象还是某些时候为对象赋新值,均可采用一组由花括号括起来的初始值。如:
int units_sold{
0};
值得注意的是,当用于内置类型的变量时,如果使用初始化列表且初始值存在丢失信息的风险,则编译器会报错。如:
long double ld = 3.1415926536;
int a{
ld}; //错误:转换未执行,因为存在丢失信息的风险
int b(ld); //正确:转换执行,且确实丢失了部分值
默认初始化
如果定义变量时没有指定初始值,则变量被默认初始化。此时变量被赋予了“默认值”,默认值到底是什么由变量类型决定,同时定义变量的位置也对此有影响。
-
内置类型变量未被显式初始化,它的值由定义的位置决定。
定义于任何函数体之外的变量被初始化为0。
定义于函数体内部的内置变量类型是不被初始化的,一个未被初始化的内置类型变量的值是未定义的,访问时会引发错误。
用一下程序测试:
#include<iostream> int i; int fun(int& c,int& d){ std::cout<<c<<" "<<d<<std::endl; return 0; } int main(){ int j; int k; fun(i,j); std::cout<<"k的值为:"<<k<<std::endl; return 0; }
反复执行这段程序,可以发现只有i的值固定为0,而j和k每次执行结果不同。因为i定义在任何函数体之外,被初始化为0;而j、k都定义在函数体内,是不被初始化的。
建议初始化每一个内置类型变量。
-
每个类各自决定其初始化对象的方式。且是否允许不经初始化就定义对象也由类自己决定。如果类允许这种行为,它将决定对象的初始值到底是什么。
绝大多数类都支持无须显式初始化而定义对象。
如
std::string str1;
中str1
非显式地初始化为一个空串。
2.2.2 变量声明和定义的关系
C++支持分离式编译:允许将程序分割为若干个文件,每个文件可独立编译。
如果程序分为多个文件,就要有文件间共享代码的方法,如std::cout
定义在标准库,却能被我们的程序使用。
为支持分离式编译,C++将声明和定义区分开来。
声明:使名字为程序所知,一个文件如想使用别处定义的名字则必须包含对那个名字的声明。
定义:创建与名字关联的实体。
区别:定义除了规定了变量的类型和名字,还会申请存储空间,并可能为变量赋初始值。而声明仅仅指明变量类型和名字。
-
只声明不定义的话,就在变量名前加关键字
extern
。如:extern int i; //声明i而非定义i int j; //声明并定义了j
-
任何包含了显式初始化的声明即称为定义。即
extern
语句若包含初始值就不再是声明,而是定义了。如:extern double pi = 3.1416; //定义
1.在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。
2.变量能且只能被定义一次,但可以被多次声明。
C++是静态类型语言,其含义是在编译阶段检查类型。编译器负责检查数据类型是否支持要执行的运算,如果不支持,编译器将会报错且不会生成可执行文件。
2.2.3 标识符
- C++的标识符由字母、数字、下划线组成。
- 必须以字母或下划线开头。
- 标识符长度没有限制,但是对大小写敏感。
- 用户自定义的标识符不能连续出现两个下划线,也不能以下划线紧连大写字母开头。
- 定义在函数体外的标识符不能以下划线开头。
2.2.4 名字的作用域
作用域以花括号分隔。
名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。
以以下程序为例:
#include<iostream>
int main(){
int sum = 0;
for(int val = 1; val <= 10; ++val)
sum += val;
std::cout<< "Sum of 1 to 10 inclusive is "
<< sum << std::endl;
return 0;
}
- main定义于所有花括号外,拥有全局作用域。
- sum和val拥有块作用域。sum在main函数限定的作用域内;val在for语句限定的作用域内。
建议:第一次使用变量时再定义它
嵌套的作用域
作用域中一旦声明了某名字,它所嵌套的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字。
以以下程序为例:
#include<iostream>
int reused = 42; //reused拥有全局作用域
int main(){
int unique = 0; //unique拥有块作用域
//输出#1:使用全局变量reused;输出42 0
std::cout << reused << " " << unique << std::endl;
int reused = 0; //新建局部变量reused,覆盖了全局变量reused
//输出#2:使用局部变量reused;输出0 0
std::cout << reused << " " << unique << std::endl;
//输出#3:显式地访问全局变量reused;输出42 0
std::cout << ::reused << " " << unique << std::endl;
return 0;
}
- 输出#1出现在局部变量reused定义之前,因此这条语句使用全局作用域中的名字reused。
- 输出#2发生在局部变量reused定义后,因此这条语句使用局部变量reused。
- 输出#3使用了作用域操作符
::
,覆盖了默认的作用域规则,因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求,获取::
右侧名字对应的变量。故这条语句使用的全局变量reused。
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。