读书过程中发现,读得越多,忘得越多。因此记录读书笔记
1.2 初始输入输出
向流写入数据
<<
运算符(输出运算符)接受两个运算对象:左侧的运算对象必须是一个ostream
对象,右侧的运算对象是要打印的值。此运算符将给定的值写到给定的ostream
对象中。输出运算符的计算结果就是其左侧运算对象。即,计算结果就是我们写入给定值的那个ostream
对象。
std::cout<<"Enter two numbers:"<<std::endl;
// 等价于
(std::cout<<"Enter two numbers:")<<std::endl;
第一个输出运算符给用户打印一条消息。这个消息是一个字符串字面值常量(stringliteral),是用一对双引号包围的字符序列。在双引号之间的文本被打印到标准输出。
第二个运算符打印endl
,这是一个被称为操纵符(manipulator)的特殊值。写入endl的效果是结束当前行,并将与设备关联的缓冲区(buffer)中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是仅停留在内存中等待写入流。
1.4.3 读取数量不定的输入数据
int a = 0;
while (std::cin >> a)
{
// todo
}
while (std::cin >> a)
此表达式从标准输入读取下一个数,保存在value中。输入运算符返回其左侧运算对象。因此,此循环条件实际上检测的是std::cin。当我们使用一个istream对象作为条件时,其效果是检测流的状态。如果流是有效的,即流未遇到错误,那么检测成功。当遇到文件结束符(end-of-file),或遇到一个无效输入时(例如读入的值不是一个整数),istream对象的状态会变为无效。
2.1 基本内置类型
2.1.1 算术类型
2.1.3 字面值常量
转义序列
对于一个整型字面值来说,我们能分别指定它是否带符号以及占用多少空间。如果后缀中有U,则该字面值属于无符号类型,也就是说,以U为后缀的十进制数、八进制数或十六进制数都将从unsigned int、unsigned long和unsigned long long中选择能匹配的空间最小的一个作为其数据类型。如果后缀中有L,则字面值的类型至少是long;如果后缀中有LL,则字面值的类型将是long long和unsigned long long中的一种。显然我们可以将U与L或LL合在一起使用。例如,以UL为后缀的字面值的数据类型将根据具体数值情况或者取unsigned long,或者取unsigned long long。
列表初始化
2.2.2 变量声明和定义的关系
分离式编译
如果想声明一个变量而非定义它,就在变量前添加关键字extern
,而且不要显示地初始化变量:
extern int i; // 声明i,而非定义i
int j; // 声明并定义j
任何包含了显示初始化的声明即成为定义。我们能给由extern
关键字标记的变量赋一个初始值,但是这么做也就抵消了extern
的作用。extern
语句如果包含初始值就不再时声明,而变成了定义:
extern double pi = 3.14159; // 定义
变量能且只能被定义一次,但是可以多次声明
2.3.1 引用
2.3.2 指针
void* 指针
void*
是一种特殊的指针类型,可用于存放任意对象的地址。一个void*
指针存放着一个地址,这点和其他指针类似。不同的是,我们对指针中到底是个什么类型的数据并不了解
double obj = 3.14, *pd = &obj; // 正确:void* 可以存放任意类型对象的地址
void *pv = &obj; // obj可以是任意对象的类型
pv = pd;
利用void*
指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*
指针,或者进行强制类型转换。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。概括说来,以void*
的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对。
指向指针的指针
一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。通过*的个数可以区分指针的级别。也就是说,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推:
指向指针的引用
要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号。)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引l用的是一个int指针。
2.4 const限定符
有时我们希望定义这样一种变量,它的值不能被改变。例如,用一个变量来表示缓冲区的大小。使用变量的好处是当我们觉得缓冲区大小不再合适时,很容易对其进行调整。另一方面,也应随时警惕防止程序一不小心改变了这个值。为了满足这一要求,可以用关键字const对变量的类型加以限定:
const int bufSize =512;//输入缓冲区大小
const int i=get size(); //正确:运行时初始化
const int j=42; //正确:编译时初始化
const int k; //错误:k是一个未经初始化的常量
2.4.1 const 的引用
可以把引用绑定到const
对象上,就像绑定到其他对象上一样,我们呢称之为对象常量的引用。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:
const int ci = 1024;
const int &r1 = ci; // 正确:引用及其对应的对象都是常量
r1 = 42; // 错误:r1是对常量的引用
int &r2 = ci; // 错误:图让一个非常量引用指向一个常量对象
初始化和对const的引用
对const的引用可能引用一个并非const的对象
2.4.4 constexpr 和常量表达式
常量表达式(constexpression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。后面将会提到,C++语言中有几种情况下是要用到常量表达式的。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:
const int max_files = 20; // max_files 是常量表达式
const int limit = max_files + 1; // limit 是常量表达式
int staff_size = 27; // staff_size 不是常量表达式
const int sz = get_size(); // sz不是常量表达式
尽管staffsize的初始值是个字面值常量,但由于它的数据类型只是一个普通int而非constint,所以它不属于常量表达式。另一方面,尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。
constexpr 变量
在一个复杂系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。当然可以定义一个const变量并把它的初始值设为我们认为的某个常量表达式,但在实际使用时,尽管要求如此却常常发现初始值并非常量表达式的情况。可以这么说,在此种情况下,对象的定义和使用根本就是两回事儿。
C++11新标准规定,允许将变量声明为constexpr
类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr
的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf = 20; // 20是常量表达式
constexpr int limit = mf + 1; // 是常量表达式
constexpr int sz = size(); // 只有当size是一个constexpr函数时,才是一条正确的声明语句
尽管不能使用普通函数作为constexpr变量的初始值,但是正如6.5.2节(第214页)将要介绍的,新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。
指针和constexpr
必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效(相当于底层const),与指针所指的对象无关:
const int *p=nullptr; //p是一个指向整型常量的指针
constexpr int *g=nullptr; //g是一个指向整数的常量指针
p和g的类型相差甚远,p是一个指向常量的指针,而g是一个常量指针,其中的关键在于constexpr把它所定义的对象置为了顶层const(参见2.4.3节,第57页)。
2.5.1 类型别名
类型别名(typealias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。有两种方法可用于定义类型别名。传统的方法是使用关键字typedef:
typedef double wages; //wages是double的同义词
typedef wages base,*p; //base是double的同义词,p是double*的同义词
基本类型和复合类型均可使用
新标准规定了一种新的方法,使用别名声明(aliasdeclaration)来定义类型的别名:
using SI=Sales_item; //SI是Sales_item的同义词
2.5.2 auto 类型说明符
编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而要做到这一点并非那么容易,有时甚至根本做不到。为了解决这个问题,C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如double)不同,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一般会忽略掉顶层const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci = i;
auto b = ci; // b 的类型是int
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f = ci;//ci的推演类型是int,f是constint
还可以将引用的类型设为auto,此时原来的初始化规则仍然适用:
auto &g = ci; //g是一个整型常量引用,绑定到ci
auto &h = 42; //错误:不能为非常量引用绑定字面值
const auto &j =42; //正确:可以为常量引用绑定字面值
decltype 类型指示符
有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
decltype(f()) sum = x; //sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。换句话说,编译器为sum指定的类型是什么呢?就是假如f被调用的话将会返回的那个类型。
decltype处理顶层const和引l用的方式与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 和引用
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。如4.1.1节(第120页)将要介绍的,有些表达式将向decltype返回一个引用类型。一般来说当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值:
//decltype的结果可以是引用类型
int i = 42, *p = &i,&r = i;
decltype(r+0) b; //正确:加法的结果是int,因此b是一个(未初始化的)int
decltype(*p)c; //错误:c是int&,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。
另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
**decltype和auto的另一处重要区别是,decltype的结果类型与表达式形式密切相关。**有一种情况需要特别注意:对于decltype所用的表达式来说,如果变量名加上了一对括号,则得到引用:
//decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i)) d; // 错误: d是int&,必须初始化