一、c++基础
1、函数重载、重写、重定义区别
1、重载
- 前提是同一个类中,函数名相同,参数列表不同的2个函数
– 函数返回值不能作为函数重载的条件
1、重载在编译时期通过函数名和参数确定调用哪个函数
2、不关心函数返回(在编译时只会根据参数列表和函数名对函数进行重命名)
3、因为调用时不能指定类型信息,编译器不知道你要调用哪个函数
- float max(int a, int b);
int max(int a, int b);2、重写/覆盖
- 前提是父类和子类之间,父类中的虚函数,在子类中定义了
1、相同的函数名;
2、相同参数列表;
3、相同的返回值类型的2个函数;- c++规定,当一个成员函数被声明为虚函数后,其子类中的同名函数都自动成为虚函数。
- 子类在重写虚函数的时候,函数前是可以不用加virtual关键字,但是习惯上都给加上
3、重定义
- 前提是父类和子类之前,父类中的普通函数,在子类中定义了同名函数,不管参数列表是否相同,基类函数都会被隐藏
2、explicit介绍
1、explicit关键词作用:
- 可以抑制内置类型隐式转换,所以在类的构造函数中,最好尽可能多用explicit关键字,防止不必要的隐式转换
2、使用条件:
- 作用于单个参数的类构造函数,如果类构造函数大于或者等于2个时,此关键词也就无效了(但是第二第三个等形参有默认值,那么此关键词依然有效)
class Round class Round_n
{ {
public: public:
Round(){} explicit Round_n(){}
Round(double a):a_(a){} explicit Round_n(double a):a_(a){}
Round(int b, int c):b_(b), c_(c){} explicit Round_n(int b, int c):b_(b), c_(c){}
Round(const Circle& A) explicit Round_n(const Circle_n& A)
{ {
a_=A.a_; b_=A.b_; c_=A.c_; a_=A.a_; b_=A.b_; c_=A.c_;
} } }
private: private:
double a_; double a_;
int b_; int b_;
int c_; int c_;
}; };
int main()
{
Round round_a(1);
Round round_b(6, 9);
// 隐式调用, 不会报错
Round round_c = 1; // 等于调用 Round q(1); 调用的是Circle(double _a)
Round round_d = 1.0; // 等于调用 Round q(1.0); 调用的是Circle(double _a)
Round round_e = round_c; // 调用的是Round (const Circle& A)
// 禁止隐式调用,以下三个都会报错
// Round_n round_n_a = 1;
// Round_n round_n_b = 1.0;
// Round_n round_n_c = round_n_a;
// 显式调用是没问题的
Round_n round_n_a(1);
Round_n round_n_b(1.0);
Round_n round_n_c(round_n_a);
}
3、深拷贝和浅拷贝(指的是拷贝构造函数)
- 网上的例子,借用一下,记不得出处了
class Person
{
public:
Person() {
cout << "Person的默认构造函数调用"<<endl;
}
Person(int age,int height) {
m_Age = age;
m_Height = new int(height); # 堆区重新申请空间,进行深拷贝,手动申请,手动释放;
cout << "Person的有参函数调用" << endl;
}
Person(const Person& p) { # 自己实现拷贝构造函数,来避免编译器的拷贝构造函数造成浅拷贝问题;
cout << "Person拷贝构造函数" << endl;
m_Age = p.m_Age; # m_Height = p.m_Height; 浅拷贝,编译器默认实现这行代码;
m_Height = new int(*p.m_Height); # 深拷贝
}
~Person() { # 析构代码,将堆区开辟数据做释放操作
if (m_Height != NULL) {
delete m_Height;
m_Height = NULL;
}
cout << "Person的析构函数调用" << endl;
}
int m_Age;
int *m_Height;
};
void test01(){
Person p1(18,160);
cout << "p1的年龄为:" << p1.m_Age<<"p1身高为:"<<*p1.m_Height<< endl;
Person p2(p1); //编译器默认调用拷贝构造函数,进行浅拷贝操作
cout << "p2的年龄为:" << p2.m_Age<< "p2身高为:"<<*p2.m_Height << endl;
}
int main(){
test01();
system("pause");
}
4、lambda函数
[ capture-list ] (params)opt->ret{ body; };
[捕获列表] (函数参数) mutable/exception/attribute 函数声明选项 -> 返回值类型 {函数体}
- 捕获列表: 函数内部可以使用的外部变量
- 函数参数: 和常规函数形参一样
- 返回值类型:和常规函数返回值类型一样
- -> 和 返回值类型 配套出现的
- 例子:
auto f = [](int a) -> int { return a + 1; };
std::cout << f(1) << std::endl; // 输出: 21、返回值类型可以不写的情况
- auto e= [](int a){ return a + 1; }; // return后,编译器可做自动推导出返回值类型
auto f = { return { 1, 2 }; }; // error: 初始化列表不能做自动化推导,无法推导出返回值类型2、在没有参数列表时,参数列表也是可以省略的
- auto f = []{ return 1; }; // 省略空参数表
3、捕获方式:
[ ] 空捕获列表。lambda不能使用所在函数中的变量。
1、值捕获:
[ names ] names是一个逗号分隔的名字列表,这些名字都是lambda所在函数的局部变量,默认情况下是值捕获,名字前加&指明是引用捕获。
size_t v1 = 42;
// 使用了值捕获,将v1拷贝到名为f的可调用对象。
auto f = [v1] { return v1; };
v1 = 0;
// j为42,f保存了我们创建它是v1的拷贝。由于被捕获的值是在lambda函数创建时拷贝,因此在随后对其修改不会影响到lambda内部的值
auto j = f();2、引用捕获(变量在lambda函数体内改变,会影响外部变量):
size_t v1 = 42;
auto f = [&v1] { return v1; }; // 引用捕获,将v1拷贝到名为f的可调用对象。
v1 = 0;
auto j = f();3、隐式捕获(除了显示的使用作用域外的变量,= 和 & 可以自动推断使用的外部变量):
1、[&] 隐式捕获列表,采用引用捕获方式。和函数引用参数一样,引用变量的值在lambda函数体中改变时,将影响被引用的对象
2、[ = ] 隐式捕获列表,采用值捕获方式。lambda体将拷贝所使用的来自所在函数的实体的值。
int a = 123;
auto f = [=] { cout << a << endl; }; // 值捕获
f(); // 输出:123
auto f1 = [&] { cout << a++ << endl; }; // 引用捕获
f1(); // 输出:123(采用了后++)
cout << a << endl; // 输出 1243、混合捕获(混合捕获时,捕获列表中的第一个元素必须是 = 或 &):
[&,identifier list ] identifier list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用捕获。identifier list中的名字前面不能使用&
–
[=, identifier list ] identifier list中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值捕获。identifier list中的名字不能包括this,且这些名字之前必须使用&
int i = 10;
int j = 20;
auto f1 = [=, &i] () { return j + i; }; // 正确,默认值捕获,显式是引用捕获
auto f2 = [=, i] () { return i + j; }; // 编译出错,默认值捕获,显式值捕获,冲突了
auto f3 = [&, &i] () { return i +j; }; // 编译出错,默认引用捕获,显式引用捕获,冲突了4、如果希望修改值捕获变量的值,可以加mutable选项(只能修改拷贝,而不是值本身)
int a = 123;
auto f = amutable { cout << ++a << endl; }; // 不加mutable,内部修改a会报错,如果是值捕获,并且lambda内部需要用到外部变量并修改,需要加mutable选项
cout << a << endl; // 输出:123
f(); // 输出:124
cout << a << endl; // 输出:123总结:
- 值捕获,只是在lambda内部使用一下,外部变量不能做修改操作(除非使用mutable选项,但是也只是内部修改,外部变量不受影响)
- 引用捕获,lambda内部修改变量或者外部修改变量,都会相互影响
- 混合捕获,= 默认值捕获,就不能搭配显示值捕获 & 默认引用捕获,就不能搭配显示引用捕获
调用:
5、菱形继承
菱形继承会引发一些问题,主要是由于多次继承同一基类所带来的二义性和内存冗余问题。具体问题如下:
- 1、二义性: 如果在派生类中调用一个在共同基类中定义的成员函数或变量,编译器将不知道应该选择哪个基类的版本,因为它们都有相同的成员。这会导致编译错误。
- 2、内存冗余:当一个类同时继承了两个基类,而这两个基类又继承自同一个基类时,派生类将包含两个共同基类的副本,造成内存空间的浪费。
- 解决方法:通过在继承关系中使用virtual关键字,可以告诉编译器这两个基类应该共享一个共同的子对象,从而解决了内存冗余和二义性的问题。
Animal
/ \
Mammal Bird
\ /
Bat
class Mammal:virtual public Animal
{
//...
};
class Bird:virtual public Animal
{
//...
};
6、类型转换
1、static_cast
- 1.用于基本数据类型的转换;
- 2.用于类层次之间的基类和派生类之间 指针或者引用 的转换(不要求必须包含虚函数,但必须是有相互联系的类),
进行上行转换 (派生类的指针或引用转换成基类表示)是安全的;进行下行转换基类的指针或引用转换成派生类表示) 由于没有动态类型检查,所以是不安全的,最好用dynamic cast 进行下行转换;- 3.可以将空指针转化成目标类型的空指针;
- 4.可以将任何类型的表达式转化成 void 类型
2、dynamic_cast
- 1.其他三种都是编译时完成的,动态类型转换是在程序运行时处理的,运行时会进行类型检查;
- 2.只能用于带有虚函数的基类或派生类的指针或者引用对象的转换,转换成功返回指向类型的指针或引用,转换失败返回 NULL,不能用于基本数据类型的转换;
- 3.在向上进行转换时,即派生类类的指针转换成基类类的指针和 static cast 效果是一样的,(注意:这里只是改变了指针的类型,指针指向的对象的类型并未发生改变);
- 4.在下行转换时,基类的指针类型转化为派生类类的指针类型,只有当要转换的指针指向的对象类型和转化以后的对象类型相同时,才会转化成功。
3、const_cast
4、reinterpret_cast
7、静态成员函数、非静态成员函数和普通函数
1、普通函数没有“从属”的概念,或者说,它是直接属于某个namespace的。
2、非静态成员函数是和实例联系在一起的,我们需要一个实例才能调用非静态成员函数。
3、静态成员函数是和类绑定的,可以直接通过类名调用,但是静态成员函数内部不能调用非静态成员变量。
如果类的成员函数想作为回调函数来使用,如创建线程等,一般只能将它定义为静态成员函数才行
- 静态成员函数和普通的静态函数区别
- 范围(Scope):
静态成员函数:属于类的范围,可以访问类的静态成员和其他静态成员函数,以及类的非静态成员和成员函数(但需要通过对象或指针访问)。
静态函数:属于全局范围,不能直接访问类的成员变量或成员函数,除非它们是公开的并且通过类名限定。- 访问权限:
静态成员函数:可以访问类的私有成员和受保护成员,因为它们在类的作用域内。
静态函数:不能直接访问类的私有成员或受保护成员,因为它们不在类的作用域内,但可以通过对象或指针调用公有成员函数来访问这些成员。- 调用方式:
静态成员函数:可以通过类名来调用,也可以通过对象来调用。
静态函数:通常通过函数名来调用,因为它们不与特定的类相关联。
8、指针和引用区别
参考链接:https://21xrx.com/Articles/read_article/157024
1、 指针传递也等于值传递,只是复制的是指针的地址,会在栈中创新创建一个指针(指针的地址)指向实参值
引用是一种对变量的别名,它实际上并不创建新的内存地址
2、 在函数中修改引用的值时,原始变量的值也会随之改变
在函数中改变指针的值时,原始变量的值不会受到影响,但如果使用指针访问的变量
void modifyValue(int* ptr) {
//*ptr = 20; // 第一种:修改指针所指向的值(实参的值)
int y = 20;
ptr = &y; // 第二种:修改指针的指向
int* newPtr = new int(20);
ptr = newPtr; // 第二种:修改指针的指向
}
3、 指针可以为 nullptr,表示不指向任何变量;引用必须绑定到一个变量,不能为 nullptr。
4、 使用引用可以使代码更加简洁,程序员不必考虑如何传递指针,而是直接传递变量的别名即可。
使用指针传递需要使用复杂的解引用语法和地址符(&)
void modifyValue(int* ptr) {
//*ptr = 42; // 第一种:修改指针所指向的值(实参的值)
int y = 20;
ptr = &y; // 第二种:修改指针的指向
}
int main(int argc, char** argv) {
int value = 10;
printf("Before function call: %d\n", value);
modifyValue(&value);
printf("After function call: %d\n", value); //第一种:实参的值被修改,值为42 第二种:只是修改了形参的指针指向,不影响实参,值为10
}
9、拷贝构造为什么要传引用
- 若拷贝构造函数参数为值传递,当实参赋值给形参时,又发生了一次拷贝函数的调用,无限递归下去,导致爆栈。
10、多态
- 简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用子类的成员函数。
-
动态绑定:
- 就是在运行时,虚函数会根据绑定对象的实际类型,选择调用函数版本(我们在使用基类的引用(指针)调用虚函数时,就会发生动态绑定);
-
C++类的虚函数表和虚函数在内存中的位置
- 虚函数表指针是虚函数表所在位置的地址。.虚函数表指针属于对象实例。因而通过new 出来的对象的虚函数表指针位于堆,声明对象的虚函数表指针位于栈
- 虚函数位于只读数据段(.rodata),即:C++内存模型中的常量区;
- 虚函数代码则位于代码段(.text),也就是C++内存模型中的代码区
父类函数有virtual,子类有同名函数但参数不同,用父类指针指向子类对象,调用这个函数时,调用的是父类的函数。
原因是子类没有完成覆盖(多态性),也就没有在虚函数表中增加子类的函数,运行时查找虚函数表,但虚函数表中只有父类函数,所以执行父类函数。
int main(){
Base* basePtr;
Base baseObj;
Derived deriverObj;
basePtr = &baseObj;// 基类指针指向基类对象
basePtr->show();//输出:在Base中显示
basePtr = &derivedObj;// 基类指针指向派生类对象
basePtr->show();//输出:在Derived中显示
return 0;
}
- 通过基类指针来调用不同对象的同名虚函数时,程序会根据指针所指向的具体对象类型来动态的选择要调用的函数,从而实现了多态性的效果
11、C语言中*p++ ,(p)++ ,++p ,++*p的区别
- 参考网址:https://www.cnblogs.com/Xuxiaokang/p/15714240.html
- ++在右优先级最低 ++在前和*平级 再结合自右向左原则
*p++ 指向下一个元素值
*++p 指向下一个元素值
12、ref
// std::ref 用于取某个变量的引用,这个引入是为了解决一些传参问题
void foo(int& x){
x += 1;
}
int main(){
int a = 1;
std::thread t(foo, std::ref(a));
t.join();
std::cout << a << std::endl; // 2
return 0;
}
13、互斥量
- 多个线程进行写操作的时候,写操作的时候加锁,写操作完成后,进行解锁
- 线程安全:如果多线程程序每一次的运行结果和单线程运行的结果始终是一样的,那么你的线程就是安全的
- 锁的分类:互斥锁、自旋锁、信号量、条件变量等
死锁 发生在双线程多锁申请
Thread A Thread B
_mu1.lock() _mu2.lock()
// 死锁 // 死锁
_mu2.lock() _mu1.lock()
// 当进程A持有锁1请求锁2,进程B持有锁2请求锁1时,两者都不会释放自己的锁,两者都需要对方的锁,就会造成死锁。
如何处理死锁:
- 1.避免循环等待,如果需要在业务中获取不同的锁,保证所有业务按照相同的顺序获取锁。
- 2.使用超时锁(timed_mutex),当锁超时时,自动释放锁。
- 3.使用try_lock,当锁被占用时,返回false并继续执行。
- 4.锁的粒度尽量要小,只保护竟态数据而不是整个流程。
std::lock_guard和std::unique_lock区别:
- 1.灵活性:std::unqiue_lock的灵活性要高于std::lock_gurad,std::unique_lock可以在任何时间解锁和锁定,而std::lock_guard在构造时锁定,在析构时解锁,不能手动控制。
- 2.所有权:std::unique_lock支持所有权转移,而std::lock_gurad不支持。
- 3.性能:由于std::unique_lock的灵活性更高,它的性能可能会稍微低一些。
参考链接:
- https://www.jb51.net/program/2911694s3.htm
- https://juejin.cn/post/7248531532393005114
14、gdb使用
- 参考链接:https://blog.csdn.net/chen1415886044/article/details/105094688
15、automic
- 在多核CPU下,当某个CPU核心开始运行原子操作时,会先暂停其他CPU内核对内存的操作,以保证原子操作不会被其他CPU内核所干扰
16、静态库和动态库的区别
- 静态库:由于静态库的代码和数据被完整的复制到可执行文件中,所以静态库会增加可执行文件的大小。
动态库:多个可执行文件可以共享同一个动态库文件,因此动态库可以减小可执行文件的大小。- 静态库的函数调用是在编译时解析的,因此执行效率相对较高。
动态库的函数调用是在运行时进行解析的,会稍微增加一些运行开销。
17、设计模式
观察者模式建议你为发布者类添加订阅机制,让每个对象都能订阅或取消订阅者事件流:
该机制包括:
1)一个用于存储订阅者对象引用的列表成员变量;
2)几个用于添加或删除该列表中订阅者的公有方法;
参考示例:https://refactoringguru.cn/design-patterns/observer/cpp/example
在发布者Subject类:
virtual void Attach(IObserver *observer) = 0;
virtual void Detach(IObserver *observer) = 0;
进行list的添加和删除:
virtual void Notify() = 0;
在Notify中,实现遍历每个订阅者的Update方法在订阅者Observer类:
virtual void Update(const srd::string &message_from_subject) = 0;
进行比如打印操作
在类的构造函数中实现对Attach的添加订阅
有专门实现Detach的移除自己的操作
18、空类
- 占一个字节,默认6个成员函数
class Empty
{
public:
Empty(); // 缺省构造函数
Empty(const Empty&); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=(const Empty&); // 赋值运算符
Empty* operator&(); // 取值运算符
const Empty* operator&()const; // 取值运算符 const
}
# 举例 取址运算符符 和 const 取址运算符的使用区别
class Example
{
public:
Example(int value):data(value){}
int getData() const {return data;}
private:
int data;
}
int main()
{
Example obj(10);
int* ptr = &obj.getData(); //使用普通的取地址运算符获取对象的地址,并修改对象的值
*ptr = 20;
const Example constObj(30);
const int* constPtr = &constObj.getData(); // 使用const修饰的取地址运算符获取常量对象的地址(无法修改)
// *constPtr = 40; //错误,无法修改常量对象的值
return 0;
}
19、strcpy,strncpy会有什么安全问题
- 他们都不检查边界,极易造成栈溢出:
char *test=“hellohellohellohellohellohellohellohellohellohellohello”;
char test2[2];
strcpy(test2,test);
// strcpy实现:
char* strcpy(char* strDest, const char* strSrc){
assert((strDest != NULL) && (strSrc != NULL));
char* address = strDest;
while((*strDest++ = *strSrc++) != '\0');
return address;
}
>- 1、源字符串参数用const修饰,防止修改源字符串
>- 2、空指针检查
>- 3、返回dst的原始值使函数能够支持链式表达式
链式表达式的形式如下:
int i = strlen(strcpy(strA,strB));
// strcpy_s使用:
char src[] = "hello world";
char dest[6];
strcpy_s(dest, sizeof(dest), src);
- 字符操作:strlen、strcpy、strcat、strcmp、strstr、strtok。
- 内存操作:memset、memcpy、memmove
20、不能声明为虚函数的有哪些
1、静态成员函数; 2、类外的普通函数: 3、构造函数: 4、友元函数
- (1) 类的构造函数不能是虚函数,上面有解释
- (2) 类的静态成员函数不能是虚函数
类的静态成员函数是该类共用的,与该类的对象无关,静态函数里没有this指针,所以不能为虚函数。- (3) 内联函数
内联函数的目的是为了减少函数调用时间。它是把内联函数的函数体在编译器预处理的时候替换到函数调用处,这样代码运行到这里时候就不需要花时间去调用函数。
inline是在编译器将函数类容替换到函数调用处,是静态编译的。而虚函数是动态调用的,在编译器并不知道需要调用的是父类还是了类的虚函数,所以不能够inline声明展开,所以编译器会忽略- (4) 友元函数与该类无关,没有this指针,所以不能为虚函数。
21、如何判断结构体是否相等? 能否用 memcmp 函数判断结构体相等?memset注意项
- 需要重载操作符 ==判断两个结构体是否相等,不能用函数 memcmp 来判断两个结构体是否相等因为 memcmp 函数是逐个字节进行比较的,而结构体存在内存空间中保存时存在字节对产,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法比较。
- memset为什么只能赋值为0
- 参考链接:https://blog.csdn.net/ding_programmer/article/details/90380077
22、define 和 const区别
1、编译器处理方式不同
- #define 宏是在预处理阶段展开
const常量是在编译运行阶段使用2、类型和安全检查不同
- #define 宏没有类型,不做任何类型检查,仅仅是展开
const常量有具体的类型,在编译阶段会执行类型检查3、存储方式不同
- #define宏仅仅是展开,有很多地方使用,就展开多少次,不会分配内存(宏定义不分配内存,变量定义分配内存)
const常量会在内存中分配(可以是堆也可以是栈中)
23、回调函数
如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数
- 1、c++在类中定义的回调函数一般是静态的,因为静态成员函数不依赖于任何特定的对象实例,因此它们可以在没有对象实例的情况下被调用
- 2、c++中,回调函数也可以是全局函数或者由lambda表达式定义的函数对象,这些函数不属于类的成员函数,因此也不需要是静态的
- 以此举例,回调函数使用四点:
// 1、回调函数声明(定义了回调函数的参数和返回类型)
typedef void(*CallbackFun)(double height, void* context);
// CallbackFun backfun;//声明全局函数指针
// 2、注册回调函数,提供给外部类使用的
void registHeightCallback(CallbackFun callback,void* context){
double h = 100;
callback(h, nullptr);
// backfun = callback;
}
class Person
{
public:
// 3、类中定义回调函数,成员函数的话,一定是定义成静态成员函数(也就是实现函数,用来具体的实现)
static void onHeight(double height,void*context){ count<<height<<endl; }
}
// 4、使用注册回调函数,将回调函数传递给注册回调函数
void registCallback(){
registCallback(onHeight,nullptr);
}
//void test(double height){
//backfun(height,nullptr);//此处给数据,和在注册回调函数给数据一样的意思
//}
int main(){
Person p;
p.registCallback();
}
- 参考链接:https://developer.aliyun.com/article/1377719?spm=5176.26934562.main.1.3eeb58a7SHP3ZP
24、linux文件权限
- 参考链接:https://blog.csdn.net/pythonw/article/details/80263428
25、左值和右值
左值一定能取地址,但是左值不一定支持修改(特殊的一个:字符串常量(存储在静态数据区))
- 左值:
具名的变量名
左值引用
右值引用也是左值
返回左值引用的函数或是操作符重载的调用语句
a=b,a+=b 等内置的赋值表达式
前缀自增 如:++a --a 都是左值
字符串常量
左值引用的类型转换语句,如:static_cast<int&>(x)右值是临时产生的值,不能取地址,仅存在寄存器中
- 右值:
除字符串以外的常量,如:1 true nullptr
返回非引用的函数或操作符重载的调用语句
a++ a–是右值
a+b a<<b等
&a 对变量取地址的表达式是右值
this指针
lambda表达式
其实就是一些运算时的中间值,这些值不会实际写到内存地址空间中,因此无法对他们取地址将亡值,即将销毁的,就2种
- 1、返回右值引用的函数或者操作符重载的调用表达式。如:某个函数返回值是std::move(x),并且函数返回类型是T&&
- 2、目标为右值引用的类型转换表达式,如:static_cast<int&&>(a)
左值引用:非const左值引用只能绑定左值 const左值引用既能绑定左值,又能绑定右值
int a = 1;
int& lref = a;
lref++;//可修改其值
const int& lref_const = a;
lref_const++;//error不能修改其值
const int& lref_const_v = 999;//const 左值引用可以直接绑定右值999,当然const左值引用肯定是无法修改其值的,只可读,不可写
26、进程分区
- 栈、堆、自由存储区、全局静态存储区、代码段
27、String类能不能被继承?为什么?
- string类被设计成final类,即禁止继承
28、内存检测工具Valgrind和AssressSanitizer
内存泄漏解决逻辑:
- 1、查询new与delete,看看内存的申请与释放是不是成对释放的
- 2、在类中追加一个静态变量 static int count;在构造函数中执行count++;在析构函数中执行count–;
通过在程序结束前将所有类析构,之后输出静态变量,看count的值是否为0,如果为0,则问题并非出现在该处,如果不为0,则是该类型对象没有完全释放
- 参考链接:https://blog.csdn.net/xiaofeilongyu/article/details/128538777
asan使用:https://www.yanrongyun.com/zh-cn/blogs/blog+AddressSanitizer+20220706
29、RTSP
OPTIONS:客户端请求服务端 获取服务器支持的方法(Pulic:OPTIONS,DESCRIBE…)
DESCRIBE:告诉客户端可以通过这个方法,获取该流媒体服务器的数据的描述
SETUP:通过 DESCRIBE 获取媒体类型以后,就可以建立连接了
PLAY:连接建立完了,就可以从服务端,通过PLAY建立的通道,推数据推到客户端
SDP协议(会话描述协议)
一个会话级描述:两端的IP,就是建立连接的基本描述(通过哪个端口哪个IP建立的)
1、会话的名称和目的
2、会话存活时间
3、会话中包括多个媒体信息
多个媒体级描述:当前请求的流媒体资源(音频、视频,2个媒体进行描述)
1、媒体格式
2、传输协议
3、纯属IP和端口
4、媒体负载类型
RTP是负责传输媒体数据的,RTCP是用来确保RTP传输的质量的,一般来说RTP使用一个偶数的port,
RTCP使用RTP下一个port,两者联合使用
30、如何稳定的每隔5ms执行某个函数?
单开线程轮询,windows或linux也是无法稳定准确的5ms轮询一次
只有硬实时操作系统才能做到
- 硬件实时操作系统:对任务响应时间有极高要求的系统,必须在规定的时间内完成任务的执行
如:VxWorks、QNX、FreeRTOS、RTOS-32- 通用操作系统:为了满足一般计算和应用程序执行需求而设计的操作系统。这类操作系统通常具有较高的灵活性和通用性,能够支持多样化的任务和应用
如:Windows、Linux、macOS会在任务优先级的基础上进一步平衡每个任务获取的CPU时间,但这也导致,无论高低优先级任务,谁都不能说自己能够准确的执行指定时间中途不被内核切走挂起。
31、线程池
- 线程池实现的核心是对任务队列的处理,线程池需要提供接口,供主线程往队列中添加任务,线程池中的线程则从队列中取任务并执行处理
- 因为涉及线程安全,我们使用c++11提供的mutex unique_lock 来保证线程安全,以及condition_variable来通知等待的线程
32、Linux下C++11 thread库注意项
- 在Linux/GCC/libstdc++下,C++11 thread库居然强制动态连接pthread,如果你编译连接的时候忘了-pthread参数,一直要到运行的时候才会报错,得多脑残的人才会把这个库做成这样?
33、音视频编码解码大致步骤
// 打开音视频文件,初始化 AVFormatContext 结构体
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
// 读取音视频文件的流信息
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
// 初始化解码器上下文
av_find_best_stream
avcodec_find_decoder
avcodec_alloc_context3
avcodec_parameters_to_context # 从音频或者视频的stream中拷贝解码参数到解码器的上下文
avcodec_open2 # 初始化并打开给定的音视频流中的编解码器
// 初始化编码器
avcodec_find_encoder
video_encoder_codec_ctx
avcodec_open2
// 解封装 解码
av_read_frame
avcodec_send_packet
avcodec_receive_frame
// 编码
avcodec_send_frame
avcodec_receive_packet
// 封装
av_packet_rescale_ts # 重新调整 AVPacket 结构体中的时间戳(timestamp)和时间基(timebase)
// 对 packet 进行缓存和 pts 检查,适用于写入音视频交错(interleaved)数据到输出容器
av_interleaved_write_frame(output_fmt_ctx, pkt);
34、websocket
- 客户端通过HTTP Upgrade请求,即101 Switching Prorocol 到HTTP服务器,然后由服务器进行协议转换
35、线程同步方式
C++多线程同步措施:
- 1、 C++线程和基础同步原语
Thread
mutex, lock_guard, unique_lock
condition variable, semaphore- 2、高级同步原语:
future and async/packaged_task/promise
36、进程同步方式
- ZMQ框架:zeroMQ不是TCP,不是socket,也不是消息队列,而是这些的综合体
管道
命名管道
共享内存
消息队列
信号
信号量
套接字
37、为什么父类析构函数申明为虚函数?
子类中有 指针 成员变量时,而父类的析构函数未声明为虚析构,造成无法调用子类的析构
名词解释:
- 多态:简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用子类的成员函数。
#(也就是父类把某个成员函数定义为虚函数后,在子类重写,A * a = new B; 1、 a可以调用子类B中的成员函数,2、子类函数中可以直接调用父类的虚函数 参考网址:https://blog.csdn.net/sangba2019/article/details/117257706)- 动态绑定: 就是在运行时,虚函数会根据绑定对象的实际类型,选择调用函数的版本(我们在使用基类的引用(指针)调用虚函数时,就会发生动态绑定);
- 实例化: 把用类创建对象的过程称为实例化(实例化过程中一般由类名 对象名 = new 类名(参数1,参数2…参数n)构成,实例化一个类时返回的是新创建的指针,也可以不用new实例化,那么就是在栈上创建,由内存自由释放)
析构函数:在销毁对象时自动执行,析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。如果用户没有定义,编译器会自动生成一个默认的析构函数。
- 注意:
- C++ 的 构造函数 不可以被声明为虚函数,但 析构函数 可以被声明为虚函数(子类不会继承父类的构造和析构,但在子类的对象构造或析构的时候会调用父类的构造或析构)
- 虚析构函数是为了避免内存泄露,而且是当子类中会有指针 成员变量 时才会使用得到的
参考网址:https://blog.csdn.net/qq_40051406/article/details/126770088 (解释虚表只属于类,每创建一个对象会有一个*__vptr指向类的虚表(指向虚函数表的指针是存在于对象实例中最前面的位置),通过对象实例的地址得到这张虚函数表,然后对虚函数表进行遍历,并调用其中的函数)
参考网址:https://blog.csdn.net/weixin_42127358/article/details/121492842- 如果父类的析构函数是虚函数,则子类的析构函数一定是虚函数(即使是子类的析构函数不加virtual,这是C++的语法规则)
- 当使用基类指针删除一个派生类对象时,编译器只会根据指针类型选择调用相应的析构函数。如果基类的析构函数不是虚函数,编译器在编译期就确定了要调用的析构函数是基类的析构函数。
这就导致即使指针指向的是派生类的对象,也只会调用基类的析构函数- 当父类未定义为虚析构后,会用到父类的指针去操作子类的函数功能,delete了父类指针,只会执行了父类的析构,而子类里的析构不会执行(如果子类中定义了堆内存,就会造成内存泄漏)
当父类定义为虚析构后,先执行子类的析构,再执行父类的析构。- 为什么构造函数不能为虚函数?
虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数—构造函数了
38、单链表数据,找出倒数第三个数值
参考链接:https://blog.csdn.net/m0_59938453/article/details/122532602
- 1、单指针法
基本思路:
先遍历一遍,记录链表的总节点个数。
再遍历一遍,找到倒数第k个节点。- 2、快慢指针法
基本思路:
先将快慢指针均指向链表的第一个节点,
然后让 fast 先向后走 k 步,再让两指针同步向后走。
当 fast 最后指向空时,slow 此时指向的就是倒数第 k 个节点。
39、两个int相加溢出,如何判断和处理
- 参考链接:https://blog.csdn.net/weixin_42109012/article/details/97134342
40、c++ 异常处理,如果程序抛出了异常,但是没有处理,程序就会崩溃
try{
if(ii==1) throw "一只鸟";
if(ii==2) throw ii;
if(ii==1) throw string("三只鸟");
}
// catch(int ii){ # 这种类型抛出了异常,没处理,程序就会崩溃
// cout<<"异常的类型是int="<<ii<<endl;
//}
catch(const char* ss){
cout<<"异常的类型是const char*="<<ss<<endl;
}
catch(string str){
cout<<"异常的类型是string="<<str<<endl;
}
41、STL
包括两部分内容:容器和算法。(重要的还有融合这二者的迭代器)
容器:
容器分为两类:
序列式容器:其中的元素不一定有序,但都可以被排序。
如:vector、list、deque、stack、queue、heap、priority_queue、slist;
# 队列是先进先出,水管(FIFO --- queue、priority_queue、deque),栈是先进后出(LIFO --- stack)
vector:连续存储结构,每个元素在内存上是连续的;支持 高效的随机访问和在---尾端---插入/删除操作,但其他位置的插入/删除操作效率低下,空间不足的时候,进行双倍扩容;array空间固定,不能扩容;
vector<T> v1; vector保存类型为T的对象。默认构造函数v1为空。
vector<T> v2(v1); v2是v1的一个副本。
vector<T> v3(n, i); v3包含n个值为i的元素。
vector<T> v4(n); v4含有值初始化的元素的n个副本
[ ]操作符或vector.at() 随机访问
deque:连续存储结构,即其每个元素在内存上也是连续的,类似于vector,不同之处在于, deque提供了两级数组结构, 第一级完全类似于vector,代表实际容器;
另一级维护容器的首位地址。这样,deque除了具有vector的所有功能外, 还支持高效的首/尾端插入/删除操作。
deque是在功能上合并了vector和list。
deque<T> v1; deque保存类型为T的对象。默认构造函数v1为空。
dq.empty(); 判断队列是否为空,为空返回true
dq.push_front(s); 将s从队头入队
dq.push_back(s); 将s从队尾入队
dq.front(); 只返回队头元素
dq.back(); 只返回队尾元素
dq.pop_front(); 将队头元素弹出
dq.pop_back(); 将队尾元素弹出
dq.clear(); 将队列清空aq
优点:(1) 随机访问方便,即支持[ ]操作符和vector.at()
(2) 在内部方便的进行插入和删除操作
(3) 可在两端进行push、pop(push_front、pop_front、push_back、pop_back)
list:非连续存储空间,能进行高效的随机插入和删除,但是随机访问低效,只能通过指针来访问;
使用区别:
(1)如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
(2)如果你需要大量的插入和删除,而不关心随机存取,则应使用list
(3)如果你需要随机存取,而且关心两端数据的插入和删除,则应使用deque
queue:支持push_back、pop_front,没有迭代器,不能遍历;
stack: 没有迭代器,不能遍历;
关联式容器:内部结构基本上是一颗平衡二叉树。所谓关联,指每个元素都有一个键值和一个实值,元素按照一定的规则存放。
如:RB-tree、set、map、multiset、multimap、hashtable、hash_set、hash_map、hash_multiset、hash_multimap。
下面各选取一个作为说明。
vector:它是一个动态分配存储空间的容器。区别于c++中的array,array分配的空间是静态的,分配之后不能被改变,而vector会自动重分配(扩展)空间。
set:其内部元素会根据元素的键值自动被排序。区别于map,它的键值就是实值,而map可以同时拥有不同的键值和实值。
算法,如排序,复制……以及个容器特定的算法。
迭代器是STL的精髓,我们这样描述它:迭代器提供了一种方法,使它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构。它将容器和算法分开,好让这二者独立设计。
# 1、vector
# 先进后出---LIFO https://cloud.tencent.com/developer/article/1812654
# 在内存上是连续的;支持 高效的随机访问和在---尾端---插入/删除操作,但其他位置的插入/删除操作效率低下,空间不足的时候,进行双倍扩容;array空间固定,不能扩容
# 例如:头部后有十万个数据,则往头部插入一个数据时,十万个数据都需要往后挪一挪才能在头部插入数据
# 举例中,i代表序号 item代表某个数值
# array 数组:(插入个不可变数组array)
array<int, 10>data; # int-->数据类型,10-->array容量 未指定10个元素值,各个元素的值是不确定的
array<int, 10>data{}; # 与上面语句唯一区别,将所有元素初始化为0
array<int, 10>data{2,3,5,7}; # 初始化前4个元素,剩余的元素初始化值都是0
get<3>(data); # array提供的辅助函数,获取array第三个元素
int array[]={1,2,3};
vector<int>data(array, array+2); # 此时 data 的值为{1,2}
vector<int>data(20); # 创建20个int型数据,默认初始值都为0
vector<int>data{20}; # 只有一个初始值为20 ()和{}的区别:()表示元素个数,{}容器只有大括号里的元素
vector<int>data(100,99); # 创建100个int型数据,所有元素的初始值都是99
vector<int> data2(data); # data2 是 data 的一个副本
data.assign(data.begin()+4, data.end()); # 将[beg, end)区间中的数据拷贝赋值给本身,等于重置数据,比如之前data里有10个7,从第4(10-4还剩6个7)位重置,此时data被重置成6个7的数组
data.assign(5, 6); # 将 5 个 6 拷贝赋值给本身。,此时data被重置成5个6
data.begin(); # 返回指向容器第一个元素的迭代器
data.end(); # 返回指向容器最后一个元素的迭代器
data.rbegin(); # 返回一个逆序迭代器,它指向容器c的最后一个元素
data.rend(); # 返回一个逆序迭代器,它指向容器c的第一个元素前面的位置
data.front(); # 返回容器中第一个数据元素
data.back(); # 返回容器中最后一个数据元素
data.at(i); # at()为成员函数访问,与 [] 区别是,会判断是否越界 所以 1.比[]慢一点 2.更安全
data.clear(); # 移除所有元素,容量还是100
# c++11 引入emplace_back(实际使用时,建议优先选用 emplace_back)
data.push_back(item); # 尾部添加元素i
data.emplace_back(item); # 尾部添加元素i
# 注意:push_back() 在底层实现时,调用完构造函数后,会优先选择调用移动构造函数(如果没有才会调用拷贝构造函数);emplace_back只会调用构造函数,执行效率比 push_back高
(参考地址:https://blog.csdn.net/qq_38196982/article/details/119136650)
data.pop_back(); # 尾部删除元素
data.insert(data.begin() + i, item); # 在第i+1的位置插入元素item
data.insert(data.begin() + i, 10, item); # 在第i+1的位置插入 10个 元素item
# 避免自动扩容:
data.reserve(20); # 容器分配可容纳20个元素,如词语句之前,容器已经大于或等于20个元素,那么此条语句什么也不做,不影响已存储的元素,也不生成任何元素
# 注意:如果reserve增加了容器容量,之前创建好的任何迭代器(如:开始迭代器和结束迭代器)都可能失效,因为,增加后容器可能被复制或移到了新内存地址,后续使用这些迭代器时,最好重新生成一下
# 提前设置容量,避免自动扩容(自动扩容导致拷贝构造函数频繁被调用,参考地址:https://blog.csdn.net/ganfanren00001/article/details/122024529)
data.resize(2); # 重新指定容器的长度为 2,若容器变长,则默认填充0到新位置。如果容器变短,则末尾超出容器长度的元素---被删除
data.resize(21, item); # 重新指定容器的长度为 num,若容器变长,则将 item 值填充到新位置。如果容器变短,则末尾超出容器长度的元素---被删除
# 删除元素
# 第一种
data.erase(data.begin() + i); # 删除第i+1个元素
data.erase(i,i+3); # 删除从i到i+3之间的元素
# 第二种(删除data中的第二个元素)
swap(begin(data)+1,end(data)-1); # 将第二个元素和最后一个元素相互交换
data.pop_back(); # 此时再移除最后一个元素(也就是之前的第二个元素)
# 遍历元素
vector<int>::iterator it;
for(it = data.begin(); it != data.end(); it++ )
{
cout<<" "<< *it;
}
# 常用算法,引入:#include <algorithm>
# 翻转元素,即逆序排列
reverse(vec.begin(), vec.end());
# 将元素以升序排列(由小到大)
sort(vec.begin(), vec.end());
bool compare(const int &a, const int &b) { # 定义排序比较函数
return a > b;
}
sort(vec.begin(), vec.end(), compare); # 将元素以降序排列(由大到小)
# 查找
find(vec.begin(), vec.end(), 10); # 查找向量中的值为10的元素
# 复制q
copy(vec.begin(), vec.end(), vec_1.begin() + 1); # 把vec向量中所有元素全部复制到vec_1向量中,从vec_1.begin() + 1 的位置开始复制,覆盖掉原有的元素
# 提升:resize和erase区别和效率:
resize 主要是关于容器的大小管理。
erase 主要是关于删除容器中的元素。
resize 通常是相对高效的,因为它不一定需要移动或复制元素,它通常只需要分配或释放内存。
erase 的效率可能取决于容器的类型。对于某些容器,删除元素可能涉及移动其他元素,因此性能开销可能较高。
42、智能指针
- 本身是个类,都不支持指针的运算(+、-、++、–)
# 1、unique_ptr
unique_ptr<AA> pu(new AA("西施"));
void func1(const AA* a) { cout << a->m_name << endl; }
void func2(AA* a) { cout << a->m_name << endl; delete a; }
void func3(const unique_ptr<AA> &a) { cout << a->m_name << endl; }
void func4(unique_ptr<AA> a) { cout << a->m_name << endl; }
unique_ptr<AA> pu(new AA("西施"));
func1(pu.get()); // 函数func1()需要一个指针,因为可能是远古的代码,它的形参需要一个原始指针,get()方法返回裸指针
func2(pu.release()); // 函数func2()需要一个指针,release()释放对原始指针的控制权,将unique_ptr置为空,返回裸指针(可用于把unique_ptr传递给子函数,子函数将负责释放对象)
func3(pu); // 函数func3()需要一个unique_ptr,不会对这个unique_ptr负责。
func4(move(pu)); // 函数func4()需要一个unique_ptr,std::move()可以转移对原始指针的控制权。(可用于把unique_ptr传递给子函数,子函数形参也是unique_ptr)
pu = nullptr; // 默认调用析构函数,用nullptr给unique_ptr赋值将释放对象,空的unique_ptr==nullptr
# 1、用作函数传参时,可以传引用,不能直接传值(不能直接将智能指针当做形参传入,因为unique_ptr没有拷贝构造函数)
func4(pu); # 拿上面的例子说明,直接传入pu会奔溃
# 可以将func4修改为如下func5方式传入智能指针:
void func5(unique_ptr<AA> &a)
func5(pu);
# 2、reset()释放对象
pu.reset(); // 释放pu对象指向的资源对象
pu.reset(nullptr); // 释放pu对象指向的资源对象
pu.reset(new AA("bbb")); // 释放pu指向的资源对象,同时指向新的对象
# 3、swap()交换两个unique_ptr的控制权
void swap(unique_ptr<T> &_Right);
# 4、unique_ptr不是绝对安全,如果程序中调用exit()退出,全局的unique_ptr可以自动释放,但局部的unique_ptr无法释放
# 5、unique_ptr提供了支持数组的具体化版本
// unique_ptr<int[]> parr1(new int[3]); // 不指定初始值。
unique_ptr<int[]> parr1(new int[3]{ 33,22,11 }); // 指定初始值。
cout << "parr1[0]=" << parr1[0] << endl;
cout << "parr1[1]=" << parr1[1] << endl;
cout << "parr1[2]=" << parr1[2] << endl;
unique_ptr<AA[]> parr2(new AA[3]{string("西施"), string("冰冰"), string("幂幂")});
# 6、重写unique_ptr
# unique_ptr中的源代码(MSVC),拷贝构造函数和拷贝赋值运算符均为delete
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
参考链接:https://zhuanlan.zhihu.com/p/395065368
# 7、可通过下面方法转换为shared_ptr
std::unique_ptr<std::string> foo()
{
return std::make_unique<std::string>("foo");
}
int main()
{
std::shared_ptr<std::string> sp1 = foo();
auto up = std::make_unique<std::string>("Hello World");
std::shared_ptr<std::string> sp2 = std::move(up);
//std::shared_ptr<std::string> sp3 = up; 错误,编译报错
}
# 2、shared_ptr
# 两种初始化方法:
shared_ptr<AA> p0(new AA("西施")); // 分配内存并初始化。
shared_ptr<AA> p0 = make_shared<AA>("西施"); // C++11标准,效率更高。
面试知识点(后续有时间再做总结)
- makefile使用
- 友元函数和友元类
- TCP粘包
- 对称加密和非对称加密
- 线程间到底共享了哪些进程资源
- 找出前三大的数
- 单链表形式 找出倒数第三个
- int*p[] 和 int(*p)[] 区别
- 如何优化内存?
- 虚拟内存和物理内存是怎么关联的?
- http和https的区别
常见面试题:https://www.yisu.com/zixun/194286.html
常见的内存泄露:https://www.yisu.com/zixun/528772.html
https://blog.csdn.net/qianqian7759/article/details/111373477