-
空类型不对应具体的值,仅用于一些特殊的场合
-
long
的长度为32位,float
有7个有效位,double
有16个有效位 -
如果数值超过了
int
的范围,应该用long long
而不是long
,long
一般和int
一样大 -
在算术表达式中不要使用
char
或bool
,只有在存放字符或布尔值时才是使用它们。因为类型char
在一些机器上是有符号的,而在另一些机器上又是无符号的,所以使用char
进行运算特别容易出现问题。如果需要使用一个不大的整数,那么明确指明它的类型是signed char
或unsigned char
。 -
执行浮点运算时用
double
。这是因为float
通常精度不够而且双精度和单精度浮点数的计算代价相差无几(甚至在一些机器上double
更快)。long double
提供的精度一般是没有必要的,而且计算带来的运行消耗也不容忽视。 -
当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为
false
,否则为true
;当我们把一个布尔值赋给非布尔类型是,初始值为false
则结果为0,初始值为true
则结果为1;当我们把一个浮点数赋给整数类型时会截取整数部分;当我们赋给无符号类型一个超出它范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数,而当赋予带符号类型一个超出它表示范围的值时,结果是未定义的 -
程序应该尽量避免依赖于实现环境的行为(例如
int
的尺寸),否则会导致程序的不可移植性 -
不要混用带符号类型和无符号类型,带符号数会自动转化为无符号数
-
在涉及无符号数的表达式中不要出现负数,例如下面这个很容易产生错误的代码:
for (unsigned u = 10; u >= 0; --u)std::cout << u << std::endl;
这个代码看起来很简单,但是实际上会出现死循环,因为当
u==-1
的时候因为u
是无符号数的缘故-1
将会自动转化成一个合法的无符号数,无法跳出循环 -
以
0
开头的整数表示八进制数,以0x
开头的整数代表十六进制数 -
如果我们使用了一个负十进制字面值,那么负号并不在字面值之内,它的作用仅仅是对字面值取负值。
-
单引号括起来的一个字符称为
char
型字面值,双引号括起来的零个或多个字符则构成字符串字面值。字符串字面值的类型实际上由常量字符构成的数组。编译器在每个字符串的结尾处添加一个空字符(\0
),因此,字符串字面值的实际长度比它的内容多1。如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则他们实际上是一个整体 -
退格符
\b
会删除输出缓冲区中的一个字符(如果没有就无效),进纸符\f
和纵向制表符\v
效果相同,都会在下一行的当前位置的后一个位置开始输出,回车符\r
会在缓冲区的开始位置开始覆盖,报警符\a
会响一下 -
我们可以使用泛化的转义序列,
\x
后紧跟一个或者多个十六进制数字,或者\
后紧跟1个,2个或3个(最多)八进制数字 -
通过添加前缀或者后缀可以改变整型、浮点型和字符型字面值的默认类型。如:对整型字面值添加
LL
后缀意味着字面值为long long
类型,对浮点型字面值添加L
意味着字面值为long double
类型。在字符或者字符串字面值前面添加L
表示宽字符 -
通常情况下,对象是指一块能存储数据并具有某种类型的内存空间(意义同我们经常说的变量
-
当一次定义了两个或者多个变量时,对象的名字随着定义也可以马上使用了。因此在同一条定义语句中,可以用先定义的变量值去初始化后定义的其他变量
-
在C++中,初始化和赋值是两个完全不同的操作。两者的区别在于:初始化的含义是创建变量的时候赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来代替。
-
初始化格式:
int a = 0; int b = {0}; int c{0}; int c(0);
上面四种方式都是允许的,其中使用
{}
进行初始化的方式被称为列表初始化,在C++11标准被全面使用。使用列表初始化的一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,那么编译器会报错。例如double a = 1.5; int b{a};
将不被允许 -
如果定义变量的时候没有指定初始值,则变量被默认初始化。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此产生影响。对于内置类型,如果定义在任何函数体之外的变量被默认初始化为0,定义在函数内部的内置类型将不被初始化。使用一个未初始化的内置将产生未定义的行为。建议初始化每一个内置类型的变量
-
为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。为了支持分离式编译,C++语言将声明和定义区分开。声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明,而定义负责创建与名字关联的实体
-
如果想要声明一个变量而非定义它,就在变量名前面添加关键字
extern
,而且不要显式地初始化变量。任何包含了显式初始化的声明即成为定义,即使使用extern
。在函数内部,如果试图初始化一个由extern
关键字标记的变量将引发错误 -
变量能且只能定义一次,但是可以被多次声明
-
C++是一种静态类型语言,其含义是在编译阶段检查类型。其中检查类型的过程称为类型检查。对象的类型决定了对象能够参与的运算。
-
C++标识符由字母、数字和下划线组成,其中必须以字母和下划线开头(不能以数字开头)。标识符的长度没有限制,但是对大小写字母敏感。用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧跟大写字母开头。此外,定义在函数体外的标识符不能以下划线开头。变量名一般用小写字母,用户自定义的类名一般以大写字母开头
-
名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束
-
一般来说,在对象第一次被使用的地方定义他是一种好的选择,因为这样可以更容易找到变量的定义,也容易赋给他一个比较合理的初始值。
-
作用域中一旦声明了某个名字,那么他所嵌套着的所有作用域中都能访问该名字,同时允许在内层作用域中重新定义外层作用域中已有的名字
-
可以使用作用域操作符来覆盖默认的作用域规则,因为全局作用域本身没有名字,所以当作用与操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量
-
复合类型是基于其他类型定义的类型(例如[左值]引用和指针)。一条声明语句由一个基本数据类型和紧随其后的一个声明符列表组成。每个声明符命名了一个变量并指定该变量为与基本类型有关的某种类型。
-
引用为对象起了另一个名字。在定义引用时,程序把引用和他的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和他的初始值对象一直绑定在一起。因为无法另引用重新绑定到另一个对象,所以引用必须被初始化。因为引用本身不是一个对象,因此不能定义引用的引用。引用类型必须和要绑定的对象类型匹配。而且引用只能绑定在对象上,而不能与字面值或者某个表达式的计算结果绑定在一起。
int a = 10; int &b = a; //b指向a
-
指针是指向另外一种类型的复合类型,存放指定对象的地址。和引用的区别:
-
指针本身就是一个对象,允许对指针复制和拷贝,而且在指针的生命周期内它可以先后指定几个不同的对象,而引用绑定的对象不会发生改变。
-
指针无须在定义时赋初值(虽然最好这样做)。
-
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。但是可以定义指向指针的引用。
因为在声明语句中指针的类型实际上被用于指定它所指向对象的类型,因此两者必须匹配。
-
试图拷贝或以其他方式访问无效指针的值都将引发错误,编译器不负责检查此类错误。
-
空指针定义
int *p1 = nullptr; //推荐,C++11标准 int *p2 = 0; //#include<cstdlib> int *p3 = NULL
把
int
变量直接赋给指针是错误的操作,即使int
变量的值恰好等于0也不行 -
void*
是一种特殊的指针类型,可用于存放任意对象的地址。
作用:- 和别的指针比较
- 作为函数的输入或输出
- 赋给另外一个
void*
指针
局限:不能直接操作
void*
指针所指的对象 -
代码规范:把类型修饰符(* 或者 &)与变量名写在一起,与数据类型分开
int a = 0, *b = &a, *&c = b;
当声明语句比较复杂的时候,从右往左阅读有助于弄清楚他的真实含义。
-
const
对象一旦创建后其值就不能再改变,所以const
对象必须进行初始化,可以用另一个对象去初始化一个const
对象,对变量的初始化不算改变变量的内容。(这里也能看出初始化和赋值的区别) -
使用
const
变量以后,编译器在编译过程中用到该变量的地方都替换成对应的值。为了执行上述操作,编译器必须知道变量的初始值,因此就必须在每一个用到const
变量的文件中有该变量的定义。为了避免重复定义,默认情况下,const
对象被设定为仅在文件内有效。如果想要在多个文件之间共享const
对象,必须在变量的定义(当然声明也要加)之前添加extern
关键字,使其被其他文件使用。 -
对常量对象的引用也必须用
const
修饰,表示该引用所指向的是一个常量。const int a = 1; const int &b = a; //b是a的引用 a = 2; //错误 int &c = a; //错误,试图让一个非常量引用指向一个常量对象
需要注意的是引用定义中的
const
不是表示引用本身的常量属性,引用变量b
本身就具有常量属性(不能解除与a
的绑定),而是说b
对引用的变量的操作的常量属性(如果引用的对象是一个const
对象,则操作必须是对常量的操作,后面还有其他情况的说明)。我们一般称这种引用为常量引用 -
一般情况下,引用的类型必须和其所引用的对象保持一致,但是对于常量引用的初始化允许用任意表达式作为初始值,只要该表达式的结果能够转化为引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个表达式。
int a = 1; const int &b = a; //正确 const int &c = 2; //正确 const int &d = b * 2;//正确 int &e = r1 * 2; //错误,普通的引用必须绑定在一个对象上 double e = 3.14; const int &f = e; //正确
想要理解上面这些奇怪的行为,就要明白对常量引用初始化的过程,为了完成对
f
的初始化,编译器会将最后一行代码翻译为:const int tmp = e; const int &f = tmp;
在这种情况下,
f
绑定了一个临时量对象(编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象)。其他初始化语句同理。 -
必须要认识到,常量引用仅仅限定了引用可参与的操作,对于引用的变量本身是不是一个常量未作限定,我们仍旧可能通过其他方式对常量引用所指向的对象进行修改:
int i = 1; int &r1 = i; const int &r2 = i; const int &r3 = i * 3.14; r1 = 2; //i == 2 //r1 == 2 //r2 == 2 //r3 == 3
再次说明了数据类型对数据可以进行的操作进行了限定。
-
指向常量的指针不能用于改变其所指对象的值,想要存放常量对象的地址,只能使用指向常量的指针。在这一点上,对常量的引用和指向常量的指针类似。然而两者最大的不同点在于,指向常量的指针本身不是一个常量,可以发生变化。而对常量的引用却因为是一个引用一旦初始化便不能再发生改变
const double pi = 3.14; double *p1 = π //错误 const double *p2 = π //正确 *p2 = 4; //错误,不能改变指向常量的指针所指向随想的值 double a = 4.0; p2 = &a; //正确 a = 5.0; //正确
-
指针的类型必须和所指对象的类型一致,但是允许一个指向常量的指针指向一个非常量对象(但是数据类型必须一致,这一点和对常量的引用不同)。如上面代码所示,指向常量的指针也没有规定所只想的对象必须是一个常量,我们仍可能通过其他方式改变所指向对象的值。
-
常量指针必须初始化,一旦初始化完成,常量指针所指向的地址就不能再发生变化。我们把
*
放在const
之前用来说明指针是一个常量(声明的阅读法则:从右往左看)。可能通过常量指针改变所指向的值(如果指向的不是一个常量的话)。int i = 1; const int &r1 = i * 3.14; const int *temp = &r1; const int **const &tmp = &temp; //**tmp等于3 *tmp = &i; //**tmp等于1
上面的代码是可以运行通过的(实测),
tmp
是一个指向整型常量的常量二级指针的引用。但是我们是可以改变*tmp
的值的,因为const
只修饰了最近的那个*
,如果需要*tmp
的值也不能改变,我们就要将声明改为:const int *const *const &tmp = &temp
(属实是我写过最长的声明了)。 -
用名词顶层
const
表示指针本身是一个常量,用名词底层const
表示指针所指的对象是一个常量。顶层const
可以表示任意的对象是常量,这一点对任何数据类型都适用。底层const
则与指针和引用等复合类型的基本类型部分有关。其中用于声明引用的const
都是底层const
(其实很好理解,因为引用本身不能更改,相当于本身就具有顶层const
的属性) -
当执行拷贝操作时,常量是顶层
const
还是底层const
区别明显,其中顶层const
不受什么影响,但是底层const
的限制却不能忽视,当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const
资格,或者两个对象的数据类型必须能够转换(非常量可以转换为常量) -
对顶层
cosnt
和底层const
的理解:这只是一个人为理解的概念,我认为对程序本身没有什么影响。顶层const
是指对象的值的不可更改的性质,底层const
表示对所指向对象的操作权限比较有限的性质。虽然书中没有说,但是我认为把非复合类型的数据类型声明中的const
应该同时看作顶层和底层const
比较好理解,因为这个时候对象和指向的对象是相同的,这样就可以推广上面的规则:任何一个底层const
必须初始化/赋值给一个底层const
(给基本数据类型的拷贝不算),否则报错:- 一个基本数据类型(非复合)常量只能初始化/赋值给常量引用和指向常量的指针
- 一个常量引用只能初始化给一个常量引用,一个指向常量的指针只能初始化/赋值给一个指向常量的指针
-
常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式。显然字面值属于常量表达式,用常量表达式初始化的
const
对象也是常量表达式。特征:常量+在编译时就能确定结果 -
在C++11新标准规定,允许将变量声明为
constexpr
类型以便编译器来验证变量的值是否是一个常量表达式,声明为constexpr
的变量一定是一个常量,而且必须用常量表达式初始化constexpr int i = 20; 20是常量表达式 constexpr int limit = i +1; i+1是常量表达式 constexpr int sz = size(); 只有size()是一个constexpr函数时才是一条正确的声明语句
-
声明
constexpr
类型的必须是字面值类型:算数类型、引用和指针。一个constexpr
指针必须是nullprt
或0
或存储在某个固定地址中的对象。定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr
指针。需要注意的是constexpr
会把变量声明为顶层const
constexpr const int *p = &i; 要求i的地址固定不变(例如在函数体外)
-
关键字
typedef
作为声明语句中的基本数据类型中的一部分,含有typedef
的声明语句定义的不再是变量而是类型别名typedef double db; db是double的同义词 typedef db base, *p; base是double的同义词,p是double *的同义词
新标准规定了一种新的方法,使用类型别名来定义类型的别名:
using SI = Sales_item; SI是Sales_item的同义词
-
使用类型别名以后,类型别名成为了新的基本类型,不能错误尝试把类型别名替换成它原本该的样子来进行理解。
typedef char * cp; const cp p = nullptr; p是一个指向char的指针常量,如果错误地将将cp替换成char *以后p是一个指向常量char的指针
-
C++11标注引入了
auto
类型说明符,编译器会代替我们去分析表达式所属的类型,通过初始值来推算变量的类型,显然,auto
定义的变量必须有初始值。使用auto
也能在一条语句中声明多个变量,因为一条声明语句只能有一个基本数据类型,所以该语句中的所有变量的初始基本数据类型必须一样 -
编译器推断出来的
auto
类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使得其更符合初始化规则:-
用引用进行初始化的时候编译器会把引用的对象的类型作为
auto
类型 -
auto
会忽略顶层const
,但是底层const
会保留下来。int i = 1;const int ci = i, &cr = ci;auto a = ci; a是整型auto b = cr; b是整型auto c = &i; c是整型指针auto d = &ci; d是指向整型常量的指针
-
如果希望推断出的
auto
类型是一个顶层const
,需要明确指出:const auto e = ci; //e是整型常量
-
可以将引用的类型设置为
auto
,原来的规则依然适用,初始值中的顶层const
依然保留(如果我们给初始值绑定一个引用,则由引用所绑定的对象决定):auto &f = ci; f是一个整型常量引用 auto &g = 42; 错误,不能为非常量引用绑定字面值 const auto &g = 42; 正确
-
在一条语句中定义多个
auto
的引用时,符号&
和*
只从属于某个声明符,而非基本数据类型的一部分。auto &h = ci, *p = &ci; //h是对整型常量的引用,p是指向整型常量的指针//错误写法 auto &n = i, *p2 = &ci; //n是对整型的引用,p2是指向整型常量的指针
auto
在不同声明表现出对顶层const
的不同处理方式可能令人费解,在普通声明中会忽略顶层const
,但是在引用的声明中不会忽略顶层const
,我认为从另一个方向理解可能更好理解,在普通数据类型赋值给复合类型的时候,普通数据类型的const
同时具有顶层和底层const
属性。更底层的理解方法是复合类型往往意味着对已有对象的操作,但是我们不能放大对对象的操作权限(缩小可以)。
-
-
如果我们希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化该变量,可以用
decltype
类型说明符,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。decltype
处理顶层const
和引用的方式与auto
不同,如果decltype
使用的表达式是一个变量,则decltype
返回该变量的类型,包括顶层const
和引用在内。const int ci = 0, &cr = ci; decltype(ci) x = 0; x是const int decltype(cr) xr = x; xr 是 const int &
引用一般都是作为其所指对象的同义词出现,只有在
decltype
处是一个例外 -
decltype
的结果类型与表达式形式密切相关。如果decltype
的表达式是一个变量,则声明就是变量的类型,如果给这个变量加上括号,则声明是引用类型。因为加上括号以后比编译器会把变量当做一种可以作为赋值语句左值的特殊表达式,所以这样的decltype
就会使引用类型int i = 0; decltype(i) e; //正确,e是int decltype((i)) d; //错误,d是int &
-
类以关键字
struct
开始,紧跟类名和类体(类体可以为空)。类体由花括号包围形成了一个新的作用域。类体右侧的表示结束的花括号后必须写一个分号,这是因为类体后面可以紧跟变量名以表示对该类型对象的定义,所以分号必不可少。但是最好不要把对象的定义和类的定义放在一起。 -
C++11新标准规定可以为数据成员提供一个类内初始值,没有初始值的成员将会被默认初始化。记住不能使用圆括号进行初始化,可以使用花括号或者等号进行初始化。
-
尽管可以在函数体内部定义类,但是这样的类会收到一定的限制。因此类一般不定义在函数体中,当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且如果要在多个文件中使用同一个类,类的定义就必须保持一致。为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样。
-
为了确保头文件多次包含仍能够安全工作,我们使用预处理器中的头文件保护符进行处理,对于头文件
Sales_data.h
,文件的格式应该如下:#ifndef __SALES_DATA_H__ #define __SALES_DATA_H__ //定义部分 #endif
预处理变量无视C++语言中关于作用域的规则
-
太感动了,我终于看完第二章了。