变量和基本类型
1 基本内置类型类型
类型 | 含义 | 最小尺寸 |
---|---|---|
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 | 双精度浮点数 | 15位有效数字 |
long double | 扩展精度浮点数 | 15位有效数字 |
- C++规定,一个 int 至少和一个 short 一样大,一个 long 至少和一个 int 一样大,一个long long 至少和一个 long 一样大。其中 long long 是在 C++11 中新定义的。
- 一个char的大小和一个机器字节一样。
- 类型决定了数据所占的比特数以及该如何解释这些比特内容。
- 除去布尔型和扩展的字符型之外,其他整形可划分为 带符号的(signed) 和 不带符号的(unsigned) 两种。带符号的可表示正数、负数或0,无符号类型则仅能表示大于等于0的值。
注: 可寻址的最小内存块称为“字节(byte)”,存储的基本单元称为“字(word)”,它通常由几个字节组成。在C++语言中,一个字节要至少能容纳机器基本字符集中的字符。大多数机器的字节由8比特构成,字则由32或64比特构成,也就是4或8字节。
选择类型的一些经验准则
- 当明确知晓数值不可能为负时,选用无符号类型。
- 使用int执行整数运算。在实际应用中,short常常显得太小而long一般和int有一样的尺寸。如果运算范围超过int的表示范围,则选用long long。
- 在算术表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它。因为类型char在一些机器上是有符号的,而在一些机器上又是无符号的,所以使用char进行算术运算特别容易出问题。如果真的需要使用一个不大的整数,那么明确指出它的类型是signed char还是 unsigned char。
- 执行浮点数运算选用double,这是因为float的精度通常不够而双精度浮点数和单精度浮点数的计算代价相差无几。事实上,对于某些机器来说,双精度运算甚至比单精度运算还要快。long double提供的精度在一般情况下不用用到,况且它带来的运算时的消耗也不容忽视。
2 字面值常量
2.1 整形和浮点型字面值
整形:以0开头的整形数代表八进制数或者 \115,反斜线后面跟着的八进制数字限制为3位超过三个,只有前三个数字与反斜线构成转义序列,以0x或0X开头的代表十六进制数或者 \x4d,会用到后面跟着的所有数字
注: 尽管整形字面值可以存储在带符号数据类型中,但严格来说,十进制字面值不会是负数。如果我们使用了一个形如-42的负十进制字面值,那个负号并不在字面值之内,它的作用仅仅是对字面值取负值而已。
浮点:浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用e或E标识
2.2 字符和字符串字面值
注: 编译器在每个字符串的结尾处添加一个空字符串(‘\0’),因此,字符串字面值的实际长度要比他的内容多1。
2.3 指定字面值的类型
字符和字符串字面值
前缀 | 含义 | 类型 |
---|---|---|
u | Unicode字符 | char16_t |
U | Unicode字符 | char32_t |
L | 宽字符 | wchar_t |
u8 | UTF-8 | char |
整形字面值
后缀 | 最小匹配类型 |
---|---|
u or U | unsigned |
l or L | long |
ll or LL | long long |
浮点形字面值
后缀 | 类型 |
---|---|
f or F | float |
l or L | long double |
3 变量
3.1 初始值
当对象在创建时,获得了一个特定的值,我们说这个对象被初始化了。当一次定了了两个或多个变量时,对象的名字随着定义也就马上可以使用了。因此在同一条定义语句中,可以用先定义的变量值去初始化后定义的其他变量。
// 正确: price现被定义并复制,随后被用于初始化discount
double price = 109.00, discount = price * 0.16;
注: 初始化不是赋值,初始化的含义时创建变量时赋予其一个初始值,而赋值的含义时把对象的当前值擦除,而以一个新值来代替。
3.2 变量声明和定义的关系
为了允许吧程序拆分成多个逻辑部分来编写,C++语言支持 分离式编译 机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
为了支持分离式编译,C++语言将声明和定义区分开来。 声明 使得名字为程序所知,一个文件如果想使用别处定义的名字,则必须包含对那个名字的声明。 而 定义 负责创建与名字关联的实体。
注: 变量能且只能被定义一次,但是可以被多次声明。
如果想声明一个变量而非定义它,就在变量名前添加关键字 extren,而且不要显示的初始化变量。
extern int i; // 声明而非定义
int j; // 声明并定义
extren double pi = 3.1416; // 定义
关键概念:静态类型
- c++是一种 静态类型语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为 类型检查。
- 对象的类型决定了对象所能参与的运算,在c++语言中,编译器负责检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错并且不会生成可执行文件。
- 程序越复杂,静态类型检查越有助于发现问题。然而,前提是编译器必须知道每一个实体对象的类型,这就要求我们在使用某个变量之前必须声明其类型。
4 复合类型
复合类型 是指基于其他类型定义的类型。如 引用和指针。
4.1 引用
引用 为对象起了另外一个名字,引用类型引用类外一种类型。通过声明符写成的&d的形式来定义引用类型,其中d是声明的变量名,引用必须被初始化。一旦定义了引用,就无法令其再绑定到另外的对象上。
int ival = 1024;
int &refVal = 10; // 错误引用类型的初始值必须是一个对象
int &refVal = ival; // refVal指向ival(是ival的另一个名字)
int &refval2; // 报错;引用必须被初始化
- 注: C++11中新增了一种引用:右值引用,这种引用主要用于内置类。通常所说的引用指的是左值引用。
一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。引用并非对象,相反的,它只是为一个已经存在的对象所起的另一个名字,因此不能定义引用的引用。
4.2 指针
指针 是指向另外一种类型的复合类型。与引用类似,指针也实现了对其它对象的间接访问,但是又有很多不同点。
-
- 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内他可以先后指向几个不同的对象。
-
- 指针无需在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。通过声明符写成*d的形式,其中d是变量名。
获取对象的地址
指针存放某个对象的地址,要想获取该地址,需要使用取地址符(&) ,由于引用不是对象,索引不能定义只想引用的指针。
int ival = 42;
int *p = %ival; // p存放变量ival的地址,或者说p是指向变量ival的指针
指针值
指针的值(即地址),应属于下列4种状态之一
- 指向一个对象
- 指向紧邻对象所占空间的下一个位置
- 空指针,意味着指针没有指向任何对象
- 无效指针,也就是上述情况之外的其他值
注: 试图拷贝或以其他方式访问无效指针的值都将引发错误,且编译器并不负责检查此类错误。尽管第2种和第3种形式的指针是有效的,但试图访问此类指针对象的行为不被允许。
利用指针访问对象
如果指针指向了一个对象,则允许使用 解引用符(操作符)* 来访问该对象。
int ival = 42;
int *p = &ival; // p存放变量ival的地址,或者说p是指向变量ival的指针
cout << *p; // 由符号*得到指针p所指的对象,输出42
对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值
*p = 0; // 由符号*得到指针p所指的对象,即可经由p为变量ival赋值
cout << *p; // 输出0
解引用操作仅适用于那些确实指向了某个对象的有效指针。
空指针
空指针内不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。定义空指针的方法如下:
int *p1 = nullptr; // 等价于 int *p1 = 0
int *p2 = 0; // 直接将p2初始化为字面常量0
// 需要西安 #include <cstdlib>
int #p3 = NULL; // 等价于 int *p2 = 0
得到空指针最直接的办法就是使用字面值 nullptr 来初始化指针,这也是C++11新标准刚引用的一种方法,它可以被转换成任意其他的指针类型。
void* 指针
void* 是一种特殊的指针类型,可用于存放任意对象的地址。利用void* 指针能做的事比较有限:拿它和别的指针比较,作为函数的输出和输入,或赋给另外一个void* 指针。不能直接操作void* 指针所指的对象因为并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。
换句话说,以void* 的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
指向指针的指针
指针式内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址在存放到另一个指针当中。
int ival = 1024;
int *pi = &ival; // pi指向一个int型的数
int **ppi = π // ppi指向一个int型的指针
通过* 的个数可以区分指针的级别。 解引用int型指针会得到一个int型的数,解引用指向指针的指针会得到一个指针。此时为了访问最原始的那个对象,需要对指针的指针做两次解引用。
指针的引用
指针是对象,所以存在对指针的引用。
int i = 42;
int *p; // p 是一个int型指针
int *&r = p; // r 是一个对指针p的引用
离变量名最近的符号(&)对变量的类型有最直接的影响,因此r是一个引用。声明符其余部分用以确定r引用的类型是什么。
注: 面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。
5 const限定符
默认状态下,const对象尽在文件内有效。
注: 如果想在多个文件之间共享const对象,必须在变量的定义和声明之前都添加extern关键字。
5.1 const的引用
初始化和对const的引用
引用的类型必须与其所引用对象的类型一致,但是有两个列外。
1.在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。
2.允许为一个常量引用绑定非常量的对象、字面值、甚至是个一般表达式。
int i = 42;
const int &r1 = i; // 允许将 const int&绑定到一个普通 int对象上
const int &r2 = 42; / 正确: r2是一个常量引用
const int &r3 = r1 * 2; // 正确: r1、r3都是常量引用
int &r4=r1 * 2; // 错误: r4是一个普通的非常量引用
const double &ri = i; // 正确: int可以转换为double类型
当一个常量引用被绑定到另外一种类型上时,为确保ri绑定一个整数,编译器会将上述代码解释成:
const int temp = dval; // 由双精度浮点数生成一个临时的整形常量
const int &ri = temp; // 让 ri 绑定这个临时量
5.2 const和指针
指向常量的指针
指向常量的指针 不能用于改变其所指对象的值。想要存放常量对象的地址,只能使用指向常量的指针。
const double pi = 3.14; // pi是个常量,它的值不能改变
double *ptr = π // 错误: ptr时普通指针
const double *cptr = π // 正确
*cptr = 42; // 错误: 不能给*cptr赋值
和常量引用一样,指向常量的指针也没有规定所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
const指针
常量指针必须被初始化,而且一旦完成初始化,则它的值(存放在指针中的地址)就不能被改变了。把* 放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意思,即不变的时指针本身而不是指向的那个值。
const double pi = 3.14;
const double *const pip = π // pip是一个指向常量对象的常量指针
注: 指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。
5.3 顶层const
指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。用名词 顶层const 表示指针本身是个常量,而用名词 底层const 表示指针所指的对象是一个常量。
更一般的,顶层const可以表示任意的对象是常量,底层const则与指针和引用等复合类型的基本类型部分有关。
指针类型既可以是顶层const也可以时底层const。
int i = 0;
int *const p1 = &i; // 不能改变p1的值,这是一个顶层const
const int ci = 42; // 不能改变ci的值,这是一个顶层const
const int *p2 = &ci; // 允许改变p2的值,这是一个底层const
const int *const p3 = p2; // 靠右的是顶层const,靠左的是底层const
const int &r = ci; // 用于声明引用的const都是底层const
注: 当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说非常量可以转换为常量。
顶层const执行拷贝操作并没有什么影响。
5.4 constexpr和常量表达式
常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
一个对象(或表达式)是不是常量表达式由他的数据类型和初始值共同决定。
const int max = 20; // 是
const in limit = max + 1; // 是
int size = 27; // 不是,数据类型不是const int
const inn sz = get_size(); // 不是,需要在运行时才能获取到具体值
constexpr变量
在复杂系统中,很难分辨一个初始值到底是不是常量表达式。C++11中规定,允许将变量声明为constexpr类型以便编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
constexpr int mf = 20; // 20是常量表达式
constexpr int limit = mf + 1; // mf + 1 是常量表达式
constexpr int sz = size(); // 当size是一个constexpr函数时,才正确
注:如果认定变量是一个常量表达式,那就把它声明为constexpr。
字面值类型
常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须所有限制。
尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储与某个固定地址中的对象。
注:如果constexpr声明中定义了一个指针,限定符constexpr仅对指针有效,对指针所指的对象无效。constexpr把它所定义的对象支撑了顶层const
5.5 const和constexpr
C++11 constexpr和const的区别详解
const 关键字,在实际使用中经常会表现出两种不同的语义。举个例子
void dis_1(const int x){
// 错误,x是只读的变量
array <int,x> myarr{1,2,3,4,5};
cout << myarr[1] << endl;
}
void dis_2(){
// 正确
const int x = 5;
array <int,x> myarr{1,2,3,4,5};
cout << myarr[1] << endl;
}
int main(){
dis_1(5);
dis_2();
}
可以看到,dis_1() 和 dis_2() 函数中都包含一个 const int x,但 dis_1() 函数中的 x 无法完成初始化 array 容器的任务,而 dis_2() 函数中的 x 却可以。
这是因为,dis_1() 函数中的“const int x”只是想强调 x 是一个只读的变量,其本质仍为变量,无法用来初始化 array 容器;而 dis_2() 函数中的“const int x”,表明 x 是一个只读变量的同时,x 还是一个值为 5 的常量,所以可以用来初始化 array 容器。
C++ 11标准中,为了解决 const 关键字的双重语义问题,保留了 const 表示“只读”的语义,而将“常量”的语义划分给了新添加的 constexpr 关键字。因此 C++11 标准中,建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。
6 处理类型
6.1 类型别名
类型别名是一个名字,它是某种类型的同义词,可以让复杂的类型名字变得简洁明了、易于理解和使用,有助于了解使用该类型的真实目的。
有两种方法可以定义类型别名。
1.传统方法,使用关键字 typedef
// 方法1
typedef double wages; // wages 是 double 的同义词
typedef wages base, *p; // base 是 double 的同义词, p是double*的同义词
含有typedef的声明语句定义的不再是变量而是类型别名
2.新标准规定了一种新的方法,使用 别名声明 来定义类型的别名
// 方法2
using SI = Sales_item; // SI是Sales_item的同义词
使用关键字using作为别名声明的开始,其作用是把等号左侧的名字规定成等号右侧类型的别名
指针、常量和类型别名
下面的声明语句用到了类型pstring,它实际上是类型char*的别名
typedef char *pstring;
const pstring cstr = 0; // cstr是指向char的常量指针
const pstring *ps; // ps是一个指针,它的对象是指向char的常量指针
pstring实际上是指向char的指针,因此,const pstring就是指向char的常量指针,而非指向常量字符的指针。
遇到一条使用了类型别的的声明语句时,容易错误地尝试把别类型别名替换成它本来的样子,以理解该语句的含义。
const char *cstr = 0; // 这是对const pstring cstr的错误理解
这种理解时错误的。前后两中声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
6.2 auto类型说明符
解决问题:编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型,但实际做到这一点并不容易,为解决这个问题,C++11引入了auto说明符,用它就能让编译器替我们去分析表达式所属类型。
auto让编译器通过初始值来推算变量的类型,所以,auto定义的变量必须有初始值。
// 由val1和val2相加的结果可以推断出item的类型
auto item = val1 + val2; // item初始化为val1和val2相加的结果
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样
auto i = 0, *p = &i; // 正确: i是整数、p是整形指针
auto sz = 0, pi = 3.14; // 错误: sz和pi的类型不一致
符合类型、常量和auto
编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。
-
使用引用其实是使用引用的对象,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto的类型。
-
auto一般会忽略顶层const,同时底层const则会保留下来。
-
如果希望退出的auto类型是一个顶层const,需要明确指出
const auto f = ci; // ci的推演类型是int,f是const int
6.3 decltype类型指示符
解决问题:希望从表达式的类型推断出要定义的变量类型,但是不想用表达式的值初始化,为满足这一央求,C++11引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。
在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
decltype(f()) sum = x; // sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型是const int
decltype(cj) y = x; // y的类型是const int&,y绑定到变量x
decltype(cj) z; // 错误: z是一个引用,必须初始化
// decltype的结果可以是引用类型
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // 正确:加法的结果是int,b是一个int
decltype(*p) c; // 错误: c是int&,必须被初始化
如果decltype使用的是一个不加括号的变量,得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当成一个表达式
// decltype 的表达式如果是加上了括号的变量,结果将是引用
decltype((i)) d; // 错误: d是int &,必须被初始化
decltype(i) e; // 正确: e是一个int