C++ Primer笔记

C++ Primer笔记

大学时学的C,C++都是C98,C99标准,且学的比较基础,许多新特性、细节问题没有了解过,在此记录学习C++ Primer过程中一些要点。

类型选择建议

和C语言一样,C++的设计准则之一也是尽可能地接近硬件。C++的算术类型必须满足各种硬件特质,所以它们常常显得繁杂而令人不知所措。事实上,大多数程序员能够(也应该)对数据类型的使用做出限定从而简化选择的过程。以下是选择类型的一些经验准则:

  • 当明确知晓数值不可能为负时,选用无符号类型。
  • 使用int执行整数运算。在实际应用中,short常常显得太小而long一般和int有一样的尺寸。
  • 如果你的数值超过了int的表示范围,选用long long。
  • 在算术表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们。因为类型char在一些机器上是有符号的,而在另一些机器上又是无符号的,所以如果使用char进行运算特别容易出问题。如果你需要使用一个不大的整数,那么明确指定它的类型是signed char或者unsigned char。
  • 执行浮点数运算选用double,这是因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。事实上,对于某些机器来说,双精度运算甚至比单精度还快。long double提供的精度在一般情况下是没有必要的,况且它带来的运行时消耗也不容忽视。

初始化

int main() {
    int b = 0;
    int x(0);
    int y{}; // 0
    int a(); // 0
    int z = {0};

    double ld = 3.1415926536;
    int c(ld), d = ld; // 成功转化,丢失部分值
    int a{ld}, b = {ld}; // 拒绝转化,可能丢失值

    return 0;
}

变量声明

变量的定义包括一个基本数据类型(base type)和一组
声明符。在同一条定义语句中,虽然基本数据类型只有一个,但是声明
符的形式却可以不同。也就是说,一条定义语句可能定义出不同类型的
变量。

int i = 1024, *p = &i, &r = i;

基本数据类型和类型修饰符,后者是声明符的一部分。

复合声明判断类型

int i = 1024;
int *p = &i;
int *&r = p;
int *const ptr1 = &i; // 不能写ptr1=xxx
const int *ptr2 = &i; // 不能写*ptr2==xxx

要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号对变量的类型有最直接的影响。

  1. 离r最近的是&,因此是一个引用。声明符的其余部分用li以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。
  2. 离ptr1最近的是const,意味着ptr1本身是一个常量,下一个符号是*,表示ptr1是一个常量指针,即ptr1所指向的地址是不能变化的,最后是int,其指向的内存地址中保存的值是一个int。ptr1是一个指向整型变量的常量指针。
  3. 离ptr2最近的是*,表示ptr2是一个指针,下一个符号是int,表示该指针指向的值都类型是int,最后是const,表示其指向的int类型的值不可改变。ptr2是一个指向整型常量的指针。

编译连接过程

  1. 预处理(PreProcessing),预处理用于将所有的include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多,对每个文件都要进行该操作。
  2. 完成代码替换后,将每个文件编译为.obj文件,这时候需要依次解析文件中的每个符号,在不同的文件中符号允许多次声明,但只能有一次定义
  3. 链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。

默认状态下,const对象仅在文件内有效

编译器将在编译过程中把用到const变量的地方都替换成对应的值。为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义(通过include或者其他文件或者每个文件都自己定义,因为const是不可变的,只要每个文件的const值一样,就不会有问题)。为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。以下是两个例子。

// 1.cpp
const int x = 100;

// main.cpp
#include <iostream>
#include 1.cpp”
int main(){
  std::cout << x;
  return 0;
};
  1. 处理编译1.cpp,预处理无替换代码,只声明且定义了一个const变量,因为const只有在该文件中有效,符号表中会标记该符号仅在当前文件有效。
  2. 处理编译main.cpp, #incude "1.cpp"被替换为const int x = 100,编译时x被替换为常量100,编译通过且结果正确。
  3. 链接,无变量的缺失定义和重复定义。事实上,const变量在完成替换后本身就没有什么意义了
    去掉const,即如下所示,编译通过,但是链接报错,x被重复定义。
// 1.cpp
int x = 100;

// main.cpp
#include <iostream>
#include 1.cpp”
int main(){
  std::cout << x;
  return 0;
};
  1. 处理编译1.cpp,预处理无替换代码,只声明且定义了一个全局变量x。
  2. 处理编译main.cpp, #incude "1.cpp"被替换为int x = 100,声明且定义了一个全局变量x。
  3. 链接,1.obj和main.obj都定义了一个全局变量x,报重复定义错误(符号允许多次声明,但只能有一次定义)。

某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类const对象像其他(非常量)对象一样工作,也就是说,只在一个文件中定义const,而在其他多个文件中声明并使用它。解决的办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了。

// a.cpp
extern const int x = function();
// b.cpp
extern const int x;

如上述程序所示,a.cpp定义并初始化了bufSize。因为这条语句包含了初始值,所以它(显然)是一次定义。然而,因为x是一个常量,必须用extern加以限定使其被其他文件使用。b.cpp中的声明也由extern做了限定,其作用是指明x并非本文件所独有,它的定义将在别处出现。

typedef别名

typedef int *pint;
typedef double **ppd;

int main() {
    int a = 100;
    int b = 200;
    const pint ptr = &a; // 指向int类型的常量指针
    const double x = 1.0;
    const double *ptr1 = &x; // 指向double常量的指针
    const double **ptr2 = &ptr1; // 指向一个指针,该指针指向的是double常量
    cdouble ptr3 = &ptr1; // 指向一个指针,该指针指向的是double常量
    double z = 1;
    double *ptr4 = &z;
    double **ptr5 = &ptr4;
    ppd ptr6 = &ptr4;
    const ppd ptr7 = &ptr4; // ptr7是一个常量指针,它指向的是一个指向double的指针。
    return 0;
}

从右往左读。

  1. pint是类型别名,*表示是指针,int表示指向int类型,pint即是指向int类型的指针。
  2. ppd是类型别名,*表示是指针,**表示指向指针的指针,double表示指向的是double类型,所以ppd即使指向double指针的指针。
  3. 右往左读,ptr是指向int类型的指针,被const修饰,ptr是指向int类型的常量指针。简单将pint替换为int *表示的是指向int常量的指针,是错误的理解

避免类型转换

强制类型转换干扰了正常的类型检查,强烈建议程序员避免使用强制类型转换。这个建议对于reinterpret_cast尤其适用,因为此类类型转换总是充满了风险。在有重载函数的上下文中使用const_cast无可厚非;但是在其他情况下使用const_cast也就意味着程序存在某种设计缺陷。其他强制类型转换,比如static_cast和dynamic_cast,都不应该频繁使用。每次书写了一条强制类型转换语句,都应该反复斟酌能否以其他方式实现相同的目标。就算实在无法避免,也应该尽量限制类型转换值的作用域,并且记录对
相关类型的所有假定
,这样可以减少错误发生的机会。

可变参数

可变参数宏(C实现)

利用头文件stdarg.h中的宏定义:va_list、va_start(va_list, arg)、va_arg(va_list, type)、va_end(va_list)。可以实现可变参数且参数类型不同的函数。

#include <stdarg.h>
#include <stdio.h>
int sum(int count, ...) //count 表示可变参数个数
{
    va_list ap;          //声明一个va_list变量
    va_start(ap, count); //初始化,第二个参数为最后一个确定的形参(不能另开局部变量)
    int sum = 0;
    for (int i = 0; i < count; i++)
        sum += va_arg(ap, int); //读取可变参数,第二个参数为可变参数的类型
    va_end(ap); //清理工作
    return sum;
}
int main()
{
    printf("%d\n", sum(5, 1, 2, 3, 4, 5));
    return 0;
}

可变参数是跟在count后面,依次压到栈中的,这种方式对与参数类型和数量没有任何安全性保证,全靠程序员控制。

initializer_list标准库类型(C++11)

#include <initializer_list>
#include <iostream>
int sum(std::initializer_list<int> il)
{
    int sum = 0;
    for (auto p : il)   // 使用范围for
        sum += p;
    // for (auto p = il.begin(); p != il.end(); p++) //使用迭代器访问参数
    //     sum += *p;
    return sum;
}
int main()
{
    std::cout << sum({1, 2, 3, 4, 5}) << std::endl;
    return 0;
}

python中的*args与此类似。

可变参数模板(C++11)

更方便安全有效地实现可变参数且参数类型不同的函数。

  1. 编写含有模板参数包和函数参数包的模板函数
  2. 函数定义递归调用自己
  3. 利用函数重载(参数包含有零个参数)来处理边界情况,编写处理边界情况的模板

C++ 内联函数和constexpr函数可以在程序中定义不止一次

能定义不止一次的好处是方便你放到头文件里,放到头文件里的好处是每个include这个头文件的.c文件都能看到函数体,看到函数体的好处是编译器可以内联。内联的好处是代码变快了。另外,所有函数体定义必须一模一样,不然出了问题概不负责。constexpr自带inline属性。当你下决心在.c文件中定义函数体的时候,自然不需要inline关键字了。而这时候也必须link相应的.o来确保找得到定义。

这篇文章回答得十分详细,涉及许多工具链编译链接的细节,推荐阅读。

NDEBUG预处理变量

#ifndef NDEBUG
    std::cout << __FILE__ << std::endl;
    std::cout << __func__ << std::endl;
    std::cout << __DATE__ << std::endl;
    std::cout << __LINE__ << std::endl;
    std::cout << __TIME__ << std::endl;
    std::cout << __TIMESTAMP__ << std::endl;
#endif

函数指针

#include <iostream>

// 以下两个别名等价
// bpf2是一个指针,有形参列表,指向的是一个函数,该函数的返回值是bool类型
typedef bool (*bpf2)(double, double);
using bpf = bool (*)(double, double);

bool f(double a, double b) {
    std::cout << a << std::ends << b << std::endl;
    return true;
}

void f(bpf2 pf) {
    std::cout << *pf << std::endl;
    pf(3, 4);
}


int main(int argc, char *argv[]) {
    bpf pf = f;
//    bpf2 pf = f;
//    bool (*pf)(double, double) = f;
    f(pf);
    return 0;
}

内联函数

当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用。

  1. 当函数体比较小的时候, 内联该函数可以令目标代码更加高效。对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联。
  2. 滥用内联将导致程序变慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
  3. 一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用。
  4. 另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行)。
  5. 在类中定义的成员函数默认是内联的。也可以在类的内部把inline作为声明的一部分显式地声明成员函数,同样的,也能在类的外部用inline关键字修饰函数的定义:虽然我们无须在声明和定义的地方同时说明inline,但这么做其实是合法的。不过,最好只在类外部定义的地方说明inline,这样可以使类更容易理解。

有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数。

本人在学习过程中碰到了一个内联函数声明单位定义的链接错误。

// screen.h

#ifndef UNTITLED_SCREEN_H
#define UNTITLED_SCREEN_H

#include <iostream>

class Screen {
public:
    typedef std::string::size_type pos;

    Screen() = default;

    Screen(Screen::pos, Screen::pos, char);

    // 内联,定义在screen.cpp
    char get() const;

    // 默认内联
    char get(pos r, pos c) const {
        return contents[r * width + c];
    };

    // 内联,定义在screen.cpp
    Screen &move(pos w, pos h);

private:
    pos cursor{0};
    pos height{0}, width{0};
    std::string contents;

};


#endif //UNTITLED_SCREEN_H

// screen.cpp

#include "screen.h"

Screen::Screen(Screen::pos wt, Screen::pos ht, char c) : width(wt), height(ht),
                                                         contents(std::string(wt * ht, c)) {
    // 在构造函数中调用此内联函数后,链接不报错
//    get();
}


inline Screen &Screen::move(Screen::pos r, Screen::pos c) {
    cursor = r * width + c;
    return *this;
}

inline char Screen::get() const {
    return contents[cursor];
}

// main.cpp
#include "clazz/screen.h"

int main() {
    Screen s(10, 10, 'c');
    std::cout << s.get() << std::endl;
    return 0;
}

链接报错, undefined reference to Screen::get() const,原因是内联函数声明与定义分开时,不可以被其他文件引用。就是说,在类A中,有一个内联函数a(),如果我们在A.h中对其声明并定义,那么并没有影响;但如果我们在A.h中对其声明,然后在A.cpp中对其定义,那么不能在其他的类中再使用它。
内联时必须已经有了函数体才能做代码展开,如果函数定义在其它.cpp文件中编译的时候函数定义的代码就在其它obj文件里了,也就无法展开。这就解释了为什么在构造函数中提前调用一次get函数就不会报错了,因为找到了内联函数的定义
更多关于内联函数的注意点可以查看这篇博客

构造函数初始化及顺序

使用构造函数初始值

  1. 如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数
    体之前执行默认初始化(类中定义属性时的初始值)。
  2. 随着构造函数体一开始执行,初始化就完成了。我们初始化const、引用类型的数据成员或无默认构造函数的类的唯一机会就是通过构造函数初始值。
  3. 在很多类中,初始化和赋值的区别事关底层效率问题:前者直接初始化数据成员,后者则先初始化再赋值。除了效率问题外更重要的是,一些数据成员必须被初始化。建议养成使用构造函数初始值的习惯,这样能避免某些意想不到的编译错误,特别是遇到有的类含有需要构造函数初始值的成员时。

构造函数初始化顺序

  1. 初始化顺序与构造函数定义指定的初始化顺序无关,而由成员属性的声明顺序决定。
  2. 最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可能的话,尽量避免使用某些成员初始化其他成员,全部使用参数初始化所有成员。

聚合类

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

  • 所有成员都是public的。
  • 没有定义任何构造函数。
  • 没有类内初始值。
  • 没有基类,也没有virtual函数。
    聚合类不像普通类那么复杂,仅仅是维护一堆属性的组合。使用花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员,初始值的顺序必须与声明的顺序一致。

IO流

  • C++流包括istream, ostream,基于istream继承实现了istringstream和ifstream,基于ostream继承实现了ostringstream和ofstream。

  • 由于不能拷贝IO对象,因此我们也不能将形参或返回类型设置为流类型。进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。

  • IO库定义了一个与机器无关的iostate类型,它提供了表达流状态的完整功能。IO库定义了4个iostate类型的constexpr值,表示特定的位模式。这些值用来表示特定类型的IO条件,可以与位运算符一起使用来一次性检测或设置多个标志位。

    • badbit表示系统级错误,如不可恢复的读写错误。通常情况下,一旦badbit被置位,流就无法再使用了。
    • 在发生可恢复错误后,failbit被置位,如期望读取数值却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用。
    • 如果到达文件结束位置,eofbit和failbit都会被置位。
    • goodbit的值为0,表示流未发生错误。
    • 如果badbit、failbit和eofbit任一个被置位,则检测流状态的条件会失败。
  • 流对象的rdstate成员返回一个iostate值,对应流的当前状态。

  • setstate操作将给定条件位置位,表示发生了对应错误。

  • clear成员是一个重载的成员:它有一个不接受参数的版本,而另一个版本接受一个iostate类型的参数。clear不接受参数的版本清除(复位)所有错误标志位。执行clear()后,调用good会返回true。

标准库容器

在这里插入图片描述
新标准库的容器比旧版本快得多。新标准库容器的性能几乎肯定与最精心优化过的同类数据结构一样好(通常会更好)。现代C++程序应该使用标准库容器,而不是更原始的数据结构,如内置数组。

lambda表达式

lambda的实质

当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。
类似的,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。
捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字。

尽量保持lambda的变量捕获简单化

  • 一个lambda捕获从lambda被创建(即,定义lambda的代码执行时)到lambda自身
    执行(可能有多次执行)这段时间内保存的相关信息。确保lambda每次执行的时候这
    些信息都有预期的意义,是程序员的责任。
  • 捕获一个普通变量,如int、string或其他非指针类型,通常可以采用简单的值捕获
    方式。在此情况下,只需关注变量在捕获时是否有我们所需的值就可以了。
  • 如果我们捕获一个指针或迭代器,或采用引用捕获方式,就必须确保在lambda执
    行时,绑定到迭代器、指针或引用的对象仍然存在
    。而且,需要保证对象具有预期的
    值。在lambda从创建到它执行的这段时间内,可能有代码改变绑定的对象的值。也就
    是说,在指针(或引用)被捕获的时刻,绑定的对象的值是我们所期望的,但在
    lambda执行时,该对象的值可能已经完全不同了。
  • 一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而
    且,如果可能的话,应该避免捕获指针或引用

    在这里插入图片描述

std::bind包装函数

#include <iostream>
#include <functional>

int func(int x, int y) {
    return x + y;
}

int main() {
    auto fn = std::bind(func, std::placeholders::_1, 6);
    std::cout << fn(8) << std::endl; // 8 + 6
    return 0;
}

相当于python中的functools.partial
如果我们希望传递给bind一个对象而又不拷贝它(例如istream或者ostream或者传递一个引用),就必须使用标准库ref函数。函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库中还有一个cref函数,生成一个保存const引用的类。

动态内存管理

手动new和delete的弊端

使用new和delete管理动态内存存在三个常见问题:

  1. 忘记delete内存。忘记释放动态内存会导致人们常说的“内存泄漏”问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄露错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误。
  2. 使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误。
  3. 同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了。如果我们随后又delete第二个指针,自由空间就可能被破坏。相对于查找和修正这些错误来说,制造出这些错误要简单得多。
    坚持只使用智能指针,就可以避免所有这些问题。对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。

不要混合使用普通指针和智能指针

考虑以下代码

#include <iostream>
#include <memory>

class Demo {
public:
    int x{0};

    Demo() = default;

    ~Demo() {
        std::cout << "deconstructed" << std::endl;
    }
};

void use_ptr(const std::shared_ptr<Demo> ptr) {
}

int main() {
    // 错误的使用方法
    Demo *pDemo = new Demo;
    std::cout << pDemo->x << std::endl;
    use_ptr(std::shared_ptr<Demo>(pDemo)); // 调用正确,但调用结束后内存会被释放
    std::cout << pDemo->x << std::endl;
    std::cout << "========" << std::endl;
    // 正确的使用方法
    std::shared_ptr<Demo> ptr1 = std::make_shared<Demo>();
    std::cout << ptr1->x << std::endl;
    use_ptr(ptr1);
    std::cout << ptr1->x << std::endl;
    return 0;
}

// 输出结果
0
deconstructed
1157962512
========
0
0
deconstructed

use_ptr函数参数是值传递的,因此会拷贝智能指针。

  1. 第一种调用方式,std::shared_ptr<Demo>(pDemo)创建了临时变量,引用计数+1;调用use_ptr,需要进行拷贝,此时引用计数为2;函数调用结束,引用计数减1;临时变量销毁,引用计数减1,pDemo指向的内存被释放!再次使用pDemo发生未定义的后果!
  2. 第二种调用方式ptr1保证了引用计数为1,内存不会被释放。

使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

智能指针陷阱

智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:

  • 不使用相同的内置指针值初始化(或reset)多个智能指针。
  • 不delete get()返回的指针。
  • ** 不使用get()初始化或reset另一个智能指针。**
  • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的
    指针就变为无效了。
  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
int main() {
    int demo;
    /*demo非堆内存,会被自动释放,我们什么也不做*/
    auto sp = std::shared_ptr<int>(&demo,
                                   [](int *d) {}
    );
    return 0;
}

类的拷贝控制

拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。如果我们没有定义拷贝构造函数,编译器会合成默认拷贝构造函数,从给定对象中依次将每个非static成员拷贝到正在创建的对象进行初始化

  • 对类类型的成员,会使用其拷贝构造函数来拷贝。
  • 内置类型的成员则直接拷贝,指针类型只拷贝指针本身。
  • 数组则逐元素按照上述规则拷贝。

拷贝构造发生在以下几种请况

  • 用=定义变量。
  • 将一个对象作为实参传递给一个非引用类型的形参。
  • 从一个返回类型为非引用类型的函数返回一个对象。
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员。

拷贝赋值运算符

接受一个相同类型的引用参数,返回自身的引用。
如果类未定义自己的拷贝赋值运算符,编译器会合成默认的,从给定对象中依次将每个非static成员拷贝到正在创建的对象进行赋值

析构函数

如同构造函数有一个初始化部分和一个函数体,析构函数也有一个函数体和一个析构部分。在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。
在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何收尾工作。通常,析构函数释放对象在生存期分配的所有资源。
在一个析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。
隐式销毁一个内置指针类型的成员不会delete它所指向的对象,程序员在析构函数的函数体中释放指针所指的内存,析构部分完成指针本身的析构。
编译器默认合成的析构函数函数体为空。
无论何时一个对象被销毁,就会自动调用其析构函数

  • 变量在离开其作用域时被销毁。
  • 当一个对象被销毁时,其成员被销毁。
  • 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。
  • 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁。
  • 对于临时对象,当创建它的完整表达式结束时被销毁。

default和delete

  • 使用=default告诉编译器合成默认的构造函数或拷贝构造函数。当我们在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的。
  • 使用=delete阻止拷贝或拷贝赋值运算符,不应使用private访问控制符。
  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值