第二章 变量和基本类型
基本内置类型
基本算数类型:
类型 | 含义 | 最小尺寸 |
---|---|---|
bool |
布尔类型 | 8bits |
char |
字符 | 8bits |
wchar_t |
宽字符 | 16bits |
char16_t |
Unicode字符 | 16bits |
char32_t |
Unicode字符 | 32bits |
short |
短整型 | 16bits |
int |
整型 | 16bits (在32位机器中是32bits) |
long |
长整型 | 32bits |
long long |
长整型 | 64bits (是在C++11中新定义的) |
float |
单精度浮点数 | 6位有效数字 |
double |
双精度浮点数 | 10位有效数字 |
long double |
扩展精度浮点数 | 10位有效数字 |
如何选择类型
- 当明确知晓数值不可能是负数时,选用无符号类型;
- 使用
int
执行整数运算。一般long
的大小和int
一样,而short
常常显得太小。此外一旦超过了int
的范围,那么就选择long long
。 - 算术表达式中不要使用
char
或bool
,只有字符和布尔值才用,因为char
在一些机器上是有符号的,另一些机器上是无符号的,如果使用char
进行运算很容易出现问题,如果非要用char
进行运算,那么使用signed char
或者unsigned char
- 浮点运算尽量用
double
类型转换
- 非布尔型赋给布尔型,初始值为0则结果为false,否则为true。
- 布尔型赋给非布尔型,初始值为false结果为0,初始值为true结果为1。
- 把浮点数转换成整数类型时候,将会舍去小数部分,而保留整数部分
- 把整数类型转换成浮点数时候,小数部分记0,如果整数所占的空间超过了浮点类型的容量,那么可能导致精度损失
- 给无符号一个超过访问的数值,那么将会保留取模后的余数,给一个带符号的类型赋超过表示范围的值时,结果是未定义的
字面值常量
- 一个形如
42
的值被称作字面值常量(literal)。- 整型和浮点型字面值,特别的,十进制的字面值不会是负数,负号只是作为取负值而已
- 字符和字符串字面值。
- 字符字面值:单引号,
'a'
- 字符串字面值:双引号,
"Hello World"
,此外,字符串字面值的类型实际上是由常量字符构成的数组 - 如果多个字符串紧邻且仅由空格、缩进和换行符分割,则他们实际上是一个整体,如:
std:cout<<"wow, a really, really long string" "literal that spans two lines" <<std::endl;
- 字符字面值:单引号,
- 转义序列。换行符:
\n
、横向制表符\t
,回车符\r
,等。 - 布尔字面值。
true
,false
。 - 指针字面值。
nullptr
特别的: 字符串型实际上时常量字符构成的数组,结尾处以
'\0'
结束,所以字符串类型实际上长度比内容多1。
变量
变量提供一个具名的、可供程序操作的存储空间。 C++
中变量和对象一般可以互换使用。
变量定义(define)
- 定义形式:类型说明符(type specifier) + 一个或多个变量名组成的列表。如
int sum = 0, value, units_sold = 0;
- 初始化(initialize):对象在创建时获得了一个特定的值。
- 切记:初始化不是赋值!:
- 初始化 = 创建变量 + 赋予初始值
- 赋值 = 擦除对象的当前值 + 用新值代替
- 列表初始化:使用花括号
{}
,如int units_sold{0};
- 默认初始化:定义时没有指定初始值会被默认初始化;在函数体内部的内置类型变量将不会被初始化。
- 建议初始化每一个内置类型的变量。
变量的声明(declaration) vs 定义(define)
- 为了支持分离式编译,
C++
将声明和定义区分开。声明使得名字为程序所知。定义负责创建与名字关联的实体。声明规定了变量的类型和名字,在这一点上定义与之相同,但除此之外,定义还申请了存储空间,也可能会为变量赋一个初始值 - extern:声明一个变量而非定义一个变量(切记不可赋初始值,不然即使使用了
extern
也算作是定义) - 只声明而不定义: 在变量名前添加关键字
extern
,如extern int i;
。但如果包含了初始值,就变成了定义:extern double pi = 3.14;
- 变量只能被定义一次,但是可以多次声明。定义只出现在一个文件中,其他文件使用该变量时需要对其声明。
- 名字的作用域(namescope)
{}
- 第一次使用变量时再定义它。
- 嵌套的作用域
- 同时存在全局和局部变量时,因为全局作用域没有名字,所以可用
::reused
显式访问全局变量reused - 但是用到全局变量时,尽量不适用重名的局部变量。
- 同时存在全局和局部变量时,因为全局作用域没有名字,所以可用
变量命名规范
- 需体现实际意义
- 变量名用小写字母
- 自定义类时用大写字母开头:Sales_item
- 标识符由多个单词组成,中间须有明确区分:student_loan或studentLoan,不要用studentloan。
左值和右值
- 左值(l-value)可以出现在赋值语句的左边或者右边,比如变量;
- 右值(r-value)只能出现在赋值语句的右边,比如常量。
复合类型
引用
一般说的引用是指的左值引用
- 引用:引用是一个对象的别名,引用类型 引用(refer to)另外一种类型。用
&
定义引用类型,如int &refVal = val;
。 - 引用必须初始化。
- 引用和它的初始值是绑定bind在一起的,而不是拷贝。一旦定义就不能更改绑定为其他的对象
- 引用即别名,它只是一个已经存在的对象所起的另外一个名字,所以给一个引用 赋值就是给它所绑定的变量赋值,同理,获取引用的值,实际上是获取 它绑定的变量的值,再者,以引用为初始值,就是以 它绑定的变量为初始值
指针
int *p; //指向int型对象的指针
是一种
"指向(point to)"
另外一种类型的复合类型。定义指针类型:
int *p;
,从右向左读有助于阅读,p
是指向int
类型的指针。指针存放某个变量的地址。
获取对象的地址,使用
&
:int i=42; int *p = &i;
。&
是取地址符,读音:and,它其实是第27个字母,hhh指针的类型与所指向的对象类型必须一致(均为同一类型int、double等)
指针的值的四种状态:
- 1.指向一个对象;
- 2.指向紧邻对象的下一个位置;
- 3.空指针;
- 4.无效指针。
-
对无效指针的操作均会引发错误,第二种和第三种虽为有效的,但理论上是不被允许的
指针访问对象:
cout << *p;
输出p指针所指对象的数据,*
是解引用符,读音:star,也可读:星号空指针不指向任何对象。使用字面值
nullptr
来初始化指针,如int *p=nullptr;
,当然也可以使用预处理变量NULL
,这个变量在cstdlib
中定义,它的值就是0-
指针和引用的区别:引用本身并非一个对象,引用定义后就不能绑定到其他的对象了;指针并没有此限制,相当于变量一样使用。
-
赋值语句永远改变的是左侧的对象。
-
任何非0指针对应的条件值都是true
void*
指针可以存放任意对象的地址。因无类型,不能操作对所指对象其他指针类型必须要与所指对象严格匹配。
两个指针相减的类型是
ptrdiff_t
。建议:初始化所有指针。
特别的,
int* p1, p2;//*是对p1的修饰,所以p2还是int型
引用不是对象,所以不能定义指向引用的指针,但是指针是对象,所以可以定义指向指针的引用,例如:
int *p = nullptr; int *&r = p
特别的,遇见这种比较复杂的声明语句时,从右往做阅读更有效
const限定符
- 动机:希望定义一些不能被改变值的变量。
初始化和const
- const对象必须初始化,且不能被改变。
- const变量默认不能被其他文件访问,非要访问,必须在指定const定义之前加extern。要想在多个文件中使用const变量共享,定义和声明都加const关键字即可。
const的引用
- reference to const(对常量的引用):指向const对象的引用,如
const int ival=1; const int &refVal = ival;
,可以读取但不能修改refVal
。 - 常量引用可以绑定非常量的对象,字面值,甚至是一个表达式,反之不可,因为常量引用属于底层
const
,如:
int i = 42;
const int &x = i; //允许将常量引用绑定到一个普通int对象上
int &y = x * 2; //错误,y是一个普通的非常量引用,不可绑定到常量引用上
- 临时量(temporary)对象:当编译器需要一个空间来暂存表达式的求值结果时,临时创建的一个未命名的对象。
- 对临时量的引用是非法行为。
指针和const
- pointer to const(指向常量的指针):不能用于改变其所指对象的值, 如
const double pi = 3.14; const double *cptr = π
指向常量的指针,由于有常量的性质,因而也可以指向非常量对象,故而指针可以不和所指向的类型一致 - const pointer:指针本身是常量,也就是说指针固定指向该对象,(存放在指针中的地址不变,地址所对应的那个对象值可以修改)如
int i = 0; int *const ptr = &i;
- 区分二者很简单,当
const
和*
一同修饰一个变量时,只要看哪个符合和变量更接近,那么便属于哪一种,如
const int *p = nullptr; //属于指向常量的指针
int * const q = nullptr; //属于常量指针
const int * const ptr = nullptr; //属于指向常量的常量指针
- 切记,常量指针具有常量的性质,所以一定要初始化
顶层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
则不受影响,如:
i = ci; //正确,因为ci是顶层const
p2 = p3; //正确,因为p2和p3都是底层const
- 牢记,赋值是赋值,引用是引用,赋值是计算,可以转换,但是引用是绑定
- 特别的,虽然要求要有相同的底层
const
,但是如果两个对象的类型可以转换也是允许的,一般来说,非常量可以转换成常量,反之不行,当然,顶层可以赋值给非常量变量,如:
int *p = p3; //错误,p3包括底层const
p2 = &i; //正确,int* 可以转换成const int *
int &r = ci; //错误,普通的int &不能绑定到const int 上
const int &r2 = i; //正确,const int &可以绑定到普通int上
constexpr
和常量表达式(▲可选)
constexpr
修饰的指针是顶层const
- 常量表达式:指值不会改变,且在编译过程中就能得到计算结果的表达式,用常量表达式初始化
const
对象也是常量表达式,如
const int a = 2; //是
const int b = a + 1; //是
int c = 27; //不是
C++11
新标准规定,允许将变量声明为constexpr
类型以便由编译器来验证变量的值是否是一个常量的表达式,声明为constexpr
的变量一定是一个常量,而且必须用常量表达式初始化- 常量表达式的值要在编译阶段就得到计算,因此对于这些
constexpr
能声明的类型称之为字面值类型
。算术类型,引用和指针都属于字面值类型。自定义类、IO库、string类型则不属于字面值类型也就不能被定义为constexpr
。尽管指针和引用都能定义成constexpr
但是他们的初始值都受到严格的限制,如指针只能被初始化为0或nullptr`或者某个固定地址的对象(静态变量)。
处理类型
类型别名
- 传统别名:使用typedef来定义类型的同义词。
typedef double wages;
C++11)
新标准别名:别名声明(alias declaration):using SI = Sales_item;
SI是sales_item的别名
// 对于复合类型(指针等)不能代回原式来进行理解
typedef char *pstring; // pstring是char*的别名
const pstring cstr = 0; // 指向char的常量指针
const char *cstr = 0; //不正确,现在为指向const char的指针
auto类型说明符 c++11
- auto类型说明符:让编译器
通过初始值
自动推断类型,所以auto变量必须有初始值
- 一条声明语句只能有一个数据类型,所以一个auto声明多个变量时只能相同的变量类型(包括复杂类型&和*)。
auto sz = 0, pi =3.14//错误
int i = 0, &r = i; auto a = r;
推断a
的类型是int
。- 会忽略
顶层const
,如果希望是顶层const需要自己加const
,底层const
则会被保留 - 定义
auto
类型的引用时,初始值的常量属性也会被保留 const int ci = 1; const auto f = ci;
推断类型是int
- 一些例子:
int i = 0.&r = i;
auto a = r; //a是整数
const int ci = i, cr = ci;
auto b = ci; //b是一个整数,ci的顶层const被忽略
auto c = cr; //c是一个整数,同样忽略了顶层const
auto d = &i; //d是一个整形指针
auto e = &ci; //e是指向整数常量的指针,即底层const(对常量变量取地址是一种底层const)
auto &g = ci; //g是整形常量引用,绑定到ci
auto &h = 42; //错误,不能为非常量引用绑定字面值,因为常量引用算底层const
const auto &j = 42; //正确,可以为常量引用绑定字面值
auto &n = i, *p2 = &ci; //错误,一条语句只能有一个基本数据,显然i是整型,ci是const int,即顶层const,类型不同
decltype类型指示符
- 从表达式的类型推断出要定义的变量的类型,在此过程中会得到变量的类型而不去计算值
- decltype:选择并返回操作数的数据类型。
decltype(f()) sum = x;
推断sum
的类型是函数f
的返回类型,这里并不会调用函数f- 不会忽略
顶层const
。 - 如果对变量加括号,编译器会将其认为是一个表达式,如int i–>(i),则decltype((i))得到结果为int&引用。
- 特别的,引用一直被视为是绑定变量的同义词,只有在
decltyoe
是例外,如
const int ci = 0, &cj = ci;
int i = 42, &r = i;
int *p = &i;
decltype(cj) x = 0; //x的类型是const int
decltype(cj) y = x; //y的类型是const int&,y绑定到变量x
decltype(cj) z; //错误,z是一个引用,必须初始化
- 因为
r
是一个引用,所以decltype(r)
的结果是引用类型。如果想让表达式类型是r
所绑定到类型,可以把r
作为表达式的一部分,如r +0
,因此decltype(r + 0)
的结果是一个具体的值,而非引用 - 此外,如果表达式的内容是解引用操作,则
decltype
将得到引用类型,也就是说,decltype(*p)
的类型是int
类型的引用而非int
在《c++ primer习题集第五版》练习2.37中提到:赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型。也就是说,如果 i 是 int,则表达式 i=x 的类型是 int&。所以,以下的例子:
d 是 int& 类型
自定义数据结构
struct
尽量不要吧类定义和对象定义放在一起。如
struct Student{} xiaoming,xiaofang;
- 类可以以关键字
struct
开始,紧跟类名和类体。 - 类数据成员:类体定义类的成员。
C++11
:可以为类数据成员提供一个类内初始值(in-class initializer)。
编写自己的头文件
- 头文件通常包含哪些只能被定义一次的实体:类、
const
和constexpr
变量。
预处理器概述:
- 预处理器(preprocessor):确保头文件多次包含仍能安全工作。
- 当预处理器看到
#include
标记时,会用指定的头文件内容代替#include
- 头文件保护符(header guard):头文件保护符依赖于预处理变量的状态:已定义和未定义。
#indef
已定义时为真#inndef
未定义时为真- 头文件保护符的名称需要唯一,且保持全部大写。养成良好习惯,不论是否该头文件被包含,要加保护符。
#ifndef SALES_DATA_H //SALES_DATA_H未定义时为真
#define SALES_DATA_H
strct Sale_data{
...
}
#endif