关于
本仓库是面向 C/C++ 技术方向校招求职者、初学者的基础知识总结,包括语言、程序库、数据结构、算法、系统、网络、链接装载库等知识及面试经验、招聘、内推等信息。
侧边目录支持方式: Docsify 文档、Github + TOC 导航(TOC预览.png)
保存为 PDF 方式:使用 Chrome 浏览器打开 Docsify 文档 页面,缩起左侧目录-右键 - 打印 - 选择目标打印机是另存为PDF - 保存(打印预览.png)
仓库内容如有错误或改进欢迎 issue 或 pr,建议或讨论可在 #12 提出。由于本人水平有限,仓库中的知识点有来自本人原创、读书笔记、书籍、博文等,非原创均已标明出处,如有遗漏,请 issue 提出。本仓库遵循 CC BY-NC-SA 4.0(署名 - 非商业性使用 - 相同方式共享) 协议,转载请注明出处,不得用于商业目的。
(为了方便记忆可以想成)被 const 修饰(在 const 后面)的值不可改变,如下文使用例子中的
p2
、p3
const 使用
// 类
class A
{
private:
const int a; // 常对象成员,可以使用初始化列表或者类内初始化
public:
// 构造函数
A() : a(0) {
};
A(int x) : a(x) {
}; // 初始化列表
// const可用于对重载函数的区分
int getValue(); // 普通成员函数
int getValue() const; // 常成员函数,不得修改类中的任何数据成员的值
};
void function()
{
// 对象
A b; // 普通对象,可以调用全部成员函数
const A a; // 常对象,只能调用常成员函数
const A *p = &a; // 指针变量,指向常对象
const A &q = a; // 指向常对象的引用
// 指针
char greeting[] = "Hello";
char* p1 = greeting; // 指针变量,指向字符数组变量
const char* p2 = greeting; // 指针变量,指向字符数组常量(const 后面是 char,说明指向的字符(char)不可改变)
char* const p3 = greeting; // 自身是常量的指针,指向字符数组变量(const 后面是 p3,说明 p3 指针自身不可改变)
const char* const p4 = greeting; // 自身是常量的指针,指向字符数组常量
}
// 函数
void function1(const int Var); // 传递过来的参数在函数内不可变
void function2(const char* Var); // 参数指针所指内容为常量
void function3(char* const Var); // 参数指针为常量
void function4(const int& Var); // 引用参数在函数内为常量
// 函数返回值
const int function5(); // 返回一个常数
const int* function6(); // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7(); // 返回一个指向变量的常指针,使用:int* const p = function7();
this
指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数所属的那个对象。
当对一个对象调用成员函数时,编译程序先将对象的地址赋给 this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用 this
指针。
当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
this
指针被隐含地声明为: ClassName *const this
,这意味着不能给 this
指针赋值;在 ClassName
类的 const
成员函数中,this
指针的类型为:const ClassName* const
,这说明不能对 this
指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);
this
并不是一个常规变量,而是个右值,所以不能取得 this
的地址(不能 &this
)。
注意:静态成员函数没有this指针
在以下场景中,经常需要显式引用 this
指针:
为实现对象的链式引用;
为避免对同一对象进行赋值操作;
在实现一些数据结构时,如 list
,链式编程,
Person& addAge(Person& p) //这里返回的是引用Person&,才能一直累加,如果返回的是Person,一直是副本,最后值等于28
{
this->age+=p.age;
return *this;//链式编程此处必须使用this指针
}
int main()
{
Person p1(10);
Person p2(18);
p2.addAge(p1).addAge(p1).addAge(p1).....
//再自身的基础上继续累加,链式编程
}
要看成员函数有没有用到this指针,用到会报空指针异常,因为入参用到了this指针。
成员函数声明后面加const,代表常函数,里面不可以修改成员属性了
void showPersion() const
常对象也不可以修改属性了
const Person p1
inline 使用
// 声明1(加 inline,建议使用)
inline int functionName(int first, int second,...);
// 声明2(不加 inline)
int functionName(int first, int second,...);
// 定义
inline int functionName(int first, int second,...) {
/****/};
// 类内定义,隐式内联
class A {
int doA() {
return 0; } // 隐式内联
}
// 类外定义,需要显式内联
class A {
int doA();
}
inline int A::doA() {
return 0; } // 需要显式内联
优点
缺点
Are “inline virtual” member functions ever actually “inlined”?
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()
),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。虚函数内联使用
#include
using namespace std;
class Base
{
public:
inline virtual void who()
{
cout << "I am Base\n";
}
virtual ~Base() {
}
};
class Derived : public Base
{
public:
inline void who() // 不写inline时隐式内联
{
cout << "I am Derived\n";
}
};
int main()
{
// 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。
Base b;
b.who();
// 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。
Base *ptr = new Derived();
ptr->who();
// 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
delete ptr;
ptr = nullptr;
system("pause");
return 0;
}
由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。 虚函数只能借助于指针或者引用来达到多态的效果。
#include
#include
using namespace std;
class A
{
public:
virtual void foo()
{
cout<<"A::foo() is called"<<endl;
}
};
class B:public A
{
public:
void foo()
{
cout<<"B::foo() is called"<<endl;
}
};
int main(void)
{
A *a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!因为class A中使用了virtual修饰
//由a所指的内存中的虚函数表的foo()的位置已经被B::foo()函数地址所取代,于是在实际调用发生时,是B::foo()被调用了。这就实现了多态。
return 0;
}
其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数
volatile int i = 10;
断言,是宏,而非函数。assert 宏的原型定义在
(C)、
(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG
来关闭 assert,但是需要在源代码的开头,include
之前。
assert() 使用
#define NDEBUG // 加上这行,则 assert 不可用
#include
assert( p != NULL ); // assert 不可用
设定结构体、联合以及类成员变量以 n 字节方式对齐
#pragma pack(n) 使用
#pragma pack(push) // 保存对齐状态
#pragma pack(4) // 设定为 4 字节对齐
struct test
{
char m1;
double m4;
int m3;
};
#pragma pack(pop) // 恢复对齐状态
Bit mode: 2; // mode 占 2 位
类可以将其(非静态)数据成员定义为位域(bit-field),在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
extern "C"
修饰的变量和函数是按照 C 语言方式编译和链接的extern "C"
的作用是让 C++ 编译器将 extern "C"
声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。
extern “C” 使用
#ifdef __cplusplus
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif
// c 结构体重命名
typedef struct Student {
int age;
} S;
等价于
// c
struct Student {
int age;
};
typedef struct Student S;
此时 S
等价于 struct Student
,但两个标识符名称空间不相同。
另外还可以定义与 struct Student
不冲突的 void Student() {}
。
由于编译器定位符号的规则(搜索规则)改变,导致不同于C语言。
一、如果在类标识符空间定义了 struct Student {...};
,使用 Student me;
时,编译器将搜索全局标识符表,Student
未找到,则在类标识符内搜索。
即表现为可以使用 Student
也可以使用 struct Student
,这和C语言不同,C语言需要使用struct Student,即必须带上struct,C++不加struct会自己在类标识符中搜索,如下:
// cpp
struct Student {
int age;
};
void func1( Student me ); // 正确,"struct" 关键字可省略
//C
void func2(struct Student me) //C语言必须使用struct
二、若定义了与 Student
同名函数之后,则 Student
只代表函数,不代表结构体,如下:
typedef struct Student {
int age;
} S;
void Student() {
} // 正确,定义后 "Student" 只代表此函数
//void S() {} // 错误,符号 "S" 已经被定义为一个 "struct Student" 的别名
int main() {
Student();
struct Student me; // 或者 "S me";
return 0;
}
可以看出,结构体的重命名和函数名不能重复,但是结构体的原名Student和函数名是可以重复的,并且重复后,Student是代表函数,不会代表结构体名,因此使用Student me;会报错(也就是回归了C,失去了C++搜索规则特性),必须使用 struct Student me;不分谁先定义的顺序。
总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
出现联合的目的是什么: 就是为了省空间,较多域的数据结构里比较省,其实很少使用到,联合的大小就是这个体内占空间最大的那个值,每次改里面的值,都会影响到体内的其他值(其他值变更为未定义)。
联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
union 使用
#include
union UnionTest {
UnionTest() : i(10) {
};
int i;
double d;
};
static union {
int i;
double d;
};
int main() {
UnionTest u;
union {
int i;
double d;
};
std::cout << u.i << std::endl; // 输出 UnionTest 联合的 10
::i = 20;
std::cout << ::i << std::endl; // 输出全局静态匿名联合的 20
i = 30;
std::cout << i << std::endl; // main里 输出局部匿名联合的 30
return 0;
}
C 实现 C++ 的面向对象特性(封装、继承、多态)
封装:使用函数指针把属性与方法封装到结构体中
封装是隐藏对象的属性和实现细节,仅对外公开接口和对象进行交互,将数据和操作数据的方法进行有机结合
继承:结构体嵌套
多态:父类与子类方法的函数指针不同
Can you write object-oriented code in C? [closed]
explicit 使用
struct A
{
A(int) {
}
operator bool() const {
return true; }
};
struct B
{
explicit B(int) {
}
explicit operator bool() const {
return true; }
};
void doA(A a) {
}
void doB(B b) {
}
int main()
{
A a1(1); // OK:直接初始化
A a2 = 1; // OK:复制初始化
A a3{
1 }; // OK:直接列表初始化
A a4 = {
1 }; // OK:复制列表初始化
A a5 = (A)1; // OK:允许 static_cast 的显式转换
doA(1); // OK:允许从 int 到 A 的隐式转换
if (a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a6(a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a7 = a1; // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a8 = static_cast<bool>(a1); // OK :static_cast 进行直接初始化
B b1(1); // OK:直接初始化
B b2 = 1; // 错误:被 explicit 修饰构造函数的对象不可以复制初始化
B b3{
1 }; // OK:直接列表初始化
B b4 = {
1 }; // 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化
B b5 = (B)1; // OK:允许 static_cast 的显式转换
doB(1); // 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换
if (b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
bool b6(b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
bool b7 = b1; // 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换
bool b8 = static_cast<bool>(b1); // OK:static_cast 进行直接初始化
return 0;
}
私有默认构造函数(不然别人通过new Singleton触发默认构造函数会创建多实例)
私有拷贝构造函数(不然会通过拷贝构造函数会创建多实例)
私有类静态成员指针(类外初始化)
新增public访问静态成员指针(唯一对象)接口getInstant
为什么会出现友元:你的家有客厅public,有卧室private,但是你偶尔会让你的基友允许进入你的卧室。这个基友就是友元。全局函数、类的成员函数、整个类都可以作为友元
能访问私有成员
破坏封装性
友元关系不可传递
友元关系的单向性
友元声明的形式及数量不受限制
[友元函数案例](https://www.c nblogs.com/zoneofmine/p/6497933.html)
#include
#include
class MyIntArray
{
public:
MyIntArray();
MyIntArray(int capcity);
MyIntArray(const MyIntArray & myarray);
int getlength();
void setvalue(int pos,int value);
int getvalue(int pos);
void back_push(int value);
~MyIntArray();
private:
int m_capcity;
int m_length;
int * m_arrayAddress;
};
MyIntArray::MyIntArray()
{
m_capcity =100;
m_length=0;
m_arrayAddress =new int[m_capcity];
}
//拷贝构造函数
MyIntArray::MyIntArray(const MyIntArray & myarray)
{
m_capcity =myarray.m_capcity;
m_length=myarray.m_length;
m_arrayAddress =new int[m_capcity];
for(int i= 0;i< m_length;i++)
{
m_arrayAddress[i] = myarray.m_arrayAddress[i];
}
}
MyIntArray::~MyIntArray()
{
if(m_arrayAddress!=NULL)
{
printf("析构函数\n");
delete [] m_arrayAddress;
m_arrayAddress=NULL;
}
}
MyIntArray::MyIntArray(int capcity)
{
m_capcity = capcity;
m_length=0;
m_arrayAddress =new int[m_capcity];
}
int MyIntArray::getlength()
{
return m_length;
}
void MyIntArray::setvalue(int pos,int value)
{
m_arrayAddress[pos]=value;
}
int MyIntArray::getvalue(int pos)
{
return m_arrayAddress[pos];
}
void MyIntArray::back_push(int value)
{
m_arrayAddress[m_length]=value;
m_length++;
}
int main()
{
MyIntArray *myarray= new MyIntArray();
myarray->back_push(12);
myarray->back_push(13);
myarray->back_push(14);
myarray->back_push(15);
myarray->back_push(16);
int length = myarray->getlength();
printf("length=%d\n",length);
printf("getvalue1=%d\n",myarray->getvalue(2));
MyIntArray myarray2;
myarray2.back_push(222);
printf("getvalue2=%d\n",myarray2.getvalue(0));
MyIntArray myarray3(10);
myarray3.back_push(333);
printf("getvalue3=%d\n",myarray3.getvalue(0));
//拷贝构造函数
MyIntArray myarray4(myarray3);
//myarray4.back_push(333);
printf("getvalue4=%d\n",myarray4.getvalue(0));
//调用了三次析构函数,但是对象创建了四次啊
//第一个没有调用析构函数MyIntArray *myarray= new MyIntArray();这是为什么呢?
}
#include
#include
#include
using namespace std;
class MyString
{
public:
MyString();
MyString(char *p);
MyString(const MyString &p);//注意这里必须使用const,否则编译报错:myString.cpp:260:17: 错误:对‘MyString::MyString(MyString)’的调用没有匹配的函数,要识别常数,必须用const
~MyString();
// 为什么重载<<、>>需要使用友元函数:如果是重载双目操作符(即为类的成员函数),就只要设置一个参数作为右侧运算量,而左侧运算量就是对象本身。。。。。。而 >> 或<< 左侧运算量是 cin或cout 而不是对象本身,所以不满足后面一点。。。。。。。。就只能申明为友元函数了。。
friend ostream & operator<<(ostream & cout,MyString & p);
friend istream & operator>>(istream & cin,MyString & p);
char & operator[](int index);
MyString& operator=(char *str);
MyString& operator=(MyString &str);
MyString operator+(char *str);
MyString operator+(MyString &str);
bool operator==(char *p);
bool operator==(MyString &p);
private:
char *m_string;
int m_size;
};
MyString::MyString(char *p)
{
m_string =new char[strlen(p)+1];
strcpy(m_string,p);
m_size = strlen(m_string);
}
MyString::MyString(const MyString & p)
{
m_string =new char[strlen(p.m_string)+1];
strcpy(m_string,p.m_string);
m_size = strlen(p.m_string);
}
MyString::~MyString()
{
if(m_string !=NULL)
{
printf("析构\n");
delete [] m_string;
m_string = NULL;
m_size = 0;
}
}
ostream & operator<<(ostream & cout,MyString &p)
{
cout << p.m_string;
return cout;
}
istream & operator>>(istream & cin,MyString &p)
{
if(p.m_string!=NULL)
{
delete [] p.m_string;
p.m_string = NULL;
}
char buf[1024];
cin >> buf;
p.m_string = new char[strlen(buf)+1];
strcpy(p.m_string,buf);
p.m_size= strlen(p.m_string);
p.m_size= strlen(p.m_string);
return cin;
}
char & MyString::operator[](int index)
{
return m_string[index];
}
MyString& MyString::operator=(char *p)
{
if(m_string !=NULL)
{
delete [] m_string;
m_string = NULL;
}
m_string =new char[strlen(p)+1];
strcpy(m_string,p);
m_size=strlen(m_string);
return *this;
}
MyString& MyString::operator=(MyString &p)
{
if(m_string !=NULL)
{
delete [] m_string;
m_string = NULL;
}
m_string =new char[strlen(p.m_string)+1];
strcpy(m_string,p.m_string);
m_size=strlen(m_string);
return *this;
}
MyString MyString::operator+(char *p)
{
char * tmp = new char[m_size+strlen(p)+1];
strcat(tmp,m_string);
strcat(tmp,p);
MyString p1(tmp);
delete [] tmp;
return p1;
}
MyString MyString::operator+(MyString &p)
{
char * tmp = new char[m_size+strlen(p.m_string)+1];
strcat(tmp,m_string);
strcat(tmp,p.m_string);
MyString p1(tmp);
delete [] tmp;
return p1;
}
bool MyString::operator==(char *p)
{
if(p!=NULL)
{
if(strcmp(m_string,p) == 0)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
bool MyString::operator==(MyString &p)
{
if(p.m_string!=NULL)
{
if(strcmp(m_string,p.m_string) == 0)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
int main()
{
//friend ostream & operator<<(ostream & cout,MyString & p);
MyString mStr1("my name is liuwei");
printf("mStr1:");
cout << mStr1 << endl;
//friend istream & operator>>(istream & cin,MyString & p);
MyString mStr2("liuwei");
cin >> mStr2;
printf("mStr2:");
cout << mStr2 << endl;
//char & operator[](int index);
MyString mStr3(mStr2);
printf("mStr3[0]:");
cout << mStr3[0] << endl;
//MyString & operator=(char *p);
MyString mStr4(mStr3);
mStr4 = "createStr";
printf("mStr4:");
cout << mStr4 << endl;
//MyString & operator=(MyString &p);
MyString mStr5("A");
mStr5 = mStr4;
printf("mStr5:");
cout << mStr5 << endl;
//MyString operator+(char *p);
MyString add("111");
MyString mStr6("mStr6");
//add = mStr6 + "addStr";
printf("mStr6:");
//cout << add << endl;
//MyString operator+(MyString &p);
//MyString mStr7("mStr7");
MyString mStrA="abc";
MyString mStrB="def";
MyString mStr7 = mStrA+ mStrB;
printf("mStr7:");
cout << mStr7 << endl;
//bool operator==(char *p);
MyString mStr8("mStr8");
if(mStr8=="mStrx")
{
printf("mStr8 == mStrx\n");
}
else
{
printf("mStr8 != mStrx\n");
}
//bool operator==(MyString &p);
MyString mStr10("mStr10");
MyString mStr9("mStr10");
if(mStr10==mStr9)
{
printf("mStr10 == mStr9\n");
}
else
{
printf("mStr10 != mStr9\n");
}
return 0;
}
包括成员函数运算符,其中一个this对象作为运算成员,和全局函数运算符,需要多个入参对象
重载运算符还可以函数重载(参数列表不同)
案例<<运算符重载
ostream & operator<<(ostream & count,Person & p1)
{
count <<"m_A=" << p1.mA<<"m_B=" << p1.m_B;
return count
}
注意前置++和后置++的区别
//前置++
MyInter & operator()//返回类型需要使用引用,可以链式追加,不然使用MyInter operator+只会加一次,每次都是副本++。
{
m_Num++;
return *this;
}
//后置++,入参中多一个int
MyInter & operator(int)
{
MyInter tmp = *this;//因为要后输出,所以先临时保存
m_Num++;
return tmp;
}
赋值运算符重载:赋值之前需要先把之前的this释放
Person & operator=(const Person & p)
{
if(this->m_name !=NULL)
{
delete [] this->m_name;
this->name =NULL;
}
this->name =new char[strlen(p.m_name)+1];
strcpy(this->name,p.name);
}
一条 using 声明
语句一次只引入命名空间的一个成员。它使得我们可以清楚知道程序中所引用的到底是哪个名字。如:
using namespace_name::name;
在 C++11 中,派生类能够重用其直接基类定义的构造函数。
class Derived : Base {
public:
using Base::Base;
/* ... */
};
如上 using 声明,对于基类的每个构造函数,编译器都生成一个与之对应(形参列表完全相同)的派生类构造函数。生成如下类型构造函数:
Derived(parms) : Base(args) {
}
using 指示
使得某个特定命名空间中所有名字都可见,这样我们就无需再为它们添加任何前缀限定符了。如:
using namespace_name name;
using 指示
污染命名空间一般说来,使用 using 命令比使用 using 编译命令更安全,这是由于它只导入了指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using编译命令导入所有的名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发出警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。
using 使用
尽量少使用 using 指示
using namespace std;
应该多使用 using 声明
int x;
std::cin >> x ;
std::cout << x << std::endl;
或者
using std::cin;
using std::cout;
using std::endl;
int x;
cin >> x;
cout << x << endl;
::name
):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间class::name
):用于表示指定类型的作用域范围是具体某个类的namespace::name
):用于表示指定类型的作用域范围是具体某个命名空间的:: 使用
int count = 11; // 全局(::)的 count
class A {
public:
static int count; // 类 A 的 count(A::count)
};
int A::count = 21;
void fun()
{
int count = 31; // 初始化局部的 count 为 31
count = 32; // 设置局部的 count 的值为 32
}
int main() {
::count = 12; // 测试 1:设置全局的 count 的值为 12
A::count = 22; // 测试 2:设置类 A 的 count 为 22
fun(); // 测试 3
return 0;
}
enum class open_modes {
input, output, append };
//限定在此类中的枚举
enum color {
red, yellow, green };
enum {
floatPrec = 6, doublePrec = 10 };
decltype 关键字用于检查实体的声明类型或表达式的类型及值分类。语法:
decltype ( expression )
decltype 使用
// 尾置返回允许我们在参数列表之后声明返回类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
// 处理序列
return *beg; // 返回序列中一个元素的引用
}
// 为了使用模板参数成员,必须用 typename
template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
// 处理序列
return *beg; // 返回序列中一个元素的拷贝
}
常规引用,一般表示对象的身份。
右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。
右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:
X& &
、X& &&
、X&& &
可折叠成 X&
X&& &&
可折叠成 X&&
好处
用花括号初始化器列表初始化一个对象,其中对应构造函数接受一个 std::initializer_list
参数.
initializer_list 使用
#include
#include
#include
template <class T>
struct S {
std::vector<T> v;
S(std::initializer_list<T> l) : v(l) {
std::cout << "constructed with a " << l.size() << "-element list\n";
}
void append(std::initializer_list<T> l) {
v.insert(v.end(), l.begin(), l.end());
}
std::pair<const T*, std::size_t> c_arr() const {
return {
&v[0], v.size()}; // 在 return 语句中复制列表初始化
// 这不使用 std::initializer_list
}
};
template <typename T>
void templated_fn(T) {
}
int main()
{
S<int> s = {
1, 2, 3, 4, 5}; // 复制初始化
s.append({
6, 7, 8}); // 函数调用中的列表初始化
std::cout << "The vector size is now " << s.c_arr().second << " ints:\n";
for (auto n : s.v)
std::cout << n << ' ';
std::cout << '\n';
std::cout << "Range-for over brace-init-list: \n";
for (int x : {
-1, -2, -3}) // auto 的规则令此带范围 for 工作
std::cout << x << ' ';
std::cout << '\n';
auto al = {
10, 11, 12}; // auto 的特殊规则
std::cout << "The list bound to auto has size() = " << al.size() << '\n';
// templated_fn({1, 2, 3}); // 编译错误!“ {1, 2, 3} ”不是表达式,
// 它无类型,故 T 无法推导
templated_fn<std::initializer_list<int>>({
1, 2, 3}); // OK
templated_fn<std::vector<int>>({
1, 2, 3}); // 也 OK
}
面向对象程序设计(Object-oriented programming,OOP)是种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。
面向对象三大特征 —— 封装、继承、多态
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。关键字:public, protected, private。不写默认为 private。
public
成员:可以被任意实体访问protected
成员:只允许被子类及本类的成员函数访问private
成员:只允许被本类的成员函数、友元类或友元函数访问基类(父类)——> 派生类(子类)
继承方式图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cvf70haI-1628235096302)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1626951777668.png)]
继承中的构造和析构
构造:先父后子
析构:先子后父
子类不会继承父类中的构造和析构函数
继承中同名成员处理(成员变量及成员函数 )
就近原则,谁近用谁
Son s;
s.m_A;//调用本类Son的m_A;当然如果子类Son中没有m_A成员,则等同于直接调用父类的m_A,
//不管父类中重载的同名函数,直接屏蔽
s.Base::m_A//调用父类的m_A,需要使用作用域。
同名静态成员处理
与上面相同,多一个通过类名访问
...
Son::m_A;
Son::Base::m_A;//第一个::代表通过类名访问,第二个::代表作用域
多继承
class Son:public Base1,public Base2;
对于同名成员,使用作用域区分
菱形继承
解决继承浪费问题,两个父类中的同名成员都被继承
可以通过虚继承方式解决?
`class Sheep: virtual public Animal;class Tuo: virtual public Animal;
原因:
继承父类的内容为vbptr虚基类指针,指向虚基类表vbtable,vbtable中有偏移量,通过偏移量找到唯一的一份数据。
虚继承的工作原理(感觉有点像指针啊,指针省空间,一份地址,任何地方都能赋值,)
The Four Polymorphisms in C++
函数重载
class A
{
public:
void do(int a);
void do(int a, int b);
};
对有父子关系的两个类,C++可以不需要通过
注意:
可以将派生类的对象赋值给基类的指针或引用,反之不可
普通函数(非类成员函数)不能是虚函数
静态函数(static)不能是虚函数
构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
内联函数不能是表现多态性时的虚函数,解释见:虚函数(virtual)可以是内联函数(inline)吗?
多态的满足条件
1、父类中有虚函数
2、子类重写父类的虚函数
3、父类的指针或者引用指向子类的对象
重写:子类与父类的函数名参数都相同
动态多态使用
class Shape // 形状类
{
public:
virtual double calcArea()
{
...
}
virtual ~Shape();
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea();
...
};
class Rect : public Shape // 矩形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
Shape * shape2 = new Rect(5.0, 6.0);
shape1->calcArea(); // 调用圆形类里面的方法
shape2->calcArea(); // 调用矩形类里面的方法
delete shape1;
shape1 = nullptr;
delete shape2;
shape2 = nullptr;
return 0;
}
//案例2
#include
#include
class Animal
{
public:
//如果没有virtual,就是静态联编,只会调用父类的方法,因此会打印动物在说话!
//void speck()
//{
// printf("动物在说话!\n");
//}
//有virtual,虚函数,会是动态联编,后期绑定,可实现子类的方法:小猫在说话!实现了多态
void virtual speck()
{
printf("动物在说话!\n");
}
};
class Cat:public Animal
{
public:
void speck()
{
printf("小猫在说话!\n");
}
};
class Dog:public Animal
{
public:
void speck()
{
printf("小狗在说话!\n");
}
};
//C++里父子关系的两个类,不需要强制转换
void doSpeck(Animal & animal)
{
animal.speck();
}
int main()
{
Cat cat;
doSpeck(cat);
Dog dog;
doSpeck(dog);
//或者
Animal *animal = new Cat;
animal->speck();//执行为子类的函数
return 0;
}
//虚函数是为4,不是虚函数是1,
//vfptr虚函数表指针指向虚函数表,子类发生重写后,会覆盖继承来的父类的函数,但不会修改父类的函数,所以,会执行子类的函数
printf("sizeof class:%s\n",sozeof(Animal));
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4p3eTtMa-1628235096305)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1627610975865.png)]
多态计算器的实现
#define _CRT_SECURE_NO_WARNINGS
#include
using namespace std;
#include
//开发有个原则 --- 开闭原则
// 开闭原则 --- 对扩展进行开放 对修改进行关闭
//利用多态实现计算器
class AbstractCalculator
{
public:
//纯虚函数
virtual int getResult() = 0;
//如果一个类中有纯虚函数出现,那么这个类就无法实例化对象了
//有纯虚函数的类,也称为 抽象类
//virtual int getResult()
//{
// return 0;
//}
int m_A;
int m_B;
};
//如果子类继承了抽象类,那么子类必须要重写父类中的纯虚函数,否则子类也属于抽象类
//class A :public AbstractCalculator
//{
//public:
// virtual int getResult()
// {
// return 0;
// }
//};
//加法计算器
class AddCalculator: public AbstractCalculator
{
public:
virtual int getResult()
{
return m_A + m_B;
}
};
//减法计算器
class SubCalculator :public AbstractCalculator
{
public:
virtual int getResult()
{
return m_A - m_B;
}
};
//乘法计算器
class MultiCalculator :public AbstractCalculator
{
public:
virtual int getResult()
{
return m_A * m_B;
}
};
void test01()
{
//加法计算器
AbstractCalculator * calculator = new AddCalculator;
calculator->m_A = 10;
calculator->m_B = 20;
cout << calculator->getResult() << endl;
delete calculator;
//改为减法计算器
calculator = new SubCalculator;
calculator->m_A = 10;
calculator->m_B = 20;
cout << calculator->getResult() << endl;
}
//多态的好处: 对扩展性提高,组织性强,可读性强
//如果父类中有了虚函数,子类并没有重写父类的虚函数,那么这样的代码是毫无意义的
//如果子类不重写父类虚函数,那么没有用到多态带来的好处 ,而且内部结构还变得更为复杂
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
虚析构函数使用
class Shape
{
public:
Shape(); // 构造函数不能是虚函数
virtual double calcArea();
virtual ~Shape(); // 虚析构函数
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
shape1->calcArea();
delete shape1; // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
shape1 = NULL;
return 0;
}
纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。
virtual int A() = 0;
CSDN . C++ 中的虚函数、纯虚函数区别和联系
.rodata section
,见:目标文件存储结构),存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。C++中的虚函数(表)实现机制以及用C语言对其进行的模拟实现
虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。
底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
用于分配、释放内存
malloc、free 使用
申请内存,确认是否申请成功
char *str = (char*) malloc(100);
assert(str != nullptr);
释放内存后指针置空
free(p);
p = nullptr;
new、delete 使用
申请内存,确认是否申请成功
int main()
{
T* t = new T(); // 先内存分配 ,再构造函数
delete t; // 先析构函数,再内存释放
return 0;
}
定位 new(placement new)允许我们向 new 传递额外的地址参数,从而在预先指定的内存区域创建对象。
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] {
braced initializer list }
place_address
是个指针initializers
提供一个(可能为空的)以逗号分隔的初始值列表Is it legal (and moral) for a member function to say delete this?
合法,但:
new
(不是 new[]
、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的delete this
的成员函数是最后一个调用 this 的成员函数delete this
后面没有调用 this 了delete this
后没有人使用了如何定义一个只能在堆上(栈上)生成对象的类?
方法:将析构函数设置为私有
原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
方法:将 new 和 delete 重载为私有
原因:在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。
头文件:#include
std::auto_ptr<std::string> ps (new std::string(str));
多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源。
weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和 copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。
unique_ptr 是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有着被销毁或编程 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。
被 c++11 弃用,原因是缺乏语言特性如 “针对构造和赋值” 的 std::move
语义,以及其他瑕疵。
move
语义;delete
),unique_ptr 可以管理数组(析构调用 delete[]
);MSDN . 强制转换运算符
向上转换是一种隐式转换。
char*
到 int*
或 One_class*
到 Unrelated_class*
之类的转换,但其本身并不安全)bad_cast 使用
try {
Circle& ref_circle = dynamic_cast<Circle&>(ref_shape);
}
catch (bad_cast b) {
cout << "Caught: " << b.what();
}
typeinfo
typeid、type_info 使用
#include
using namespace std;
class Flyable // 能飞的
{
public:
virtual void takeoff() = 0; // 起飞
virtual void land() = 0; // 降落
};
class Bird : public Flyable // 鸟
{
public:
void foraging() {
...} // 觅食
virtual void takeoff() {
...}
virtual void land() {
...}
virtual ~Bird(){
}
};
class Plane : public Flyable // 飞机
{
public:
void carry() {
...} // 运输
virtual void takeoff() {
...}
virtual void land() {
...}
};
class type_info
{
public:
const char* name() const;
bool operator == (const type_info & rhs) const;
bool operator != (const type_info & rhs) const;
int before(const type_info & rhs) const;
virtual ~type_info();
private:
...
};
void doSomething(Flyable *obj) // 做些事情
{
obj->takeoff();
cout << typeid(*obj).name() << endl; // 输出传入对象类型("class Bird" or "class Plane")
if(typeid(*obj) == typeid(Bird)) // 判断对象类型
{
Bird *bird = dynamic_cast<Bird *>(obj); // 对象转化
bird->foraging();
}
obj->land();
}
int main(){
Bird *b = new Bird();
doSomething(b);
delete b;
b = nullptr;
return 0;
}
const
、enum
、inline
替换 #define
)operator=
返回一个 reference to *this
(用于连锁赋值)operator=
中处理 “自我赋值”new
中使用 []
则 delete []
,new
中不使用 []
则 delete
)(T)expression
、T(expression)
;新式:const_cast(expression)
、dynamic_cast(expression)
、reinterpret_cast(expression)
、static_cast(expression)
、;尽量避免转型、注重效率避免 dynamic_casts、尽量设计成无需转型、可把转型封装成函数、宁可用新式转型)tr1::function
成员变量替换 virtual 函数,将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数)this->
指涉 base class templates 内的成员名称,或藉由一个明白写出的 “base class 资格修饰符” 完成)static_cast
、const_cast
、dynamic_cast
、reinterpret_cast
)&&
,||
和 ,
操作符(&&
与 ||
的重载会用 “函数调用语义” 取代 “骤死式语义”;,
的重载导致不能保证左侧表达式一定比右侧表达式更早被评估)new operator
、operator new
、placement new
、operator new[]
;delete operator
、operator delete
、destructor
、operator delete[]
)STL 方法含义索引
容器 | 底层数据结构 | 时间复杂度 | 有无序 | 可不可重复 | 其他 |
---|---|---|---|---|---|
array | 数组 | 随机读改 O(1) | 无序 | 可重复 | 支持随机访问 |
vector | 数组 | 随机读改、尾部插入、尾部删除 O(1) 头部插入、头部删除 O(n) |
无序 | 可重复 | 支持随机访问 |
deque | 双端队列 | 头尾插入、头尾删除 O(1) | 无序 | 可重复 | 一个中央控制器 + 多个缓冲区,支持首尾快速增删,支持随机访问 |
forward_list | 单向链表 | 插入、删除 O(1) | 无序 | 可重复 | 不支持随机访问 |
list | 双向链表 | 插入、删除 O(1) | 无序 | 可重复 | 不支持随机访问 |
stack | deque / list | 顶部插入、顶部删除 O(1) | 无序 | 可重复 | deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 |
queue | deque / list | 尾部插入、头部删除 O(1) | 无序 | 可重复 | deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 |
priority_queue | vector + max-heap | 插入、删除 O(log2n) | 有序 | 可重复 | vector容器+heap处理规则 |
set | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 不可重复 | |
multiset | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 可重复 | |
map | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 不可重复 | |
multimap | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 可重复 | |
unordered_set | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 不可重复 | |
unordered_multiset | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 可重复 | |
unordered_map | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 不可重复 | |
unordered_multimap | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 可重复 |
算法 | 底层算法 | 时间复杂度 | 可不可重复 |
---|---|---|---|
find | 顺序查找 | O(n) | 可重复 |
sort | 内省排序 | O(n*log2n) | 可重复 |
SqStack.cpp
顺序栈数据结构和图片
typedef struct {
ElemType *elem;
int top;
int size;
int increment;
} SqStack;
队列数据结构
typedef struct {
ElemType * elem;
int front;
int rear;
int maxSize;
}SqQueue;
非循环队列图片
SqQueue.rear++
循环队列图片
SqQueue.rear = (SqQueue.rear + 1) % SqQueue.maxSize
SqList.cpp
顺序表数据结构和图片
typedef struct {
ElemType *elem;
int length;
int size;
int increment;
} SqList;
LinkList.cpp
LinkList_with_head.cpp
链式数据结构
typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode, *LinkList;
链队列图片
单链表图片
双向链表图片
循环链表图片
HashTable.cpp
哈希函数:H(key): K -> D , key ∈ K
Hi = (H(key) + i) % m
Di = 1^2, -1^2, ..., ±(k)^2,(k<=m/2)
H = (H(key) + 伪随机数) % m
线性探测的哈希表数据结构和图片
typedef char KeyType;
typedef struct {
KeyType key;
}RcdType;
typedef struct {
RcdType *rcd;
int size;
int count;
bool *tag;
}HashTable;
函数直接或间接地调用自身
广义表的头尾链表存储表示和图片
// 广义表的头尾链表存储表示
typedef enum {
ATOM, LIST} ElemTag;
// ATOM==0:原子,LIST==1:子表
typedef struct GLNode {
ElemTag tag;
// 公共部分,用于区分原子结点和表结点
union {
// 原子结点和表结点的联合部分
AtomType atom;
// atom 是原子结点的值域,AtomType 由用户定义
struct {
struct GLNode *hp, *tp;
} ptr;
// ptr 是表结点的指针域,prt.hp 和 ptr.tp 分别指向表头和表尾
} a;
} *GList, GLNode;
扩展线性链表存储表示和图片
// 广义表的扩展线性链表存储表示
typedef enum {
ATOM, LIST} ElemTag;
// ATOM==0:原子,LIST==1:子表
typedef struct GLNode1 {
ElemTag tag;
// 公共部分,用于区分原子结点和表结点
union {
// 原子结点和表结点的联合部分
AtomType atom; // 原子结点的值域
struct GLNode1 *hp; // 表结点的表头指针
} a;
struct GLNode1 *tp;
// 相当于线性链表的 next,指向下一个元素结点
} *GList1, GLNode1;
BinaryTree.cpp
二叉树数据结构
typedef struct BiTNode
{
TElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
二叉树顺序存储图片
二叉树链式存储图片
一种不相交的子集所构成的集合 S = {S1, S2, …, Sn}
F(n)=F(n-1)+F(n-2)+1
(1 是根节点,F(n-1) 是左子树的节点数量,F(n-2) 是右子树的节点数量)平衡二叉树图片
平衡二叉树插入新结点导致失衡的子树
调整:
RedBlackTree.cpp
B 树、B+ 树图片
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CwHlMDRu-1628235096335)(https://i.stack.imgur.com/l6UyF.png)]
对于在内部节点的数据,可直接得到,不必根据叶子节点来定位。
B 树、B+ 树区别来自:differences-between-b-trees-and-b-trees、B树和B+树的区别
八叉树图片
八叉树(octree),或称八元树,是一种用于描述三维空间(划分空间)的树状数据结构。八叉树的每个节点表示一个正方体的体积元素,每个节点有八个子节点,这八个子节点所表示的体积元素加在一起就等于父节点的体积。一般中心点作为节点的分叉中心。
排序算法 | 平均时间复杂度 | 最差时间复杂度 | 空间复杂度 | 数据对象稳定性 |
---|---|---|---|---|
冒泡排序 | O(n2) | O(n2) | O(1) | 稳定 |
选择排序 | O(n2) | O(n2) | O(1) | 数组不稳定、链表稳定 |
插入排序 | O(n2) | O(n2) | O(1) | 稳定 |
快速排序 | O(n*log2n) | O(n2) | O(log2n) | 不稳定 |
堆排序 | O(n*log2n) | O(n*log2n) | O(1) | 不稳定 |
归并排序 | O(n*log2n) | O(n*log2n) | O(n) | 稳定 |
希尔排序 | O(n*log2n) | O(n2) | O(1) | 不稳定 |
计数排序 | O(n+m) | O(n+m) | O(n+m) | 稳定 |
桶排序 | O(n) | O(n) | O(m) | 稳定 |
基数排序 | O(k*n) | O(n2) | 稳定 |
- 均按从小到大排列
- k:代表数值中的 “数位” 个数
- n:代表数据规模
- m:代表数据的最大值减最小值
- 来自:wikipedia . 排序算法
查找算法 | 平均时间复杂度 | 空间复杂度 | 查找条件 |
---|---|---|---|
顺序查找 | O(n) | O(1) | 无序或有序 |
二分查找(折半查找) | O(log2n) | O(1) | 有序 |
插值查找 | O(log2(log2n)) | O(1) | 有序 |
斐波那契查找 | O(log2n) | O(1) | 有序 |
哈希查找 | O(1) | O(n) | 无序或有序 |
二叉查找树(二叉搜索树查找) | O(log2n) | ||
红黑树 | O(log2n) | ||
2-3树 | O(log2n - log3n) | ||
B树/B+树 | O(log2n) |
图搜索算法 | 数据结构 | 遍历时间复杂度 | 空间复杂度 |
---|---|---|---|
BFS广度优先搜索 | 邻接矩阵 邻接链表 |
O(|v|2) O(|v|+|E|) |
O(|v|2) O(|v|+|E|) |
DFS深度优先搜索 | 邻接矩阵 邻接链表 |
O(|v|2) O(|v|+|E|) |
O(|v|2) O(|v|+|E|) |
算法 | 思想 | 应用 |
---|---|---|
分治法 | 把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并 | 循环赛日程安排问题、排序算法(快速排序、归并排序) |
动态规划 | 通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,适用于有重叠子问题和最优子结构性质的问题 | 背包问题、斐波那契数列 |
贪心法 | 一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法 | 旅行推销员问题(最短路径问题)、最小生成树、哈夫曼编码 |
对于有线程系统:
对于无线程系统:
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制
进程之间的通信方式以及优缺点来源于:进程线程面试题总结
对比维度 | 多进程 | 多线程 | 总结 |
---|---|---|---|
数据共享、同步 | 数据共享复杂,需要用 IPC;数据是分开的,同步简单 | 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 | 各有优势 |
内存、CPU | 占用内存多,切换复杂,CPU 利用率低 | 占用内存少,切换简单,CPU 利用率高 | 线程占优 |
创建销毁、切换 | 创建销毁、切换复杂,速度慢 | 创建销毁、切换简单,速度很快 | 线程占优 |
编程、调试 | 编程简单,调试简单 | 编程复杂,调试复杂 | 进程占优 |
可靠性 | 进程间不会互相影响 | 一个线程挂掉将导致整个进程挂掉 | 进程占优 |
分布式 | 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 | 适应于多核分布式 | 进程占优 |
优劣 | 多进程 | 多线程 |
---|---|---|
优点 | 编程、调试简单,可靠性较高 | 创建、销毁、切换速度快,内存、资源占用小 |
缺点 | 创建、销毁、切换速度慢,内存、资源占用大 | 编程、调试复杂,可靠性较差 |
多进程与多线程间的对比、优劣与选择来自:多线程还是多进程的选择及区别
在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实像多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问。尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上的执行单元对共享的数据的访问。
来自:Linux 内核的同步机制,第 1 部分、Linux 内核的同步机制,第 2 部分
主机字节序又叫 CPU 字节序,其不是由操作系统决定的,而是由 CPU 指令集架构决定的。主机字节序分为两种:
32 位整数 0x12345678
是从起始位置为 0x00
的地址开始存放,则:
内存地址 | 0x00 | 0x01 | 0x02 | 0x03 |
---|---|---|---|---|
大端 | 12 | 34 | 56 | 78 |
小端 | 78 | 56 | 34 | 12 |
大端小端图片
判断大端小端
可以这样判断自己 CPU 字节序是大端还是小端:
#include
using namespace std;
int main()
{
int i = 0x12345678;
if (*((char*)&i) == 0x12)
cout << "大端" << endl;
else
cout << "小端" << endl;
return 0;
}
网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。
网络字节顺序采用:大端(Big Endian)排列方式。
在地址映射过程中,若在页面中发现所要访问的页面不在内存中,则产生缺页中断。当发生缺页中断时,如果操作系统内存中没有空闲页面,则操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法。
全局:
局部:
本节部分知识点来自《计算机网络(第 7 版)》
计算机网络体系结构:
分层 | 作用 | 协议 |
---|---|---|
物理层 | 通过媒介传输比特,确定机械及电气规范(比特 Bit) | RJ45、CLOCK、IEEE802.3(中继器,集线器) |
数据链路层 | 将比特组装成帧和点到点的传递(帧 Frame) | PPP、FR、HDLC、VLAN、MAC(网桥,交换机) |
网络层 | 负责数据包从源到宿的传递和网际互连(包 Packet) | IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGRP(路由器) |
运输层 | 提供端到端的可靠报文传递和错误恢复( 段Segment) | TCP、UDP、SPX |
会话层 | 建立、管理和终止会话(会话协议数据单元 SPDU) | NFS、SQL、NETBIOS、RPC |
表示层 | 对数据进行翻译、加密和压缩(表示协议数据单元 PPDU) | JPEG、MPEG、ASII |
应用层 | 允许访问OSI环境的手段(应用协议数据单元 APDU) | FTP、DNS、Telnet、SMTP、HTTP、WWW、NFS |
通道:
通道复用技术:
主要信道:
三个基本问题:
SOH - 数据部分 - EOT
点对点协议(Point-to-Point Protocol):
广播通信:
IP 地址分类:
IP 地址 ::= {<网络号>,<主机号>}
IP 地址类别 | 网络号 | 网络范围 | 主机号 | IP 地址范围 |
---|---|---|---|---|
A 类 | 8bit,第一位固定为 0 | 0 —— 127 | 24bit | 1.0.0.0 —— 127.255.255.255 |
B 类 | 16bit,前两位固定为 10 | 128.0 —— 191.255 | 16bit | 128.0.0.0 —— 191.255.255.255 |
C 类 | 24bit,前三位固定为 110 | 192.0.0 —— 223.255.255 | 8bit | 192.0.0.0 —— 223.255.255.255 |
D 类 | 前四位固定为 1110,后面为多播地址 | |||
E 类 | 前五位固定为 11110,后面保留为今后所用 |
IP 数据报格式:
ICMP 报文格式:
应用:
0.0.0.0
, Netmask: 0.0.0.0
)指向自治系统的出口。根据应用和执行的不同,路由表可能含有如下附加信息:
协议:
端口:
应用程序 | FTP | TELNET | SMTP | DNS | TFTP | HTTP | HTTPS | SNMP |
---|---|---|---|---|---|---|---|---|
端口号 | 21 | 23 | 25 | 53 | 69 | 80 | 443 | 161 |
特征:
TCP 如何保证可靠传输:
TCP 报文结构
TCP 首部
TCP:状态控制码(Code,Control Flag),占 6 比特,含义如下:
URG=1
时,表明紧急指针字段有效,代表该封包为紧急封包。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据), 且上图中的 Urgent Pointer 字段也会被启用。ACK=1
时确认号字段才有效,代表这个封包为确认封包。当 ACK=0
时,确认号无效。RST=1
时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。FIN=1
时,表明此报文段的发送端的数据已发送完毕,并要求释放运输连接。特征:
UDP 报文结构
UDP 首部
TCP/UDP 图片来源于:https://github.com/JerryC8080/understand-tcp-udp
TCP 是一个基于字节流的传输服务(UDP 基于报文的),“流” 意味着 TCP 所传输的数据是没有边界的。所以可能会出现两个数据包黏在一起的情况。
\r\n
标记。FTP 协议正是这么做的。但问题在于如果数据正文中也含有 \r\n
,则会误判为消息的边界。流量控制(flow control)就是让发送方的发送速率不要太快,要让接收方来得及接收。
利用可变窗口进行流量控制
拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。
TCP的拥塞控制图
因为 TCP 三次握手建立连接、四次挥手释放连接很重要,所以附上《计算机网络(第 7 版)-谢希仁》书中对此章的详细描述:https://gitee.com/huihut/interview/raw/master/images/TCP-transport-connection-management.png
【TCP 建立连接全过程解释】
【答案一】因为信道不可靠,而 TCP 想在不可靠信道上建立可靠地传输,那么三次通信是理论上的最小值。(而 UDP 则不需建立可靠传输,因此 UDP 不需要三次握手。)
Google Groups . TCP 建立连接为什么是三次握手?{技术}{网络通信}
【答案二】因为双方都需要确认对方收到了自己发送的序列号,确认过程最少要进行三次通信。
知乎 . TCP 为什么是三次握手,而不是两次或四次?
【答案三】为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
《计算机网络(第 7 版)-谢希仁》
【TCP 释放连接全过程解释】
【问题一】TCP 为什么要进行四次挥手? / 为什么 TCP 建立连接需要三次,而释放连接则需要四次?
【答案一】因为 TCP 是全双工模式,客户端请求关闭连接后,客户端向服务端的连接关闭(一二次挥手),服务端继续传输之前没传完的数据给客户端(数据传输),服务端向客户端的连接关闭(三四次挥手)。所以 TCP 释放连接时服务器的 ACK 和 FIN 是分开发送的(中间隔着数据传输),而 TCP 建立连接时服务器的 ACK 和 SYN 是一起发送的(第二次握手),所以 TCP 建立连接需要三次,而释放连接则需要四次。
【问题二】为什么 TCP 连接时可以 ACK 和 SYN 一起发送,而释放时则 ACK 和 FIN 分开发送呢?(ACK 和 FIN 分开是指第二次和第三次挥手)
【答案二】因为客户端请求释放时,服务器可能还有数据需要传输给客户端,因此服务端要先响应客户端 FIN 请求(服务端发送 ACK),然后数据传输,传输完成后,服务端再提出 FIN 请求(服务端发送 FIN);而连接时则没有中间的数据传输,因此连接时可以 ACK 和 SYN 一起发送。
【问题三】为什么客户端释放最后需要 TIME-WAIT 等待 2MSL 呢?
【答案三】
TCP 有限状态机图片
域名:
域名 ::= {<三级域名>.<二级域名>.<顶级域名>}
,如:blog.huihut.com
TELNET 协议是 TCP/IP 协议族中的一员,是 Internet 远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。
HTTP(HyperText Transfer Protocol,超文本传输协议)是用于从 WWW(World Wide Web,万维网)服务器传输超文本到本地浏览器的传送协议。
SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。
Socket 建立网络通信连接至少要一对端口号(Socket)。Socket 本质是编程接口(API),对 TCP/IP 的封装,TCP/IP 也要提供可供程序员做网络开发所用的接口,这就是 Socket 编程接口。
标准格式:
协议类型:[//服务器地址[:端口号]][/资源层级UNIX文件路径]文件名[?查询][#片段ID]
完整格式:
协议类型:[//[访问资源需要的凭证信息@]服务器地址[:端口号]][/资源层级UNIX文件路径]文件名[?查询][#片段ID]
其中【访问凭证信息@;:端口号;?查询;#片段ID】都属于选填项
如:https://github.com/huihut/interview#cc
HTTP(HyperText Transfer Protocol,超文本传输协议)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP 是万维网的数据通信的基础。
请求方法
方法 | 意义 |
---|---|
OPTIONS | 请求一些选项信息,允许客户端查看服务器的性能 |
GET | 请求指定的页面信息,并返回实体主体 |
HEAD | 类似于 get 请求,只不过返回的响应中没有具体的内容,用于获取报头 |
POST | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改 |
PUT | 从客户端向服务器传送的数据取代指定的文档的内容 |
DELETE | 请求服务器删除指定的页面 |
TRACE | 回显服务器收到的请求,主要用于测试或诊断 |
状态码(Status-Code)
更多状态码:菜鸟教程 . HTTP状态码
Linux Socket 编程(不限 Linux)
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
我们知道 TCP 建立连接要进行 “三次握手”,即交换三个分组。大致流程如下:
只有就完了三次握手,但是这个三次握手发生在 Socket 的那几个函数中呢?请看下图:
从图中可以看出:
上面介绍了 socket 中 TCP 的三次握手建立过程,及其涉及的 socket 函数。现在我们介绍 socket 中的四次握手释放连接的过程,请看下图:
图示过程如下:
这样每个方向上都有一个 FIN 和 ACK。
本节部分知识点来自《数据库系统概论(第 5 版)》
关系名(属性1, 属性2, ..., 属性n)
对象类型 | 对象 | 操作类型 |
---|---|---|
数据库模式 | 模式 | CREATE SCHEMA |
基本表 | CREATE SCHEMA ,ALTER TABLE |
|
视图 | CREATE VIEW |
|
索引 | CREATE INDEX |
|
数据 | 基本表和视图 | SELECT ,INSERT ,UPDATE ,DELETE ,REFERENCES ,ALL PRIVILEGES |
属性列 | SELECT ,INSERT ,UPDATE ,REFERENCES ,ALL PRIVILEGES |
SQL 语法教程:runoob . SQL 教程
各大设计模式例子参考:CSDN专栏 . C++ 设计模式 系列博文
设计模式工程目录
单例模式例子
抽象工厂模式例子
适配器模式例子
桥接模式例子
观察者模式例子
本节部分知识点来自《程序员的自我修养——链接装载库》
一般应用程序内存空间有如下区域:
栈保存了一个函数调用所需要的维护信息,常被称为堆栈帧(Stack Frame)或活动记录(Activate Record),一般包含以下几方面:
堆分配算法:
典型的非法指针解引用造成的错误。当指针指向一个不允许读写的内存地址,而程序却试图利用指针来读或写该地址时,会出现这个错误。
普遍原因:
平台 | 可执行文件 | 目标文件 | 动态库/共享对象 | 静态库 |
---|---|---|---|---|
Windows | exe | obj | dll | lib |
Unix/Linux | ELF、out | o | so | a |
Mac | Mach-O | o | dylib、tbd、framework | a、framework |
#include
、#define
等预编译指令,生成 .i
或 .ii
文件).s
文件).o
文件).out
文件)现在版本 GCC 把预编译和编译合成一步,预编译编译程序 cc1、汇编器 as、连接器 ld
MSVC 编译环境,编译器 cl、连接器 link、可执行文件查看器 dumpbin
编译器编译源代码后生成的文件叫做目标文件。目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。
可执行文件(Windows 的
.exe
和 Linux 的ELF
)、动态链接库(Windows 的.dll
和 Linux 的.so
)、静态链接库(Windows 的.lib
和 Linux 的.a
)都是按照可执行文件格式存储(Windows 按照 PE-COFF,Linux 按照 ELF)
.obj
格式.o
格式a.out
格式.COM
格式PE 和 ELF 都是 COFF(Common File Format)的变种
段 | 功能 |
---|---|
File Header | 文件头,描述整个文件的文件属性(包括文件是否可执行、是静态链接或动态连接及入口地址、目标硬件、目标操作系统等) |
.text section | 代码段,执行语句编译成的机器代码 |
.data section | 数据段,已初始化的全局变量和局部静态变量 |
.bss section | BSS 段(Block Started by Symbol),未初始化的全局变量和局部静态变量(因为默认值为 0,所以只是在此预留位置,不占空间) |
.rodata section | 只读数据段,存放只读数据,一般是程序里面的只读变量(如 const 修饰的变量)和字符串常量 |
.comment section | 注释信息段,存放编译器版本信息 |
.note.GNU-stack section | 堆栈提示段 |
其他段略
在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。
如下符号表(Symbol Table):
Symbol(符号名) | Symbol Value (地址) |
---|---|
main | 0x100 |
Add | 0x123 |
… | … |
Linux 下的共享库就是普通的 ELF 共享对象。
共享库版本更新应该保证二进制接口 ABI(Application Binary Interface)的兼容
libname.so.x.y.z
大部分包括 Linux 在内的开源系统遵循 FHS(File Hierarchy Standard)的标准,这标准规定了系统文件如何存放,包括各个目录结构、组织和作用。
/lib
:存放系统最关键和最基础的共享库,如动态链接器、C 语言运行库、数学库等/usr/lib
:存放非系统运行时所需要的关键性的库,主要是开发库/usr/local/lib
:存放跟操作系统本身并不十分相关的库,主要是一些第三方应用程序的库动态链接器会在
/lib
、/usr/lib
和由/etc/ld.so.conf
配置文件指定的,目录中查找共享库
LD_LIBRARY_PATH
:临时改变某个应用程序的共享库查找路径,而不会影响其他应用程序LD_PRELOAD
:指定预先装载的一些共享库甚至是目标文件LD_DEBUG
:打开动态链接器的调试功能使用 CLion 编写共享库
创建一个名为 MySharedLib 的共享库
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MySharedLib)
set(CMAKE_CXX_STANDARD 11)
add_library(MySharedLib SHARED library.cpp library.h)
library.h
#ifndef MYSHAREDLIB_LIBRARY_H
#define MYSHAREDLIB_LIBRARY_H
// 打印 Hello World!
void hello();
// 使用可变模版参数求和
template <typename T>
T sum(T t)
{
return t;
}
template <typename T, typename ...Types>
T sum(T first, Types ... rest)
{
return first + sum<T>(rest...);
}
#endif
library.cpp
#include
#include "library.h"
void hello() {
std::cout << "Hello, World!" << std::endl;
}
使用 CLion 调用共享库
创建一个名为 TestSharedLib 的可执行项目
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(TestSharedLib)
# C++11 编译
set(CMAKE_CXX_STANDARD 11)
# 头文件路径
set(INC_DIR /home/xx/code/clion/MySharedLib)
# 库文件路径
set(LIB_DIR /home/xx/code/clion/MySharedLib/cmake-build-debug)
include_directories(${INC_DIR})
link_directories(${LIB_DIR})
link_libraries(MySharedLib)
add_executable(TestSharedLib main.cpp)
# 链接 MySharedLib 库
target_link_libraries(TestSharedLib MySharedLib)
main.cpp
#include
#include "library.h"
using std::cout;
using std::endl;
int main() {
hello();
cout << "1 + 2 = " << sum(1,2) << endl;
cout << "1 + 2 + 3 = " << sum(1,2,3) << endl;
return 0;
}
执行结果
Hello, World!
1 + 2 = 3
1 + 2 + 3 = 6
/SUBSYSTEM:WINDOWS
/SUBSYSTEM:CONSOLE
_tWinMain 与 _tmain 函数声明
Int WINAPI _tWinMain(
HINSTANCE hInstanceExe,
HINSTANCE,
PTSTR pszCmdLine,
int nCmdShow);
int _tmain(
int argc,
TCHAR *argv[],
TCHAR *envp[]);
应用程序类型 | 入口点函数 | 嵌入可执行文件的启动函数 |
---|---|---|
处理ANSI字符(串)的GUI应用程序 | _tWinMain(WinMain) | WinMainCRTSartup |
处理Unicode字符(串)的GUI应用程序 | _tWinMain(wWinMain) | wWinMainCRTSartup |
处理ANSI字符(串)的CUI应用程序 | _tmain(Main) | mainCRTSartup |
处理Unicode字符(串)的CUI应用程序 | _tmain(wMain) | wmainCRTSartup |
动态链接库(Dynamic-Link Library) | DllMain | _DllMainCRTStartup |
部分知识点来自《Windows 核心编程(第五版)》
DllMain 函数
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
// 第一次将一个DLL映射到进程地址空间时调用
// The DLL is being mapped into the process' address space.
break;
case DLL_THREAD_ATTACH:
// 当进程创建一个线程的时候,用于告诉DLL执行与线程相关的初始化(非主线程执行)
// A thread is bing created.
break;
case DLL_THREAD_DETACH:
// 系统调用 ExitThread 线程退出前,即将终止的线程通过告诉DLL执行与线程相关的清理
// A thread is exiting cleanly.
break;
case DLL_PROCESS_DETACH:
// 将一个DLL从进程的地址空间时调用
// The DLL is being unmapped from the process' address space.
break;
}
return (TRUE); // Used only for DLL_PROCESS_ATTACH
}
LoadLibrary、LoadLibraryExA、LoadPackagedLibrary、FreeLibrary、FreeLibraryAndExitThread 函数声明
// 载入库
HMODULE WINAPI LoadLibrary(
_In_ LPCTSTR lpFileName
);
HMODULE LoadLibraryExA(
LPCSTR lpLibFileName,
HANDLE hFile,
DWORD dwFlags
);
// 若要在通用 Windows 平台(UWP)应用中加载 Win32 DLL,需要调用 LoadPackagedLibrary,而不是 LoadLibrary 或 LoadLibraryEx
HMODULE LoadPackagedLibrary(
LPCWSTR lpwLibFileName,
DWORD Reserved
);
// 卸载库
BOOL WINAPI FreeLibrary(
_In_ HMODULE hModule
);
// 卸载库和退出线程
VOID WINAPI FreeLibraryAndExitThread(
_In_ HMODULE hModule,
_In_ DWORD dwExitCode
);
GetProcAddress 函数声明
FARPROC GetProcAddress(
HMODULE hInstDll,
PCSTR pszSymbolName // 只能接受 ANSI 字符串,不能是 Unicode
);
在 VS 的开发人员命令提示符
使用 DumpBin.exe
可查看 DLL 库的导出段(导出的变量、函数、类名的符号)、相对虚拟地址(RVA,relative virtual address)。如:
DUMPBIN -exports D:\mydll.dll
LoadLibrary 与 FreeLibrary 流程图
DLL 库的编写(导出一个 DLL 模块)
DLL 头文件
// MyLib.h
#ifdef MYLIBAPI
// MYLIBAPI 应该在全部 DLL 源文件的 include "Mylib.h" 之前被定义
// 全部函数/变量正在被导出
#else
// 这个头文件被一个exe源代码模块包含,意味着全部函数/变量被导入
#define MYLIBAPI extern "C" __declspec(dllimport)
#endif
// 这里定义任何的数据结构和符号
// 定义导出的变量(避免导出变量)
MYLIBAPI int g_nResult;
// 定义导出函数原型
MYLIBAPI int Add(int nLeft, int nRight);
DLL 源文件
// MyLibFile1.cpp
// 包含标准Windows和C运行时头文件
#include
// DLL源码文件导出的函数和变量
#define MYLIBAPI extern "C" __declspec(dllexport)
// 包含导出的数据结构、符号、函数、变量
#include "MyLib.h"
// 将此DLL源代码文件的代码放在此处
int g_nResult;
int Add(int nLeft, int nRight)
{
g_nResult = nLeft + nRight;
return g_nResult;
}
DLL 库的使用(运行时动态链接 DLL)
// A simple program that uses LoadLibrary and
// GetProcAddress to access myPuts from Myputs.dll.
#include
#include
typedef int (__cdecl *MYPROC)(LPWSTR);
int main( void )
{
HINSTANCE hinstLib;
MYPROC ProcAdd;
BOOL fFreeResult, fRunTimeLinkSuccess = FALSE;
// Get a handle to the DLL module.
hinstLib = LoadLibrary(TEXT("MyPuts.dll"));
// If the handle is valid, try to get the function address.
if (hinstLib != NULL)
{
ProcAdd = (MYPROC) GetProcAddress(hinstLib, "myPuts");
// If the function address is valid, call the function.
if (NULL != ProcAdd)
{
fRunTimeLinkSuccess = TRUE;
(ProcAdd) (L"Message sent to the DLL function\n");
}
// Free the DLL module.
fFreeResult = FreeLibrary(hinstLib);
}
// If unable to call the DLL function, use an alternative.
if (! fRunTimeLinkSuccess)
printf("Message printed from executable\n");
return 0;
}
一个程序的 I/O 指代程序与外界的交互,包括文件、管程、网络、命令行、信号等。更广义地讲,I/O 指代操作系统理解为 “文件” 的事物。
_start -> __libc_start_main -> exit -> _exit
其中 main(argc, argv, __environ)
函数在 __libc_start_main
里执行。
int mainCRTStartup(void)
执行如下操作:
大致包含如下功能:
包含:
huihut/CS-Books: Computer Science Books 计算机技术类书籍 PDF
C/C++ 发展方向甚广,包括不限于以下方向, 以下列举一些大厂校招岗位要求。
【后台开发】
【PC 客户端开发】
【游戏客户端开发】
【测试开发】
【安全技术】
【嵌入式应用开发】
【音视频编解码】
【计算机视觉研究】
Avalive:一个面部捕捉的虚拟形象扮演软件。
本仓库遵循 CC BY-NC-SA 4.0(署名 - 非商业性使用 - 相同方式共享) 协议,转载请注明出处,不得用于商业目的。