《C++ Primer》笔记 C++基础部分

《C++ Primer》笔记 C++基础部分

变量和基本类型

基本内置类型

算术类型

类型char和类型signed char并不一样。尽管字符型有三种,但是字符的表现形式却只有两种:带符号的和无符号的。类型char实际上会表现为上述两种形式中的一种,具体是哪种由编译器决定。

类型转换

当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。

当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)

与一个算术表达式中既有无符号数又有int值时,那个int值就会转换成无符号数。把int转换成无符号数的过程和把int直接赋给无符号数—样。

字面值常量

enter description here

变量

变量定义

初始化不是赋值,初始化的含义是创建一个变量的时候赋予其一个初始值,而赋值的含义是把对象的当前值删除,而以一个新值来代替

默认初始化:如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。定义在函数体内部的内置类型变量将不被初始化(uninitialized)。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。

使用未初始化的值将带来无法预计的后果

变量声明和定义的关系

变量能且只能被定义一次,但可以被声明多次

标识符(identifier)

名字的作用域

建议:当你第一次使用变量的时候再定义它

复合类型

引用

引用(reference)为对象起了另外一个名字,引用类型引用(refers to)另外一种类型。

一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化

enter description here

指针

指针必须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,将拥有一个不确定的值。

指针的他(即地址)应属下列4种状态之一:

  1. 指向一个对象。
  2. 指叫紧邻对象所占空间的下一个位置。
  3. 空指针,意味着指针没有指向任何对象。
  4. 无效指针,也就是上述情况之外的其他值。

访问无效指针和空指针或者未知指针的后果无法预计

void*指针时一种特殊的指针类型,可用于存放任意对象的地址。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象做哪些操作。

理解复合类型的声明

引用本身不是对象,因此不能定义指向引用的指针。但是指针是对象,所以存在对指针的引用。

const限定符

const对象被设定为仅在文件内有效

对const的引用可能引用一个并非const的对象:常量引用仅对引用可参与的操作做出了限定,对于引用对象本身是不是一个常量未作限定。

顶层const

用名词顶层const(top-levelconst)表示指针本身是个常量,而用名词底层const(low-levelconst)表示指针所指的对象是一个常量。

constexpr 和常量表达式

常量表达式(constexpression)是指值不会改变而且在编译过程就能得到汁算结果的表达式。当然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。

C++11标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。

处理类型

auto类型说明符

编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而要做到这一点并非那么容易,有时甚至根本做不到。为了解决这个问题,C++11新标准引入了auto类型说明符,用它就能让编译器帮我们去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如double)不同,auto让编译器通始值来推算变量的类型。显然,auto定义的变量必须有初始值:

auto一般会忽略顶层const,同时底层const则会保留下来

decltype类型指示符

有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变蛩。为了满足这一要求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数椐类型。在此过程中,编译器分忻表达式并得到它的类型,却不实际汁算表达式的值:

1
decltype(f()) sum = x; // sum的类型就是函数f的返回类型

decltype 处理顶层const和引用的方式与auto有些许不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)


字符串、向量和数组

标准库类型string

如果提供一个字符串字面值,则该字面值中除了最后那个空字符外其他所有字符都被拷贝新建的string对象中。

直接初始化和拷贝初始化

如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器将等号右侧的初始值拷贝到新创建的对象中去。
与之相反,不使用等号,则执行的是直接初始化(direct initialization)

enter description here

string:size_type 类型

string类及其他大多数标准库类型都定义了几种配套的类型。这些配套体现了标准库类型与机器无关的特性,类型size_type即是其中的一种。在具体使用的时候,通过作用域操作符表明名字size_type是在类string中定义的。

字面值和string对象相加

当把string对象和字符字面值以及字符串字面值混在一条语句中使用的时候,必须保证每个加法运算符的两侧至少有一个是string对象

enter description here

标准库类型vector

如果循环体内部包含有向 vector对象添加元元素的语句,则不能使用范 围for循环

enter description here

不能用下标形式添加元素:vector对象(以及 string对象)的下标运算符可用于访问已存在的元素, 而不能用于添加元素。

迭代器介绍

所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符

类似于指针类型,迭代器也提供了对对象的间接访问。就迭代器而言,其对对象是容器中的元素或者者 string对象中的字符。使用迭代器可以访问某 个元素,迭代器也能从一个元素移动到另外一个元素。迭代器有有效和无效之分,这一点 和指针差不多。有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置,其他所有情况都属于无效。

使用迭代器

end成员负责返回指向容器(或 string对象)“尾元素的下一位置( one past the end) 的迭代器,也就是说,该迭代器指示的是容器的一个本不存在的“尾后( off the end)”元 素。这样的迭代器没什么实际含义,仅是个标记而已,表示我们已经处理完了容器中的所 有元素。end成员返回的迭代器常被称作尾后迭代器( off-the- end iterator)或者简称为尾 迭代器( end iterator)。特殊情况下如果容器为空,则beg1n和end返回的是同一个迭 代器。(都是尾后迭代器)

enter description here

迭代器运算符

enter description here

迭代器类型

就像不知道 string和 vector的 size type成员员到底是什么类型一样,一般来说我们也不知道(其实是无须知道)迭代器的精确类型。而实际上, 那些拥有迭代器的标准库类型使用 iterator和 const iterator来表示迭代器的类型

enter description here

某些对vector对象的操作会使迭代器失效

不能在for循环范围内向vector对象添加元素;任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效

迭代器运算

迭代器的递增运算令迭代器每次移动一个元素,所有的标准库容器都有支持递增运算 的迭代器。类似的,也能用=和!=对任意标准库类型的两个有效迭代器进行比较

vector和string迭代器支持更多的迭代器运算

enter description here

数组

不允许拷贝和赋值

不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值

理解复杂的数组声明

类型修饰符从右向左依次绑定,就数组而言,由内向外阅读比从右向左好很多

enter description here

访问数组元素

在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的 无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。

标准库函数

尽管能计算得到尾后指针,但这种用法极易出错。为了让指针的使用更简单、更安全, C++11新标准引引入了两个名为 begin和end的函数。

enter description here

多维数组

严格来说,C++语言中没有多维数组,通常所说的的多维数组其实是数组的数组。


表达式

基础

基本概念

重载运算符

C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自行定义其含义义。因为这种自定义的过程事实 上是为已存在的运算符赋予了另外一层含义,所以称之为重載运算符( overloaded operator)。

左值和右值

C++的表达式要不然是右值( rvalue,读作“are- value”),要不然就是左值( lvalue, 读作“ ell-value”)。这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以 位于赋值语句的左侧,右值则不能。

在C++语言中,二者的区别就没那么简单了。一个左值表达式的求值结果是一个对象 或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对 象。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。可以做一个简单 的归纳:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的 时候,用用的是对象的身份(在内存中的位置)。

不同的运算符对运算对象的要求各不相同,有的需要左值运算对象、有的需要右值运 算对象:返回值也有差异,有的得到左值结果、有的得到右值结果。一个重要的原则是:在需要右值的地方可以用左值来代替,但 是不能把右值当成左值(也就是位置)使用。 当一个左值被当成右值使用时,实际使用的 是它的内容(值)。

求值顺序

对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为

举个简单的例子,<<运算符 没有明确规定何时以及如何对运算对象求值,因此下面的输出表达式是末定义的:

1
2
int i = 0;
cout << i << "+" << ++i << endl; // 未定义的

C++语言没有明确规定大多数二元运算符的求值顺序,给编译器优化留下了 余地。这种策略实际上是在代码生成效率和程序潜在缺陷之间进行了权衡.

算术运算符

enter description here

算术运算符能作用域任何算术类型以及任意能转化为算术类型的类型。

算术表达式有可能产生未定义的结果。一部分原因是数学性质本身:例如除数是0 的情况;另外一部分则源于计算机的特点:例如溢出,当计算的结果超出该类型所能表 示的范围时就会产生溢出。

除法和取模运算

在除法运算中,如果两个运算对象的符号相同则商为正(如果不为0的话),否则商 为负。C++语言的早期版本允许结果为负值的商向上或向下取整,C++11新标准则规定商 律向0取整(即直接切除小数部分)

根据取余运算的定义,如果m和n是整数且n非0,则表达式(m/n)*n+m%n的求值 结果与m相等。隐含的意思是,如果m%n不等于0,则它的符号和m相同。C++语言的早 期版本允许m%n的符号匹配n的符号,而且商向负无穷一侧取整,这一方式在新标准中 已经被禁止使用了。除了-m导致溢出的特殊情况,其他时候(-m)/n和m/(-n)都等于 (m/n),m%(-n)等于m%n,(-m)%n等于-(m%n)。

具体示例如下:

enter description here

逻辑运算符和关系运算符

enter description here

逻辑与运算符和逻辑或运算符都是先求左側运算对象的值再求右侧运算对象的值,当 且仅当左側运算对象无法确定表达式的结果时オ会计算右侧运算对象的值。这种种策略称为 短路求值( short- circuit evaluation)。

赋值运算符

赋值运算符的左侧运算对象必须是一个可修改的左值

递增运算符和递减运算符

递增和递减运算符有两种形式:前置版本和后置版本。前置递增运算符首先将运算对象加1(或减1),然后将改变后的对象作为求 值结果。后置版本也会将运算对象加1(或减1),但是求值结果是运算对象改变之前那个 值的副本

建议:除非必须,否则不用递增递减运算符的后置版本:
有C语言背景的读者可能对优先使用前置版本递增运算符有所疑间问,其实原因非常 简单:前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回改变了的运 算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如 果我们不需要修改前的值,那么后置版本的操作就是一种浪费。

sizeof运算符

sizeof运算符返回一条表达式或一个类型名字所占的字节数。sizeof运算符满足右结合律,其所得得的值是一个size_t类型

运算符的运算对象有两种形式:

1
2
3
sizeof(type)
sizeof expr

在第二种形式中,sizeof返回的是表达式结果类型的大小。与众不同的一点是,sizeof并不实际计算其运算对象的值:

因为执行sizeof运算能得到整个数组的大小,所以可以用数组的大小除以单个元素的大小得到数组中的元素个数:

1
2
constexpr size_t sz = sizeof(ia)/sizeof(*ia);
int arr2[sz]; // ok sizeof returns a constant expression

逗号运算符

逗号运算符( comma operator)含有两个运算对象,按照从左向右的顺序依次求值 和逻辑与、逻辑或以及条件运算符一样,逗号运算符也规定了运算对象求值的顺序。 对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右側表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值。

类型转换

其他隐式类型转换

数组转换为指针:在大多数用到数组的表达式中,数组自动转换为指向数组首元素的指针

指针的转换:C++还规定了几种其他的指针转换方式,包括常量整数值0或者者字面值 nu11ptr能转换成任意指针类型:指向任意非常量的指针能转换成void;指向任意对 象的指针能转换成 const void

转换为布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制

转换为常量:允许将指向非常量类型的指针转换成为相应常量类型的指针,对于引用也是这样。

显式转换

待整理

运算符优先级表

enter description here

enter description here


语句

条件语句

悬垂else

当一个if语句嵌套在另一个if语句内部时,很可能if分支会多于else分支。事实上,之前那个成绩转换的程序就有4个if分支,而只有2个else分支。这时候问题 出现了:我们怎么知道某个给定的else是和和哪个if匹配呢? 这个问题通常称作悬垂else( dangling else),在那些既有if语句又有if else语句的编程语言中是个普遍存在的问题。不同语言解决该问题的思路也不同,就就C++而言, 它规定else与离它最近的尚未匹配的if匹配,从而消除了程序的的二义性。

case标签

case关键字和它对应的值一起被称为case标签( case label)。case标签签必须是整 型常量表达式

switch内部的变量定义

如前所述, switch的执行流程有可能会跨过某些case标标签。如果程序跳转到了某个特定的case,则switch结构中该该case标签之前的部分会被忽略掉。这种忽略掉一 部分代码的行为引出了一个有趣的问题:如果被略过的代码中含有变量的定义该怎么办? 答案是:如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为。

迭代语句

范围for语句

C++11新标准引入了一种更简单的for语句,这种语句可以遍历容器或其他序列的 所有元素。范围for语句( range for statement)的语法形式是:

1
2
for (declaration expression)
statement

expression表示的必须是一个序列,比如用花括号括起来的初始值列表、数组或者 vector或string等类型的对象,这些类型的共同特点是拥有能返回迭代器的 begin和end成员。

declaration定义一个变量,序列中的每个元素都得能转换成该变量的类型。确保类型相容最简单的办法是使用auto类型说明符,这个关键字可以令编译器帮助我们指定合适的类型。

如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型 每次迭代都会重新定义循环控制变量,并将其初始化成序列中的下一个值,之后オ会 执行 statement。像往常一样, statement可以是一条单独的语句也可以是一个块。所有元素 都处理完毕后循环终止。

try语句块和异常处理

异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。当程序的某部分检测到一个他无法处理的问题的时候,需要用到异常处理。

异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持。在C++语言 中,异常处理包括

  • throw表达式( throw expression),异常检测部分使用 throw表达式来表示它遇到 了无法处理的问题。我们说 throw引发( raise)了异常。
  • ty语句块( try block),异常处理部分使用try语句块处理异常。try语句块以 关键字try开始,并以一个或多个 catch子句( catch clause)结束。try语句块 中代码抛出的异常通常会被某个 catch子句处理。因为 catch子句“处理”异常, 所以它们也被称作异常处理代码( exception handler)。
  • 套异常类( exception class),用于在 throw表达式和相关的 catch子句之间传 递异常的具体信息。

标注异常

enter description here


函数

函数基础

我们通过调用运算符( call operator)来执行函数。调用运算符的形式是一对圆括물 它作用于一个表达式,该表达式是函数或者指向函数的指针:圆括号之内是一个用逗号隔 开的实参( argument)列列表,我们用实参初始化函数的形参。调调用表达式的类型就是函数 的返回类型。

函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调 用函数。此时,主调调函数( calling function)的执行被暂时中断,被调函数( called function) 开始执行。

形参和实参

实参是形参的初始值。尽管实参和形参存在对应关系,但是并没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值。

局部对象

名字有作用域,对象有生命周期(lifetime):名字的作用域是程序文本的一部分,名字在其中可见;对象的生命周期是程序执行过程中该对象存在的一段时间。

如果局部静态变量没有显式的初始值,它将执行初始化:内置类型的局部静态变量初始化为0

函数声明

函数的声明不包含函数体,所以也不需要形参的名字,但是加上名字能够让使用者更好地理解函数的功能。

分离式编译

分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译

编译以后,如果我们修改了其中一个源文件,那么只需要重新编译那个改动的文件即可

参数传递

当形参是引用类型时,我们说它对应的实参被引用传递( Dassed by reference )或者函 数被传引用週用( called by reference)。和其他引用一样,引用形参也是它绑定的对象的别 名;也就是说,引用形参是它对应的实参的别名。

当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们们说这样的实参 被值传递( passed by value)或者者函数被传值调用( called by value)

使用引用避免拷贝

拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括括1O类型在内) 根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类 型的对象。

如果函数无需改变引用形参的值,最好将其声明为常量引用

const形参和实参

当形参时const时,其为顶层const,顶层const作用于对象本身。

和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层 const。换句话说,形参的顶层 const被忽略掉了。当形参有顶层 const时,传给它常量对象或者非常量对象都是 可以的:

数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别 是:不允许拷贝数组以及使用数组时(通常)会将其转换成 指针。因为不能拷贝数组,所以我们无法以值传递的方式使用 数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的 是指向数组首元素的指针。

为了程序的可读性,我们可以把形参写成类似数组的形式:

1
2
3
void print( const *int);
void print( const int[]);
void print( const int[10]); // 这里的维度表示我们期望有多少元素,实际不一定

管理指针形参的三种常用技术:

  • 使用标记指定数组长度,要求数组本身包含一个结束标记
  • 使用标准库规范,传递指向数组首元素和数组尾元素的指针
  • 显式传递一个表示数组大小的形参

传递多维数组

和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针 。因为我们处理的是数组的数组,所以首元素本身就是一个数组, 指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的 部分,不能省略:

含有可变形参的参数

为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:如如果所 有的实参类型相同,可以传递一个名为initializer_list的标准库类型:如果实参的 类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板,

initializer_list 形参

如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_listt类型的形参。initializer_list是一种标准库类型,用于表示 某种特定类型的值的数组

省略符形参

省略符形参是为了便于C++程序访访问某些特殊的C代码而设置的,这些们代码使用了名 为 varargs的C标准库功能。

省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:

  • void foo(parm list, …);
  • void foo(…)

第一种形式指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的 类型检査。省略符形参所对应的实参无须类型检査。在第一种形式中,形参声明后面的逗 号是可选的。

返回类型和return语句

不要返回局部对象的引用或指针

函数完成后,它所占用的存储空间也随之被释放掉。因此, 函数终止意味着局部变量的引用将指向不再有效的内存区域。会引发未定义行为

引用返回左值

调用一个返回引用的函数返回左值,返回其他类型得到右值

函数重载

不允许两个函数除了返回类型外其他所有的要素都相同

一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来

调用重载函数

定义了一组重载函数后,我们需要以合理的实参调用它们。函数匹配( functionmatching)是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定( overload resolution)。编编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。

特殊用途语言特性

默认实参

某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参( default argument)。调调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。

一旦某个形参被赋予了默认值,后面所有的额形参都要赋予默认值。所以当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面

在给定的作用域中一个形参只能被赋予一次默认实参。换句句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。

局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参:

内联函数和constexpr函数

在大多数机器上,一次次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。

内联函数可以避免函数调用的开销

将函数定义为内联函数(inline),通常就是把它在每一个调用点上“内联地”展开。函数返回类型前面加上关键字inline,这样就可以将它声明为内联函数:

1
2
3
4
inline const string & shortString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}

一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而而且一个75行的函数也不大可能在调用点内联地展开。

constexpr函数

constexpr函数( constexpr function)是指能用于常量表达式的函数。定义 constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条 return语句:

把內联函数和 constexpr函数放在头文件内和其他函数不一样,内联函数和 constexpr函数可以在程序中多次定义。毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者 constexpr函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和 constexpr函数通常定义在头文件中。

调试帮助

程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能: assert和 NDEBUG

assert 预处理宏

assert是一种预处理宏( preprocessor marco)。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。 assert宏使用一个表达式作为它的条件:assert(expr)

首先对expr求值,如如果表达式为假(即0), assert输出信息并终止程序的执行。如果表达式为真(即非0), assert什么也不做。

assert宏定义在 assert头文件中。如我们所知,预处理名字由预处理器而非编译器管理,因此我们可以直接使用预处理名字而无须提供uS1ng明。也就是说,我们应该使用 assert而不是std:: assert,也不需要为 assert提供using声明。

NDEBUG 预处理变量

assert的行为依赖于一个名为 NDEBUG的预处理变量的状态。如果定义了 NDEBUG,则 assert什么也不做。默认状态下没有定义 NDEBUG,此时 assert将执行运行时检查。我们可以使用一个# define语句定义 NDEBUG,从而关闭调试状态。同时,很多编译器都提供了一个命令行选项使我们可以定义预处理变量

函数匹配

确定候选函数和可行函数

函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数( candidate function)。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。

第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数数( viable function)。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。

寻找最佳匹配

函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。实参类型与形参类型越接近,他们匹配得越好。

含有多个形参的函数匹配

当实参的数量有两个或更多时,函数匹配就比较复杂了。编译器依次检査每个实参以确定哪个函数是最佳匹配。如如果有且只有个函数满足下列条件,则匹配成功・该该函数每个实参的匹配都不劣于其他可行函数需要的匹配至少有一个实参的匹配优于其他可行函数提供的匹配。如果在检査了所有实参之后没有任何一个函数脱颖而出,则则该调用是错误的。

实参类型转换

为了确定最佳匹配,编译器将实参类型到形参类型的转換划分成儿个等级,具体排序如

  1. 精确匹配,包括以下情况:实参类型和形参类型相同・实参从数组类型或函数类型转换成对应的指针类型。向实参添加顶层 const或者从实参中删除顶层 const
  2. 通过 const转换实现的匹配
  3. 通过过类型提升实现的匹配
  4. 通过算术类型转换或指针转换实现的匹配。
  5. 通过类类型转换实现的匹配配(参见14.9节

函数指针

函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型,函数的类型由它的返回类型和形参类型共同决定,与函数名无关。

想要声明一个可以指向该函数的指针,只需要用指针替换函数名即可

当我们把函数名作为一个值使用时,该函数自动地转换成指针。

此外,我们们还能直接使用指向函数的指针调用该函数,无须提前解引用指针

函数指针形参

和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用:

返回指向函数的指针

和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,我们们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。与往常一样,要想声明一个返回函数指针的函数,最简单的办法是使用类型别名


类的基本思想是数据抽象( data abstraction)和封装( encapsulation)。数据抽象是二种依赖于接口( interface)和实现( implementation)分离的编程程(以及设计)技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。

封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。

定义抽象数据类型

定义类的基本

引入 this

成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this

对于我们来说,this形参是隐式定义的。实际上,任何自定义名为this的参数或变量的行为都是非法的。我们们可以在成员函数体内部使用this因此尽管没有必要,但我们还是能把isbn定义成如下的形式:

1
std::string isbn() const { return this->bookNo; }

引入const 成员函数

上面的ibsn函数的另一个关键之处是紧随参数列表之后的 const关键字,这里, const的作用是修改隐式this指针的类型。

默认情况下,this的类型是指向类类型非常量版本的常量指针。尽管this是隐式的,但它仍然需要遵循初始化规则,意味着(在默认情况下)我们不能把this绑定到一个常量对象上。

C++语言的做法是允许把 const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的 const表示this是一个指向常量的指针。像这样使用 const的成员函数被称作常量成员函数( const member function)。

类作用域和成员函数

编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体(如果有的话)。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。

在类外部定义成员函数

当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。也就是说,返回类型、参数列表和函数名都得与类内部的声明保持一致。如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定 const属性。同时,类外部定义的成员的名字必须包含它所属的类名

定义一个返回this对象的函数

一般来说,当我们定义的函数类似于某个内置运算符时,应该令该函数的行为尽量模仿这个运算符。内置的赋值运算符把它的左側运算对象当成左值返回,因此为了与它保持一致,combine函数必须返回引用类型。

构造函数

每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数( constructor)。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数

构造函数的名字和类名相同。和其他函数不一样的是,构造函数没有返回类型;除此之外类似于其他的函数,构造函数也有一个(可能为空的)参数列表和一个(可能为空的)函数体。类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量或参数类型上有所区别。

不同于其他成员函数,构造函数不能被声明成 const的

合成的默认构造函数

我们没有为这些对象提供初始值,因此我们知道它们执行了默认初始化。类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数( default constructor)默认构造函数无须任何实参。

如我们所见,默认构造函数在很多方面都有其特殊性。其中之一是,如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数。编译器创建的构造函数又被称为合成的默认构造函数( synthesized defaultconstructor)。对于大多数类来说,这个合成的默认构造函数将按照如下规则初始化类的数据成员:

  • 如果存在类内的初始值,用它来初始化成员
  • 否则,默认初始化该成员。

某些类不能依赖于合成的默认构造函数

对于一个普通的类来说,必须定义它自己的默认构造函数,原因有三:

第一个原因也是最容易理解的一个原因就是编译器只有在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。

第二个原因是对于某些类来说,合成的默认构造函数可能执行错误的操作。回忆我们之前介绍过的,如果定义在块中的内置类型或复合类型(比如数组和指针)的对象被默认初始化,则它们的值将是未定义的。该准则则同样适用于默认初始化的内置类型成员。因此,含有内置类型或复合类型成员的类应该在类的内部初始化这些成员,或者定义一个自己的默认构造函数。否则,用户在创建类的对象时就可能得到未定义的值。

第三个原因是有的时候编译器不能为某些类合成默认的构造函数

=default的含义

在C++11标准中,如果我们需要默认行为,那么可以通过在参数列表后面写上=default来要求编译器生成构造函数

构造函数的初始值列表

1
2
3
Sales_data(const std: string &s): bookno(s) { } ;
Sales_data(const std: string &s, unsigned n, double p):bookno(s), units_sold(n), revenue(p*n) { } ;

这两个定义中出现了新的部分,即冒号以及冒号和花括号之间的代码,其中花括号定义了函数体。我们把新出现的部分称为构造函数初始值列表( constructor initialize list),它负责为新创建的对象的一个或几个数据成员赋初值。构造函数初始值是成员名字的一个列表,每个名学后面紧跟括号括起来的(或者在花括号内的)成员初始值。不同成员的初始化通过逗号分隔开来

拷贝、赋值和析构

某些类不能依赖于合成的版本

尽管编译器能替我们合成拷贝、赋值和销毁的操作,但是必须要清楚的一点是,对于某些类来说合成的版本无法正常工作。特別是,当类需要分配类对象之外的资源时,合成的版本常常会失效

访问控制与封装

定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。

定义在 private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问, private部分封装了(即隐藏了)类的实现细节。

使用class 和struct关键字

我们可以使用class和struct这两个关键字中的任何一个定义类。唯一的一点区别是, struct和class的默认访问权限不太一样。类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义。如果我们使用 struct关键字,则定义在第一个访问说明符之前的成员是public的;相反,如果我们使用class关键字,则这些成员是private的

友元

类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元( friend)。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可:

友元的声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类成员也不受它所在区域访问控制级别的约束

封装有两个重要的优点
确保用户代码不会无意间破坏封装对象的状态。
被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。

友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明

类的其他特性

类成员再探

除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名。由类定义的类型名字和其他成员一样存在访问限制,可以是public或者private中的一种

令成员函数作为内联函数

在类中,常有一些规模较小的函数适合于被声明成内联函数。如我们之前所见的,定义在类内部的成员函数是自动inline的。

我们可以在类的内部把inline作为声明的一部分显式地声明成员函数,同样的,也能在类的外部用inline关键字修饰函数的定义

enter description here

可变数据成员

有时(但并不频繁)会发生这样一种情况,我们希望能修改类的某个数据成员,即使是在一个 const成员函数内。可以通过在变量的声明中加入 mutable关键字做到这点

一个可変数据成员( mutable data member)永远不会是 const,即使它是 const对象的成员。因此,一个 const成员函数可以改变一个可变成员的值。

enter description here

返回*this的成员函数

返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本

基于const的重载

通过区分成员函数是否是 const的,我们可以对其进行重载,其原因与我们之前根据指针参数是否指向 const而重载函数的原因差不多。具体说

类类型

每个类定义了唯一的类型。对于两个类来说,即使它们的成员完全一样,这两个类也是两个不同的类型。

类的声明

就像可以把函数的声明和定义分离开来一样,我们也能仅仅声明类而暂时不定义它

1
class Screen; // screen类的声明

这种声明有时被称作前向声明( forward declaration),它向程序中引入了名字 Screen并且指明 Screen是一种类类型。对于类型 Screen来说,在它声明之后定义之前是一个不完全类型( incomplete type),也就是说,此时我们已知 Screen是一个类型,但是不清楚它到底包含哪些成员。

不完全类型只能在非常有限的情景下使用:可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。

友元再探

类还可以把其他的类定义成友元,也可以把其他类(之前已定义过的)的成员函数定义成友元。此外,友元函数能定义在类的内部,这样的函数是隐式内联的。

如果一个类定义了友元类,则友元类的成员函数可以访问此类包括非公有函数在内的所有成员。

友元不具备传递性

令成员函数作为友元

除了令整个Window mgr作为友元之外, Screen还可以只为clear提供访问权限。当把一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类:

函数重载和友元

尽管重载函数的名字相同,但它们仍然是不同的函数。因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明:

友元声明和作用域

类和非成员函数的声明不是必须在它们的友元声明之前。当一个名字第一次出现在个友元声明中时,我们隐式地假定该名字在当前作用域中是可见的。

类的作用域

每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由对象、引用用或者指针使用成员访问运算符来访问。对于类类型成员则使用作用域运算符访问。不论哪种情况,跟在运算符之后的名字都必须是对应类的成员

作用域和定义在类外部的成员

在类的外部,成员的名字被隐藏起来了。一旦遇到了类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体。结果就是,我们可以直接使用类的其他成员而无须再次授权了。

enter description here

另一方面,函数的返回类型通常出现在函数名之前。因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。这时,返回类型必须指明它是哪个类的成员。

名字查找和类的作用域

名字查找( name lookup)(寻找与所用名字最匹配的声明的过程)的过程比较直截了当:

  • 首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明。
  • 如果没找到,继续查找外层作用域。
  • 如果最终没有找到匹配的声明,则程序报错。

一般来说,内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过。然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表种类型,则类不能在之后重新定义该名字:

enter description here

构造函数再探

构造函数初始值列表

构造函数的初始值有时必不可少

有时我们可以忽略数据成员初始化和赋值之间的差异,但并非总能这样。如果成员是const或者是引用的话,必须将其初始化。类似的,当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。

一些数据成员必须初始化,建议养成使用构造函数初始化值的习惯

成员初始化的顺序

构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。

成员的初始化顺序与它们在类定义中的出现顺序一致:第一个成员先被初始化,然后第二个,以此类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序

委托构造函数

C++11新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数( delegating constructor)。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自自己的一些(或者全部)职责委托给了其他构造函数。

和其他构造函数一样,一个委托构造函数也有一个成员初始值的列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。和其他成员初始值一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中另外一个构造函数匹配。

enter description here

隐式的类类型转换

我们能为类定义隐式转换规则。如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们]把这种构造函数称作转换构造函数

抑制构造函数定义的隐式转换

在要求隐式转换的程序上下文中,我们们可以通过将构造函数声明为explicit加以阻止:

enter description here

聚合类

聚合类( aggregate class)使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。当一个类满足如下条件时,我们说它是聚合的:

  • 所有成员都是public的
  • 没有定义任何构造函数。
  • 没有类内初始值
  • 没有基类,也没有 virtual函数,关于这部分知识我们将在第15章详细介绍

字面值常量类

数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但它符合下述要求,则它也是一个字面值常量类

  • 数据成员都必须是字面值类型。
  • 类必须至少含有一个 constexpr构造造函数。
  • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的 constexpr构造函数
  • 类必须使用析构函数的默认定义,该成员负责销毀类的对象

类的静态成员

有的时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。

声明静态成员

我们通过在成员的声明之前加上关键字static使得其与类关联在一起。和其他成员样,静态成员可以是 public的或 private的。静态数据成员的类型可以是常量、引用、指针、类型等

我们使用作用域运算符直接访问静态成员:

1
2
double r;
r = Account::rate();

定义静态成员

和其他的成员函数一样,我们既可以在类的内部也可以在类的外部定义静态成员函数。当在类的外部定义静态成员时,不能重复 static关键字,该关键字只出现在类内部的声明语句:

1
2
3
4
void Account::rate( double newRate)
{
interesRate = newRate;
}

我们不能在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员。

静态成员的类内初始化

通常情况下・类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供 const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr