C++系统知识(非常实用)

目录

一、三块核心内容

1.1 进程的虚拟地址空间内存划分和布局

任何的编程语言编译所产生的只有 指令数据

CPU运行程序,需要将.exe/.out文件加载到内存中,但不是直接加载到实际的物理内存中,而是加载到进程的虚拟地址空间。x86 32位Linux环境下,Linux内核会给当前进程分配一块2^32大小的空间,大小为4G。而虚拟地址空间不是物理内存,本质底层就是内核所创建的一系列数据结构。
在这里插入图片描述
在代码中定义的变量分别储存在虚拟空间的那个位置?
在这里插入图片描述
还需注意,每一个进程的用户空间是私有的,而内核空间是共享的。

1.2 函数的调用堆栈详细过程

原理:函数调用堆栈是由操作系统管理的一块内存区域,它用于存储函数调用和返回的过程中的临时数据。在函数调用过程中,程序会将函数的参数和返回地址压入堆栈中,然后跳转到函数的入口地址执行函数代码。在函数返回过程中,程序会从堆栈中弹出返回地址和临时数据,然后跳转到返回地址执行函数调用后的代码。

栈指针(Stack Pointer):指向堆栈顶部的指针。
帧指针(Frame Pointer):指向当前函数的帧的指针。
在x86结构里,负责存放栈底指针的寄存器是ebp,负责存放栈顶指针的寄存器是esp

详细步骤:
1.将形参压入堆栈中。(参数的压入顺序是从右到左)
2.将返回地址压入堆栈中。(返回地址是指函数返回后跳转的地址,它通常是函数调用指令的下一条指令地址)
3.跳转到函数入口地址执行函数代码。(在x86架构中,函数入口地址是通过CALL指令设置的)
4.收回函数的内存空间(恢复堆栈指针的值)
5.弹出返回地址
6.恢复堆栈指针,收回形参空间,跳转到返回地址执行函数调用后的代码。
函数调用过程示意图如下:
示意图

1.3 程序编译链接原理

在这里插入图片描述
1.预处理:处理以“#”开头的预处理指令,如#include,#define 等(#pragma lib/link 等命令则是链接时使用)。预处理器会将头文件包含进来,展开宏定义等,生成一个经过预处理的源代码文件。
2.编译:在这个阶段,编译器将预处理后的源代码翻译成中间代码(通常是汇编代码),这个中间代码仍然是针对特定的硬件平台的抽象代码。
3.汇编:分为两种x86和AT&T;将汇编代码转变成机器可以执行的指令,汇编后生成二进制可重定位的目标文件.o/.obj,可以通过objdump命令来查看.o或.obj文件的相关信息(objdump -t main.o)。
.o文件的格式组成(elf文件头–.text–.data–.bss–.symbal–.section table)
4.链接(.o → a.out / .obj→.exe):
(1)所有.o文件段的合并, 也就是main.o和sum.o的.text、.data等段合并到一起。
(2)符号解析,可以理解为: 所有对符合引用,都要找到该符号定义的地方;也就是链接器寻找main.o文件中UND(调用)符号定义的地方,如果找遍了所有地方都没有找到,那么链接器就会报错:符号未定义!,或者是在多个地方都找到了相同的符号定义,那么也会报错:符号重定义!
(3)符号的重定向,给所有的符号分配虚拟地址。

二、C++学习还必须掌握的-基础知识

2.1 malloc/free,new/delete

malloc/free和new/delete一般情况下的使用:

//    malloc/free
int *p = (int* ) malloc (sizeof (int) ); 
if (p == nullptr) return -1;
*p = 20;
free(p); 

//    new/delete
int *pl = new int (20);
delete p1;

malloc/free和new/delete的区别
new 先开辟内存,再调用构造函数; delete先析构函数,再释放内存。

  1. malloc和free是函数,new和delete是操作符
  2. malloc/free只会开辟,释放空间;而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可。
  4. malloc开辟内存失败,是通过返回值和nullptr做比较;而new开辟内存失败,是通过抛出bad_alloc类型的异常来判断的。

什么是内存泄漏?
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

new和delete能混用吗?
new delete new[ ] delete[ ]
对于普通的编译器内置类型int等(只有内存开辟,无析构),可以进行混用。
自定义的类类型,有析构函数,为了调用正确的析构函数,那么开辟对象数组的时候,会多开辟4个字节,记录对象的个数,故不能混用。

C++为什么区分单个元素和数组的内存分配和释放呢?
数组内存释放会多次调用析构函数。

2.2 引用与指针

指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元,即指针是一个对象
引用:和原来的变量实质上是同一个东西,相当于是原变量的一个别名。可以看作一种更安全的指针

int a=1;int *p=&a;
int a=1;int &b=a;

区别:
1.引用是必须初始化的,指针可以不初始化。
2.引用只有一级引用,没有多级引用;指针可以有一级指针,也可以有多级指针。
3.指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了,从一而终。
4.sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;

联系:
定义一个引用变量,和定义一个指针变量,其汇编指令是一模一样的;通过引用变量修改所引用内存的值,和通过指针解引用修改指针指向的内存的值,其底层指令也是一模一样的。

2.3 C++传递方式

2.3.1 传值(Pass by Value)

void foo(int x) { };
当函数参数是以传值的方式传递时,实际上传递的是参数的副本,而不是参数本身。
优点:由于是操作副本,所以原始数据不会被修改,这在很多情况下是预期的行为。
缺点:如果参数是大型结构或类实例,那么复制整个对象可能会导致性能损耗。

void foo(int x) {  //此处传值
    x = 30; // 只修改了 x 的副本,不会影响到原始变量
}
int main() {
    int a = 10;
    foo(a);   // a 的值仍然是 10 
}

2.3.2 传引用(Pass by Reference)nice

void foo(int& x) { };
传引用意味着传递的是参数的引用(实际上是内存地址),而非其副本。
优点:可以直接修改原始数据,对于大型数据结构或对象,传引用避免了复制的开销,可以提高程序的性能。
缺点:由于可以修改原始数据,如果不小心处理,可能会导致错误或不可预期的结果。

void foo(int& x) {
    x = 30; // 修改了引用的实际对象,即原始变量
}
int main() {
    int a = 10;
    foo(a); // 现在 a 的值变为了 30
}

2.3.3 传常引用(Pass by Constant Reference)

结合了传值和传引用的优点:它通过引用传递参数以提高效率,同时通过使参数为 const,防止函数修改参数值。

void foo(const int& x) {
    // x = 30; // 这行代码会导致编译错误,因为 x 是 const 的
}
int main() {
    int a = 10;
    foo(a);
    // a 的值仍然是 10,foo 函数不能修改 x
}

2.4 Const,Const与指针(引用)的结合

const的语义为只读。修饰的值不能改变;必须在定义时就给它赋予初值,无论是const还是非const变量都可以对其进行初始化;初始化后不能再作为左值。

c和c++中const的区别是什么?
const的编译方式不同。c中const就是当作一个变量来编译生成指令的;
C++中,所有出现const常量名字的地方,都被常量的初始化替换了! ! !

const int a = 20;
int array [a]= { }; // C中会报错,以变量来生成指令。 C++中可以执行
int *p = (int* ) &a;
*p = 30;
printf ("%d %d %d \n",20,*p,20 ) ; // C中输出为 30 30 30,C++中输出为 20 30 20

const修饰的量常出现的错误是:

  1. 常量不能再作为左值 ⬅ 直接修改常量的值
  2. 不能把常量的地址泄露给一个普通的指针或者普通的引用变量 ⬅ 可以间接修改常量的值大

const在C++中的语言规范:

  1. const修饰的是离它最近的类型
  2. const如果右边没有指针*的话, const是不参与类型的
const int *p = &a;  // *p = 20, p = & b
可以任意指向不同的int类型的内存,但是不能通过指针间接修改指向的内存的值。

int *const p = &a;//   p = &b;
常量指针指向的值不能改变,但是这并不是意味着指针本身不能改变,常量指针可以指向其他的地址。

总结const和指针的类型转换公式:
int* ⬅ const in* 是错误的!
!const int⬅int 是可以的!
int⬅const int* 是错误的!
const int
⬅ int* 是错误的!

2.5 函数重载

什么是函数重载?
一组函数,函数名相同,参数列表的个数或者类型不同,那么这一组函数就称作-函数重载。
—组函数要称得上重载,一定先是处在同一个作用域当中的。

函数重载需要注意些什么?
1.C++为什么支持函数重载,c语言不支持函数重载?
C++代码产生函数符号的时候,函数名+参数列表类型组成的! 如:sum_int_int
c代码产生函数符号的时候,函数名来决定! 如:sum
2.添加const不影响函数名,仅仅返回值不同不叫重载。

C++和C语言代码之间如何协调?
1.c调用C++:无法直接调用了!怎么办?
把c++源码括在extern “c”{ … }
2.C++调用c代码:无法直接调用了!怎么办?
把c函数的声明括在extern “c”{ … }

2.6 inline内联函数

内联函数使用: inline关键字 + 函数。

inline内联函数 和 普通函数的区别???
内联函数:在编译过程中,就没有函数的调用开销了,在函数的调用点直接把函数的代码进行展开处理了
Iinline函数不再生成相应的函数符号。
普通函数:有标准的函数调用过程,参数压栈,函数栈帧的开辟和回退过程,有函数调用的开销

注意:
inline只是建议编译器把这个函数处理成内联函数,但是不是所有的inline都会被编译器处理成内联函数。
debug版本上,inline是不起作用的;inline只有在release版本下才能出现

2.7 参数带默认值的函数

1.给默认值的时候,从右向左给。
2.定义处可以给形参默认值,声明也可以给形参默认值。
3,形参给默认值的时候,不管是定义处给,还是声明处给,形参默认值只能出现一次。

三、C++面向对象

3.1 类和对象、this指针

3.1.1 类和对象

类是实体的抽象类型
实体包括属性和行为→ ADT(abstract data type )
对象 ⬅ (实例化) 类 (属性→成员变量行为→成员方法)

ooP语言的四大特征是什么?
抽象、封装/隐藏、继承、多态

类的特点:
在类中,属性一般都是私有变量,通过给外部提供公有的成员方法,来访问私有的属性(如给成员变量提供一个getXXX或者setXXX方法)
类通过实例化来创建对象,每个类可以创建无数个对象,每个对象都有自己的成员变量,但是他们共享成员方法。
类的成员方法一经编译,所有的方法参数,都会加一个this指针,接收调用该方法的对象的地址。

3.1.2 this指针

this指针的主要用途包括:
1.访问对象的成员变量和成员函数。
2.实现方法链(method chaining)。
3.用于区分成员变量和函数参数。
4.返回对象本身的指针。

3.2 构造函数和析构函数

构造函数(初始化) :
1.语法:类名(){ … }
2.构造函数可以有参数,因此可以发生重载。
3.程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次。
析构函数(释放内存):
1.语法:~类名(){ … }
2.析构函数不可以有参数,因此不可以发生重载。
3.程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次

构造函数的初始化列表:可以指定当前对象成员变量(私有变量中创建另一个类的对象,具有被包含关系)的初始化方式
在构造函数后接:进行初始化。

3.3 对象的浅拷贝和深拷贝

浅拷贝:指复制对象时,只复制对象的指针,而不复制对象本身。这意味着新对象和原对象将共享相同的内存地址。如果原对象发生变化,新对象也会受到影响。
深拷贝:指复制对象时,先创建足够的空间,再把蓝本的内容拷贝过去。这意味着新对象和原对象不共享相同的内存地址。如果原对象发生变化,新对象不会受到影响。
编译器默认为浅拷贝,深拷贝需要自己进行编写。
示例(顺序栈-深拷贝)

   SeqStack(const SeqStack& src)  //拷贝构造函数,深拷贝
  {
	 _pstack = new int [src._size];
	 for(int i = 0; i <= src._top; ++i){
	 _pstack[i] = src._pstack[i];
  }
	 _top = src._top;
	 _size = src._size;

3.4类的各种成员方法以及区别

3.4.1 普通的成员方法(编译器会添加一个this形参变量)

属于类的作用域。
调用该方法时,需要依赖一个对象(常对象是无法调用的)。
可以任意访问对象的私有成员,protected继承 public private。

3.4.2 static静态成员方法(不会生成this形参)

属于类的作用域。
用类名作用域来调用方法。
可以任意访问对象的私有成员,仅限于不依赖对象的成员(只能调用其它的static静态成员)。
注意:static成员变量必须在类外定义且初始化,定义时不添加static关键字

class A
{
private:
	//声明
	static int count;
};
//定义
int A::count = 0;

3.4.3 const常成员方法(const 类 *this)

属于类的作用域。
调用依赖一个对象,普通对象或者常对象都可以。
可以任意访问对象的私有成员,但是只能读,而不能写。

3.5 指向类成员(成员变量、成员函数)的指针

3.5.1 指向成员变量的指针

如何新建类指向类成员变量的指针?

// 变量类型 类名::*pointer = &类名::变量名
string Student::*p = &Student::name;

想要具体使用指针,还是要使用类的对象去调用。

Student s1("zhangsan", 100);
Student s2("lisi", 95);
string Student::*p = &Student::name;

cout << s1.*p << endl;   //不同的对象可以调用同一个指针
cout << s2.*p << endl;   

3.5.2 指向类成员函数的指针

如何新建类指向类成员函数的指针?

// 返回值类型 (类名::*p)(函数参数) = &类名:: 成员函数
void (Test::*pfunc)(int) = &Student::func;

想要具体使用指针,还是要使用类的对象去调用。

class Test {
public: void func() { cout << "test" << endl;}
}

Test ti;
void (Test::*pfunc)(int) = &Student::func;
(t1.*pfunc)();

3.6 对象的优化

3.6.1 函数调用过程中对象背后的调用方法

在这里插入图片描述

3.6.2 调用对象该如何优化?

1.丞数参数传递过程中,对象优先按引用传递,不要按值传递
传值会调用拷贝构造函数,将函数调用变量传递到形参,增加函数调用开销。
2.函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象
会少了出函数作用域时构造临时对象和析构的开销。
在这里插入图片描述

3.接收返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收。
编译器会按照初始化方式进行调用,少了对象构造,析构,拷贝构造等开销。
在这里插入图片描述
4.右值引用对对象的优化
左值:有内存,有名字。
右值:没名字(临时量),没内存。同时,一个右值引用变量,本身是一个左值。
优化的方式,将变量指针指向临时对象的地址,同时将临时对象指针置空。少了赋值,析构等开销。如下示例:

//带右值引用参数的拷贝构造
CMystring (CMystring &&str) // str引用的就是一个临时对象
{
	cout << "CMystring (CMyString&& ) " << endl;
	mntr = str.mptr;
	str.mptr = nullptr;
}

右值引用常用函数
move(左值)︰移动语义,得到右值类型。
forward︰类型完美转发,能够识别左值和右值类型。
右值详解:链接: link

四、模板编程-C++类库的编程基础

模板的意义:对类型可以进行参数化。

4.1 函数模板

定义函数模板:

template <typename T>
T add(T a, T b) {
    return a + b;
}

模板的类型参数和非类型参数?
类型形参:出现在模板参数列表中,在class或者typename之后的参数类型名称
非类型形参:模板的一个参数,必须是整数类型(常量、地址),只能使用,不能修改。如下例 value:

template <class T, int value>
T Add(const T& x)
{
    return x + value;
}

模板的实例化:在函数调用点,编译器根据用户指定的类型,从原模板实例化一份函数代码出来(将对应的T替换为指定类型的函数),这份代码叫做模板函数

// 函数调用点
compare<int> (10, 20) ; 
compare (10, 20) ;	    // 函数模板实参推演,根据用户传入的实参类型,来推导出模板类型

故函数模板不进行编译,因为不知道类型;在函数调用点进行模板的实例化;模板函数则是被编译器所编译的。

模板的特例化:普通函数模板无法正确实例化,需要开发者根据实际情况提供的特殊的实例化。又分为完全特例化和部分特例化。

template<template T>
class Vector { }

// 下面这个是对char*类型提供的完全特例化版本
template<>
class Vector<char*> { }

//下面这个是对指针类型提供的部分特例化版本
template<template Ty>
class Vector<Ty*> { }

函数模板、模板特例化、非模板函数(普通函数)之间的关系?
1.非模板函数:如果非模板函数与调用匹配,编译器会优先选择非模板函数。
2.模板特例化:如果没有匹配的非模板函数,但有匹配的模板特例化,编译器会选择模板特例化。
3.函数模板:如果上述两者都没有匹配,编译器会考虑函数模板。

注意:模板代码是不能在一个文件中定义,而在另外一个文件中使用。模板代码调用之前,一定要看到模板定义的地方,这样的话,模板才能够进行正常的实例化,产生能够被编译器编译的代码。所以,模板代码都是放在头文件当中的,然后在源文件当中直接进行#include包含。

4.2 类模板

为什么需要类模板?
类模板与函数模板的定义和使用类似,有时,有两个或多个类,其功能是相同的,仅仅是数据类型不同。

类模板由模板说明和类说明构成
模板说明同函数模板,如下:
template <类型形式参数表>
类声明

template  <typename Type>
class ClassName{ //模板名称+类型参数列表 = 类名称
public:
   //ClassName 的成员函数(构造和析构函数名不用加<T>,其它出现模板的地方都加上类型参数列表)
   bool compare<T>(T a, T b){ .. } 
   
private:
   Type DataMember; 
}

类模板 -> 实例化 -> 模板类
通过对象调用成员函数才会进行相应的实例化,未调用部分不会实例化。

4.3 类模板实现vector容器(包括空间配置器allocator的实现)

空间配置器是什么?为什么要有空间配置器?
空间配置器(Allocator)是一种用来管理内存分配和释放的工具。它提供了一套标准的接口,用于内存分配对象构造对象销毁内存释放。它提供了一种抽象机制,使得容器(如vector, list, map等)可以灵活地使用不同的内存分配策略。通过使用自定义的分配器,开发者可以优化内存使用,提高程序性能,或满足特定的内存管理需求。

主要思路:通过模板类定义Allocator类,空间配置器对象allocator来实现容器底层内存开辟,内存释放,对象构造和析构。vector容器,进行构造,析构,拷贝构造,拷贝赋值,以及一系列push_back等操作。以下为代码部分,相关备注已经写好。
包括容器的定义和空间配置器的定义。

#include <iostream>

using namespace std;

template<typename T>
struct Allocator 
{
    T* allocate(size_t size) {// 负责开辟内存
        return (T*)malloc(sizeof(T) * size);
    }
    void deallocate(void* p) {// 负责内存释放
        free(p);
    }
    void construct(T* p, const T &val) {// 负责对象构造
        new(p) T(val); //定位new,指定的内存上,构造一个值为val的对象
    }
    void destroy(T* p) {// 负责对象析构
        p->~T(); 
    }
};

// 容器底层内存开辟,内存释放,对象构造和析构,都通过allocator空间配置器来实现
template<typename T, typename Alloc = Allocator<T>>
class vector
{
public:
    vector(int size = 10) { // 构造
        // 需要把内存开辟和对象构造分开处理
        _first = _allocator->allocate(size);
        _last = _first;
        _end = _first + size;
    }
    ~vector(){ // 析构
        // 析构容器有效的元素,然后释放_first指针指向的堆内存
        for (T* p = _first; p != _last; p++) { _allocator->destroy(p); } // 把_first指向的数组有效元素进行析构
        _allocator->deallocate(_first);// 释放堆上的数组内存
        _first = _last = _end = nullptr;
    }
    vector(const vector<T>& rhs) { // 拷贝构造
        int size = rhs._end - rhs._first;
        _first = _allocator.allocate(size);
        int len = rhs._last - rhs._first;
        for (int i = 0; i < len; i++) {
            _allocator.construct(_first + i, rhs._first);
        }
        _last = _first + len;
        _end = _first + size;
    }
    vector<T>& operator=(const vector<T>& rhs) { // 拷贝赋值
        if (this = &rhs) return *this;

        for (T* p = _first; p != _last; p++) { _allocator.destroy(p); } // 把_first指向的数组有效元素进行析构
        _allocator.deallocate(_first);// 释放堆上的数组内存

        int size = rhs._end - rhs._first;

        _first = _allocator.allocate(size);
        int len = rhs._last - rhs._first;
        for (int i = 0; i < len; i++) {
            _allocator.construct(_first + i, rhs._first);
        }
        _last = _first + len;
        _end = _first + size;
        return *this;
    }
    void push_back(const T& val) { // 从容器末尾插入一个元素
        if (full()) { expand();}
        _allocator->construct(_last, val);
        _last++;
    }
    void pop_back() { // 删除容器末尾元素
        if (empty()) return;        
        --_last;//不仅要把_last指针--,还需要析构删除的元素
        _allocator->destroy(_last);
    }
    T back()const { // 返回容器末尾元素的值
        return *(_last - 1);
    }
    bool full()const { return _last == _end; }
    bool empty()const { return _first == _last; }
    int size()const { return _last - _first; }


private:
    T *_first;//指向数组起始位置
    T *_last;//指向数组中有效元素的后继位置
    T *_end;//指向数组空间的后继位置
    Alloc* _allocator;//定义容器的空间配置器对象

    void expand() { //容器二倍扩容
        int size = _end - _first;
        //T* ptmp = new T[2 * size];
        T* ptmp = _allocator->allocate(2 * size);
        for (int i = 0; i < size; i++) { _allocator->construct(ptmp + i, _first[i]); }
        for (T* p = _first; p != _last; p++) { _allocator->destroy(p); }
        _allocator->deallocate(_first);
        _first = ptmp;
        _last = _first + size;
        _end = _first + 2 * size;
    }
};

class Test {
public:
    Test() { cout << "Test()" << endl; }
    ~Test() { cout << "~Test()" << endl; }
    Test(const Test&) { cout << "Test(const Test&)" << endl; }
};

int main()
{
    Test t1, t2, t3;
    cout << "111111111111" << endl;
    vector<Test> vec;
    vec.push_back(t1);
    vec.push_back(t2);
    vec.push_back(t3);
    cout << "222222222222" << endl;

    vec.pop_back();
    cout << "333333333333" << endl;


    return 0;
}

五、C++运算符重载-使面向对象编程更加灵活

5.1运算符重载简介

为什么会有运算符重载?
由于无法对用户自定义数据类型进行运算,故需对已有的运算符重新进行定义,以适应不同的数据类型。例如复数,对象等。
重载运算符的基本格式:
返回类型 operator 运算符(参数…)
{
重载函数体;
}
主要有两种实现方式:
成员重载和全局重载
1、成员重载(this绑定到左侧的运算对象)

class Point{
public:
    Point(){};
    Point (int x, int y): x(x),y(y) {};
    Point operator+(const Point &b){ //类内重载,运算符重载函数作为类的成员函数
        Point ret;
        ret.x = this->x + b.x;
        ret.y = this->y + b.y;
        return ret;
    }
    int x,y;
};

2、全局重载 (一般通过友元函数实现)

class Point{
public:
    Point(){};
    Point (int x, int y): x(x),y(y) {};
    friend ostream &operator<<(ostream &out , const Point &a);  //因为 << 是C++提供的类,我们无法访问,只能用友元函数。
private:
    int x,y;
};

//<< 运算符重载的函数实现   ostream是输入输出流的类
ostream &operator<<(ostream &out , const Point &a){
    out << "<Point>( " << a.x << ", " << a.y << ")";
    return out;
}

5.2 迭代器iterator(vector实现)

迭代器(Iterator)是编程中一种用于遍历数据结构(如列表、集合、字典等)元素的对象。迭代器提供了一种统一的方式来访问容器中的元素,而不需要了解底层的实现细节。

为什么是要设计迭代器?
为了统一访问不同容器时的访问方式,STL为每种容器在实现的时候设计了一个内嵌的iterator类,不同的容器有自己专属的迭代器(专属迭代器负责实现对应容器访问元素的具体细节),使用迭代器来访问容器中的数据。

迭代器一般实现成容器的嵌套类型,不同容器的迭代器实现不同。
但一般都包含构造函数,begin()和end()方法,!= ++ 解引用*()运算符重载等。
以下为vector容器的迭代器实现及使用的示例:

class iterator {
public:
    iterator(T* ptr = nullptr): _ptr(ptr){ }
        bool operator!=(const iterator& it)const {return _ptr != it._ptr; }
        void operator++() {_ptr++; }
        T& operator*() {return *_ptr; }// int data = *it; *it = 20
        const T& operator*() const{ return *_ptr; }// int data = *it;
    private:
        T* _ptr;
    };
    iterator begin() { return iterator(_first); }
    iterator end() { return iterator(_end); }
    
void main()
{
    auto it = vec.begin(); // vector<int>::iterator it = vec.begin();
    for (; it != vec.end(); ++it) { cout << *it << " "; }
}

迭代器失效问题?

1.迭代器为什么会失效?
a:当容器调用erase方法后,当前位置到容器末尾元素的所有的迭代器全部失效了
b:当容器调用insert方法后,当前位置到容器末尾元素的所有的迭代器全部失效了
迭代器依然有效 迭代器失效
首元素-→插入点/删除点-→末尾元素
c:insert来说,如果引起容器内存扩容原来容器的所有的迭代器就全部失效了
d:不同容器的迭代器是不能进行比较运算的

2.迭代器失效了以后,问题改怎么解决?
对插入/删除点的迭代器进行更新操作
如:it = vec1.erase(it); 语句会删除迭代器 it 当前指向的元素,并更新迭代器以指向被删除元素的下一个元素。

5.3 对象池应用

通过new和delete运算符重载实现对象池的应用。

对象池解决什么问题?
减少频繁创建和销毁对象带来的成本,实现对象的缓存和复用,适用于创建对象的成本比较大且创建比较频繁的场景。

5.4 智能指针

智能指针的基本原理:首先智能指针在裸指针基础上进行了一次面向对象的封装,其次利用栈上的对象出作用域会自动析构这么一个特点,把资源释放的代码全部放在这个析构函数中执行。

5.4.1 不带引用计数的智能指针

常用std::unique_ptr()(在任何时刻只能有一个指针拥有对象的所有权)
从底层源码可以看到,该智能指针去掉了拷贝构造函数和 operator= 赋值重载函数,禁止用户对unique_ptr进行显示的拷贝构造和赋值,防止智能指针浅拷贝问题的发生;并提供了带右值引用参数的拷贝构造和赋值,从而转移对象的所有权。使用示例如下:

unique_ptr<int> ptr(new int);
unique_ptr<int> ptr2 = std::move(ptr); // 使用了右值引用的拷贝构造
ptr2 = std::move(ptr); // 使用了右值引用的operator=赋值重载函数

5.4.2 带引用计数的智能指针

允许多个指针共享同一个对象的所有权,主要搭配使用强指针和弱指针。

1.强指针 (std::shared_ptr)
std::shared_ptr 是一种智能指针,多个 std::shared_ptr 可以共享同一个对象的所有权。每个 std::shared_ptr 都会增加对象的引用计数,当引用计数变为零时,对象会进行析构。

2.弱指针 (std::weak_ptr) 主要解决交叉引用的问题
weak_ptr不会改变资源的引用计数,只是一个观察者的角色,通过观察shared_ptr来判定资源是否存在。
weak_ptr持有的引用计数,不是资源的引用计数,而是同一个资源的观察者的计数。
weak_ptr没有提供常用的指针操作,无法直接访问资源,需要先通过lock方法提升为shared_ptr强智能指针,才能访问资源。

参考博客(非常详细): link

六、STL相关

6.1 顺序容器vector

vector: 向量容器
底层数据结构:动态开辟的数组,每次以原来空间大小的2倍进行扩容。

vector<int> vec; //定义
增加:
vec.push_back(20); // 末尾添加元素o(1)导致容器扩容
vec.insert(it,20); // it迭代器指向的位置添加一个元素20      o(n)     可能导致容器扩容

删除:
vec.pop_back(); // 末尾删除元素     o(1)
vec.erase (it); // 删除it迭代器指向的元素     o(n)

查询:
// operator[ ]  下标的随机访问vec[5]       o(1)
// iterator 迭代器进行遍历
// find,for_each

/*注意:对容器进行连续插入或者删除操作(insert/erase),
一定要更新迭代器,否则第一次insert或者erase完成,迭代器就失效了*/
// 常用方法介绍:
vec.size ( ) //
vec.empty () //
vec.reserve ( 20) // vector预留空间的  只给容器底层开辟指定大小的内存空间,并不会添加新的元素
resize (20) //容器扩容用的,不仅给容器底层开辟指定大小的内存空间,还会添加新的元素
swap : 两个容器进行元素交换

顺序容器、容器适配器、关联容器。
参考博客 链接: link

6.2迭代器

迭代器是一种方便的工具,用于在容器中遍历元素并执行相关操作

6.2.1 定义和初始化

每种容器都定义了自己的迭代器类型,例如vector:

vector<int>::iterator    iter;    //定义一个名为iter的变量

每种容器都定义了一对名为begin和end的函数,用于返回迭代器。

vector<int>  vec;
// 正向遍历
vector<int>::iterator   iter1=vec.bengin();     //将迭代器iter1初始化为指向ivec容器的第一个元素
vector<int>::iterator   iter2=vec.end();   		 //将迭代器iter2初始化为指向ivec容器的最后一个元素的下一个
// 反向遍历
vector<int>::iterator   iter1=vec.rbengin();     //将迭代器iter1初始化为指向ivec容器的第一个元素
vector<int>::iterator   iter2=vec.rend();   		 //将迭代器iter2初始化为指向ivec容器的最后一个元素的下一个

6.2.2常用操作

*iter              	  //对iter进行解引用,返回迭代器iter指向的元素的引用
iter->men             //对iter进行解引用,获取指定元素中名为men的成员。等效于(*iter).men
iter++                //给iter加1,使其指向容器的下一个元素
iter--                //给iter减1,使其指向容器的前一个元素
iter1 == iter2        //比较两个迭代器是否相等,当它们指向同一个容器的同一个元素或者都指向同同一个容器的超出末端的下一个位置时,它们相等 
// 常用遍历
vector<int>  vec;
for (auto iter = vec.begin(); iter != vec.end(); it++)  //从容器开始循环到结束(auto自动将it设置为对应迭代器类型)
for (auto iter : vec) //从容器开始循环到结束(C14新特性)
// const_iterator类型的迭代器只能用于读不能进行重写
for(vector<int>::const_iterator iter=ivec.begin();iter!=ivec.end();++iter)
     cout<<*iter<<endl;       												//合法,读取容器中元素值
for(vector<int>::const_iterator iter=ivec.begin();iter!=ivec.end();++iter)
    *iter=0;       														    //不合法,不能进行写操作

6.3 函数指针和函数对象

指针函数本质是一个函数,其返回值为指针。
函数指针本质是一个指针,其指向一个函数。
优点:通过将不同函数对函数指针进行赋值,最终统一调用函数指针变量,实现统一接口调用不同函数的目的,简化函数调用过程。
缺点:通过函数指针调用函数,是没有办法内联的,效率很低,因为有函数调用开销。

函数对象(也称为仿函数,functor)是一个行为类似于函数的对象。它是一个重载了operator()的类实例,允许像调用函数一样调用该对象。函数对象的主要优势是它们可以在函数调用时携带状态,并且可以作为模板参数传递给算法。
1.通过函数对象调用operator(),可以省略函数的调用开销,比通过函数指针调用函数(不能够inline内联调用)效率高。
2.因为函数对象是用类生成的,所以可以添加相关的成员变量,用来记录函数对象使用时更多的信息。

6.4 泛型算法

泛型算法= template +迭代器+函数对象
特点一:泛型算法的参数接收的都是迭代器
特点二:泛型算法的参数还可以接收函数对象(c函数指针)
sort, find, find_if,binary_search, for_each

sort (vec.begin(), vec.end(), greater<int>() ); //从大到小排序

绑定器+二元函数对象=》一元函数对象
bindlst:把二元函数对象的operator( ) (a,b)的第一个形参绑定起来
bind2nd:把二元函数对象的operator ( ) (a,b)的第二个形参绑定起来

auto it2 = find_if (vec.begin(), vec.end(), bindlst( greater<int>(), 48));
vec.insert (it2,48 );

6.5 绑定器、function和lambda表达式

C++11从Boost库中引入了bind绑定器和function函数对象机制。
lambda表达式底层依赖函数对象的机制实现。

6.5.1 绑定器(本质还是函数对象)

C++ STL中的绑定器:
将函数对象或函数与部分参数绑定,生成一个新的函数对象,减少函数对象的参数数量,方便函数调用。实质上是将多元函数对象用函数模板方式封装(可以实参推演),底层还是调用多元函数方法。

常见:绑定器 + 二元函数对象 = 一元函数对象
bind1st : operator ()的第一个形参变量绑定成一个确定的值
bind1st + greater → bool operator() (70, const _Ty& _Right)
bind2nd : operator ()的第二个形参变量绑定成一个确定的值

使用(如find): auto it = std::find_if(vec.begin(), vec.end(), std::bind(greater(), 70));

6.5.2 function(函数封装器)

主要是为了解决绑定器,函数对象,lambda表达式只能使用在一条语句中的问题。
template<class _Fty>
class function
: public _Get_function_impl<_Fty> : :type
从function的类模板定义处,看到希望用一个函数类型实例化function

使用: std::function <返回类型(参数列表)> 函数名 = 被封装的函数名;
1.用函数类型实例化function
2.通过function调用operator () 函数的时候,需要根据函数类型传入相应的参数

void hello (){
cout <<"hello world ! " <<endl;
}
int main (){
std::function<void()> func = hello;
func (); // func.operator() => hello1()
return 0;
}

应用场景:如菜单栏通过键来绑定操作来实现选择,而不是switch case,不符合开闭原则。

底层如何实现:
通过模板类,封装一个接受可变参数的函数指针,并允许通过调用运算符 operator() 来调用这个函数。

#include <iostream>

// 定义一个函数对象模板类,支持任意参数列表
template<typename R, typename... A>
class myfunction {
public:
    using PFUNC = R(*)(A...); // 定义函数指针类型

    // 构造函数,接受一个函数指针作为参数
    myfunction(PFUNC pfunc) : _pfunc(pfunc) {}

    // 重载调用运算符,接受任意参数列表
    R operator()(A... args) const {
        return _pfunc(args...); // 调用函数指针
    }

private:
    PFUNC _pfunc; // 存储函数指针
};

// 示例函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 创建一个 myfunction 对象,封装了函数 add
    myfunction<int, int, int> func(add);

    // 使用 myfunction 对象调用 add 函数
    std::cout << "Sum of 3 and 4: " << func(3, 4) << std::endl; // 输出: Sum of 3 and 4: 7

    return 0;
}

6.5.3 lambda表达式

主要解决函数对象需要定义一个类,对于一些简单操作来说,这样的代码可能会降低代码的可读性和可维护性。
C++11 函数对象的升级版 → lambda表达式

Lambda表达式的语法:

auto 对象 = [捕获外部变量] (形参列表) -> 返回值 {操作代码};  //相当于产生了一个函数对象

1.捕获列表[ ]:(相当于构造函数,初始化对象)
[&]:按引用捕获所有变量。
[=]:按值捕获所有变量。
[this]:捕获当前对象的指针。
[x, &y]:按值捕获变量x,按引用捕获变量y。

2.(形参列表): 相当于小括号运算符重载函数,可以接收形参。
3.-> 返回值 相当于小括号运算符重载函数的返回类型,返回类型可以省略,编译器会自动推导。

七、继承与多态

7.1 继承的本质和原理

继承的本质: 代码的复用
继承: a kind of … 一种的关系
用法: class B : public A A 基类/父类 B 派生类/子类
派生类拥有基类的所有内容, 如人是基类,学生是派生类。学生拥有人的所有特性,还具有自己的特性。

继承方式基类的访问限定派生类的访问限定(main)外部的访问限定
publicpublicpublicY
.protectedprotectedN
.private不可见N
protectedpublicprotectedN
.protectedprotectedN
.private不可见N
privatepublicprivateN
.protectedprivateN
.private不可见N

总结:
1.外部只能访问对象public的成员,protected和private的成员无法直接访问
2.在继承结构中,派生类从基类可以继承过来private的成员,但是派生类却无法直接访问
3.protected和private的区别?在基类中定义的成员,想被派生类访问,但是不想被外部访问.那么在基类中,把相关成员定义成protected保护的;如果派生类和外部都不打算访问,那么在基类中,就把相关成员定义成private私有的。

默认的继承方式是什么?
要看派生类是用class定义的,还是struct定义的?
class定义派生类,默认继承方式就是private私有的。struct定义派生类,默认继承方式就是public公有的。

派生类从继承可以继承来所有的成员(变量和方法),以及构造函数和析构函数。
派生类怎么初始化从基类继承来的成员变量呢?
解答:通过调用基类相应的构造函数来初始化
Class Derive : public Base
构造: Derive(int data) :Base(data), Derive成员变量(data)

派生类的构造函数和析构函数,负责初始化和清理派生类部分
派生类从基类继承来的成员,的初始化和清理由谁负责呢?是由基类的构造和析构函数来负责

派生类对象构造和析构的过程是:
1.派生类调用基类的构造函数,初始化从基类继承来的成员。
2.调用派生类自己的构造函数,初始化派生类自己特有的成员。
3.调用派生类的析构函数,释放派生类成员可能占用的外部资源(堆内存,文件)。
4.调用基类的析构函数,释放从基类继承来的成员可能占用的外部资源(堆内存,文件)。

重载、隐藏、覆盖?
1.重载关系
一组函数要重载,必须处在同一个作用域当中;而且函数名字相同,参数列表不同
2.隐藏(作用域的隐藏)的关系
在继承结构当中,派生类的同名成员,把基类的同名成员给隐藏调用了
3.覆盖:
虚函数表中虚函数地址的覆盖

把继承结构,也说成从上(基类)到下(派生类)的结构
在继承结构中进行上下的类型转换,默认只支持从下到上的类型的转换。

7.2 虚函数、静态绑定和动态绑定

静态绑定:静态 (编译时期)的绑定 (函数的调用)
动态绑定:动态 (运行时期)的绑定 (函数的调用)
如果调用函数是发现函数为普通函数,就进行静态绑定。若为虚函数,则进行动态绑定。

虚函数:虚函数是基类中通过关键字 virtual 声明的函数,允许在派生类中被重写。当基类指针或引用指向派生类对象时,可以通过基类指针或引用调用派生类中重写的函数,从而实现动态绑定(Dynamic Binding)。
1.一个类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区。
2.一个类里面定义了函数数,那么这个类定义的对象y基运行时,内存中开始部分,多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable。一个类型定义的n个对象,它们的额vfpt指向的都是同一张虚函数表。
3.一个类里面虚函数的个数,不影响对象内存大小(vfptr),影响的是虚函数表的大小。
4.如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数。

问题一:哪些函数不能实现成虚函数?
虚函数依赖:
1.虚函数能产生地址,存储在vftable当中
2.对象必须存在(vfptr -> vftable ->虚函数地址)
virtual+ 构造函数 NO!构造函数中(调用的任何函数,都是静态绑定的)调用虚函数,也不会发生动态绑定派生类对象构造过。
static静态成员方法 NO!virtual + static 静态成员方法不会产生对象。

问题二:
虚析构函数析构函数调用的时候,对象是存在的! YES!
什么时候把基类的析构函数必须实现成虚函数?
基类的指针(引用)指向堆上new出来的派生类对象的时候, delete pb(基类的指针),它调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用。

问题三:虚函数和动态绑定问题:是不是虚函数的调用一定就是动态绑定?
肯定不是的。在类的构造函数当中,调用虚函数,也是静态绑定,不会发生动态绑定。
如果不是通过指针或者引用变量来调用虚函数,那也是静态绑定。

Base b;
Derive d;
//静态绑定用对象本身调用虚函数,是静态绑定
b.show ( ); //虚函数call Base : : show
d.show ( ); //虚函数call Derive : : show

//动态绑定(虚函数通过指针或者引用变量调用,才发生动态绑定)
Derive *pdl = &d;
pdl->show ( ) ;
Derive &rdl = & ;
rd1.show ( );

7.3 多态

如何解释多态?
静态(编译时期)的多态:函数重载、模板(函数模板和类模板)

bool compare (int, int) { }
bool compare (double, double) { }
compare (10,20); call compare_int_int 			 //在编译阶段就确定好调用的函数版本
compare(10.5,20.5); call compare_douoble_double //在编译阶段就确定好调调用的函数版本

template<typename T>
bool compare(T a, T b) { }
compare (10,20);				//=> int实例化一个compare<int>
compare (10.5,20.5); 		 	//=> double实例化一个compare<double>

动态(运行时期)的多态: Base Derive
在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就会调用哪个派生类对象的同名覆盖方法,称为多态。
pbase->show ();
多态底层是通过动态绑定来实现的,pbase→访问谁的vfptr→继续访问谁的vftable→调用对应的派生类对象的方法。

继承的好处是什么?
1.可以做代码的复用I
⒉.在基类中给所有派生类提供统一的虚函数接口,让派生类进行重写,然后就可以使用多态了。

抽象类和普通类有什么区别? 一般把什么类设计成抽象类(基类)?
//动物的基类 泛指 类-→抽象一个实体的类型
定义Animal的初衷,并不是让Animal抽象某个实体的类型
1.string _name;让所有的动物实体类通过继承Animal直接复用该属性
2.给所有的派生类保留统一的覆盖/重写接口
注意:
拥有纯虚函数的类,叫做抽象类!(Animal)
抽象类不能再实例化对象了,但是可以定义指针和引用变量

C++多重继承-菱形继承问题?
优点:可以做更多的代码复用;
缺点:派生类有多份基类数据。
如何解决缺点?
用虚继承的方式,添加一个虚基类指针表vbptr,将基类移动至继承类数据下方。

八、设计模式

设计模式概念
设计模式简单来说就是在解决某一类问题场景时,有既定的,优秀的代码框架可以直接使用,有以下优点可取:
1.代码更易于维护,代码的可读性,复用性,可移植性,健壮性会更好
2.当软件原有需求有变更或者增加新的需求时,合理的设计模式的应用,能够做到软件设计要求的“开-闭原则”,即对修改关闭,对扩展开放,使软件原有功能修改,新功能扩充非常灵活
3.合理的设计模式的选择,会使软件设计更加模块化,实现高内聚(一个模块或类内部的元素彼此之间的相关性很高,它们共同完成一个单一的功能或任务)和低耦合(指不同模块或类之间的依赖关系尽可能少,每个模块或类的变化对其他模块或类的影响最小)。

8.1 创建型模式(Creational Patterns)

创建型模式关注对象的创建过程,旨在以合适的方式创建对象,以解决对象创建过程中的问题。

8.1.1 单例模式

单例模式是一种常用的软件设计模式,其主要目的是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这种模式在许多应用程序中都有应用,尤其是在需要管理共享资源或控制资源访问的场景中。

单例模式的特点:
唯一性:确保一个类只有一个实例(构造函数私有化),定义该类型唯一的对象。
全局访问:通过一个static静态成员方法返回唯一的对象实例

饿汉式单例模式:就是程序启动时就实例化了该对象,如果运行过程中没有使用到,该实例对象就被浪费掉了。
懒汉式单例模式:就是对象的实例化,延迟到第一次使用它的时候。

如何实现线程安全的单例模式?

static CSingleton* getInstance()
{
	if (nullptr == single)
	{
		// 获取互斥锁
		pthread_mutex_lock(&mutex);
		/* 
		这里需要再添加一个if判断,否则当两个
		线程都进入这里,又会多次new对象,不符合
		单例模式的涉及
		*/
		if(nullptr == single)
		{
			single = new CSingleton();
		}
		// 释放互斥锁
		pthread_mutex_unlock(&mutex);
	}
	
	return single;
}

详细内容可参考博客: link

8.1.2 工厂模式

1.简单工厂simple Factory:
优点:把对象的创建封装在一个接口函数里面,通过传入不同的标识,返回创建的对象客户不用自己负责new对象,不用了解对象创建的详细过程。
缺点:提供创建对象实例的接口函数不闭合,不能对修改关闭。

2.工厂方法Factory Method:
优点:Factory基类,提供了一个纯虚函数(创建产品),定义派生类(具体产品的工厂)负责创建对应的产品,可以做到不同的产品,在不同的工厂里面创建,能够对现有工厂,以及产品的修改关闭。
缺点:实际上,很多产品是有关联关系的,属于一个产品簇,不应该放在不同的工厂里面去创建,这样一是不符合实际的产品对象创建逻辑,二是工厂类太多了,不好维护。

3.抽象工厂Abstract Factory:
优点:把有关联关系的,属于一个产品簇的所有产品创建的接口函数,放在一个抽象工厂里面AbstractFacto,派生类(具体产品的工厂)应该负责创建该产品簇里面所有的产品。
缺点:派生类(具体产品的工厂)必须定义重写基类的虚函数接口。

8.2 结构型模式(Structural Patterns)

结构型模式处理对象的组合,以形成更大的结构。它们关注类和对象的组合,以创建更大的结构。
迭代器模式

8.2.1 适配器模式(Adapter)

通过适配器让不兼容的接口可以在一起工作。

举例:一个只有VGA接口的电脑,一个只有HDMI接口的显示器。
方法1:换一个支持HDMI接口的电脑,这个就叫代码重构。
方法2∶买一个转换头(适配器),能够把vGA信号转成HDMI信号,这个叫添加适配器类

添加接口示例代码:

#include <iostream>
using namespace std;

class VGA // VGA接口类
{
public:
    virtual void play() = 0;
};

class HDMI // HDMI接口类
{
public:
    virtual void play() = 0;
};

class TV: public HDMI // 显示器支持HDMI接口
{
public:
    void play() { cout << "通过HDMI接口连接投影仪,进行视频播放" << endl; }
};

class computer // 电脑类(只支持VGA接口)
{
public:
    void playVideo(VGA* pVGA){
        pVGA->play();
    }
};

//由于电脑(VGA接口)和投影仪(HDMI接口)无法直接相连,所以需要添加适配器类
class VGAToHDMIAdapter : public VGA
{
public:
    VGAToHDMIAdapter(HDMI* pHdmi) : pHdmi(pHdmi) {}
    void play() { pHdmi->play(); }
private:
    HDMI* pHdmi;
};

int main()
{
    computer comp;
    TV tv;
    VGAToHDMIAdapter adapter(&tv);
    comp.playVideo(&adapter);
    return 0;
}

8.2.2 代理模式(Proxy)

通过代理类,来控制实际对象的访问权限。
客户(代理类) 助理(委托类) 老板(抽象类)

代理模式的实现步骤:
1.创建一个抽象类,用虚函数来写老板需要做的事情的接口
2.创建一个委托类(继承自抽象类),实现老板需要做的事情
3.创建不同的代理类(继承自抽象类)(针对不同客户,开放相应权限),具体通过创建私有基类指针,通过组合的方式,动态调用对应的委托类方法来实现。
4.客户直接访问代理来获取权限。

8.2.3 装饰器模式(Decorator)

通过子类实现功能增强的问题:
通过实现子类的方式重写接口,是可以完成功能扩展的,但是代码中有太多的子类添加进来了

装饰器:通过定义装饰类(通过组合的方式)来增加现有类的功能。
1.动态添加功能:可以在运行时动态地给对象添加功能。
2.不修改对象:不需要修改原始对象的代码,符合开闭原则。
3.单一职责:每个装饰器类负责添加一种特定的功能。

装饰器模式的实现步骤:
定义抽象组件:创建一个抽象组件类,定义了所有组件的公共接口。
实现具体组件:创建一个具体组件类,实现抽象组件的接口。
定义抽象装饰者:创建一个抽象装饰者类,持有一个组件对象,并实现与抽象组件一致的接口。
实现具体装饰者(可直接继承抽象组):创建一个或多个具体装饰者类,继承自抽象装饰者,并实现具体的装饰逻辑(公有函数),可通过基类指针来调用。

8.3 行为型模式(Behavioral Patterns)

行为型模式专注于对象间的通信,以解决对象间的交互问题。

8.3.1 观察者模式(Observer)

观察者-监听者模式(发布-订阅模式)设计模式:
主要关注的是对象的一对多的关系,也就是多个对象都依赖一个对象,当该对象的状态发生改变时,其它对象都能够接收到相应的通知。

详细内容可参考博客: link

8.3.2 迭代器模式(Iterator)

九、C++ 11

9.1 C++11 标准相关的内容总结

一:关键字和语法
auto:可以根据右值,推导出右值的类型,然后左边变量的类型也就已知
nullptr:给指针专用(能够和整数进行区别) #define NULL 0
foreach:可以遍历数组,容器等。
for (Type val : container) =>底层就是通过指针或者迭代器来实现的
{
cout<<val<<" ";
}
右值引用: move移动语义函数和forward类型完美转发函数
模板的一个新特性: typename. . . A表示可变参(类型参数)

二:绑定器和函数对象function:函数对象
bind:绑定器bind1st和bind2nd+二元函数对象=》一元函数对象lambda表达式

三:智能指针
shared ptr和weak ptr

四:容器
unordered_set和unordered_map:哈希表0(1)
array:数组vector
forward_ list:前向链表

五:C++语言级别支持的多线程编程
C++语言级别的多线程编程 → 代码可以跨平台windows / linux/ mac
thread /mutex / condition_variable
lock_quard/unique_lock
atomic 原子类型基于cAs操作的原子类型线程安全的sleep_for
C++语言层面thread 本质上还是调用对应api接口
windows → createThread
linux → pthread create

9.2 std::thread线程库、线程互斥操作、线程同步通信操作

9.2.1 std::thread线程库

线程内容:
一.怎么创建启动一个线程?
std: :thread定义一个线程对象,传入线程所需要的线程函数和参数线程自动开启
std::thread 线程对象 (自定义线程函数, 线程函数所需参数);

二.子线程如何结束?
子线程函数运行完成,线程就结束了

三.主线程如何处理子线程
t.join ( ) :等待t线程结束,当前线程继续往下运行
t.detach () :把t线程设置为分离线程,主线程结束,整个进程结束,所有的子线程结束。

示例:

void threadHandle1 (int time)
{
	//让子线程1睡眠time秒
	std: :this_thread: :sleep_for (std: : chrono: : seconds (time)
	cout <<"hello thread1 !" <<endl;
}
void threadHandle2 (int time)
{
	//让子线程2睡眠time秒
	std: :this_thread: :sleep_for (std: : chrono: : seconds (time)
	cout <<"hello thread2 !" <<endl;
}
int main ()
{
	//创建了一个线程对象,传入一个线程函数,新线程就开始运行了
	std::thread t1 (threadHandle1, 2);
	std::thread t2 (threadHandle2, 3);
	//主线程等待子线程结束,主线程继续往下运行
	t1.join ( ) ;
	t2.join ( );
	//把子线程设置为分离线程
	//t1.detach ( );
	cout <<"main thread done ! " <<endl;
}

在这里插入图片描述

9.2.1 线程互斥操作、线程同步通信操作

首先来了解几个概念:
竞态条件(Race Condition):指在多个线程或进程在共享资源(如变量、内存、文件等)上的竞争导致程序行为不确定的情况。由于线程或进程的执行顺序可能不同,每次运行程序时,结果可能会有所不同,这是不可接受的。

互斥锁(Mutex):一种基本的同步机制,用于确保在任何时刻只有一个线程可以访问共享资源。当一个线程获得了互斥锁,其他线程必须等待直到锁被释放。

条件变量(Condition Variable):一种用于线程同步的机制,它允许线程在某个条件不满足时挂起(进入等待状态),并在条件满足时被唤醒。条件变量通常与互斥锁(Mutex)一起使用,以确保线程安全地访问共享资源。

生产者消费者模型:一种经典的多线程并发模型,用于协调生产者线程和消费者线程之间的工作。生产者线程负责生成数据并放入缓冲区,消费者线程则从缓冲区取出数据进行处理。std::mutex 和 std::condition_variable 是常用的同步原语,用于在 C++ 中实现这一模型。

atomic原子类型:通过硬件支持的原子操作和内存屏障来实现线程安全,当一个线程开始对 std::atomic 变量进行操作,其他线程不能在操作完成之前访问或修改该变量。

使用示例:

std::mutex mtx; // 创建互斥锁
std: :condition_variable cv;  // 定义条件变量,做线程间的同步通信操作

// 1. 手动加锁解锁
mtx.lock();
mtx.unlock();

// 2. lock_guard不能用在函数参数传递或者返回过程中,只能用在简单的临界区代码段的互斥操作中
lock_guard<std: :mutex> guard(mtx) ; // 用智能指针封装,保证所有线程都能释放锁。类似scoped_ptr

//3 unique_lock不仅可以使用在简单的临界区代码段的互斥操作中,还能用在函数调用过程中
unique_lock<std: :mutex> lck (mtx) ;
cv.wait (lck); //=>#1.使线程进入等待状态   #2.lck.unlock可以把mtx给释放掉

cv.notify_all(); // 通知在cv上等待的线程,条件成立了,起来干活了!
 // 其它在cv上等待的线程,收到通知,从等待状态→阻塞状态→获取到互斥锁→线程继续执行

//4 CAS 操作比较一个变量的当前值与预期值,这个操作在硬件层面上是原子的。
std: :atomic bool isReady = false;
if( !isReady ) { 操作 }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值