💡 关于
📚 本仓库是面向 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();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; } // 需要显式内联优点
内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。内联函数在运行时可调试,而宏定义不可以。缺点
代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。Are “inline virtual” member functions ever actually “inlined”?
虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。虚函数内联使用
#include <iostream> 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; }断言,是宏,而非函数。assert 宏的原型定义在 <assert.h>(C)、<cassert>(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG 来关闭 assert,但是需要在源代码的开头,include <assert.h> 之前。
assert() 使用
#define NDEBUG // 加上这行,则 assert 不可用 #include <assert.h> 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-field),在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
位域在内存中的布局是与机器有关的位域的类型必须是整型或枚举类型,带符号类型中的位域的行为将因具体实现而定取地址运算符(&)不能作用于位域,任何指针都无法指向类的位域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 struct Student { int age; }; typedef struct Student S;此时 S 等价于 struct Student,但两个标识符名称空间不相同。
另外还可以定义与 struct Student 不冲突的 void Student() {}。
由于编译器定位符号的规则(搜索规则)改变,导致不同于C语言。
一、如果在类标识符空间定义了 struct Student {...};,使用 Student me; 时,编译器将搜索全局标识符表,Student 未找到,则在类标识符内搜索。
即表现为可以使用 Student 也可以使用 struct Student,如下:
// cpp struct Student { int age; }; void f( Student me ); // 正确,"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; }总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
默认访问控制符为 public可以含有构造函数、析构函数不能含有引用类型的成员不能继承自其他类,不能作为基类不能含有虚函数匿名 union 在定义所在作用域可直接访问 union 成员匿名 union 不能包含 protected 成员或 private 成员全局匿名联合必须是静态(static)的union 使用
#include<iostream> 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; // 输出局部匿名联合的 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; }一条 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 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;:: 使用
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; }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),它的主要目的有两个方面:
消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。能够更简洁明确地定义泛型函数。好处
更高效:少了一次调用默认构造函数的过程。有些场合必须要用初始化列表: 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化用花括号初始化器列表初始化一个对象,其中对应构造函数接受一个 std::initializer_list 参数.
initializer_list 使用
#include <iostream> #include <vector> #include <initializer_list> 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 成员:只允许被本类的成员函数、友元类或友元函数访问The Four Polymorphisms in C++
函数重载
class A { public: void do(int a); void do(int a, int b); };注意:
普通函数(非类成员函数)不能是虚函数静态函数(static)不能是虚函数构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)内联函数不能是表现多态性时的虚函数,解释见:虚函数(virtual)可以是内联函数(inline)吗?动态多态使用
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; }虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
虚析构函数使用
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;. C++ 中的虚函数、纯虚函数区别和联系
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?
合法,但:
必须保证 this 对象是通过 new(不是 new[]、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的必须保证调用 delete this 的成员函数是最后一个调用 this 的成员函数必须保证成员函数的 delete this 后面没有调用 this 了必须保证 delete this 后没有人使用了如何定义一个只能在堆上(栈上)生成对象的类?
方法:将析构函数设置为私有
原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
方法:将 new 和 delete 重载为私有
原因:在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。
头文件:#include <memory>
多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源。
支持定制型删除器(custom deleter),可防范 Cross-DLL 问题(对象在动态链接库(DLL)中被 new 创建,却在另一个 DLL 内被 delete 销毁)、自动解除互斥锁weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和 copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。
可打破环状引用(cycles of references,两个其实已经没有被使用的对象彼此互指,使之看似还在 “被使用” 的状态)的问题unique_ptr 是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有着被销毁或编程 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。
unique_ptr 用于取代 auto_ptr被 c++11 弃用,原因是缺乏语言特性如 “针对构造和赋值” 的 std::move 语义,以及其他瑕疵。
MSDN . 强制转换运算符
向上转换是一种隐式转换。
bad_cast 使用
try { Circle& ref_circle = dynamic_cast<Circle&>(ref_shape); } catch (bad_cast b) { cout << "Caught: " << b.what(); }typeid、type_info 使用
#include <iostream> 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; }STL 方法含义索引
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
线性探测的哈希表数据结构和图片
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}
平衡二叉树图片
平衡二叉树插入新结点导致失衡的子树
调整:
LL 型:根的左孩子右旋RR 型:根的右孩子左旋LR 型:根的左孩子左旋,再右旋RL 型:右孩子的左子树,先右旋,再左旋RedBlackTree.cpp
B 树、B+ 树图片
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8St2ZqwI-1593672719708)(https://i.stack.imgur.com/l6UyF.png)]
对于在内部节点的数据,可直接得到,不必根据叶子节点来定位。
B 树、B+ 树区别来自:differences-between-b-trees-and-b-trees、B树和B+树的区别
八叉树图片
八叉树(octree),或称八元树,是一种用于描述三维空间(划分空间)的树状数据结构。八叉树的每个节点表示一个正方体的体积元素,每个节点有八个子节点,这八个子节点所表示的体积元素加在一起就等于父节点的体积。一般中心点作为节点的分叉中心。
对于有线程系统:
进程是资源分配的独立单位线程是资源调度的独立单位对于无线程系统:
进程是资源调度、分配的独立单位线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制
进程之间的通信方式以及优缺点来源于:进程线程面试题总结
多进程与多线程间的对比、优劣与选择来自:多线程还是多进程的选择及区别
在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实像多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问。尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上的执行单元对共享的数据的访问。
来自:Linux 内核的同步机制,第 1 部分、Linux 内核的同步机制,第 2 部分
主机字节序又叫 CPU 字节序,其不是由操作系统决定的,而是由 CPU 指令集架构决定的。主机字节序分为两种:
大端字节序(Big Endian):高序字节存储在低位地址,低序字节存储在高位地址小端字节序(Little Endian):高序字节存储在高位地址,低序字节存储在低位地址32 位整数 0x12345678 是从起始位置为 0x00 的地址开始存放,则:
内存地址0x000x010x020x03大端12345678小端78563412大端小端图片
判断大端小端
可以这样判断自己 CPU 字节序是大端还是小端:
#include <iostream> using namespace std; int main() { int i = 0x12345678; if (*((char*)&i) == 0x12) cout << "大端" << endl; else cout << "小端" << endl; return 0; }网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。
网络字节顺序采用:大端(Big Endian)排列方式。
在地址映射过程中,若在页面中发现所要访问的页面不在内存中,则产生缺页中断。当发生缺页中断时,如果操作系统内存中没有空闲页面,则操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法。
全局:
工作集算法缺页率置换算法局部:
最佳置换算法(OPT)先进先出置换算法(FIFO)最近最久未使用(LRU)算法时钟(Clock)置换算法本节部分知识点来自《计算机网络(第 7 版)》
计算机网络体系结构:
通道:
单向通道(单工通道):只有一个方向通信,没有反方向交互,如广播双向交替通信(半双工通信):通信双方都可发消息,但不能同时发送或接收双向同时通信(全双工通信):通信双方可以同时发送和接收信息通道复用技术:
频分复用(FDM,Frequency Division Multiplexing):不同用户在不同频带,所用用户在同样时间占用不同带宽资源时分复用(TDM,Time Division Multiplexing):不同用户在同一时间段的不同时间片,所有用户在不同时间占用同样的频带宽度波分复用(WDM,Wavelength Division Multiplexing):光的频分复用码分复用(CDM,Code Division Multiplexing):不同用户使用不同的码,可以在同样时间使用同样频带通信主要信道:
点对点信道广播信道三个基本问题:
封装成帧:把网络层的 IP 数据报封装成帧,SOH - 数据部分 - EOT透明传输:不管数据部分什么字符,都能传输出去;可以通过字节填充方法解决(冲突字符前加转义字符)差错检测:降低误码率(BER,Bit Error Rate),广泛使用循环冗余检测(CRC,Cyclic Redundancy Check)点对点协议(Point-to-Point Protocol):
点对点协议(Point-to-Point Protocol):用户计算机和 ISP 通信时所使用的协议广播通信:
硬件地址(物理地址、MAC 地址)单播(unicast)帧(一对一):收到的帧的 MAC 地址与本站的硬件地址相同广播(broadcast)帧(一对全体):发送给本局域网上所有站点的帧多播(multicast)帧(一对多):发送给本局域网上一部分站点的帧IP 地址分类:
IP 地址 ::= {<网络号>,<主机号>} IP 地址类别网络号网络范围主机号IP 地址范围A 类8bit,第一位固定为 00 —— 12724bit1.0.0.0 —— 127.255.255.255B 类16bit,前两位固定为 10128.0 —— 191.25516bit128.0.0.0 —— 191.255.255.255C 类24bit,前三位固定为 110192.0.0 —— 223.255.2558bit192.0.0.0 —— 223.255.255.255D 类前四位固定为 1110,后面为多播地址E 类前五位固定为 11110,后面保留为今后所用IP 数据报格式:
ICMP 报文格式:
应用:
PING(Packet InterNet Groper,分组网间探测)测试两个主机之间的连通性TTL(Time To Live,生存时间)该字段指定 IP 包被路由器丢弃之前允许通过的最大网段数量根据应用和执行的不同,路由表可能含有如下附加信息:
花费(Cost):就是数据发送过程中通过路径所需要的花费。路由的服务质量路由中需要过滤的出/入连接列表协议:
TCP(Transmission Control Protocol,传输控制协议)UDP(User Datagram Protocol,用户数据报协议)端口:
应用程序FTPTELNETSMTPDNSTFTPHTTPHTTPSSNMP端口号212325536980443161特征:
面向连接只能点对点(一对一)通信可靠交互全双工通信面向字节流TCP 如何保证可靠传输:
确认和超时重传数据合理分片和排序流量控制拥塞控制数据校验TCP 报文结构
TCP 首部
TCP:状态控制码(Code,Control Flag),占 6 比特,含义如下:
URG:紧急比特(urgent),当 URG=1 时,表明紧急指针字段有效,代表该封包为紧急封包。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据), 且上图中的 Urgent Pointer 字段也会被启用。ACK:确认比特(Acknowledge)。只有当 ACK=1 时确认号字段才有效,代表这个封包为确认封包。当 ACK=0 时,确认号无效。PSH:(Push function)若为 1 时,代表要求对方立即传送缓冲区内的其他对应封包,而无需等缓冲满了才送。RST:复位比特(Reset),当 RST=1 时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。SYN:同步比特(Synchronous),SYN 置为 1,就表示这是一个连接请求或连接接受报文,通常带有 SYN 标志的封包表示『主动』要连接到对方的意思。FIN:终止比特(Final),用来释放一个连接。当 FIN=1 时,表明此报文段的发送端的数据已发送完毕,并要求释放运输连接。特征:
无连接尽最大努力交付面向报文没有拥塞控制支持一对一、一对多、多对一、多对多的交互通信首部开销小UDP 报文结构
UDP 首部
TCP/UDP 图片来源于:https://github.com/JerryC8080/understand-tcp-udp
TCP 是一个基于字节流的传输服务(UDP 基于报文的),“流” 意味着 TCP 所传输的数据是没有边界的。所以可能会出现两个数据包黏在一起的情况。
流量控制(flow control)就是让发送方的发送速率不要太快,要让接收方来得及接收。
利用可变窗口进行流量控制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1f23IEmF-1593672719718)(https://raw.githubusercontent.com/huihut/interview/master/images/利用可变窗口进行流量控制举例.png)]
拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。
TCP的拥塞控制图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PVzGohwq-1593672719719)(https://raw.githubusercontent.com/huihut/interview/master/images/TCP拥塞窗口cwnd在拥塞控制时的变化情况.png)]
因为 TCP 三次握手建立连接、四次挥手释放连接很重要,所以附上《计算机网络(第 7 版)-谢希仁》书中对此章的详细描述:https://raw.githubusercontent.com/huihut/interview/master/images/TCP-transport-connection-management.png
【TCP 建立连接全过程解释】
客户端发送 SYN 给服务器,说明客户端请求建立连接;服务端收到客户端发的 SYN,并回复 SYN+ACK 给客户端(同意建立连接);客户端收到服务端的 SYN+ACK 后,回复 ACK 给服务端(表示客户端收到了服务端发的同意报文);服务端收到客户端的 ACK,连接已建立,可以数据传输。【答案一】因为信道不可靠,而 TCP 想在不可靠信道上建立可靠地传输,那么三次通信是理论上的最小值。(而 UDP 则不需建立可靠传输,因此 UDP 不需要三次握手。)
Google Groups . TCP 建立连接为什么是三次握手?{技术}{网络通信}
【答案二】因为双方都需要确认对方收到了自己发送的序列号,确认过程最少要进行三次通信。
知乎 . TCP 为什么是三次握手,而不是两次或四次?
【答案三】为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
《计算机网络(第 7 版)-谢希仁》
【TCP 释放连接全过程解释】
客户端发送 FIN 给服务器,说明客户端不必发送数据给服务器了(请求释放从客户端到服务器的连接);服务器接收到客户端发的 FIN,并回复 ACK 给客户端(同意释放从客户端到服务器的连接);客户端收到服务端回复的 ACK,此时从客户端到服务器的连接已释放(但服务端到客户端的连接还未释放,并且客户端还可以接收数据);服务端继续发送之前没发完的数据给客户端;服务端发送 FIN+ACK 给客户端,说明服务端发送完了数据(请求释放从服务端到客户端的连接,就算没收到客户端的回复,过段时间也会自动释放);客户端收到服务端的 FIN+ACK,并回复 ACK 给客户端(同意释放从服务端到客户端的连接);服务端收到客户端的 ACK 后,释放从服务端到客户端的连接。【问题一】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 呢?
【答案三】
为了保证客户端发送的最后一个 ACK 报文能够到达服务端。若未成功到达,则服务端超时重传 FIN+ACK 报文段,客户端再重传 ACK,并重新计时。防止已失效的连接请求报文段出现在本连接中。TIME-WAIT 持续 2MSL 可使本连接持续的时间内所产生的所有报文段都从网络中消失,这样可使下次连接中不会出现旧的连接报文段。TCP 有限状态机图片
域名:
域名 ::= {<三级域名>.<二级域名>.<顶级域名>},如:blog.huihut.comTELNET 协议是 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)
1xx:表示通知信息,如请求收到了或正在进行处理 100 Continue:继续,客户端应继续其请求101 Switching Protocols 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到 HTTP 的新版本协议 2xx:表示成功,如接收或知道了 200 OK: 请求成功 3xx:表示重定向,如要完成请求还必须采取进一步的行动 301 Moved Permanently: 永久移动。请求的资源已被永久的移动到新 URL,返回信息会包括新的 URL,浏览器会自动定向到新 URL。今后任何新的请求都应使用新的 URL 代替 4xx:表示客户的差错,如请求中有错误的语法或不能完成 400 Bad Request: 客户端请求的语法错误,服务器无法理解401 Unauthorized: 请求要求用户的身份认证403 Forbidden: 服务器理解请求客户端的请求,但是拒绝执行此请求(权限不够)404 Not Found: 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置 “您所请求的资源无法找到” 的个性页面408 Request Timeout: 服务器等待客户端发送的请求时间过长,超时 5xx:表示服务器的差错,如服务器失效无法完成请求 500 Internal Server Error: 服务器内部错误,无法完成请求503 Service Unavailable: 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的 Retry-After 头信息中504 Gateway Timeout: 充当网关或代理的服务器,未及时从远端服务器获取请求更多状态码:菜鸟教程 . HTTP状态码
Linux Socket 编程(不限 Linux)
我们知道 TCP 建立连接要进行 “三次握手”,即交换三个分组。大致流程如下:
客户端向服务器发送一个 SYN J服务器向客户端响应一个 SYN K,并对 SYN J 进行确认 ACK J+1客户端再想服务器发一个确认 ACK K+1只有就完了三次握手,但是这个三次握手发生在 Socket 的那几个函数中呢?请看下图:
从图中可以看出:
当客户端调用 connect 时,触发了连接请求,向服务器发送了 SYN J 包,这时 connect 进入阻塞状态;服务器监听到连接请求,即收到 SYN J 包,调用 accept 函数接收请求向客户端发送 SYN K ,ACK J+1,这时 accept 进入阻塞状态;客户端收到服务器的 SYN K ,ACK J+1 之后,这时 connect 返回,并对 SYN K 进行确认;服务器收到 ACK K+1 时,accept 返回,至此三次握手完毕,连接建立。上面介绍了 socket 中 TCP 的三次握手建立过程,及其涉及的 socket 函数。现在我们介绍 socket 中的四次握手释放连接的过程,请看下图:
图示过程如下:
某个应用进程首先调用 close 主动关闭连接,这时 TCP 发送一个 FIN M;另一端接收到 FIN M 之后,执行被动关闭,对这个 FIN 进行确认。它的接收也作为文件结束符传递给应用进程,因为 FIN 的接收意味着应用进程在相应的连接上再也接收不到额外数据;一段时间之后,接收到文件结束符的应用进程调用 close 关闭它的 socket。这导致它的 TCP 也发送一个 FIN N;接收到这个 FIN 的源发送端 TCP 对它进行确认。这样每个方向上都有一个 FIN 和 ACK。
本节部分知识点来自《数据库系统概论(第 5 版)》
SQL 语法教程:runoob . SQL 教程
各大设计模式例子参考:专栏 . C++ 设计模式 系列博文
设计模式工程目录
单例模式例子
抽象工厂模式例子
适配器模式例子
桥接模式例子
观察者模式例子
本节部分知识点来自《程序员的自我修养——链接装载库》
一般应用程序内存空间有如下区域:
栈:由操作系统自动分配释放,存放函数的参数值、局部变量等的值,用于维护函数调用的上下文堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收,用来容纳应用程序动态分配的内存区域可执行文件映像:存储着可执行文件在内存中的映像,由装载器装载是将可执行文件的内存读取或映射到这里保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称,如通常 C 语言讲无效指针赋值为 0(NULL),因此 0 地址正常情况下不可能有效的访问数据栈保存了一个函数调用所需要的维护信息,常被称为堆栈帧(Stack Frame)或活动记录(Activate Record),一般包含以下几方面:
函数的返回地址和参数临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量保存上下文:包括函数调用前后需要保持不变的寄存器堆分配算法:
空闲链表(Free List)位图(Bitmap)对象池典型的非法指针解引用造成的错误。当指针指向一个不允许读写的内存地址,而程序却试图利用指针来读或写该地址时,会出现这个错误。
普遍原因:
将指针初始化为 NULL,之后没有给它一个合理的值就开始使用指针没用初始化栈中的指针,指针的值一般会是随机数,之后就直接开始使用指针现在版本 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)
PE 和 ELF 都是 COFF(Common File Format)的变种
其他段略
在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。
如下符号表(Symbol Table):
Symbol(符号名)Symbol Value (地址)main0x100Add0x123……Linux 下的共享库就是普通的 ELF 共享对象。
共享库版本更新应该保证二进制接口 ABI(Application Binary Interface)的兼容
libname.so.x.y.z
x:主版本号,不同主版本号的库之间不兼容,需要重新编译y:次版本号,高版本号向后兼容低版本号z:发布版本号,不对接口进行更改,完全兼容大部分包括 Linux 在内的开源系统遵循 FHS(File Hierarchy Standard)的标准,这标准规定了系统文件如何存放,包括各个目录结构、组织和作用。
/lib:存放系统最关键和最基础的共享库,如动态链接器、C 语言运行库、数学库等/usr/lib:存放非系统运行时所需要的关键性的库,主要是开发库/usr/local/lib:存放跟操作系统本身并不十分相关的库,主要是一些第三方应用程序的库动态链接器会在 /lib、/usr/lib 和由 /etc/ld.so.conf 配置文件指定的,目录中查找共享库
使用 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...); } #endiflibrary.cpp
#include <iostream> #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 <iostream> #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_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.dllLoadLibrary 与 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 <windows.h> // 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 <windows.h> #include <stdio.h> 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)
执行如下操作:
初始化和 OS 版本有关的全局变量。初始化堆。初始化 I/O。获取命令行参数和环境变量。初始化 C 库的一些数据。调用 main 并记录返回值。检查错误并将 main 的返回值返回。大致包含如下功能:
启动与退出:包括入口函数及入口函数所依赖的其他函数等。标准函数:有 C 语言标准规定的C语言标准库所拥有的函数实现。I/O:I/O 功能的封装和实现。堆:堆的封装和实现。语言实现:语言中一些特殊功能的实现。调试:实现调试功能的代码。包含:
标准输入输出(stdio.h)文件操作(stdio.h)字符操作(ctype.h)字符串操作(string.h)数学函数(math.h)资源管理(stdlib.h)格式转换(stdlib.h)时间/日期(time.h)断言(assert.h)各种类型上的常数(limits.h & float.h)变长参数(stdarg.h)非局部跳转(setjmp.h)huihut/CS-Books:📚 Computer Science Books 计算机技术类书籍 PDF
C/C++ 发展方向甚广,包括不限于以下方向, 以下列举一些大厂校招岗位要求。
【后台开发】
编程基本功扎实,掌握 C/C++/JAVA 等开发语言、常用算法和数据结构;熟悉 TCP/UDP 网络协议及相关编程、进程间通讯编程;了解 Python、Shell、Perl 等脚本语言;了解 MYSQL 及 SQL 语言、编程,了解 NoSQL, key-value 存储原理;全面、扎实的软件知识结构,掌握操作系统、软件工程、设计模式、数据结构、数据库系统、网络安全等专业知识;了解分布式系统设计与开发、负载均衡技术,系统容灾设计,高可用系统等知识。【PC 客户端开发】
计算机软件相关专业本科或以上学历,热爱编程,基础扎实,理解算法和数据结构相关知识;熟悉 windows 操作系统的内存管理、文件系统、进程线程调度;熟悉 MFC/windows 界面实现机制,熟练使用 VC,精通 C/C++,熟练使用 STL,以及 Windows 下网络编程经验;熟练掌握 Windows 客户端开发、调试,有 Windows 应用软件开发经验优先;对于创新及解决具有挑战性的问题充满激情,具有良好的算法基础及系统分析能力。【游戏客户端开发】
计算机科学/工程相关专业本科或以上学历,热爱编程,基础扎实,理解算法、数据结构、软件设计相关知识;至少掌握一种游戏开发常用的编程语言,具 C++/C# 编程经验优先;具游戏引擎(如 Unity、Unreal)使用经验者优先;了解某方面的游戏客户端技术(如图形、音频、动画、物理、人工智能、网络同步)者优先考虑;对于创新及解决具有挑战性的问题充满激情,有较强的学习能力、分析及解决问题能力,具备良好的团队合作意识;具阅读英文技术文档能力;热爱游戏。【测试开发】
计算机或相关专业本科及以上学历;一至两年的 C/C++/Python 或其他计算机语言的编程经验;具备撰写测试计划、测试用例、以及实现性能和安全等测试的能力;具备实现自动化系统的能力;具备定位调查产品缺陷能力、以及代码级别调试缺陷的能力;工作主动积极,有责任心,具有良好的团队合作精神。【安全技术】
热爱互联网,对操作系统和网络安全有狂热的追求,专业不限;熟悉漏洞挖掘、网络安全攻防技术,了解常见黑客攻击手法;掌握基本开发能力,熟练使用 C/C++ 语言;对数据库、操作系统、网络原理有较好掌握;具有软件逆向,网络安全攻防或安全系统开发经验者优先。【嵌入式应用开发】
有良好的编程基础,熟练掌握 C/C++ 语言;掌握操作系统、数据结构等软件开发必备知识;具备较强的沟通理解能力及良好的团队合作意识;有 Linux/Android 系统平台的开发经验者优先。【音视频编解码】
硕士及以上学历,计算机、信号处理、数学、信息类及相关专业和方向;视频编解码基础扎实,熟常用的 HEVC 或 H264,有较好的数字信号处理基础;掌握 C/C++,代码能力强, 熟悉一种汇编语言尤佳;较强的英文文献阅读能力;学习能力强,具有团队协作精神,有较强的抗压能力。【计算机视觉研究】
计算机、应用数学、模式识别、人工智能、自控、统计学、运筹学、生物信息、物理学/量子计算、神经科学、社会学/心理学等专业,图像处理、模式识别、机器学习相关研究方向,本科及以上,博士优先;熟练掌握计算机视觉和图像处理相关的基本算法及应用;较强的算法实现能力,熟练掌握 C/C++ 编程,熟悉 Shell/Python/Matlab 至少一种编程语言;在计算机视觉、模式识别等学术会议或者期刊上发表论文、相关国际比赛获奖、及有相关专利者优先。Avalive:一个面部捕捉的虚拟形象扮演软件。
本仓库遵循 CC BY-NC-SA 4.0(署名 - 非商业性使用 - 相同方式共享) 协议,转载请注明出处,不得用于商业目的。