文章目录
- 1.函数模板
- 2.类模板
- 3.ifndef的用法(可以用#pragma once代替)
- 4.顺序容器
- 5.vector
- 6.迭代器
- 7.关联容器
- 8.map
- 9.set
- 10.visual studio debug技巧
- 11.static关键字
- 12.函数重载
- 13.一些小技巧
- 14.虚函数
- 15.纯虚函数
- 16.可见性
- 17.string
- 18.字符串字面量
- 19.const
- 20.成员初始化列表
- 21.三元运算符
- 22.对象创建在栈上还是在堆场?
- 23.explicit
- 24.运算符重载
- 25.this关键字
- 26.作用域指针
- 27.智能指针
- 28.拷贝构造函数
- 29.箭头操作符
- 30.c++的stdvector使用优化
- 31.c++使用库(静态库和动态库)
- 32.iota算法:
- 33.strtok(分割字符串)
- 34.c++处理多返回值问题
- 35.c++模板(meta programming元编程)
- 36.堆与栈
- 37.宏
- 38.auto关键字
- 39.c++函数指针
- 40.lambda表达式
- 41.c++名称空间
- 42.c++线程
- 43.c++计时
- 43.多维数组
- 44.std::sort
- (外传):优先队列
- 45.c++类型双关
- 46.c++的联合体
- 47.c++虚析构函数
- 48.结构体与类的对比
- 49.c++类型转换
- (外传):vsual studio操作断点
- (外传):why use smart ptr
- 50.c++预编译头文件(pch)
- 51.基准测试
- 52.结构化绑定(structured bindings)
- 53.处理OPTIONAL数据
- 54.单一变量存放多种类型数据
- 55.单一变量存放任意类型数据
- 56.async
- 57.文件读取
- 58.string_view(让c++字符串运行更快)
- 59.单例模式
- 60.小字符串(SSO)优化
- 61.追踪内存分配
- 62.左值和右值
- 63.参数计算顺序(argument evaluation order)
- 64.移动语义
- 65.std::move和移动赋值操作符
- 66.sizeof关键字与strlen函数的区别
- 67.c++运行流程
- 68.STL 中常见的容器
- 70.vector 和 list 的区别
- 71.迭代器失效原因,有哪些情况
- 72.红黑树的特性,为什么要有红黑树
- 73.unordered_map 实现原理
- 74.map,unordered_map 的区别
- 75.makefile和Cmake
- 76.git使用
- 77.const和宏定义的区别
1.函数模板
函数模板会直接识别函数里的泛型T,无需指定
template<typename T>
bool isEqual(T s1,T s2)
{
return s1 == s2;
}
//函数模板特化
template<>
bool isEqual<char*> (char * s1, char* s2)
{
}
2.类模板
需要在使用的时候指定泛型的类型
类模板特化
template<>
class pair<char>
{
public:
pair();
char ssss;
};
pair<char>::pair()
{
cout << "ss" << endl;
}
类模板偏特化
tempate<typename T>
class pair<char,T>
{
public:
pair();
T ssss;
}
template<typename T>
pair<char,T>::pair()
{
}
3.ifndef的用法(可以用#pragma once代替)
作用:主要是用于防止头文件被重复包含和编译
语句:
#ifndef 标识1
#define 标识1
#endif 标识1
不用会导致的问题:假如你有一个C源文件,它包含了多个头文件,比如头文件A和头文件B,头文件B又包含了头文件A,则最终该源文件包含了两次头文件A。如果你在头文件A里定义了结构体或者类类型(这是最常见的情况),那么问题来了,编译时会报大量的重复定义错误。
4.顺序容器
vector:数组,支持快速随机访问
list:链表,支持快速插入删除
deque:队列,支持对首尾元素的快速增删,和随机访问
5.vector
vector::push_back
vector::pop_back
vector::operator[]
vector::sort(重载了运算符小于号(<),或者自行指定了比较函数)
bool stuCmp(Student s1,Student s2)
{
return s1.getId() < s2.getId();
}
vector<Student> stu;
sort(stu.begin(),stu.end(),stuCmp);
6.迭代器
1.迭代器是一种对象,用来遍历标准数据库里的元素,每个迭代器对象代表容器种元素的确定地址。
2.迭代器提供了一些类似于指针的运算符:*,++,->,!=,==
3.迭代器.end()指向的是末尾元素的下个元素
eg:
for(vector<Student>::iterator it = stu.begin(); it < stu.end() ; it++)
{
it->introduce();
const Student& rst = it;
rst.introduce();
}
4.迭代器失效:就是当容器内的元素被删掉了,但是迭代器指向被删的元素也会正常进行,不会报错!!!!!!!!!!!!!!!
7.关联容器
关联容器内部存储元素的形式大多数都是以哈希表或者二叉树等非线性的数据结构进行存储。
特性:增删元素效率较低,查找速度比线性表快。
种类:
set 容器中存储的元素作为键,键不能重复。
map 容器中除了键外,每个键还有对应的值,键不能重复,但是值可以
multiset 支持一个键多次出现的set
multimap 支持一个键多次出现的map
8.map
map的key必须是可以比较的,不然会报错,如果不可比较就需要把比较函数通过函数指针的形式传进去,
eg:
typedef bool (func_t)(const & Student,const& Student);
bool stdcmp(const& Student s1,const & Student s2)
{
return s1.getId() < s2.getId();
}
map<Student,double,fuc_t> s(stdcmp);
map可以通过下标的方式修改value,下标里的内容是key(形式[key] = value)
9.set
set的key也必须是可以比较的,不然会报错,如果不可比较的类型就需要重载<运算符或者把比较函数通过函数指针的形式传进去
eg:
set<Student,func_t> stus(stdcmp);
find函数是用来查找这个值有没有的,如果没有则返回迭代器的结尾,如果有返回迭代器指针。
eg:
auto it = stus.find(stu);
if(it == stus.end())
{
cout << "no result" << endl;
}
else
{
cout << it->introduce();
}
erase函数也是通过迭代器输入来进行删除的!
10.visual studio debug技巧
1)通过设置断点,在执行到断点的时候右键该行然后选择disassembly视图.
11.static关键字
static在类外和结构体外定义的时候,代表的是这些函数和变量在与实际定义的符号链接时链接器只会在本翻译单元的作用域内寻找该变量或者函数的符号定义只会在本翻译单元内进行,比如:
main.cpp
static int s = 5;
int main(void)
{
cout << s << endl;
}
static.cpp
static int s = 10;(这是对的)
main.cpp
int s = 5;
int main(void)
{
cout << s << endl;
}
static.cpp
static int s = 10;(这也是对的)
main.cpp
int s = 5;
int main(void)
{
cout << s << endl;
}
static.cpp
int s = 10;(这是错的)
main.cpp
extern int s;
int main(void)
{
cout << s << endl;
}
static.cpp
int s = 10;(这是对的)
main.cpp
extern int s;
int main(void)
{
cout << s << endl;
}
static.cpp
static int s = 10;(这是错的)【类似于定义了一个私有变量】
第二种:static修饰类内变量和函数的时候,是作为一个类内的静态变量或静态方法,并且这个静态变量一定要在类外定义!!!!!!可以不初始化,但是一定要定义,eg:int A::a;
第三种:static修饰函数内的变量,这种被称为局部静态,这种变量的作用是为了该变量能实现某种功能并且外部不能修改该变量。
局部静态意味着生命周期是整个程序的运行周期,但是作用域(可以访问这个变量的区域)是仅仅在函数内。
eg:比如说我们需要一个函数来实现输出从1到10,这时我可以定义一个全局变量,然后再函数内实现++,然后在main中不断的调用该函数,但是如果在main中修改了这个变量的值,那么就达不到我们想要的效果 ||https://www.bilibili.com/video/BV1tT411b7xE?p=24&vd_source=f95b5b5a9e388ffe1a5a7ce2f0272be4
#include <iostream>
void Functional()
{
static int i = 0;
i++;
std::cout << i << std::endl;
}
int main(void)
{
for(int i = 0 ; i < 5 ; i++)
Functional();
return 0;
}
另一种应用环境是单例类
复杂的实现方法是:[饿汉版的单例模式]
class Singleton
{
private:
static Singleton* s_instance;
public:
static Singleton& Get() { return *s_instance;}
void Hello(){}
};
Singleton* Singleton::s_instance = nullptr;
int main(void)
{
Singleton::Get().Hello();
}
简单的实现方法:[懒汉版单例模式]
class Singleton
{
public:
static Singleton& Get()
{
static Singleton s;
return s;
}
};
12.函数重载
函数重载是指相同函数名,但是参数不同的不同版本。
13.一些小技巧
eg:如果你不希望该类被实例化,那么可以把构造函数设置为private或者删除默认构造函数
class Log{
private:
Log() {}
public:(或者)
Log() = delete;
}
ASCII可以拓展为UTF-8,UTF-16,UTF-32
14.虚函数
1.虚函数其实只是为了子类重写(override)基类的函数的时候,基类指针仍然可以调用正确的重写函数,当我们重写函数的时候,建议在(c++11)函数后面写上override这样可以防止我们出错,但是虚函数并不是没有开销的,一共有两方面开销,一方面虚函数表需要额外的内存空间,并且在基类里需要一个成员指针来指向虚函数表,另一方面在我们调用虚函数的时候,我们需要遍历虚函数表来找到正确的函数来进行调用(但是这个开销好像并没有想想的那么大)
虚函数或虚方法是一种可继承或可重载(override)的函数或方法,用来实现动态绑定。动态绑定是面向对象编程方式的多态特性中一个非常重要的概念,简而言之,一个虚函数定义了一个将要执行的目标函数,但在编译时期并不知所要执行的目标函数,需要在运行时根据对象的属性确定(绑定)目标函数。
一旦一个类从虚拟基类派生或者直接从基类继承的虚函数,编译器就会在类对象中生成一个指针。这个就是虚拟指针vptr,它指向一个虚表vtable,编译器会将虚拟指针代码添加到构造函数中对虚拟指针进行初始化,并同样将该部分代码添加到析构函数中,用以删除该虚拟指针。
虚表vtable包含如下三个部分:
- 虚拟函数动态绑定信息
- 相对于虚拟基类的子对象和虚拟表顶部的偏移量
- 对象运行时类型信息(RTTI)
你在对象中调用的函数在编译时就已经确定,并且它们是不可变的,因此虚表vtable在编译时就已经建立。每个虚函数都在虚表中分配了一个固定位置,该位置在整个类继承过程中保持不变。
接下来我们看看在继承关系中,虚拟表的关系,虚拟表是理解与掌握虚拟函数使用的钥匙。
在派生类的虚拟表中包含了指向基类虚函数的指针,如果覆盖了基类的虚函数,则指针指向该类的虚函数。
如果派生类中添加了自己的虚函数,它们将被添加到虚拟表的基函数之后。
1、纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
2、虚函数声明如下:virtual ReturnType FunctionName(Parameter); 虚函数必须实现,如果不实现,编译器将报错,错误提示为:
error LNK****: unresolved external symbol “public: virtual void __thiscall ClassName::virtualFunctionName(void)”
3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
4、实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
5、虚函数是C++中用于实现多态(polymorphism)的机制。 核心理念就是通过基类访问派生类定义的函数。
6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。
析构函数作为虚函数的作用
析构函数为虚函数的场景
虚析构函数的作用是为了防止内存泄漏
1.内存泄漏的场景
用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。
2.解决内存泄漏的方式
a.用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构
b.使用虚析构函数
构造函数不能是虚函数,原因如下:
- 虚函数对应一个虚指针,虚指针其实是存储在对象的内存空间的。如果构造函数是虚函数,就需要通过虚函数表中对应的虚函数指针(编译期间生成属于类)来调用,可对象目前还没有实例化,也即是还没有内存空间,何来的虚指针,所以构造函数不能是虚函数;
- 虚函数的作用在于通过父类的指针或者引用来调用它的成员函数的时候,能够根据动态类型来调用子类相应的成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,所以构造函数不能是虚函数;
2.virtual关键字的虚继承可以解决菱形继承的问题比如A类有一个成员变量a,这时B,C都继承于A,D又继承于B,C,这时访问a就会报错,我们可以通过访问B::a,C::a,来解决访问问题,通过查看两个a的地址可以发现,这两个地址是不一样的,这个问题就可以通过虚继承来解决,让B,C都虚继承与A就会发现B::a,C::a的地址是一样的了。
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;//Outputs:00AFFD48(该值随机)
cout << &d.B::a << endl;//Outputs:00AFFD48(该值随机)
cout << &d.C::a << endl;//Outputs:00AFFD48(该值随机)
return 0;
}
15.纯虚函数
纯虚函数允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数。纯虚函数其实就是在基类里去掉虚函数的函数体变成=0。这里的作用范围可以是创建的所有类有一个共同的特性需要查看的时候,我们就可以创建一个接口类,然后让所有的类继承该接口类,然后设置一个以该接口类指针作为输入的函数来查看。
一个纯虚函数或者纯虚方法是需要在它派生类中实现的虚函数,并且该派生类是非抽象类。抽象类是指在一个类中包含纯虚函数,这样的类不能被直接实例化。一个抽象类的子类只有在它所继承的纯虚方法(函数)都被该类或者父类实现时才能实例化。纯虚方法一般只有一个声明而没有定义(实现)。
换句话说,抽象类只能作为基类使用,它的纯虚函数(方法)均由派生类进行实现。抽象类是一种特殊的类,它是为抽象和设计的目的建立。通常位于类继承层次结构的上层,为下面的派生类提供一个通用的接口,仅用来输出不同的结果。而具体的实现与操作由它的子类进行实现。
16.可见性
private证明只能在类内(还有友元)访问,protected是可以在类内和结构内的子类可以访问
17.string
string其实就是一个字符串,本质上就是一个char*。
如果要在一个字符串里查找某个短字符串的话可以用find函数
std::string name = std::string("ztc") + "helloc!";
bool contains = name.find("c") != std::string::npos;(最后这个npos就是一个不存在的位置)
涉及到成本和安全性的问题,所以在传递字符串的时候最好设置成const std::string& string),因为如果不用引用的话,复制字符串其实是需要一个很大的成本在的。
18.字符串字面量
字符串字面量其实就是一个const char[],c14的std::string_literals库里提供了一些让字符串使用更简单的操作符s,L,U,u,R
19.const
const修饰变量的时候代表这个变量变成了一个常量,内容不可修改;
const修饰指针的时候,放在之前,就代表指针指向的内容不可以修改,但是指针可以修改指向;放在之后,就代表指针的指向不可修改,但是指针指向的内容可以随意修改。
eg1:
const int MAX_AGE = 90;
const int * p = new int;
int const* p = new int;
*p = 2;//这里是错误的
eg2:
int * const p = new int;
*p = 2;//(这是正确的)
p = (int*)& MAX_AGE;//(这时错误的)
//const第三个用法是修饰类内的方法,作用是承诺本函数不会修改类成员变量。
class pos{
private:
int m_x,m_y;
public:
int Get() const
{
m_x = 2;//(错误)
return m_x;
}
};
eg3:
class Entity2{
private:
int m_x,m_y;
mutable int var;
public:
int GetX() const{
var = 2;//这里如果我们需要改变成员变量,但是我们又必须声明函数为const,那么我们就需要把变量声明为mutable[关键字]。mutable的用途是能在被const修饰的方法里修改成员变量
return m_x;
}
};
void PrintPos(const Entity2& e)//这里如果把Get的const去掉就会报错,因为编译器不确定Get是否会改变e对象的成员变量
{
std::cout << e.GetX() << endl;
}
int main(void)
{
Entity2 e2;
PrintPos(e2);
}
20.成员初始化列表
要确保成员初始化列表和成员变量声明顺序一致。(!const类型的成员变量以及引用类型的成员变量其实是可以通过初始化列表的方式赋初值的,还有初始化父类私有变量!)
class Entity
{
private:
int x;
std::string name;
public:
Entity():x("0"),name("Unknown") {}
};
21.三元运算符
用三元运算符可以避免构造中间变量,这与返回值优化有关。并且三元运算符可以嵌套!
eg1:
int level = 10;
std::string name = level > 10 ? "Master":"Bigenner";
eg2:
int level = 13;
int speed = level > 5 ? level > 10 ? 15 : 10 : 5;
22.对象创建在栈上还是在堆场?
如果对象比较大,或者需要显式的控制对象的生存周期的话就创建在堆上
23.explicit
这个关键字是用于防止构造函数进行隐式转换的,当构造函数被explicit修饰的时候,这个构造函数就不能再被隐式的形式使用了。
class Entity{
private:
std::string m_name;
public:
explicit Entity(std::string Name):m_name(name) {}
};
int main(void)
{
Entity a = "ztc";//这是错误的(如果没有explicit就是正确的)
Entity b("ztc");//这是正确的
}
24.运算符重载
对于运算符两侧都是相同类型的变量,那么运算符的重载是可以写在类内的,但是如果是重载输出流这类的就需要把重载写在类外。
eg:
struct Vector2{
float x,y;
Vector2(float x1,float x2):x(x1),y(x2){}
Vector2 Add(const Vector2& other) const
{
return Vector2(x + other.x,y + other.y);
}
Vector2 operator+(const Vector2& other) const{
return Add(other);
}
Vector2 Multiply(const Vector2& other) const{
return Vector2(x * other.x , y * other.y);
}
Vector2 operator*(const Vector2& other) const{
return Multiply(other);
}
};
std::ostream& operator<<(std::ostream& stream , const Vector2& other)
{
stream << other.x << "," << other.y;
return stream;
}
int main(void)
{
Vector2 position(1.0f,1.0f);
Vector2 speed1(2.0f,1.4f)
Vector2 powerUp(1.1f,1.1f);
Vector2 res = position + speed1*powerUp;
std::cout << res << std::endl;
return 0;
}
25.this关键字
this关键字从本质上将就是一个const类型的实例指针,可以通过这个指针访问实例的类内的方法和变量。
26.作用域指针
作用域指针实际上就是一个类,一个指针的包装器,在构造时用堆分配指针,然后再析构时删除指针。作用域指针实现的功能是你可以将对象生成在堆上,但是出了作用域,该指针就会被析构,在指针析构的同时对象也会被析构,这样就实现了即在堆上分配了空间,又可以出作用域就销毁。unique_ptr,shared_ptr,weak_ptr
用途:比如说计时器,在作用域开始时创建一个,然后在作用域结束的时候,也就是计时器被析构的时候,就可以知道整个作用域的运行时间;或者是互斥锁,可以在多线程的时候锁定一个函数,防止多个线程访问它的时候爆炸。
27.智能指针
unique_ptr代表这个指针是唯一的,你不能复制unique_ptr,因为unique_ptr在释放的时候会同步释放掉指向的地址,那么如果有两个unique_ptr指向同一个地方,第二个指针就会指向一个被释放的内存。
shared_ptr代表这个指针是可以共享的,你可以拷贝和复制这个指针,这个实现的方式是引用计数,它会统计所有指向该引用的指针,当指针个数为0的时候,那么释放指向的内存
weak_ptr是弱指针,这个可以指向shared_ptr并且不会引起引用计数的增加,而且你可以查询weak_ptr是否过期。
这些智能指针可以很方便的管理内存,如果不想显示的释放内存(delete),那么智能指针是非常有用的,但是也不能完全的替代new和delete
#include<memory>
#include<iostream>
class My{
public:
My(){std::cout << "Construct My!" << std::endl;}
~My(){std::cout << "Destroy My!" << std::endl;}
}
int main(void)
{
{
std::unique_ptr<My> my(new My());
std::unique_ptr<My> my1 = std::make_unique<My>();
std::shared_ptr<My> my2;
{
std::shared_ptr<My> my3 = std::make_shared<My>();
my2 = my3;
std::weak_ptr<My> my4 = my3;
}
}
{
std::weak_ptr<My> my1;
{
std::shared_ptr<My> my = std::make_shared<My>();
my1 = my;//这个例子说明的是弱指针不会增加引用计数
}
}
return 0;
}
28.拷贝构造函数
c++默认提供的拷贝构造函数是浅拷贝,也就是说当类内有的变量是指针的时候,拷贝构造函数只会复制指针的地址,而不会复制一份指针所指向的内容,所以当我们想使用拷贝构造函数的时候,最好是自己去实现一个拷贝构造函数,unique_ptr的唯一性也是通过delete拷贝构造函数实现的。!!!所有函数在实现的时候,最好是传递const引用,因为你在函数参数里传值的时候,默认是调用拷贝构造函数的,这样会导致大量无用的复制,影响性能,const是为了安全性!!!
class MyString{
private:
char* m_buffer;
int m_size;
public:
MyString(const char * string)
{
m_size = strlen(string);
m_buffer = new char[m_size + 1];
memcpy(m_buffer, string, m_size);
m_buffer[m_size] = 0;
}
MyString(const MyString& other):m_size(other.m_size)
{
m_buffer = new char[m_size];
memcpy(m_buffer,other.m_buffer,m_size+1);
}
~MyString()
{
delete[] m_buffer;
}
friend std::ostream& operator<<(std::ostream& stream,MyString string);
char& operator[](unsigned int index)
{
return m_buffer[index];
}
};
std::ostream& operator<<(std::ostream& stream,MyString string)
{
stream << string.m_buffer;
return stream;
}
int main(void)
{
MyString string = "ztc";
MyString second = string;
second[2] = 'a';
std::cout << string << std::endl;
std::cout << second << std::endl;
return 0;
}
29.箭头操作符
从本质上来将箭头操作符其实就是一个指针的逆向引用,一个有趣的操作是你可以通过箭头操作符来获取到变量的字节偏移量->这个偏移量是一个十六进制的数,如果为了方便可以转换成10进制的数。重载箭头操作符必须返回指向类类型(对象)的指针,或者返回定义了自己的箭头操作符的类类型对象。
eg1:
class ScopedPtr{
private:
Entity* e;
public:
ScopedPtr(Entity* e1):e(e1){}
Entity* operator->()
{
return e;
}
void print()
{
std::cout << e->x << std::endl;
}
};
int main(void)
{
ScopedPtr p = new Entity();
p->print();
return 0;
}
eg2:
struct vect3{
float x,z,y,c,v,s,q,a;
};
int main(void)
{
std::cout << &((vect3*)0)->v << std::endl;
return 0;
}
30.c++的stdvector使用优化
vector动态分配的实现方式是通过不断的扩容实现的,每一次扩容都会先将旧的内容复制到新的内容中,再将新内容放进去。
在使用vector之前最好是能够预分配出vector的大小,这样就避免了重新resize导致的赋值,同时如果push_back传入的是对象的话,那么有又出现一次使用push_back函数到vector之间的复制,前一种复制的解决方法是运用reserve函数来预分配大小,后一种复制的解决方法是通过使用emplace_back函数,这个函数的含义是通过vector的结构体的构造函数来创建一个对象,所以需要传入的是一个构造函数的参数列表
emplace_back的vector是自己定义的类或结构体的时候,一定要写一个默认构造函数!!!!!
#include <iostream>
#include <vector>
class vectex{
float x;
float y;
float z;
public:
vectex(float x1,float y1,float z1):x(x1),y(y1),z(z1)
{
}
vectex(const vectex & t):x(t.x),y(t.y),z(t.z)
{
std::cout << "copied!" << std::endl;
}
};
int main() {
std::vector<vectex> tem;
tem.reserve(3);
tem.emplace_back(1,2,3);
tem.emplace_back(4,5,6);
tem.emplace_back(7,8,9);
}
31.c++使用库(静态库和动态库)
静态库和动态库的区别
静态库。在编译阶段被完整地链接到目标程序中,生成一个较大的可执行文件。这意味着静态库中的代码被复制到最终的可执行文件中,每个使用该库的程序都会包含一份库的副本。这有助于确保代码的独立性和完整性,但会占用更多的存储空间。
动态库。在编译阶段不会被链接到目标程序中,而是在程序运行时由操作系统动态加载到内存。动态库通常以.so(Linux/Unix)或.dll(Windows)结尾。这种设计允许多个程序共享同一个库的代码,从而节省内存并提高程序的模块化。但是,使用动态库的程序在运行时需要动态库的存在,并且在更改动态库时,所有引用该库的程序都需要重新编译
使用:静态链接库 预编译的二进制文件 glfw
(1)静态链接:
配置包含文件:(visual studio)
右键点击项目-点击property-c+±general-Additional include Directories-[KaTeX parse error: Undefined control sequence: \Dependencies at position 14: {SolutionDir}\̲D̲e̲p̲e̲n̲d̲e̲n̲c̲i̲e̲s̲\GLFW\include] …{SolutionDir}\Dependencies\GLFW\lib-vc2015]
右键点击项目-点击property-linker-Input-Additional Dependencies-添加glfw3.lib
(2)动态链接:
配置包含文件:(visual studio)
右键点击项目-点击property-c+±general-Additional include Directories-[KaTeX parse error: Undefined control sequence: \Dependencies at position 14: {SolutionDir}\̲D̲e̲p̲e̲n̲d̲e̲n̲c̲i̲e̲s̲\GLFW\include] …{SolutionDir}\Dependencies\GLFW\lib-vc2015]
右键点击项目-点击property-linker-Input-Additional Dependencies-添加glfw3.dll.lib
然后将dll文件放在exe同目录下
(3)创建库,链接库:将两个项目创建到同一个解决方案下,然后进行静态链接配置,最后右键住项目-ADD-Reference选择需要编译成lib的文件就行了
32.iota算法:
iota算法是C++标准库中的一个函数模板,用于填充一个区间。它通过指定一个起始值,并根据区间的长度递增生成后续的值。它有助于快速生成递增的序列。
用途:可以生成一个vector的下标值数组,如果原数组不可移动,那么可以通过将下标排序的办法来将原数组排序。
用例:
std::vector<int> heights;
std::vector<int> names;
//....为上述两个数组赋值
std::vector<int> indexs(names.size());
iota(indexs.begin(),indexs.end(),0);//这样就生成了一个上述两个数组的下标数组,可以通过把indexs排序的办法来排序heights;
33.strtok(分割字符串)
std::string s = "hello world hello";
char* s_c = (char*)s.c_str();
char* token = strtok(s_c," ");
std::vector<std::string> words;
while(token != NULL)
{
std::string tp = token;
words.emplace_back(tp);
token = strtok(NULL," ");
}
34.c++处理多返回值问题
1.可以采用将返回值作为函数参数(引用)来实现。
2.函数返回值可以是用std::tuple或者std::pair。
3.最建议的就是创建一个结构体,这样可以将获取到的值和代表的含义对应起来。
#include<iostream>
struct position{
std::string x;
std::string y;
};
position getResult()
{
return {"ztc","ztc"};
}
int main(void)
{
position tp = getResult();
std::cout << tp.x << " " << tp.y << std::endl;
return 0;
}
35.c++模板(meta programming元编程)
模板其实就是根据一套规则让编译器来帮你写代码。
当模板类函数没有被调用的时候,其实对于编译器来讲这个函数就是不存在的,只有当模板类函数被调用了,这个函数才会被创建
template<typename T> //typename其实就是模板参数的类型,T是name,这个name就是类型名
void Print(T value)
{
std::cout << value << std::endl;
}
int main(void)
{
Print(5);//这里没用Print<int>(5)可以通过编译说明实际上编译器自己推导出了T的类型是int,隐式进行
return 0;
}
template<typename T,int N>
class Array{
private:
T m_Array[N];
public:
int getSize() const{return N;}
};
int main(void)
{
Array<std::string,5> array;
std::cout << array.getSize() << std::endl;
return 0;
}
36.堆与栈
内存分为五区:堆区,栈区,常量区,静态区,代码区
c++的内存分配有三种方式:
从静态存储区分配内存
从静态存储区域分配的内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。静态分配的区域的生命期是整个软件运行期,就是说从软件运行开始到软件终止退出。只有软件终止运行后,这块内存才会被系统回收。
堆分配内存方式:堆上分配一般需要借助new关键字。堆上分配空间不是连续的,例如:p和p_array的地址空间完全没有关系。同时new出来的空间需要自己去delete掉,如果用了智能指针,智能指针会自动delete掉new出来的空间。【需要手动释放内存】【在堆上分配内存是一件比较麻烦的事,需要做一堆操作,例如操作系统需要从空闲表中找到当前空闲的内存块,空间的大小需要大于等于所需的内存大小,然后返回给你一个指针,同时操作系统还需要维护其他的各种表,更新内存表这个内存块已经分配出去了等信息】堆上会有一些cache miss的情况
new关键字:new其实就是调用了malloc函数(memory allocate),malloc会调用底层操作系统的API或者平台提供的特定接口函数
栈分配内存方式:栈上分配不用。栈分配的地址空间是连续的,当我们定义一个新的变量,栈会根据这个变量的size往低地址空间移动size个字节,比如说下面定义了一个a,int类型四个字节,栈顶指针就会想着低地址空间移动四个字节。【不需要手动释放内存,作用域结束,空间自动回收】【栈分配内更像是一条CPU指令】栈上的空间访问时由于大部分都在cache里,所以基本上访问时都不会cache miss
#include <iostream>
struct vector_{
float x,y,z;
};
int main(void)
{
int a = 5;//栈上分配
int array[5];
vector_ vec_;
int * p = new int;
*p = 5;//堆上分配
int* p_array = new int[5];
vector_ * vec__ = new vector_();
return 0;
}
37.宏
预处理器评估所有以#开头的语句,评估没有问题后,将代码交给编译器进行编译,宏进行在预处理阶段,而模板的评估是进行在编译阶段,预处理阶段进行的其实就是查找与替换,将宏和头文件找到相应的位置,然后进行替换,发送给编译器进行编译。【实际上就像是宏改变了文本生成的方式,当被发给编译器时】
visual studio可以通过设置来设置宏,右键项目->属性->Configuration Properties->c/c+±>Preprocessor->Preprocessor Definitions 输入:(debug模式)PR_DEBUG=1 (release模式)PR_RELEASE
#include <iostream>
#if PR_DEBUG == 1
#define Log(x) std::cout << x << std::endl
#elif defined(PR_RELEASE)
#define Log(x)
#endif
int main(void)
{
Log("hello");
return 0;
}
反斜杠可以写多行的宏,(注意反斜杠后面不要加上空格,那代表对空格的转义,确保反斜杠后面直接换行)例如:
#include <iostream>
#define MAIN int main(void)\
{\
std::cout << "hello" << std::endl;\
}
MAIN
38.auto关键字
auto关键字可以自动推导出来类型,基本的应用场景就是迭代器和自定义长类型替换。[还可以有using和typedef来起别名]
class Device {};
class DeviceManager
{
private:
std::unordered_map<std::string,std::vector<Device*>> m_Devices;
public:
const std::unordered_map<std::string,std::vector<Device*>>& GetDevices() const
{
return m_Devices;
}
}
int main(void)
{
using DeviceMap = std::unordered_map<std::string,std::vector<Device*>>;
typedef std::unordered_map<std::string,std::vector<Device*>> DeviceMap;
DeviceManager dm;
const std::unordered_map<std::string,std::vector<Device*>>&devices = dm.getDevices();
auto devices = dm.GetDevices();
DeviceMap devices = dm.GetDevices();
return 0;
}
39.c++函数指针
应用场景:当有一些数据需要处理时,可以将需要处理的数据,以及对每个数据要进行的行为(函数指针)传入函数。
#include <iostream>
#include <vector>
void PrintValue(int value)
{
std::cout << "value: " << value << std::endl;
}
void ForEach(const std::vector<int> values,void(*function)(int))
{
for(auto value:values)
function(value);
return ;
}
int main(void)
{
std::vector<int> values = {3,4,1,5,6,8,7,5};
ForEach(values,PrintValue);
ForEach(values,[](int value) {std::cout << "value::" << value << std::endl;});
return 0;
}
40.lambda表达式
lambda其实就像是一个一次性的小函数,可以给变量赋值
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
void ForEach(const std::vector<int> values,std::function<void(int)> function)
{
for(auto value:values)
function(value);
return ;
}
int main(void)
{
std::vector<int> values = {3,4,1,5,6,8,7,5};
int a = 5;
ForEach(values,[=](int value) mutable { a = 6; std::cout << "value::" << value << std::endl;});//如果想通过值传递的方式修改变量,那么就需要加mutable关键字,如果想把改动同步到作用域外,就需要引用传递
auto it = std::find_if(values.begin(),values.end(),[](int a){return a > 5;});//std::find_if就是找到满足lambda表达式的第一个元素,返回迭代器
for(;it != values.end();++it)
std::cout << *it << std::endl;
return 0;
}
41.c++名称空间
名称空间的主要目的就是避免命名冲突
#include <iostream>
namespace apple{ namespace function{
void print() {std::cout << "apple!" << std::endl;}
}
}
namespace orange{
void print() {std::cout << "orange!" << std::endl;}
}
1.using namespace apple::function;
//2.using namespace orange;
int main(void)
{
//3.namespace a = apple::function;
//4.using orange::print;
print();
return 0;
}
42.c++线程
可以通过this_thread来给当前线程下发命令
#include <iostream>
#include <thread>
static bool is_finish = false;
void DoWork()
{
std::cout << "Start thread id: " << std::this_thread::get_id() << std::endl;
using namespace std::literals::chrono_literals;
while(!is_finish)
{
std::cout << "working..." << std::endl;
std::this_thread::sleep_for(1s);
}
}
int main(void)
{
std::thread worker(DoWork);
std::cin.get();
is_finish = true;
thread.join();
std::cout << "Finished." << std::endl;
std::cout << "Start thread id: " << std::this_thread::get_id() << std::endl;
std::cin.get();
return 0;
}
43.c++计时
在c++11之后有了chrono可以用来计时【chrono是c++库的一部分,不需要去使用操作系统库】(这种库是无关平台的,也有平台的特殊库可以用来计时这种库是无关平台的,也有平台的特殊库可以用来计时),在那之前需要使用操作系统库来计算程序的耗时。
#include <iostream>
#include <chrono>
struct Timer{
std::chrono::duration<float> duration;
std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> start,end;
Timer()
{
start = std::chrono::high_resolution_clock::now();
}
~Timer()
{
end = std::chrono::high_resolution_clock::now();
duration = end - start;
float ms = duration.count()*1000;
std::cout << "Timer took: " << ms << "ms" << std::endl;
}
};
void function()
{
Timer timer;
for(int i = 0 ; i < 100 ; i++)
std::cout << "hello/n"; //std::cout << "hello" << std::endl;
}
int main(void)
{
function();
}
43.多维数组
多维数组其实本质上就是数组的数组,就好像是一个数组的集合。
如下例:当我们在堆上分配了50个int类型的数组,这是其实分配了200个字节的内存,不涉及初始化,当我们分配了一个二维数组,这是其实只是在内存中分配了200个字节的指针,并没有实际分配空间。这里可以举一个例子,比如说如果数组的类型不是int,而是自定义的结构体(这个结构体需要占用200个字节),那么分配50个的话,就需要分配1000个字节的内存,这时再去分配50个一维数组给二维数组,这时其实只是分配了50个指针(指针其实就是个地址,占用4个字节)的内存空间,并没有实际分配。
由于在堆场分配是不连续的,这样就会导致遍历数组去访问内部元素时,会造成cache miss,导致我们浪费时间从ram(内存)中获取数据
#include <iostream>
int main(void)
{
int* p = new int[50];
int** a2d = new int*[50];
for(int i = 0 ; i < 50 ; i++)
a2d[i] = new int[50];
//三维数组--内存分配
int*** a3d = new int**[50];
for(int i = 0 ; i < 50 ; i++)
{
a3d[i] = new int*[50];
for(int j = 0 ; j < 50 ; j++)
{
a3d[i][j] = new int[50];
}
}
初始化
//a3d[0][0][0] = 1;
//由于是在堆上分配的,所以需要手动delete,多维数组的delete是不可以用delete[][][] a3d这种形式去释放,而且如果只释放上层的空间,只是将指向真正空间的指针删掉,是很容易造成内存泄露的。
for(int i = 0 ; i < 50 ; i++)
{
for(int j = 0 ; j < 50 ; j++)
{
delete[] a3d[i][j];
}
}
return 0;
}
44.std::sort
std::sort在未传入比较器时,默认是升序排列的。
c++标准库里的排序函数在元素小于16or5(记不清了)时用的是插入排序,大于那个数值是用快排实现的,这里还有一个问题就是当输入比较器的时候,相同元素的比较返回一定要是false。
当需要传入一定的比较规则来排序的时候,我们可以通过传入一个函数的方式来实现,它既可以是一个结构体内的函数,也可以是一个lambda表达式,也可以是内置函数。
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
int main(void)
{
std::vector<int> values = {1,4,5,2,3};
//1.std::sort(values.begin(),values.end(),std::greater<int>());//从大到小
//2.std::sort(values.begin(),values.end(),[](int a, int b){return a > b;});
//3.//如果我想除了某个元素外其他正常升序排列
/*
int except = 1;
std::sort(values.begin(),values.end(),[&](int a,int b)
{
if(a == x)
return false;
if(b == x)
return true;
return a < b;
})
*/
for(int value : values)
std::cout << value << " ";
std::cout << std::endl;
return 0;
}
(外传):优先队列
优先队列其实就是堆排序。
priority_queue<int,vector<int>,less<int>> q;//(大根堆)
45.c++类型双关
类型双关的意思其实就是绕过类型系统,由于c++是强类型语言,所以c++有自己的类型系统,但是这个类型系统又不想java和c#那样严格。
说白了类型双关就是要把自己拥有的内存,当作不同的类型的内存来看待。
这里强调以下结构体其实是没有任何填充的(如果没有指针什么的,那么结构体完全就可以当作是一个字节数组),但是空结构体是至少有一个字节的,因为我们可能会对这段内存进行寻址.
c++是一种强力语言的关键也在于能够自如的操作内存。
eg1:
#include <iostream>
int main(void)
{
int a = 40;
double value = a;//这里是进行了正常的隐式类型转化,因为40的16进制字节其实是28 00 00 00,但是double的存储方式是不一样的,有正负号,阶等一系列东西,最后才是尾数,所以其实去看value的地址空间,会发现和int的40完全不同
double value2 = *(double*)&a;//这种就是类型双关的结果,这种方式两边的字节流是一致的。
}
eg2:
#include <iostream>
struct Entity{
int x,y;
};
int main(void)
{
Entity entity = {5,8};
int* array = (int*)&entity;
for(int i = 0 ; i < 2 ; i++)
std::cout << array[i] << " ";
std::cout << std::endl;
int y = entity.y;
int y2 = *(int*)((char*)&entity + 4);
std::cout << y2 << std::endl;
return 0;
}
46.c++的联合体
联合体类似于结构体和类,但是它一次只能占用一个成员的内存。通常union是匿名使用的但是匿名union不能有成员函数。union常常用来做类型双关(很容易将一个数据转换成两个不同的变量类型)。
应用场景:当有一个vector(x,y,z)同时想代表颜色RGB。
eg1:
#include <iostream>
int main(void)
{
struct Union{
union{
float a;
int b;
}
};
Union u;
u.a = 2.0f;
std::cout << u.b << " " << u.a << std::endl;
return 0;
}
eg2:
#include <iostream>
struct Vector2{
float x,y;
};
struct Vector4{
//第一种转换成两个Vector2的方法
/*float x,y,a,b;
&Vector2 getA()
{
return *(Vector2*)&x;
}*/
//第二种方法是使用匿名联合体的方法
union{
struct{
float x,y,a,b;
};
struct{
Vector2 m,n;
};
};
};
void PrintVector2(Vector2& v)
{
std::cout << v.x << " " << v.y << std::endl;
}
int main(void)
{
//第二种办法使用方法
Vector4 v4 = {1.0f,2.0f,3.0f,4.0f};
std::cout << v4.x << std::endl;
PrintVector2(v4.m);
PrintVector2(v4.n);
v4.a = 500.0f;
std::cout << "-------------------" << std::endl;
PrintVector2(v4.m);
PrintVector2(v4.n);
return 0;
}
47.c++虚析构函数
主要的作用就是当我们定义一个基类指针指向派生类的时候,这是首先会调用基类的构造函数,然后调用派生类的构造函数,但是在析构的时候,由于程序不知道派生类实现了自己的析构函数,所以会调用基类的析构函数,具体场景如下:
#include <iostream>
class Base
{
public:
Base() { std::cout << "Base Constructor" << std::endl;}
~Base() { std::cout << "Base Destructor" << std::endl;}
};
class Derive: public Base
{
public:
Derive() { std::cout << "Derive Constructor" << std::endl;}
~Derive() { std::cout << "Derive Destructor" << std::endl;}
};
int main(void)
{
Base* base = new Base();
delete base;
std::cout << "-------------------------" << std::endl;
Derive* derive = new Derive();
delete derive;
std::cout << "-------------------------" << std::endl;
Base* derive_base = new Derive();
delete derive_base;
return 0;
}
结果:
这时就需要用到虚析构函数,virtual定义基类析构函数,这样就可以告诉程序:ok可以会有一种在派生类中重写的方法。虚析构函数和其他的虚函数不同,其他的虚函数可以需要在派生类重写,且需要做虚函数表,但是虚析构函数不像是重写, 更像是添加了一个析构函数。也就是说当我们在程序中添加了一个虚析构函数时,其实是调用了两个析构函数,首先调用派生类的析构函数,然后沿层次结构向上调用基类的析构函数。
下面是一个不加虚析构函数导致内存泄露的例子:
#include <iostream>
class Base
{
public:
Base() { std::cout << "Base Constructor" << std::endl;}
~Base() { std::cout << "Base Destructor" << std::endl;}
};
class Derive: public Base
{
public:
Derive() { array = new int[5](); std::cout << "Derive Constructor" << std::endl;}
~Derive() { delete array; std::cout << "Derive Destructor" << std::endl;}
private:
int * array;
};
int main(void)
{
Base* base = new Base();
delete base;
std::cout << "-------------------------" << std::endl;
Derive* derive = new Derive();
delete derive;
std::cout << "-------------------------" << std::endl;
Base* derive_base = new Derive();
delete derive_base;
return 0;
}
解决方法就是在基类析构函数之前加一个virtual。
48.结构体与类的对比
其实本质上结构体与类并没有区别,无论是成员函数的定义实现还是成员变量的声明,唯一的区别就是结构体定义的成员默认是公开的,类定义的成员默认是私有的。
49.c++类型转换
显式类型转换:
c语言风格显式类型转换:
#include <iostream>
int main(void)
{
double a = 5.3;
double value = a + 5.34;
std::cout << value << std::endl;
double value2 = (int)a + 5.34;
std::cout << value2 << std::endl;
return 0;
}
c++风格显示类型转换:
static_cast:静态类型转换,在静态类型转换的情况下,可能还做了其他的编译时检查,看看这种转换是否可行。
reinterpret_cast:意思是把这段内存重新解释成其他的东西。
dynamic_cast:
const_cast:移除或者添加变量的const限定。
1.static_cast
(1)static_cast的使用基本等价于隐式转换的一种类型转化运算符,可使用于需要明确隐式转换的地方。就相当于把隐式转换给明确写了出来而已。
(2)可以用于低风险的转换
什么叫低风险的转化,一般只要编译器能自己进行隐式转换的都是低风险转换,一般平等转换和提升转换都是低风险的转换。比如以下几种情况:
①整形和浮点型
②字符与整形
③转换运算符
④空指针转换为任何目标类型的指针
(3)不可以用于风险较高的转换
①不同类型的指针之间互相转换
②非指针类型和指针类型之间的相互转换
③不同类型的引用之间的转换
#include <iostream>
int main()
{
char c_a = 0;
int i_a = 0;
float f_a = 0;
double d_a = 1.111111;
void* v_ptr = NULL;
int* i_ptr = new int(1);
char* c_ptr = new char(1);
//下面部分没有报错,可以运行,但是平时不允许这样写,除非自己很明确自己在干什么
//从高字节数到低字节数的转换平常肯定是不允许这样用的,因为将一个多字节的内容转换到少字节,非常容易丢失数据
char c_sc = static_cast<char>(i_a);
c_sc = static_cast<char>(f_a);
c_sc = static_cast<char>(d_a);
//类似于下面的转换不允许,因为两个不同的指针类型之间不允许相互转换
//int* i_scptr = static_cast<int*>(c_ptr);//报错
//下面的指针类型转换允许
int* i_scptr = static_cast<int*>(v_ptr);
void* v_scptr = static_cast<void*>(i_ptr);
//下面的可取,只不过有时候精度可能会降低而已,比如float转换为int,被视为低风险
float f_sc = static_cast<float>(i_a);
int i_sc = static_cast<int>(c_a);
cout << i_sc << endl;
return 0;
}
2.reinterpret_cast
这个和C语言的强制转换没什么区别,只不过C++用自己的写法替代了C语言的强制转换而已。
①不同类型的指针之间的转换
②指针和能容纳指针的整数类型之间的转换(比如将int类型强转成int*类型)
③不同类型的引用之间的转换
编译期处理执行的是逐字节复制的操作。
类似于强制转换,至于强制转换会产生什么后果需要自己承担。
由于和C语言的强制转换一样,这里不进行赘述。
#include <iostream>
int main(void)
{
int b = 10;
int* c = reinterpret_cast<int*>(b);
return 0;
}
3.dynamic_cast
dynamic_cast不是在编译时检测,而是在运行时进行检测类型。它是专门用于沿继承层次结构进行的强制类型转换。更多情况下dynamic_cast是用来做验证的。比如玩家类和敌人类都派生于实体类,当我们想要验证一个实体类究竟是玩家还是敌人时,就可以把实体类指针强制转换成其中一类,如果不是这一类,那么dynamic_cast就会返回一个NULL指针。
eg1:
#include <iostream>
class Base
{
public:
Base() {}
virtual ~Base() {}
};
class Derive: public Base
{
public:
Derive() {}
~Derive() {}
};
class AnotherClass: public Base
{
public:
AnotherClass() {}
~AnotherClass() {}
};
int main(void)
{
Derive* derive = new Derive();
Base* base = derive;
AnotherClass* ac = dynamic_cast<AnotherClass*>(base);
if(!ac)
std::cout << "dynamic_cast is not successful!" << std::endl;
Derive* derive2 = dynamic_cast<Derive*>(base);
if(derive2)
std::cout << "dynamic_cast is successful!" << std::endl;
return 0;
}
eg2:
#include <iostream>
class Entity{
virtual void PrintVaule(){}
};
class Player:public Entity{
};
class Enemy:public Entity{
};
int main(void)
{
Entity* actualEnemy = new Enemy();
Entity* actualPlayer = new Player();
Player* p0 = dynamic_cast<Player*>(actualPlayer);
Enemy* p = dynamic_cast<Enemy*>(actualPlayer);
if(p0)
std::cout << "right!is player!" << std::endl;
if(p == NULL)
std::cout << "wrong!is not enemy!" << std::endl;
return 0;
}
那dynamic_cast是如何知道玩家类是玩家类而不是敌人类的?因为它存储了运行时类型信息(runtime type information,RTTI),也正是因为需要记录RTTI,而且也需要比较类型,所以dynamic_cast也会损耗一些性能,浪费一些时间来进行校验。
4.const_cast
(1)const_cast只针对指针、引用,当然,this指针也是其中之一。
(2)const_cast的大部分使用主要是const作用消除。
(3)const_cast只能调节类型限定符,不能修改基本类型。
指针eg:
#include <iostream>
int main(void)
{
const int *p = new int(4);
*const_cast<int*>(p) = 50;
std::cout << *p << std::endl;
//int* s = p;//错误,因为不能从const int*转换成int*
const int * sp = p;
//const_cast可以解决这个问题,消除const
int* ssp = const_cast<int*>(p);
*ssp = 100;
std::cout << *ssp << std::endl;
//char* sssp = const_cast<char*>(p);//错误,不可以进行类型转换
int * const pp = new int(4);
std::cout << *pp << std::endl;
int b = 10;
int * sp = const_cast<int*>(pp);
sp = &b;
return 0;
}
引用eg:
int main()
{
int a = 10;
const int& b = a;
//b = 20;//错误原因:常量引用,不允许修改值
//int& c = b;//错误,和常量指针不允许给普通指针赋值或者初始化一样
int& c = const_cast<int&>(b);
c = 20;
cout << a << endl;//20
const_cast<int&>(b) = 30;
cout << a << endl;//30
return 0;
}
this指针eg:
class Test
{
public:
Test() {}
void fun()const//此时this指针相当于const Test* const this
{
//this->val1 = 10;//错误
const_cast<Test*>(this)->val1 = 10;//OK
}
private:
int val1;
int val2;
};
四个cast原文链接:https://blog.csdn.net/yi_chengyu/article/details/121921622
(外传):vsual studio操作断点
在调试时灵活运用condition和action在,因为在大型应用程序里,可能很难让bug复现,所以运用这这两个东西就可以在不让程序停止运行的情况下就可以调试,正常调试可能需要停下来程序,加输出打断点,再重新编译运行。
(外传):why use smart ptr
如果不使用智能指针可能造成的问题:[智能指针可以自动delete和free memory]
1.如果忘记释放在堆上分配的内存,那么有可能造成严重的内存泄露。
2.所有权问题,如果指向堆分配的内存在指针在类和函数里来回传递,究竟由谁来管理和清理这块内存。比如有两个函数都需要去使用这片内存空间,但是并不确定哪个函数是最后使用这片内存的,这时候即使我们做出第三个函数来确定当前两个函数都运行完了,然后清理这片内存,也会极大的复杂化我们的程序。
50.c++预编译头文件(pch)
一个源文件经过预处理器处理后的输出叫做翻译单元。【翻译单元指C编译器产生目标文件(object file)的最终输入。在非正式使用情况下,翻译单元也叫编译单元。一个编译单元大致由一个经过C预处理器处理过的源文件组成,意味着由#include指令列出的头文件会被正确的包含进来,由#ifdef指令包含的代码会被包含进来,定义的宏会被展开。】每一个翻译单元都是独立编译的,然后链接到一起,把所有东西连接到一起。
如果每个编译单元都include了同一个头文件,那么这个头文件需要被编译无数次,大大增加了编译的时间,预编译头文件做的事就是会包起来你需要用的头文件,只需要编译一次就够了。
注意:不要将频繁更改的文件放入预编译头文件中。
例如:c++标准库等这些不需要修改的头文件放入预编译头文件中
//pch.h
#include <windows.h>
#include <iostream>
#include <vector>
#include <string.h>
#include <math.h>
#include <algorithm>
通过创建pch.cpp,然后在visual studio中设置pch.cpp 的预编译头文件设置为Create,然后整个项目的预编译头文件设置为Use,再在下一行设置预编译头文件名(pch.h),就可以使用了。最根本的好处就是提升效率
51.基准测试
基准测试其实就是衡量一下你写的代码的运行时间,如果你使用了新技术相对比一下两者是否提速了。[未验证!!!!!]
#include <iostream>
#include <chrono>
#include <array>
struct Timer{
std::chrono::time_point<std::chrono::high_resolution_clock> m_StartTimepoint;
Timer()
{
m_StartTimepoint = std::chrono::high_resolution_clock::now();
}
~Timer()
{
Stop();
}
void Stop()
{
auto endTimepoint = std::chrono::high_resolution_clock::now();
auto start = std::chrono::time_point_cast<std::chrono::microseconds>(m_StartTimepoint).time_since_epoch().count();
auto end = std::chrono::time_point_cast<std::chrono::microseconds>(endTimepoint).time_since_epoch().count();
auto duration = stop - start;
double ms = duration*0.001;
std::cout << duration << "us(" << ms << "ms)\n";
}
};
int main(void)
{
int value = 0;
{
Timer timer;
for(int i = 0 ; i < 1000 ; i++)
value += 2;
}
std::cout << value << std::endl;//测试验证
std::cout << "--------------------------\n";
struct Vector2{
float x,y;
};
std::cout << "Make Shared\n";
{
std::array<std::shared_ptr<Vector2>,1000> sharedPtrs;
Timer timer;
for(int i = 0 ; i < sharedPtrs.size() ; i++)
sharedPtrs[i] = std::make_shared<Vector2>();
}
std::cout << "New Shared\n";
{
std::arrray<std::shared_ptr<Vector2>,1000> sharedPtrs;
Timer timer;
for(int i = 0 ; i < sharedPtrs.size() ; i++)
sharedPtrs[i] = std::shared_ptr<Vector2>(new Vector2());
}
std::cout << "Make Unique\n";
{
std::array<std::unique_ptr<Vector2>,1000> uniquePtrs;
Timer timer;
for(int i = 0 ; i < uniquePtrs.size() ; i++)
uniquePtr[i] = std::make_unique<Vector2>();
}
return 0;
}
52.结构化绑定(structured bindings)
只针对c++17-------tuple,pair,tie[解决多类型返回值问题]
#include <iostream>
#include <tuple>
std::tuple<std::string,int> CreatePerson()
{
return {"ztc",24};
}
int main(void)
{
auto person = CreatePerson();
std::string name_tuple = std::get<0>(person);
int age_tuple = std::get<1>(person);
std::cout << name_tuple << " " << age_tuple << std::endl;
std::string name_tie;
int age_tie;
std::tie(name_tie,age_tie) = CreatePerson();
std::cout << name_tie << " " << age_tie << std::endl;
//下面是结构化绑定例子
auto[name,age] = CreatePerson();
std::cout << name << " " << age << std::endl;
return 0;
}
53.处理OPTIONAL数据
optional也是c++17的新特性,这个关键字其实就是用来存储可能存在也可能不存在的数据,拿文件读取来举例子,当我们需要读取一个文件的时候,返回一个字符串,这时我们可能需要知道读取是否成功了,可能实现方式是没成功就返回一个空字符串,但是这样实现如果文件本来就是空的,那么就没办法判断是否读取成功了,这时我们可能会通过传入一个引用参数(读取是否成功的布尔值)。我们可以通过使用optional来解决这个问题,如果返回失败就可以直接判断。
#include <iostream>
#include <optional>
#include <fstream>
std::optional<std::string> ReadFileAsString(std::string& filepath)
{
std::ifstream stream(filepath)
if(stream)
{
std::string result;
//read file
stream.close();
return result;
}
return {};
}
int main(void)
{
std::string filepath = "data.txt";
std::optional<std::string> data = ReadFileAsString(filepath);
std::string value = data.value_or("Not present");//这个函数是如果返回的string是失败的,那么就传入一个默认值,这个可以用于基准测试,比如一个文件里存储的是基准测试的次数,这时如果文件时空的,但是我们默认至少需要进行100次基准测试,那么就可以使用这个函数。
std::cout << value << std::endl;
if(data)
{
std::cout << "read file successfully!" << std::endl;
}
else
std::cout << "file cound not be opened!" << std::endl;
return 0;
}
54.单一变量存放多种类型数据
用c++17新特性的variant来实现单一变量存放多种类型数据
#include <iostream>
#include <variant>
enum class ErrorCode{
None = 0,NotFound = 1,NoAcess = 2
};
std::variant<std::string,ErrorCode> ReadFileAsString(std::string& filepath)
{
//这个例子是继承于上一个的,也是文件读取,相比较于optional,这里就不仅仅能处理文件是否读取的问题,而是可以解决更多的问题,如果读取成功,那么返回读取到的字符串,如果读取不成功,那么我们可以返回错误代码,这样在调用端就可以更好的知道究竟是出了什么问题导致文件读取失败
}
int main(void)
{
std::variant<std::string,int> value;
value = "ztc";
if(auto data = std::get_if<std::string>(&value))//如果是string类型
{
std::string& result = *data;
}
else if(auto data = std::get_if<int>(&value)))
int result = *data;
return 0;
}
55.单一变量存放任意类型数据
也是用c++17的方式来处理
any究竟做了什么?any在存储时是有一个大类型和小类型还有一个类型集合的union,当数据是小类型的时候就是variant的处理方式,当是大类型时,存储的是一个void*,然后去内存里动态分配
相比较于variant:
1.variant更加安全,因为需要你列出所有的类型,当出现需要隐式类型转换的地方,any可能会有类型转换的异常,但是由于variant就在定义的几种类型里面,所以一定会进行隐式转换。
2.variant不会进行内存分配,但是any在面对大类型的时候(visual studio+ msvc是32个字节)会进行动态内存分配。
#include <iostream>
#include <any>
int main(void)
{
//std::any data = std::make_any();
std::any data;
data = 2;
data = "ztc";
data = std::string("ztc");
std::string value = std::any_cast<std::string>(data);//如果data不是string那么将抛出一个类型转换异常
return 0;
}
56.async
async其实就是用来处理多线程,在future头文件中,假如需要处理的事务都是完全独立的(加入下面的计算函数是一个很复杂的计算,然后每次的计算都是独立的,那么就可以把每个计算单独放在一个线程里,然后最终放在一个结果vector里),那么可以就把事务全都放在独立的线程里。
#include <future>
#include <iostream>
#include <vector>
std::mutex m_cal_mutex;
void CalculateSomething(std::vector<long long> cal_results, int m, int n,int s)
{
long long result;
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
for (int k = 0; k < s; k++)
result = i + j + k;
std::lock_guard<std::mutex> lock(m_cal_mutex);//这里不需要解锁是因为lock_guard在出了这个作用域之后就会调用析构函数自动解锁
cal_results.emplace_back(result);
}
int main(void)
{
std::vector<long long> cal_results;
std::vector<std::future<void>> m_futures;
for (int i = 0; i < 20; i++)
{
m_futures.emplace_back(std::async(std::launch::async, CalculateSomething, cal_results, i, i + 1, i + 2));
}
return 0;
}
57.文件读取
#include <fstream>
void ReadFile()
{
std::ifstream stream("./test.txt");
std::string line;
while(std::getline(stream,line))
{
//数据处理
}
}
58.string_view(让c++字符串运行更快)
c++17新特性
string的内存分配都是在堆上进行的,所以如果我们在程序里经常滥用string的话(即使是使用substr也会自己new一个字符串),就会很影响效率,而使用string_view和const char*来代替string的话就不会浪费堆上的空间。string_view其实在本质上就是一个指向string的头指针和一个size,即使没有c++17新特性的string_view也能实现。
#include <iostream>
#include <vector>
static uint32_t s_AllocCount = 0;
void* operator new(size_t size)//重载new关键字
{
s_AllocCount++;
std::cout << "Allocating" << size << "bytes\n";
return malloc(size);
}
//#define STRING_VIEW 1
#if STRING_VIEW
void PrintName(std::string_view name)
{
std::cout << name << std::endl;
}
#else
void PrintName(const std::string& name)
{
std::cout << name << std::endl;
}
#endif
int main(void)
{
std::string name = "tianci zhang";
#if STRING_VIEW
std::string_view firstName(name.c_str(), 6);
std::string_view lastName(name.c_str() + 7, 5);
#else
std::string firstName = name.substr(0, 6);
std::string lastName = name.substr(7, 5);
#endif
//PrintName("tianci zhang");
PrintName(firstName);
PrintName(lastName);
std::cout << s_AllocCount << "allocations" << std::endl;
return 0;
}
59.单例模式
c++中的单例类其实就是组织一堆全局变量和静态函数的方式
单例模式应用场景:当我们想要拥有应用于某种全局数据集的功能,且我们只想要重复使用时。其实需要用到单例模式的地方,在c++中我们想做的就是能够将类用作命名空间,来调用某些函数。
单例模式基础案例:随机数生成器类(我们调用它只是想获得一个随机数,我们只想实例化一次,这样它就会生成随机数生成器的种子),渲染器(渲染器通常是一个非常全局的东西,通常不会有多个渲染器的实例,我们有一个渲染器,我们会向它提交所有的渲染命令,实际上就是通过渲染器调用openGL调用的东西)
#include <iostream>
class Random{//懒汉局部静态方法
public:
Random(const Random& random) = delete;
static Random& Get()
{
static Random random;
return random;
}
static float Float() {return Get().IFloat();}
private:
float IFloat() { return m_RandomGenerator;}
Random(){}
float m_RandomGenerator =0.5f;
};
int main(void)
{
float number = Random::Float();
std::cout << number << std::endl;
return 0;
}
60.小字符串(SSO)优化
字符串众所周知最不好的地方就是需要堆分配,但是c++标准库声明并不是所有字符串都是堆,当字符串小于某个长度时,是分配在一块基于栈的缓冲区里 。在visual studio 19下小字符串的最大长度是15个字节。这里还有一个问题是,debug模式下调用了某个内存分配的模板类导致小于等于15个字节也会分配,但是在release版本下就正常了。
#include <iostream>
void* operator new(size_t size)
{
std::cout << "Allocating" << size << "bytes/n";
return malloc(size);
}
int main(void)
{
std::string name = "ttttttccccczzzzzzz";
return 0;
}
61.追踪内存分配
#include <iostream>
#include <memory>
struct AllocationMetrics
{
uint32_t totalAllocated = 0;
uint32_t totalFreed = 0;
uint32_t CurrentUsage() { return totalAllocated - totalFreed;}
};
static AllocationMetrics m_AllocationMetrics;
void* operator new(size_t size)
{
m_AllocationMetrics.totalAllocated += size;
return malloc(size);
}
void operator delete(void* memory,size_t size)
{
m_AllocationMetrics.totalFreed += size;
free(memory);
}
static void PrintMemoryUsage()
{
std::cout << "Memory Usage: " << m_AllocationMetrics.CurrentUsage() << " bytes/n";
}
struct Object{
int x,y,z;
};
int main(void)
{
PrintMemoryUsage();
std::string name = "ztcssssssszzzzzz";//可以在输入ztc试一试
PrintMemoryUsage();
{
std::unique_ptr<Object> obj = std::make_unique<Object>();
PrintMemoryUsage();
}
PrintMemoryUsage();
return 0;
}
62.左值和右值
左值其实就是有地址的值(located value)
右值其实就是没有地址,没有存储空间的值,比如说字面量或者函数返回值(函数的返回值其实是一个临时变量)。
我们不可以用左值来引用右值,而函数有时候输入的明明是一个右值,为什么却不会报错?因为这是会将输入的右值构造一个左值出来,再进行操作。
在验证一个变量究竟是左值还是右值的时候,可以写一个函数,以这个变量作为输入,函数参数类型为左值引用,如果是右值就会报错,如果是左值就不会。
如果我们在写一个函数,只想接收临时变量参数,那么就可以使用右值引用。
#include <iostream>
void setValue(int& value)
{
}
void setValuePlus(const int & value)
{
}
void printValueLRef(std::string& value)//如果这里加上const就兼容右值了
{
}
void printValueRRef(std::string&& value)
{
}
int main(void)
{
int a = 10;
setValue(a);
setValue(10);//这里会报错,因为输入的是一个右值,左值无法引用一个右值
setValuePlus(10);//这里是可以的,这算是编译器的特殊规则,但是本质上是创建了一个临时左值赋值成10,然后再将value指向临时左值。
std::string firstName = "ztc";
std::string lastName = "ztc";
std::string fullName = firstName + lastName;//这里等号右边所有的是一个右值
printValueLRef(fullName);
printValueLRef(firstName + lastName);//会报错,因为这个表达式是右值,但是接收的是左值引用
printValueRRef(fullName);
printValueRRef(firstName + lastName);
}
左值引用只接收左值,除非加上const,右值引用只接收右值。右值引用可以在很大程度上优化代码,因为右值都是临时值,所以在写函数的时候,其实可以写两个分别重载左值引用和右值引用(如果函数开销极大的话),因为右值其实是函数不关心的值,不必在乎变量是否活着,是否完整,是否拷贝等,我们可以简单的偷它的资源,给到特定对象或者地方。
63.参数计算顺序(argument evaluation order)
eg:
#include <iostream>
void Function(int a,int b)
{
std::cout << a << "+" << b << "=" << (a + b) << std::endl;
}
int main(void)
{
int value = 0;
Function(value++,value++);
return 0;
}
在如上例子中,第一个参数的(value++),第二个参数的(value++),究竟是谁先计算,怎么计算?这就是参数计算顺序,那究竟顺序是什么呢?其实这个取决于编译器,这个行为其实是个未定义行为,根本不知道顺序应该是什么。因为c++根本没有提供c++规范,并没有提供一个定义来说明在这种情况下应该发生什么,形参(实参)应该按照什么顺序来求值。但是c++17至少说了这两个计算不能同时进行。
64.移动语义
移动语义解决的问题本质上就是在很多情况下我们不需要把一个对象从一个地方复制到另一个地方,但是又不得不复制,因为这时唯一可以得到这个信息的地方。这时我们可以通过移动语义的方式,将一个东西不需要通过复制,仅仅通过移动来避免复制。
demo:
#include <iostream>
#include <string.h>
class String{
public:
String() = default;
String(const char* data)
{
std::cout << "Create!\n";
m_Size = strlen(data);
m_data = new char[m_Size];
memcpy(m_data,data,m_Size);
}
String(const String& string)
{
std::cout << "Copied!\n";
m_Size = string.m_Size;
m_data = new char[m_Size];
memcpy(m_data,string.m_data,m_Size);
}
String(String&& other) noexcept //移动语义,类似于浅拷贝
{
std::cout << "Move!\n";
m_Size = other.m_Size;
m_data = other.m_data;
other.m_Size = 0;
other.m_data = nullptr;
}
~String()
{
std::cout << "Destroy!\n";
delete m_data;
}
void Print()
{
for(int i = 0 ; i < m_Size ; i++)
std::cout << m_data[i];
std::cout << std::endl;
}
private:
int m_Size;
char* m_data;
};
class Entity{
public:
Entity(const String& name):m_name(std::move(name))//这里可以将std::move换成(String&&),如果这里进行转换,那么编译器默认调用的还是拷贝构造函数,而不会调用移动构造函数
{
}
Entity(String&& name):m_name(std::move(name))//这里可以将std::move换成(String&&),如果这里进行转换,那么编译器默认调用的还是拷贝构造函数,而不会调用移动构造函数
{
}
void PrintName()
{
m_name.Print();
}
private:
String m_name;
};
int main(void)
{
Entity entity("ztc");
entity.PrintName();
return 0;
}
65.std::move和移动赋值操作符
移动构造更像是把数据偷了过来,原有的数据将不复存在。std::move其实从本质上就是将任意类型转换成右值引用,这个比强制类型转换好的一点就是有时如果类型并不是静态(auto)的那么你将无法通过强制类型转换来转换。[std::move其实就是将一个对象转换为临时对象]移动复制操作符是只针对于移动来定义的操作符,只有有移动需求时才会调用。
c++三法则:如果需要析构函数,则一定需要拷贝构造函数和拷贝赋值操作符;
c++五法则:为了支持移动语义,又增加了移动构造函数和移动赋值操作符
#include <iostream>
#include <string.h>
class String{
public:
String() = default;
String(const char* data)
{
std::cout << "Create!\n";
m_Size = strlen(data);
m_data = new char[m_Size];
memcpy(m_data,data,m_Size);
}
String(const String& string)
{
std::cout << "Copied!\n";
m_Size = string.m_Size;
m_data = new char[m_Size];
memcpy(m_data,string.m_data,m_Size);
}
String(String&& other) noexcept //移动语义,类似于浅拷贝
{
std::cout << "Move!\n";
m_Size = other.m_Size;
m_data = other.m_data;
other.m_Size = 0;
other.m_data = nullptr;
}
String& operator=(String&& other) noexcept
{
std::cout << "Move = !\n";
if(this != &other)
{
delete[] m_data;
m_Size = other.m_Size;
m_data = other.m_data;
other.m_Size = 0;
other.m_data = nullptr;
}
return *this;
}
~String()
{
std::cout << "Destroy!\n";
delete m_data;
}
void Print()
{
for(int i = 0 ; i < m_Size ; i++)
std::cout << m_data[i];
std::cout << std::endl;
}
private:
int m_Size;
char* m_data;
};
class Entity{
public:
Entity(const String& name):m_name(std::move(name))//这里可以将std::move换成(String&&),如果这里进行转换,那么编译器默认调用的还是拷贝构造函数,而不会调用移动构造函数
{
}
Entity(String&& name):m_name(std::move(name))//这里可以将std::move换成(String&&),如果这里进行转换,那么编译器默认调用的还是拷贝构造函数,而不会调用移动构造函数
{
}
void PrintName()
{
m_name.Print();
}
private:
String m_name;
};
int main(void)
{
String apple = "apple";
String dest;
std::cout << "apple:";
apple.Print();
std::cout << "dest:";
dest.Print();
dest = std::move(apple);
std::cout << "apple:";
apple.Print();
std::cout << "dest:";
dest.Print();
return 0;
}
66.sizeof关键字与strlen函数的区别
Sizeof:编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度
strlen只能测量字符串 计算字符串 str 的长度,直到空结束字符,但不包括空结束字符
67.c++运行流程
1.首先编译器进行预处理,他处理所有以#开头的语句(预处理语句),然后将代码交给编译器进行编译,宏也进行在预处理阶段,预处理阶段进行的其实就是查找与替换,将宏和头文件找到相应的位置,然后进行替换。预处理阶段会生成.i文件。
预处理语句包括:include define if endif
2.编译阶段,编译器会将所有的c++代码[翻译单元]转换成机器代码(常量数据或者指令->CPU将执行的代码)[创建一个抽象语法树,进行语法分析和词法分析],编译规则决定了我们的程序将如何被编译,这里只有cpp文件才会被编译,头文件在预处理阶段被include进来了,每个cpp将会被单独编译,生成目标文件object file(.o /.obj(visual studio))。
3.链接阶段,将所有目标文件整合成一个可执行文件(.exe),链接的焦点是如何将找到每个符号和函数的位置,即使只有一个cpp文件也需要进行链接过程,因为需要链接到main函数的位置。例如:我在一个cpp中写了几个函数,但是没有写main函数,这时就会发生链接错误(没有找到入口点[入口点不一定非得是main函数,也可以自定义]),但是编译期间是不会报错的。
链接期间还有一种错误叫未解决的外部符号(unresloved externel symbol),这种错误就是链接器找不到需要的东西时报的错。链接期间只会链接调用过的函数,没调用过的函数即使在其他翻译单元没有定义也不会出错。但是如果这个函数被其他函数调用过也会报链接错误,除非其他函数被声明只在本翻译单元里使用----static
main.cpp
#include <iostream>
void log(const char* message);
int multi(int a,int b)
{
log("multi");
return a * b;
}
int main(void)
{
mutli(5,4);//如果把这行注释掉也会报链接错误,除非把multi函数定义为static函数
return 0;
}
log.cpp
//(空的,里面没有函数实现)
链接期间还有一种错误就是重定义(already defined),相同的函数名相同的参数和返回值在不同的cpp被实现两次,这样链接器将不知道链接到哪一个函数定义上
68.STL 中常见的容器
STL 中容器分为顺序容器、关联式容器、容器适配器三种类型,三种类型容器特性分别如下:
1. 顺序容器 元素并非排序的,元素的插入位置同元素的值无关,包含 vector、deque、list
vector:动态数组 元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
deque(双端队列):双向队列实现, 元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于 vector )。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
list:双向链表 元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
2. 关联式容器 元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现,包含set、map。
set set中不允许相同元素
map map 与 set 的不同在于 map 中存放的元素有且仅有两个成员变,一个名为 first,另一个名为 second,map 根据 first 值对元素从小到大排序,并可快速地根据 first 来检索元素。
3. 容器适配器 封装了一些基本的容器,使之具备了新的函数功能,包含 stack、queue。
stack:栈 栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项),后进先出。
queue:队列 插入只可以在尾部进行,删除、检索和修改只允许从头部进行,先进先出。
69.STL 容器查找的时间复杂度
以下是其中一些常见容器的查找时间复杂度以及原因:
vector(向量):查找时间复杂度为O(n),因为vector是基于数组实现的,需要线性遍历整个数组来查找元素。
deque(双端队列):在未排序状态下,查找时间复杂度为O(n),类似于vector。但在有序状态下,可以利用二分查找,降低查找时间复杂度为O(log n)。
list(链表):查找时间复杂度为O(n),因为链表是一种线性结构,需要从头开始顺序查找元素。
set(集合)和multiset(多重集合):查找时间复杂度为O(log n),底层通常使用红黑树实现,具有较好的平衡性能。set中元素不允许重复,multiset可以重复
map(映射)和multimap(多重映射):查找时间复杂度为O(log n),底层通常使用红黑树实现,按键进行自动排序。map中元素的key不允许重复,multimap可以重复
stack(栈)和queue(队列):查找时间复杂度为O(n),因为它们是容器适配器,提供了先进先出(FIFO)或后进先出(LIFO)的接口,并不支持快速查找操作。
70.vector 和 list 的区别
1.底层数据结构:
vector: 底层使用动态数组实现。
list: 底层使用双向链表实现。
2.插入和删除操作:
vector: 插入和删除元素效率低。
list: 插入和删除元素效率高,因为只需要修改相邻节点的指针。
3.随机访问:
vector: 支持随机访问,可以通过下标快速访问元素。
list: 不支持随机访问,只能通过迭代器顺序访问元素。
4.空间和内存分配:
vector: vector 一次性分配好内存,不够时才进行扩容。
list: list 每次插入新节点都会进行内存申请。
适用场景:
vector: 适用于连续存储,支持随机访问,而不在乎插入和删除的效率。
list: 适用于不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问。
71.迭代器失效原因,有哪些情况
迭代器失效是指迭代器在遍历容器过程中,由于容器的结构发生改变而导致迭代器指向的元素不再有效。
以下是导致迭代器失效的常见情况:
1.插入和删除操作: 当在容器中插入或删除元素时,可能会导致容器内存重新分配或元素位置的改变,这可能会使迭代器失效。
2.清空容器: 清空容器会使容器内的所有元素被删除,这样迭代器指向的元素就会失效。
3.使用引起重新分配的操作: 例如,在vector中使用push_back()添加元素时,如果超出了当前容量,可能会触发重新分配操作,从而使所有迭代器失效。
4.排序操作: 如果在排序过程中,容器的元素被移动了位置,迭代器可能会失效。
72.红黑树的特性,为什么要有红黑树
红黑树是一种自平衡的二叉搜索树,它具有以下特性:
节点颜色: 每个节点要么是红色,要么是黑色。
根节点和叶子节点: 根节点、叶子节点(NIL节点,即空节点)是黑色的
颜色相邻节点规则: 不能有两个相邻的红色节点。
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。 这保证了红黑树的关键性质:最长路径不超过最短路径的两倍。
- 各操作的时间复杂度 插入: O(logN) 查看: O(logN) 删除: O(logN)
73.unordered_map 实现原理
unordered_map 容器和 map 容器一样,以键值对(pair类型)的形式存储数据,存储的各个键值对的键互不相同且不允许被修改。但由于 unordered_map 容器底层采用的是哈希表存储结构,该结构本身不具有对数据的排序功能,所以此容器内部不会自行对存储的键值对进行排序。底层采用哈希表实现无序容器时,会将所有数据存储到一整块连续的内存空间中,并且当数据存储位置发生冲突时,解决方法选用的是“链地址法”(又称“开链法”)
74.map,unordered_map 的区别
map是基于红黑树实现的,unordered_map是基于哈希表实现的
map根据元素的键值会自动排序,而unordered_map是乱序的
map的增删改查时间复杂度是O(logN),而unordered_map的时间复杂度是最好情况是O(1),最坏情况是O(N)。
75.makefile和Cmake
makefile
Makefile 可以简单的认为是一个工程文件的编译规则,描述了整个工程的编译和链接等规则。其中包含了那些文件需要编译,那些文件不需要编译,那些文件需要先编译,那些文件需要后编译,那些文件需要重新编译等等。makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。makefile带来的好处就是——“自动化编译”, 一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
下面是一个makefile例子
cc:test.o
@echo "Hello World!"
test.o:test.cc
g++ test.cc -o test
从这个例子中就可以看出目标和依赖还有命令的关系,cc是最终生成的目标文件,cc的生成需要依赖test,而test的生成又需要依赖test.cc文件。这样,我们在运行make命令的时候,Makefile会生成cc,但因为cc依赖test,那就需要先生产test,而test又依赖test.cc ,所以我们需要首先执行命令去生成目标test,然后才能根据test得到cc。这样我们对于目标、依赖和命令就有比较直观的理解了。
cmake
CMake是一个跨平台的安装编译(Build)工具,可以用简单的语句来描述所有平台的安装(编译过程)。能够输出各种各样的makefile或者project文件,能测试编译器所支持的C++特性,类似UNIX下的automake。
只是 CMake 的组态档取名为CMakeLists.txt。Cmake 并不直接建构出最终的软件,而是产生标准的建构档(如 Unix 的 Makefile 或 Windows Visual C++ 的projects/workspaces),然后再依一般的建构方式使用。 这使得熟悉某个集成开发环境(IDE)的开发者可以用标准的方式建构他的软件,这种可以使用各平台的原生建构系统的能力是 CMake 和 SCons 等其他类似系统的区别之处。
CMake主要特点
1、开放源代码,使用类 BSD 许可发布。下载CMake
2、跨平台,并可生成 native 编译配置文件,在 Linux/Unix 平台,生成 makefile,在苹果平台,可以生成 xcode,在 Windows 平台,可以生成 MSVC 的工程文件。
3、能够管理大型项目,KDE4 就是最好的证明。
4、简化编译构建过程和编译过程,Cmake 的工具链非常简单:cmake+make。
5、高效率,按照 KDE 官方说法,CMake 构建 KDE4 的 kdelibs 要比使用 autotools 来构建 KDE3.5.6 的 kdelibs 快 40%,主要是因为 Cmake 在工具链中没有 libtool。
6、具备可扩展性,可以为 cmake 编写特定功能的模块,扩充 cmake 功能。
# 单个目录实现
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 工程,他不是执行文件名
PROJECT(Ray)
# 手动加入文件 ${变量名}} ,比如${SRC_LIST}
# 其实就是将.c文件换一个名称方便后面使用
SET(SRC_LIST main.c)
SET(SRC_LIST2 main2.c)
# MESSAGE和echo类似
MESSAGE(STATUS "THIS IS BINARY DIR " ${PROJECT_BINARY_DIR})
MESSAGE(STATUS "THIS IS SOURCE DIR " ${PROJECT_SOURCE_DIR})
# 生产执行文件名
ADD_EXECUTABLE(Ray ${SRC_LIST})
ADD_EXECUTABLE(RayCpp ${SRC_LIST2})
cmake_minimum_required(VERSION 3.5)
project(test)
set(CMAKE_CXX_STANDARD 17)
# aux_source_directory(main.cpp SRC)
add_executable(test main.cpp)
target_include_directories(test SYSTEM PRIVATE ${INCLUDE_DIRES})
# link .lib
target_link_libraries(test PRIVATE ${LINK_LIBS})
76.git使用
git add .
git commit -m “修改信息”
git push
git pull
git clone
77.const和宏定义的区别
const和宏定义的区别主要体现在以下几个方面:编译阶段、类型检查、内存分配和调试能力。具体如下:
- 编译阶段。宏定义通常在编译的预处理阶段发生作用,而const常量则在编译和运行阶段都有效。
- 类型检查。宏定义只是简单的字符串替换,没有类型检查,可能导致错误,如边际效应。而const常量有对应的数据类型,需要在编译阶段进行类型检查,有助于避免低级错误。
- 内存分配。宏定义直接替换,不会分配内存,其定义的常量在内存中有多个备份,而const常量需要内存分配,通常在程序运行过程中只有一份备份。
- 调试能力。宏定义在预编译阶段就被替换,不能进行调试,而const常量可以进行调试。
- 定义内容。宏定义可以定义代码或字符串,而const常量主要用于修饰变量,使其成为只读的。