/*基本内置类型: 算术类型 空类型
位/比特(bit): 0/1
字节byte: 8个二进制位(计算机存储信息的基本单位)即 8 比特/位
字word: 若干个字节组成一个字(可以存放一条计算机指令或数据)
字长word size: CPU中每个字包含的二进制的长度(即位数)
位/比特bit: 0/1 --> 字节 byte(8比特:00000000)--> 字 (若干字节)
算术类型:整型( 包含字符和布尔类型在内)和 浮点型
C++算术类型:
类型 含义 最小尺寸 main返回0表示成功
Bool 布尔类型 未定义 真转换成1 假转换成0
Char 字符 8位 即8个二进制位 一个字节
Wchar_t 宽字符 16位
Char16_t Unnicode字符 16位
Char32_t Unicode字符 32位
Short 短整型 16
Int 整型 16
Long 长整型 32
Long long 长整型 64
Float (6位有效数) 单精度浮点数 6位有效数字
Double 双精度浮点数 10位有效数
Long double 扩展精度浮点数 10
基本的字符类型是char 一个char的空间应该确保可以存放机器基本字符集中任意字符对应的数字值。也就是说, 一个char的大小和一个机器字节一样
其他字符类型用于扩展字符集:Wchar_t 类型用于确保可以存放机器最大扩展字符集中任意一个字符
Char16_t Char32_t 则为 Unicode字符集服务(Unicode是用于表示所有自然语言中字符的标准)
除了字符和布尔类型外,其他整型用于表示不同尺寸的整数。 C++规定一个int至少和一个short一样大,一个long至少和一个int一样大, 一个long long至少和一个long一样大。
Long long是 新标准定义的。 short <=int <=long <= long long
内置类型 的机器实现
除了布尔型和 扩展的字符型之外, 其他整型可分为 带符号的signed 和 不带符号的 unsigned两种。 带符号的表示 正数 负数 或0, 无符号的类型则仅表示大于等于0 的值。
类型 int、 short 、 long 和long long都是带符号的,通过在类型名前添加unsigned就可以表示无符号类型: unsigned long, unsigned int可以缩写为unsigned
与其他整型不同, 字符型分为: char signed char unsigned char ,必须特别注意: 类型char 和 signed char 并不一样。尽管字符型有三种,但字符的表现形式却只有两种: 带符号和无符号的。 类型char 实际上会表现为上诉两种形式中的一种,具体是哪种 由编译器决定。
无符号类型中所有比特都用来存储值,例如 8比特的 unsigned char表示0 至 255区间内的值。
C++标准没规定带符号类型应该如何表示,但是约定了在表示范围内正值和负值的量应该平衡(表示范围对称)。 8比特的 signed char 理论上可以表示 -127 至 127区间内的值。
建议:如何选择类型: 一些选择类型 的经验:
当明确知晓数值不可能为负时, 选用 无符号类型
使用int执行整数运算。 :实际应用中 short显得太小,而long一般和int有一样的尺寸。 如果数值超过了int的表示范围, 选用long long
算术表达式中不要使用char 或bool, 只有在存放字符或布尔值时才使用。
执行浮点数运算用double 因为float通常精度不够,且单精度浮点数和双精度的计算待机相差无几。
类型转换: 对象的类型定义 了对象能包含的数据和 能参与的运算,其中一种运算被大多数类型支持, 就是将对象从一种给定的类型转换 convert成另一个相关类型。
当程序在某处我们使用了一种类型而实际对象应该去另一种类型时, 程序会自动进行类型转换。
给一个类型对象强行赋予另一种类型的值时,会发生什么?::
Bool b = 42; b位真 即b 为1. 非0时bool为1(true),0时bool才为0(false).
Int i = b; i 的值为1;
i = 3.14; i的值为3 double和float自动转换成int
Double pi = i; pi 的值为3.0 int自动转换成double时 是带小数位的。
Unsigned char c= -1; 假设char 占8比特 c 的值为255
Signed char c2 = 256; 假设 char占8比特, c2的值是未定义的。
类型所能表示的值的范围决定了转换的过程:
当我们把一个非布尔类型的算术值赋给布尔类型时, 初始值为0则结果为false
否则结果为true
当我们把一个布尔值赋给非布尔类型时, 初始值为false则结果为0, 初始值为true则结果为1
当我们把一个浮点数赋给整数类型时,进行了近似处理。 结果值将仅保留浮点数中小数点之前的整数部分
当我们把一个整数赋给浮点数时, 小数部分记为0,如果该整数所占空间超过了浮点数类的容量, 精度可能会损失。
//当我们赋给无符号类型一个超过他表示的范围值时,结果是初始值对无符号类型表示数值总数取模后的余数。 例如 8比特大小的unsigned char可以表示0到256区间的值, 如果我们赋了一个区间之外的值, 则实际的结果是该值对256取模后所得的余数。 所以 -1 赋给 8比特大小的unsigned char的结果是 255
当我们赋给带符号类型一个超过他表示范围的值时, 结果是未定义的 undefined。 程序可能继续工作,可能崩溃 或者其他
避免无法预知和依赖实现环境的行为:p33
含有无符号类型的表达式::尽管我们不会故意给无符号对象赋一个负值, 却可能容易写出这样的代码。 例如: 一个算术表达式里既有无符号数又有int值时,int值就会转换成无符号数。把int转换成无符号数的过程和 吧int直接赋给无符号变量一样。
Unsigned u= 10;
Int i = -42; i是带符号的
Std::cout<< i+i <<std::endl; 输出 - 84
Std::cout<< u + i<<std::endl; 如果int是32位,输出 4294967264
第二个表达式在相加前首先把整数-42转换成无符号数。 把负数转换成无符号数类似于直接给无符号数赋一个负值,结果等于这个负数加上无符号数的模。
当从无符号数unsigned 中减去一个值时, 不管这个值是不是无符号数,我们都必须确保结果不能是一个负值:
Unsigned u1 = 42, u2 = 10;
Std::cout << u1 - u2<<std::endl; 输出32
Std::cout<< u2 - u1 <<std::endl; 正确 不过 结果是取模后的值
无符号数不会小于0,这一事实同样关系到循环的写法:
第11页中需要些一个循环loop, 通过控制变量递减的方式把从10到0 的数字降序输出。 这个循环可能类似于下面的形式:
For ( int i = 10; i>=0; - -i)
Std::cout<< i <<std::endl;
可能你会觉得反正不打算输出负值, 可以用无符号数unsigned来重写这个循环。 然而,这个不经意的改变却意味着死循环:
// 错误 :无符号数不会小于0,变量U永远不会小于0, 循环条件一直成立
For( unsigned u = 10; u >=0; - -u)
Std::cout<< u <<std::endl;
来看看当u等于0时会发生什么,这次迭代输出0, 然后继续执行for语句里的表达式。
表达式- -u从u 中减去1,结果是 - 1并不满足无符号数的要求,此时像所有表示范围之外的其他数字一样, -1会自动转换成一个合法的无符号数。 假设int类型占32位, 则当u等于0时,- -u的结果将会是 4294967295。
如果非要用unsigned来写这个循环,一个解决办法是,用while语句代替for语句, 因为前者让我们能在输出变量之前(而非之后)先减去1:
Unisgned u= 11; // 确定要输出的最大数,从比他大1 的书开始
While( u > 0)
{
- -u; // 先减去1, 这样最后一次迭代就会输出0
Std::cout<< u << std::endl;
}
这样改写后的循环先执行对循环控制变量减1的操作, 这样最后一次迭代时,进入循环的u 值是1,。此时将其减1, 则这次迭代输出的数就是0; 下一次再检验循环条件时,u的值等于0而无法再进入循环。 因为我们要先做减1的操作, 所以初始u 的值应该比要输出的最大值大1. 这里u 的初始化为11, 输出的最大数是10。
提示: 切勿混用带符号和 无符号类型:
如果表达式里既有带符号类型又有 无符号类型时, 当带符号类型取值为负时会出现异常结果, 这是因为带符号数会自动地转换成无符号数。
带符号和不带符号类型:signed可表示正数 负数 0 。unsigned 只表示大于0 的数
字符和字符串字面值: 一个字符成为char 型字面值
字符串字面值的类型实际上是由常量字符构成的数组 array 以一个空字符结尾。
类型转换。
字面值常量:每个字面值常量对应一种数据类型, 字面值常量的形式和值决定了他的数据类型:整型和浮点型字面值, 字符和字符串字面值,布尔字面值和指针字面值
变量:即对象:是具有某种数据类型的内存空间。
在使用对象这个词时, 不严格区分是类还是内置类型,也不区分是否命名或只读
// 初始化初始值: 当对象被创建的同时被赋予一个特定的值,叫初始化。 初始化变量的值可以是任何复杂的表达式。
// 初始化和赋值是两种完全不同的操作: 初始化不是赋值,初始化的含义是在创建变量时赋予一个初始值,而赋值的含义是吧对象当前的值擦除,用一个新值来代替。
列表初始化:用花括号来初始化变量
默认初始化: 如果定义变量时没有被指定初值,则变量被默认初始化,被赋予一个默认值。若内置类型未被显式初始化,则其默认值由位置决定:定义于
任何函数体之外的变量被初始化为0, 定义于函数体内的未被初始化的变量其值是未定义的,访问此类值会引发错误。
类的对象若没有被显示初始化, 则其值由类确定: 类内初值 或其他
对于内置类型, 当用列表初始化 并且 可能存在丢失信息的风险时,编译器会报错:
Long double ld= 3.1415926;
Int a{ld} , b= {ld}; 错误, 转换 不执行, 因为存在丢失信息的风险, 用花括号来列表初始化。
Int a(ld), d = ld; 正确 转换执行, 并且确实丢失了部分值。注意用的是圆括号()和 赋值=
// 声明和定义的关系: C++ 支持分离式编译,将定义和声明区分开来。声明使得名字被程序所知,一个文件若想使用别处定义的名字,就必须包含对这个名字的声明;
// 变量只能被定义一次,但可以声明多次。就是说变量定义有且只能出现在一个文件中, 其他用到该变量的文件就必须对其进行声明,决不能重新定义。
在一个文件中只是声明变量,可以加上 extern 外部的:
extern int i; 声明
int j; 定义
任何包含了 显式初始化的声明即成为定义,所以extern 语句包含初始值就不再是声明,而是定义了。
Extern double pi = 3.14; 定义。
静态类型: C++是一种静态类型语言, 就是 在编译阶段检查类型。 检查类型的过程称为类型检查。所以编译器负责类型检查。
标识符: 字母 数字 下划线组成。
// 不能连续两个下划线__ 不能下划线紧邻着大写字母开头。在函数体外的标识符不能以下划线开头。
规范的变量命名可以有效提高程序的可读性:
标识符要能体现实际含义
变量名一般用小写字母,不要用大写
用户自定义的类名一般用大写字母开头。 Sale_item
// 如果标识符由多个单词组成,则单词间应该有明显区分: student_loan 或 studetLoan 不要使用studentloan
名字的作用域: C++中作用域都用花括号分隔; 同一个名字在不同的作用域中 可能指向不同 实体。名字的有效区域始于名字的声明语句, 以声明语句所在作用域末端为结束。
全局变量定义于所有花括号之外的变量拥有全局作用域,在整个程序的范围内都可以使用。
局部变量定义于花括号内的变量拥有块作用域,在函数体内可以访问,出来函数体不可访问。
建议: 当第一次使用变量的时候再定义它。
嵌套的作用域: 作用域中一旦声明了某个名字, 它所嵌套着的所有作用域都可以访问该名字。而且同时,可以在内层作用域中重新定义在外层作用域中已经有的名字, 就隐藏了外层作用域中同名的变量。
复合类型:
引用: reference 引用只是为对象起了另外一个名字。引用不是对象。引用不能绑定到字面值和一个表达式的计算结果。引用类型要与绑定的对象类型严格匹配。
(C++11新增加右值引用 rvalue reference , 当我们使用术语 引用 reference 时,指的其实是左值引用 lvalue reference )
除了两种例外:55页 初始化常量引用时,允许一个常量引用绑定到非常量对象、字面值甚至一个表达式。
534页 存在继承关系的类是一个重要例外: 我们可以将一个基类的指针或引用绑定到一个派生类对象上。例如 我们可以用一个Quote& 指向一个Bulk_quote对象, 也可以把一个Bulk_quote 的地址赋给一个Quote*
一般在初始化时,初始值会被拷贝到新建的对象中。 但是定义引用时, 程序会把引用和初始值绑定在一起,
而不是拷贝给引用, 一旦绑定完成, 引用就和他的初始值对象绑定在一起, 不能再重新绑定到另一个对象,所以引用必须初始化。
使用引用其实就是 使用引用绑定的对象, 所以给引用赋值即是给绑定的对象赋值, 用引用给其他对象赋值就是用绑定的对象的值给其他对象赋值。
指针: pointer
与引用的不同点: 指针是一个对象。可以赋值和拷贝。 2 不需定义时赋值。
指针存放对象的地址, 用取地址符&获取对象地址 &ival , 引用不是对象,没有实际地址, 所以不能定义指向引用的指针。
指针类型要和它指向的对象类型严格匹配
除了两种例外:
56页 和引用一样, 可以令一个指针指向常量或非常量,类似于常量引用,指向常量的指针不能用于改变其所指对象的值,要想存放常量对象的地址,只能用指向常量的指针。。
两个例外:允许一个指向常量的指针指向一个非常量对象,但不能通过此指针改变对象的值。。和常量引用一样, 指向常量的指针没有规定其所指的对象必须是一个常量对象。所谓指向常量的指针
仅仅要求不能通过该指针改变对象的值, 而没有规定对象的值不能通过其他途径改变。用解引用符* 来访问、取得指针所指的对象。 可以像使用对象本事一样使用解引用后的指针 *p ,所以 ,给解引用对象赋值,实际就是给指针所指对象赋值。
解引用操作仅适用于 那些确实指向了某个对象的有效指针。
// 空指针:不指向任何对象。 在试图使用一个指针之前 代码可以首先检查它是否为空。
int *p1 = nullptr; 得到空指针最直接的办法就是用字面值nullptr来初始化指针。
int *p2= 0;
// 建议: 初始化所有指针。
预处理变量:过去还会用一个名为NULL的预处理变量给指针赋值, 这个变量在头文件cstdlib中定义,其值就是0;
预处理器: 是运行在编译过程之前的一段程序。 预处理变量不属于命名空间std, 它由预处理器管理,因此我们直接使用预处理变量而不需要在他前面加上std::
预处理变量无视C++语言中关于 作用域的规则。
赋值和指针:
引用一旦绑定后, 之后使用的都是最初绑定的那个对象。 而指针是一个对象, 可以被赋值。
int i= 42;
int *pi =0; 空指针
int *pi2= &i; pi2 被初始化, 存有i的地址。
int *pi3; 若pi3定义于块内 即函数花括号内, 则其值无法确定
pi3=pi2; 现在指向同一对象。
pi2=0; 现在pi不指向任何对象
有时候 要想搞清楚一条赋值语句到底是改变了指针的值还是改变了指针所指对象的值 不太容易, 最好的办法就是记住 赋值永远改变的是 等号左侧的对象。
Pi = &ival; pi被赋予新值, 即改变了存放在pi内的地址值, 现在pi指向了 ival
*pi = 0; ival的值变为0;
其他指针操作:
指针可以作为判断条件,和算术值作为条件一样: 指针的值为0 则为false 任何非0 指针都是true 如果两个指针地址相同,则他们== 否则!=
int ival = 1024;
Int *pi =0; 空指针 pi 的值为0
Int *pi2 = &ival;
If(pi) pi的值为0 所以 false
。。。。。。
If(pi2) pi2指向ival 所以 pi2的值不是0 为 true。
判断指针是否指向合法的对象, 只有指针p 作为if的条件即可,若p值为0或nullptr 则为false 否则 为真
void*指针: 可以存放任意对象的地址。 不能直接操作void*所指的对象,没办法访问内存空间所存的对象。 因为我们不知道该对象的类型,无法确定在对象上做哪些操作。Void* 所做的事比较有限。
理解复合类型的声明: 变量的定义包含一个基本数据类型和 一组声明符 int double 是基本数据类型,* & 是类型修饰符(距离哪个变量最近修饰谁), *p &r是声明符, 。
int* p1, p2; 基本数据类型是int, * 仅修饰了p1, p2是 int
Int *&r = p; 理解复合类型 最简单的方法是: 从右向左阅读r的定义, 离变量最近的符号对变量的类型有最直接的影响。 所以r是一个引用, 声明符其他部分说明
r引用的类型, * 说明r引用的是指针, int说明 r引用的是一个int 指针。
指向指针的指针:
int ival= 1024;
int *pi = &ival;
int **ppi = π ppi 指向一个int 型的指针。
// 解引用一个指向指针的指针得到一个指针 两次解引用ppi 才得到原始所指的对象。 **ppi
指向指针的引用:
int i =42;
int *p; 如果在块作用域内, 其值不确定。
int *&r=p; //r是指针p 的引用; r是指针p 的别名, r是引用,所以必须用 //指针类型p来初始化引用r。
r=&i; p指向i 给r赋值就是给p赋值
*r=0; 解引用r得到i, 就是p所指的对象, 将i 的值改为0
const 限定符: 防止程序修改某个值
定义一个变量, 它的值不能被改变,所以必须初始化(引用一旦绑定对象,就不能改变绑定,所以必须初始化),初始值是任何表达式。 任何给const变量赋值行为都会引发错误,即使是赋予相同的值。
Const int i =get_size(); // 用表达式为常量i 初始化。
只能在const对象上执行不改变其内容的操作。例如初始化, 利用一个对象初始化另一个对象时, 他们是不是const都无关紧要:
int i= 42;
const int ci= i; i 是int 不是const int,
int j= ci; ci 可以初始化另一个对象。虽然ci是 const int, ci 拷贝给j, 一旦拷贝完成,新对象就和原来对象没有什么关系了。
// 编译器在编译过程中,把用到的该变量的地方都替换成对应的值。 为了替换,需要在每个文件中都定义const变量,为了避免重复定义,默认情况下,
const对象被设定为仅在文件内有效。 当多个文件中出现同名的const变量时,等同于在不同文件中分别定义了独立的变量。
// 为了使得const变量想其他变量一样, 一次定义,多次声明, 不管定义还是声明都在前面加上 extern 关键字。这样只需要定义一次就行了。
extern const int bufSize= fcn(); 在源文件 file.cc 中定义,是常量,必须用extern来限定使其能被其他文件使用。
extern const int bufSize; 在头文件 file_1.h头文件中声明。此处extern说明bufSize不是独有, 其定义在别处出现。
const的引用称为: 对常量的引用,与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象的值。
const int ci= 1234;
const int &r1 =ci; r1是对常量ci 的引用。
r1 =42; 不能通过对常量的引用来修改绑定的对象
int &r2=ci; 一个非常量引用不能绑定到一个常量对象上,只能绑定到非常量对象上。但常量引用可以绑定一个非常量对象或常量对象
引用的对象是常量还是非常量可以决定其所能参与的操作, 但不影响引用和对象的绑定关系。
// 初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其允许一个常量引用绑定到非常量对象、字面值甚至一个表达式(但是一个普通引用不能绑定到字面值和表达式的计算结果):必须认识到: 常量引用仅对引用可参加的操作进行限定,即不能通过他修改对象。// 但是对于对象是不是常量没有限定,而且我可以通过其他途径修改非常量对象。//
int i=42;
const int &r1=i; r1是个 常量引用,可以绑定到非常量上
const int &r2=42; 常量量引用可以绑定到 字面值
const int &r3= r1*2; 常量引用可以 绑定 表达式
int &r4= r1*2; 错 r4是一个普通的非常量引用,不能绑定到字面值或表达式
Const int &r4 = r1*2; 如果换成常量引用就没问题了
Double dval = 3.14;
Const int &ri = dval; // 将dval 隐式转换为一个 int类型的临时量。
编译器把上诉代码变成:
Const int temp = dval; 双精度浮点苏生成一个临时的整型常量
Const int &ri = temp; ri绑定 临时量 temp
临时量对象就是 当编译器需要一个空间来 暂存表达式的求值结果时临时创建的一个未命名的对象。 一般称这个临时量对象 为 临时量。
正如上面所诉, 常量引用可以绑定到 非常量、 字面值、 或一个表达式,常量引用就是常量本身(因为引用就是个别名), 常量不能参与改变自身内容的操作, 所以常量引用只对引用可参与的操作做了限定, 对于常量引用所引用的对象本身是不是常量未做限定,允许通过其他途径改变,
int i=42;
int &r1=i;
const int &r2=i; r2是个常量引用。
r1=0;
r2=0; 错 r2是一个常量引用。不能通过r2修改绑定的对象, 指向常量的指针也是这样的。
指向常量的指针: 不能修改所指向的对象的值。要想存放常量对象的地址,只能用指向常量的指针。
指针类型必须与所指对象的类型匹配: 除了两种例外: 1 指向常量的指针可以指向一个非常量对象。
和常量引用一样, 指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值, 而没有规定那个对象的值不能通过其他途径改变。
const double pi= 3.14;
double *ptr=π 错 要想存放常量对象的地址,只能用指向常量的指针。
const double *cptr=π
*cptr = 42; 错 指向常量的指针: 不能用于修改所指向的对象的值。
const 指针: 把指针本身定义为常量。常量指针必须初始化,一旦初始化值就不能再改变。
把int*放在const关键字之前来说明指针是一个常量,含义是:不变的是指针本身的值而不是指向的那个值。
int errNum=0;
int *const curErr = &errNum; curErr是一个常量指针,一直指向errNumb,离curErr最近的是const,意味着curErr本身是一个常量对象,其类型由声明符的其余部分确定,*说明是一个常量指针,基本数据类型int说明是一个int说明 常量指针指向的是int对象。
const double pi =3.14159;
const double *const pip= π pip 是一个指向常量对象的常量指针,两个const
顶层const、底层const: 指针本身是不是常量以及指针所指的是不是一个常量是两个相互独立的问题,用名词顶层const (top_level const)表示指针本身是常量, 用名词底层const (low_level 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, p2指向常量的指针(存放常量地址只能用指向常量的指针)
const int *const p3= p2; p3是指向常量的常量指针。 第一个const 是底层 第二个是 顶层
const int &r=ci; 用于声明引用的const都是底层
当执行 拷贝操作时,常量是顶层const还是底层const区别明显:
当拷贝时,顶层const 是不受影响的;
i = ci; 对 拷贝ci , ci是顶层const, 用常量ci给int变量赋值。常量可以用于赋值。
P2 = p3; 对 赋值后指向的对象类型相同, p3的顶层const的部分不影响。二者都有底层const。
底层const的限制不能忽略,当拷贝对象时, 拷入和拷出的对象必须具有相同的底层const资格,或两个对象的数据类型必须能够转换:一般非常量可以转换成常量。反之不行。
int *p= p3; 错 p3包含底层const(第一个)含义, p 没有,
p2=p3; 对 p2 p3都有底层const含义 p3的顶层const不影响
p2= &i; 对 int *可以转换成const int*, &是取地址符,&i表示地址
int &r= ci; 错 普通引用int& 不能绑定到 int常量上,但是常量引用(const int &)可以。
const int &r=ci; 对,
const int &r2= i; 常量引用 const int& 可以绑定到一个普通的int上。常量引用和指向常量的指针对于绑定或指向的对象是否是常量没有限定,只是不能通过自身改变所绑定或指向的对象。
Constexpr 和常量表达式
常量表达式:在编译时就能得到计算结果的表达式,并且 值不会改变。显然 字面值(整型浮点型字面值,字符和字符串字面值,bool指针字面值)就是常量表达式,用常量表达式初始化的const对象也是常量表达式。
// C++11新标准,允许将一个变量声明为《constexpr类型》以便编译器验证变量的值是否是一个常量表达式。声明为constexptr的变量一定是一个常量,且
必须用常量表达式初始化。
一个对象是不是常量表达式有他的数据类型和初始值一起决定:
Const int max_files = 20; max_files 是常量表达式
Const int limit = max_files + 1; limit 是 , max_file在编译阶段就知道是常量表达式。
Int staff_size = 27 ; satff_size 不是
Const int sz = get_size(); 不是
constexpr int mf =20; 20是常量表达式来初始化mf, 所以mf是常量
constexpr int limit = mf +1; limit ,mf+1 是常量表达式,编译时就能确定,且不变
constexpr int sz = size(); 只有当size()是一个constexpr函数时, 才是正确的语句。 constexpr函数简单到在编译时就计算出结果,
214页 constexpr函数 : 是指能用于常量表达式的函数。 constexpr函数的返回类型及所有形参的类型都得是字面值类型,而且函数体内必须有且只有一条return语句。
constexpr int new_sz(){return 42;} // new_sz定义成constexpr函数。在编译时验证
// new_sz返回的是常量表达式
constexpr int foo= new_sz(); foo是一个常量表达式。
编译器能在程序编译时验证new_sz()函数的返回的是常量表达式, 所有可以用 new_sz 函数初始化constexpr类型的变量foo
// 执行该初始化任务时, 编译器把对constexpr函数的调用替换成其结果值。 为了能在编译时随时展开, constexpr函数被隐式的指定为内联函数。Inline函数
字面值类型: 算术类型 、 指针 、引用都属于字面值类型。 自定义类型 IO库, string类不是字面值类型。
指针和引用可以定义成constexpr类型。 但是,constexpr指针的初始化初始值只能是nullptr 和0 , 或者存储于某个固定地址中的对象:
// 函数体内定义的变量一般 来说不是存放在固定地址中,constexpr对象不能指向这样的对象, 定义在函数体外的变量的地址时固定的,constexpr可以指向这样的对象。 185页 静态局部对象的存储地址也是固定的。
必须明确指出, 在constexpr声明中定义一个指针, 限定符constexpr 仅对指针有效,与指针所指对象无关。
constexpr声明中定义一个指针:p 和 q 的类型想去甚远。 关键在于constexpr 把它所定义的对象置为顶层const,即本身值不变
const int *p= nullptr; p是一个指向整型常量的指针。
constexpr int *q = nullptr; q 是一个指向整数的常量指针。
和其他指针类似, constexpr指针(是个常量)既可以指向常量也可以指向非常量。
constexpr int *np = nullptr; //np 的类型是指向一个整数的常量指针。值为空,
//因为constexpr变量是一个常量,所以必须初始化。
int j = 0;
constexpr int i= 42; i的类型是整型常量
// i 和j 都必须定义在函数体之外。因为constexpr指针必须指向固定地址的对象
constexpr const int *p = &i; // p 是常量指针(因为是constexpr类型), 指向整型常量i
constexpr int *p1 = &j; p1是常量指针, 指向整数j.
类型别名: 是一个名字, 是某种类型的同义词,只要类型名字能出现的地方,都能用类型别名。 能让复杂的类型名字变得简单明了、易于理解和使用。
定义类型别名的两种方式: type_define
关键字typedef 作为基本数据类型的一部分出现,含有typedef 的声明语句定义的不再是变量而是类型别名, 声明符(*p、&r)可以包含类型修饰符(* 、&),从而从基本数据类型构造出复合类型。
typedef double wages; wages 是double的别名同义词
typedef wages base ,*p; base是double的别名, p是 double* 的别名。
新标准下: 用别名声明 来定义类型的别名
using si = Sales_item; si 是别名
指针、常量和类型别名:
typedef char *pstring; // pstring 是char *的别名,pstring 是指向char的指针
const pstring cstr=0;
const pstring *ps; ps是一个常量指针, 他的对象是一个指向char的常量指针
上面的语句的基本数据类型是 const pstring , const是对给定基础类型pstring的修饰, pstring 实际上是指向char的指针。所以 const pstring 就是指向char的常量指针, 而非指向常量字符的指针。
遇到一个类型别名的声明语句时, 人们往往会错误的尝试吧类型别名替换成他本来的样子理解含义,这样的理解是错误的:
const char *cstr= 0; // 是对 const pstring cstr的错误理解
再强调一次 , : 声明语句中用到pstring 时, 其基本数据类型是指针,就像int 、double 一样。 可是用char* 重新写声明语句后, 数据类型变成了char,
*成为了声明符的一部分。 这样改写的结果是: const char 成了基本数据类型。 前后两种语义截然不同, 前者声明了一个指向char的常量指针, 改写
后的形式则声明了一个指向常量const char的指针。
auto和decltype 类型说明符:
auto 类型说明符:编程时有时把表达式的值赋给变量。
auto 让编译器去分析 表达式所属的类型,auto让编译器通过初始值来推算 变量的类型 , 所以auto定义的变量必须有初始值。
Auto item = val1 + val2; 编译器根据相加结果来推断item的类型
auto的可以一条语句中定义多个变量 , 但是一条语句中只能有一个基本数据类型, 所以该语句中所变量的初始基本数据类型都必须一样:
auto i =0, *p= &i; 对
auto sz=0, pi=3.14; 错 sz 和 pi 的类型不一致。
复合类型、 常量和auto:
使用引用就是使用所引用的对象,编译器用所引用的对象的类型作为auto的类型:
int i=0, &r=i;
auto a=r; a是一个整数
auto 忽略顶层const, 保留底层const:
const int ci = i, &cr = ci; cr是常量引用
auto b=ci; b是一个整数, ci的顶层const被忽略了
auto c =cr; c是整数, cr是ci 的引用,也就是一个别名,ci是顶层const
auto d= &i; d是一个整型指针 (一个对象保存地址就是指向整数的指针。)
auto e= &ci; // e 是一个指向整数常量的 指针( 对常量对象取地址 是一种 底层const,指向的对象不会改变即底层const)
若希望保留推断出的auto的 一个顶层const, 需要显示明确指出:
const auto f = ci;
将引用的类型设为auto,原来的初始化规则依然使用:
auto &g = ci; g是一个整型常量引用,绑定到ci
auto &h= 42; 不能为非常量引用绑定字面值。 普通引用不能绑定到字面值和表达式的计算结果;而const引用可以绑定到任何表达式。
const auto &j = 42; 可以为常量引用绑定字面值。
要在一条语句中定义多个变量, 切记, 符号& 和* 只从属于某个声明符, 而非基本数据类型的一部分, 因此初始值必须是同一种类型:
auto k= ci, &l =i; k是整数, l是整型引用
auto &m = ci, *p = &ci; m 是对整形常量的引用, p是指向整型常量的指针
auto &n= i, *p2 = &ci; 错误 i类型是int, 而&ci 类型是const int。初始值必须是同一种类型
Decltype: declear+ type
有时希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,
为了满足这个要求,用 decltype(declare宣布、声明 + type)类型说明符
decltype 选择并返回操作数的数据类型。 编译器分析表达式得到他的类型,但是不实际计算表达式的值。
decltype ( f() ) sum =x; sum 的类型是函数f()的返回类型。
和auto不同, 如果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; 错 cj 是引用,所以 z是一个引用, 必须初始化。
引用从来都是所指对象的同义词,但在decltype这里是例外::
如果decltype使用的表达式不是一个变量,则decltype 返回表达式结果对应的类型。
int i =42, *p= &i, &r = i;
decltype (r+0) b; 加法的结果是 int, 所以b是一个int
decltype (*p) c ; 错误,c 是int&, 必须初始化。
decltype(r) 的结果是引用类型,因为r是引用, 如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0, 显然表达式的结果是一个具体的值而非一个引用。
如果表达式是 解引用操作, 则decltype 将得到引用类型。 解引用指针得到所指对象,还能给对象赋值,类似引用定义, 因此,decltype(*p) 的结果类型是 int&, 而不是int
decltype 的表达式如果是加上了括号的变量,那么结果将是引用类型。 如果使用的不加括号的变量,则得到的结果是该变量 的类型:
decltype( (i) ) d; 错误 d 是int& 必须初始化。
Decltype ( i ) e; 正确 e是一个 为初始化的 int
切记: decltype(( variable)) (注意是双括号) 的结果永远是引用。而decltype (variable)的结果只有当 variable 本身是一个引用时才是引用。
自定义数据结构:
数据结构是把一组相关数据元素组织起来然后使用他们的策略和方法。比如Sales_item类就是一个数据结构:包含各种数据和 一些函数,运算等操作。
关键字 struct /class 后跟类名和类体, 右花括号后要跟一个分号,这是因为类体后面可以紧跟变量名表示对该类型对象的定义,一般不要这样定义变量。
定义没有任何运算的类Sales_data:类体用花括号包围 形成新的作用域,类体后紧跟分号;
struct Sales_data{ 定义数据成员的方法和定义普通变量一样。 每个对象都有自己的一份数据成员的拷贝, 修改一个对象的数据成员,不影响其他对象。
std::string bookNo;
unsigned units_sold= 0; 0和0.0是类内初值: 用来初始化数据成员,没有类内初值的成员被默认初始化。放在花括号内或者等号的右边。 不能放在圆括号中。P39
double revenue = 0.0; 销售收入
};
使用Sales_data类: 和Sales_item类不同, Sales_data 类没有自己的操作, 所以必须自己动手编写代码实现输入输出和相加功能。
假定输入是下面两条记录:
0-201-7883-x 3 20.00
0-201-7883-x 2 25.00
#include <iostream>
#include <string> 代码中将使用到string类型的成员变量bookNo,就是字符序列
#include"Sales_data.h" // #include 指令: 包含来自标准库的头文件时, 用尖括号包围头文件名, 不属于标准库的头文件,使用双引号包围。
int main
{
Sales_data data1, data2;
// 读入data1 data2 的代码
// 检查data1和data2 的ISBN号是否相等的代码
// 如果相同 则求 data1和data2的总和。
}
Sales_data对象读入数据:
double price =0; 输入的交易信息记录的是书的单价, 而数据结构存储的是销售收入, 所以需要将单价读入到double 变量price中,然后计算销售收入revenue
std::cin>> data1.boolNo >> data1.units_sold >> price;
data1.revenue = data1.units_sold *price;
std::cin>> data2.bookNo>> data2.units_sold >> price;
data2.revenue =data2.units_sold* price;
输出两个对象的和:
if( data1.bookNo == data2.bookNo)
{
units_totleCnt =data1.units_sold + data2.units_sold;
double totalRevenue = data1.revenue + data2.revenue;
// 输出 ISBN 总销量 总销售额 平均价格
std::cout<< data1.bookNo << "" << totalCnt << "" << totalRevenue<<"" ;
if( totalCnt!=0)
std::cout<< totalRevenue /totalCnt <<std::endl; 相除的平均价格
else
std::cout<< "( no sales)" <<std::endl;
return 0; // 标志成功 两个书号相等
}else{ // 两笔交易的ISBN不一样。
std::cerr<< "Data must refer to the same ISBN" <<std:: endl;
return -1 ; // 标志失败 书号不相等
}
编写自己的头文件:
类一般不定义在函数体内,通常类定义在头文件中,类所在的头文件名应与类名相同。
为了确保在不同文件中使用的同一个类,类的定义就必须保持一致,所以类要定义在头文件中
头文件通常包含那些只能定义一次的实体: 类 const constexpr变量
某个头文件也经常用到其他头文件的功能。Sales_data类包含一个string成员, 所以Sales_data.h 必须包含是string.h 头文件。同时使用到Sales_data类的程序为了能操作bookNo成员需要再一次包含string.h头文件。
这样就两次包含同一个头文件string.h ,为了能正常安全工作,需要做适当处理:
预处理器: 确保头文件多次包含仍能安全工作的常用技术。 是编译器之前执行的一段程序,可以部分改变我们所写的程序。继承自c语言
#include 也是预处理功能,当预处理器看到#include标记, 就会用指定的头文件内容代替#include。
C++还有的预处理功能是:
头文件保护符:头文件保护符能有效防止重复包含的发生。头文件保护符依赖于预处理变量: 预处理变量有两种状态: 已定义和 未定义。
#define 指令把一个标识符/名字设定为预处理变量。
另外两个指令则分别检查某个指定的预处理变量是否已经定义:
#ifdef 当且仅当变量已定义时为真。
#ifndef 当且仅当变量未定义时为真。 (#if_no_define)
一旦检查为真, 就执行后续操作直至遇到 #endif 指令为止。
使用这些功能能有效防止重复包含的发生:如下编写头文件和头文件中的类
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data{ 定义数据成员的方法和定义普通变量一样。 每个对象都有自己的一份数据成员的拷贝, 不影响其他对象。
std::string bookNo;
unsigned units_sold= 0; 类内初值: 用来初始化数据成员,没有类内初值的成员被默认初始化。放在花括号内或者等号的右边。 不能放在圆括号中。
double revenue = 0.0; 销售收入
};
#endif
第一次包含Sales_data.h时, #ifndef 检查结果为真, 预处理器将顺序执行后面的操作直至遇到#endif为止。 此时 预处理变量SALES_DATA_H 的值将变为已定义
而且头文件Sales_data.h 也会被拷贝到我们的程序中来。 后面再一次包含Sales_data.h , 则#include的检查结果将为假, 编译器会忽略#ifndef 到# endif之间的部分。
预处理变量无视C++语言中关于作用域的规则。
整个程序中的预处理变量包括头文件保护符都必须唯一, 通常的做法是基于头文件中类的名字来构建保护符的名字(即类名、头文件名和头文件保护符名字都和类名相同), 确保唯一性。 为了避免与程序中的
其他实体发生名字冲突, 一般吧预处理变量的名字全部大写。
头文件即使(目前还)没有被包含在任何其他头文件中,也应该设置保护符。头文件保护符很简单,程序员只需要习惯性地加上就可以了, 没必要太在乎你的程序到底需不需要。
*/