留给自己忘了看一下

239 阅读15分钟

留给自己忘了看一下

默认构造函数和构造函数默认参数(总忘,记录一下)

#include <bits/stdc++.h>

class Message {
public:
    Message() { std::cout << "Message Construct()" << std::endl; }
    Message(const std::string& str = "") : content{str}{ std::cout << "Message Construct(const std::string& str)" << std::endl;}

private:
    std::string content;
};

class Folder {
public:
    Folder() {std::cout << "Message Folder()" << std::endl;}
};

int main()
{
    Message m1("abc");
    Message m2; // 会出错;Message m2()这样写也会出错
    Folder f;

    return 0;
}

这样写不行的,因为Message() 和 Message(const std::string& str = "")有重合的部分,当我不给 Message(const std::string& str = "")传任何字符串的时候,编译器不知道该执行 Message() 还是 Message(const std::string& str = "")了,

所以会报错:类 "Message" 包含多个默认构造函数

解决:当有构造函数默认参数时,就不要写无参的构造了~(不知道对不对)

构造函数的参数缺省(总忘)

#include <iostream>
using namespace std;
class A
{
  public :
  A(int aa=0,int bb=00); //在声明构造函数时指定默认参数
  int volume( );
 
  int a;
  int b;
  
};

int main( )
{
 
 A obj(4);
 cout<<obj.a<<" "<<obj.b;  
return 0;
}

运行结果:

4 0

overload重载

overload:同一作用域内的几个函数名字相同但形参列表(形参数量或形参类型)不同,

特别注意:

int lookup(Eigen);
int lookup(const Eigen);
int lookup(Eigen&);
int lookup(const Eigen&);
// 如果只是函数对象或顶层const,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分是否重载:
int lookup(Eigen)
{
    return 5;
}

int lookup(const Eigen) // 报错!!!,Redefinition of 'lookup'
{
    return 5;
}

int lookup(Eigen*) // 报错!!!,Redefinition of 'lookup'
{
    return 5;
}

int lookup(Eigen* const) // 报错!!!,Redefinition of 'lookup'
{
    return 5;
}

// 如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载
int lookup(Eigen&)
{
    return 5;
}

int lookup(const Eigen&) // overload
{
    return 5;
}

int lookup(Eigen*) // overload
{
    return 5;
}

int lookup(const Eigen*) // overload
{
    return 5;
}

const_cast和overload

const std::string& shorterString(const std::string &s1, const std::string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}

std::string& shorterString(std::string &s1, std::string &s2)
{
    auto &r = shorterString(const_cast<const std::string&>(s1),
                            const_cast<const std::string&>(s2));
    return const_cast<std::string&>(r);
}

把inline函数和constexpr函数在头文件内

unique_ptr

#include <iostream>
#include <memory>
#include <vector>
#include <exception>

using namespace std;

void test1()
{
    /* unique_ptr的两个方法:reset和release */
    std::unique_ptr<std::string> p1(new std::string("Text"));
    std::unique_ptr<std::string> p2(p1.release()); // release将p1置位空
    //cout << "*p1 = " << *p1 << endl;
    cout << "*p2 = " << *p2 << endl;
    unique_ptr<string> p3(new string("happy"));
    p2.reset(p3.release()); // 将所有权从p3转移给p2 reset释放了p2原来指向的内存"Text"
    cout << "*p2 = " << *p2 << endl;
}

// 例外:不能拷贝unique_ptr的规则有一个列外,我们可以拷贝或赋值一个将要被销毁的unique_ptr
unique_ptr<int> test2(int p)
{
    //return unique_ptr<int>(new int(p));
    //或者
    unique_ptr<int> ret(new int(p));
    return ret;
}

void test3()
{
    /* unique_ptr: 只移型别(move-only type) 类似的还有std::mutex std::move
     *
     * */
}
/* -----------------------------分隔符 */

class Animal
{
public:
    virtual void print() const = 0;
};

class Dog : public  Animal
{
public:
    void print() const override
    {
        cout << "Dog!" << endl;
    }
};

/* 抽象工厂 针对抽象类的工厂函数 */
class AnimalFactory
{
public:
    virtual unique_ptr<Animal> createAnimal() = 0;
};

/* Dog工厂继承抽象工厂 */
class DogFactory : public AnimalFactory
{
    unique_ptr<Animal> createAnimal() override
    {
        cout << "class DogFactory  createAnimal() " << endl;
        return make_unique<Dog>(); // 在堆上开辟空间创建对象 相当于return new Dog(); 
    }
};

class MyTest
{
public:
    MyTest(unique_ptr<AnimalFactory> animal_fac)
            : animal_fac_(std::move(animal_fac)) { cout << "MyTest()" << endl; } // unique_ptr<AnimalFactory> animal_fac_ = make_unique<DogFactory>();
    void buttonClick()
    {
        // 发生多态  因为传入的animal_fac有可能是派生类  生成一个Animal
        auto create_animal = animal_fac_->createAnimal();
//        cout << "" << typeid(create_animal).name() << endl;
        create_animal->print();
    }
private:
    unique_ptr<AnimalFactory> animal_fac_; // 内部成员封装工厂
};

int main()
{
    test1();
    auto p = test2(10);
    test3();
    cout << "---------------分割符" << endl;

    // 这样传入的实际上是一个DogFactory派生类工厂  所以会创建一个派生类对象
    // 右侧相当于unique_ptr<AnimalFactory> animal_fac = make_unique<DogFactory>();
    auto test = MyTest( make_unique<DogFactory>() ); // auto test = ... 相当于拷贝构造初始化
    test.buttonClick();
    cout << "---------------分割符" << endl;

    /* 了解unique_ptr的两个参数形式 */
    // 这个lambda是一个闭包的形式 my_del:定制的删除器
    auto my_del = [](Animal *animal) -> void
    {
        cout << "delete :";
        animal->print();
        delete animal;
    };
    // unique_ptr的源码包含两个模板参数,
    unique_ptr<Animal, decltype(my_del)> t(new Dog, my_del);
    // make_unique<>()方法只能使用默认的删除器,如果想使用定制的删除器就不行了,
    t->print();
    cout << "---------------分割符" << endl;

    /* unique_ptr可以很方便的传给shared_ptr 可以看到虽然shared_ptr没有第二个模板参数,但是也可以转成shared_ptr */
    shared_ptr<Animal> s_ptr = std::move(t); // 把t变成右值的形式
    s_ptr->print();

    return 0;
}

参考: bilibili

shared_ptr

move construct(不保证对)

//
// Created by user on 2022/5/25.
//
/* move construct */

#include <bits/stdc++.h>

#define PRINT_INFO

using std::cout;
using std::endl;


namespace utils
{
    template<typename Func, typename... Para>
    void testAndTiming(Func f, Para&& ... args)
    {
        auto start = std::chrono::steady_clock::now();
        // f是一个变量 这个变量的类型是一个Func
        f (std::forward<Para>(args)...);
        auto end = std::chrono::steady_clock::now();
        auto duration = static_cast<std::chrono::duration<double, std::ratio<1, 1000>>>(end - start);
        std::cout << "time is " << duration.count() << std::endl;
    }
}


class MyString
{
private:
    char *data_ = nullptr;
public:
     MyString()
     {
       cout << "MyString construct()" << endl;
     }
    explicit MyString(char *str = nullptr) // 加了 = nullptr 相当于无参构造!!!所以会报错:类 "MyString" 包含多个默认构造函数
    {
#ifdef PRINT_INFO
        cout << "MyString construct(str)" << endl;
#endif
        if(str == nullptr)
            return;
        data_ = new char[strlen(str) + 1]; // 开空间
        strcpy(data_, str);
    }
    // copy construct (MyString &表示左值引用)
    MyString(const MyString &other)
    {
#ifdef PRINT_INFO
        cout << "MyString copy construct(other)" << endl;
#endif
        if(other.data_ == nullptr)
            return;
        data_ = new char[strlen(other.data_) + 1];
        strcpy(data_, other.data_);
    }

    ~MyString()
    {
        cout << "~MyString destruct()" << endl;
        delete []data_;
    }
    // 返回字符串
    const char* c_str() const
    {
        return data_;
    }
};

MyString foo()
{
    MyString str1{"shuaisi"}; // 构造一次
    // do something ...
    return str1; // 返回匿名 拷贝构造一次
}

class A
{
public:
    MyString str_;
    // 不是A的拷贝构造 这是A的构造函数  str_(str)会调用MyString的拷贝构造
    A(const MyString& str) : str_(str){}
};


int main()
{
    //MyString str1{"Hello"};


    // 拷贝一次
    MyString str = foo(); // 很可能会出现这种情况
    /* 1.目前对于上面一行的执行,MyString类中只有构造和拷贝构造,有一个工厂函数foo()返回一个MyString对象,如果在没有任何编译器优化的情况下,一共发生了多少次构造(有多少个MyString的对象被创建出来了)? */
    // 这里clion默认加了编译器优化,加了编译器优化后只有一次构造  不加编译器优化会首先执行一次构造函数,然后在执行两次copy构造 add_compile_options(-fno-elide-constructors) #关闭编译器优化

    /* 2.移动构造就是针对临时值(临时对象)的传递的时候,如何避免过多的copy构造? -> move construct! */
    // 只开一份空间 只是指针的移动 ,相当于一种浅拷贝 而copy construct相当于一种深拷贝
    //MyString str1 = foo(); // 调用move construct 浅拷贝
    //MyString str2 = str1;  // 调用copy construct 深拷贝
    /* 上述这两行的区别在于 一个是临时的返回值 一个是句名(named)的变量
    到底是用深拷贝(copy construct)还是用浅拷贝(move construct)的区别在于:用来初始化一个新变量的那个已经存在的变量的属性到底是一个左值还是右值
    临时的返回值只能是一个右值 而句名变量可以是一个左值
     */

    // 测试时间   Func f = void (*){}  100:是test1函数中的参数
    //utils::testAndTiming(test1, 100);

    return 0;
}

输出结果:

MyString construct(str) -> 对应MyString str1{"shuaisi"}; // 构造一次
MyString copy construct(other) -> return str1的时候,又会调用一次拷贝构造,拷贝到匿名的返回对象中 // 拷贝构造一次
~MyString destruct() -> foo()函数体执行结束,foo()中的str1析构一次
MyString copy construct(other) -> MyString str = foo() 又会调用一次拷贝构造,将foo()中返回的匿名对象拷贝到str中 //拷贝构造一次 
~MyString destruct() -> foo()中返回的匿名对象析构
~MyString destruct() -> str析构

当有了c++11标准后,可以这样写 : A a{MyString{"abc"}};

#include <bits/stdc++.h>

#define PRINT_INFO

using std::cout;
using std::endl;


class MyString
{
private:
    char *data_ = nullptr;
public:
     MyString()
     {
       cout << "MyString construct()" << endl;
     }
    explicit MyString(char *str = nullptr) // 加了 = nullptr 相当于无参构造!!!所以会报错:类 "MyString" 包含多个默认构造函数
    {
#ifdef PRINT_INFO
        cout << "MyString construct(str)" << endl;
#endif
        if(str == nullptr)
            return;
        data_ = new char[strlen(str) + 1]; // 开空间
        strcpy(data_, str);
    }
    // copy construct (MyString &表示左值引用)
    MyString(const MyString &other)
    {
#ifdef PRINT_INFO
        cout << "MyString copy construct(other)" << endl;
#endif
        if(other.data_ == nullptr)
            return;
        data_ = new char[strlen(other.data_) + 1];
        strcpy(data_, other.data_);
    }

    // move construct(移动构造) (MyString &&表示左值引用) 移动构造前面不能加const
    MyString(MyString&& other) noexcept
    {
#ifdef PRINT_INFO
        cout << "MyString move construct(other)" << endl;
#endif
        data_ = other.data_;
        other.data_ = nullptr; // 改值了 所以move construct传去参数前面是不能加const的
    }

    ~MyString()
    {
        cout << "~MyString destruct()" << endl;
        delete []data_;
    }
    // 返回字符串
    const char* c_str() const
    {
        return data_;
    }
};


class A
{
public:
    MyString str_;
    // 不是A的拷贝构造 这是A的构造函数  str_(str)会调用MyString的拷贝构造
    // 外部的MyString{"Hello"}是一个临时变量,就会移动给内部的MyString str,这里发生移动构造(浅拷贝),str_(str)还是发生拷贝构造!
    A(MyString str) : str_(str){}
};


int main()
{
    A a{MyString{"abc"}};

    return 0;

}

输出结果:

MyString construct(str)
MyString move construct(other)
MyString copy construct(other)
~MyString destruct()
~MyString destruct()
~MyString destruct()

但是传入的形参str也应该是一个临时变量,所以没有必要触发str_(str)的深拷贝,但为什么str_(str)会触发拷贝构造(深拷贝)而不是移动构造(浅拷贝)?

因为只有右值的时候,才会触发移动构造,str是句名(named)变量,句名变量是一个左值,所以不会触发移动构造!右值引用是不能绑定左值的 比如:

int c = 100;
int&& p = c; // 错的,右值引用不能绑定左值c

右值rvalue :非句名的

把左值转换成右值 : std::move(左值)

class A
{
public:
    MyString str_;
    // 把左值str转换成右值 因为str本身只起到传递作用 传递给str_
    A(MyString str) : str_{std::move(str)} {}
};

输出结果:

MyString construct(str)
MyString move construct(other)
MyString move construct(other)
~MyString destruct()
~MyString destruct()
~MyString destruct()
// move源码:就是把原来一些修饰符拿掉,让之强行转成右值引用的形式
/*
 * _Tp如果是int&  std::remove_reference<_Tp>就会把&去掉变成int  然后在加上&&就变成了右值引用的形式
 * static_cast<typename std::remove_reference<_Tp>::type&&> : 强转成&&右值引用的形式
 *
 * */
template<typename _Tp>
_GLIBCXX_NODISCARD
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

如果在加了个const 和 & 在 std::move()?首先外部的A a{MyString{"abc"}}会构造一次,然后传给内部A(const MyString& str)的时候,因为是引用,所以少一次构造,然后当str_{std::move(str)}时再调用一次移动构造????

class A
{
public:
    MyString str_;
    // 加了const 和 &
    A(const MyString& str) : str_{std::move(str)} {}
};

输出结果:

MyString construct(str)
MyString copy construct(other)
~MyString destruct()
~MyString destruct()

并没有与想的一样,调用移动构造,而是调用了拷贝构造 不写const 和 & 时,调用了两次移动构造,写const 和 & 后,调用了一次拷贝构造,std::move()不起作用了!

// 1. std::move(str), move::__Tp -> 推断成const MyString &;
// move()中调用remove_reference, remove_reference有三个模板,会选择特化模板(带一个&的)

/// remove_reference
template<typename _Tp>
  struct remove_reference
  { typedef _Tp   type; };

template<typename _Tp>
  struct remove_reference<_Tp&>
  { typedef _Tp   type; };

template<typename _Tp>
  struct remove_reference<_Tp&&>
  { typedef _Tp   type; };
  
// 2.remove_reference::_Tp -> 会与<_Tp&>匹配,并把&丢掉,推断成const MyString
// 3.之后std::remove_reference<_Tp>::type&&再加两个&&符号 -> 推断成const MyString&& 而移动构造的传入参数是没有const的!MyString&&,所以不会调移动构造,只能调拷贝构造!

参考: bilibili

  • 标准库容器、string和shared_ptr类既支持移动也支持拷贝

  • IO类和unique_ptr类可以移动但不能拷贝

  • 1.不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept

  • 2.在使用移动构造函数和移动赋值运算符时,一定要记住将源对象销毁!

  • 3.什么时候才用移动操作,确保以后不用此对象的情况下,才用移动

function pointer

对于下面这个函数:

int func()
{
    int n = 1;
    return n;
}

函数也是有类型的,比如上面这个函数的类型就是【无参并且返回值类型为整型】的函数,可以通过int (*func)()表示这种函数类型,再比如,int (*func)(int, int)表示有两个整型参数并且返回值为int的函数类型。

和变量一样,函数在内存中有固定的地址,函数的实质也是内存中一块固定的空间

cout << reinterpret_cast<void*>(func); // 结果是内存中的一个地址:0x7ff7875a16e0

指向函数的指针写法:

int (*func)();
int (*const func)(); // 静态的函数指针
const int(*func)(); // 指针指向的函数的返回值是常量

把一个函数赋值给函数指针:

int (*funcPtr)() = func; // funcPtr指向func()函数  注意!:不要带(),要不然表示将func()函数的int类型的值赋值给函数指针funcPtr
funcPtr = foo; // funcPtr改变指向另一个函数 
//但是千万不要写成funcPtr = foo();这是把foo的返回值赋值给了funcPtr

通过函数指针调用函数:

#include <bits/stdc++.h>
using namespace std;

int foo();
double goo();
int hoo(int x);

int foo()
{
    int n = 2;
    return n;
}
double goo()
{
    double n = 3;
    return n;
}
int hoo(int x)
{
    return x;
}
void test1()
{
    /* 函数的返回值类型 (*funcPtr)(函数的参数类型) = 函数名; */
    int (*funcPtr1)() = foo;
    //int (*funcPtr2)() = goo; // 不可以,goo的返回值为double,所以funcPtr2函数指针的类型也应该为double
    double (*funcPtr3)() = goo; // 可以
    //funcPtr1 = hoo; // 不可以,参数不匹配,funcPtr1只能指向不含参数的函数!!因为定义函数指针的时候为int (*funcPtr1)(),而hoo含有int型的参数
    int (*funcPtr4)(int) = hoo; // 可以

    int (*funcPtr5)() = foo;
    int (*funcPtr6)() = &foo; // // c++会隐式的将foo转换成&foo,所以不用加&
    /* 通过函数指针调用函数用法:funcPtr(传入参数) 或者 (*funcPtr)(传入参数) */
    cout << funcPtr5() << endl; //
    cout << (*funcPtr6)() << endl;

    int (*funcPtr7)(int) = hoo;
    cout << (*funcPtr7)(5) << endl; // 通过funcPtr7调用hoo(5)
    cout << funcPtr7(5) << endl;    // 也可以这样
}

函数作为参数传入另一个函数(函数指针的主要作用!):

int calEDT(int x, int y)
{
    // do something...
    return sqrt(pow(x, 2) + pow(y, 2));
}
int calMan(int x, int y)
{
    // do something...
    return norm(x) + norm(y);
}
/* 这才是函数指针最重要的作用!当做某个函数的参数,把函数作为参数传入另一个函数 */
// 这个函数要接收两个参数和一个函数指针,并且要在这个函数中完成通过函数指针调用函数的功能
void func(int a, int b, int (*fPtr)(int, int)) /* 相当于int (*fPtr)(int, int) = calEDT; 这里传入参数也可以写成int (*fPtr)(int x, int y) */
{
    cout << "把函数指针当做某个函数的参数,通过函数指针调用函数的功能:" << fPtr(a, b) << endl;
}

void test2()
{
    /* 把函数作为参数传入另一个函数 */
    int x = 4, y = 3;
    func(x, y, calEDT); // 注意:calEDT不要加(),因为要作为函数指针的右值,而不是要calEDT函数的返回值

}

例子:

void HelloWorld()
{
    cout << "hello world!" << endl;
}

void HelloWorld2(int a)
{
    cout << "a = " << a << endl;
}

int main()
{
    // 将函数赋值给了一个变量  -> 函数指针
    auto function = HelloWorld; // 如果去掉HellpWorld后面的(),实际上就不是在调用这个函数,而是获取到这个函数指针,得到函数的地址
    // 调用这个函数
    function();
    function();

    // auto的类型? 这样写起来很奇怪,所以很多人用auto
    void(*funcPtr)() = HelloWorld; // 我们需要给这个函数指针一个名字,就跟给其他变量的名字一样,这里给的函数指针的名字是funcPtr
    // 调用这个函数
    funcPtr();

    // 如果不用auto,可以使用typedef或者using来使用函数指针
    typedef void(*HelloWorldFuncPtr)();
    // 使用 HelloWorldFuncPtr:类型名(是个函数指针) hwfuncPtr:变量名
    HelloWorldFuncPtr hwfuncPtr = HelloWorld;
    // 调用
    hwfuncPtr();

    cout << "如果想传入参数" << endl;
    typedef void(*HelloWorld2FuncPtr)(int);
    HelloWorld2FuncPtr hw2funcPtr = HelloWorld2;
    // 调用的时候传入参数
    hw2funcPtr(8);

    return 0;
}

何时用函数指针?

void printValue(int value)
{
    cout << "val : " << value << endl;
}
void forEach(const vector<int>& vals, void(*func)(int))
{
    for(auto val : vals)
        func(val); // 会调用printValue,因为传入的函数指针func指向了printValue()这个函数
}

int main()
{
    // 为什么首先使用函数指针
    vector<int> vals = {1,5,4,2,3};
    // 我想创建一个函数,迭代遍历所有元素,然后执行某些操作
    forEach(vals, printValue);

    return 0;
}

不需要使用一个专门的函数printValue来打印一点点信息,-> lambda

forEach(vals,
        [](int value)
        {cout << "value :" << value << endl;}
);

lambda

lambda 表达式的类型在 C++11 中被称为“闭包类型(Closure Type)” -> 匿名函数

lambda 表达式还可以通过捕获列表捕获一定范围内的变量:

  • [] 不捕获任何变量。
  • [&] 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
  • [=] 捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。
  • [=,&foo] 按值捕获外部作用域中所有变量,并按引用捕获 foo 变量。
  • [bar] 按值捕获 bar 变量,同时不捕获其他变量。
  • [this] 捕获当前类中的 this 指针,让 lambda 表达式拥有和当前类成员函数同样的访问权限。如果已经使用了 & 或者 =,就默认添加此选项。捕获 this 的目的是可以在 lamda 中使用当前类的成员函数和成员变量。
class A
{
    public:
    int i_ = 0;
    void func(int x, int y)
    {
        auto x1 = []{ return i_; };                    // error,没有捕获外部变量
        auto x2 = [=]{ return i_ + x + y; };           // OK,捕获所有外部变量
        auto x3 = [&]{ return i_ + x + y; };           // OK,捕获所有外部变量
        auto x4 = [this]{ return i_; };                // OK,捕获this指针
        auto x5 = [this]{ return i_ + x + y; };        // error,没有捕获x、y
        auto x6 = [this, x, y]{ return i_ + x + y; };  // OK,捕获this指针、x、y
        auto x7 = [this]{ return i_++; };              // OK,捕获this指针,并修改成员的值
    }
};
int a = 0, b = 1;
auto f1 = []{ return a; };               // error,没有捕获外部变量
auto f2 = [&]{ return a++; };            // OK,捕获所有外部变量,并对a执行自加运算
auto f3 = [=]{ return a; };              // OK,捕获所有外部变量,并返回a
auto f4 = [=]{ return a++; };            // error,a是以复制方式捕获的,无法修改
auto f5 = [a]{ return a + b; };          // error,没有捕获变量b
auto f6 = [a, &b]{ return a + (b++); };  // OK,捕获a和b的引用,并对b做自加运算
auto f7 = [=, &b]{ return a + (b++); };  // OK,捕获所有外部变量和b的引用,并对b做自加运算

lambda 表达式的捕获列表精细地控制了 lambda 表达式能够访问的外部变量,以及如何访问这些变量。

一个容易出错的细节是关于 lambda 表达式的延迟调用c

int a = 0;
auto f = [=]{ return a; };      // 按值捕获外部变量
a += 1;                         // a被修改了
std::cout << f() << std::endl;  // 输出?

在这个例子中,lambda 表达式按值捕获了所有外部变量。在捕获的一瞬间,a 的值就已经被复制到f中了。之后 a 被修改,但此时 f 中存储的 a 仍然还是捕获时的值,因此,最终输出结果是 0

从上面的例子中我们知道,按值捕获得到的外部变量值是在 lambda 表达式定义时的值。此时所有外部变量均被复制了一份存储在 lambda 表达式变量中。此时虽然修改 lambda 表达式中的这些外部变量并不会真正影响到外部,我们却仍然无法修改它们。

那么如果希望去修改按值捕获的外部变量应当怎么办呢?这时,需要显式指明 lambda 表达式为 mutable:

int a = 0;
auto f1 = [=]{ return a++; };             // error,修改按值捕获的外部变量
auto f2 = [=]() mutable { return a++; };  // OK,mutable

需要注意的一点是,被 mutable 修饰的 lambda 表达式就算没有参数也要写明参数列表。

参考:c.biancheng.net/view/3741.h…

文本查询程序

/*
 * @Author: tonghuaa
 * @Date: 2022-06-21 15:38:52
 * @LastEditors: tonghuaa
 * @LastEditTime: 2022-06-21 18:13:30
 * @FilePath: \c++Primer\c++Primer书\第十二章\TextQuery.h
 * @Description: 
 * 
 * Copyright (c) 2022 by tonghuaa/LiXiang, All Rights Reserved. 
 */

#include <iostream>
using std::cout;

/* 对文件操作可以使用fstream,fstream分为ofstream和ifstream,
ofstream是向文件里写,ifstream是从文件里读 */
#include <fstream>
using std::ifstream; using std::ofstream;

#include <vector>
using std::vector;

#include <string>
using std::string;

#include <map>
using std::map;

#include <set>
using std::set;

#include <memory>
using std::shared_ptr;
using std::make_shared;

#include <string.h>
#include <sstream>
/*
需求分析:
1 我们需要实现一个TextQuery类,将文件的内容按照一行一行放入vector中,vector每个元素就是这一行的内容。
2 将每一行的内容按照单词分割,构造一个map,单词作为key,行号放入set,set作为value 。这样可以根据单词找到行号的set。
3 根据输入单词查找其出现的次数和行号,将结果作为一个类QueryResult返回。
*/
class TextQuery {
public:
    TextQuery() = default;
    // 将.txt文件中的数据以按行的方式划分,存入到成员变量中
    TextQuery(std::ifstream&);
    void print(std::string& word);
    ~TextQuery() = default;
private:
    // 问题2:这里写成vetcor合理吗?在没有QueryResult类的时候
    //std::vector<std::string> text_; // 存放每行文本内容
    shared_ptr<vector<string>> text_;

    // 问题1:set<int>这样写合理吗?因为一个单词出现的次数一定不会是负数,所以用int并不合理,而vector类定义了size_type一定是正数
    map<string, set<int>> wm_; // key:单词  val:是个set,根据单词找到行号的set
};


// 读取输入文件并建立单词到行号的映射
TextQuery::TextQuery(std::ifstream& infile) : text_{std::make_shared<vector<string>>()} {
    string each_line_of_text;
    // 从文件中读取一行
    while (getline(infile, each_line_of_text)) {
        // test
        //std::cout << "each_line_of_text = " << each_line_of_text << std::endl;
        text_->push_back(each_line_of_text);
        // 记录行数
        int n = text_->size() - 1;
        
        // 继续将行文本行拆分成单词
        std::istringstream line(each_line_of_text);
        string word;
        // 从一行中读取一个单词
        while(line >> word) {
            // 将这个单词和对应的行数添加到成员变量
            wm_[word].insert(n);
        }
    }
}


// 输出结果 根据输入单词查找其出现的次数和行号
void TextQuery::print(string& word) {   
    std::cout << "word occurs:" << wm_[word].size() << std::endl;
    // 打印当前的每一行(遍历每行文本 找到当前单词)
    
    // 这个不对,当输入!的时候,本来!不是单词,但还是打印了带有!的内容,原因是find不能找!
/*     for (auto line : *text_) {
        // 在一行中找到word出现的下标
        if (line.find(word) != std::string::npos) {// 标准库:static const size_type npos = -1;
             std::cout << "line(" << line_num << ")" << line << std::endl;   
        }
        line_num ++;
    } */

   
    for ( auto line_num : wm_[word] ) { // wm_[word] : set 遍历在每行中存在word的行号
        // text_->begin() : 表示第一行文本的迭代器 + line_num : 表示行号(偏移量)   迭代器 + 偏移量 = 第几行
        std::cout << "line(" << line_num + 1 << ")" <<  *(text_->begin() + line_num) << std::endl;
    }

}





// 当我们设计一个类时,在真正实现成员之前先编写使用这个类的程序,是一种非常有用的方法
void runQueries(std::ifstream& infile) {
    // 根据输入文件构建一个TextQuery对象
    TextQuery tq(infile);
    
    // 与用户交互:提示用户输入要查询的单词,完成查询并打印结果
    while (true) {
        std::cout << "enter word to look for, or q to quit" << std::endl;
        //输入单词查询结果,或者输入q退出循环,或者遇到终止退出。
        std::string s;
        
        if ( !(std::cin >> s) || (s == "q"))    break;
        // 打印查询结果
        tq.print(s);
    }
}

#include <fstream>
#include <iostream>

int main() {
    // 1.从文件中读取数据
    std::ifstream file("./config/text.txt");
    // if (file.is_open()) {
    //     std::cout << "open file " << std::endl;
    // }
    
    runQueries(file);

    return 0;
}

拷贝控制和资源管理

一旦一个类需要析构函数,那么它几乎肯定也需要一个拷贝构造函数和一个拷贝赋值运算符。比如对于一个类,有的成员变量是指针,那么这个类就需要在析构函数中手动delete p来释放p所指向的内存,那么这个类就一定需要一个拷贝构造函数和一个拷贝赋值运算符,并且要实现深拷贝!

类值行为和类指针行文

类值行为(深拷贝):副本和原对象是完全独立的,改变副本不会对原对象有任何影响。

#include <string>

class HasPtr
{
private:
    std::string *ps_;
    int i_;
public:
    HasPtr(/* args */) { }
    // 定义一个有参构造,字符串默认值为空
    HasPtr(const std::string& s = std::string()) 
        : ps_(new std::string(s)), i_(0) { }
    HasPtr(const HasPtr& rhs);
    HasPtr& operator=(const HasPtr& rhs);
    ~HasPtr() { delete ps_; }
};


// copy construct
HasPtr::HasPtr(const HasPtr& rhs) { 
    ps_ = new std::string(*rhs.ps_);
    i_ = rhs.i_;    
}
/*
赋值运算符必须处理自我赋值!!!!
对于赋值运算符的两点:
    如果将一个对象赋予它自身,赋值运算符必须能正确工作
    大多数赋值运算符组合了析构函数和拷贝构造的工作
一个好的模式是先将右侧运算符对象拷贝到一个局部临时对象中。当拷贝完成后,销毁左侧运算对象的现有成员就是安全的了。
一旦左侧运算对象的资源被销毁,就只剩下将数据从临时对象拷贝到左侧运算对象的成员中了。
*/
HasPtr& HasPtr::operator=(const HasPtr& rhs) {
    // 可以处理自身赋值的代码 如果rhs和本对象是同一个对象 直接delete ps_会释放*this和rhs指向的string,会访问一个无效内存的指针
    auto newp = new std::string(*rhs.ps_); // 拷贝底层string
    delete ps_; // 释放旧内存
    ps_ = newp; // 拷贝指针数据对象到本对象
    i_  = rhs.i_;
    return *this;
}

赋值运算符必须处理自我赋值!!!!

类指针行为(浅拷贝):共享状态,副本和原对象使用相同的底层数据,改变副本也会改变原对象。

class HasPtr
{
private:
    std::string *ps_;
    int i_;
    std::size_t *use_;
public:
    HasPtr() { }
    // 定义一个有参构造,字符串默认值为空
    HasPtr(const std::string& s = std::string()) 
        : ps_(new std::string(s)), i_(0), use_(new std::size_t(1)) { }
    HasPtr(const HasPtr& rhs);
    HasPtr& operator=(const HasPtr& rhs);
    ~HasPtr();
    std::size_t getUseCount() const { return *use_; }
};

// copy construct
HasPtr::HasPtr(const HasPtr& rhs) { 
    // 如果是自己拷贝自己就跳过  比如HasPtr h1(h2);
    if (&rhs == this)
        return;
    
    ps_ = rhs.ps_;
    i_  = rhs.i_;
    use_= rhs.use_;
    // 引用计数增加
    ++*use_; // 先算*use_,由于rhs和this的use_指向一个东西,所以对*use解引用后++也是对*this.use_++
}

HasPtr& HasPtr::operator=(const HasPtr& rhs) {
    // 赋值运算符必须处理自赋值
    //如果是自赋值则跳过
    if (&rhs == this) {
        return *this;
    }
    // 右侧引用计数增加
    ++*rhs.use_;
    //减少自己原来的引用计数
    //判断引用计数是否为0
    if (--*use_ == 0) {//释放
        delete ps_;
        delete use_;
    }

    ps_ = rhs.ps_;
    use_ = rhs.use_;
    i_ = rhs.i_;
    return *this;
}

HasPtr::~HasPtr() {
    if (--*use_ == 0) {
        delete ps_;
        delete use_;
    }
}

int main()
{ 
	HasPtr h("hi mom!");
	HasPtr h2 = h;  // no new memory is allocated, 
	                // h and h2 share the same underlying string
    HasPtr h3(h2);  // copy construct
    std::cout << h3.getUseCount() << std::endl; // 3
    HasPtr* h4 = new HasPtr(h3); // 会调用copy construct吗?  会,只要传入的是HasPtr类型的参数
    std::cout << h3.getUseCount() << std::endl; // 4
    delete h4;
    std::cout << h3.getUseCount() << std::endl; // 3
} 

动态内存管理类(涉及allocator)

#include <string>
#include <iostream>
#include <memory> // allocate

/* 动态内存的开辟,可以通过new, malloc,以及alloc等方式,本文通过介绍alloc方式,构造一个StrVec类,这个类的功能类似于一个vector,实现字符串的管理,
其中包含push一个字符串,动态扩容,析构,回收内存等操作。 */
class StrVec
{
public:
    // allocaor成员进行默认初始化
    StrVec() : 
        elements{nullptr}, first_free{nullptr}, cap{nullptr} { }
    /* 拷贝控制函数:copy construct,copy assignment,析构 */
    StrVec(const StrVec& rhs);
    StrVec& operator=(const StrVec& rhs);
    ~StrVec();

    void push_back(const std::string& str);
    // 这里为什么指针直接减就可以得到size和capacity
    /*
    两个指针相减的结果的类型为ptrdiff_t,它是一种有符号整数类型。
    减法运算的值为两个指针在内存中的距离(以数组元素的长度为单位,而非字节),
    因为减法运算的结果将除以数组元素类型的长度。
    所以该结果与数组中存储的元素的类型无关。
    */
    std::size_t size() const { return first_free - elements; }
    std::size_t capacity() const { return cap - elements; }
    std::string* begin() const { return elements; }
    std::string* end() const { return first_free; }
private:
    std::string *elements;   // 指向数组首元素的指针
    std::string *first_free; // 指向数组第一个空闲元素的指针
    std::string *cap;        // 指向数组尾后位置的指针
    static std::allocator<std::string> alloc; // 构造string类型allocator静态成员
    // 判断容量不足开辟新空间(调用push_back时需要用)
    void check_n_alloc() {
        if (size() == capacity())
            reallocate();
    }
    // 获得更多内存并拷贝已有元素(一般是重新分配一个两倍大的空间)(难点)
    void reallocate();
    // 释放内存
    void free();

    /* 被拷贝构造、拷贝赋值、析构所使用(难点) */
    std::pair<std::string*, std::string*> alloc_n_copy(const std::string* b, const std::string* e);
};
//静态数据成员需要在类外定义
std::allocator<std::string> StrVec::alloc;

//copy construct
inline
StrVec::StrVec(const StrVec& rhs) {
    // 先复制一份内存到newdata中
    auto newdata = alloc_n_copy(rhs.begin(), rhs.end());
    this->elements   = newdata.first;
    // 拷贝的时候让first_free 和 cap 相等,等对这个拷贝的容器push_back的时候判断check_n_alloc就会reallocate
    this->first_free = this->cap = newdata.second; // 分配的空间恰好容纳给定的元素
}
// assignment
inline
StrVec& StrVec::operator=(const StrVec& rhs) {
    auto data = alloc_n_copy(rhs.begin(), rhs.end());
    free(); // 析构并销毁内存
    this->elements   = data.first;
    this->first_free = this->cap = data.second; 
    return *this;
}

/* 析构函数也可以定义为inline和noexcept! */
inline
StrVec::~StrVec() noexcept {
    free();
}

inline
void StrVec::push_back(const std::string& str) {
    // 在存放str之前,先检查当前空间是否够用,不够用就要reallocate扩容
    check_n_alloc();
    // 当空间够用或者已经扩容后,将这个str放入容器中
    alloc.construct(first_free++, str); // 将str放到first_free指针所指向的内存处,并且first_free后移
}

// 获得更多内存并拷贝已有元素(扩容,难点)
void StrVec::reallocate() {
    // 分配新空间容量大小为原空间的两倍
    auto newcapacity = (size() != 0) ? 2*size() : 1;
    // 分配一块新空间的内存,allocate()返回指向空间的起始地址
    auto newAddress = alloc.allocate(newcapacity);
    // 并将旧空间中的元素拷贝到新空间中 需要在新空间中构造construct
    auto dest = newAddress; // 指向新数据
    auto elem = elements;   // 指向旧数据
    // 
    for (size_t i = 0; i < size(); i++) {
        // 这里会先将*elem的拷贝到dest指向的内存中,并让dest和elem++(指向下一个)
        alloc.construct(dest++, std::move(*elem++));
    }
    free(); // 移动完成后旧数据的空间一定要释放

    // 更新数据的位置
    elements   = newAddress;
    first_free = dest; // 更新第一个空闲位置
    cap        = elements + newcapacity; // elem + 2*size();   
}
// 释放内存 与先分配内存,在构造相反,释放要先destory,再deallocate
inline
void StrVec::free() {
    // elements不能是空指针
    if (elements) {
        // 1.先destory 注意!:逆序销毁(也可以正序) //要先遍历析构每一个对象
        for (auto p = first_free; p != elements; ) {
            alloc.destroy(--p);
        }
        /* // 正序销毁
        auto dest = elements;
        for (int i = 0 ; i < size(); i ++) {
            alloc.destroy(dest++);
        } */
        // 2. 整体回收内存 再deallocate 从elements开始回收cap - elements个类型为string的内存
        alloc.deallocate(elements, cap - elements);
    }
}

/* 比如要把StrVec类中 sv对象的 sv[0]~sv[3]的内容拷贝到一个新的内存中,需要先分配内存,再拷贝内容 */
inline
std::pair<std::string*, std::string*> 
StrVec::alloc_n_copy(const std::string* b, const std::string* e) {
    // 首先要申请内存空间 获取e - b个类型为string的内存空间
    auto newAddress = alloc.allocate(e - b); // data是string*类型,指向空间的起始位置

    // 返回一个pair  
    // uninitialized_copy(b, e, newAddress):
    //    1.把b到e指向的内容拷贝到newAddress指向的开始位置(目标空间),
    //    2.并且uninitialized_copy()的返回值为最后一个构造的元素之后的位置
    return std::make_pair(newAddress, std::uninitialized_copy(b, e, newAddress));
}


int main() {
    using std::cout; using std::endl;
    StrVec sv;
    cout << "sv.size() = " << sv.size() << ", capacity() = " << sv.capacity() << endl; // 0 0
    auto vector_s1 = sv;
    vector_s1.push_back("abc");
    cout << "vector_s1.size() = " << vector_s1.size() << ", capacity() = " << vector_s1.capacity() << endl;
    
    
    vector_s1.push_back("abc");
    auto vector_s2 = vector_s1;
    cout << "vector_s2 : size() = " << vector_s2.size() << ", capacity() = " << vector_s2.capacity() << endl;
    vector_s2.push_back("def");
    cout << "vector_s2.size() = " << vector_s2.size() << ", capacity() = " << vector_s2.capacity() << endl;
    vector_s2.push_back("hhh");
    cout << "vector_s2.size() = " << vector_s2.size() << ", capacity() = " << vector_s2.capacity() << endl;
    return 0;
}