C++笔记

C++ 学习笔记 简单和平凡要比复杂和精致重要的多

不想翻页所以顺序是基础在下面 大家将就下

四 标准库

declaration and definitions
1a .cpp file is a compile unit.
2Only declaration are allowed to be in .h
extern variables
function prototypes
class/struct declaration

类class
类是用户自定义类型。C++类是创建新类型的工具,创建出的新类型可以像内置类型一样方便地使用。
定义新类型的基本思想是将实现的细节(例如某种类型对象的数据存储布局)与正确使用它的必要属性(例如,可以访问数据的函数的完整列表)分离。这种分离的最佳表达方式是:通过一个专用接口引导数据结构及其内部辅助例程的使用。 (以成员函数操作private数据,成员函数就是接口,对象以接口和外界打交道)

类的简要概括
·一个类就是一个用户自定义类型
·一个类由一组成员构成。最常见的成员类别是数据成员和成员函数
·成员函数可定义初始化(创建),拷贝,移动和清理(析构)等语义
·对对象使用.(点)访问成员,对指针使用->(箭头)访问成员
·可以为类定义运算符,如+,!和[]
·一个类就是一个包含其成员的名字空间
·public成员提供类的接口,private成员提供实现细节
·struct是成员默认为public的class

声明于类定义内的函数称为成员函数。由于不同结构可能有同名成员函数,在定义成员函数时必须指定结构名。
默认情况下对象是可以拷贝的

private
私有的成员,它们只能被成员函数使用。
public
构成类对象的公共接口
struct就是一个成员默认为公有的class,成员函数的声明和使用是一样的,但是非成员函数禁止使用私有成员。

限制只有一组显示声明的函数才能访问一个数据结构可以带来多方面的好处
·任何导致对象保存非法数据的错误都必然是由成员函数中的代码引起的。这意味着调试的第一阶段–定位–甚至在程序运行之前就完成了
·class的行为的任何改变都受到且必然收到其成员的改变的影响。特别是,如果我们改变了一个类的表示方式,就只能修改成员函数来利用新的表示方式。
·用户代码则直接依赖于公共接口,因此无需重写
·另一个好处是潜在用户为了学习类的使用只需要观察成员函数的定义
·还有一个更微妙的最重要的好处,聚焦于设计一个好的接口能产生更好的代码,因为我们可以对调试投入更多的思考和时间。将精力花费在程序正确使用的相关问题上更有价值

私有数据的保护依赖于对类成员名的使用限制。因此通过地址操作和显示类型转换可以绕过私有保护,这是一种欺骗。

下面的语法结构
class X{…};
称为类定义(class definition)它定义了一个名为X的类型。由于历史原因,类定义常常被称为类声明(class declaration)这样叫它的另一个原因是,与其他并非定义的C++声明类似,我们可以在不同源文件中使用#include重复类定义而不会违反单一定义规则。

构造函数
这种函数的本质是构造一个给定类型的值,因此被称为构造函数(constructor),构造函数的显著特征是与类具有相同的名字。
我们可以指明构造函数不用做隐士类型转换。如果构造函数的声明带有关键字explicit,则它只能用于初始化和显示类型转换
默认情况下,应该将单参数的构造函数声明为explicit.

常量成员函数
int month() const {return m;}
函数声明中(空)参数列表后的const指出这些函数不会修改Date状态。

//表达式 this 引用的就是调用此成员函数的对象。
在非static成员函数中,关键字this是指向调用它的对象的指针。在类X的非const成员函数中,this的类型是X
。但是this被当作是一个右值,因此我们无法获得this的地址或给它赋值。在类X的const成员函数中this的类型是const X*,以防止修改对象。

this:pointer to the caller

成员访问
我们可以通过对类X的对象使用.点运算符或对X对象的指针使用->箭头运算符来访问X的成员。在类的内部访问成员则不需要任何运算符。成员函数可以在一个成员声明前就引用它

static成员
是类的一部分但不是某个类对象一部分的变量称为static成员。static成员只有唯一副本,而不是想非static成员那样每个对象都有一个副本,需要访问类成员而不需要通过特定对象调用的函数称为static成员函数。
如果使用了static函数或数据成员,我们就必须在某处定义它们。
多线程中,static数据成员需要需要某种锁机制或访问规则来避免竞争。

成员类型
成员类(member class,常称为嵌套类,nested class)可以引用其所属类的类型和static成员。
相反,一个类没有任何特殊权限能访问其嵌入类的成员。

·一个构造函数指出此类型的对象/变量如何初始化
·一组允许用户检查Date的函数,这些函数标记为const,表明他们不会修改调用它们的对象/变量的状态
·一组允许用户无须了解表示细节也无需摆弄复杂语法即可修改Date的函数
·隐式定义操作,允许Date自由拷贝
·类Bad_date,用来报告错误,抛出异常
·一组有用的辅助函数。这些函数不是类成员,不能直接访问Date的表示。

构造 清理 拷贝和移动
移动(move)和拷贝(copy)的区别在于,拷贝操作后两个对象具有相同的值,移动操作后移动源不一定具有其原始值。如果移动源对象在操作后不再使用,我们就可以使用移动操作。

class X
{
X(Sometype); //普通构造函数 创建一个对象
X(); //默认构造函数
X(const X&); //拷贝构造函数
X(X&&); //移动构造函数
X& operator=(const X&); //拷贝赋值运算符: 清理目标对象并进行拷贝
X& operator=(X&&) //移动赋值运算符: 清理目标对象并进行移动
~X(); //析构函数:清理
};

构造函数和析构函数
我们可以通过定义一个构造函数来指出一个类的对象应如何初始化。与构造函数对应,我们还可以定义一个析构函数来确保对象销毁时进行恰当的“清理操作”。

构造函数和不变式
与类同名的成员称为构造函数(constructor)
构造函数的声明指出其参数列表(与一个函数的参数列表完全一样)但未指出返回类型。类名不可用于此类内的普通成员函数,数据成员,成员类型
struct S{
S(); //正确
void S(int); //错误 不能为构造函数指定返回类型
int S; //错误 类名只能表示构造函数
enum S{foo,bar}; //错误 类名只能表示构造函数
};
构造函数从本质上来说是在创建一个指定类型的值。

不变式
构造函数的任务是初始化该类的一个对象。一般而言初始化操作必须建立一个类不变式(class invariant),所谓类不变式就是当成员函数(从类外)被调用时必须保持的某些东西
例如
class Vector{
public:
Vector(int s);
private:
double* elem; //elem指向一个数组,保存sz个double
int sz; //sz非负
};
我们用注释陈述不变式:elem指向一个数组,保存sz个double,sz非负,构造函数必须保证这两点为真。
Vecotr::Vector(int s)
{
if(s<0) throw Bad_size{s};
sz = s;
elem = new double[s];
}
此构造函数尝试建立不变式,如果失败就抛出一个异常,如果构造函数无法建立不变式,则不应创建对象且必须确保没有资源泄漏。需要获取并在用完后最终(显示或隐式地)归还(释放)的任何东西都是资源,例如内存,锁,文件句柄以及线程句柄。

为什么要定义一个不变式呢?
·聚焦于类的设计工作
·理清类的行为
·简化成员函数的定义
·理清类的资源管理
·简化类的文档

析构函数和资源
在对象被销毁时保证它会被调用,这样的函数被称为析构函数
析构函数不接受参数,每个类只能有一个析构函数。当一个自动变量离开作用域时,自由空间中的一个对象被释放时,等等时刻析构函数就会被调用。
这种基于构造函数/析构函数的资源管理风格被称为资源获取即初始化或者简称RAII

构造函数会“自顶向下”地创建一个类对象
1首先构造函数会调用其基类的构造函数
2然后它调用成员的构造函数
3最后它执行自身的函数体
析构函数按相反的顺序“拆除”一个对象:
1首先析构函数执行自身的函数体
2然后它调用其成员的析构函数
3最后调用其基类的析构函数
特别是,一个virtual基类必须在任何可能使用它的基类之前构造,并在它们之后销毁。

当对象退出作用域或被delete释放时,析构函数会被隐式调用。显示调用析构函数通常是不必要的。

virtual析构函数
析构函数可以声明为virtual,而且对于含有虚函数的类通常就应该这么做
我们需要一个virtual析构函数的原因是,如果通常是通过基类提供的接口来操纵一个对象,那么通常也应该通过此接口来delete它

我们不能为内置类型定义构造函数,但能用一个恰当类型的值初始化内置类型对象。

C++作者使用{}语法来明确表示正在进行初始化,而不(仅仅)是在赋值,调用函数或是声明函数,只要是在构造对象的地方,我们都可以用{}初始化语法为构造函数提供参数。{}初始化有时也被称为通用(universal)初始化:这种语法可以用在任何地方。而且{}初始化还是一致的:无论我在哪里用语法{v}将类型X的对象初始化为值v,都会创建相同的值(X{v})。{}初始化器语法不允许窄化转换,这是倾向于使用{}风格而不是()或=的另一个原因

默认构造函数
无参的构造函数被称为默认构造函数(default constructor)。
如果构造对象的时候未指定参数或提供了一个空初始化器列表,则会调用默认构造函数。
Vector v1;
Vector v2{};

指针类型的默认值为nullptr

引用和const必须被初始化,因此,一个包含这些成员的类不能默认构造,除非程序员提供了类内成员初始化器或定义了一个默认构造函数来初始化它们。

接受单一std::initializer_list参数的构造函数被称为初始化器列表构造函数(initializer-list constructor)。一个初始化器列表构造函数使用一个{}列表作为其初始化值来构造对象。

一个初始化器列表的长度可以任意,但它必须是同构的,即所有元素的类型都必须是模版参数T,或者可以隐式转换为T。

可以接受一个initializer_list参数的函数作为一个序列来访问,即通过成员函数begin(),end(),size(),不幸的是initializer_list不提供下标操作。

initializer_list的元素是不可变的,不要考虑修改它们的值。

成员初始化
通过成员初始化器列表(member initialize list)给出成员的构造函数的参数。成员初始化器列表是以一个冒号开始,后面成员初始化器用逗号间隔。

类自身的构造函数在其函数体执行之前会优先调用成员的构造函数,成员的构造函数按成员在类中声明的顺序调用,而不是按成员在初始化器列表中出现的顺序。

委托构造函数
如果希望两个构造函数做相同的操作。
class X
{
int a;
public:
X(int x){if(0<x && x<=max) a=x;else throw Bad_X(x);}
X():X{42}{}
X(string s):X{to(s)}{}
};
使用一个成员风格的初始化器,但用的是类自身的名字(也是构造函数名),他会调用另一个构造函数,作为这个构造过程的一部分。这样的构造函数被称为委托构造函数(delegating constructor,有时也称为转发构造函数)

直到构造函数执行完毕,对象才被认为完成构造,当使用委托构造函数时,委托者执行完毕才表明构造完成,仅仅被委托者执行完毕是不够的

static成员初始化
一般来说,static成员声明充当类外定义的声明
class Node
{
static int node_count; //声明
};
int Node::node_count = 0; //定义
在类内声明中初始化static成员也是可能的,条件是static成员必须是整型或枚举类型的const,或字面值类型constexpr。且初始化器必须是一个常量表达式(constant-expression)

拷贝和移动
移动操作不能抛出异常,而拷贝操作则可以(因为拷贝可能需要获取资源),移动操作通常比拷贝操作更高效。当编写一个移动操作时,应该将原对象置于一个合法的但未指明的状态,因为它最终会被销毁,而析构函数不能销毁一个处于非法状态的对象。而且,标准库算法要求能够向一个一处状态的对象进行赋值(使用移动或者拷贝),因此设计移动操作的时候不要让它抛出异常,并令源对象处于可析构和赋值的状态。

拷贝
·拷贝构造函数:X(const X&)
·拷贝赋值运算符:X& operator=(const X&)

拷贝的含义
一个构造函数或拷贝赋值运算符应该怎么做才会被认为是一次正确的拷贝操作呢
拷贝操作必须满足两个原则
·等价性:在x=y之后,对x和y执行相同的操作应该得到相同的结果。特别是,如果它们的类型定义了==,应该有x==y,并且对任何质只依赖于x和y的值的函数f()(与依赖于x和y的地址的函数不同)有f(x)==f(y)
·独立性:在x=y之后,对x的操作不会隐式地改变y的状态,即只要f(x)未引用y,它就不会改变y的值。
例如
struct S
{
int *p; //一个指针
};

S x{new int{0}};

void f()
{
S y{x}; //拷贝x

*p.y = 1;  		//改变y: 影响了x
*x.p = 2;		//改变x: 影响了y
delete y.p;     //影响了x和y
y.p = new int{3};	//正确的: 改变y:未影响x
*x.p = 4;		//糟糕 写入已释放的内存

}
在将x“拷贝”到y后,我们可以通过y操纵x的部分状态,这有时也被称为浅拷贝(shallow copy),一个明显的替代方法就是拷贝对象的完整状态,这也被称为深拷贝(deep copy),通常比深拷贝更好的方法是移动操作,它能最小化拷贝量而又不会增加复杂性

一次浅拷贝会令两个对象进入共享状态(shared state),这会带来严重的潜在混乱和错误

移动构造函数
Matrix(Matrix&&); //移动构造函数
Matrix& operator=(Matrix&&); //移动赋值运算符
&&表示右值饮用
移动赋值背后的思想是将左值的处理与右值的处理分离:拷贝赋值操作和拷贝构造函数接受左值,而移动赋值操作和移动构造函数则接受右值。对于return值,采用移动构造函数。

编译器如何知道它什么时候可以使用移动操作而不是拷贝操作呢?在少数情况下,例如返回值,语言规则指出编译器可以使用移动操作(因为下一个动作就是销毁元素).但是一般情况下我们必须通过传递右值引用参数告知编译器。
template
void swap(T& a,T& b)
{
T tmp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
move()是一个标准库函数,它返回其实参的一个右值引用。

参数类型的转换
要想实现同一个函数的不同参数组合,一种有效措施是丽颖类型转换技术为函数提供一个最通用的版本,再辅以少数几种必要的变形。这样可以解决由多种参数带来的组合爆炸问题。

C++作者认为为类的每一个成员都提供单独的访问控制并不是个好主意;而且通常情况下也确实如此。对于很多类型来说,单独的访问控制(有时称为读写函数,get-and-set functions)意味着程序灾难。单独的访问控制一不小心就会破坏不变式,而且想要改变类的表示也会变的困难。

派生类
如果一个类Derived有一个公有基类Base,那么我们可以将一个Derived* 赋予一个Base类型的变量而无须显示类型转换。相反的转换有Base到Derived*,必须是显式的。
换句话说,若通过引用和指针进行操作,派生类对象可以作为其基类对象处理,反过来则不能。

成员函数
派生类成员可以使用基类的公有和保护成员,就好像它们声明在派生类中一样,但派生类不能访问基类的私有成员。
构造函数和析构函数
·对象自底向上构造(基类先于成员,成员先于派生类),自顶向下销毁(派生类先于成员,成员先于基类);
·每个类都可以初始化其成员和基类(但不能直接初始化其基类的成员或基类的基类)
·类层次中的析构函数通常应该是virtual的
·类层次中类的拷贝构造函数须小心使用,以避免切片现象
·虚函数调用的解析,dynamic_cast,以及构造函数或析构函数中的typeid()反映了构造和析构阶段

从最基础的部分(如基类)开始构造,依赖于它的部分(如派生类)稍后构造,即我们是从根(基类)向叶(派生类)进行构造的。

类型域
看一看为什么不要使用类型域方法。
strut Employee{
enum Empl_type{man,empl};
Empl_type type;

};

void print_employee(const Employee* e)
{
switch(e->type)
{
case Employee::empl:

break;
case Employee:👨

break;
}
}
在一个处理很多派生类的大型函数中找到所有这种类型域的检测是非常困难的。即使都找到了,理解发生什么也很困难。而且增加任何一种新的Employee都要修改系统中的所有关键函数,只要包含类型域的检测就要修改。在修改之后,程序猿还必须考虑所有可能需要检测类型域的函数。这意味着要检查关键源码,还要检查受影响的代码,这会带来不可避免的额外开销。是否使用显示类型转换可以作为代码是否需要检查的重要提示,这可以在一定程度上减少工作量。
换句话说使用类型域技术很容易出错,也容易导致维护问题。随着程序规模增大,为题变得更为严重,因为类型域的使用违反了模块化和数据隐藏的思想。使用类型域的每个函数都必须了解类型域的类的派生类的实现细节
而且所有派生类都可以访问类型域这样的公共数据。这似乎会诱使人们增加更多这样的数据。公共积累从而变成“有用信息”的仓库。这最终会使基类和派生类的实现变得错综复杂,这是最糟糕的。在一个大型类层次中,公共积累中的可访问的(非private)数据就变成了类层次中的“全局变量”。为了令设计简洁,维护容易,我们还希望独立的问题保持分离,避免相互依赖。

虚函数
虚函数机制允许程序猿在基类中声明函数,然后在每个派生类中重新定义这些函数,从而解决了类型域方法的固有问题。编译器和链接器会保证对象和施用于对象之上的函数之间的正确关联。
class Employee{
public:
virtual void print() const;
};
关键字virtual指出print()作为这个类自身定义的print()函数及派生类中定义的print()函数的接口。 为了允许一个虚函数声明能作为派生类中定义的函数的接口,派生类中函数的参数类型必须与基类中声明参数类型完全一致,返回类型也只允许细微改变。

首次声明虚函数的类必须定义它(除非虚函数被声明为纯虚函数),即使没有派生类,也可以使用虚函数,而一个派生类如果不需要自有版本的虚函数,可以不定义它。当派生一个类时,如需要某个函数,定义恰当版本即可。

如果派生类中一个函数的名字和参数类型与基类中的一个虚函数完全相同,则称它覆盖(override)了虚函数的基类版本

无论真正使用的确切基类类型是什么,都能令基类的函数表现出正确的行为,这称为多态性(polymorphism)。具有虚函数的类型称为多态类型或运行时多态类型。在C++中为了获得运行时多态行为,必须调用virtual成员函数,对象必须通过指针或引用进行访问。当直接操作一个对象时(不是指针或引用),编译器了解其确切类型,从而不需要运行时多态了。

默认情况下,覆盖虚函数的函数自身也变味virtual的。我们在派生类中不建议重复关键字virtual

常用的编译器实现技术是将虚函数名转换为函数指针表中的一个索引。这个表通常称为虚函数表(the virtual function table)简称vbtl。

使用作用域解析运算符::调用函数能保证不使用virtual机制。

对于大型类层次,就要用到特定的控制机制了:
·virtual: 函数可能被覆盖
·=0: 函数必须是virtual的,且必须备覆盖
·override: 函数要覆盖基类中的一个虚函数
·final: 函数不能被覆盖

说明符override和final不是函数类型的一部分,而且在类外定义中不能重复。

返回类型放松
覆盖函数的类型必须与它所覆盖的虚函数类型完全一致,C++对这一规则提供放松规则。即如果原返回类型为B*,则覆盖函数的返回类型可以为D*,只要B是D的一个公有基类即可。类似返回类型B&可放松为D&,这一规则有时称为协变返回(covariant return)规则。
这一放松规则只能用于返回类型指针或引用的情况,但不能是unique_ptr这样的智能指针,特别是对参数类型没有类似的放松规则。否则会引起类型违背。

为了创建一个对象,构造函数需要确切了解要创建的对象的类型。因此构造函数不能是virtual的,而且构造函数也并不完全是一个普通的函数。特别是它与内存管理例程的交互方式与普通成员函数不同。因此不能接受一个构造函数的指针并将其传递给一个对象创建函数。通过定义一个函数来调用构造函数并返回构造的对象,我们就可以解决所有这些问题。

抽象类
将类Shape的虚函数声明为纯虚函数(pure virtual function)。通过使用"伪初始化器"=0可以将虚函数变为纯虚函数
class Shape
{
public:
virtual void rotate(int) = 0; //纯虚函数
virtual void draw() const = 0; //纯虚函数
virtual bool is_closed() const = 0; //纯虚函数
virtual ~Shape(); //虚函数
};
具有一个或多个纯虚函数的类称为抽象类(abstract class),我们无法创建抽象类的对象
Shape S;//错误:创建抽象类Shape的对象
抽象类就是要作为通过指针和引用访问的对象的接口(为保持多态行为)。因此对一个抽象类来说,定义一个虚构函数通常很重要。由于抽象类提供的接口不能用来创建对象,因此抽象类通常没有构造函数。

如果纯虚函数在派生类中未被定义,那么它仍保持是纯虚函数,因此派生类也是一个抽象类。只有当它们都被覆盖时,我们才得到一个可以创建对象的类。

访问控制
·如果它是private的,仅可被所属类的成员函数和友元函数所使用
·如果它是protected的,仅可被所属类的成员函数和友元函数以及派生类的成员函数和友元函数所使用。
·如果它是public的,可被任何函数所使用。

protected成员
一个派生类只能对自身类型的对象访问其基类的保护成员。

访问基类
·public派生令派生类成为基类的一个子类型
·private基类最有用的情形就是当当我们定义一个类时将其接口限定为基类,从而可提供更强的保障
·protected基类在类层次中很有用,其中进一步的派生是常态
基类的访问说明符可以省略,此时对class,基类默认为私有的;对struct基类默认是公有的

基类的访问说明符控制基类成员的访问以及从派生类类型到基类类型的指针和引用转换。考虑一个类D派生自基类B的情况:
·如果B是一个private基类,其公有和保护成员只能被D的成员函数和友元函数使用。只有D的友元和成员可以将一个D转换为一个B
·如果B是一个protected基类,其公有和保护成员只能被D的成员函数和友元函数以及D的派生类的成员函数和友元函数使用。只有D的成员和友元以及D的派生类的成员和友元可以将一个D转换为一个B
·如果B是一个public基类,其公有成员可被任何函数访问。而且其保护成员可被D的成员和友元以及D的派生类的成员和友元使用。任何函数都可以将一个D转换为一个B

成员指针
成员指针是一种类似偏移量的语言构造,允许程序猿间接引用类成员。
成员指针不能赋予void*或任何其他普通指针。空指针(如nullptr)可赋予成员指针表示无成员

获得成员指针(pointer to member)的方法是对一个完全限定的类成员名使用地址运算符&,例如&Std_interface::suspend。我们可以使用形如X::*的声明符来声明"类X的成员指针类型的变量“。

一个成员指针并不指向一片内存区域,这一点与变量指针和函数指针是不同的。

一个static成员不关联某个特定对象,因此静态成员的指针就是一个普通指针。

一个派生类至少包含从基类那里继承来的成员,通常还包含其他成员。这意味着我们可以安全地将一个基类成员指针赋予一个派生类成员指针,但反方向赋值则不行。

21章类层次
我们最好保持数据成员是私有的,这样派生类的作者就不能随意使用它们了。更好的做法是把数据放在派生类的内部使其定义可以尽量符合实际的需要,并且能保证无关的派生类之间不会发生不必要的联系。在绝大多数情况下,受保护的接口应该仅包含函数,类型和常量。
对于绝大多数C++的实现来说,只要基类的大小发生了改变所有派生类就必须重新编译。

多重继承
直接从多个类中派生称为多重继承(multiple inheritance)
继承好处
·共享接口(shared interface):通过使用类使得重复代码较少。且代码规格统一。通常称为运行时多态(run-time polymorphism)或者接口继承(interface inheritance)
·共享实现(share implementation):代码量较少且实现代码的规格统一,通常称为实现继承(implementation inheritance)。
通过多重继承的方式把两个不相关的类强行“粘连”在一起作为第三个类的实现是一种非常有效的做法,很重要,它使程序猿不必要再编写那么多转发函数。

在派生类中声明的函数会覆盖基类中所有同名及同类型的函数。

基类的构造函数一定是在派生类的构造函数之前调用的,否则会造成混乱(也就是说对象还没初始化就使用了)。为了避免发生这样的混乱,每个虚基类的构造函数都由完整对象的构造函数(最终派生类的构造函数)负责调用

如果不同的派生类覆盖了同一个函数会怎样呢? 当且仅当其中一个覆盖的类派生自其他所有覆盖了该函数的类时,才允许这种情况发生。换句话说,一个函数必须覆盖所有其他版本。

22章 运行时类型信息
在运行时检测对象类型的最明显也最有用的操作就是类型转换。若对象确为预期类型。该操作返回一个合法的指针,否则返回一个空指针。

在运行时使用类型信息通常被称为“运行时类型信息”,简写为RTTI(Run-Time Type Information)
从基类到派生类的转换通常称为向下转换(downcast),因为我们画继承树的习惯时从根(基类)向下画。类似的从派生类到基类的转换称为向上转换(upcast),而从基类到兄弟类的转换则称为交叉转换。

dynamic_cast
运算符dynamic_cast接受两个运算对象:被<和>包围的一个类型和被包围的一个指针或引用。
但是dynamic_cast不会允许意外地破坏对私有和保护基类的保护,dynamic_cast再用于向上转换时与简单赋值无二,这意味着dynamic_cast没有额外的开销且对上下文是敏感的。
dynamic_cast的真正用武之地是编译器无法确定类型转换正确性的情形,再次情况下dyname_cast<T*>§查看p指向的对象(如果有的画)。如果对象的类型是类T或其类型有唯一的基类T,则dynamic_cast返回一个只想该对象的T类型的指针,否则返回nullptr。如果p的值时nullptr,dynamic_cast<T>§也会返回nullptr。注意类型转换要求必须对可唯一识别的对象进行,如果不满足哦可唯一识别的要求则转换失败并返回nullptr。

dynamic_cast要求给定的指针或引用指向一个多态类型,以便进行向下或向上的转换。
限制dynamic_cast智能转换多态类型在逻辑上也是有道理的–如果一个对象没有虚函数,那么在不了解其确切类型的情况下,是无法安全操作它的。
用dynamic_cast将void*转换成其它类型也是不允许的。

用dynamic_cast转换引用类型
我们可以合理地假定一个引用肯定指向一个对象,因此对一个引用r,dynamic_cast<T&>®并不是一个问题,而是一个断言:“r引用的对象的类型为T”,对r进行dynamic_cast得到的结果实际上已经隐含地被dynamic_cast实现自身检查过了,如果dynamic_cast的引用对象不是所期望的类型,他会抛出一个bad_cast类型的异常。

static_cast
dynamic_cast可以从一个多基类转换到一个派生类或是一个兄弟类。static_cast则不行,因为它不检查要转换的对象。
对于一个void所指向的内存,编译器不能做任何假设。这意味着dynamic_cast不能将一个void转换为其他类型,因为dynamic_cast必须探查一个对象的内部来确定其类型。这是需要使用static_cast

从设计的角度来看,dynamic_cast可以被看作一种询问对象是否提供了指定接口的机制。

23章模版
前缀template指出将要声明一个模版,而在声明中将用到类型参数C。像这样引入C之后,我们就可以想使用普通类型名一样来使用它。C的作用域一直延伸到以template为前缀的模版声明的末尾。C是一个类型名而不是类名。
对于一个类模版,如果在其名字后面跟一个用<>包围的类型,他就会成为一个类名。

模版本质上是一个说明,描述如何基于给定的恰当的模版实参来生成某些东西。

当定义一个类模版时,通常一种好的方式是:先编写调试一个特定类,如String,然后再将其转换为一个模版,如String。这样我们就能针对一个具体实例来处理很多设计问题和大多数代码错误,这种调试对所有程序猿来说都很熟悉。而且较之抽象概念,我们大多数人还是更擅长处理具体实例。随后我们就可以专心处理那些可能由泛化所引起的问题,而不必再为混杂其中的很多常规错误分心。类似地每当我们尝试理解一个模版时,一个通常很有用的方法首先设想它对一个特定类型实参如char的行为是怎样的,然后再尝试理解他最通用的行为。这也符合我们所习惯的哲学:一个通用组件应该从一个或多个具体实例泛化而得,而不是简单地从第一原理直接设计。

我们不能重载一个类模版名,因此如果在一个作用域中声明了一个类模板,在此作用域中就不能声明任何其他同名实体了。

从一个模版和一个模版实参列表生成一个类或一个函数的过程通常被称为模版实例化(template instantiation).一个模版针对某个特定模版实参列表的版本被称为特例化(specialization)。

显然,模版提供了一种从相对较短的定义生成大量代码的强有力方法,但也正因为如此我们要避免几乎相同的函数定义泛滥,占据大量内存。另一方面模版代码能达到其他方式编写的代码所达不到的质量。特别是组合使用模版和简单内联来编写程序能消除很多直接或间接的函数调用。例如关键数据结构上的简单操作(如sort()中的<操作和矩阵运算中标量的+操作)在高度参数化的库中会被约简为单个机器指令。因此,轻率使用模版会生成大量非常相似的
函数,从而导致代码膨胀,而正确使用模版会使小函实现内联,从而和其他方法相比能大幅缩减代码量,提高运行速度。贴别是简单的<或[]生成的代码通常就是单个机器指令,既比任何函数调用快的多,也比任何需要调用函数取得返回结果的代码短的多。

模版实例画就是从一个模版和一组模版实参来生成代码。由于这些信息在实例化时都能获得。因此从模版定义和模版实参类型来编织这些信息能提供最大程度的灵活性和无与伦比的运行时性能。不幸的是这种灵活性的同事也意味着复杂的类型检查和难以精确报告错误类型。

模版机制最大弱点是无法直接表达对模版实参的要求。

类模版成员
·数据成员
·成员函数
·成员类型别名
·static成员
·成员类型
·成员模版

友元关系既不能继承也不能传递。即使A是B的友元,C是A的友元,C也不会自然称为B的友元。

函数模版
当调用一个函数模版时,函数实参类型决定了使用哪个模版版本。即模版实参是从函数实参推断出来的。

编译器可以从一次调用中推断类型和非类型模版实参,前提是函数实参列表唯一标识出模版实参集合。
注意,类模版参数并不是靠推断来确定的。原因在于一个类可以有很多构造函数,这种灵活性是的实参推断在很多情况下不可行,更多情况下得到含混不清的结果。取而代之,类模版依赖特例化机制在可选的定义中隐式地进行选择。如果我们需要给予推断出的类型闯进啊一个对象,常用的方法是通过调用一个函数来进行推断。如果不能从函数实参推断出一个模版实参,我们就必须显示指定它。

模版别名
我们可以使用using语法或typedef语法为一个类型定义别名。using语法更常用,一个重要原因是他能用来为模版定义别名,模版的一些参数可以固定。

注意我们从别名定义中从using得到的永远是一个别名。即当使用别名时,与原始模版时完全一样的。

源码组织
·在一个编译单元中,在使用模版前包含其定义。
·在一个编译单元中,在使用模版前包含其声明。在编译单元中稍后的位置包含模版定义。
由于历史和技术原因,C++不支持模版定义和使用的分离编译。目前最常用的方法是在每个用到模版的编译单元中都包含模版定义,优化编译时间和消除目标代码冗余的任务就交给编译器了。

多头文件组织可以伸缩到比我们的玩具语法分析器大几个数量级的模块以及比我们的玩具大几个数量级的程序。使用这种组织方式的根本原因是它提供了一种更好的关注点局部化的机制。当分析和修改一个大程序时,对程序员而言,能聚焦在一个相对较小的代码片段上是非常重要的。多头文件组织方式能很容易地准确确定语法分析器代码依赖什么,从而忽略程序其他部分。而单头文件方式则会迫使我们分析所有模块使用的每个声明,来确定哪些是相关的。一个简单的事实是,代码维护工作总是在信息不完整,视角受局限的条件下进行的,多头文件组织令我们在仅有局部视角的情况下能成功地“由内而外”地进行代码维护。而单头文件方法与其他任何以全局信息库为中心的方法类似,需要一种自顶向下的方法,而且总是让我们受困于代码之间的依赖关系。

#include
把对应的头文件以文本的形式插入到include所在的位置

C++作者对#include的建议
·只#include头文件(不要#include“包含变量定义和非inline函数的普通源码”)
·只#include完整的声明和定义
·只在全局作用域,链接说明块及名字空间定义中#include头文件
·将所有#include放在其他代码之前,以尽量减少无意造成的依赖关系
·避免使用宏技巧
·尽量减少在头文件中使用非局部的名字

返回引用的函数可以很好地替代全局变量。

main
一个程序必须恰好包含一个名为main()的函数.通过调用全局函数main()开始执行程序的主要计算任务,从main()返回后程序就终止了。main()的返回类型是int作为程序执行的结果被传递给调用main()的系统,非零返回值表示发生了一个错误。
原则上,定义在任何函数之外的变量(即,全局变量,名字空间变量以及类static变量)在mian()被调用前初始化。

程序终止
·从main()返回
·调用exit()
·调用abort()
·抛出一个未捕获的异常
·违反noexcept
·调用quick_exit()
如果使用标准库函数exit()终止一个程序,则会调用已构造的静态对象的析构函数。在一个析构函数中调用exit()会导致无限递归。

错误处理
异常(exception)的概念可以帮助我们将信息从检测到错误的地方传递到处理该错误的地方。如果函数无法处理某个问题则抛出(throw)异常。
·主调函数如果想处理某些失败的情形,可以把这些异常置于try块的catch从句中。
·被调组件如果无法完成既定的任务,可以用throw表达式抛出一个异常来说明这一情况。

异常是指被程序抛出的一个对象,它表示在程序中出现了一个错误。异常可以任意类型的对象,只要它能被拷贝即可。

异步事件不属于异常的范畴,当然也就不能用异常机制直接处理。

程序员应该尽一切肯能坚持“异常是错误处理”的观点。这样有助于把代码明确的划分成两部分:普通代码和错误处理代码,从而提高代码的可读性。

异常处理机制的目的是提供一种措施,使得当某处程序发现有未正常完成的任务时,它可以尽快通知程序的另一部分。

谨记内存不是唯一一种可能泄漏的资源。我们通常把从系统某处申请并且将(显式的或者隐式的)归还回去的东西统称为资源。文件,锁,网络连接和线程都是系统资源。函数在抛出异常之前必须释放这些资源或者把它们移交给其他资源句柄。

异常从它的抛出点开始向上传递到处理程序的过程称为栈展开(stack unwinding)。

throw
可以throw任意类型的异常,前提是他能被复制和移动
捕获的异常对象从本质上来说就是被抛出的对象的一份拷贝,换句话说,throw x用x初始化了一个x的类型的临时变量。在我们做中捕获这个临时变量之前,还可能复制它好几次。
因为异常在被捕获前有可能被拷贝很多次,所以我们一般不会在异常中存放太多数据。

例如
void f()
{
try{
throw E{};
}
catch(H)
{
//何时到达此处呢
}
}
当满足一下条件时,系统会调用异常处理程序:
[1]如果H和E的类型相同
[2]如果H是E的无歧义的公有基类
[3]如果H和E都是指针类型,并且它们所指的类型满足[1]或者[2]
[4]如果H是引用类型,并且它所引用的类型满足[1]或者[2]

try块和catch从句中的{}都是实实在在的作用域。因此我们想在try语句的两个部分使用同一个名字,或者在try块的外部使用某个名字,都必须把改名字声明在try块的外部

重新抛出
捕获一个异常之后,异常处理程序经常发现他自己无法完整的处理该错误。此时异常处理程序先完成在局部能完成的任务,然后再次抛出异常。通过这种方式,错误就能被很好的处理了。甚至当处理错误所需要的信息散落在程序的多个部分时,程序也可以协同多个处理程序共同完成恢复操作。
我们用不带运算对象的throw表示重新抛出。重新抛出的异常就是我们一开始捕获的那个异常,而不会是它的一部分。 如果在没有异常的情况下强行重新抛出异常,则系统会调用std::terminate()

事实上,我们是需要处理每一种异常的,在函数中省略号…表示“任意实参”,因此catch(…)的含义是“捕获任意异常”。

终止
在某些情况下最好不要使用异常处理,基本原则是:
·处理异常的时候不要抛出异常。
·不要抛出一个无法捕获的异常。
如果违反了上述原则,程序就会终止。

任何措施都无法捕获在名字空间和线程局部对象的初始化及析构过程中抛出的异常。这恰恰是我们应爱尽量避免使用全局变量的另一个原因。

C++作者建议
[1]在设计初期尽早确定异常处理策略
[2]当无法完成既定任务的时候抛出异常
[3]用异常机制处理错误
[4]为特定任务设计用户自定义异常类型(而非内置类型)
[5]如果由于某种原因你无法使用异常,尽量模仿其机制
[6]使用层次化异常处理
[7]保持异常处理的各个部分尽量简洁
[8]不要试图捕获每个函数的每个异常
[9]至少提供基本保障
[10]
[11]
[12]
[13]
[14]
[15]
[16]
[17]
[18]
[19]
[20]
[21]
[22]
[23]
[24]
[25]
[26]
[27]让main()捕获和报告所有异常
[28]
[29]
[30]不要让析构函数抛出异常
[31]把普通的代码和异常处理代码分离开来
[32]
[33]
[34]
[35]

函数
函数声明负责指定函数的名字,返回值的类型以及调用该函数所需的参数数量和类型,参数的传递语义与拷贝初始化语义完全一致,编译器检查实参的类型,如果需要的话还会执行隐式的参数类型转换,对参数的检查和转换非常重要,程序员要予以足够的重视。

函数的基本建议是控制长度在40行以内
把一个复杂的运算分解为若干有意义的片段,然后分别为他们命名,我们希望代码是易于理解的,因为这是实现可维护性的第一步。而代码易于理解的第一步恰恰就是把计算任务分解为易于理解的小块。 通过构建函数我们就能把注意力集中在为活动命名以及理清依赖关系上,同时通过函数调用和返回还能让我们远离那些充满错误风险的控制结构,比如goto和continue,另外代码应该尽量避免使用嵌套的循环,因为除非这类循环写的非常规范,否则通常包含大量的错误。例如应该使用点乘而非嵌套的循环来实现矩阵算法。
关于函数的一条最基本的建议是应该令其规模较小,以便于我们一眼就能看清该函数的全部内容(避免使用过多的if else 语句),如果我们一次只能看到算法的一小部分,纳闷程序就可能发生错误。对于大多数程序猿来说,函数的规模最好控制在40行以内。

函数调用的性能
大多数情况下,调用函数所产生的代价并不是程序性能的关键因素。一旦我们发现函数调用的代价比较高,可以考虑使用内联机制消除其影响。

函数的声明
函数声明负责指定函数的名字,一组参数,返回值的类型还可以包含多种限定符和修饰符
·函数的名字 requirement
·参数列表,可以为空(); requirement
·返回类型,可以是void(),可以是前置或者后置类型(使用auto);requirement
·inline 表示一种愿望,通过内联函数体实现函数调用。
·constexor,表示当给定常量表达式作为实参时,应该可以在编译时对函数求值
·noexcept,表示该函数不可以抛出异常
·连接说明。例如static
·[[noreturn]],表示该函数不会常规的调用/返回机制返回结果。
此外成员函数还能被限定为:
·virtual,表示该函数可以被派生类覆盖。
·override,表示该函数必须覆盖其基类的一个虚函数
·final,表示该函数不能被派生类覆盖
·static,表示该函数不与某一特定函数关联。
·const,表示该函数不能修改其对象的内容。

函数定义
如果函数会在程序中调用,那么它必须在某处定义(只定义一次 )函数定义是特殊的函数声明,给出函数的整体内容。 函数定义是特殊的函数声明 函数定义是特殊的函数声明,函数定义是特殊的函数声明,函数定义是特殊的函数声明,函数定义是特殊的函数声明,函数定义是特殊的函数声明,函数定义是特殊的函数声明,函数定义是特殊的函数声明,函数定义是特殊的函数声明,函数定义是特殊的函数声明,函数定义是特殊的函数声明。
函数的参数名字不属于函数类型的一部分,不同的声明语句中参数的声明无须保持一致。

返回值
传统上函数的返回类型位于函数的声明语句的一开始部分。然而我们也可以在函数的声明语句中把返回类型写在参数列表之后
例如
string to_string(int a); //前置返回类型
auto to_string(int a) -> string; //后置返回类型
前置的auto表明函数的返回类型放在参数列表之后。后置的返回类型由符号->引导
后置返回类型的必要性源于函数模版的声明,因为其返回类型是依赖于参数的。实际上任何函数都可以使用后置返回类型。

如果函数不会回任何值,则其函数“返回类型”是void,如果函数没有被声明称void的,则它必须返回某个值(main()是个例外)。相反,void函数无权返回任何值。

递归
函数调用它自身,称之为递归(recursive);

函数返回值的语义也与拷贝初始化的语义一致。return语句初始化一个返回类型的变量。
当我们每次调用函数的时候,重新创建它的实参以及局部变量的拷贝。一旦函数返回结果所占的存储空间就被重新分配了。因此不应该返回指向局部非static变量的指针,我们无法预计该指针所指位置的内容将发生什么改变。
并不存在void值,但是我们可以调用void函数将其当作了你一个void函数的返回值。

形如[[…]]的概念被称为属性,属性可以置于C++语法的任何位置。把[[noreturn]]放在函数声明语句开始的位置表示我们不希望函数返回任结果。如果函数被设定为[[noreturn]],但是在函数的内部依然返回了某个值,将产生未定义的行为。

局部名字
定义在函数内部的名字被称为局部名字(local name),当线程执行到局部变量或者常量的定义处时,它们将被初始化,除非我们把变量声明称static,否则函数的每次调用都会拥有该变量的一份拷贝,相反如果我们把局部变量声明称static,则在函数的所有调用中都将使用唯一的一份静态分配的对象,该对象在线程异地次到达它的定义处时被初始化。

static局部变量有一个非常重要的作用,它可以在函数的多次调用间维护一份公共信息而无需使用全局变量。如果使用了全局变量,则可能会被其他不相关函数访问甚至干扰。递归的初始化一个static局部变量将产生未定义的结果
int fn(int n)
{
static int n1 = n; //ok
static n2 = fn(n-1) + 1; //递归定义导致未定义结果
return n;
}

参数传递
程序员应该尽力避免修改引用类型的实参,但是用到大对象的时候,引用传递比值传递效率更高。我们应该将该引用类型的参数声明成const的,以表明我们之所以使用引用只是处于效率上的考虑,而并非想让函数修改对象的值。 程序的规模越大,合理使用const参数就显得越重要。
参数传递的方式选择
·对于小对象使用值传递的方式
·对于我们无须修改的大对象使用const引用传递
·如果需要返回计算结果,最好使用return而非通过参数修改对象。
·使用右值引用实现移动
·如果找不到合适的对象则传递指针
·除非万不得已,不要使用引用传递(当我们需要修改对象的值时,传递指针比使用引用更容易表达清楚程序的原意)

lambda表达式
lambda表达式有时也称为lambda函数或者简称lambda。他是定义和使用匿名函数对象的一种简便方式。
一个lambda表达式包含以下组成要件:
·一个可能为空的捕获列表(capture list),指明定义环境中的那些人名字能被用在lambda表达式内,以及这些名字的访问方式是拷贝还是引用。捕获列表位于[]内

·一个可选的参数列表(parameter list),指明lambda表达式所需的参数。参数列表位于()内。
·一个可选的mutable修饰符,指明该lambda表达式可能会修改它自身的状态(即,改变通过值捕获的变量的副本)
·一个可选的noexcept修饰符
·一个可选的->形式的返回类型声明。
·一个表达式(body),指明要执行的代码,表达式位于{}内。

C++作者说如果把lambda表达式看成是一种定义并使用函数对象的便捷方式,将非常有助于我们理解lambda表达式的语义。
lambda的主要用途是封装一部分代码以便于将其用作参数。lambda允许我们"内联地"这么做,而无须命名一个函数(或者函数对象)然后在别处使用它.

lambda引入符的形式有很多种
·[]空捕获列表。这意味着在lambda内部无法使用其外层上下文中的任何局部名字.对于这样的lambda表达式来说,其数据需要从实参或者非局部变量中获得.
·[&]通过引用隐式捕获。所有局部名字都能使用,所有局部变量都通过引用访问。
·[=]通过值隐式捕获,所有的局部名字都能使用,所有的名字指向局部变量的副本,这些副本是在lambda表达式调用点获取的。
·[捕获列表] 显示捕获,捕获列表是通过值或者引用的方式捕获的局部变量的名字列表。以&为前缀的变量名字通过引用捕获,其他变量通过值捕获。捕获列表中可以出现this,或者紧跟…的名字以表示元素。
·[&,捕获列表] 对于名字没有出现在捕获列表中的局部变量,通过引用隐式捕获。捕获列表中可以出现this。列出的名字不能以&为前缀。捕获列表中变量名是通过值的方式捕获。
·[=,捕获列表] 对于名字没用出现在捕获列表中的局部变量,通过值隐式捕获。捕获列表中不允许包含this。列出的名字必须以&为前缀,捕获列表中的变量名通过引用的方式捕获。
***请注意,以&为前缀的局部名字总是通过引用捕获,不易&为前缀的局部名字总是通过值捕获。只有通过引用的捕获允许修改调用环境中的变量。

lambda的生命周期可能比它的调用者更长。因此如果我们发现lambda的生命周期可能比它的调用者更长,就必须确保所有局部信息都被拷贝到闭包对象中(否则lambda可能会访问一个早已不存在的局部变量),并且这些值应该通过return机制或者适当的实参返回。

因为名字空间变量(包括全局变量)永远是可访问的(确保在作用域内),所以我们无须捕获它们。

当lambda被用在成员函数中时,怎么访问类对象的成员呢?
做法是把this添加到捕获列表中,这样类的成员就位于可被捕获的名字集合中了。
成员通过引用的方式捕获。也就是说,[this]意味着成员是通过this访问的,而非拷贝到lambda中。不幸的是,[this]和[=]互不兼容,可能在多线程程序中产生竞争条件。

调用与返回
·如果一条lambda表达式不接受任何参数,则其参数列表可被忽略。因此lambda表达式的最简形式是[]{}。
·lambda表达式的返回类型能由lambda表达式本身推断得到,然而函数无法做到。
如果在lambda的主体部分不包含return语句,则该lambda的返回类型是void。如果lambda的主体部分只包含一条return语句,则该lambda的返回类型是该return表达式的类型。其他情况下我们必须显示地提供一个返回类型。

void g(double y)
{
[&]{f(y);} //返回类型是void
auto z1 =[=](int x){return x+y;} //返回类型是double
auto z2 =[=,y]{if(y) return 1; else return 2;} //错误:lambda主体部分过于复杂
//无法推断其类型

auto z3 =[y](){return 1:2;}								//返回类型是int
auto z4 =[=,y]()->int{if(y) return 1;else return 2;}	//OK:显示地返回类型

}
如果使用了后缀返回类型,则可以忽略参数列表。

对于发生在两种标量数字类型之间的转化,可以使用显示类型转换函数 例如narrow_cast;
template<class Target,class Source>
Target narrow_cast(Source v)
{
auto r = static_cast(v); //把值转换成目标类型
if(static_cast®!=v)
throw runtime_error(“narrow_cast<>()failed”);
return r;
}
如果把某个值转换成目标类型而在这个过程中发生了窄化运算,则把结果转换成原类型,并且恢复原值。 对于浮点数的转换应该使用范围检查而非!=。

符号T{v}有一个好处,就是它只执行“行为良好的”类型转换。截断浮点数不是良好的行为,如果必须这样做就必须显示的指出来。如果希望4舍5入,可以使用标准哭函数round();它执行“传统的四舍五入” 7.9到8。7.4到7。

显式类型转换也称为强制类型转换,只有在极个别的情况下才有用。然而用在过去,显式类型转换被严重滥用,成为了程序错误的主要来源。在决定使用显式类型转换之前,请花时间仔细考虑一下是否真的必须这么做。很多情况下,C或者早期版本的C++需要用到显示类型转换,但是现在的C++语言并不需要。完全可以避免使用显式类型转换,即使使用也应该限定在少量代码片段中。

·static_cast 执行关联类型之间的转换,比如一种指针类型向同一个类层次其他指针类型的转换,或者整数类型向枚举类型的转换,或者浮点类型向整数类型的转换。他还能执行构造函数和转换运算符定义的类型转换。

·reinterpret 处理非关联类型之间的转换,比如整数向指针的转换以及指针向另一个非关联指针类型的转换。

·const_cast 参与转换的类型仅在const修饰符及volatile修饰符上有所区别。

·dynamic_cast 执行指针或者引用向类层次体系的类型转换,并执行运行时检查。

句柄(Handle)是一个是用来标识对象或者项目的标识符,可以用来描述窗体、文件等,值得注意的是句柄不能是常量(百度百科)

自由存储
命名对象的生命周期尤其作用域决定。但是某些情况下我们希望对象与创建它的语句所在的作用域独立开来。例如在函数内部创建了对象,并且希望在函数返回后仍能使用这些对象。运算符new负责创建这样的对象,运算符delete负责销毁他们。new分配的对象“位于自由存储之上”(或者说“在堆上”或“在动态内存中”)

可以使用{}列表的形式传递实参,当然也可以使用传统的()列表形式指定初始化器。但是,如果试图用符号=初始化一个用new 创建的对象会引发错误。
int *o = new int = 7; //error

对于一个用new创建的对象来说,我们必须用delete显示地将它销毁,否则它将一直存在。只有将它销毁了,它占用的空间才能被其他new使用。垃圾收集器的思路,由它负责看管为引用的对象并使得new能重新使用被这些对象占用的空间,但是C++的具体实现并不能确保这一点。因此C++作者假设new创建的对象需要由delete手动地释放。

delete运算符只能作用于new返回的指针或者nullptr,不过对nullptr使用delete不产生什么实际效果。
如果被删除的对象类型是一个含有析构函数的类,则delete将调用该析构函数,然后释放该对象所占用的内存空间以供后续使用。

delete负责删除单个对象 delete[]负责删除数组。

当new发现没有多余内存可供分配时,分配器会跑出一个标准库bad_alloc异常

限定列表
T x{v};
那么我们也能用T{v}或者new T{v}的形式创建一个对象并将其当成一条表达式.使用new会把目标对象置于自由存储之上,并返回一个只想该对象的指针;相反"普通的T{v}"仅在局部作用域中创建一个临时对象。

内存管理
自由存储的问题
·对象泄漏(leaked object):使用new,但是忘了用deleted释放掉分配的对象。
·提前释放(premature deletion):在尚有其他指针指向该对象并且后续仍会使用该对象的情况下过早地delete。
·重复释放(double deletion):同一对象被释放两次,两次调用对象的析构函数(如果有的话)

对象泄漏是一种潜在的严重的错误,因为它可能会令程序面临资源耗尽的情况。与之相比,提前释放更容易造成恶果,因为只想“已删除对象”的指针所指的可能已经不是一个有效的对象了(此时读取的结果很可能与预期不符),又或者该内存区域已经存放了其他对象(此时对该区域执行写操作将会影响本来无关的对象)
int* p1 = new int{999};
int* p2 = p1; //存在潜在的麻烦
delete p1; //此时p2指向的不再是一个有效的对象
p1 = nullptr; //造成代码安全的错觉
char* p3 = new char{‘x’}; //此时p3可能指向了p2所指向的内存区域
*p2 = 999; //该行代码可能会造成错误
cout<<*p3<<’\n’; //输出的内容可能不是x

两种法避免上述问题 代替裸new 和delete
·除非万不得已不要把对象放在自由存储上,优先使用作用域内的变量。
·当在自由存储上构建对象是,把它的指针放在一个管理器对象(manager object,有时也称为句柄)中。此类对象通常含有一个析构函数,可以确保释放资源.例如string,vector等标准库容器,以及unique_ptr和shared_ptr等近可能让这个管理器对象作为作用内的变量出现。很多习惯于使用自由存储的场合都可以使用移动语义代替,只要从函数中返回一个表示大对象的管理器对象就可以了,称为RAII(“资源获取即初始化”) 是一项避免资源泄漏的基本技术
标准库的vector就是一个例子
void f(const string& s)
{
vector v;
for(auto c:s)
v.push_back©;
}
vector的元素位于自由存储上,但是它的分配和释放资源的操作都限定在其内部进行,在栗子中push_back()负责执行new(为元素分配空间)和delete(释放不需要的空间)的操作。具体实现细节用户无需了解,不会导致内存泄漏。

对于new和delete,C++作者的经验是应该尽量确保"没有裸new",即令new位于构造函数或者类似的函数中,delete位于析构函数中,由他们提供内存管理策略。此外new常用作资源句柄的实参

C++作者的原则是尽可能利用标准库功能,有些标准库函数是内敛的,有的甚至是用特定的机器指令实现
的。因此在决定选用手工编写的代码之前,最好确认它的功能优于标准库函数

隐式类型转换
整数和浮点数可以在赋值语句表达式中自由地混合使用。在可能的情况下,值的类型会自动转换以避免损失信息。不幸的是,在这一过程中也可能会发生某些隐式的损失值(”窄化“)的类型转换。
使用{}列表能防止窄化计算的发生
void f(double d)
{
char c{d}; //错误:编译器发现程序试图把双精度浮点数转换成字符类型
}
要求避免在同一条表达式中混用无符号整数和带符号整数。

constexpr
作为数据项(此处C++作者特意没有使用“变量”这个词)定义的一部分,constexpr表达了编译时求值的意愿。如果编译时无法求值,编译器将报错。
int x1 = 7;
constexpr int x2 = x1;//error 初始化器不是常量表达式
constexpr int x3 = 7//ok
在大型程序中,要想在编译时确定变量的值通常非常困难。

求值顺序
C++没有明确规定表达式中子表达式的求值顺序,尤其注意,不能假定表达式总是按照从左到右的顺序求值的。
int x = f(2) + g(3); //到底先调用f() 还是g()并没有明确规定

int i = 1;
v[i] = i++; //未定义的结果
其中的赋值操作既可能执行为v[1] = 1,也可能执行为v[2]=1,甚至会产生非常奇怪的运行结果,编译器可能不会发现这样的二意型操作,但事实上大多数编译器都会置之不理。因此,一定要避免在同一条表达式中同事读写一个对象。除非只用到了一个运算符。或者显式使用使用短路求值(比如使用逗号或者&&或者||)
f1(v[i],i++)
f1的调用语句包含了两个实参v[i]和i++,C++并没有明确规定这两个实参表达式的求值顺序。因此这种用法应该尽量避免。依赖实参表达式求值顺序会产生未定义的结果。

main()参数
程序通过调用main()启动。之后main()被传入两个实参,分别是:argc指明实参的数量,argv代表实参的数组。这里的实参是C风格的字符串,因此argv的类型是char*[argc+1]。argv[0]表示程序的名字,因此argc的值至少是1.实参列表以0作为结束符,即argv[argc] == 0。

注释
糟糕的注释还不如没有注释
·在针对每个源文件的注释中指明:该文件中的声明有盒共同点,对应的参考手册条目,程序员的名字以及维护该文件所需的其他信息。
·为每个类 模版和名字空间分别编写注释。
·为每个非平凡的函数分别编写注释并指明:函数的目的,用到的算法(如果很明显的可以不用提),以及该函数对其应用环境所做的某些设定。
·为全局和名字空间内的每个变量及常量分别编写注释。
·为某些不太明显或者不可

for循环
最简单的循环是范围for语句,可以一次访问指定范围内的每个元素
int sum(vector& v)
{
int s = 0;
for(int x:v)
s+=x;
return s;
}
for(int x:v) 读作“对于范围v中的每个元素x”,或者干脆说“对于v中的每个x”,程序从头到位依次访问v的全部元素。
命名元素变量x的作用域是整个for语句。
如果在范围for循环中修改元素的值,则应该使用元素的引用
int sum = 0;
for(int& x:v)
sum += x;
while语句重复执行它的受控语句直到条件部分变成false。

if语句
if语句中,如果条件为真则执行第一条;否则执行第二条语句,即使条件的求值结果不是布尔值,也能尽量隐式的转换成bool类型。因此算数类型及指针类型的表达式都能作为条件
对于指针p来说
if§ 这条语句的含义是:“指针p指向一个有效的对象吗?” 它等价于if(p!=nullptr)
普通的enum可以隐式的转换成整数,然后再转换成布尔类型,但是enum class不能。

switch语句
想要结束一个分支,最常用的是break语句,有时候也用return语句。
有一种情况下不应该使用default:switch语句希望它的每个分支对应枚举类型中的一个枚举值。如果这样的话最好不要使用default语句,应该让编译器负责发现并报告case分支与枚举值未能完全匹配的问题。
下列代码几乎肯定是错误的
enum class Vessel{cup,glass,goblet,chalice};

void problematic(Vessel v)
{
switch(v)
{
case Vessel::cup: break;
case Vessel::glass: break;
case Vessel::goblet: break;
}
}
如果在程序的后期维护和升级过程中,我们给枚举类型加了一个新的枚举值则很容易引发上述错误。

C++允许switch语句的块内声明变量,但是不能不初始化,程序员应该避免使用未经初始化的变量。

我们在条件变量中声明声明变量:
if(double d = prim(true))
{
left /= d;
break;
}
首先声明d并给它赋了初值,然后把初始化后的d的值作为条件的值进行检查。d的作用域从声明处开始,到条件控制语句结束为止。假设还有一个else分支与上面的if分支对应则d在两个分支中都有效。

引用
引用作为对象的别名存放的是对象的机器地址。与指针相比,引用不会带来额外的开销
引用与指针的区别主要包括
·访问引用与访问对象本身从语法形式上看是一样的
·引用所引的对象永远是一开始初始化的那个对象
·不存在空引用,我们可以认为引用一定对应着某个对象
引用实际上是对象的别名。引用最重要的用途是作为函数实参或返回值。

三种引用形式
·左值引用(lvalue reference):引用那些我们希望改变值的对象
·const 引用(const reference):引用那些我们不希望改变值的对象
·右值引用(rvalue reference):所引对象的值在我们使用之后就不需要保留了

左值引用
在类型名字中,符号X& 的意思是"X的引用",常用于表示左值的引用 因此称为左值引用。为了确保引用对应某个对象,我们必须初始化引用
例如
void f()
{
int var = 1;
int& r{var};
r = 2;// var的值变为2
}

引用本身的值一旦经过初始化就不能再改变了,它永远指向一开始指向的对象,我们可以通过对引用取地址得到一个指向引所引对象的指针。但是不能令指针指向引用,也不能定义引用的数组。从这个意义上来说引用不是对象。
我一定要记住引用不是对象,但指针是对象。

变量引用和常量引用必须区分开 //const double& cdr{1};
为变量引入一个临时量充满风险,当我们为该变量赋值是实际上是在为一个转瞬即逝的临时量复制、、赋值,常量引用不存在这一问题。

&&表示右值引用,我们不使用const右值引用,因为右值引用大多数用法都是建立在能够修改所引对象基础上的,
·右值引用实现了一种“破坏性读取”,某些数据本来需要被拷贝,使用右值引用可以优化其性能。
·const 左值引用的作用是保护保护参数内容不被修改。

标准库的move(x)函数并不真的移动x(它只是为x创建一个右值引用)

把引用指向某种类型的引用那么得到的还是该类型的引用,而非特殊的引用的引用类型。
永远是左值引用优先,左值引用的优先级高于右值引用 左值引用遇到右值引用还是左值引用不管怎么做都无法改变左值引用绑定左值的事实。

struct
数组是相同类型元素的集合,struct是不同元素类型的集合

如果p是一个指针,则p->m等价于(*p).m

struct Readout
{
char hour;
int value;
char seq;
};
在内存中为成员分配空间之时,顺序与生命结构的时候保持一致。
一个struct对象的大小不一定恰好等于它所有元素大小的累计之和。因为很多机器要求一些特定类型的对象沿着系统结构设定的边界分配空间,以便机器能高效处理这些对象。对齐

struct是一种class,它的成员默认是public的。
如果只想按照默认的顺序初始化结构的成员,则不需要专门定义一个构造函数。
但是如果需要改变实参的顺序,检验实参的有效性,修改实参或者建立不变式,则应该编写一个专门的构造函数。

对于两个struct来说,即使它们成员相同,它们本身仍是不同类型。
struct S1 {int a;};
struct S2 {int a;};
S1和S2是两种类型

联合Union
C++作者认为union是一种被过度使用的语言特性,最好不要使用
union是一种特殊的struct,它的所有成员都分配在同一个地址空间上。因此一个union实际占用的空间大小与其最大成员一样。在同一时刻union只能保存一个成员的值。
联合有时会误用于类型转换的目的,这种误用的情况常常发生在特定的程序猿身上,它们曾经使用的语言缺少显式的诶性转换,因此不得不采用这种方式。
如果我们确实需要类似的转换,最好使用显示类型转换符,这样其他程序员才会知道到底发生了什么。
使用union的目的就是让数据更紧密,从而提高程序的性能。然而大多数程序即使使用了union也不会提高太多。

从技术来讲,union是一种特殊的struct,而struct是一种特殊的class,很多提供给类的功能与联合无关,因此对union施加了一些限制
·union不能有虚函数
·union不能含有引用类型的成员
·union不能含有基类
·如果union的成员含有用户自定义的构造函数,拷贝操作,移动操作或者析构函数,则此类函数对于union来说被delete掉了,换句话说union类型的对象不能含有这些函数
·在union的所有成员中,最多只能有一个成员包含类内初始化器
·union不能被用作其他类的基类。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值