目录
2.1 基本内置类型
2.1.1算术类型
算术类型分为两类:整型(包括字符和布尔类型在内)和浮点型。
算术类型的尺寸(也就是该数据类型所占的比特数)在不同机器上有所差别。下表列出的C++标准规定的尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。某一类型所占的比特数不同,它所能表示的数据范围就不一样。
bool的取值是真或假
一个char的空间应该确保可以存放机器机器基本字符集中任意字符对应的数字值。也就是说,一个char的大小和一个机器字节一样。
整型用于表示(可能)不同尺寸的整数。
浮点型可以表示单精度、双精度和扩展精度值。C++标准指定了一个浮点数有效位数的最小值,然而大多数编译器都实现了更高的精度。通常,float以1个字(32比特)来表示,double以2个字(64比特)来表示,long double 以3或4个字(96或128比特表示)。
内置类型的机器实现
计算机以比特序列存储数据,每个比特非0即1。
可寻址最小内存块称为“字节(byte)”,储存的基本单位称为“字(word)”,它通常由几个字节组成。在C++语言中一个字至少要能容纳机器基本字符集中的字符。大多数机器的字节由8比特构成,字则由32或64比特构成,也就是4或8字节。
大多数计算机将内存中的每一个字节与一个数字(被称为“地址(address)”)关联起来,在一个字节为8比特、字为32比特的机器上,我们可能看到一个字的内存区域如下所示:
736424 | 0 0 1 1 1 0 1 1 |
736425 | 0 0 0 1 1 0 1 1 |
736426 | 0 1 1 1 0 0 0 1 |
736427 | 0 1 1 0 0 1 0 0 |
其中,左侧是字节的地址,右侧是字节中8比特的具体内容。
带符号类型和无符号类型
除去布尔型和扩展的字符型之外,其它整型可以分为带符号的和无符号的两种。带符号的类型可以表示正数、负数或0,无符号的仅能表示大于等于0的值。
字符型被分为了三种:char、signed char、unsigned char。其中类型char和类型signed char并不一样。类型char会表现为有符号的或无符号的中的一种,具体是哪种由编译器决定。
2.1.2 类型转换
类型转换是一种运算,且能被大多数类型支持。
当在程序中的某处,我们使用了一种类型而其实对象应该取另一种类型时,程序会自动进行类型转换。
含有无符号类型的表达式
尽管我们不会故意给无符号对象赋一个负值,却可能写出这么做的代码。例如:当一个算术表达式中既有无符号数,又有int值时,那个int就会转化为无符号数。
(切勿混用带符号类型和无符号类型)
2.1.3 字面值常量
每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。
整型和浮点型字面值
我们可以将字面值写作十进制数、八进制数或十六进制数的形式。以0开头的整数代表八进制数,以0x或0X开头的代表16进制数。如数值20可表示为:
20 024 0x14
默认情况下,十进制字面值是带符号数,八进制和十六进制字面值不确定(由具体数值而定)
十进制字面值的类型是,int,long,long long 中 能容纳该数值的且最小的 一个类型。八进制和十六进制,则还包含了无符号类型的数据,依旧是能容纳的该数值的且最小的一个数据类型。如果一个字面值连与之关联的最大数据类型都放不下,将会产生错误。类型short没有对应的字面值。
严格来说,十进制字面值不会是负数的。一个形如-42的十进制字面值,这个负号并不在字面值之内,它的作用仅仅是对字面值取负值而已。
默认的浮点型字面值是一个double。
字符和字符串字面值
如果两个字符串字面值位置紧邻且仅由空格、缩进和换行赋分隔,则它们实际上是一个整体。(字符串字面值太长了,需要换行时可以使用这一特性)
转义序列
适当的做个了解,刚觉不大会用的上。使用泛化的转义序列,其形式是\x后紧跟1个或多个十六进制数字,或者\后面紧跟1个、2个或3个八进制数字,其中数字部分表示的字符对应的数值。
指定字面值的类型
哇,一个字面值硬是被整出了这么多花样,还是做一个适当的了解吧。这里不记载了,书中了解吧。
2.2 变量
2.2.1 变量定义
初始值
初始化不是赋值,初始化的含义是创建变量的同时赋予其一个值,而赋值的含义是吧对象当前的值擦除,而以一个新的值来代替。
列表初始化
C++语言定义了初始化的好几种不同的形式,如下4种,达到的效果是相同:
int a = 0;
int a = {0};
int a{0};
int a(0);
作为C++11的新标准,用花括号来初始化变量得到了全面的运用。这种初始化形式被称为列表初始化。当运用于内置类型的变量时,这种初始化形式有一个重要的特点:如果我们使用列表初始化且初始化值存在丢失信息的风险(容量不够,类型不同等可能导致出现该问题),则编译器将报错。
2.2.2 变量声明和定义的关系
为了允许程序拆分成为多个逻辑部分来编写,C++语言支持分离式编译机制。
为了支持分离式机制,C++语言将声明与定义区分开来。声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但除此之外,定义还申请了存储空间,也可能会为变量赋一个初始值。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示地初始化变量:
extern int i; //声明i而非定义i
int i; //声明并定义i
2.2.3 标识符
这些限制闻所未闻,记录并试验一下吧。C++也为标准库保留了一些名字。用户自定义标识符中不能连续出现两个下划线,也不能以下划线紧连大写字母开头。此外,定义在函数体外的标识符不能以下划线开头。(我试验了一下,违反上述规定,全都没毛病啊)
2.2.4 名字的作用域
作用域是程序的一部分,在其中名字有其特点的含义。C++语言中大多数作用域都以花括号分割。
同一个名字,在不同的作用域中可能指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。
嵌套的作用域
作用域能够彼此包含,被包含的作用域称为内层作用域,包含着别的作用域的作用域称为外层作用域。(应该不难理解,内层与外层是一个相对的概念)
作用域中一旦声明了某个名字,它的内层作用域都能访问该,名字。同时允许在内层作用域中重新定义外层作用域已有的名字。
让我们来关注下下面这段代码
#include<iostream>
int reused = 42;
int main()
{
int unique = 0;
int reused = 0;
std::cout << ::reused << " " << unique << std::endl; //结果为 42 0
}
这里使用了作用域操作符来覆盖默认的作用域规则,因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作佛右侧名字对应的变量。
2.3 复合类型
复合类型是指基于其它类型定义的类型。C++语言有几种复合类型,本章将介绍其中两种:引用和指针。(哈,引用和指针比我预计的出现的要早啊)
一条简单的声明语句由一个数据类型和紧随其后的一个变量名列表组成。其实更通用的描述是,一条声明语句由一个基本数据类型和紧随其后的一个声明符列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
2.3.1 引用
(好家伙先讲引用后讲指针,看这个章节并未打算揭露引用的本质了。)
引用为对象起了另外一个名字,引用类型 引用 另外一种类型。
引用为什么必须初始化?一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和初始值绑定在一起,而不是讲初始值拷贝给引用。一旦初始化完成,引用将和它的初始对象一直绑定在一起(表现出了指针常量的性质)。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
引用即别名
引用并非对象,相反的,它只是一个已经存在的对象所起的另一个名字。
定义了一个引用后,对其进行的所有操作都是在与之绑定的对象上进行的。
因为引用本身不是一个对象,所以不能定义引用的引用。
引用的定义
允许在一条语句中定义多个引用,其中每个引用标识符都必须以&开头。
引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起(字面值和表达式的计算结果大多为右值,是不可取址的。)
我正试图逐渐简化现在的笔记形式。制作一份可以脱离书本,直接阅读便获得全面信息的笔记现在对于我而言是不大现实的。
一些很为关键的知识点,理应被记录,但出于我对它非常熟悉了解,便可能省略不计。
2.3.2 指针
指针是指向另一种类型的复合类型。指针也实现了对其它对象的间接访问。
空指针
过去的程序会用到一个名为NULL的预处理变量来给指针赋值,这个变量在头文件cstdlib中定义,它的值就是0.
在新标准C++程序最好使用nullptr,同时尽量避免使用NULL。
void* 指针
在此之前我还从未见过该指针的使用。
void*是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着一个地址,但该地址中到底是什么类型的对象并不了解.
利用void*指针能做的事比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。不能直接操作void*指针所指的对象。
2.3.3 理解复合类型的声明
变量的定义包含一个基本数据类型和一组声明符。同一条定义语句中,基本数据类型只有一个,但声明符的形式可以不同。也就是说,一条定义语句可能定义出不同类型的变量。
int i = 1024, *p = &i, &p = i;
指向指针的引用
int 8=*p;
int *&r = p;
要理解r的类型到底什么,可以从右往左阅读r的定义。离变量名最近的符号(这里是&)对变量的类型有最直接的影响,因此r是一个引用。而*说明了r引用的是一个指针,然后int又进一步说明了r引用的是一个指向int类型数据的指针。
2.4 const限定符
2.4.1 const的引用
把引用绑定到const对象上,我们称之为对常量的引用。
前面说过,引用和指针需要类型的高度匹配,对 const 的引用是一个列外。
对const的引用可以指向右值(短暂而临时的值,无名则无存)。
double dval = 3.14;
cosnt int &ri = dval;
上面的程序是合法的,但引用所绑定的类型和引用的类型确实不符,原因如下:
const int temp = dval;
const int &ri = temp;
ri其实绑定了一个临时对象。
这里细思就会发现问题,临时量,无名则无存,往往它的生命周期在“;”后就结束。但是明显这里并没有,这个临时对象的生命周期被延长到了引用被销毁时。防止了悬空的引用的出现。这一特例书中并未做出说明。
2.4.3 顶层const
指针本身是一个对象,它又可以指向另一个对象。因此,指针本身是不是常量以及指针所指对象是不是一个常量是两个相互独立的问题。用名词 顶层 const 表示指针本身是一个常量,而名词 底层 const 表示指针所指对象是个常量。
2.4.4 cosntexpr和常量表达式
常量表达式(cosnt expression)是指值不会改变且在编译阶段就能得到计算结果的表达式。
constexpr变量
C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
constexpr int mf = 20; //ok
constexpr int limit = mf +1; //ok
constexpr int se = size(); //当size是一个constexpr函数时ok,否则报错
不能用普通函数作为constexpr变量的初始值,新标准允许定义一种constexpr函数。这种函数足够简单,使得编译时期就可以计算出其结果,这样就可以使用constexpr函数去初始化constexpr变量了。
一般说,如果你认定一个变量就是一个常量表达式,就把它声明为cosntexpr
字面值类型
常量表达式的值需要在编译时期就得到计算,因此对声明constexpr时用到的类型就必须限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”。
到目前为止接触过的数据类型中,算术类型、引用和指针都属于字面值类型。
2.5 处理类型
类型别名
(开辟新大陆了,很少使用或没使用过的内容)
类型别名是一个名字,它是某种类型的同义词。
有两种方法可以用于定义类型别名。传统方法是使用关键字typedef:
typedef double wages; //wages是double的同义词
typedef wages base, *p; //base是double的同义词,p是double*的同义词
其中,关键字typedef作为声明语句中的基本数据类型的一部分出现。含有typedef的声明语句定义的不再是变量而是类型的别名。和以前的声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基类数据类型构造出复合数据类型来。
新标准规定了一种新的方法,使用别名声明来定义类型的别名
using SI = Sales_item; //SI是Sales_item的同义词
这种方法用关键子using作为别名声明的开始,其后紧跟着别名和等号,其作用是把等号左边的名字规定为等号右边类型的别名。
指针、常量和类型别名
const修饰指针问题,相信大家都有了解过。本书中对顶层const类型指针,以及底层const类型指针的称呼和我们习惯称呼存在冲突。
书中:
int *const p 顶层const——常量指针
const int *p 底层cosnt——指向常量的指针
我们所习惯的:
顶层const——指针常量(指针本身是常量,指针指向不可修改)
底层const——常量指针(指针认为它所指的对象是常量)
后续我将用习惯的方式去指代这两种const修饰的指针。
typedef char* pstring;
const pstring cstr = 0; //定义了一个名为cstr的指针常量
const char *cstr = 0; //定义了一个名为cstr的常量指针
2、3条语句含有不同的关键在于 pstring 已成为一个基础数据类型,而这个基础数据类型是 char*,是一个指针,而cosnt修饰了基础数据类型,2的最终的结果就是得到了一个 指针常量。
2.5.2 atuo类型说明符
C++新标准引入了auto类型说明符,用它能让编译器替我们去分析表达式所属的类型。
atuo让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须有初始值。
auto item = val1 + val2; //item初始化为val1和val2相加的结果
如果val都是double,那么auto就是double,如果val都是Sales_item,那么auto就是Sales_item,以此类推。
使用auto也能在一条语句中声明多个变量。但要注意产生歧义的问题,一个声明语句只能有一个基本数据类型。
复合类型、常量和auto
编译器推断出来的auto类型有时候和初始值的类型并不完全一样。
当引用被作为推导的初始值时,实际是对引用绑定对象的推导
再有,auto一般会忽略掉顶层const,保留底层const。
如果希望推断出的auto类型是一个顶层const,需要显示说明
const auto type = val;
2.5.3 decltype类型指示符
希望从表达式推导出要定义的变量类型,但是又不想用该表达式的值去初始化变量。(这是针对auto而说的话。)为了满足这一需求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
decltype(f()) sum = x; //sum的类型就是函数f的返回值的类型。
编译器并不实际调用函数f,而是使用当前调用发送时f的返回值类型作为sum的类型。
decltype处理顶层const和引用的方式与auto有些不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)。
decltype和引用
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
2.6 自定义数据结构
struct结构体
末尾必须加分号是因为,可以在末尾直接定义该类型的变量,即在末尾直接添加声明符,而分号表示声明符的结束。末尾后面直接加分号,表示没有声明变量。
2.6.3 编写自己的头文件
预处理概述
确保头文件多次包含任然能安全工作的常用技术是预处理器,它由C++语言从C语言继承而来。预处理器是在编译之前执行的一段程序,可以部分的改变我们所写程序。#include就是一种预处理功能,当预处理器看到#include标记时就会用指定的头文件的内容代替#include。
参考书籍 《C++ Prmer (第5版)》