# 指针

# 指针所占内存空间

指针在 32 位系统下所占 4 个字节,64 位操作系统占 8 个字节,不管是什么数据类型。

# 空指针

空指针:指变量指向内存中编号为 0 的空间。

** 用途:** 初始化指针变量

注意:空指针指向的内存是不可以访问的。内存编号 0~255 为系统占用内存,不允许用户访问。

# ** 野指针 **

野指针:指针变量指向非法的内存空间,未经分配的地址,没有访问权限的地址。

# const 修饰指针

常量指针:指针的指向可以修改,但是指针指向的值不可以修改

const int *P=&a;		
*p=20;		// 错误,指向 a 的值不可以被修改
p=&b;		// 正确,指向可以被修改

指针常量:指针的指向不可以改,但是指针指向的值可以修改。

int * const P=&a;		
*p=20;		// 正确,指向的值可以修改
p=&b;		// 错误,指针的指向不可以被修改

const 即修饰指针又修饰常量:指针的指向和指针指向的值都不可以修改

const int * const P=&a;		
*p=20;		// 错误,指向的值不可以修改
p=&b;		// 错误,指针的指向不可以被修改

# 内存分区模型

C++ 程序在执行时,将内存分为 4 个区域

代码区:存放函数体的二进制代码,由操作系统进行管理;

全局区:存放全局变量和静态变量以及常量;

栈区:由编译器自动分配释放,存放函数的参数值,局部变量等;

堆区:由程序员分配和释放,若程序员不释放,程序结束时候由操作系统回收;

# 深拷贝与浅拷贝

浅拷贝:简单的赋值操作

优点:通过拷贝构造函数的实例化的对象的指针数据变量指向的共享的内存空间,因此内存开销较小

缺点:对象的析构的时候,看你会造成重复释放或者造成内存泄漏

深拷贝:在堆区重新申请空间,进行拷贝操作

浅拷贝带来的问题就是堆区的内存重复释放,需要利用深拷贝进行解决。

优点:每一个的对象的指针都有指向的内存空间,而不是共享,所以在对象析构的时候,不存在重复释放或者内存泄漏问题。

缺点:内存开销较大

总结:如果属性有在堆区开辟,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。

# C++11

# 特性一:原始字符串

C++11 新增的另一种类型是原始字符串 raw。原始字符串中的字符表示的就是自己。例如 "\n" 表示的不是换行符,而是两个字符。如果使用原始字符串 R"(\n)" 的话,那么屏幕上面就会显示原始的 \n 两个字符。原始字符串用"(和)" 用作定界符,并使用前缀 R 来标识原始字符串。

cout<<R"(hello\n\t,"Bob".)"<<endl;

上述代码将显示

hello\n\t,"Bob".

在 C++11 之前如果一个字符串分别写到了不同的行里边,需要加连接符,这种方式不仅繁琐,还破坏了表达式的原始含义,如果使用原始字面量就变得简单很多,很强直观,可读性强。

string str = R"(<html>
        <head>
        <title>
        啵啵的黄金屋
        </title>
        </head>
        <body>
        <p>
        啵啵
        </p>
        </body>
        </html>)";
    cout << str << endl;

最后输出内容也将原样显示:

<html>
        <head>
        <title>
        啵啵的黄金屋
        </title>
        </head>
        <body>
        <p>
        啵啵
        </p>
        </body>
        </html>

原始字符串还可以自定义定界符,默认定界符是 "(和)"。因此若想要在字符串中允许)",则必须自定义定界符。如:

cout<<R"+*("(Who is it?)")+*"<<endl;

自定义定界符的方法就是在 "和 (之间添加字符,当然在末尾的定界符应保持一致。以上例子自定义的定界符是"+(,则末尾定界符是)+"。自定义定界符时,在默认定界符之间添加任意数量的基本字符,但空格,左括号,右括号,斜杠和控制字符等除外。

输出内容是:

"(Who is it?)"

# 特性二:指针空值类型 -- nullptr

在 C++ 程序开发中,为了提高程序的健壮性,一般会在定义指针的同时完成初始化操作,或者在指针的指向尚未明确的情况下,都会给指针初始化为 NULL ,避免产生野指针(没有明确指向的指针,操作也这种指针极可能导致程序发生异常)。C++98/03 标准中,将一个指针初始化为空指针的方式有 2 种:

char *ptr = 0;
char *ptr = NULL;

在底层源文件当中 NULL 的定义如下:

#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif

也就是说如果源码是 C++ 程序 NULL 就是 0,如果是 C 程序 NULL 表示 (void*)0 。那么为什么要这样做呢? 是由于 C++ 中, void * 类型无法隐式转换为其他类型的指针,此时使用 0 代替 ((void *)0) ,用于解决空指针的问题。这个 0(0x0000 0000) 表示的就是虚拟地址空间中的 0 地址,这块地址是只读的。

在 c 当中 NULL0 是等价的,出于兼容性的考虑,C11 标准并没有对 NULL 的宏定义做任何修改,而是另其炉灶,引入了一个新的关键字 nullptrnullptr 专用于初始化空类型指针,不同类型的指针变量都可以使用 nullptr 来初始化:

int*    ptr1 = nullptr;
char*   ptr2 = nullptr;
double* ptr3 = nullptr;

在 C++11 标准下,相比 NULL 和 0,使用 nullptr 初始化空指针可以令我们编写的程序更加健壮。

# 特性三:智能指针及 RALL

原理:利用 c++ 中的对象出了其作用域就会被自动析构,因此我们只需要在构造函数的时候申请空间,析构函数的时候释放空间,这就是 RALL

在 C++11 当中 STL 提供了 3 种智能指针,分别是:

std::shared_ptr			// 强指针,共享指针
std::unique_ptr			// 独占指针
std::weak_ptr			// 弱指针

虽然可以引用计数器解决重复释放内存问题,但是如果对其中的某一个类对象中的资源进行修改,那么所有引用该资源的对象全部会被修改。

智能指针雏形

class Cptr{
    
};
class Cmartptr{
public:
    // 一定是一个堆对象
    Cmartptr(Cptr *ptr){
        m+ptr=ptr;
    }
    ~Cmartptr(){
        if(m_ptr!=nullptr){
            delete m_ptr;
        }
    }
private:
    Cptr* m_ptr;
};
int main()
{
    Cmarptr Cm(new Cptr())
}

但是用起来不像是指针,没有指针相关的运算,那么,我们可以增加运算符重载函数。

并且再增加引用计数的技术:

class Cptr{
    
};
class CRefCount
{
  friend class Cmartptr;
  CRefCount(CStudent* pStu)
  {
    m_pObj = pStu;
    m_nCount = 1;
  }
  ~CRefCount()
  {
    delete m_pObj;
    m_pObj = NULL;
  }
  void AddRef()
  {
    m_nCount++;
  }
  void Release()		// 判断是否为 0
  {
    if (--m_nCount == 0)
    {
      delete this;
    }
  }
private:
  CStudent* m_pObj;
  int       m_nCount;
};
class Cmartptr
{
public:
  Cmartptr()
  {
    m_pRef = NULL;
  }
  Cmartptr(Cptr* pStu)
  {
    m_pRef = new CRefCount(pStu);
  }
  ~Cmartptr()
  {
    m_pRef->Release();
  }
    Cmartptr(Cmartptr& obj)
  {
    m_pRef = obj.m_pRef;
    m_pRef->AddRef();
  }
    // 运算符重载
    Cptr* operator->()
  {
    return m_pRef->m_pObj;
  }
  Cptr** operator&()
  {
    return &m_pRef->m_pObj;
  }
  Cptr& operator*()
  {
    return *m_pRef->m_pObj;
  }
  operator Cptr*()
  {
    return m_pRef->m_pObj;
  }
private:
  CRefCount* m_pRef;
}

接下来,添加模板使用,只需把上方的 Cpref 替换成 T 即可;

完整代码如下

// 智能指针:
// 1. 用起来像指针
// 2. 会自己对资源进行释放
class CStudent
{
public:
    CStudent(){}
    void test(){
        cout << "CStudent" << endl;
        m_nSex = 1;
    }
private:
    char* m_pszBuf;
    int   m_nSex;
};
template<typename T>
class CSmartPtr;
template<typename T>
class CRefCount
{
    friend class CSmartPtr<T>;
public:
    CRefCount(T* pStu){
        m_pObj = pStu;
        m_nCount = 1;
    }
    ~CRefCount(){
        delete m_pObj;
        m_pObj = NULL;
    }
    void AddRef(){
        m_nCount++;
    }
    void Release(){
        if (--m_nCount == 0){
            // 这么写就表示自己一定要是一个堆对象
            delete this;
        }
    }
private:
    T* m_pObj;
    int       m_nCount;
};
// 致命问题, CSmartPtr 中表示的类型是固定的,是 CStudent, 需要添加模板
template<typename T>
class CSmartPtr
{
public:
    CSmartPtr()
    {
        m_pRef = NULL;
    }
    CSmartPtr(T* pStu)
    {
        m_pRef = new CRefCount<T>(pStu);
    }
    ~CSmartPtr()
    {
        m_pRef->Release();
    }
    CSmartPtr(CSmartPtr& obj)
    {
        m_pRef = obj.m_pRef;
        m_pRef->AddRef();
    }
    CSmartPtr& operator=(CSmartPtr& obj)
    {
        if (m_pRef == obj.m_pRef){
            return *this;
        }
// 判断堆区是否还有数据,如果有的话,先释放干净再来执行深拷贝操作
        if (m_pRef != NULL)
        {
            m_pRef->Release();
        }
        m_pRef = obj.m_pRef;
        m_pRef->AddRef();
        return *this;
    }
    T* operator->()
    {
        return m_pRef->m_pObj;
    }
    T** operator&()
    {
        return &m_pRef->m_pObj;
    }
    T& operator*()
    {
        return *m_pRef->m_pObj;
    }
    operator T*()
    {
        return m_pRef->m_pObj;
    }
private:
    CRefCount<T>* m_pRef;
};
class CTest{
public:
    CTest(){}
};
int main()
{
    CStudent* pStu = new CStudent();
    CSmartPtr<CStudent> sp1(pStu);
    CSmartPtr<CStudent> sp2(new CStudent()); // 拷贝构造
    //sp2 = sp1; // 运算符重载
    CSmartPtr<CTest> sp3(new CTest);
    return 0;
}

auto_ptr 被废弃的原因

  • 不能有两个 auto_ptr 对象拥有同一个指针的所有权,因为有可能再某个时机,两者均会尝试析构这个内部指针

  • 当两个 auto_ptr 对象之间发生赋值操作的时候,内部指针被拥有的所有权会发生转移,这意味着这个赋值的右者对象会丧失该所有权,不在指向这个内部指针。

shared_ptrweak_ptr

void foo_test()
{
    int* p = new int(3);
    {
        std::shared_ptr<int> sptr(p);
        {
            std::shared_ptr<int> sptr2(p);
        }
    }
}

显然出了最里面的作用域之后,sptr2 对象就已经释放了,此时,对于 sptr2 来说,p 的引用计数为 0,所有 p 被释放,但是实际上 sptr 还存在,所以再释放 sptr 时,就会内存泄漏.

shared_ptr 最大的问题是存在循环引用的问题:

如果两个类的原始指针的循环使用,那么会出现重复释放的问题:

weak_ptr 的使用

  1. weak_ptr 本身并不具有普通内部指针的功能,而只是用来观察其对应的强指针的使用次数。
  2. 因此,这里弱指针的在使用上,实际上是一个特例,即不增加引用计数也能获取对象,因此,实际上在使用弱指针时,不能通过弱指针,直接访问内部指针的数据,而应该是先判断该弱指针所观察的强指针是否存在(调用 expired () 函数),如果存在,那么则使用 lock () 函数来获取一个新的 shared_ptr 来使用对应的内部指针。
  3. 实际上,如果不存在循环引用,就不需要使用 weak_ptr 了,这种做法仍然增加了程序员的负担,所以不如 java c# 等语言垃圾回收机制省心。

shared_ptrweak_ptr 计数器增减的规则

初始化及增加的情形:

  • 当创建一个 shared_ptr 时,内部对象计数器和自身的计数器均置为一;
  • 当将另一个 share_ptr 赋值给新的 share_ptr 时,内部对象计数器 + 1,自身计数器不变;
  • 当将另一个 share_ptr 赋值给新的 weak_ptr 时,内部对象计数器不变,自身计数器 + 1;
  • 当从 weak_ptr 获取一个 share_ptr 时,内部对象计数器 + 1,自身计数器不变;

减少的情形:

  • 当一个 share_ptr 析构时,内部对象计数器 - 1。当内部计数器减为 0 时,则释放对象,并将自身计数器 - 1。
  • 当一个 weak_ptr 析构时,自身计数器 - 1。当自身计数器减为 0 时,则释放本身 _Ref_count* 对象;

# 特性四:自动类型推导

auto 能够对变量进行自动类型推导,但是需要初始化变量

auto x=3.14		//double 类型
auto x='x'		//char 类型

引用规则:

  • 当变量不是指针或者引用类型时,推导的结果中不会保留 const、volatile 关键字
  • 当变量是指针或者引用类型时,推导的结果中会保留 const、volatile 关键字

auto 限制

  1. 不能作为函数的形参,因为只要函数调用时候才会给函数参数传递实参,但是 auto 要求必须给修饰的变量赋值,因此二者矛盾;
  2. 不能用于非静态常量成员变量的初始化,因为中国变量是属于类,只有对象被创建出来才会被初始化
  3. 不能用 auto 定义数组,但是可以指向数组地址
  4. 不能用 auto 推导类模板参数

decltype

decltype 的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的值

int a=10;
decltype(a) b=99;		//b->int
decltype(a+3.14) c=52.12		//向上推导,c->double

如果是右值引用,那么 decltype 会忽略其返回类型的 const、volatile

如果是左值引用,那么 decltype 不会忽略其返回类型的 const、volatile

返回值后置

应用场景:

不知道需要返回什么了,类型的数值时候

代码格式:

auto func(参数1,参数2....)->decltype(参数表达式)

auto 会自动追踪 decltype () 推导出的类型

例如:

template <typename T,typename U>
auto add(T,U)->decltype(t+u)
{
	return t+u;
}
int x=123;
int y=1.1;
auto z=add(x,y)

# 特性五:lambda 表达式

lambda 表达式具有以下优点:

  • 声明式的编程风格:就地匿名定义目标函数或者对象,不需要额外写一个命名函数或者对象;
  • 简介:避免了代码膨胀和功能分散,让开发更加高效。
  • 在需要的时间和地点实现功能闭包,使程序更加灵活
[capture](params) opt->ret {boby;};

其中 capture 是捕获列表, params 是参数列表,opt 是函数选项,ret 是返回值类型, boby 是函数体。

1. 捕获列表 []: 捕获一定范围内的变量

void tset(int x,int y)
{
	int a=x;
	int b=y;
	[](){
		int c;
		int d;
	}
}
  • [] - 不捕捉任何变量
  • [&] - 捕获外部作用域中的所有变量,并作为引用在函数体内使用 (按引用捕获)
  • [=] - 捕获外部作用域中的所有变量,并作为副本在函数体内使用 (** 按值捕获)** 拷贝的副本在匿名函数体内部是只读
  • [=,&foo] - 按值捕获外部作用域中所有变量,并引用捕获外部作用域当中的 foo 变量;
  • [bar] - 捕获外部作用域当中变量 bar,但是不捕获其他变量;
  • [&bar] - 按照引用方式捕获外部作用域当中的 bar 变量,但是不捕获其他变量;
  • [this] - 捕获当前作用域当中的当前类的指针
    • 让 lambda 表达式拥有和当前类成员函数一样的访问权限;
    • 如果已经使用了 & 或者 =,默认添加此选项

2. 参数列表 params

和普通函数参数列表一样,如果没有参数参数列表可以省略不写。

3. opt 选项:

  • mutable :可以修改按值传递进来的拷贝(注意,是能修改拷贝,而不是值本身)
  • exception :指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw() ;

4.ret 返回值类型:

​ 在 C++11 中,lambda 表达式的返回值是通过返回值后置语法来定义的

5. 函数体:

函数的实现。

#

# 特性六:可变参数模版

#include <iostream>
using namespace std;
void test(){
    cout<<"end"<<endl;
}
template<typename T,typename... Args>void test(T t,Args... args){
    cout<<"result:"<<t<<endl;
    test(args...);
}
int main(){
    test(1,2,3,4,5,6);
    return 0;
}

# C++STL

# vector 容器

# 1. 容器和容器初始化及其遍历:

功能:

  • vector 数据结构和数组非常相似,也称为单端数组

vector 与普通数组区别:

  • 不同之处在于数组是静态空间,而 vector 可以动态扩展

动态扩展:

  • 并不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝新空间,释放原空间
  • vector 容器的迭代器是支持随机访问的迭代器
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main()
{
    int b[] = { 1,8,6,2,5,4,8,3,7 };
    vector<int> qwe(b,b+9);
    vector<int> a(b,b+9);
    // 向最后加入数据 10
    a.push_back(10);
    // 访问容器当中元素,通过迭代器访问
    vector<int>::iterator is = a.begin();
    for (; is != a.end(); is++)
    {
        cout << *is << endl;
    }
    // 通过自动类型推导
    auto at = qwe.begin();
    for (; at != qwe.end(); at++)
    {
        cout << *at << endl;
    }
	return 0;
}

# 2. 自定义数据类型使用方法:

#include<iostream>
#include<string>
#include<algorithm>
#include<vector>
using namespace std;
class test
{
public:
    char name;
    int age;
};
int main()
{
    vector<test> qwe;
    test t1,t2;
    t1.age = 18;
    t1.name = 'q';
    t2.age = 20;
    t2.name = 'e';
    qwe.push_back(t1);
    qwe.push_back(t2);
    auto at = qwe.begin();
    for (; at != qwe.end(); at++)
    {
        cout <<(*at).age<<" "<<(*at).name << endl;
    }
	return 0;
}

# 3.Vector 容器嵌套容器

#include<iostream>
#include<string>
#include<algorithm>
#include<vector>
using namespace std;
int main()
{
    char test[] = { 'q','w','e','r' };
    char test1[] = { '1','2','3','4' };
    char test2[] = { '+','-','*','/' };
    vector<vector<char>> q;
    vector<char> v1(test,test+4);
    vector<char> v2(test1,test1+4);
    vector<char> v3(test2,test2+4);
    q.push_back(v1);
    q.push_back(v2);
    q.push_back(v3);
    auto at = q.begin();
    for (; at != q.end(); at++)
    {
        for (auto en = (*at).begin(); en != (*at).end(); en++)
        {
            cout << *en << " ";
        }
    }
	return 0;
}

# 4.vector 容量和大小

函数原型:

  • empty(); // 判断容器是否为空,真是为空

  • capacity(); // 容器的容量

  • size(); // 返回容器中元素的个数

  • resize(int num); // 重新指定容器的长度为 num,若容器变长,则以默认值填充新位置。

    // 如果容器变短,则末尾超出容器长度的元素被删除。

  • resize(int num, elem); // 重新指定容器的长度为 num,若容器变长,则以 elem 值填充新位置。

    // 如果容器变短,则末尾超出容器长度的元素被删除

# 5.vector 插入和删除

  • at(int idx); // 返回索引 idx 所指的数据
  • operator[]; // 返回索引 idx 所指的数据
  • front(); // 返回容器中第一个数据元素
  • back(); // 返回容器中最后一个数据元素
vector<int>v1;
	for (int i = 0; i < 10; i++)
	{
		v1.push_back(i);
	}
	for (int i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}
	cout << endl;
	for (int i = 0; i < v1.size(); i++)
	{
		cout << v1.at(i) << " ";
	}
	cout << endl;
	cout << "v1的第一个元素为: " << v1.front() << endl;
	cout << "v1的最后一个元素为: " << v1.back() << endl;

# 6.vector 互换容器

  • swap(vec); // 将 vec 与本身的元素互换
v1.swap(v2);		//v1和v2进行互换

补充:

互换函数开源进行对容器节省空间

v.resize(3);		先进行收缩空间,但是容量并没有减少
vector<int>(v).swap(v); // 匿名对象,该行执行完,系统自动回收

完整测试代码:

void test02()
{
	vector<int> v;
	for (int i = 0; i < 100000; i++) {
		v.push_back(i);
	}
	cout << "v的容量为:" << v.capacity() << endl;
	cout << "v的大小为:" << v.size() << endl;
	v.resize(3);
	cout << "v的容量为:" << v.capacity() << endl;
	cout << "v的大小为:" << v.size() << endl;
	// 收缩内存
	vector<int>(v).swap(v); // 匿名对象
	cout << "v的容量为:" << v.capacity() << endl;
	cout << "v的大小为:" << v.size() << endl;
}

# 7.vector 预留空间

功能描述:

  • 减少 vector 在动态扩展容量时的扩展次数

函数原型:

  • reserve(int len); // 容器预留 len 个元素长度,预留位置不初始化,元素不可访问。

# String 容器

# 1. 本质:

  • string 是 C++ 风格的字符串,他的本质上是一个类

string 和 char * 区别

  • char * 是一个指针
  • string 是一个类,类内部封装了 char**,管理这个字符串,是一个 char 型的容器

# 2.string 构造函数

  • string(); // 创建一个空的字符串 例如: string str;
    string(const char* s); // 使用字符串 s 初始化
  • string(const string& str); // 使用一个 string 对象初始化另一个 string 对象
  • string(int n, char c); // 使用 n 个字符 c 初始化

示例:

string s1; // 创建空字符串,调用无参构造函数
	cout << "str1 = " << s1 << endl;
	const char* str = "hello world";
	string s2(str); // 把 c_string 转换成了 string
	cout << "str2 = " << s2 << endl;
	string s3(s2); // 调用拷贝构造函数
	cout << "str3 = " << s3 << endl;
	string s4(10, 'a');
	cout << "str3 = " << s3 << endl;

# 3.string 赋值操作

  • string& operator=(const char* s); //char * 类型字符串 赋值给当前的字符串
  • string& operator=(const string &s); // 把字符串 s 赋给当前的字符串
  • string& operator=(char c); // 字符赋值给当前的字符串
  • string& assign(const char *s); // 把字符串 s 赋给当前的字符串
  • string& assign(const char *s, int n); // 把字符串 s 的前 n 个字符赋给当前的字符串
  • string& assign(const string &s); // 把字符串 s 赋给当前字符串
  • string& assign(int n, char c); // 用 n 个字符 c 赋给当前字符串

# 4. 字符串拼接

  • string& operator+=(const char* str); // 重载 += 操作符
  • string& operator+=(const char c); // 重载 += 操作符
  • string& operator+=(const string& str); // 重载 += 操作符
  • string& append(const char *s); // 把字符串 s 连接到当前字符串结尾
  • string& append(const char *s, int n); // 把字符串 s 的前 n 个字符连接到当前字符串结尾
  • string& append(const string &s); // 同 operator+=(const string& str)
  • string& append(const string &s, int pos, int n); // 字符串 s 中从 pos 开始的 n 个字符连接到字符串结尾

# 5.string 查找和替换

  • int find(const string& str, int pos = 0) const; // 查找 str 第一次出现位置,从 pos 开始查找
  • int find(const char* s, int pos = 0) const; // 查找 s 第一次出现位置,从 pos 开始查找
  • int find(const char* s, int pos, int n) const; // 从 pos 位置查找 s 的前 n 个字符第一次位置
  • int find(const char c, int pos = 0) const; // 查找字符 c 第一次出现位置
  • int rfind(const string& str, int pos = npos) const; // 查找 str 最后一次位置,从 pos 开始查找
  • int rfind(const char* s, int pos = npos) const; // 查找 s 最后一次出现位置,从 pos 开始查找
  • int rfind(const char* s, int pos, int n) const; // 从 pos 查找 s 的前 n 个字符最后一次位置
  • int rfind(const char c, int pos = 0) const; // 查找字符 c 最后一次出现位置
  • string& replace(int pos, int n, const string& str); // 替换从 pos 开始 n 个字符为字符串 str
  • string& replace(int pos, int n,const char* s); // 替换从 pos 开始的 n 个字符为字符串 s

# 6. 字符串比较

比较方式:

字符串比较是按照字符的 ASCII 码进行比较的

相等 返回 0;

大于 返回 1

小于 返回 - 1

  • int compare(const string &s) const; // 与字符串 s 比较
  • int compare(const char *s) const; // 与字符串 s 比较

# 7.string 插入和删除

  • string& insert(int pos, const char* s); // 插入字符串

  • string& insert(int pos, const string& str); // 插入字符串

  • string& insert(int pos, int n, char c); // 在指定位置插入 n 个字符 c

  • string& erase(int pos, int n = npos); // 删除从 Pos 开始的 n 个字符

  • string str = "hello";
    //插入
    	str.insert(1, "111");
    	cout << str << endl;
    
    //删除
    	str.erase(1, 3);  //从1号位置开始3个字符
    	cout << str << endl;
    

# 8.string 子串

  • string substr(int pos = 0, int n = npos) const; // 返回由 pos 开始的 n 个字符组成的字符串

# deque 容器

功能:

  • 双端数组,可以对头端进行插入删除操作

deque 与 vector 区别:

  • vector 对于头部的插入删除效率低,数据量越大,效率越低
  • deque 相对而言,对头部的插入删除速度回比 vector 快
  • vector 访问元素时的速度会比 deque 快,这和两者内部实现有关

deque 内部工作原理:

deque 内部有个中控器,维护每段缓冲区中的内容,缓冲区中存放真实数据

中控器维护的是每个缓冲区的地址,使得使用 deque 时像一片连续的内存空间

  • deque 容器的迭代器也是支持随机访问的

# 1.deque 构造函数

  • deque<T> deqT; // 默认构造形式
  • deque(beg, end); // 构造函数将 [beg, end) 区间中的元素拷贝给本身。
  • deque(n, elem); // 构造函数将 n 个 elem 拷贝给本身。
  • deque(const deque &deq); // 拷贝构造函数

# 2.deque 赋值操作

  • deque& operator=(const deque &deq); // 重载等号操作符
  • assign(beg, end); // 将 [beg, end) 区间中的数据拷贝赋值给本身。
  • assign(n, elem); // 将 n 个 elem 拷贝赋值给本身。

示例代码:

deque<int> d1;
	for (int i = 0; i < 10; i++)
	{
		d1.push_back(i);		// 向最后一个位置插入元素
	}
	deque<int>d2;
	d2 = d1;		// 等号赋值
	
	deque<int>d3;
	d3.assign(d1.begin(), d1.end());
	deque<int>d4;
	d4.assign(10, 100);

# 3.deque 大小操作

  • deque.empty(); // 判断容器是否为空,返回真为空

  • deque.size(); // 返回容器中元素的个数

  • deque.resize(num); // 重新指定容器的长度为 num, 若容器变长,则以默认值填充新位置。

    // 如果容器变短,则末尾超出容器长度的元素被删除。

  • deque.resize(num, elem); // 重新指定容器的长度为 num, 若容器变长,则以 elem 值填充新位置。

    // 如果容器变短,则末尾超出容器长度的元素被删除。

# 4.deque 插入和删除

两端插入操作:

  • push_back(elem); // 在容器尾部添加一个数据
  • push_front(elem); // 在容器头部插入一个数据
  • pop_back(); // 删除容器最后一个数据
  • pop_front(); // 删除容器第一个数据

指定位置操作:

  • insert(pos,elem); // 在 pos 位置插入一个 elem 元素的拷贝,返回新数据的位置。
  • insert(pos,n,elem); // 在 pos 位置插入 n 个 elem 数据,无返回值。
  • insert(pos,beg,end); // 在 pos 位置插入 [beg,end) 区间的数据,无返回值。
  • clear(); // 清空容器的所有数据
  • erase(beg,end); // 删除 [beg,end) 区间的数据,返回下一个数据的位置。
  • erase(pos); // 删除 pos 位置的数据,返回下一个数据的位置。

提供的位置是迭代器位置

# 5.deque 排序

  • sort(iterator beg, iterator end) // 对 beg 和 end 区间内元素进行排序

# stack 容器

功能描述:栈容器常用的对外接口

构造函数:

  • stack<T> stk; //stack 采用模板类实现, stack 对象的默认构造形式
  • stack(const stack &stk); // 拷贝构造函数

赋值操作:

  • stack& operator=(const stack &stk); // 重载等号操作符

数据存取:

  • push(elem); // 向栈顶添加元素
  • pop(); // 从栈顶移除第一个元素
  • top(); // 返回栈顶元素

大小操作:

  • empty(); // 判断堆栈是否为空
  • size(); // 返回栈的大小

# queue 容器

** 概念:*Queue 是一种 * 先进先出 (First In First Out,FIFO) 的数据结构,它有两个出口

队列容器允许从一端新增元素,从另一端移除元素

队列中只有队头和队尾才可以被外界使用,因此队列不允许有遍历行为

队列中进数据称为 — 入队 push

队列中出数据称为 — 出队 pop

# 3.6.2 queue 常用接口

功能描述:栈容器常用的对外接口

构造函数:

  • queue<T> que; //queue 采用模板类实现,queue 对象的默认构造形式
  • queue(const queue &que); // 拷贝构造函数

赋值操作:

  • queue& operator=(const queue &que); // 重载等号操作符

数据存取:

  • push(elem); // 往队尾添加元素
  • pop(); // 从队头移除第一个元素
  • back(); // 返回最后一个元素
  • front(); // 返回第一个元素

大小操作:

  • empty(); // 判断堆栈是否为空
  • size(); // 返回栈的大小

# C++ 服务器开发

# C++ 多线程

# 第一个多线程

多线程的缺点

  • 死锁
  • 乱序
  • 并发访问数据造成问题
  • 低效率

创建一个多线程程序

#include <iostream>
#include <thread>
// 创建一个子线程函数
void hello()
{
    std::cout<<"heelo wold"<<std::endl;
}
int main()
{
    std::thread t(hello);   // 开启一个线程
    std::cout<<"hello"<<std::endl;
    t.join();   // 线程结束
    return 0;
}

查看该设备使用多少线程效率最高

#include <iostream>
#include <thread>
using namespace std;
void hello()
{
}
int main()
{
    unsigned int n=thread::hardware_concurrency(); // 跑多少个线程时候效果最高
    cout <<n<<endl;
    return 0;
}

仅仅作为参考

# detach()

传统多线程程序中,主线程要等待子线程执行完毕,然后自己才能向下执行

detach: 分离,主线程不再与子线程汇合,不再等待子线程 detach 后,子线程和主线程失去关联,驻留在后台,由 C++ 运行时库接管 t.detach (), 称为守护进程

detach 子线程传递参数注意事项

原因:主线程退出释放资源,但子线程还没有进行获取资源

若用 int 等简单类型传递参数,建议使用值传递,不要用引用和指针类型

如果传递类对象,避免隐式转换,在创建线程时就构建临时对象,函数参数使用引用进行接收,如果不用引用的话,将会构造三次临时对象

测试代码

#include <iostream>
#include <thread>
using namespace std;
class A {
public:
	mutable int m_i; //m_i 即使实在 const 中也可以被修改
	A(int i) :m_i(i) {}
};
void myPrint(const A& pmybuf)
{
	pmybuf.m_i = 199;
	cout << "子线程myPrint的参数地址是" << &pmybuf << "thread = " << std::this_thread::get_id() << endl;
}
int main()
{
	A myObj(10);
	//myPrint (const A& pmybuf) 中引用不能去掉,如果去掉会多创建一个对象
	//const 也不能去掉,去掉会出错
	// 即使是传递的 const 引用,但在子线程中还是会调用拷贝构造函数构造一个新的对象,
	// 所以在子线程中修改 m_i 的值不会影响到主线程
	// 如果希望子线程中修改 m_i 的值影响到主线程,可以用 thread myThread (myPrint, std::ref (myObj));
	// 这样 const 就是真的引用了,myPrint 定义中的 const 就可以去掉了,类 A 定义中的 mutable 也可以去掉了
	thread myThread(myPrint, myObj);
	myThread.join();
	//myThread.detach();
	cout << "Hello World!" << endl;
}

# 其他创建线程方法

1. 类方法

class Ta
{
public:
	void operator()() // 不能带参数
	{
		cout << "我的线程开始运行" << endl;
		//-------------
		//-------------
		cout << "我的线程运行完毕" << endl;
	}
};
int main(){
	Ta ta;
	thread myThread(ta);
	myThread.join();
	return 0;
}

2. 匿名函数

int main(){
auto lambdaThread = [] {
		cout << "我的线程开始执行了" << endl;
		//-------------
		//-------------
		cout << "我的线程开始执行了" << endl;
	};
	thread myThread(lambdaThread);
	myThread.join();
	return 0;
}

# 多线程共享数据

互斥量

lock() , unlock()

使用样例:

std::mutex my_lock;
void test(){
......
my_lock.lock();
/........
共享数据部分
...../
my_lock.unlock();
}

为了避免忘记使用 lockunlock , 可以使用 std::lock_guard<std::mutex> 进行智能管理释放

使用样例:

std::mutex my_lock;
void test(){
	......
   std::lock_guard<std::mutex> tmp(my_lock)//tmp 变量名
  /........
  共享数据部分
  ...../
}

std::lock(参数一,参数二。。。)

可以一次锁住多个互斥量

std::unique_lock<std::mutex>

用法:

当本线程所需的互斥量正被另一个线程所使用,并且需要较长时间才能获取到时候,本线程就会一直处于等待状态浪费资源,使用

std::unique_lock<std::mutex> 的话可以在未拿到所时候进去处理其他任务

示例:

std::unique_lock<std::mutex> tmp(my_lock,std::try_to_lock);
if(tmp.owns_lock()){
// 拿到了锁,进行处理
}
else{
// 未拿到锁处理代码
}

# 多线程函数

# std::call_once

保障多线程函数,在无论什么情况下只会被调用一次

#include <iostream>
#include <thread>
#include <mutex>
std::once_flag flag1;
void test(){
    std::cout<<"调用"<<std::endl;
}
void simple_do_once()
{
    std::call_once(flag1, test);
}
int main()
{
    std::thread st1(simple_do_once);
    std::thread st2(simple_do_once);
    st1.join();
    st2.join();
}

# Std::condition_variable

std::condition_variable 实际上是一个类,是一个和条件相关的类,在多线程当中,某一个线程需要等待另一个线程的条件,就会进行不断的判断,而该类即可解决这方面问题。

【wait()】

该成员函数用来等一个成立条件

如果第二个参数的 lambda 表达式返回值是 false,那么 wait () 将解锁互斥量,并阻塞到本行
如果第二个参数的 lambda 表达式返回值是 true,那么 wait () 直接返回并继续执行。

阻塞到其他某个线程调用 notify_one() 成员函数为止;

如果没有第二个参数,那么效果跟第二个参数 lambda 表达式返回 false 效果一样

wait () 将解锁互斥量,并阻塞到本行,阻塞到其他某个线程调用 notify_one () 成员函数为止。

当其他线程用 notify_one () 将本线程 wait () 唤醒后,这个 wait 恢复后

1、wait () 不断尝试获取互斥量锁,如果获取不到那么流程就卡在 wait () 这里等待获取,如果获取到了,那么 wait () 就继续执行,获取到了锁

2.1、如果 wait 有第二个参数就判断这个 lambda 表达式。

  • 如果表达式为 false,那 wait 又对互斥量解锁,然后又休眠,等待再次被 notify_one () 唤醒
  • b) 如果 lambda 表达式为 true,则 wait 返回,流程可以继续执行(此时互斥量已被锁住)。

tips:当 wait () 被 notify_one () 激活时,会先执行它的 条件判断表达式 是否为 true,如果为 true 才会继续往下执行

#include <thread>
#include <iostream>
#include <list>
#include <mutex>
using namespace std;
 
class A {
public:
    void inMsgRecvQueue() {
        for (int i = 0; i < 100000; ++i) 
        {
            cout << "inMsgRecvQueue插入一个元素" << i << endl;
            std::unique_lock<std::mutex> sbguard1(mymutex1);
            msgRecvQueue.push_back(i); 
            // 尝试把 wait () 线程唤醒,执行完这行,
            // 那么 outMsgRecvQueue () 里的 wait 就会被唤醒
            // 只有当另外一个线程正在执行 wait () 时 notify_one () 才会起效,否则没有作用
            condition.notify_one();
        }
	}
 
	void outMsgRecvQueue() {
        int command = 0;
        while (true) {
            std::unique_lock<std::mutex> sbguard2(mymutex1);
            //wait () 用来等一个东西
            // 如果第二个参数的 lambda 表达式返回值是 false,那么 wait () 将解锁互斥量,并阻塞到本行
            // 阻塞到什么时候为止呢?阻塞到其他某个线程调用 notify_one () 成员函数为止;
            // 当 wait () 被 notify_one () 激活时,会先执行它的 条件判断表达式 是否为 true,
            // 如果为 true 才会继续往下执行
            condition.wait(sbguard2, [this] {
                if (!msgRecvQueue.empty())
                    return true;
                return false;});
            command = msgRecvQueue.front();
            msgRecvQueue.pop_front();
            // 因为 unique_lock 的灵活性,我们可以随时 unlock,以免锁住太长时间
            sbguard2.unlock(); 
            cout << "outMsgRecvQueue()执行,取出第一个元素" << endl;
        }
	}
 
private:
	std::list<int> msgRecvQueue;
	std::mutex mymutex1;
	std::condition_variable condition;
};
 
int main() {
	A myobja;
	std::thread myoutobj(&A::outMsgRecvQueue, &myobja);
	std::thread myinobj(&A::inMsgRecvQueue, &myobja);
	myinobj.join();
	myoutobj.join();
}

notify_one ():通知一个线程的 wait ()

notify_all ():通知所有线程的 wait ()

# Std::async

std::async 是一个函数模板,用来启动一个异步任务,启动起来一个异步任务之后,它返回一个 std::future<template> 对象,这个对象是个类模板

【get()】

std::future 对象的 get() 成员函数会等待线程执行结束并返回结果,拿不到结果它就会一直等待,感觉有点像 join() 。但是,它是可以获取结果的。

并且只能调用一次,因为 get () 函数的设计是一个移动语义,相当于将 result 中的值移动到了 a 中,再次 get 就报告了异常。

  • ** std::shared_futur ** 也是一个类模板

  • std::futureget() 成员函数是转移数据

    std::shared_futureget() 成员函数是复制数据,用法与 std::future 一样

【wait()】

std::future 对象的 wait() 成员函数,用于等待线程返回,本身并不返回结果,这个效果和 std::threadjoin() 更像。

第一个参数:

如果是 std::launch::async ,那么就是声明的时候就回去创建新的线程
系统默认使用的就是这个参数

如果是 std::launch::deferred 那么线程的执行就会推迟到 std::future.get () 方法时才会启动 如果不使用 get 或者 wait 时,线程直接结束,不会执行线程入口函数内的东西 如果使用的话,也会发现,根本没有创建新的线程,就是再主线程中调用的线程入口函数

使用如下:

class A
{
  test(int n){
    ......
  }
};
std::future<int> result = std::async(std::launch::async,&A::test ,n);

# std::future 的成员函数

1、 std::future_status status = result.wait_for(std::chrono::seconds(几秒));
卡住当前流程,等待 std::async() 的异步任务运行一段时间,然后返回其状态 std::future_status 。如果 std::async() 的参数是 std::launch::deferred (延迟执行),则不会卡住主流程。
std::future_status 是枚举类型,表示异步任务的执行状态。类型的取值有
std::future_status::timeout 线程超时
std::future_status::ready 线程未超时
std::future_status::deferred 线程延迟执行

用法样例:

int mythread() {
	cout << "mythread() start" << "threadid = " << std::this_thread::get_id() << endl;
	std::chrono::milliseconds dura(5000);// 停顿五秒
	std::this_thread::sleep_for(dura);
	cout << "mythread() end" << "threadid = " << std::this_thread::get_id() << endl;
	return 5;
}
int main() {
	cout << "main" << "threadid = " << std::this_thread::get_id() << endl;
	std::future<int> result = std::async(mythread);
	cout << "continue........" << endl;
	//cout << result1.get () << endl; // 卡在这里等待 mythread () 执行完毕,拿到结果
	// 等待 1 秒
    std::future_status status = result.wait_for(std::chrono::seconds(1));
	if (status == std::future_status::timeout) {
		// 子线程需要 5 秒时间运行,但是等待时间只有 11 秒,所以超时
		cout << "超时了,线程还没有执行完" << endl;
	}
	// 类成员函数
	return 0;
}

# std::async 和 std::thread () 区别:

  • std::thread () 如果系统资源紧张可能出现创建线程失败的情况,如果创建线程失败那么程序就可能崩溃,而且不容易拿到函数返回值(不是拿不到)
  • std::async () 创建异步任务。可能创建线程也可能不创建线程,并且容易拿到线程入口函数的返回值;
  • async 不确定性问题的解决
    不加额外参数的 async 调用时让系统自行决定,是否创建新线程。

# std::atomic

原子操作:在多线程中不会被打断的程序执行片段。

从效率上来说,原子操作要比互斥量的方式效率要高。

互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量。

原子操作,一般都是指 “不可分割的操作”;也就是说这种操作状态要么是完成的,要么是没完成的,不可能出现半完成状态。

std::atomic 来代表原子操作,是个类模板。其实 std::atomic 是用来封装某个类型的值的

需要添加 #include <atomic> 头文件

用法示例

#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
std::atomic<int> g_count = 0; // 封装了一个类型为 int 的 对象(值)
void mythread1() {
	for (int i = 0; i < 1000000; i++) {
		g_count++;
	}
}
 
int main() {
	std::thread t1(mythread1);
	std::thread t2(mythread1);
	t1.join();
	t2.join();
	cout << "正常情况下结果应该是200 0000次,实际是" << g_count << endl;
}

# 函数作为参数被调用

#include<iostream>
#include <map>
using namespace std;
class T{
public:
    int a=9;
};
void add(T t){
    cout<<"test:"<<t.a<<endl;
}
map<string,function<void(T)>> myMap;
void test(string s,function<void(T)> p){
    myMap[s]=p;
}
int main()
{
    T qw;
    test("te",add);
    myMap["te"](qw);
    return 0;
}

# c++ 线程池

# 线程池原理:

线程:

线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是 CPU 调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由 CPU 独立调度执行,在多 CPU 环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

一个正在运行的软件就是一个进程,一个进程可以同时运行多个任务,可以简单的认为进程是线程的集合。

线程是一条可以执行的路径。

对于单核 CPU 而言:多线程就是一个 CPU 在来回的切换,在交替执行。
对于多核 CPU 而言:多线程就是同时有多条执行路径在同时 (并行) 执行,每个核执行一个线程,多个核就有可能是一块同时执行的

线程带来了如此多的便利同时,当一个服务器面对高并发的请求时候,那么就会不断的创建线程,销毁线程,造成了不必要的性能浪费,那么有没有一种方法可以不用这样重复创建销毁呢

那就是引入这篇利用线程池来进行管理线程的创建和销毁来重复利用已经创建好的线程进行任务以避免频繁创建销毁的开销

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果任务队列为空则进入阻塞状态等待唤醒;如果所有线程池线程都始终保持繁忙,超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

线程池主要分为三个部分:

  • 工作线程
    • 里面维护这一定数量的线程,不停的对任务队列进行读取,判断是否有任务
    • 如果任务队列为空则进入阻塞,等待唤醒
    • 如果任务队列有任务,那么则唤醒一个等待中的线程进行依次处理任务
  • 任务队列
    • 创建一个队列,对任务依次压入,并唤醒一个等待中的线程
    • 取出任务时候将该任务弹出
  • 结束线程
    • 在线程的析构函数当中进行结束线程
    • 唤醒所有等待当中的线程,并进行 join 结束

任务队列代码:

#include <queue>
#include <thread>
template<typename T>
class ThreadQueue{
private:
    std::queue<T> safeQueue;    // 线程队列
    std::mutex queueLock;   // 访问队列锁
public:
    // 判断队列是否为空
    bool empty(){
    std::unique_lock<std::mutex> lock(queueLock);   // 加锁,防止队列被改变
    return safeQueue.empty();
}
    // 添加队列元素
    void add(T &t){
        std::unique_lock<std::mutex> lock(queueLock);
        safeQueue.template emplace(t);
    }
    // 取出元素
    bool getQueue(T &t){
        std::unique_lock<std::mutex> lock(queueLock);
        if(safeQueue.empty())
            return false;
        t=std::move(safeQueue.front());
        safeQueue.pop();
        return true;
    }
};

线程工作代码:

#include "ThreadQueue.h"
#include <future>
class ThreadPool{
private:
    class ThreadWork{
    private:
        ThreadPool *in_pool;
    public:
        ThreadWork(ThreadPool *pool):in_pool(pool){
        }
        ThreadWork operator()(){
            std::function<void()> func;
            bool que;   // 是否正在取出元素
            while (!in_pool->shut_thread){
                {
                    std::unique_lock<std::mutex> lock(in_pool->mutually_thread);
                    // 如果任务为空就阻塞
                    if(in_pool->task_queue.empty()){
                        // 等待唤醒
                        in_pool->notice_thread.wait(lock);
                    }
                    que=in_pool->task_queue.getQueue(func);
                    if(que){
                        func();
                    }
                }
            }
        }
    };
    bool shut_thread;   // 判断线程池是否关闭
    std::vector<std::thread> thread_list;       // 线程工作列表
    std::mutex mutually_thread;    // 互斥锁
    std::condition_variable notice_thread;  // 通知唤醒线程
    ThreadQueue<std::function<void()>> task_queue;      // 任务队列
public:
    ThreadPool(const int n_thread=4){
        thread_list=std::vector<std::thread>(n_thread);
        shut_thread= false;
    }
    // 删除默认拷贝构造函数
    ThreadPool(const ThreadPool&)=delete;
    ThreadPool(const ThreadPool&&)=delete;
    ThreadPool &operator=(const ThreadPool&)=delete;
    ThreadPool &operator=(const ThreadPool&&)=delete;
    // 初始化线程池
    void init(){
        for(int i=0;i<thread_list.size();i++){
            // 分配线程
            thread_list.at(i)=std::thread(ThreadWork(this));
        }
    }
    // 结束关闭线程池
    ~ThreadPool(){
        shut_thread= true;
        // 唤醒所有线程池
        notice_thread.notify_all();
        for(int i=0;i<thread_list.size();i++){
            // 判断线程是否在等待
            if(thread_list.at(i).joinable()){
                thread_list.at(i).join();
            }
        }
    }
    template <typename F, typename... Args>
    auto submit(F &&f, Args &&...args) -> std::future<decltype(f(args...))>
    {
        std::function<decltype(f(args...))()> func = std::bind(std::forward<F>(f), std::forward<Args>(args)...); // 连接函数和参数定义,特殊函数类型,避免左右值错误
        auto task_ptr = std::make_shared<std::packaged_task<decltype(f(args...))()>>(func);
        std::function<void()> warpper_func = [task_ptr]()
        {
            (*task_ptr)();
        };
        // 队列通用安全封包函数,并压入安全队列
        task_queue.add(warpper_func);
        // 唤醒一个等待中的线程
        notice_thread.notify_one();
        // 返回先前注册的任务指针
        return task_ptr->get_future();
    }
};

测试代码:

void test(){
    std::thread::id tid=std::this_thread::get_id();
    std::cout<<"test id:"<<tid<<std::endl;
}
void test1(){
    std::thread::id tid=std::this_thread::get_id();
    std::cout<<"test1 id:"<<tid<<std::endl;
}
void test2(){
    std::thread::id tid=std::this_thread::get_id();
    std::cout<<"test2 id:"<<tid<<std::endl;
}
int main()
{    // 创建 3 个线程的线程池
    ThreadPool pool(8);
    // 初始化线程池
    pool.init();
    std::thread::id tid=std::this_thread::get_id();
    std::cout<<"主线id"<<tid<<std::endl;
    pool.submit(test);
    pool.submit(test1);
    pool.submit(test2);
    return 0;
}
阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Charmber 微信支付

微信支付

Charmber 支付宝

支付宝

Charmber 贝宝

贝宝