c++
argument实参 parameter形参
默认参数只在声明时指定,定义时不设置
多线程
智能指针
代理绑定
设计模式 了解
emplace_back代替push_back
- 减少对象构造和复制 / 移动操作
push_back
需要先构造一个临时对象,然后将其复制或移动到容器中。而emplace_back
则直接在容器的内存位置上构造对象,省去了临时对象的创建和后续的复制 / 移动操作。
- 直接使用参数构造对象
emplace_back
可以接受构造对象所需的参数,直接在容器内构造对象:
// 使用push_back需要先创建对象
vec.push_back(MyClass(1, "hello")); // 临时对象 + 移动操作
// 使用emplace_back直接在容器内构造
vec.emplace_back(1, "hello"); // 直接构造,无需临时对象
- 支持隐式转换
emplace_back
支持隐式类型转换,可以接受能转换为容器元素类型的参数:
移动语义
函数返回一个临时对象时调用拷贝构造,但构造后马上析构
cstring getstring(cstring &str)
{
const char* pstr = str.c_str();
cstring tmpstr(pstr);//普通构造
return tmpstr;
}
int main()
{
cstring str1("abcde");
cstring str2;
str2 = getstring(str1);
}
typename
强制声明类型
C++ 规定,当依赖名称表示一个类型时,必须使用 typename
显式声明:
编译时多态和运行时多态
编译时多态
编译时多态是指在编译阶段就确定调用哪个函数的多态形式,主要通过函数重载和模板来实现。
运行时多态
运行时多态是指在程序运行阶段才确定调用哪个函数的多态形式,主要通过虚函数和指针或引用来实现。
虚函数和虚函数表
当一个函数被声明为虚函数(在基类中使用 virtual
关键字声明)时,派生类可以重写这个虚函数。通过基类指针或引用调用虚函数时,程序会在运行时根据指针或引用实际指向的对象类型,来决定调用哪个类的虚函数版本。
虚函数表
- 用于实现c++运行时多态(动态绑定)
- 每个含有虚函数的类会自动生成一个虚函数表,表中存放所有虚函数的地址
- 实现的对象中存储指向虚函数表的指针(vptr),运行时通过该指针调用对应虚函数
- 每个类只有一份,存储在只读数据段
多重继承对vtable的影响
继承多个类就会有多个vptr,分别指向每个基类对应的vtable
虚继承对vtable的影响
虚继承解决菱形继承中重复基类实例的问题
引入虚基表(vbptr)记录虚基类相对对象的偏移
共享成员单独存放在最后
函数指针
在 C++ 中,函数名在大多数情况下可以隐式地转换为指向该函数的指针。所以下面这行代码:
int (*func)(int, double) = print;
而显式使用取地址符&
来获取函数指针也是可行的,即:
int (*func)(int, double) = &print;
c++11
NULL c++中define为0,c语言中define为(void* 0)
原始字面量R“(内容)”
constexpr
常量表达式在编译阶段完成,变量计算在运行阶段。常量表达式可以提高程序运行效率。const有双重语义:变量只读(如函数形参),修饰常量。使用constexpr可以让编译器知道改行是常量表达式。
使用constexpr定义常量和修饰函数,称为 常量表达式函数 ,函数中不能出现非常量表达式之外的语句
若用constexpr修饰构造函数,则初始化必须写在初始化列表中,函数体要为空
auto
auto使用时必须要初始化
auto只在推导const *或 volitale *或const &时保留const和volitale关键字,其他时候会忽略const与volitale,所以要使用const auto xxx来推导定义。
decltype在编译时推导新的变量
decltype(表达式)
若表达式是一个左值或被()包围,推导出的是表达式类型的引用,且若有const需要加上
返回值类型后置
template< typename T, typename U>
auto add(T t, U u)->decltype(t+u)
{
return t+u;
}
auto与decltype对比
1. 推导规则不同
-
auto
:根据初始化表达式的值类别(value category)推导类型,通常会 忽略引用和 cv 限定符。int x = 42; const int& cref = x; auto y = cref; // y 的类型为 int(忽略引用和 const) int arr[5]; auto ptr = arr; // ptr 的类型为 int*(数组退化为指针)
-
decltype
:根据表达式的 值类别和类型 精确推导,保留 引用和 cv 限定符。int x = 42; const int& cref = x; decltype(cref) z = x; // z 的类型为 const int&(保留引用和 const) int arr[5]; decltype(arr) arr2; // arr2 的类型为 int[5](不退化)
2. 推导时机不同
-
auto
:在 编译期 根据初始化表达式推导类型。 -
decltype
:在 编译期 根据表达式的静态类型推导,但可用于延迟类型计算(如模板元编程)。
3. 使用场景不同
auto
:简化类型声明,避免冗长的类型名,适用于迭代器、lambda 等场景。decltype
:精确获取表达式类型,用于模板元编程、返回类型推导、保持引用特性等场景。
volatile
1. 禁止编译器优化
若 value
被声明为 volatile
,编译器会强制每次都从内存读取:
2. 确保内存可见性
在嵌入式系统或多线程环境中,某些变量可能被硬件或其他线程直接修改,volatile
确保程序每次都能获取最新值:
左值右值和左右值引用
左值(L - value)
-
定义:左值是可以出现在赋值表达式左边的值,它表示一个占据内存空间、有持久化存储位置的实体。一般来说,左值具有名称,可以通过地址运算符
&
获取其地址。例如变量就是典型的左值
右值(R - value) -
定义:右值是只能出现在赋值表达式右边的值,它通常是临时的、短暂存在的。右值没有持久化的存储位置,一般不能对其取地址(C++11 引入的
std::move
等特殊情况除外)。例如字面常量、函数的临时返回值等都是右值:
右值引用通过双&&
符号来声明。例如:
int&& rref = 10; // 将一个右值(字面常量10)绑定到右值引用rref
右值引用只能用右值初始化
右值引用的作用
延迟生命周期
- 移动语义:在传统 C++ 中,当对象传递或赋值时,即使对象是临时的,也会进行深拷贝,这在处理大对象时效率较低。右值引用使移动语义成为可能。移动语义允许我们将临时对象(右值)的资源 “窃取” 过来,而不是进行深拷贝。
- 完美转发:右值引用在模板元编程中实现完美转发功能。完美转发允许函数模板将其参数原封不动地转发给其他函数,即保持参数的左值或右值属性。例如:
template<typename T>
void forward(T&& arg) {
otherFunction(std::forward<T>(arg));
}
右值引用只能绑定到右值,即临时对象,比如字面常量、函数的临时返回值等。
final和override
final修饰函数只能修饰虚函数,阻止子类重写父类函数
final修饰类,该类不允许被继承,即无派生类
final写在函数名或类名的后面
override不是必须的,但编译器可以检查函数是为了重写的,而不是相似名字的新函数
委托构造函数
在一个构造函数中调用其他构造函数,减少代码复用。必须在初始化列表调用
Test(int max)
{
this->m_max=max;
}
Test(int max,int min):Test(max)
{
//相当于在此处执行了第一个构造函数
this->m_min=min;
}
继承构造函数
class Base
{
Base(){
}
}
class Child:public Base
{
public:
using Base::Base;
}
initializer_list<>
接收任意多相同类型数据,使用size(),begin(),end()确定范围
范围循环
for(auto &t:容器,数组,初始化列表)
{
//...
}
set,map的key即使不限定cosnt也是只读的
范围循环只会访问对象一次,确定对象元素的个数
lambda
会被看做仿函数
[capture](params)opt->ret{body;};
捕捉方式 参数列表 选项 返回值类型 函数体
其中capture是捕获列表,params是参数列表,opt是函数选项,ret是返回值类型,body是函数体。
捕获列表
lambda表达式的捕获列表可以捕获一定范围内的变量,具体使用方式如下:
[] - 不捕捉任何变量
[&] - 捕获外部作用域中所有变量, 并作为引用在函数体内使用 (按引用捕获)
[=] - 捕获外部作用域中所有变量, 并作为副本在函数体内使用 (按值捕获)
拷贝的副本在匿名函数体内部是只读的
[=, &foo] - 按值捕获外部作用域中所有变量, 并按照引用捕获外部变量 foo
[bar] - 按值捕获 bar 变量, 同时不捕获其他变量
[&bar] - 按引用捕获 bar 变量, 同时不捕获其他变量
[this] - 捕获当前类中的this指针
让lambda表达式拥有和当前类成员函数同样的访问权限
如果已经使用了 & 或者 =, 默认添加此选项
作者: 苏丙榅
链接: https://subingwen.cn/cpp/lambda/#2-捕获列表
来源: 爱编程的大丙
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
仿函数
重载了()运算符的类对象
class Car{
Car(){};
void operator () (){
}
}
using使用
https://subingwen.cn/cpp/using/#2-模板的别名
1.命名空间
2.在子类中调用隐藏的父类同名函数 Father::func()
3,.c++11中,给类型定义别名,和typedef类似
using newtypename = oldtype;
using uint_t = int;
区别:
定义函数指针
//定义函数指针类型,返回Int,参数有int和string
typedef int(*func)(int,string);
using func1 = int(*)(int,string);//更直观
定义模板别名
template <typename T>
typedef map<int, T> mapType;//错误,语法不允许这样定义
//正确
template <typename T>
struct MyMap
{
typedef map<int, T> mapType;
}
可调用对象
按函数调用
- 函数指针
- 重载operator()的类对象(即仿函数)
- 可转换为函数指针的类对象
using func_ptr = void(*)(int,string);
Class MyClass{
static void print(int a,string b)
{
cout<<a<<b<<endl;
}
//类型转换运算符重载
//类对象转换为函数指针
//格式为operator type()
operator func_ptr()//把对象转换为func_ptr类型,可以使用对象作为参数隐式调用
{
return print;
}
int my_id;
}
//调用时
Myclass obj;
obj(15,"岁");
using ptr1 = int Test::*;
ptr1 pt = &Test::my_id;
- 一个类成员函数指针或类成员指针
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Test
{
void print(int a, string b)
{
cout << "name: " << b << ", age: " << a << endl;
}
int m_num;
};
int main(void)
{
// 定义类成员函数指针指向类成员函数
void (Test::*func_ptr)(int, string) = &Test::print;
// 类成员指针指向类成员变量
int Test::*obj_ptr = &Test::m_num;
Test t;
// 通过类成员函数指针调用类成员函数
(t.*func_ptr)(19, "Monkey D. Luffy");
// 通过类成员指针初始化类成员变量
t.*obj_ptr = 1;
cout << "number is: " << t.m_num << endl;
return 0;
}
作者: 苏丙榅
链接: https://subingwen.cn/cpp/bind/#1-%E5%8F%AF%E8%B0%83%E7%94%A8%E5%AF%B9%E8%B1%A1
来源: 爱编程的大丙
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
可调用对象包装器
#include<functional>
std::function<返回值类型(参数类型列表)> diy_name = 可调用对象
1. 包装普通函数
function<void(int, string)> f1 = print;
2. 包装类的静态函数
function<void(int, string)> f2 = MyClass::print;
3. 包装仿函数
4. 包装转化为函数指针的对象
调用
f1(18,"岁");
f2(18,"岁");
例子
class A
{
public:
//构造函数参数是一个包装器对象
A(const function<void(int,string)> &f): callback(f)
{
}
void notify(int id, string name)
{
callback(id,name);//调用通过构造函数得到的函数指针
}
private:
function<void(int,string)> callback;
}
若要让包装对象可读可写,加&
function<int&(void)> = bind(Test::m_number,&t);
绑定器bind
std::bind用来将可调用对象与其参数一起绑定。绑定的结果可以用std::function进行保存,并延迟调用到任何需要的时候。
作用:
1.将可调用对象与参数绑定为一个仿函数 (使用auto f = bind()得到的是仿函数类型,而不是包装器类型)
2.将多参数可调用对象转换为一元或(n-1)元可调用对象,即只绑定部分参数
//绑定非类成员函数与变量
auto f = std::bind(可调用对象地址,绑定的参数/占位符);
//绑定成员函数与变量(变量不用填参数)
auto f = std::bind(类函数/成员地址,类实例对象地址,绑定的参数/占位符)
占位符如placeholders::_1,表示函数调用时被传入的第一个参数替代
注意:绑定时的参数有效时,调用对象传入的参数无效
void output(int x,int y)
{
cout<<x<<y<<endl;
}
bind(output,1,2)();//最后一个()相当于调用该仿函数,且调用时不用传入参数,因为绑定了1,2
bind(output,placeholders::_1,2)(10);//10作为传入参数,在placeholders位置传入调用
bind(output,2,placeholders::_2)(10,20);//执行时,10被忽略,使用绑定的2,传入20
右值引用
右值包括
纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、lambda表达式等
将亡值:与右值引用相关的表达式,如T&&类型返回值,std::move的返回值
有时产生临时对象后马上析构,造成内存开销,为了优化,c++11引入右值引用·
class Test
{
public:
//移动构造函数(有右值引用参数的函数),复用另一个对象的(堆内存)
//实现浅拷贝而不是深拷贝,因为对象a已经消耗资源进行构造,再次构造的花费很多
Test(Test&& a):m_num(a.m_num)
{
a.m_num = nullptr;//防止a析构时销毁a的堆内存,导致m_num失效
cout<<"move construct"<<endl;
}
int *m_num;
}
Test getObj()
{
Test obj;
return obj;
}
Test getObj1()
{
return Test();
}
Test &&getObj2()//返回右值引用
{
return Test();
}
int main()
{
Test t = getObj();//编译器会判断,如果右侧产生的是临时对象,则优先使用移动构造函数,而不是拷贝构造函数
Test&& t1 = getObj();
//如果没有移动构造函数,使用右值引用初始化要求右侧是一个临时的不能取地址的对象
Test&& t2= getObj1();
return 0;
}
move与forward完美转发
右值引用在传递时会被转换为左值引用,通过std::move方法可以把左值转换为右值(将亡值)
把资源的所有权转移
Test&& t2 = getObj1();
Test&& t3 = t2;//错误,右值引用只能用右值初始化
Test&& t3 = move(t2);//正确
例子:
list<string> ls1{"nihao","shijie","hello","world"};
list<string> ls2 = ls1;
list<string> ls3 = move(ls1);//资源转移,不再是用ls1了,通过Move减少开销
完美转发
- 概念:完美转发是指函数模板能够将其参数原封不动地转发给其他函数,同时保持参数的左值或右值属性。也就是说,传入函数模板的实参如果是左值,转发后仍然是左值;如果是右值,转发后仍然是右值。这在编写通用库或框架时非常有用,因为它允许库函数在不丢失实参语义的情况下,将实参传递给其他函数。
右值引用独立于值,右值引用在作为函数参数的形参时,在函数内部转发该参数给内部其他函数时会作为左值,不是原来的类型了。如果需要按照原来的类型转发给其他函数,使用forward(),该函数实现的功能称为完美转发
//函数原型
template<class T> T&& foward( typename remove_reference<T>::type& t) noexcept
//精简
std::forward<T>(t);
- 当T为左值引用类型时,t被转换为T类型的左值
- 当T不是左值引用类型时,t被转换为T类型的右值
未定义类型引用
即auto&& 或模板T &&得到的变量
通过右值推导,得到右值引用
非右值(右值引用、左值、左值引用、常量左值右值引用)推导得到左值引用
const T&&一定是右值引用
智能指针
new -delete malloc- free区别:new delete能自动调用构造函数和析构函数
智能指针能保证资源自动释放,不产生野指针和内存溢出。利用栈上对象出作用域时析构自动释放
智能指针头文件<memory>
- shared_ptr共享智能指针,多个智能指针共享内存引用计数,在计数为0时使用析构释放内存
- unique_ptr独占智能指针,只有一个指针管理一块内存
- weak_ptr弱引用智能指针,不共享指针,不能操作资源,用来监视shared_ptr
shared_ptr
1.初始化
shared_ptr<T> 智能指针名字(管理的堆内存)
2.拷贝和移动构造初始化
shared_ptr<int> ptr1(new int(520));
shared_ptr<int> ptr2(ptr1);//ptr1.use_count()会增加
shared_ptr<int> ptr3 =ptr1;
shared_ptr<int> ptr4 = move(ptr1);//use_count不会增加
3.通过make_shared初始化
template<class T, class... Args>
shared_ptr<T> make_shared(Args&&... args);//参数列表即初始化传入的参数
shared_ptr<Test> ptr = make_shared<Test>(参数);
- T:模板参数数据类型
- Args&&... args:要初始化的数据,如果通过make_shared创建对象,需要按照构造函数参数列表指定
4.通过reset方法初始化
ptr.reset();//ptr使用的空间被析构,use_count变为0
//指定新内存
ptr.reset(new int(1));//重置内存
获取原始指针
Test* t = ptr.get();
若直接使用智能指针调用所指对象,就当成指针来用
ptr->function();
指定删除器
引用计数为0时内存会被智能指针析构,也可以在初始化时指定删除动作
shared_ptr<Test> ppp(new Test(100),删除器);
shared_ptr<Test> ppp(new Test(100),[](Test* t){
//释放内存操作
delete t;
});
当智能指针管理数组内存时,必须指定删除器
1.自定义删除器
shared_ptr<Test> ppp(new Test[5],[](Test* t){
//释放内存操作
delete[] t;
});
2.模板参数中指定数组类型
shared_ptr<Test[]> ppp(new Test[5]);
3.使用c++默认删除器函数
shared_ptr<Test> ppp(new Test[5],default_delete<Test[]>());//<Test[]>是数组,不加[]就不是数组
unique_ptr
1.初始化
1.构造函数
unique_ptr<int> ptr1(new int(2));
2.移动构造函数初始化;
unique_ptr<int> ptr2 = move(ptr1);
3.reset
ptr2.reset();//因为是unique,所以原内存被释放
4.获取原始指针
unique_ptr<Test> ptr3(new Test(1));
Test* pt = ptr3.get();
pt->function();
2.删除器
和shared_ptr的区别是,指定删除器时unique_ptr需要在模板参数里指定删除器类型
using func_ptr = void(*)(int*);
unique_ptr<int,func_ptr> ptr1(new int(10),[](int*p) {delete p});
独占智能指针可以管理数组类型指针,自动释放
unique_ptr<Test[]> ptr(new Test[3]);
weak_ptr
辅助shared_ptr,不管理shared_ptr内部的指针
1.当一个函数返回shared_ptr<Test>(this),当释放该地址时,this对象会被析构两次
2.循环引用时,count无法减为0,造成内存泄漏
初始化
shared_ptr<int> sp;
weak_ptr<int> wp(sp);
weak_ptr<int> wp1 = sp;
weak_ptr可以使用use_count方法
expired()观测资源是否已经释放,返回true(shared_ptr全部释放)或false
lock()得到管理监测的shared_ptr对象,即一块内存,可以给其他指针赋值初始化
reset()清空对象,使其不监测任何资源
注意
shared_ptr
1.不能用一个原始地址(new得到的地址)初始化多个shared_ptr,要用智能指针做第二次赋值
2.不能返回管理了this的共享智能指针对象,否则引用计数不会增加,会重复析构
class Test:public enable_shared_from_this<Test>//必须继承enable...才能调用shared_from_this
{
shared_ptr<Test> getSharedPtr(){
return shared_from_this();//返回了一个检测shared_ptr的weak_ptr
}
}
3.共享智能指针不能循环引用
实现
template<typename T>
classs CSmartPtr
{
public:
CSmartPtr(T* ptr = nullptr):mptr(ptr){}
~CSmartPtr(){ delete mptr;}
T& operator*() {return *mptr;}
T* operator->() {return mptr;}
private:
T *mptr;
}
对资源进行引用计数shared_ptr
//引用计数类
template<typename T>
class ReftCnt
{
public:
RefCnt(T *ptr=nullptr):mptr(ptr)
{
if(mptr!=nullptr)
mcount = 1;
else mcount = 0;
}
void addRef(){mcount++;}//增加引用计数
void delRef(){mcount--;}
private:
T *mptr;
int mcount;
}
template<typename T>
classs CSmartPtr
{
public:
CSmartPtr(T* ptr = nullptr):mptr(ptr){
mpRefCnt = new RefCnt();//生成对象同时生成引用计数对象
}
~CSmartPtr(){ //析构时检查引用计数是否为0,再释放
if(0==mpRefCnt->delRef())
{
delete mptr;
mptr = nullptr;
}
}
T& operator*() {return *mptr;}
T* operator->() {return mptr;}
//拷贝构造
CSmartPtr(const CSmartPtr<T> &src)
:mptr(src.mptr),mpRefCnt(src.mpRefCnt)
{
if(mptr!=nullptr)
mpRefCnt->addRef();
}
CSmartPtr<T>& operator=(const CSmartPtr<T> &src)//等号左边的计数减1,右边的加1
{
if(this == &src)
return *this;
if(0==mpRefCnt->delRef())//引用计数为0则析构资源
{
delete mptr;
}
mptr = src.mptr;
mpRefCnt = src.mpRefCnt;
mpRefCnt->addRef();
return *this;
}
private:
T *mptr;//指向资源的指针
RefCnt<T> *mpRefCnt;//指向该资源引用计数对象的指针
}
分区与存储
栈区(Heap):局部变量,函数参数,返回地址等临时数据,编译器自动分配释放
堆区(Stack):动态分配内存,理论上受限于虚拟内存总量
全局区(静态存储区):静态变量、全局变量。程序启动时分配,结束时释放,未初始化的自动初始化为0
常量存储区:存放常量数据和const修饰的全局变量
代码区:存储可执行代码,只读
流式存储:
用结构体把变量包裹起来,可以把分散的变量连续存储
const
1 . const int*
和 int const*
等价(指向常量的指针),如定义
int x=10,y=20;
const int* p=&x;
*p=15; 错误,指向的是常量,不可修改
p = &y; 正确:指针本身可以修改
2 . int* const
是常量指针。
指针 p
的地址固定,但可以通过 p
修改指向的值。
int x = 10;
int* const p = &x;
*p = 15; 正确:值可修改
p = &y; 错误:指针地址不可修改
3 . const
对象:只能调用 const
成员函数。
总结:
const限制了定义时离const之前最近的修饰,如int* const p=&x;最近的星号表示地址,即p地址固定,但指向的值可以变
修饰变量:值不可变
修饰指针:限制指针或指向的数据
const修饰的函数可以重载。const成员函数既不能改变类内的数据成员,也无法调用非const的成员函数;const类对象只能调用const成员函数,非const对象无论是否是const成员函数都能调用,但是如果有重载的非const函数,非const对象会优先调用重载后的非const函数。
static
类中的static方法属于类而不属于某个类对象,可以通过类名调用
static即静态的意思,可以对变量和函数进行修饰。分三种情况:
(1)当用于文件作用域的时候(即在.h/.cpp文件中直接修饰变量和函数),static意味着这些变量和函数只在本文件可见 ,其他文件是看不到也无法使用的,可以避免重定义的问题。
(2)当用于函数作用域时,即作为局部静态变量时,意味着这个变量是全局的,只会进行一次初始化,不会在每次调用时进行重置,保留这个数值,但只在这个函数内可见。
(3)当用于类的声明时,即静态数据成员和静态成员函数,static表示这些数据和函数是所有类对象共享的一种属性,而非每个类对象独有。
explicit
1. 禁止隐式转换,仅允许显式转换
class String { public: explicit String(const char* str); // 使用 explicit 禁止隐式转换
// ...
};
void printString(const String& s) { std::cout << s << std::endl; } int main() { printString("Hello"); // 错误:无法隐式转换
printString(String("Hello")); // 正确:显式转换
printString(static_cast<String>("Hello")); // 正确:显式转换
return 0; }
引用
性质:相当于一个变量的别名,一个int * const ref = &a
必须引用一块合法内存空间(变量),不能是常量,对初始化后的引用不能更改,只能赋值
int& ref=10; 错误,不能引用常量
但加const后,编译器修改代码为int tmp=10;int& ref=tmp;
const int & ref = 10; 正确
引用可做函数返回值
1.不要返回局部变量引用
int &test(){
int a=10;
return a;
}
int main()
{
int &ref= test();
cout<<ref;//第一次正常输出,因为编译器做了保留
cout<<ref;//第二次是乱值,a的内存已被释放
}
2.函数调用可作为左值
int &test()
{
static int a = 10;//静态变量存放在全局区,在程序运行结束后才释放
return a;
}
int main()
{
int &ref=test();
cout<<ref<<endl; //结果为10
test() = 1000;
cout<<ref<<endl;//结果为1000
}
常量引用
作用:修饰形参,仿制误操作
struct和class区别:class默认私有,struct默认公有
拷贝构造函数
class Person{
Person(const Person& p)拷贝构造函数
{
}
}
在函数调用形参时是值传递按,会调用拷贝构造
调用时机:
- 使用一个已创建完的对象初始化新对象
- 值传递方式给形参传值
- 以值方式返回局部对象
深拷贝浅拷贝
浅拷贝:编译器默认拷贝函数,若堆区申请空间,会直接复制堆区地址,堆区内存会重复释放
深拷贝:自己实现拷贝构造函数,堆区新申请空间
内联函数与宏
- 内联函数有什么作用?存不存在什么缺点?
(1)作用是使编译器在函数调用点上展开函数,可以避免函数调用的开销;
(2)内联函数的缺点是可能造成代码膨胀,尤其是递归的函数,会造成大量内存开销,exe太大,占用CPU资源。此外,内联函数不方便调试,每次修改会重新编译头文件,增加编译时间。
- 内联函数和宏有什么区别,有了宏为什么还需要内联函数?
(1)define宏命令是在预处理阶段对命令进行替换,inline是在编译阶段在函数调用点处直接展开函数,节省了函数调用的开销;
(2)define的话是不会对参数的类型进行检查的,因此会出现类型安全的问题,比如定义一个max命令,但是传递的时候可能会传递一个整数和一个字符串,就会出错,但是内联函数在编译阶段会进行类型检查;
(3)使用宏的时候可能要添加很多括号,比较容易出错。
类对象成员
类对象作为类成员,先构造类成员
Class A{
}
Class B{
A a;
}
友元
全局函数做友元
Class Person{
friend void test();
private : int x;
}
void test()
{
cout<<x;
}
类做友元
Class Person{
friend void test();
private : int x;
}
void test()
{
cout<<x;
}
模板
模板不会编译,在调用时实例化一份函数代码
所以不能在一个文件中定义模板,另一个文件调用文件中的模板
所以模板都是放在头文件中,在源文件中include头文件
具体化模板
template<typename T>
bool compare(T a, T b)//T类型可能是个复杂的类,不能直接比较
{
if (a>b)
return true;
else return false;
}
//利用具体化Person的版本实现
template<> bool compare(Person p1, Person p2)
{
if(p1.age>p2.age)
return true;
else return false;
}
特例化模板
不是编译器提供,而是开发者提供
template<typename T>
bool compare(T a,T b)
{
return a>b;
}
//告诉编译器进行指定类型的模板实例化
template bool compare<int>(int,int);
//提供const char*类型特例化
template<>
bool compare<const char*>(const char* a,const char* b)
{
return strcmp(a,b)>0;
}
bool compare(const char* a,const char* b)
{
return strcmp(a,b)>0;
}
int main()
{
compare("aaa","bbb");//编译器优先使用普通函数,若没有普通函数,则调用特例化模板
}
非类型模板
模板的非类型参数,必须是整形或地址,必须是常量,只能使用
template<typename T,int SIZE>
void func(T *array)
{
...
}
类模板与继承
子类继承的父类是一个类模板时,子类声明时要指出父类模板类型。如果子类想灵活,子类也需是类模板
实现vector
空间配置器allocator做四件事:
内存开辟、内存释放、对象构造、对象析构
核心功能:分离了对象的内存开辟和构造,分离了内存的释放和析构
构造析构应该只针对有效元素,而不是一次把开辟空间全部构造或析构。因此不应该使用new和delete对元素管理
两级allocator:
一级:malloc/free
二级:内存池实现
类型转换???
多线程
1.创建启动线程
std::thread定义一个线程对象,传入线程函数和参数,线程自动开启
2.子线程如何结束
子线程函数运行完成,线程结束
3.主线程如何处理子线程
t.join()主线程等待子线程t结束,当前线程继续往下运行
detach()分离线程,主线程结束所以线程就结束
#include<thread>
#include<mutex>
using namespace std;
std::mutex mtx;//全局互斥锁
void threadHandle1()
{
cout<<"thread1 processing"<<endl;
}
int main()
{
//创建一个线程对象,传入线程函数,新线程开始运行
thread t1(threadHandle1);
//主线程等待子线程结束,主线程继续往下运行。若主线程先结束程序会出问题
t1.join();
//把子线程设置为分离线程
t1.detach();
}
lock_gurad封装mutex锁
lock_guardstd::mutex lock(mtx);//出了作用域析构自动释放锁,但不能拷贝构造和复制重载
unique_lock
CAS
互斥锁比较重,临界区若比较复杂,使用cas保证++,--操作的原子特性。称无锁操作
thread_local
thread_local
是 C++11 引入的一个 存储类型说明符,用于声明一个变量在每个线程中都有自己独立的副本。
- 多个线程访问同一个
thread_local
变量时,每个线程都会看到自己的一份 - 它就像是为每个线程自动复制了一份变量
- 线程之间互不干扰,线程退出时会自动销毁
STL
栈stack
栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。
从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。
我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。
deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
我们也可以指定vector为栈的底层实现,初始化语句如下:
std::stack<int, std::vector<int> > third; // 使用vector为底层容器的栈
vecotr
初始化
1.默认初始化
创建空的 vector,后续通过push_back()
或emplace_back()
添加元素。
2.指定大小初始化
// 创建包含4个元素的vector,初始值均为100
std::vector<int> v1(4, 100); // [100, 100, 100, 100]
3.从其他容器复制
// 1. 从数组复制
int arr[] = {1, 2, 3, 4, 5};
std::vector<int> v1(arr, arr + 5); // [1, 2, 3, 4, 5]
// 2. 从另一个vector复制
std::vector<int> v2 = {10, 20, 30}; std::vector<int> v3(v2); // 深拷贝:[10, 20, 30] // 3. 从迭代器范围复制
std::vector<int> v4(v2.begin(), v2.end());
// [10, 20, 30]
std::vector<int> v5(v2.begin(), v2.begin() + 2); // [10, 20]
4.使用assign方法
动态分配新内容并替换现有内容。
```cpp
std::vector<int> v1;
// 1. 从初始化列表分配
v1.assign({1, 2, 3, 4}); // [1, 2, 3, 4]
// 2. 分配n个相同值
v1.assign(3, 100); // [100, 100, 100]
// 3. 从迭代器范围分配
std::vector<int> v2 = {5, 6, 7};
v1.assign(v2.begin(), v2.end()); // [5, 6, 7]
vector如何手动释放内存
【参考资料】https://www.cnblogs.com/summerRQ/articles/2407974.html
比如有一个 **vector<int>nums , 比较hack的一种方式是nums = {},这样既可以清空元素还会释放内存(从一个大佬身上学来的);规范的做法是,vector<int>().swap(nums)或者nums.swap(vector<int> ())。
如何拷贝一段数据
(1)C++语法:std::copy(src.begin(), src.end(), dest.begin());
std::copy_n(src.begin(), int n, dest.begin());
(2)C风格语法:memcpy(void* dest, const void* src, size_t num)