一、基本内置类型
C++定义了一套包括算术类型和空类型在内的基本数据类型。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊场合,例如,当函数不返回任何值时使用空类型作为返回类型。
1、算术类型
算术类型分为两类:整型(包括字符和布尔类型在内)和浮点数。
算术类型的尺寸(也就是该类型数据所占的比特数)在不同机器上有所差别。C++标准规定了尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。某一类型所占的比特数不同,它所能表示的数据范围也不一样。
C++的算术类型:
类型 | 含义 | 最小尺寸 |
bool | 布尔类型 | 未定义 |
char | 字符 | 8位 |
wchar_t | 宽字符 | 16位 |
char16_t | Unicode字符 | 16位 |
char32_t | Unicode字符 | 32位 |
short | 短整型 | 16位 |
int | 整型 | 16位 |
long | 长整型 | 32位 |
long long | 长整型 | 64位 |
float | 单精度浮点数 | 6位有效数字 |
double | 双精度浮点数 | 10位有效数字 |
long double | 扩展精度浮点数 | 10位有效数字 |
1)带符号类型和无符号类型
除去布尔型和扩展的字符型之外,其他整型可以划分为带符号的和无符号的两种。带符号类型可以表示正数、负数或0,无符号类型仅能表示大于等于0的值。
类型int、short、long或long long都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型。
与其他整型不同,字符型被分为了三种:char、signed char和unsigned char。特别需要注意的是,类型char和类型signed char并不一样。尽管字符型有三种,但是字符的表现形式却只有两种:带符号的和无符号的。类型char实际上会表现为上述两种形式中的一种,具体是那种由编译器决定。
2、字面值常量
每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。
1)整型和浮点型字面值
我们可以把整型字面值写作十进制数、八进制数或十六进制数的形式。以0开头的整数代表八进制数,以0x或0X开头的代表十六进制数。
1 #include <iostream> 2 #include <memory> 3 #include <string> 4 #include <vector> 5 6 int main() 7 { 8 int x = 20; // 10进制 9 int y = 024; // 8进制 10 int z = 0x14; // 16进制 11 std::cout << x << ", " << y << ", " << z << std::endl; 12 return 0; 13 }
整型字面值具体的类型由它的值和符号决定。默认情况下,十进制字面值是带符号数(int、long、long long中尺寸最小的那个且能容纳下当前的值),八进制和十六进制字面值既可能是带符号的也可能是无符号的(int、unsigned int、long、unsigned long、long long和unsigned long long中尺寸最小的那个且能容纳下当前的值)。
浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识:
1 #include <iostream> 2 #include <memory> 3 #include <string> 4 #include <vector> 5 6 int main() 7 { 8 double arr[] = { 3.14159, 3.14159E0, 0., 0e0, .001 }; 9 for (auto x:arr) 10 std::cout << x << " "; 11 std::cout << std::endl; 12 return 0; 13 }
默认的,浮点型字面值是一个double。
2)字符和字符串字面值
由单引号括起来的一个字符称为char型字面值,双括号括起来的零个或多个字符则构成字符串字面值。
字符串字面值实际上是由常量字符构成的数组。编译器在每个字符串的结尾处添加一个空字符(' '),因此字符串字面值的实际长度要比它的内容多1。
如果两个字符串字面值位置紧邻且仅由空格、缩进、换行符分隔,则它们实际上是一个整体。
1 #include <iostream> 2 #include <memory> 3 #include <string> 4 #include <vector> 5 6 int main() 7 { 8 std::string s = "abcdef" 9 " QAQ"; 10 std::cout << s << std::endl; 11 return 0; 12 }
3)转义序列
有2类字符程序员不能直接使用:一类是不可打印的字符,如退格或其他空字符,因为它们没有可视的图符;另一类是在C++语言中有特殊含义的字符(单引号、双引号、问号、反斜线)。在这些情况下需要用到转义序列,转义序列均以反斜线开始。
4)指定字面值的类型
可以通过指定字面值的前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型。
字符和字符串字面值
前缀 | 含义 | 类型 |
u | Unicode16字符 | char16_t |
U | Unicode32字符 | char32_t |
L | 宽字符 | wchar_t |
u8 | UTF-8(仅用于字符串字面值) | char |
整型字面值
后缀 | 最小匹配类型 |
u或U | unsigned |
l或L | long |
ll或LL | long long |
浮点型字面值
后缀 | 类型 |
f或F | float |
l或L | long double |
二、变量
变量提供一个具名的、可供程序操作的存储空间。
1、变量定义
1)初始值
当对象在创建时获得了一个特定的值,我们说这个对象被初始化了。当一次定义了两个或多个变量时,对象的名字随着定义也就马上可以开始使用了。因此在同一条定义语句中,可以用先定义的变量值去初始化后定义的其他变量:
1 #include <iostream> 2 #include <memory> 3 #include <string> 4 #include <vector> 5 6 int main() 7 { 8 int x = 1, y = 2 * x; 9 std::cout << x << ", " << y << std::endl; 10 return 0; 11 }
注意:初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦掉,而已一个新值来替代。
2)列表初始化
c++语言定义了初始化的好几种不同形式,如下所示:
1 #include <iostream> 2 #include <memory> 3 #include <string> 4 #include <vector> 5 6 int main() 7 { 8 int a = 1; 9 int b = { 2 }; 10 int c{ 3 }; 11 int d = { 4 }; 12 std::cout << a << "," << b << "," << c << "," << d << std::endl; 13 return 0; 14 }
注意:大多数情况下,这些初始化方式可以相互等价地使用,不过也并非总是如此。有几种例外情况:其一,使用拷贝初始化时只能提供一个初始值;其二,如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号的形式初始化;其三,如果提供的是初始元素的列表,则只能把初始值都放在花括号里进行列表初始化。
用花括号来初始化变量的形式被称为列表初始化。现在无论是初始化对象还是为对象赋新值,都可以使用这样一组由花括号括起来的初始值了。
用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器会报错。
1 #include <iostream> 2 #include <memory> 3 #include <string> 4 #include <vector> 5 6 int main() 7 { 8 long double ld = 3.1415926536; 9 //int a{ ld }, b = { ld }; //错误:转换未执行,因为存在丢失信息的风险 10 int c(ld), d = ld; // 转换执行,且确实丢失了部分信息 11 std::cout << ld << "," << c << "," << d << std::endl; 12 return 0; 13 }
如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化。
当初始值只有一个时,使用直接初始化或拷贝初始化都行。当初始化要用到的值有多个时,一般来说只能使用直接初始化。
3)默认初始化
如果定义变量时没有指定初始值,则变量被默认初始化,此时变量被赋予了默认值。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。
如果是内置类型的变量未被显示初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。定义在函数体内部的内置类型变量将不被初始化,值是未定义的,如果试图拷贝或是访问此类值将会引发错误(如果局部静态变量没有显示的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0)。
类的对象如果没有显示地初始化,则其值由类决定。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,其值是未定义的。
默认初始化在以下情况下发生:
- 当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时。
- 当一个类本身含有类类型的成员且使用合成的默认构造函数时。
- 当类类型的成员没有在构造函数初始值列表中显示地初始化时。
4)值初始化
如果是内置类型,比如int,则元素初始值自动设为0。
值初始化在以下情况发生:
- 在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时。
- 当我们不使用初始值定义一个局部静态变量时。
- 当我们通过书写形如T()的表达式显式地请求值初始化时,其中T是类型名。
2、变量的声明和定义
为了允许把程序拆分成多个逻辑部分来编写,c++支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
为了支持分离式编译,c++将声明和定义区分开来。声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。定义负责创建与名字关联的实体。
变量声明规定了变量的类型和名字。但是除此之外,定义还申请存储空间,也可能为变量赋一个初始值。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示地初始化变量:
1 #include <iostream> 2 #include <memory> 3 #include <string> 4 #include <vector> 5 6 int main() 7 { 8 extern int x; //声明x 9 int y; // 声明并定义y 10 return 0; 11 }
任何包含了显示初始化的声明即成为定义。我们能给由extern关键字标记的变量赋一个初始值,但是这么做就抵消了extern的作用。extern语句如果包含初始值就不再是声明而变成定义了。
变量能且仅能被定义一次,但是可以被多次声明,声明后也可以对变量进行修改操作。
如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
3、标识符
C++的标识符由字母、数字和下划线组成,其中必须以字母或下划线开头。标识符的长度没有限制,但是对大小写敏感。
C++为标准库保留了一些名字。用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧连大写字母开头。此外,定义在函数体外的标识符不能以下划线开头。
4、名字的作用域
作用域时程序的一部分,在其中名字有其特点的含义。C++语言中大多数作用域都以花括号分隔。
同一个名字在不同的作用域可能指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。
定义在函数体之外的名字拥有全局作用域。一旦声明后,全局作用域的名字在整个程序的范围内都可使用。定义在函数体内的名字拥有块级作用域。
1)嵌套的作用域
作用域能彼此包含,被包含的作用域称为内层作用域,包含着别的作用域的作用域称为外层作用域。
作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。
因为全局作用域没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量。