c++ 学习笔记
阅读以下书籍的笔记
《c++ primer》
《c++新经典》
《effective c++》
《现代c++语言核心特性解析》
《effective modern c++》
参考
编程指北
阿秀的学习笔记
数据类型
基本内置类型:算术类型(整型,布尔,浮点等)和空类型
如何选择类型:
- 明知不可能为负,选无符号
- 整型使用int,超过这个范围选long long
- 浮点数选double
volatile
用于告诉编译器某个变量可能会在程序的执行过程中被意外地修改,从而禁止某些优化
如果一个变量被volatile修饰,编译器不会将其保存在寄存器中,每次都去内存中访问这个值
// 模拟一个硬件寄存器
volatile int* hardwareRegister = reinterpret_cast<volatile int*>(0x1000);
int main() {
int value = *hardwareRegister; // 从硬件寄存器读取值
std::cout << "Initial value: " << value << std::endl;
// 模拟其他线程或外部设备可能修改硬件寄存器的值
*hardwareRegister = 42;
value = *hardwareRegister; // 重新从硬件寄存器读取值
std::cout << "Updated value: " << value << std::endl;
}
数据对齐
alignof可以得到数据类型的对齐长度字节alignof(int)
,另外规定了std::max_align_t是所有标量类型的对齐字节长度
alignas声明结构体或结构体中成员变量的对齐字节长度
// 使用alignas(16)指定了AlignedData结构体的对齐要求为16字节
struct alignas(16) AlignedData {
char a;
int b;
char c;
};
17使得new接受std::align_val_t类型的参数,也能根据对齐长度分配对象,编译器会自动从类型对齐字节长度的属性获取这个参数
初始化
初始化与赋值不是同一个概念,初始化为对象创建时赋予其一个初始值,赋值为用一个新值代替对象当前值。
内置类型的变量未被显示化时,会有默认值进行默认初始化。
变量初始化四种方式,带花括号的赋值方式为列表赋值,当出现可能丢失数据情况时不会执行,如给整型一个double类型的数,造成的数据丢失
int a=0;
int b(0);
int c={0};
int d{0}
复合类型:引用,指针等
引用:为对象起另一个名字
int a=10;
int &b=a;//b就是a另一个名字
指针:存放一个对象的地址(通过&得到该对象地址,通过*得到指针所指对象,这里&与*是在表达式中,是运算,和定义指针和引用的符号含义不同)void* 可以存放任何类型的指针
与引用相同点:实现了对一个对象的间接访问
与引用不同点:指针本身是一个对象,可以先后指向不同对象。指针无需在定义时赋值,有默认初始化nullptr。
由于NULL即可以表示为空指针,又是0,11引入nullptr专门表示空指针
左值引用与右值引用
左值与右值
c++表达式分为左值和右值。右值是取不到地址的表达式,左值是能取到地址的表达式。左值关注内存的位置,右值关注内存中的内容。
c中左值位于赋值语句左侧,右值位于右侧。这与c++不同,如c++中常量为左值,但不能在赋值语句的左侧
右值引用和左值引用
很多情况,对象拷贝后原对象就销毁了,使用对象移动可以提升性能,为了支持移动操作,引入右值引用(通过&&获取右值的引用),与左值引用&相对
虽然不能通过&&获取左值的引用,但可以使用move()显式将左值引用转为右值引用
使用move虽然可以避免拷贝,但move并不总是高效,如string的sso优化(字符串比较短不从堆上分配空间,从栈上分配)
int i = 2;
int& z1 = i;
int&& y1 = i;//不对
int& z2 = i * 2;//不对
int&& y2 = i * 2;
int&& y3 = move(i);
动态内存与智能指针
对象都有生命周期:
栈对象:仅当其所在程序块运行时才存在
static对象:使用前分配,程序结束后销毁
堆对象:动态分配内存,随时建立和销毁
动态分配释放内存内存使用new和delete关键字
string *s1 = new string("wowowow");
delete s1;
当内存耗尽,分配内存失败时不抛出异常int *p = new (nothrow) int
定位new:传入一个指针对象的实参,new只用这个地址构造对象不分配内存int *p = new (地址指针) int
智能指针
为了更安全使用动态内存,出现了智能指针。智能指针:shared_ptr unique_ptr weak_ptr定义在头文件memory中,智能指针也是模板,自动管理释放内存,所有指针销毁时,所管理的对象也销毁
- shared_ptr允许多个指针指向同一个对象,make_shared返回一个T类型的shared_ptr指针
shared_ptr<int> ptr = make_shared<int>(12);
- unique_ptr只能一个指针指向一个对象,只能使用new直接初始化
unique_ptr<int> u1(new int(18));
- weak_ptr绑定到shared_ptr而不会增加它的引用计数。expired()是weak_ptr的成员函数,用于判断所指向的对象是否已经被销毁。如果返回true,则表示对象已被销毁;如果返回false,则表示对象仍然存在。lock()是weak_ptr的成员函数,用于获取一个有效的shared_ptr指向所指向的对象。如果对象已被销毁或者weak_ptr没有指向任何对象,则返回一个空的shared_ptr。
可以使用get()返回智能指针锁保存的原始指针,但如果智能指针释放了,这个指针也失效了
原始指针与智能指针混用存在风险T* x; func(shared_ptr<T>(x));//这时x为悬空指针
不能把get得到的指针再给另一个智能指针
可能有循环引用的错误
RAII以对象管理资源
智能指针就是这样一种方式
原因:申请内存后,需要手动delete,否则造成内存泄漏
使用RAII方式,在构造函数中申请资源,析构函数中释放资源
但是RAII对象可能被复制,可以禁止复制,可以引用计数,当引用为0时再释放资源
为什么推荐make_shared创建智能指针
智能指针除了有一块管理的内存,还有一块控制块。使用make_shared创建可以保证两个块都创建,且位于连续位置,一方面避免只创建一个块引发错误,一方面提升效率
动态数组
并不是数组,只是一个数组中元素类型的指针int *p = new int[3]
释放动态数组delete []p
unique_ptr支持管理动态数组,shared_ptr不支持,必须自定义删除器shared_ptr<int> p(new int[5],[](int *p){delete[] p;})
allocator类
分配和销毁内存,将内存分配和对象构造分离
allocator<string> alloc;
string* const p = alloc.allocate(5);
string* q = p;
string a;
while (cin>>a && q!=p+5)
{
alloc.construct(q++,a);
}
while (q != p)
{
alloc.destroy(--q);
}
alloc.deallocate(p, 5);
常量
通过const限定符修饰,必须初始化
const int a = get_num(); //运行时初始化
const int b = 20;//编译时初始化
指向常量类型的指针
const int a = 10;
const int b = 20;
const int* c = &a;
c=&b;
*c = a; //报错,指向的是常量,常量不能变
常量类型的指针
const int a = 10;
const int b = 20;
const int *const d = &a;
d =&b; //这里报错,指针是常量,不能给常量赋值
常量表达式:值不会改变并且在编译阶段就能得到计算结果的表达式
const int a=10; //是
const int c=a+1; //是
const int b=size(); //不是
int 的=10;//不是
可以将常量表达式用constexpr声明,便于编译器验证
constexpr int a=10;
constexpr int c=a+1;
constexpr int b=size(); //size()必须是一个constexpr的函数
constexpr仅对指针有效
constexpr int *c=nullptr;//常量类型的指针
const int *d=nullptr;//指向整数常量的指针
constexpr int e=nullptr;//指向整数的常量指针
尽可能使用const,防止一些值被错误修改
constexpr与const区别:constexpr声明的变量和函数在编译时就能够被求值,因此可以用于一些需要在编译期进行常量计算的场景。而const仅表示对象在初始化后不可修改,但并不要求编译器在编译时就计算出它的值。
枚举类型
使用枚举类代替枚举类型
enum class Color{red}代替enum Color{red}
因为枚举类型会污染{}外的变量,并且枚举类型是强类型,不会隐式转化为整数类型
enum class T{}; //限定作用域,在作用域外不可访问,强枚举类型的枚举标识不会隐式转换为整型
enum T{}; //不限定作用域
数组
数组的特性:数组不能被拷贝
字符数组
char a[]="abc";//数组的维度是4,a[3]得到字符串结束的空字符
char b[3]="abc";//错误
数组与指针
int *ptr[10];//含有10个整数指针的数组
int (*ptr)[10]=&arr;//指向含有10个整数的数组的指针
sizeof
数组维度的声明必须是一个常量表达式,sizeof的返回值就是一个常量表达式
char a[] = "abc";
constexpr size_t num = sizeof(a) / sizeof(*a);
int b[num];
类型别名
为类型起别名,提高可读性
typedef int zhengxing;
zhengxing a=10;
using zhengxing=int;
zhengxing a=10;
typedef int *zhengxing;
int b=10;
const zhengxing a=&b;//这里定义了指向整型的常量指针,而不是指向常量整型的指针
起别名优先使用using,不要使用typedef。因为using在给函数指针和模板起别名时更方便
类型转换
隐式转换
3+'a'//a转换为int
int *ip=a//a是个数组,数组转换为指针
while(cin>>s)//类类型定义的转换,cin自动转换为布尔值
显式转换
(int)3.2//c语言风格
int(3.2)//函数风格
c++的四种强制转换的方式有:static_cast,dynamic_cast,const_cast,reinterpret_cast
static_cast:可以转换除const的转换,静态转换与c原有的转换类似,可以子类转为父类
dynamic_cast:与static_cast不同之处为static_cast只转换,不进行类型检查,可以父类转为子类
const_cast:只能去除const性质,意味着设计缺陷
reinterpret_cast:对底层比特重新解释数据,随便转,有危险
const int *a;
int *b = const_cast<int*>(a);
运行时类型识别RTTI
对于一个基类指针,即使指向一个派生类,也无法调用派生了的函数。除非利用虚函数,让派生类重写。这是推荐的做法。但有时必须自己管理类型,可以用到dynamic_cast和typeid(返回表达式的类型)
RTTI使得在运行时获取类型信息成为可能,可以在某些情况下方便地进行对象类型的判断和类型转换
#include <iostream>
#include <typeinfo>
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base {
public:
void print() {
std::cout << "Derived class" << std::endl;
}
};
int main() {
Base* basePtr = new Derived();
// 使用dynamic_cast检查指针指向的对象是否为Derived类型
if (Derived* derivedPtr = dynamic_cast<Derived*>(basePtr)) {
derivedPtr->print();
} else {
std::cout << "Object is not of type Derived" << std::endl;
}
// 使用typeid获取对象的类型信息
std::cout << "Object type: " << typeid(*basePtr).name() << std::endl;
delete basePtr;
}
自动推断类型
auto类型说明符,让编译器根据初始值推断变量类型
int a=10;
int b=10;
auto c= a+b;
int i = 0;
int &r = i;
auto a = r; //a推导出int
const int i = 1;
const int &j = i;
auto b = i; //b推导出int
auto d = j; //d推导出int
auto e = &i; //e推导出指向int常量的指针
auto &f = i; //f推导出int常量的引用
返回类型后置
当返回类型是函数指针时,以前通过起别名
typedef int(*f)(int)
f func()
现在使用返回类型后置
auto func()->int(*)(int)
decltype返回操作数的数据类型
int a=10;
decltype(a)x=20;
递增递减
递增和递减运算符有前置版本和后置版本,优先使用前置版
不同处:优先级
int i=0;
int j=++i;//j=1
int j=i++;//j=0
不同处:运行过程
前置版本直接加一后返回改变了的对象
后置版本会将原始值存储下来,再得到新的修改的值,如果不需要再返回原始的值,就会造成存储的浪费
递增递减运算符优先级大于解运算符,*ptr++相当于 *(ptr++)
控制语句
for范围的遍历for(const auto &i:x){}//const auto &相比auto防止调用复制构造函数
因为其内部是右值引用,所以如果x是左值会造成错误
20引入for(y=x;auto &i:x)//先复制再遍历
17使if与switch的判断条件前可以添加一个初始化语句
函数
局部变量:定义在函数内部的变量
局部静态变量:函数第一次调用时初始化,生命周期贯穿函数调用及之后
int MyFnc()
{
static int a = 0;
a++;
return a;
}
int main()
{
int b=MyFnc();
cout << b << std::endl;//1
b = MyFnc();
cout << b << std::endl;//2
b = MyFnc();
cout << b << std::endl;//3
}
参数传递:传值或传引用
如果函数无需改变引用形参的值,最好将其声明为常量引用
还可以使用引用形参返回额外信息,如下的int &c
bool MyFnc(const string& a, const string& b, int & c)
{
c = a.size();
return a.size() < b.size();
}
为函数传递数组时,实际传递的为指向数组首元素的指针
如果函数不改变形参的值,最好将其声明为常量引用
含有可变形参的函数
如果形参数量位置不同,但类型相同,使用initializer_list,传值使用花括号
void MyFnc(int num,initializer_list<string> s)
{
cout << "共有" << num << "个" << endl;
for (auto b = s.begin(); b != s.end(); ++b)
{
cout << *b << endl;
}
}
int main()
{
MyFnc(3, { "abc","def","hij" });
MyFnc(2, { "abc","def" });
}
函数返回
不要返回局部对象或引用的指针,因为函数结束后,局部变量的引用的地址将被释放掉
以下函数的试图返回一个局部变量的引用,是错误的
string &MyFnc()
{
string a = "abc";
return a;
}
数组不能被拷贝,所以不能被函数返回,但可以返回数组指针,三种方法
int(*MyFnc1())[2]
auto MyFnc2()->int(*)[2] // 尾置返回
using arrPtr = int[2];
arrPtr* MyFnc3()
17引入结构化绑定,可以接收多个返回值
auto func()
{
return std::make_tuple(1,2);
}
auto [x,y]=func();//x=1,y=2
正确返回新对象还是新对象的引用
//在栈上分配,函数返回后a实际已经被销毁了
A& fun()
{
A a();
return a;
}
//在堆上分配,但必须之后delete
A& fun()
{
A* a = new A();
return *a;
}
有时必须返回一个新对象的正确写法,ROV返回值优化(在函数返回值的额内存中构造局部变量,可以避免拷贝)
A fun()
{
return A();
}
函数重载:函数名称相同,但是形参列表不同
不允许除返回类型外其它所有要素都相同
函数指针:指向函数的指针,定义时只要将函数名换为指针就好
int MyFnc(int num)
{
return num;
}
int main()
{
int (*p) (int num) = &MyFnc;
int r = p(2);
int a = (*p)(3);
cout << r << a << endl;
}
内联函数
使用关键字inline声明,在调用处内联展开,避免函数调用开销
不要随意inline
原因:inline固然可以加快调用函数(因为它是一种编译期的替换)但也会造成生成的二进制文件膨胀
并且定义在类中的函数也是隐式的inline
尽量只inline小的函数
constexpr函数
能够用于常量表达式的函数,函数体内必须有return,并不要求返回常量表达式
constexpr size_t scale(size_t cnt){return cnt;}
// scale(2)是常量表达式
// int i = 2; scale(i)不是常量表达式
lambda表达式
可调用对象:函数,函数指针,重载了函数调用运算符的类(重载了()),lambda表达式
原理
lambda使用了函数对象(仿函数)的原理:函数对象是一个类,类重载了(),使得其可以像函数一样被调用,lambda由编译器自动生成一个对象
lambda:必须包含捕获列表和函数体:[捕获列表](参数列表)->返回类型{函数体}
捕获的是函数中局部变量的拷贝,也能使用&捕获引用。如果在lambda中要修改捕获的拷贝值,可以使用mutable关键字修饰
返回类型可以自动推断,但有些情况不可,如使用了if
vector<int> v1 = { 1,2,3,1,2,2,5,6,7,8,8,0,0 };
int tmp = 4;
sort(v1.begin(), v1.end(), [tmp](int a,int b) {return a-tmp > b; });
几点注意:
- 捕获的值不能是全局变量或静态局部变量(可以通过参数传入或直接使用)
- 捕获的值默认变为常量,不能更改,除非设为mutable
- 通过&,可以捕获引用,不使用值引用则捕获的值是复制了一份
- 捕获的值在lambda定义时已经确定,不会随之后的修改而更改
- [this]捕获this指针,[=]捕获表达式所在域的全部变量的值,包括this指针,[&]捕获表达式所在域的全部变量的引用
14引入广义捕获,使得可以捕获表达式并自定义捕获变量名,14引入泛型lambda表达式,通过auto实现,如
[](auto a){return a}
20添加模板对lambda的支持[]<typename>(T t){}
,20允许无状态lambda表达式构造与赋值
标准库定义的函数对象
在functional头文件中,每个类表示一个算术,关系,逻辑运算符
如进行大于比较
sort(v1.begin(), v1.end(), greater<string>());
小于有less
function类型存储可调用对象,以存储int(int,int)类型的函数为例
int add(int a, int b)
{
return a + b;
}
int devide(int a, int b)
{
return a / b;
}
int sub(int a, int b)
{
return a - b;
}
int main()
{
function<int(int, int)> f1 = add;
function<int(int, int)> f2 = [](int a, int b) {return a * b; };
cout << f1(3, 4);
map<string, function<int(int, int)>> FunctionMap;
FunctionMap = { {"+",add},{"/",devide} };
FunctionMap.insert({ "*",f2 });
FunctionMap["-"] = sub;
cout << FunctionMap["*"](2, 3);
cout << FunctionMap["-"](4, 2);
}
参数绑定
bind函数定义在头文件functional中,接受一个可调用对象,生成一个新的可调用对象
主要为了解决这样一种情况:比如要传入一个只传入一个参数的可调用对象,但现在有一个要传入三个参数的可调用对象,就可用bind解决
_2为占位符,接受新的调用对象的传来的参数,数字为顺序,所以最后执行的为4+3+5-6。占位符定义在std::placeholders名称空间中。
如果想传入的引是用,不能使用&,使用ref函数传引用
using namespace std;
using namespace std::placeholders;
int add(int a, int b, int c, int d)
{
return a + b + c - d;
}
int main()
{
int c = 5;
int d = 6;
auto AddBind = bind(add, _2, _1, c, d);
cout << AddBind(3, 4);
}
类
sturct 与class都可以定义类,但是struct默认访问权限是public类型
构造函数
下面四个与类名相同的函数People是重载的四个构造函数,如果没有任何构造函数,会自动添加默认构造函数
- 第一个使用default和默认构造函数相同,都会赋予类中成员默认值
- 第二个初始化列表来初始化:在参数后使用:name(n),是使用括号中的n初始化name。实际的初始化顺序按照类中成员出现的顺序初始化,与这里面的顺序无关
- 第三个与第二个区别是,第二个是初始化,第三个相当于使用默认值初始化后再在花括号中赋值
- 第四个为委托构造函数,:后委托第三个构造函数进行类的初始化
类中几个特殊的函数
一个类通过这5种函数来控制对象的拷贝,移动,赋值和销毁,这些函数如果没有定义,会自动添加(如果定义了拷贝构造或拷贝赋值或析构,不会生成移动构造和移动赋值。如果定义了移动构造和移动赋值,拷贝构造和拷贝赋值被定义为删除)
- 拷贝构造函数
T(const T &t){}
- 拷贝赋值运算符函数
T& operator=(const T&){};
- 移动构造函数
T(T&& s) noexcept{} //不会抛出异常
- 移动赋值运算符函数
T& operator=(T&&){};
- 析构函数:当一个对象被销毁时,自动调用析构函数,释放对象资源,销毁非static数据成员
成员函数声明和定义
声明只能在类内
可以在类内定义如SayHello
可以在类外定义如run,run还是一个常量成员函数,在参数后加了关键字const
如果想在常量成员函数中也想要修改某个数据成员,可以在变量前加关键字mutable,如mutable int TotalWords
class People
{
public:
People()=default;
People(string n) :name(n) {};
People(int a, string n)
{
this->age = a;
this->name = n;
}
People(int a) :People(a, "佚名") {};
void SayHello() const
{
this->TotalWords++;
cout << "Hello,I'm " + name << endl;
}
void run(int d);
private:
int age;
string name;
mutable int TotalWords;
};
void People::run(int d)
{
cout << "我要跑" << d << "公里" << endl;
}
int main()
{
People my = People(18, "young");
my.SayHello();
my.run(18);
}
有时用非成员函数替换成员函数,可以提供更好的弹性和封装性
namespace space{
class A
{
void func1();
void func2();
//需要一个函数完成上两个功能,不推荐
void func3()
{
func1();
func2();
}
}
}
//推荐
namespace space{
void func3()
{
func1();
func2();
}
}
类的静态成员
与类相关,不与类的对象关联,使用static关键字
静态成员存在于任何对象之外,由所有对象共享
17以前类的非常量静态变量的声明和定义必须分开,17可以使用inline进行定义
访问控制
public:公开,可以被所有访问
protected:只能自己和派生类或友元访问
private:只能自己访问
成员变量最好声明为private
如果想让其它类或函数访问类的非公开成员,可以在类中以关键字friend定义友元。友元的友元不是友元,没有传递性。
派生类对继承来的成员的访问权限受基类中成员的访问符说明影响,也受派生时的访问说明符影响,如class People :public annimal
在继承时加入public关键字的访问说明符
继承时的访问说明符是控制派生类的用户对基类的访问
访问类的指针的成员
->将解引用与访问成员操作结合在一起
(*i).empty();//先解引用i,再访问对象的empty()成员
i->empty();//与上效果相同
类类型转换
类的构造函数将其它转换为类,同样也可以定义类型转换运算符,将该类转换为其它类
下面是一个含有类型转换符的类
SmallInt i = 10;
使用构造函数,将int转换为SmallInt对象
int a = i;
使用隐式的类转化运算符,将SmallInt转为int
隐式的类转化运算符不能向其传递参数,不能指定返回类型
但是一般使用显式的类转化运算符,即在定义前加explict关键字
如果加关键字explicit声明构造函数,那么对象构造必须是显式
class SmallInt
{
public:
SmallInt(int i = 0) :val(i) {};
operator int()const
{
return val;
}
private:
int val;
};
int main()
{
SmallInt i = 10;
int a = i;//如果没有定义operator int()这一句报错
}
显式的区别
class SmallInt
{
public:
SmallInt(int i = 0) :val(i) {};
explicit operator int()const
{
return val;
}
private:
int val;
};
int main()
{
SmallInt i = 10;
int a = int(i);//int a = i;就会报错
}
运算符函数
运算符函数是有特殊名字的函数,或是类的成员,或者至少含有一个类类型的参数
重载运算符函数相当于重载一个特殊的函数,重载的运算符可以定义为一个类的成员,或普通的函数
重载输入输出运算符只能定义为非成员函数
重载()后就可以像使用函数一样使用类的对象
struct People
{
int age;
string name;
};
ostream& operator<<(ostream& os, const People& p)
{
os << p.name << '\t' << p.age;
return os;
}
istream& operator>>(istream& in, People& p)
{
in >> p.age>>p.name;
return in;
}
int main()
{
People p1;
cin >> p1;
cout << p1;//如果没有重载<<和>>,这条语句就不正确
}
嵌套类
被嵌套的类就像外层类的成员一样,受访问限制符的影响
联合类union
可以有多个数据成员,但任意时刻智只能有一个数据成员有值,用于节省空间
位域
每个位域成员都使用unsigned int类型并指定了位数为1
使用位域,底层的类型必须是整型或枚举类型
struct Flags {
unsigned int isActivated: 1;
unsigned int isPaused: 1;
unsigned int isStopped: 1;
};
面向对象OOP
核心思想是:数据抽象,继承和动态绑定
基类
定义共同拥有的成员
派生类
派生类继承基类,要覆盖,或者叫重写基类中的虚函数,并在函数参数列表后加上override关键字
派生类的构造函数要调用基类中的构造函数,构造继承自基类的成员,另外再构造自己的成员
派生类的成员会隐藏基类的同名成员
区分重写、重载和隐藏
重写:派生类对基类虚函数的重写覆盖
重载:一个类中包含多个函数名相同,参数不同的函数
隐藏:如果派生类函数与基类函数(除了虚函数,这个是重写)的函数名相同(参数可以不同)。可以通过using关键字引入
虚函数
基类希望各派生类定义自己的版本
含有虚函数的类。析构函数也要加上virtual关键字
含有虚函数的类,会在编译阶段生成一个用于指向虚函数表的指针和虚函数表,虚函数表中包含所有虚函数的地址,当这个类实例化时,在构造函数中会让虚函数表指针指向虚函数表。
子类继承父类时,会建立自己的虚函数表,如果重写了虚函数,虚函数表中虚函数的地址会替换为自己的地址
虚析构函数
因为一个基类的指针可能指向派生类,所以要将析构函数定义为虚析构函数,以便释放指向派生类的基类指针,否则释放时会造成局部释放
class base
{
public:
base();
~base();
};
class Abase():public base{};
class Bbase():public base{};
// 如果得到一个指向派生类的基类指针
base* ptr;
// 释放时会造成局部释放,只会调用基类的析构函数
delete ptr
抽象基类
存在的目的就是为了产生“类”,包含纯虚函数的类。抽象基类不能实例化,纯虚函数以``函数=0;```形式出现,可以有定义,但只能在类外定义
防止继承
如果不想类被继承,在类名后加final关键字
动态绑定
使用同一段代码处理派生类和基类
运行时动态绑定:使用一个基类的引用(或指针)调用虚函数时将发生动态绑定,会根据实际对象类型调用函数版本
class annimal
{
public:
annimal() = default;
annimal(string n):name(n){}
void introduce() { cout << "i'm " + name; }
virtual void sleep() { cout << "睡觉……"; }
virtual ~annimal()=default;
protected:
string name;
};
class People :public annimal
{
public:
People() = default;
People(string n,int a):annimal(n),age(a){}//要实现annimal构造函数
void sleep() override{ cout << name+"躺着睡觉……"; }
private:
int age;
};
int main()
{
People p("人类",19);
annimal *a = &p;
(*a).sleep();//实际调用people的sleep
}
派生类的复制构造函数应该保证复制了基类的部分
class base
{
base(const base& rhs);
base& operator=(const base& rhs);
}
class drived : public base
{
drived(const drived& rhs):base(rhs){};
drived& operator=(const drived& rhs)
{
base::operator=(rhs);
return *this;
}
}
多态机制
运行期多态和编译器多态是两种不同的多态机制。
运行期多态是通过虚函数和动态绑定(dynamic binding)实现的。在运行期多态中,基类的指针或引用可以指向派生类对象,并根据实际对象的类型来决定调用哪个函数实现。这种多态性在运行时确定。
编译器多态是通过函数重载和模板实现的。在编译期多态中,函数或模板根据不同的参数类型进行适配和选择,编译器在编译时确定使用哪个函数或模板实例。这种多态性在编译期确定。
void print(int num) {
std::cout << "Printing an integer: " << num << std::endl;
}
void print(double num) {
std::cout << "Printing a double: " << num << std::endl;
}
template <typename T>
void printTemplate(T value) {
std::cout << "Printing a value: " << value << std::endl;
}
int main() {
print(10); // 编译器根据参数类型选择合适的重载函数
print(3.14); // 编译器根据参数类型选择合适的重载函数
printTemplate(20); // 根据实参类型推导模板参数T并实例化函数
printTemplate(3.14); // 根据实参类型推导模板参数T并实例化函数
return 0;
}
对象切片
容器存放类与派生类时,要采用间接存储的方式
将派生类对象存储在容器中时,如果容器的类型是基类类型,并且我们通过值传递(而非指针或引用),那么派生类对象会被切片成基类对象,派生类特有的成员将会丢失,这可能导致错误。
在示例中,Derived类继承自Base类,并重写了foo()函数。然后,我们将Derived对象存储在std::vector<Base>
容器中,由于对象切片的发生,容器中实际存储的是Base对象的副本。在循环中调用foo()函数时,由于对象切片,将调用基类的函数而不是派生类的函数。同时,我们无法调用派生类特有的函数bar()。
为了避免对象切片的问题,我们通常使用指针或引用来存储派生类对象的地址,以保留派生类的特性。例如,可以使用std::vector<Base*>
或std::vector<std::shared_ptr<Base>>
来存储指向派生类对象的指针或智能指针。这样可以确保在使用容器中的对象时保持多态性。
class Base {
public:
virtual void foo() {
std::cout << "Base::foo()" << std::endl;
}
};
class Derived : public Base {
public:
void foo() override {
std::cout << "Derived::foo()" << std::endl;
}
void bar() {
std::cout << "Derived::bar()" << std::endl;
}
};
int main() {
std::vector<Base> container;
Derived derived;
container.push_back(derived); // 对象切片发生在这里
for (const auto& obj : container) {
obj.foo(); // 输出 "Base::foo()",而不是 "Derived::foo()"
// obj.bar(); // 错误,Base类没有bar()函数
}
}
多重继承
一个类可以继承多个类
虚继承:表示共享重复的基类class Derived: virtual Base{}
模板与泛型编程
模板:c++既有类模板又有函数模板,vector就是类模板,模板是编译器生成类或函数的说明,编译器根据模板创建类或函数的过程称为实例化。
非类型模板参数,传入的a必须是常量表达式template<int a>
类型模板参数,typename可以换为class,但一般使用typenametemplate<typename T>
模板函数
类型参数
template <typename T>
void AFunction(T i)
{
cout << typeid(i).name() << endl;
}
int main()
{
AFunction<int>(12);
}
非类型参数,即一个值,而不是一个类型,在模板实例化时由编译器推断得出。非类型参数必须是常量
template <unsigned N>
void AFunction(const char (&a)[N])
{
cout << N<< endl;
}
int main()
{
AFunction("hhhh");
}
可以为模板提供默认参数
template<typename T,typename F=less<T>>
int compare(const T& v1, const T& v2, F f = F())
{
if (f(v1, v2)) return -1;
if (f(v2, v1)) return 1;
return 0;
}
int main()
{
cout << compare<int>(10, 20);
}
类模板
定义
template <typename T> class 类名
类模板中函数的定义
template <typename T>
返回类型 类名<T>::函数
因为模板在使用时才会实例化。如果实例出现在多个文件中,会有额外开销。所以可以只在一个文件中进行显式实例化定义,在另一个文件中直接使用实例化声明,即在定义前加关键字extern,防止相同实例出现在多个文件中
使用模板类的类型成员
使用如下函数时,无法区分T::value_type是使用类的静态成员还是表示类型,所以前面要加typename
template<typename T>
typename T::value_tye top(){}
从 左值(右值)引用函数参数 推断 类型
template<typename T> void f1(T&) // 实参必须是左值
template<typename T> void f2(const T&) // 可以接受值
template<typename T> void f3(T&&) // 实参接受右值
引用折叠
当int i = 1; f3(i)
中f3接收了一个左值发生引用折叠,T被推导为int&
引用折叠规律是:只有右值引用与右值引用折叠时才为右值引用
T&&和auto&&可以引用左值和右值,称为完美引用(通用引用)
引用折叠出现在模板实例化,auto类型生成,tydef,decltype的使用中
完美转发
某些函数需要将一个或多个实参转发给其它函数,有时需要保持所有实参的性质(const,左值,还是右值),使用forward
bar(a)的调用将实例化为bar<int&>(int& x),其中x的类型是int&(引用折叠)。然后,std::forward
void foo(int& x) {
std::cout << "foo(int&): " << x << std::endl;
}
void foo(int&& x) {
std::cout << "foo(int&&): " << x << std::endl;
}
template <typename T>
void bar(T&& x) {
foo(std::forward<T>(x));
}
int main() {
int a = 42;
bar(a); // 调用 foo(int&)
bar(123); // 调用 foo(int&&)
}
模板全特化
模板所有类型模板参数都用具体类型替代
函数模板的全特化等价于实例化一个函数模板,不等价于一个函数重载
template <typename T,typename U>
class A
template<>
class A<int,int>
模板偏特化
只有类模板才能偏特化
模板参数数量上的偏特化
template <typename T,typename U>
class A
template<typename T>
class A<T,int>
模板参数范围上的偏特化
template <typename T>
class A
template<typename T>
class A<T*>
标准库的IO类
IO类型
cout和cin是标准库之一iostream定义的标准输入输出对象。
标准库定义的所有名字都在命名空间std中,所以通过std::显示说明使用命名空间std中的名字。
endl保证所有输出写入到输出流中,避免留在内存中引起错误。
cin跳过空格,换行符,制表符等空白字符
#include <iostream>
int main()
{
std::cout << "输入一个数字"<<std::endl;
int a;
std::cin >> a;
std::cout << "这个数字是"<<a << std::endl;
}
头文件iostream中定义了输入流类型istream(cin是一个istream对象),输出流类型ostream(cout是一个ostream对象),读写流类型iostream
头文件fstream和sstream中定义了继承iostream类的类型,用于文件,和内存中string类型的读取
流的状态
当流发生错误时,定义的一些标志会被标记,并且标准库提供了查询管理状态的函数。如定义为整数,输入为字符串时,会发生错误,failbit会被标记,fail()查询是否发生了该错误
int a;
cin >> a;
cout << cin.rdstate()<<'\n'<<cin.fail() << endl;
cout<<1<<endl;//换行后再刷新缓冲区
cout<<1<<flush;//刷新缓冲区
cout<<1<<ends;//输出一个空字符再刷新缓冲区
文件读取三个类型
ifstream 读
ofstream 写
fstream 读写
ifstream in;
in.open("C:/Users/DELL/Desktop/link.txt");
//ifstream in("C:/Users/DELL/Desktop/link.txt");等于上面两条语句
cout << in.is_open() << endl;//判断文件是否被打开
string s;
while (getline(in, s))//读取文件一行
{
cout << s << endl;
}
in.close();
ofstream out("C:/Users/DELL/Desktop/link.txt",ofstream::app);//打开文件可以指定mode,这里使用app追加的模式写入数据
out.close();
输入输出格式化
在输出中输出boolalpha,oct(只针对整型)等可以控制bool,进制的输出
cout.precision()指定浮点数的精度
cin>>noskipws读入空白符而不是跳过他们
所以采用while(file>>ch)读取文件时,可以改变其遇到空白就停止的特性
单字节读入函数
char ch;
cin.get(ch);
cout.put(ch);
int ch;
cin.get(ch);
cout.put(ch);
多字节读入函数
get,getline,read,write
标准库类型string
相当于只能装char的容器
初始化string
#include <iostream>
#include <string>
using std::string;
using std::cout;
int main()
{
string s11 = "abcdefg";//拷贝初始化
string s22(10, 'c');//直接初始化
string s1 = "我是谁";
char a1[] = { 'a','b','c' };
string s2(a1, 2);//ab s2是数组a1前2个拷贝
string s3(s1, 4);//谁 s3是字符串s1从4个字符后的拷贝,中文占两个字符
string s4(s1, 2, 4);//是谁 字符串从2个字符开始4个字符的拷贝
string s5=s1.substr(0,2);//我 与上相同,返回部分的拷贝
}
范围for语句访问字符串中每个字符,通过下标访问字符串中每个字符
string s1 = "abcdefg";
for (auto c :s1)
{
cout << c <<std::endl;
}
string s1 = "abcdefg";
for (decltype(s1.size()) i=0;i<s1.size();i++)
{
cout << s1[i]<<std::endl;
}
修改string
string s1 = "我是谁";
s1.replace(0, 2, "他们");
//将我换为他们
搜索string,使用find一类函数,返回的是无符号的size_type类型位置
string s1 = "我是谁";
auto a = s1.find("是");//2
比较函数,等于,大于,小于返回0,正数,负数
string s1 = "abc";
auto q=s1.compare("bcd");
数字转字符串,字符串转数字
int s1 = 123;
string s2 = to_string(s1);
double s3 = stod(s2);//stoi stol等
C语言有头文件ctype.h用于处理字符串,在C++中为cctype
string转为C风格的字符串:c_str()
标准库类型vector等
c++容器分为顺序容器和关联容器
顺序容器中元素按在容器中的位置保存和访问
关联容器中元素按关键字保存和访问
顺序容器
vector是一种顺序容器,顺序容器其它种类如下,所有容器都定义在与他同名的头文件中:
- vector 可变大小数组
- array 固定大小数组
- string 与vector类似,只是用于保存字符
- deque 双端队列
- list 双向列表
- forward_list 单向列表
顺序容器操作
//初始化
vector<int> v11(10,1); //10个元素每个都是1
vector<int> v22{10,1}; //2个元素分别为10,1
vector<string> v1;
vector<string> v2 = { "我","你","它" };
vector<string> v3{ "qwe","asd","zxc" };
v1 = v2;
vector<string> v4(v3);
vector<string> v5(v2.begin(),v2.end()--);
vector<string> v6(10, "la");//仅顺序容器
//交换
v1.assign(3, "换");//仅顺序容器(除array)将容器中3个值换为“换”
v1.swap(v6);//将容器v1与容器v6的值进行交换
//插入数据
//push_back在尾部插入
//push_front在头部插入,只针对特定数据结构的容器,如列表队列
//insert指定位置插入,同时返回插入位置的引用
//使用emplace_back可以构造元素而不是拷贝元素
v1.push_back("尾");
list<string> v7 = { "1","2" };
v7.push_front("头");
v1.insert(v1.begin(), "头");
auto p=v1.insert(v1.begin(),2, "头tou");
cout << *p;
//取数据
//取数据可以使用下标,使用迭代,使用front back得到头尾数据的拷贝或引用
string a = v1[1];
string b = v1.front();
string &c = v1.back();
cout << c;
//清除数据
//erase(迭代器)清除指定位置,删除后会导致之后的迭代器失效,所以谨慎使用
//clear清除所有
v1.pop_back();
v7.pop_front();
v1.erase(v1.begin());
v1.clear();
//调整大小
v1.resize(20);
// 获取容量
capacity()
// 获取容器内元素个数
size()
// reserve(n) 分配至少能容纳n个元素的内存空间
关联容器
按关键字有序保存的容器(使用红黑树)
- map
- set
- multimap:关键字可重复
- multiset:关键字可重复
按关键字无序保存的容器(使用哈希组织)
- unordered_map
- unordered_set
- unordered_multimap:关键字可重复
- unordered_multiset:关键字可重复
map<string, int> WordCount;
string word;
while (cin >> word)
{
++WordCount[word];
if (word == "e")
{
break;
}
}
for (auto i : WordCount)
{
cout << i.first << '\t' << i.second << '\n' << endl;//first得到键值,second得到值
}
按关键字有序保存的容器的关键字必须定义元素比较的方法,默认采用<比较
按关键字无序保存的容器使用哈希函数将元素映射到一个桶中,桶中存一个或多个函数
容器适配器:stack,queue,priority_queue
使用一种容器来初始化适配器stack,queue等,使他们的操作像stack栈,queue队列。
默认情况stack,queue使用deque,priority_queue使用vector,可以在创建时指定
stack<int,vector<int>> s
迭代器
迭代器种类:插入迭代器,流迭代器,反向迭代器,移动迭代器
迭代器相当于容器内置的“指针”,字符串和容器都可以使用迭代器
begin得到字符串第一个元素位置,end得到字符串最后一个元素的下一个位置,++得到下一个元素 *i得到迭代器所指位置的元素,rbegin可以得到反向的迭代器(cbegin与cend得到const迭代器)
string s1 = "abcdefg";
for (auto i=s1.begin();i!=s1.end();i++)
{
*i = toupper(*i);//toupper为大写转换函数
}
cout << s1<<std::endl;
迭代器定义
vector<int>::iterator i;//定义能读写vector<int>中元素的迭代器
vector<int>::const_iterator j;//定义只能读vector<int>中元素的迭代器
插入迭代器
接受一个容器,生成一个迭代器,实现向容器添加元素
list<int> v1 = { 1,2,3,4 };
list <int> v2, v3, v4;
copy(v1.begin(), v1.end(), front_inserter(v2));//4321
copy(v1.begin(), v1.end(), back_inserter(v3));//1234
copy(v1.begin(), v1.end(), inserter(v3, ++v3.begin()));//11234234
流迭代器输入输出操作
list<int> v1 ;
istream_iterator<int> in(cin);
istream_iterator<int> e;
while (in != e)
{
v1.push_back(*(in++));
}
ostream_iterator<int> out(cout);
for (auto i : v1)
{
*(out++) = i;
}
cout<<endl;
泛型算法
算法指实现了排序搜索等经典算法接口,泛型指适用于多种类型,包括容器类型
定义在头文件algorithm与numeric中
算法是不操作容器的,基于迭代器
int ar[] = { 10,23,44,56 };
int *r = find(begin(ar), end(ar), 4);
cout << (r == end(ar) ? "不存在" : "存在");
容器写操作
vector<int> v1 = { 1,2,3 };
//替换
//所有替换为0
fill(v1.begin(), v1.end(), 0);
//所有0替换为22
replace(v1.begin(), v1.end(), 0, 22);
//插入
//使用插入迭代器,在末尾添加两个33
fill_n(back_inserter(v1), 2, 33);
//拷贝
int ar[] = { 10,23,44,56 };
int ar2[sizeof(ar) / sizeof(*ar)];
auto ret = copy(begin(ar), end(ar), ar2);//指向ar2尾
容器排序去重
vector<int> v1 = { 1,2,3,1,2,2,5,6,7,8,8,0,0 };
sort(v1.begin(),v1.end());
auto e = unique(v1.begin(), v1.end());//返回不重复部分后一个位置的迭代器
v1.erase(e, v1.end());
for (auto i : v1)
{
cout << i << '\t';
}
头文件
预处理,编译前执行的一段程序,确保头文件多次包含仍能安全工作
#ifdef 判断变量是否已经定义,当且仅当变量定义时为真,直到遇到#endif停止
#define 把一个变量设置为预处理变量
#ifndef 判断变量是否尚未定义
也可使用使用#pragam once避免同一头文件被包含多次
使用C++版本的C标准库头文件
C的头文件形如name.h,C++将其改为为cname,且这些文件中的名字从属于命名空间std
命名空间
每个命名空间都是一个作用域
namespace A{
int i;
}
void f(){
using namespace A;
//可以使用A空间中的i
}
异常
异常安全
l.lock();
//如果这中间抛出异常,那么unlock就永远不会执行
l.unlock();
异常安全需要满足三个保证之一
- 基本保证:异常被抛出,程序内任何对象和数据结构都不会被破坏,而内部前后一致
- 强保证:函数成功就是完全成功,函数失败就要回到调用函数之前
- 承诺不抛出异常
需要以对象管理资源
需要提供上述三种保证之一,强保证可以通过copy and swap实现(即先复制对象,修改后,再和原对象交换)
调试
assert用于检查程序中运行时的错误,需要诊断的表达式返回true触发,如除数不能为零,分配的内存不能为零等
静态断言static_assert,在编译时进行检,输入两个参数:常量表达式和诊断消息字符串,当表达式为fasle触发。17可以不输入第二个参数
调试宏
20引入一组用于测试编译环境对功能特性支持程度的宏,如对属性,对特性,对标准库功能特性等,如__has_cpp_attribute(属性)测试编译环境是否支持某种属性
11确定标准可变参数宏__VA_ARGS__,主要用于日志的打印中
#define LOG(msg,...) printf("["__FILE__":%d]"msg,__LINE__ __VA_OPT__(,) __VA_ARGS__)//20引入__VA_OPT__,这里表示逗号可选,只有可变参数不为空时才替换出来
LOG("hello");
LOG(("hello %d", 2022);
属性
[[属性]]
- 11引入noreturn,声明函数不会返回
[[noreturn]] void foo(){}
- 11引入carries_dependency
- 14引入deprecated,表明实体被弃用
- 17引入fallthrough,消除switch中直落行为的警告,声明在case或default语句之前
- 17引入nodiscard,声明函数的返回值不应该被舍弃
- 17引入maybe_unused,对未被使用的参数发出警告
- 20引入likely,unlikely,以指示编译器哪个分支更有可能被执行,从而帮助编译器进行更好的优化
- 20引入no_unique_address,表示其不需要唯一内存地址,主要用于无状态类
使用C语言代码
extern "C"{
}
导出c++函数到C语言
extern "C" int f(){}
协程
20引入了协程。协程允许程序在一个执行过程中暂停并在稍后的时间点继续执行,而无需阻塞线程或创建回调函数。
协程函数:使用co_await关键字和co_return语句的函数被称为协程函数。co_await会触发一个挂起点,执行流程会返回调用者继续执行,同时异步执行co_await等待的对象,在等待对象执行完毕后,(co_return设置返回值的结果,并触发挂起点的恢复)挂起点恢复执行流程继续执行后续代码
co_yield也会触发挂起点,执行流程返回调用者继续执行,下次再调用时会从co_yield触发的挂起点继续执行,有以上三个关键字的函数就是协程
#include <iostream>
#include <coroutine>
// 定义一个简单的协程函数
// 返回一个异步计算结果
std::coroutine_handle<> computeAsync(int* result) {
std::cout << "Start async computation..." << std::endl;
// 模拟异步操作
co_await std::suspend_always{};
*result = 42;
std::cout << "Async computation completed." << std::endl;
// 协程完成后返回
co_return;
}
int main() {
int result = 0;
auto coroutine = computeAsync(&result);
coroutine.resume(); // 启动协程
std::cout << "Result: " << result << std::endl;
return 0;
}
co_await实际上通过三个函数实现
await_ready:判断可等待体是否已经完成,完成返回true,否则false
await_suspend:调度协程的执行流程,会接收协程的句柄(包含协程的上下文信息)
await_resume:接收异步执行结果
co_yield和co_return基于promise_type类型
promise_type是一个嵌套类型,包含下列函数
get_return_object:创建一个对象并返回给调用者
initial_suspend与final_suspend:可以在两个函数中编写协程执行前后的逻辑,返回必须是一个等待器(suspend_always或suspend_never表示必然挂起和从不挂起)
yield_value:保存co_yield操作数并返回等待器
return_void:用于实现没有co_return的情况
对应构造函数执行协程恢复流程
协程原理
协程是可以暂停和恢复的函数,实现一个协程的关键点在于如何保存、恢复和切换上下文(协程调度)
协程如何调度(三种方式)
- 返回调度器
- 返回调度者
- 直接恢复到另一个协程
协程如何保存恢复上下文
有栈协程:在堆上提前分配一块较大的内存空间栈,这个栈用起来跟真正的线程栈差不多的,让系统认为这个堆上空间就是普通的栈,实现了上下文的保存恢复。go就是有栈协程方案
无栈协程:只用一个专门的结构体保存用到的局部变量和状态。c++就是无栈协程方案
但是无栈需要编译器支持,有栈只需要编写同一套上下文切换的代码,而无栈没有编译器支持就必须手写切换的部分(因为这些部分是跟执行的程序逻辑有关的,例如状态机转换),不管是对实现者还是使用者都不够友好。而且如果你想把一些老代码用协程跑起来,通常移植到有栈协程上比无栈也要相对简单,现在主流的无栈协程基本都需要进行侵入式的修改,比如要加asnyc await等关键字标记等等,而像Go的协程直接hook系统调用,你在go程里阻塞等于让出协程执行权,这样可以几乎不用修改就能用协程并发跑一些老的,没有使用协程的同步阻塞代码。所以有栈方案没有被无栈干掉,而是两者都有人用。(来自知乎的回答)
多进程、多线程、线程池、IO多路复用和协程都是为了提高程序的并发性能和提高系统资源的利用率
- 多进程:每个进程有自己的独立地址空间,但创建、销毁和切换进程的成本较高
- 多线程:线程是进程的执行单元,同一个进程的多个线程共享地址空间,创建、销毁和切换线程的成本较低,需要处理好线程间的数据同步问题
- 线程池:线程池是预先创建一定数量的线程,用于处理并发任务,可以避免频繁地创建和销毁线程,提高系统性能
- IO多路复用:IO多路复用可以让一个线程处理多个IO操作,提高了线程的利用率,适合IO密集型的应用。但IO多路复用的编程复杂度较高
- 协程:自由地在函数内部保存和恢复执行状态,适合处理大量的并发任务。协程的优点是编程模型简单,性能高效