C面向过程,C++面向对象
后缀名不同
一些关键字有区别:
返回值
缺省参数
函数重载
·static const int a可以在类内进行初始化,而static int a不可以
this
指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用 this
指针。this
指针被隐含地声明为: ClassName *const this
,这意味着不能给 this
指针赋值;在 ClassName
类的 const
成员函数中,this
指针的类型为:const ClassName* const
,这说明不能对 this
指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);this
并不是一个常规变量,而是个右值,所以不能取得 this
的地址(不能 &this
)。list
。inline 使用
// 声明1(加 inline,建议使用)
inline int functionName(int first, int second,...);
// 声明2(不加 inline)
int functionName(int first, int second,...);
// 定义
inline int functionName(int first, int second,...) {/****/};
// 类内定义,隐式内联
class A {
int doA() { return 0; } // 隐式内联
}
// 类外定义,需要显式内联
class A {
int doA();
}
inline int A::doA() { return 0; } // 需要显式内联
优点
缺点
总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
enum class open_modes{input, output, append};
enum color {red, yellow, green};
enum {floatPrec=6, doublePrec=10;
volatile int i = 10;
volatile
关键字是一种类型修饰符,用它声明的类型变量可以被某些编译器未知的因素改变(操作系统,硬件等)。所以使用该关键字告诉编译器不应对这样的对象进行优化volatile
关键字声明的变量,每次访问都必须从内存中取值(没有被volatile
修饰的变量,可能由于被编译器优化,从CPU寄存器中取值)const
(编译期保证代码中没有被修改的地方,运行的时候不受限制)可以是volatile
(编译期告诉编译器不优化该变量,在运行期每次都从内存中取值):表示一个变量在程序编译期不能被修改并且不能被优化,在程序运行期变量值可能会被修改,每次使用到该变量值都需要从内存中读取,防止意外错误volatile
的explicit
修饰构造函数时,可以防止隐式转换或者复制初始化explicit
修饰转换函数时,可以防止隐式转换,但按语境转换除外struct A{
A(int){}
operator bool() const {return true;}
};
struct B{
explicit B(int){}
explicit operator bool() const {return true;}
};
int main{
B b1(1); //OK,直接初始化
B b2 = 1; //Error,被explict修饰的构造函数不能复制初始化
}
using
声明语句一次只引入命名空间的一个成员,这使得我们可以清楚知道程序所引用的到底是哪个命名空间里的函数,比如using namespace_name::name;
using
声明:在C++11中,可以直接重用其直接基类定义的构造函数,比如class Derived:Base{
public:
using Base:Base;
};
Derived(params):Base(args){}
using
指示使得某个特定命名空间中所有名字都可见,这样就无需再为他们都加上任何前缀了,比如using namespace std;
应当尽量少使用
using
指示污染命名空间,尽量多使用using
声明
::name
):用于类型名称(类,类成员,成员函数,变量等)前,表示作用域为全局命名空间class::name
):用于表示指定类型的作用域范围是具体某个类的namespace::name
):用于表示指定类型的作用域范围是具体某个命名空间的左值和右值的概念
它实现了转移语义和精确传递。它的主要目的有两个方面:
总结一个类型T
T&
, 只能绑定左值T&&
, 只能绑定右值const T&
, 既可以绑定左值又可以绑定右值编译器有返回值优化
,但不要过于依赖访问一个已销毁或者访问受限的内存区域的指针
产生的原因
断言,是宏,而非函数。assert 宏的原型定义在
(C)、
(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG
来关闭 assert,但是需要在源代码的开头,include
之前。
assert() 使用
#define NDEBUG // 加上这行,则 assert 不可用
#include
assert( p != NULL ); // assert 不可用
使用到了三个常用的寄存器,EIP
为指令指针,即指向下一条即将执行的指令的地址;EBP
为基址地址,常用来指向栈底;ESP
为栈顶指针,常用来指向栈顶。
EIP
指针入栈EDP
入栈,因为每个函数都有自己的栈区域,所以栈基址不是一样的,现在需要进入新函数,为了不覆盖调用函数的EDP
,将它压入栈中。ESP
作为EBP
,即将此时的栈顶地址作为该函数的栈基址EBX,ESI,EDI
压入栈中,他们分别为基址寄存器,源变址寄存器,目的变址寄存器EDI,ESI,EBX
的值EDP
的值赋给ESP
,弹出EBP
的值struct
,union
以及类成员变量以n字节方式对齐,也就是让数据在内存中连续存储,同时以n字节对齐#pragram pack(push) //保持对齐状态
#pragram pack(4) //设定为4字节对齐
struct test{
char m1;
double m4;
int m3;
};
#pragram pack(pop) //恢复对齐状态
=dufault:将拷贝控制成员定义为=default显式要求编译器生成合成的版本
=delete:将拷贝构造函数和拷贝赋值运算符定义为删除的函数,阻止拷贝
=0:将虚函数定义为纯虚函数
直接初始化直接调用于实参匹配的构造函数
复制初始化总是调用复制构造函数,复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。
而初始化是要创建一个新的对象,并且其初值来自于另一个已存在的对象。
赋值操作是在两个已经存在的对象间进行的。
const
成员变量。初始化const
成员变量的唯一方法就是使用初始化列表std::initializer_list
参数C++中异常事件发生时,程序使用throw关键字抛出异常表达式,抛出点称为异常出现点,有操作系统为程序设置当前异常对象,然后执行程序的当前异常处理代码块,在包含了异常出现点的最内层的try块,一次匹配catch语句中的异常对象(只进行类型匹配,catch的参数有时候在catch语句中并不会使用到)。
若异常匹配成功,则执行catch块内的异常处理语句,然后执行try…catch之后的代码。如果当前的try…catch找不到匹配该异常对象的catch语句,则有更外层的try…catch块来处理该异常;如果当前函数内所有的try…catch块都不能匹配该异常,则递归回退到调用栈的上一层处理该异常。
如果一直回退到主函数都无法处理,则调用系统函数terminate()终止程序。
// 一些异常类型
exception 最常用的异常类,只报告异常的发生而不报告任何额外的信息
runtime_error 只有在运行的时候才能检测该异常
overflow_error 运行时错误:计算上溢
underflow_error 运行时错误:计算下溢
logic_error 逻辑错误
invalid_argument 逻辑错误:参数无效
out_of_range 逻辑错误:使用一个超出有效范围的值
bad_alloc 内存动态分配错误
try{
语句
}
catch(异常类型){
异常处理代码
}
catch(异常类型){
异常处理代码
}
vector num{1,2,3}
[捕获区](参数区){代码区};
比如sort(arr.begin(),arr.end(),[](int a, int b){return a > b;});
[a,&b]
:其中a以复制捕获,b以引用捕获[this]
:以引用捕获当前对象(*this)[&]
:以引用捕获所有用于lambda体内的自动变量,并以引用捕获当前对象,如果存在[=]
:以复制捕获[]
:不捕获,大多数情况下适用move
语义:
std::move
:将左值变成一个右值,它是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。std::forward()
函数可以对左值引用,右值引用,常左值引用,常右值引用进行完美转发。Standard Template Library,标准模板库。
主要由六大组件组成:
construct()
接收一个指针和初值value,该函数的用途是将初值设定到指针所指的空间上。C++的placement new
运算符可用来完成。
destory()
有两个版本,第一个版本接收一个指针,准备将该指针所指之物释放;第二版本接受first和last两个迭代器,将之间的对象析构掉。
对象构造前的空间配置和对象析构后的空间释放,由
负责:
C++内存配置由::operator new()
和::operator delete()
完成
为了考虑到内存碎片问题,SGI设计了双层级配置器,第一层配置器(__malloc_alloc_template)直接使用malloc()
和free()
;第二级配置器(__default_alloc_template)视情况使用不同的策略:
chunk_alloc()
函数以end_free-start_free
来判断内存池的大小,如果大小充足,就直接调用20个区块返回给free list,如果不足就提供一个以上的区块,这时候引用传递的nobjs参数会被修改为实际能够调用的区块数。如果一个也无法提供,就利用malloc()函数从heap中配置内存,增加内存池大小。新大小为需求量的两倍,再加上随着配置次数增加而越来越大的附加量。
一种方法,能够依序巡访某个容器所含的各个元素,而又无需暴露该容器的内部表述方式。
迭代器是一种行为类似指针的对象,而指针的各种行为中最常见也最重要的便是解引用(dereference)和成员访问(member access)。
但是迭代器的关联类型并不只是”迭代器所指对象的类型“一种而已,最常用的关联类型有5种。
也被称为特性萃取技术,就是指提取”被传进的对象“对应的返回类型,让同一个接口实现对应的功能。
在STL种,算法和容器是分离的,两者通过迭代器连接,算法在实现时并不知道传进来的是什么,traits技法相当于在接口和实现之间加了一层封装,来隐藏一些细节并协助调用合适的方法,这需要一些技巧(比如偏特化)。
根据经验,最常用的迭代器关联类型有5种:
// 内嵌类型
template<class T>
struct iterator_traits{
// 由于C++语言默认情况下,假定通过作用域运算符访问的名字不是类型,所以想要访问的是类型的时候
// 必须使用typename告诉编译器这是一个类型
typedef typename T::iterator_category iterator_category;
typedef typename T::value_type value_type;
typedef typename T::difference_type difference_type;
typedef typename T::pointer pointer;
typedef typename T::reference reference;
};
// T*特化版
template<class T>
struct iterator_traits<T*>{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef T& refernce;
};
// const T*特化版
template<class T>
struct iterator_traits<const T*>{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef ptrdiff_t difference_type;
tyepdef const T* pointer;
typedef const T& reference;
};
通过iterator_traits
的封装,则对任意类型(不论是迭代器,还是原生指针int*或const int*),都可以通过它获取正确的关联类型信息。
value_type
:指迭代器所指对象的类型difference_type
:用来表示两个迭代器之间的距离reference_type
:指引用类型const T&
pointer_type
:指针类型T*
iterator_category
:迭代器根据移动特性和操作,可以分为五类
Input Iterator
:这种迭代器所指的对象,不允许改变,只读Output Iterator
:只写Forward Iterator
:允许”写入型“算法(例如replace())在此种迭代器所形成的区间上进行读写操作Bidirectional Iterator
:可双向移动,某些算法需要逆向走访某个迭代器区间(例如逆向拷贝某范围内的元素)Random Access Iterator
:前四种迭代器都只提供一部分指针算术能力(前三支持opertor++
,第四还支持operatr--
),第五种则涵盖所有指针算术能力(p+n,p-n,p[n],p1-p2,p1相比于数组的静态空间,Vector是动态空间,随着元素的加入,内部机制会自动扩充空间以容纳新元素。
vector支持随机存取,提供的是Random_access_iterators
,也就是vector的迭代器是普通指针。
如果原大小为0,则配置1(各元素大小);
如果原大小不为1,则配置原大小的两倍(前半段用来放原数据,后半段用来放新数据);(开辟一块新的空间而不是在原来空间的后面添加)
将原来vector的内容拷贝到新的vector中;再新的vector插入新元素
析构原来的vector,并调整迭代器指向新的vector。(一旦引起空间重新配置,所有指向原来vector的迭代器就都失效了)
插入时首先检查备用空间大小是否大于新增元素个数m
在一个vector的尾部之外的任何位置添加元素,都需要重新移动元素。同时可能会引起整个对象存储空间的重新分配。
频繁调用push_back()可能会引起对象的重新分配,并将旧的空间元素移动到新的空间,这十分耗费时间。
任何位置的元素插入和删除的时间复杂度都是常数时间。
// 底层实现为双向链表
template <class T>
struct __list_node{
typedef void* void_pointer;
void_pointer prev;
void_pointer next;
T data;
};
由于底层实现为双向链表,迭代器必须具有前后移动的能力,所以list提供的迭代器为bidirectional_iterator
双向迭代器,并且插入和结合操作都不会导致原有的list迭代器的失效,删除操作时只会导致”指向被删除的那个元素“的迭代器失效。
在SGI STL中,list的实现不仅是双向链表,同时还是一个环状双向链表,所以只需要一个指针就可以完整的遍历整个链表。此时只需要在最后一个节点的后面加上一个空白节点,就可以实现STL要求的**”前闭后开“**区间的要求。
List的排序算法不能使用STL的算法sort(),必须使用自己的sort()成员函数,因为STL的sort()函数只接受RandomAccessIterator
单向链表,任何位置的插入和删除的时间复杂度都是O(1),不支持随机访问。迭代器为forward_iterator
。
元素插入使用push_front(),所以其元素次序于插入次序相反。
vector是一个单向开口的连续线性空间,deque是一个双向开口的连续线性空间(双端队列),即可以在头尾两端分别进行插入和删除操作。
相对vector的区别:
虽然deque也提供Random Access Iterator
,但是他的迭代器并不是普通的指针,这影响了他的计算效率。
对deque进行排序操作时,为了最高效率,可以将它完整的复制到一个vector中,排序之后在复制回deque。
deque由一段一段的定量连续空间组成,一旦有必要在deque的头部或者尾部添加新空间,便配置一定量的连续空间,串接在整个deque的头部或者尾部。同时使用主控维护其整体连续的假象。
deque采用一块所谓的map
来作为主控,其中每个元素都是一个指针,指向另一端连续线性空间,称为缓冲区。缓冲区才是deque的存储空间主体。
T* cur; // 指向所指缓冲区中的当前元素
T* first; // 指向所指缓冲区中的头
T* last; // 指向所指缓冲区的尾(含备用空间)
map_pointer node; // 指向map中指向该缓冲区的节点
deque::begin() //返回迭代器start,指向deque中第一个元素
deque::end() //返回迭代器end,指向deque中最后一个元素
当进行push_back()操作时,当插入的元素正好使缓冲区满时,进行扩容操作,新开辟一个缓冲区,并将end指向新开辟的缓冲区
进行push_front()操作时,首先使用start.cur != start.first
判断第一缓冲区是否还有备用空间,如果有就直接在备用空间上构造元素;如果没有就配置一个新的缓冲区,并将start指向新开辟的缓冲区。
在扩容过程中,如果map的尾部的节点备用空间不足,就配置一个更大的,拷贝原来的过去,然后释放;如果头部节点空间不足,也是如此。
是一种先进后出(FILO)的数据结构,只有一个出口,只能在其顶端操作O(1),不允许遍历,stack没有迭代器。
其底层使用deque或者list实现。因为其以底层容器完成其所有工作,而具有这种”修改某物接口,形成另一种风貌“的性质的结构称为适配器。
是一种先进先出(FIFO)的数据结构,其有两个出口,允许在头尾两端进行操作(尾部插入,头部取出)O(1),不允许遍历,没有迭代器。
底层使用deque或者list实现。
是一种有序的queue,允许在头尾两端进行操作(尾部插入,头部取出)O(logN),不允许遍历,没有迭代器。
底层使用vector(max-heap或min-heap)实现。
STL的关联式容器主要有set(集合)和map(映射)两大类,以及他们的衍生体multiset和multimap,底层均以RB-tree(红黑树)实现。RB-tree也是一个独立容器,但是不开放给外界使用。
关联式容器即每个元素都有一个键(key)和一个值(value)。当元素被插入到关联式容器中时,容器内部结构(RB-tree)便依据键值大小,将其放置在合适的位置。关联式容器没有头部和尾部的概念。
所有元素都会根据元素的键值自动被排序,set元素的键值就是value实值。
set不允许存在两个相同的元素。
底层使用RB-tree实现,存在constant bidirectional iterators
,即不允许通过迭代器对集合内元素进行修改。
与list类似,当对元素进行增删操作时,除了被删除的那个迭代器之外,其他迭代器全部有效。
所有元素都会按照元素的键值自动排序。map的所有元素都是pair,同时拥有键值(key)和实值(value)。
pair的第一个元素被视为键值,第二个元素为实值。map不允许存在两个相同的键值。
底层使用RB-tree实现,存在constant bidirectional iterators
,即不允许通过迭代器对集合内元素进行修改。
与list类似,当对元素进行增删操作时,除了被删除的那个迭代器之外,其他迭代器全部有效。
与set的唯一差别就是允许键值重复,底层的插入操作使用的是RB-tree的insert_equal()
而不是insert_unique()
迭代器为constant bidirectional iterators
与map的唯一差别就是允许键值重复,底层的插入操作使用的是RB-tree的insert_equal()
而不是insert_unique()
迭代器为constant bidirectional iterators
底层使用hash_table实现,其中哈希表里的元素节点被称为桶(bucket),桶内维护的是链表,但不是STL中的链表,而是自行维护的一个node。bucket聚合体也即hash_table使用vector实现。
template <class Value>
struct __hashtable_node{
__hashtable_node* next;
Value val;
};
所有无序关联容器的迭代器为forward iterators
是含有键值类型唯一对象集合的关联容器。搜索、插入和删除都拥有平均O(1)时间复杂度。
在内部,元素并不以任何特别顺序排序,而是组织进桶中。元素被放进哪个桶完全依赖其值的哈希。这允许对单独元素的快速访问,因为哈希一旦确定,就准确指代元素被放入的桶。
不可修改容器元素(即使通过非 const 迭代器),因为修改可能更改元素的哈希,并破坏容器。
一种关联容器,含有带唯一键的键值对pair。搜索、插入和删除都拥有平均常数时间复杂度。
元素在内部不以任何特定顺序排序,而是组织进桶中。元素放进哪个桶完全依赖于其键的哈希。这允许对单独元素的快速访问,因为一旦计算哈希,则它准确指代元素所放进的桶。
是关联容器,含有可能非唯一 Key 类型对象的集合。搜索、插入和删除拥有平均常数时间复杂度。
元素在内部并不以任何顺序排序,只是被组织到桶中。元素被放入哪个桶完全依赖其值的哈希。这允许快速访问单独的元素,因为一旦计算哈希,它就指代放置该元素的准确的桶。
插入操作使用insert_equal(),set使用的是insert_unique()
一种关联容器,含有可能非唯一键的键值对pair。搜索、插入和删除都拥有平均常数时间复杂度。
元素在内部不以任何特定顺序排序,而是组织到桶中。元素被放进哪个桶完全依赖于其关键的哈希。这允许到单独元素的快速访问,因为哈希一旦计算,则它指代元素被放进的准确的桶。
以有限的步骤解决逻辑或者数学上的问题,称为算法。Algorithm。
STL算法的一般形式:所有泛型算法的前两个参数都是一对迭代器(iterators),通常称为first和last,用于标示算法的操作区间。
STL中习惯采用前闭后开区间表示法,写成[first,last),表示区间涵盖first至last(不含last)之间的所有元素。当last==last时,表示一个空区间。
排序算法接受两个RandomAccessIterators
(随机存取迭代器),然后将区间内的所有元素以递增方式由小到大重新排列;也可以接受一个仿函数作为排序标准。
可以对vector,deque进行排序,因为他们的迭代器都属于RandomAccessIterators
。对list进行排序需要使用他们自己的sort()成员函数。
STL中的sort()
算法,数据量大时使用快速排序,分段递归排序。数据量小于某个门槛(16)时,为了避免快排的递归调用带来过大的额外负荷,使用插排。如果递归层次过深,还会使用堆排。
以双层循环的形式进行。外循环遍历整个序列,每次迭代决定出一个子区间;内循环遍历子区间,将子区间内的每一个“逆转对”倒转过来。
当数据量很少时效果很好,原因是因为STL中实现了一些技巧,同时不需要进行递归调用等操作,会减少额外负担。
void __insertion_sort(iterator first, iterator last){
if(first == last) return;
for(iterator i = first + 1; i != last; i++) // 外循环
__linear_insert(first, i, value_type(first)); // [first, i)形成一个子区间
}
// 辅助函数
void __linear_insert(iterator first, iterator last, T*){
T value = *last; // 记录尾元素
if(value < *first){ // 当尾元素比头部小时
copy_backward(first, last, last+1); // 就一次性全部向后移动一位(****)
*first = value;
}
else // 尾元素大于头部
__unguarded_linear_insert(last, value);
}
// 辅助函数
void __unguarded_linear_insert(iterator last, T value){
iterator next = last;
--next;
// 内循环
// 注意,一旦不再出现逆转对,循环就结束,因为之后的都必定不是逆转对了,也就是都是排好序的了
while(value < *next){
*last = *next;
last = next;
--next;
}
*last = value;
}
快速排序是目前已知最快的排序法,平均复杂度尾O(NlogN)
,最坏情况为O(N2)
。不过内省排序(IntroSort
)(一种与median-of-three QuickSort
极为类似的算法)可以将最坏情况推进到O(NlogN)
。
快速排序步骤为:
pivot(v)
分段的原则通常采用median-of-three
,即取(首、尾,中央三值的中位数)
T& __median(const T& a, const T& b, const T& c){
if(a < b)
if(b < c)
return b;
else if(a < c)
return c;
else
return a;
else if(a < c)
return a;
else if(b < c)
return c;
else
return b;
}
分割时使用两个迭代器,first向尾部移动,last向头部移动,当*first大于或者等于基准时就停下来,*last小于或者等于基准时也停下来,然后检验两个迭代器是否交错,如果first仍然在左,last仍然在右,就将两者交换,然后各自调整一个位置(向中间逼近),再继续进行相同的行为。如果发现两个迭代器交错了,表示已经调整完毕。
iterator __unguarded_partition(iterator first, iterator last, T pivot){
while(true){
while(*first < value) ++first; // first找到>=pivot就停下来
--last;
while(pivot < *last) --last;
if(first>=last) return first; // 交错,结束
iter_swap(first, last); // 大小值互换
++first;
}
}
不当的基准值选择,可能会导致不当的分割,导致快排时间复杂度恶化为O(N2)
。内省排序的行为再大多数情况下和median-of-three QuickSort
一致,但是当分割行为有恶化为二次的倾向时,能够自我检测转而使用堆排。
void __introsort_loop(iterator first, iterator last, T*, size depth_limit){
while(last-first > 16){
if(depth_limit == 0){ // 超过深度限制就进行堆排
partial_sort(first, last, last);
return;
}
--depth_limit; // 分了一层了
// 使用median-of-three quicksort进行分割
iterator cut = __unguarded_partition(first, last, T(__median(*first, *(first+(last-first)/2), *(last-1))));
// 对右半段递归进行sort
__introsort_loop(cut, last, value_type(first), depth_limit);
last = cut;
// 开始新的循环对做半段进行sort
}
}
void sort(iterator first, iterator last){
if(first != last){
__introsort_loop(first, last, value_type(first), __lg(last-first)*2); // lg函数为找到2^k <= n的最大值k,控制层数
__final_insertion_sort(first, last); // 经过上一步之后,每个子序列都有相当程度的排序,但尚未完成排序,这里进行最后的处理
}
}
void __final_insertion_sort(iterator first, iterator last){
if(last - first > __stl_threshold){
__insertion_sort(first, first+__stl_threshold);
__unguarded_insertion_sort(first+__stl_threshold, last); // 里面调用__unguarded_linear_insert进行排序
}
else
__insertion_sort(first, last);
}
包含在#include 头文件中,使用方法shared_ptr
实现
sing namespace std;
template <typename T>
class Shared_ptr
{
private:
size_t* m_count;
T* m_ptr;
public:
//构造函数
Shared_ptr() : m_ptr(nullptr), m_count(new size_t)
{}
Shared_ptr( T* ptr ) : m_ptr(ptr), m_count(new size_t)
{
cout<<"空间申请:"<<ptr<<endl;
*m_count = 1;
}
//析构函数
~Shared_ptr()
{
--(*m_count);
if(*m_count == 0)
{
cout<<"空间释放:"<<m_ptr<<endl;
delete m_ptr;
delete m_count;
m_ptr = nullptr;
m_count = nullptr;
}
}
//拷贝构造函数
Shared_ptr( const Shared_ptr& ptr )
{
m_count = ptr.m_count;
m_ptr = ptr.m_ptr;
++(*m_count);
}
//拷贝赋值运算符
void operator=( const Shared_ptr& ptr )
{
Shared_ptr(std::move(ptr));
}
//移动构造函数
Shared_ptr( Shared_ptr&& ptr ) : m_ptr(ptr.m_ptr), m_count(ptr.m_count)
{
++(*m_count);
}
//移动赋值运算符
void operator=( Shared_ptr&& ptr )
{
Shared_ptr(std::move(ptr));
}
//解引用运算符
T& operator*()
{
return *m_ptr;
}
//箭头运算符
T* operator->()
{
return m_ptr;
}
//重载布尔值操作
operator bool()
{
return m_ptr == nullptr;
}
T* get()
{
return m_ptr;
}
size_t use_count()
{
return *m_count;
}
bool unique()
{
return *m_count == 1;
}
void swap( Shared_ptr& ptr )
{
std::swap(*this, ptr);
}
};
使用引用计数(reference counting),引用计数就是所有管理同一个裸指针(raw pointer)的shared_ptr,都共享一个引用计数器。
每当一个shared_ptr被赋值(或者拷贝构造)给其他的shared_ptr时,这个共享的应用计数器就加1;
当一个shared_ptr被析构或者被用于管理其他裸指针时,这个计数器就减1;
如果此时发现引用计数器为0,那么说明它是管理这个指针的最后一个shared_ptr了,于是释放指针指向的资源。
class B;
class A{
public:
shared_ptr<B> ptr;
};
class B{
public:
shared_ptr<A> ptr;
};
int main(){
while(true){
shared_ptr<A> pa(new A());
shared_ptr<B> pb(new B());
pa->ptr = pb;
pb->ptr = pa;
}
return 0;
}
上例中class A和class B的对象各自被两个智能指针管理,也就是A和B的引用计数都是2。当循环结束时,pa和pb的析构函数被调用,但是A对象和B对象仍然被一个智能指针管理,他们的引用计数变成1,于是这两个对象都无法释放,造成内存泄漏。
解决方法:将class A和class B中的shared_ptr改成weak_ptr即可,由于weak_ptr不会更改shared_ptr的引用计数,所以在pa和pb析构时,会正确的释放内存。
面向对象程序设计(Object-Oriented Programming, OOP)是种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Odnx5PBa-1602215424643)(C:\Users\free\AppData\Roaming\Typora\typora-user-images\image-20200816092406117.png)]
面向对象三大特性:封装(Encapsulation),继承(Inheritance),多态(Polymorphism)
public
成员:可以被任意实体访问protected
成员:只允许被子类和本类的成员函数访问private
成员:只允许被本类的成员函数、友元类或者友元函数访问class A{
public:
void fun(int a);
voif fun(int a, int b);
};
把基类成员函数设置成虚函数,基类指针在绑定派生类的对象后就可以使用派生类覆盖过的方法。
虚函数:使用virtual
修饰成员函数,使其成为虚函数
static
)不能是虚函数过程
通过类的继承和虚函数机制在运行期间判断选择调用哪一个子类的对应函数(动态联编)举个例子:基类指针指向一个子类对象的时候,当父类调用的子类函数是虚函数的时候,就会调用子类重写过后的虚函数。
隐藏变量__vfptr,类型为void **
它应该指向一个void *的数组,这个void *的数组,就是我们的虚函数表。
不可以,因为虚函数的调用需要调用vptr,而vptr是存放在对象内存中的,如果构造函数可以是虚函数那么构造函数的vptr是没有地方存放的。
虚函数 类中如果声明了虚函数,那么这个函数是实现的,哪怕是空实现,他的作用就是为了能让这个函数在他的子类中可以被覆盖(override),这样就可以使用晚绑定实现多态了
纯虚函数 一种特殊的虚函数,在基类中不能对虚函数做出有意义的实现,而把他声明为纯虚函数,他的实现留给该基类的派生类去做
virtual int A() = 0;
final:用于限制某个类不能被继承,或者某个虚函数不能被重写
final只能用来修饰虚函数,并且要放到类或者函数的后面。
class A{
virtual void foo() final; // 限定foo()不能被重写
void bar() final; // 错误,final只能修饰虚函数
};
class B final : A{ // 限定B不能被继承
void foo(); // 错误,foo()不能被重写
};
class C :B{}; // 错误,B不能被继承
override:确保派生类中声明的重写函数于基类的虚函数有相同的签名,同时也明确表名将会重写基类的虚函数,还可以防止因疏忽而把原来想重写基类的虚函数声明为重载。
关键字要放在方法后面
不能
vfptr
):在含有虚函数类的对象中,指向虚函数表,在运行时确定.rodata section
)即内存常量区存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建class base1{
public:
int base1_1;
int base1_2;
virtual void base1_fun1(){}
virtual void base1_fun2(){}
};
class derived1:public base1{
public:
int derive1_1;
int derive1_2;
};
隐藏变量__vfptr,类型为void **
它应该指向一个void *的数组,这个void *的数组,就是我们的虚函数表。
vbptr
指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。#include =
using namespace std;
class A
{
public:
int a;
};
class B : public A
{
public:
int b;
};
class C : public A
{
public:
int c;
};
class D : public B, public C
{
public:
int d;
};
int main()
{
D d;
d.a = 5;
cout << d.a << endl;
return 0;
}
编译一哈:
main.cpp(26): error C2385: 对“a”的访问不明确
main.cpp(26): note: 可能是“a”(位于基“A”中)
main.cpp(26): note: 也可能是“a”(位于基“A”中)
main.cpp(27): error C2385: 对“a”的访问不明确
main.cpp(27): note: 可能是“a”(位于基“A”中)
main.cpp(27): note: 也可能是“a”(位于基“A”中)
这种就叫菱形继承,D中将会有两份A的副本如图:
指的就是D这个类被实例化了之后,对象d想访问基类A的成员a的时候,竟然不知道应该是通过B来找还是通过C来找
那这个时候我们该如何访问到经过B的A的成员a呢?代码2.2给出了一种解决方案,直接d.B::a :
cout << d.B::a << endl;
输出为:
5
对这两个a取地址:发现这两个地址是随机的,但是两个地址之间的地址位置差值是一样的
cout << &d.B::a << endl;
cout << &d.C::a << endl;
在需要继承的基类前加virtual关键字修饰该基类,使其成为虚基类,见代码2.4:
#include
using namespace std;
class A
{
public:
int a;
};
class B : virtual public A
{
public:
int b;
};
class C : virtual public A
{
public:
int c;
};
class D : public B, public C
{
public:
int d;
};
int main()
{
D d;
cout << &d.a << endl;
cout << &d.B::a << endl;
cout << &d.C::a << endl;
return 0;
}
我们可以发现,无论指不指定经过的类,a都只会在d中有一份副本了。
#include
using namespace std;
class Base
{
private:
int base;
public:
Base() {}
virtual ~Base() {}
virtual void func() {}
};
class Dervied : public Base
{
private:
int dervied;
public:
Dervied() {}
~Dervied() {}
};
int main()
{
cout << sizeof(Base) << endl;
cout << sizeof(Dervied) << endl;
return 0;
}
输出为:
8
12
好,我们来看看8是怎么来的,很简单:sizeof(int) + sizeof(void **)
就是base变量和虚函数指针的长度之和。
那为什么Dervied是12呢?
因为Dervied本身还有一个dervied变量呀。
好,我们再来看看菱形继承的(见代码5.2):
#include
using namespace std;
class Base
{
private:
int base;
public:
Base() {}
virtual ~Base() {}
};
class Dervied1 : public Base
{
private:
int dervied1;
public:
Dervied1() {}
virtual ~Dervied1() {}
};
class Dervied2 : public Base
{
private:
int dervied2;
public:
Dervied2() {}
virtual ~Dervied2() {}
};
class Final : public Dervied1, public Dervied2
{
private:
int final;
public:
Final() {}
~Final() {}
};
int main()
{
cout << sizeof(Base) << endl;
cout << sizeof(Dervied1) << endl;
cout << sizeof(Dervied2) << endl;
cout << sizeof(Final) << endl;
return 0;
}
(注:在C++11中,final是一个关键字,可以用来指定一个类不可再被继承)
输出结果为:
8
12
12
28
为什么呢?
我们来分析一下:
Base的大小为sizeof(Base::base) + sizeof(Base::__vfptr) = 8
Dervied1的大小为sizeof(Dervied1::dervied1) + sizeof(Base) = 12
Dervied2的大小为sizeof(Dervied2::dervied2) +sizeof(Base) = 12
Final的大小为sizeof(Dervied1)+sizeof(Dervied2) + sizeof(Final::final) = 28
OK,这个好判别。
那虚继承呢?
我们把代码5.2稍微改一下,就可以得出代码5.3:
#include
using namespace std;
class Base
{
private:
int base;
public:
Base() {}
virtual ~Base() {}
};
class Dervied1 : virtual public Base
{
private:
int dervied1;
public:
Dervied1() {}
virtual ~Dervied1() {}
};
class Dervied2 : virtual public Base
{
private:
int dervied2;
public:
Dervied2() {}
virtual ~Dervied2() {}
};
class Final : public Dervied1, public Dervied2
{
private:
int final;
public:
Final() {}
~Final() {}
};
int main()
{
cout << sizeof(Base) << endl;
cout << sizeof(Dervied1) << endl;
cout << sizeof(Dervied2) << endl;
cout << sizeof(Final) << endl;
return 0;
}
输出为:
8
16
16
28
为啥?
Base就不用讲了,和上面是一样的。
那Derived1呢?16 = sizeof(Base) + sizeof(Dervied1::dervied1) +sizeof (Dervied1::__dervied1)
Dervied2同理
那为什么Final是28呢?
请记住,因为是虚继承,Final只会有一份Base的副本,所以大小就是:
sizeof(Base::base)+sizeof(Base::__vfptr)+sizeof(Dervied1::dervied1)+sizeof(Dervied1::__vfptr)+sizeof(Derived2::dervied2)+sizeof(Dervied2::__vfptr)+sizeof(Final::final) = 28
栈(Stack):由编译器自动分配和释放,存放函数运行时分配的局部变量(包括局部常量,不可修改性由编译器保证,可以const_cast
)、函数参数值、返回数据、返回地址等。操作类似于数据结构中的栈
动态链接库:用于在程序运行期间加载和卸载动态链接库
堆(Heap):一般由程序员分配,如果程序员没有释放,程序结束时可能由操作系统回收。malloc(),calloc(),free()
操作的就是这块内存
全局数据区(global data):存放**全局变量、静态变量(包括静态局部和静态全局)**等,这块内存有读写权限,因此他们的值在程序运行期间可以任意改变。程序结束之后由系统释放
常量区(文字常量区):存放一般的常量、常量字符串,这块内存只有读权限,因此在运行期间不能改变。程序结束后由系统释放
代码区:存放函数体的二进制代码
alloca
函数进行,但是栈的动态分配由编译器进行释放,无需手动操作//下式表示new在堆上申请一块内存,然后在栈中存放一个指向这块堆内存的指针p
int* p = new int[6];
malloc
:申请指定字节数的内存。申请到的内存中的初始值不确定calloc
:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为 0realloc
:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定alloca
:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca
不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca
不宜使用在必须广泛移植的程序中//申请内存,并确认申请是否成功
char *str = (char*)malloc(100);
assert(str!=nullptr);
//释放内存并置空指针
free(p);
p = nullptr;
new/new[]
:完成两件事,先底层调用malloc
分配了内存,然后调用构造函数(创建对象)delete/delete[]
:也完成两件事,先调用析构函数(清理资源),然后底层调用free
释放空间new
在申请内存时会自动计算所需字节数,而malloc
则需要我们自己输入申请内存空间的字节数T* t = new T();
delete t;
在底层通过调用operator new/delete全局函数来实现申请/释放空间。
operator new函数通过malloc来申请空间,当malloc申请空间成功时则直接返回;申请空间失败时抛出bad_alloc类型异常。
对于内置类型new:new和malloc基本类似;唯一不同的是new失败会抛异常,malloc失败会返回NULL
对于自定义类型new:调用operator new函数申请空间;在申请的空间上执行构造函数,完成对象的构造。
new[N]:调用operator new[]函数,在operator new[]函数中实际调用operator new函数完成N个对象空间的申请;在申请的空间上执行N次构造函数。
operator delete函数通过free来释放空间。
对于内置类型delete:和free基本类似
对于自定义类型delete:在空间上执行析构函数,完成对象中资源的清理工作;调用operator delete函数释放对象的空间
对于自定义类型delete[]:在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理;调用operator delete[]释放空间,内部使用operator delete进行释放。
对于数组类型怎么删除的:需要在new[]的时候保存数组的维度,c++的做法是在分配数组空间时多分配4个字节大小的空间,专门保存数组的大小,在delete[]时就可以取出这个数,然后调用析构。
new
传递额外的地址参数,从而在预先指定的内存区域创建对象new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] {braced initializer list}
place_address
是一个指针initializers
提供一个(可能为空)以逗号分隔的初始值列表delete this 合法吗?
- 合法,但是
- 必须保证 this 对象是通过
new
(不是new[]
、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的- 必须保证调用
delete this
的成员函数是最后一个调用 this 的成员函数- 必须保证成员函数的
delete this
后面没有调用 this 了
应用程序可以通过系统的内存分配调用预先一次性申请适当大小的内存作为一个内存池,之后应用程序自己对内存的内配和释放可以通过这个内存池来完成。只有当内存池大小需要动态扩展时,才需要再调用系统的内存分配函数。
内存池可以分为单线程内存池和多线程内存池。也可以分为固定(每次分配的内存单元大小确认)内存池和可变(每次分配的大小可变)内存池。
使用默认的new/delete和malloc/free在堆上分配和释放内存会有一些额外的开销。应用程序频繁的在堆上分配和释放内存,就会导致性能的损失,并且会使系统中出现大量的内存碎片,降低内存的使用率。
固定内存池由一系列固定大小的内存块组成,每一个内存块又包含了固定数量和大小的内存单元。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YrHaOnNm-1602215424659)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/memorypool.png)]
在内存池初次生成时,只向系统申请了一个内存块,返回的指针作为整个内存池的头指针。之后随着应用程序对内存的不断需求,内存池判断需要动态扩大时,才再次向系统申请新的内存块,并把所有这些内存块通过指针链接起来。
当应用程序需要通过该内存池分配一个单元大小的内存时,只需要简单遍历所有的内存池块头信息,快速定位到还有空闲单元的那个内存池块。然后根据该块的块头信息直接定位到第1个空闲的单元地址,把这个地址返回,并且标记下一个空闲单元即可;当应用程序释放某一个内存池单元时,直接在对应的内存池块头信息中标记该内存单元为空闲单元即可。
相当于系统管理内存相比,内存池主要由以下优点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cd0Aq8hf-1602215424660)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/memorypoolstruct.png)]
#include
int main(){
print("Hello world!");
return 0;
}
gcc -o hello hello.c
g++ -o hello hello.cpp
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6iym7Tc8-1602215424662)(C:\Users\free\AppData\Roaming\Typora\typora-user-images\image-20200818131431353.png)]
预处理阶段:处理以#
开头的预处理命令,生成.i/.ii
文件
g++ -E hello.cpp -o hello.i
编译阶段:编译器进行词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成等,生成.s
汇编文件
g++ -S hello.cpp -o hello.s
汇编阶段:汇编器将汇编码生成机器码,生成.o
目标文件
g++ -c hello.cpp -o test.o
链接:链接器进行地址和空间分配、符号决议、重定位等,生成.out
可执行目标程序
ld -o hello.out hello.o ...libraries...
过程可以分为词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成等步骤
词法分析器读入组成源程序的字符流,并将其组成有意义的词素的序列。形如
比如赋值语句:position=initial+rate*60经过词法分析之后的到词法单元序列:
语法分析器使用由词法分析器生成的各词法单元的第一个分量来创建树形的中间表示。该中间表示给出了词法分析产生的词法单元的语法结构。常用的表示方法是语法树,树中每个内部节点表示一个运算,而该节点的子节点表示运算的分量。
以上赋值语句表示为语法树为:
数据的含义就是语义,简单的来说,数据就是符号。数据本身没有任何意义,只有被赋予含义的数据才能被使用,这时候数据就转换为了信息,而数据的含义就是语义。
语法分析器使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致,他同时收集类型信息,并存放在语法树或者符号表中,以便在中间代码生成过程中使用。
语法分析一个重要部分就是类型转换,比如很多语言要求数组下标必须是整数,如果使用浮点数那就必须报错。
在源程序翻译成目标代码的过程中,一个编译器可能构造出一个或者多个中间表示。这些中间表示可以由多种形式。语法树是一种中间表示形式,他们通常在语法分析和语义分析中使用。
在源程序的语法分析和语义分析完成之后,很多编译器生成一个明确的低级的或类机器语言的中间表示。该中间表示有两个重要的性质:1易于生成;2能够轻松的翻译为目标机器的语言。
代码优化试图改进中间代码,以便生成更好的目标的代码,即更快更短或者能耗更低。
代码生成以中间表示形式作为输入,并把它映射为目标语言。如果目标语言是及其代码,则必须为每个变量选择寄存器或者内存位置,中间指令则被翻译为能够完成相同任务的机器指令序列。
代码生成的一个至关重要的方面是合理分配寄存器以存放变量的值。
File Header
:文件头,描述整个文件的文件属性(包括文件是否可执行、是静态链接或动态连接及入口地址、目标硬件、目标操作系统等).text section
:代码段,执行语句编译成的机器代码.data section
:数据段,已初始化的全局变量和局部静态变量.bss section
:BSS 段(Block Started by Symbol),未初始化的全局变量和局部静态变量(因为默认值为 0,所以只是在此预留位置,不占空间).rodate section
:只读数据段,存放只读数据,一般是程序里面的只读变量(如 const 修饰的变量)和字符串常量comment section
:注释信息段,存放编译器版本信息.note.GNU-stack section
:堆栈提示段[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0z5ZmgLM-1602215424664)(C:\Users\free\AppData\Roaming\Typora\typora-user-images\image-20200816093642433.png)]
printf
这种标准函数库,如果每个程序都要有代码,这会极大浪费资源。Core Dump也叫做核心转储,当程序运行过程中发生异常,程序异常退出时,由操作系统把程序当前的内存状况存储到一个core文件中,叫做core dump。
使用ulimit -c unlimited
可以开启核心转储功能。
使用gdb [exe_path] [core_path]
打开core文件查看出错信息
使用到了三个常用的寄存器,EIP
为指令指针,即指向下一条即将执行的指令的地址;EBP
为基址地址,常用来指向栈底;ESP
为栈顶指针,常用来指向栈顶。
EIP
指针入栈EDP
入栈,因为每个函数都有自己的栈区域,所以栈基址不是一样的,现在需要进入新函数,为了不覆盖调用函数的EDP
,将它压入栈中。ESP
作为EBP
,即将此时的栈顶地址作为该函数的栈基址EBX,ESI,EDI
压入栈中,他们分别为基址寄存器,源变址寄存器,目的变址寄存器EDI,ESI,EBX
的值EDP
的值赋给ESP
,弹出EBP
的值函数可以使用临时变量作为函数返回值,但是将一个指向临时变量的指针作为函数的返回值是有问题的,因为临时变量在函数返回时会被销毁,所以指针也就指向了一块无意义的地址空间。
拷贝构造函数将临时变量拷贝到保存返回值的外部存储单元中,然后临时对象在函数结束时被销毁。
以往windows:CreateThread(),linux:pthread_create();不能跨平台
c++语言本身增加了多线程支持,可移植性(跨平台);
库 #include
用类以及一个问题范例
class TA {
public:
int &m_i;
TA(int &i) :m_i(i) {
cout << "ta的构造函数" << endl;
}
TA(const TA&ta) :m_i(ta.m_i)
{
cout << "ta的拷贝构造函数" << endl;
}
~TA()
{
cout << "ta的析构函数" << endl;
}
void operator()()
{
// cout << "我的线程开始执行了" << endl;
//
//
// cout << "我得线程执行完毕了" << endl;
cout << "m_i的值为" << m_i << endl;
cout << "m_i的值为" << m_i << endl;
cout << "m_i的值为" << m_i << endl;
cout << "m_i的值为" << m_i << endl;
cout << "m_i的值为" << m_i << endl;
cout << "m_i的值为" << m_i << endl;
cout << "m_i的值为" << m_i << endl;
}
};
int main()
{
int myi = 7;
TA ta(myi);//传入的是引用,所以一旦主线程执行完毕之后回收内存,那么分离执行的子线程中就失去了myi的对象
thread mytobj2(ta);
mytobj2.join();
//mytobj2.detach();
cout << "i love china" << endl;
return 0;
}
用类成员函数
class A
{
public:
int m_i;
//类型转换构造函数,可以直接传入一个整形构造
A(int a) :m_i(a) {
cout << "[A::A(int a)构造函数执行]" <<this<<"thread id="<<std::this_thread::get_id()<< endl;
}
A(const A & a) :m_i(a.m_i) {
cout << "[A::A(cosnt int a)构造函数执行]" <<this << "thread id=" << std::this_thread::get_id() << endl;
}
~A() {
cout << "[A::~A()析构函数执行]" <<this << "thread id=" << std::this_thread::get_id() << endl;
}
void thread_work(int num)
{
cout<<"子线程work函数执行"<<this<< "thread id=" << std::this_thread::get_id() << endl;
}
};
int main()
{
A myobj(1);
std::thread mytobj(&A::thread_work,myobj,16);//&成员函数函数名,this指针,变量
mytobj.join();
cout << "i love china" << endl;
return 0;
}
用lambda表达式
int main()
{
auto mylamthread = [] {
cout << "我的线程3开始了" << endl;
cout << "我的线程3结束了" << endl;
};
thread mytobj4(mylamthread);
mytobj4.join();
cout << "i love china" << endl;
cout << "i love china" << endl;
cout << "i love china" << endl;
cout << "i love china" << endl;
cout << "i love china" << endl;
return 0;
}
传递临时对象作为线程参数
线程id
class A
{
public:
int m_i;
//类型转换构造函数,可以直接传入一个整形构造
A(int a) :m_i(a) {
cout << "[A::A(int a)构造函数执行]" <<this<<"thread id="<<std::this_thread::get_id()<< endl;
}
A(const A & a) :m_i(a.m_i) {
cout << "[A::A(cosnt int a)构造函数执行]" <<this << "thread id=" << std::this_thread::get_id() << endl;
}
~A() {
cout << "[A::~A()析构函数执行]" <<this << "thread id=" << std::this_thread::get_id() << endl;
}
};
void myprint(const int i, const A & pmybug)
{
cout << "son thread parameter add ="<<&pmybug << endl;//打印pmybug地址
cout << "son thread id =" << std::this_thread::get_id() << endl;
return;
}
int main()
{
int mvar = 1;
int mysecondpar = 12;
cout << "father thread id="<<std::this_thread::get_id() << endl;
thread mytobj(myprint, mvar, A(mysecondpar));//将mysecondpar转成A类型对象传递给myprint
//在创建线程的同时临时构造对象的方法传递参数是可行的
mytobj.join();
cout << "i love china" << endl;
return 0;
}
class A
{
public:
int m_i;
//类型转换构造函数,可以直接传入一个整形构造
A(int a) :m_i(a) {
cout << "[A::A(int a)构造函数执行]" <<this<<"thread id="<<std::this_thread::get_id()<< endl;
}
A(const A & a) :m_i(a.m_i) {
cout << "[A::A(cosnt int a)构造函数执行]" <<this << "thread id=" << std::this_thread::get_id() << endl;
}
~A() {
cout << "[A::~A()析构函数执行]" <<this << "thread id=" << std::this_thread::get_id() << endl;
}
};
void myprint(const int i, A & pmybug)
{
cout << "son thread parameter add ="<<&pmybug << endl;//打印pmybug地址
cout << "son thread id =" << std::this_thread::get_id() << endl;
pmybug.m_i = 199;
return;
}
int main()
{
int mvar = 1;
int mysecondpar = 12;
cout << "father thread id="<<std::this_thread::get_id() << endl;
A myobj(10);
thread mytobj(myprint, mvar, std::ref(myobj));//将mysecondpar转成A类型对象传递给myprint
//在创建线程的同时临时构造对象的方法传递参数是可行的
mytobj.join();
cout << "i love china" << endl;
return 0;
}
创建十个线程,十个线程开始执行
int main()
{
for (int i = 0; i < 10; i++)
{
mythreads.push_back(thread(myprint, i));//创建并开始执行线程
}
for (auto iter = mythreads.begin(); iter != mythreads.end(); ++iter)
{
iter->join();
}
cout << "i love china" << endl;
return 0;
}
只读数据是安全稳定的,不需要特别的处理手段
vector<int>g_v = { 1,2,3 };//共享数据
void myprint(int inum)
{
cout << "id为" << std::this_thread::get_id() << "的线程 打印G_V值" << g_v[0] << g_v[1] << g_v[2] << endl;
return;
}
int main()
{
for (int i = 0; i < 10; i++)
{
mythreads.push_back(thread(myprint, i));//创建并开始执行线程
}
for (auto iter = mythreads.begin(); iter != mythreads.end(); ++iter)
{
iter->join();
}
cout << "i love china" << endl;
return 0;
}
有读有写的数据:2个线程写,8个线程读,如果代码没有特别的处理,那程序肯定崩溃
其他案例
class A {
public:
//把收到的消息入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue,插入一个元素" << i << endl;
msgRevcQuere.push_back(i);//假设数字i就是收到的命令,插入消息队列
}
}
//把数据从消息队列中取走并处理的线程
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
if (!msgRevcQuere.empty())
{
//消息不为空
int command = msgRevcQuere.front();//返回第一个元素
msgRevcQuere.pop_front();//移除第一个元素,但不返回
//处理数据
//。。。。
}
else {
cout << "outMsgRecvQueue执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int>msgRevcQuere;
};
int main()
{
A myobj;//创建对象
std::thread myOutMsgobj(&A::outMsgRecvQueue, &myobj);//成员函数创建线程
std::thread myInMsgobj(&A::inMsgRecvQueue, &myobj);
myOutMsgobj.join();//阻塞
myInMsgobj.join();
return 0;
}
互斥量基本概念
互斥量的用法
class A {
public:
//把收到的消息入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue,插入一个元素" << i << endl;
my_mutex.lock();//锁住写操作
msgRevcQuere.push_back(i);//假设数字i就是收到的命令,插入消息队列
my_mutex.unlock();
}
}
bool outMsgLULProc(int &command)//将读数据和删除数据提取到一个函数中去
{
my_mutex.lock();//lock
if (!msgRevcQuere.empty())
{
//消息不为空
command = msgRevcQuere.front();//返回第一个元素
msgRevcQuere.pop_front();//移除第一个元素,但不返回
my_mutex.unlock(); //处理数据
return true; //。。。。
}
my_mutex.unlock();//每一个分支都需要unlock
return false;
}
//把数据从消息队列中去除的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑可疑进行命令处理
}
else {
cout << "outMsgRecvQueue执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int>msgRevcQuere;
std::mutex my_mutex;
};
std::lock_guard类模板:直接取代lock()和unlcok();也就是说,用了lock_guard后,可以不用unlock了
bool outMsgLULProc(int &command)
{
std::lock_guard<std::mutex> sbguard(my_mutex);//lock_guard构造函数里执行了mutex::lock(),由于lock_gurad是一个局部对象,所以在函数结束的时候执行析构函数的时候调用mutex::unlock()
if (!msgRevcQuere.empty())
{
command = msgRevcQuere.front();//返回第一个元素
msgRevcQuere.pop_front();//移除第一个元素,但不返回
return true; //。。。。
}
return false;
}
概念:有两把互斥锁(死锁产生的前提条件,至少两个互斥量):金锁(jinlock)银锁(yinlock)
死锁演示
class A {
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue,插入一个元素" << i << endl;
my_mutex1.lock();//实际代码中,两个lock不一定挨着
my_mutex2.lock();
msgRevcQuere.push_back(i);//假设数字i就是收到的命令,插入消息队列
my_mutex2.unlock();
my_mutex1.unlock();
//...很长一段处理代码
}
}
bool outMsgLULProc(int &command)
{
my_mutex2.lock();
my_mutex1.lock();
if (!msgRevcQuere.empty())
{
command = msgRevcQuere.front();//返回第一个元素
msgRevcQuere.pop_front();//移除第一个元素,但不返回
my_mutex1.unlock();
my_mutex2.unlock();
return true; //。。。。
}
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑可疑进行命令处理
}
else {
cout << "outMsgRecvQueue执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int>msgRevcQuere;
std::mutex my_mutex1;
std::mutex my_mutex2;
};
int main()
{
A myobj;
std::thread myOutMsgobj(&A::outMsgRecvQueue,&myobj);
std::thread myInMsgobj(&A::inMsgrecvQueue,&myobj);
myOutMsgobj.join();
myInMsgobj.join();
reutrn 0;
}
#include
#include //线程头文件
#include
using namespace std;
std::mutex lock1;
std::mutex lock2;
void mythread1(int mypar)
{
for (int i = 0; i < mypar; i++)
{
lock1.lock();
lock2.lock();
cout <<"1:"<< i << endl;
lock1.unlock();
lock2.unlock();
}
}
void mythread2(int mypar)
{
for (int i = 0; i < mypar; i++)
{
lock1.lock();
lock2.lock();
cout << "2:" << i << endl;
lock1.unlock();
lock2.unlock();
}
}
int main()
{
std::thread t1(mythread1,10000);
std::thread t2(mythread2,10000);
t1.join();
t2.join();
return 0;
}
死锁的一般解决方案:
std::lock(my_mutex1, my_mutex2);
// my_mutex2.lock();
// my_mutex1.lock();
std::lock_guard的std::adopt_lock参数
std::lock(my_mutex1, my_mutex2);
std::lock_guardsbgurad1(my_mutex1,std::adopt_lock); //用一个大括号包含需要加锁的代码段,提前结束lock_guard的生命周期
std::lock_guardsbgurad2(my_mutex2,std::adopt_lock);
unique_lock取代lock_guard
unique_lock第二个参数
unique_lock的成员函数
lock(),加锁
unlock(),解锁
try_lock(),尝试给互斥量加锁,如果拿不到锁,则返回false,如果拿到了锁,返回true,这个函数不阻塞,可以执行其他任务
release(),返回它所管理的mutex对象指针,并释放所有权;也就是说,这个unique_lock和mutex不在有联系
//不要和unlock混淆,unique_lock初始化时候绑定,用release释放绑定
//一旦释放绑定,如果mutex处于锁定状态,需要手动解锁
//有时候需要unlock(),因为lock锁住的代码段越少,执行越快,整个效率运行程序越高。
所有权传递 std::unique_lockstd::mutexsbguard1(std::move(sbgurad));//移动语义
std::once_flag g_flag;//系统定义的标记
class MyCAS {
static void CreateInstance()//只被调用一次的函数
{
m_instance = new MyCAS();
static CGAEhuishou cl;
}
private:
MyCAS()
{
}
static MyCAS * m_instance;//静态成员变量
public:
static MyCAS *GetInstance()
{
std::call_once(g_flag, CreateInstance);//假设两个线程同时开始执行到这一行,其中一个线程要等另一个线程执行完createinstance后才能决定是否调用createinstance
cout << "call_once执行完毕" << endl;
return m_instance;
}
class CGAEhuishou {
public:
~CGAEhuishou()
{
if (MyCAS::m_instance)
{
delete MyCAS::m_instance;
MyCAS::m_instance = NULL;
}
}
};//类中套类,用来释放对象
void func()
{
cout << "测试" << endl;
}
};
#include "stdafx.h"
#include
#include //线程头文件
#include
#include
#include
using namespace std;
std::mutex resource_mutex;
std::once_flag g_flag;//系统定义的标记
class A {
public:
//把收到的消息入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
std::unique_lock<std::mutex>sbguard(my_mutex);
cout << "inMsgRecvQueue,插入一个元素" << i << endl;
msgRevcQuere.push_back(i);//假设数字i就是收到的命令,插入消息队列
my_cond.notify_all();//我们尝试把wait()线程唤醒,执行完这行,那么outMsgRecvQueue()里面的wait就会被唤醒
}
}
//把数据从消息队列中去除的线程
void outMsgRecvQueue()
{
int command = 0;
while (true)
{
std::unique_lock<std::mutex> sbguard(my_mutex);
my_cond.wait(sbguard, [this] {//一个lambda表达式就是一个可调用对象
if (!msgRevcQuere.empty())
return true;
return false;
});
command = msgRevcQuere.front();
msgRevcQuere.pop_front();
cout << "outMsgRecvQueue()执行,取出一个元素" << command <<"thread id"<<std::this_thread::get_id() << endl;
sbguard.unlock();//
}
}
private:
std::list<int>msgRevcQuere;
std::mutex my_mutex;
std::condition_variable my_cond;//生成一个条件变量
};
int main()
{
A myobj;//创建对象
std::thread myOutMsgobj(&A::outMsgRecvQueue, &myobj);//成员函数创建线程
std::thread myOutMsgobj2(&A::outMsgRecvQueue, &myobj);
std::thread myInMsgobj(&A::inMsgRecvQueue, &myobj);
myOutMsgobj.join();//阻塞
myOutMsgobj2.join();
myInMsgobj.join();
return 0;
}
//notify_one()(随机唤醒一个等待的线程)
//notify_all()(唤醒所有等待的线程)
#include
#include
#include
#include
using namespace std;
std::mutex data_mutex;//互斥锁
std::condition_variable data_var;//条件变量
bool flag = true;
void printfA() {
int i = 1;
while(i <= 100) {
//休息1秒
//std::this_thread::sleep_for(std::chrono::seconds(1));
std::unique_lock<std::mutex> lck(data_mutex);
data_var.wait(lck,[]{return flag;});//等待flag=true才打印奇数
std::cout<<"A " << i <<endl;
i += 2;
flag = false;
data_var.notify_all();
}
}
void printfB() {
int i = 2;
while(i <= 100) {
std::unique_lock<std::mutex> lck(data_mutex);
data_var.wait(lck,[]{return !flag;});//等待flag=false才打印偶数
std::cout<<"B " << i <<endl;
i += 2;
flag = true;
data_var.notify_all();
}
}
int main() {
// freopen("in.txt","r",stdin);
std::thread tA(printfA);
std::thread tB(printfB);
tA.join();
tB.join();
return 0;
}