CppPrimer零散知识.md

处理类型

类型别名

    `使用类型别名让复杂的类型名字变得简单明了,易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。`
传统方法
    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;
}
image-20211104181652131
  • 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操作
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容