处理类型
类型别名
`使用类型别名让复杂的类型名字变得简单明了,易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。`
传统方法
typedef double wages; // wages是double的同义词
typedef wages base, *p; // base是double的同义词,p是double*的同义词
C++11 别名声明(alias declaration)
using SI = Sale_item; // SI是Sale_item的同义词
using p = double*; // p是double*的同义词
指针、常量和类型别名
**指针常量** —— 指针类型的常量
**常量指针** —— 指向“常量”的指针
"常量"并不一定是常量,也可以是变量,只不过指针无法对指向的地址修改
`如果某个类型别名指代的复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。`
typedef char *pstring;
const pstring cstr = 0; // cstr是指向char的指针常量
const pstring *ps; // ps是一个指针,他的对象是指向char的指针常量
cstr = &a; // ERROR:表达式必须是可修改的左值
auto类型说明符
auto定义的变量必须有初始值
复合类型、常量和auto
编译器会适当地改变结果类型使其更复合初始化规则:
- 编译器以引用对象的类型作为auto的类型
- auto一般会忽略掉顶层const,同时底层const则会保留下来,比如当初始值是常量指针
<u>顶层const与底层const</u>(C++ Primer P57)
用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量
更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层const则与指针和引用等复合类型的基本类型部分有关
int i = 0;
const int ci = i;
const int *q = &i;
// r是一个常量指针,类型为const int *r
auto r = q;
// a是一个整型指针
// 整数的地址就是指向整数的指针
auto a = &i;
// b是一个指向整数常量的指针
// 对常量对象取地址是一种底层const
auto b = &ci;
- 如果希望推断出的auto是一个顶层const,需要明确指出
const auto f = ci;
- 将引用的类型设置为auto,此时初始化规则仍然适用
类型转换
隐式类型转换
略
显示类型转换
虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的
命名的强制类型转换
cast-name<type>(expression);
其中,type是转换的目标类型而expression是要转换的值。如果type是引用类型,则结果是左值
static_cast、dynamic_cast、const_cast、reinterpret_cast
-
static_cast
任何具有明确定义的类型转换,只要不包括底层const,都可以使用static_cast
- 当需要把一个较大的算数类型赋值给较小的类型时,static_cast非常有用。(Warning会关闭)
- static_cast对于编译器无法自动执行的类型转换也非常有用,当我们把指针存放在void*中,并且使用static_cast将其强制转换会原来的类型时,应该确保指针的值保持不变。也就是说,强制类型转换的结果将与原始的地址值相等,因此我们必须确保转换后所得的类型就是指针所指的类型。类型一旦不符,将产生未定义的后果
-
dynamic_cast
- 一般用于多态类型,有运行时安全检测
- C语言强制类型转换欺骗编译器,无安全检测,是不安全的;而dynamic_cast有安全检测
const_cast
const_cast只能改变运算对象的<u>底层const</u>
const char *pc;
char *p = const_cast<char*>(pc); // 正确:但是通过p写值是未定义的行为
`未定义是指不同编译器可能存在不同结果,VS code代码和运行结果如下`
#include <iostream>
int main(){
const char i = 'a';
const char *pc = &i;
char *p = const_cast<char*>(pc);
*p = 'b';
std::cout << i << std::endl;
std::cout << *p << std::endl;
std::cout << static_cast<void*>(const_cast<char*>(&i)) << std::endl;
std::cout << static_cast<void*>(p) << std::endl;
return 0;
}
-
reinterpret_cast
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。type必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。
try语句块和异常处理
throw表达式
程序的异常检测部分使用throw表达式引发(raise)一个异常。throw表达式包含关键字throw和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型
// 首先检查两条数据是否是关于同一种书籍的
if(item1.isbn() != item2.isbn())
throw runtime_error("Data must refer to same ISBN");
// 如果程序执行到这里,表示两个ISBN是相同的
...
抛出异常将终止当前的函数,并把控制权转移给能处理该异常的代码
try语句块
try语句块的通常语法形式
try{
program-statements
} catch (exception-declaration) {
hander-statments
} catch (exception-declaration) {
hander-statments
} // ...
函数在寻找处理代码的过程中退出
==寻找处理代码和函数调用链相反==。当异常被抛出时,首先搜索抛出该异常的函数。如果没找到匹配的catch子句,终止该函数,并在调用该函数的函数中继续寻找。如果还是没有找到匹配的catch子句,这个新的函数也被终止,继续搜索调用它的函数。以此类推,沿着函数的执行路径逐层回退,直到找到适当类型的catch子句为止。
如果最终还是没能找到匹配的catch子句,程序转到名为terminate的标准库函数。该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。
标准异常
<stdexcept>定义的异常类 |
---|
exception | 最常见的问题 |
runtime_error | 只有运行时才能检测出的问题 |
range_error | 运行时错误:生成的结果超出了有意义的值域范围 |
overflow_error | 运行时错误:计算上溢 |
underflow_error | 运行时错误:计算下溢 |
logic_error | 程序逻辑错误 |
domain_error | 逻辑错误:参数对应的结果值不存在 |
invalid_error | 逻辑错误:无效参数 |
length_error | 逻辑错误:试图创建一个超出该类型最大长度的对象 |
out_of_range | 逻辑错误:使用一个超出有效范围的值 |
定制操作
谓词
谓词是一个可调用的表达式,其返回的结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:**一元谓词**(unary predicate,意味着它们只接受单一参数)和**二元谓词**(binary predicate,意味着它们有两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。
bool is_shorter(const string &str1, const string &str2)
{
return str1.size() < str2.size();
}
int main(){
// 按长度由短至长排序words
sort(words.begin(), words.end(), is_shorter);
}
lambda表达式
介绍lambda
一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda表达式具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部。一个lambda表达式具有如下形式:
[capture list](parameter list) -> return type { function body }
其中,capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空);return type、parameter list和fucntion body与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,与普通函数不同,lambda必须使用<u>尾置返回</u>来制定返回类型。
尾置返回类型(trailing return type)
任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或数组的引用。尾置返回类型跟在形参列表后面并以一个<u>-></u>符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto:
// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*) [10]
因为我们把函数的返回类型放在了形参列表之后,所以可以清楚地看到func函数返回的是一个指针,并且该指针指向了含有10个整数的数组
我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体
auto f = [] { return 42; };
此例中,我们定义了一个可调用对象f,它不接受参数,返回42
lambda的调用方式和普通函数调用方式相同,都是使用调用运算符:
std::cout << f() << std::endl; // 打印42
在lambda中忽略括号和参数列表等价于指定一个空参数列表。在此例中,当调用f时,参数列表是空的。如果忽略返回类型,lambda根据函数体中的代码推断出返回类型。如果函数体只是一个return语句,则返回类型从返回的表达式的类型推断出来。否则,返回类型为void
向lambda传递参数
与一个普通函数调用类似,调用一个lambda时给定的实参被用来初始化lambda的形参。但<u>**lambda不能有默认参数**</u>。我们可以编写一个与is_shorter函数完成相同功能的lambda:
[](const string &a_str, const string &b_str)
{ return a_str.size() < b.size(); }
空捕获列表表明此lambda不使用它所在函数的任何局部变量。
如下所示,可以使用此lambda来调用stable_sort:
stable_sort(words.begin(), words.end(),
[](const string &a_str, const string &b_str)
{ return a_str.size() < b_str.size(); });
使用捕获列表
虽然一个lambda可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。
lambda捕获和返回
值捕获
类似参数传递,变量的捕获方式也可以是值或引用。
void func()
{
size_t val = 42; // 局部变量
// 将val拷贝到名为f的可调用对象
auto f = [val] { return val; };
val = 0;
auto j = f(); // j为42;f保存了我们创建它时val的拷贝
}
由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值
引用捕获
void func()
{
size_t val = 42; // 局部变量
// 对象f包含val的引用
auto f = [&val] { return val; };
val = 0;
auto j = f(); // j为0;f保存val的引用,而非拷贝
}
如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。
隐式捕获
lambda捕获列表 | |
---|---|
[ ] | 空捕获列表。lambda不能使用所在函数中的变量。一个lambda只有捕获变量后才能使用它们 |
[names] | names是一个逗号分隔的名字列表,这些名字都是lambda所在函数的局部变量。默认情况下,捕获列表中的变量都被拷贝。名字前如果使用了&,则采用引用捕获的方式 |
[&] | 隐式捕获列表,采用引用捕获方式。lambda体中所使用的来自所在函数的实体都采用引用方式使用 |
[=] | 隐式捕获列表,采用值捕获方式。lambda体将拷贝所使用的来自所在函数的实体的值 |
[&, identifier_list] | identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list中的名字前面不能使用& |
[=, identifier_list] | identifier_list中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list中的名字不能包括this,且这些名字之前必须使用& |
[this] | 捕获当前类中的 this指针,让 lambda 表达式拥有和当前类成员函数同样的访问权限。如果已经使用了 & 或者 =,就默认添加此选项。捕获 this 的目的是可以在 lamda 中使用当前类的成员函数和成员变量。 |
可变lambda
默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。因此,可变lambda能省略参数列表:
void func()
{
size_t val = 42; // 局部变量
// f可以改变它所捕获的变量的值
auto f = [val] () mutable { return ++val; };
val = 0;
auto j = f(); // j为43
}
指定lambda返回类型
当return不止一个时,要使用 -> 确定返回类型
动态内存与智能指针
==注意:智能指针在std的namespace下==
为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:**shared_ptr**允许多个指针指向同一个对象;**unique_ptr**则“独占”所指向对象。标准库还定义了一个名为**weak_ptr**的伴随类,它是一个弱引用,指向shared_ptr所管理的对象。三者均定义在memory头文件
shared_ptr类
shared_ptr<string> p1; // shared_ptr,可以指向string
shared_ptr<list<int>> p2; // shared_ptr,可以指向int的list
默认初始化的智能指针中保存着一个空指针。
智能指针的使用方式与普通指针类似。解引用一个智能指针返回它所指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空:
//如果p1不为空,检查它是否指向一个空string
if(p1 && p1->empty())
*p1 = "hi"; //如果p1指向一个空string,解引用p1,将一个新值赋予string
<u>shared_ptr独有的操作</u> | |
---|---|
make_shared<T>(args) | 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象 |
shared_ptr<T>p(q) | p是shared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换成为T* |
p = q | p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放 |
p.unique() | 若p.use_count()为1,返回true;否则返回false |
p.ues_count | 返回与p共享对象的智能指针数量;可能很慢,主要用于调试 |
shared_ptr和unique_ptr都支持的操作 | |
---|---|
p.get() | 返回p中保存的指针。要小心使用,若智能指针释放了对象,返回的指针所指向的对象也就消失了 |
swap(p, q) <=> p.swap(q) | 交换p和q中的指针 |
make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。定义在memory头文件中
当要用make_shared时,必须指定想要创建的对象的类型
// p3指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// p4指向一个值为"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10, '9');
// p5指向一个初始化的int,即,值为0
shared_ptr<int> p5 = make_shared<int>();
当然,我们通常用auto定义一个对象来保存make_shared的结果,这种方式比较简单:
// p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vector<string>>();
shared_ptr的拷贝和赋值
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:
auto p = std::make_shared<int>(42); // p指向的对象只有p一个引用者
auto q(p); // p和q指向相同对象,此对象有两个引用者
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:
auto r = std::make_shared<int>(42); // r指向的int只有一个引用者
r = q; // 给r赋值,另它指向另一个地址
// 递增q指向的对象的引用计数
// 递减r原来指向的对象的引用计数
// r原来指向的对象已没有引用者,会自动释放
shared_str通过析构函数自动销毁所管理的对象
shared_ptr的析构函数会递减它所指向的对象的引用技术。如果引用技术变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。shared_str还会自动释放相关联的内存
shared_str和new结合使用
接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
std::shared_ptr<int> p1 = new int(1024); // 错误:必须使用直接初始化形式
std::shared_ptr<int> p2(new int(1024)); // 正确:使用了直接初始化形式
p1的初始化隐式地要求编译器用一个new返回的int*创建一个shared_ptr。由于我们不能将一个内置指针到智能指针间的隐式转换,因此这条初始化语句是错误的。出于相同的原因,一个返回shared_str的函数不能在其返回语句中隐式转换一个普通指针:
std::shared_ptr<int> clone(int p)
{
return new int(p); // 错误:隐式转换为shared_ptr<int>
}
我们必须将shared_ptr显示绑定到一个想要返回的指针上:
std::shared_ptr<int> clone(int p)
{
// 正确:显示地用int*创建shared_ptr<int>
return std::shared_ptr<int>(new int(p));
}
智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:
·不使用相同的内置指针值初始化(或reset)多个智能指针 ·不delete get()返回的指针 ·不使用get()初始化或reset另一个智能指针 ·如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了 ·如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
智能指针和异常
使用异常处理的程序能在异常发生后令程序流程继续,我们注意到,这种程序需要确保在异常发生后资源能被正确地释放。一个简单的确保资源被释放的方法是使用智能指针。
如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:
void f()
{
std::shared_ptr<int> sp(new int(42)); // 分配一个新对象
// 这段代码抛出一个异常,且在f中未被捕获
// 在函数结束时shared_ptr自动释放内存
}
与之相对的,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会释放:
void f()
{
int* ip = new int(42); // 动态分配一个新对象
// 这段代码抛出一个异常,且在f中未被捕获
delete ip; // 在退出之前释放内存
}
如果在new和delete之间发生异常,且异常未在f中捕获,则内存就永远不会被释放了。在函数f之外没有指针指向这块内存,因此就无法释放它了
智能指针和哑类
struct destination; // 表示我们正在连接什么
struct connection; // 使用连接所需要的信息
connection connect(destination*); // 打开连接
void f(destination& d /* 其他参数 */); // 关闭给定的连接
{
// 获得一个连接;记住使用完后要关闭它
connect c = connect(&d);
// 使用连接
// 如果我们在f退出前忘记调用disconnect,就无法关闭c了
}
如果connection有一个析构函数,就可以在f结束时由析构函数自动关闭连接。但是,connection没有析构函数。这个问题与我们上一个程序中使用shared_ptr避免内存泄漏几乎是等价的。使用shared_ptr来保证connection被正确关闭,已被证明是一种有效方法
使用我们自己的释放操作
默认情况下,shared_ptr假定它们指向的是动态内存。因此,当一个shared_ptr被销毁时,它默认地对它管理的指针进行delete操作。为了用shared_ptr来管理一个connection,我们必须首先定义一个函数来代替delete。这个**删除器**(**deleter**)函数必须能够完成对shared_ptr中保存的指针进行释放的操作。在本例中,我们的删除器必须接受单个类型为connection*的参数:
void end_connection(connection* p) { disconnect(*p); }
当我们创建一个shared_ptr时,可以传递一个(可选的)指向删除器函数的参数:
void f(destination& d /* 其他参数 */); // 关闭给定的连接
{
connection c = connect(&d);
std::shared_ptr<connection> p(&c, end_connection);
// 使用连接
// 当f退出时(即使是由于异常而退出),connection会被正确关闭
}
当p被销毁时,调用end_connection
unique_ptr
与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式:
std::unique_ptr<string> p1(new string("Stegosaurus"));
std::unique_ptr<string> p2(p1); // 错误:unique_ptr不支持拷贝
std::unique_ptr<string> p3;
p3 = p2; // 错误:unique_ptr不支持赋值
unique_str操作 | |
---|---|