【C++】泛型编程
为了让函数或者类有更好的复用性,C++引入了模板的技术。让不同的数据类型,能使用到相同的函数或者类中去,这种编程的思想也叫做泛型编程。
一、模板
void Swap(int &left,int & right) { int temp =left; left = right; right = temp; }在学习C语言的时候,写一个交换变量数据的函数,会发现一个问题就是这个函数只能用来交换int类型的变量,那么我要交换其他类型的变量要怎么办呢,多写几个对应的函数吗?这种方法其实不是很好,因为会造成代码冗余。
这里使用C++的模板可以轻松的解决
template<typename T> void Swap(T& left, T& right) { T temp=left; left=right; right=temp; } int i=1,j=2; double x=1.1,y=2.2; Swap(i,j); Swap(x,y);模板格式template< typename(也可以用class) + 后面的类型名字T是随便取的 >,Ty,K,V,一般是大写字母或者单词首字母大写。
T 代表是一个模板类型(当成变量的类型就可以了int、char这些,也可以是自定义类型)。
PS:这里交换变量使用了一个中间变量,那么可以不可以直接使用异或来交换呢,其实不行,因为异或交换要 int 类型才可以使用。
PS:这里int和double类型的两个调用,实际上调用的不是同一个函数。
template<class T> T Add(const T& left,const T& right) { return left+right; } //编译器自动推演,隐式实例化 Add(1.1,2); //这里推演实例化出错 //强制类型转换后可以继续使用 Add((int)1.1,2); Add(1.1,(double)2);模板有个隐式类型的转换,直接输入数据编译器就能根据数据判断是什么类型的变量,但是要注意,如果两个变量类型不同,那么它就不会知道你到底想用哪个类型的变量。
//或者定义两个参数 template<class T1,class T2> T1 Add(const T1& left,const T2& right) { return left+right; } //这里就可以编译器自动推导。 Add(1.1,2); Add(1,2.2); //这里会出警告,double到int会损失精度模板中定义两个类型的参数,这里编译器就会帮你自动强制转换数据类型,这里double类型和int类型计算,int类型就会被提升为double类型。
//显示实例化 Add<int>(1.1,2);使用这种方法也可以解决,明确的告知编译器,我就是想用 int 类型的变量。
template<class T> T* func(int n) { T* a = new T[n]; return a; } //所以这里必需显示实例化 func<int>(20);这种模板函数没法自动推演,编译器不知道返回值的类型,所以使用的时候必须明确告知要返回变量的类型。
int Add(int left,int right) { } template<class T> T Add(T left,T right) { return left+right; } Add(1,2);模板函数和普通函数可以同时存在,调用时会先去检查有没有专门处理int类型的函数,如果没有就调用模板。
二、类模板
typedef char STDateType; class Stack { private: STDateType *a; int top; int capacity; }; Stack st1; Stack st2;在C语言中为了方便使用不同类型的变量,这里一般用宏定义。但是并没有解决在两个对象中,一个存储变量int,一个存储double。
也就是说定义的所有对象只能存储一种类型的数据。但是实际使用的时候是需要灵活变通的,要多种类型一起使用包括自定义类型。所以类也可以使模板。
PS:函数能根据传入的数据推导出数据类型,但是类模板不能,所以使用模板类实例化对象的时候,必须显示实例化。
template<typename T> class Stack { public: Stack(size_t capacity = 4) :_a(nullptr) ,_capacity(0) ,_top(0); { if(capacity > 0) { _a = new T[capacity]; _capacity = capacity; _top = 0; } } ~Stack() { delete[] _a; _a = nullptr; _capacity = _top =0; } void Push(const T& x) { //如果插入满了,开辟新空间 if(_top == _capacity){ size_t NewCapacity = _capatcity == 0 ? 4 : _capacity*2; T* tmp =new T[NewCapacity]; if(_a){ memcpy(tmp,_a,sizeof(T)*_top); delete[] _a; } _a = tmp; _capacity = NewCapacity; } _a[_top]=x; ++_top; } void Pop() { assert(_top > 0); --_top; } bool Empty() { return _top == 0; } T& Top() //这边使用了引用,那么可以修改,不想修改要加const。 { assert(_top > 0); return _a[_top-1]; } private: T *_a; int _top; int _capacity; }; Stack<int> st1; Stack<char> st2; st1.Top()++;这里两个Stack类型的变量就可以存储多种类型的数据了,包括自定义类型,而模板其实就是为了能更好的支持自定义类型而创建出来的。
PS:模板不支持分离编译,声明放在.h 定义放在 .cpp !!!!但是可以在当前文件分开写,所以可以创建一个.hpp的文件,这个是约定俗称的,方便人一看到就知道是什么意思。
//声明定义在本文件分离。 template<class T> class Stack { public: Stack(size_t capacity = 4) :_a(nullptr) ,_capacity(0) ,_top(0); { if(capacity > 0) { _a = new T[capacity]; _capacity = capacity; _top = 0; } } ~Stack() { delete[] _a; _a = nullptr; _capacity = _top =0; } void Push(const T& x); void Pop(); bool Empty(); T& Top(); private: T *_a; int _top; int _capacity; }; //注意这里的写法 template<class T> void Stack<T>::Push(const T& x) { if(_top == _capacity){ size_t NewCapacity = _capatcity == 0 ? 4 : _capacity*2; T* tmp =new T[NewCapacity]; if(_a){ memcpy(tmp,_a,sizeof(T)*_top); delete[] _a; } _a = tmp; _capacity = NewCapacity; } _a[_top]=x; ++_top; }三、STL
标准模板库(Standard Template Library,STL)是惠普实验室开发的一系列软件的统称。它是由Alexander Stepanov、Meng Lee和David R Musser在惠普实验室工作时所开发出来的。虽说它主要出现到C++中,但在被引入C++之前该技术就已经存在了很长时间。
STL的代码从广义上讲分为三类:algorithm(算法)、container(容器)和iterator(迭代器),几乎所有的代码都采用了模板类和模板函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。
STL六大组件:容器(数据结构),算法,迭代器,配接器,仿函数,空间配置器。
sting:
#include<string> //string是一个宏定义 typedef basic_string<char> string; string str; //等价于 basic_string<char> str使用string容器要加载头文件,并且其中的很多函数都在std的命名空间中,string是 basic/_string 的宏,其中 basic/_string 就是类名了。
basic/_string根据不同的模板参数,宏定义了多个不同的容器,定义多个是为了匹配对应的编码,因为不同的编码大小不一样。
这里先学习string容器和其中常用函数的使用,这个是一通百通的,因为其他的容器相似度非常高,也是编写库的人特意设置的,学会这一个其他的也会使用了。
构造函数:
string可以用同样的string对象来进行初始化,也可以使用字符串(char/*)来进行初始化(隐式类型装)。
还可以指定要初始化的数据段,指定开始的位置,和要初始化的长度。如果给与的长度大于了字符串后面的长度,那么把后面全部复制下来。
其中如果不指定长度,其默认的缺省值为一个静态变量npos,值为-1。
PS:npos的类型为size/_t,是一个无符号的整形,给值-1会变成一个非常大的整数。那么其意思就很明确了,不输入指针的长度,则从开始位置往后全部给要初始的对象。
这种对象的初始化的方法,在STL里面是通用的,其他容器大差不差。
重载运算符[ ]:
我们知道字符串其实就是一个字符数组,可以通过[ ]访问每个单个的字符,所以string里面重载了运算符[ ],给我们用来访问字符。
[ ]返回的是字符的引用,所以可以通过这个运算符修改单个的字符。
迭代器:
迭代器每个容器因为其特性不一样实现的方法也不一样,在string里面因为其本质还是使用数组来存储数据的,所以这里的迭代器就是指针。
既然[ ]、指针就能遍历string,为何要定义一个迭代器出来?因为并不是所有的容器都是使用指针来实现迭代器的。但是迭代器使用的方法都是一样的,是STL遍历容器的通用方法。
使用string里面的一个宏定义iterator来定义一个变量it,it就是string容器的迭代器,其使用方法和指针相似。
it!=s.end(); it<s.end();这里迭代器判断是不是到了容器尾部的时候,最好使用!=,而不是使用<符号,虽然两个效果在这里是一样的(因为string底层迭代器就是指针)。但是在其容器中会出错(比如list)。
for (auto ch : s) { cout << ch << endl; } //想修改用使用引用 for(auto & ch : it) { ch++; }范围for的底层就是迭代器。
PS:范围for里面的迭代器,能自动遍历,自动迭代,自动判断结束 。
反向迭代器:
其实看名字就大概能明白其是什么意思了,就是从容器尾部往开始位置遍历。
PS:要注意这里迭代器++是往容器前面走(左边),–是往容器后面走(右边)。
常量迭代器:
常量反向迭代器:
PS:常量迭代器是不能修改对象中的数据的。
push/_back():
push/_back()这个函数只能在string尾部插入一个字符。
append():
在string尾部插入一个字符串,并且可以指定插入字符串的开始位置和长度。
重载+=符号:
用法跟append()差不多,只不过看起来更容易理解了,用起来也方便。
insert():
在指定位置插入一个字符串,同样可以指定插入字符串的开始位置和长度。
reserve()和resize():
在插入数据的时候,函数内部就已经帮我们判断了如果容器满了,则会重新扩容空间。但是我们如果知道要输入数据大小,那么就可以提前开辟空间。
reserve()开辟空间,resize()则是开辟空间并初始化。
PS:string内置了一个函数capacity(),可以得知现在string的容量。
c/_str:
有时候需要兼容C语言,所以里面包含了兼容C语言的函数,c/_str()函数返回的是const char/*的类型。
string filename("test.cpp"); FILE* fp = fopen(filename.c_str,"r"); char ch=getc(fp); while(ch != EoF){ cout<<ch; char ch=getc(fp); }这里filename和filename.c/_str是有区别的,filename 是以容器的size()为准。filename.c/_str是常量字符串,以“/0”为结束标识。
find():
查找字符串,查找到了返回匹配字符串第一个字符的位置,没找到返回npos。
find(),在查找字符串的时候,可以指定开始查找的位置,也可以指定查找的字符串长度。但是要注意,如果有多个字符串可以匹配上,这里只返回第一个匹配上的位置。
find/_first/_of():
![]()
这个函数比较难理解,它不是要把完全匹配的字符串位置给找出来。而是匹配的字符串的字符,谁第一个在主串出现,就返回该处匹配的位置。
![]()
这里e是最先在主串里面出现的,所以这里返回1。匹配失败返回npos。
四、模拟实现string
namespace STR { class string { public: //构造函数 使用初始化列表 string(const char* str = "") //这里的缺省值是 /0 :_str(new char[strlen(str)+1]) //加1 是加上 /0的空间 ,_size(strlen(str)) ,_capacity(strlen(str)) //capacity不包括 /0 { strcpy(_str, str); } //构造函数 另一种写法 string(const char* str = "") //这里的缺省值是 /0 { //这样写可以复用变量,如果写在初始列表,变量初始顺序要严格匹配声明顺序。 _size = strlen(str); _capacity = _size; _str = new char[_capacity + 1]; //加1 是加上 /0的空间 strcpy(_str, str); } //析构函数 ~string() { delete[] _str; _str = nullptr; _capacity = _size = 0; } private: char* _str; size_t _size; //字符串的长度 不包括/0 size_t _capacity; //数组的容量 不统计/0 static size_t nops_t; }; } //静态变量 类外面定义 size_t STR::string::nops_t = -1;PS:这里为了能方便与库里面的string进行区分,这里自定义了一个命名空间STR。
这里开始写拷贝构造函数的时候就要注意了,成员变量中定义了一个数组,那么就要注意深浅拷贝的问题,为了避免浅拷贝,这里初始化的时候要为 /_str 重新开辟一段空间。
传统写法:
//拷贝构造函数 传统写法 string(const string& str) :_str(new char[str._capacity + 1]) //新开辟一段空间 , _size(str._size) , _capacity(str._capacity) { strcpy(_str, str._str); }现代写法:
const char* c_str() const //const string* const this { return _str; } void swap(string& str) { std::swap(_str, str._str); std::swap(_size, str._size); std::swap(_capacity, str._capacity); } //拷贝构造函数 现代写法 string(const string& str) :_str(nullptr) , _size(0) , _capacity(0) { //注意这里是str._str是char*类型的字符串,这里复用了构造函数 string tmp(str._str); swap(tmp); }拷贝构造函数的现代写法非常巧妙、简洁,并且借用了构造函数来进行深拷贝。
这里定义了一个交换函数swap(),里面复用了库里面的swap()来交换数据。
库里面的swap()被模板化了,可以直接进行自定义类型的交换,但是这里会多次调用拷贝构造,浪费效率。
所以这里在string内部又重载了一个 swap()函数,其本身只是进行变量数据的交换,所以效率比较高。
现代写法的思路就是把传进来的字符串重新生成一个对象,再把对象的数据跟自己交换,而且由于tmp是函数内部的局部变量,出了函数就会调用析构函数销毁,不用手动释放地址。
这种写法也可以在重载赋值符号上,重载赋值符号的时候也要考虑深浅拷贝的问题。
//传统写法 string& operator=(const string& str) { if (this!=&str) { delete[] _str; _str = new char[str._capacity + 1]; //+1是给/0留的空间 strcpy(_str, str._str); _capacity = str._capacity; _size = str._size; } return *this; } //现代写法 string& operator=(const string& str) { if (this != &str) { string tmp(str._str); swap(tmp); } return *this; }套路一样把传进来的字符串重新生成一个对象,再把对象的数据跟自己交换。
string& operator=(const string& str) { if(this!=&str){ string tmp(str); std::swap(*this,tmp); //这里使用库里面的swap,会造成错误 } return *this; }还有一点, 如果重载赋值符号,使用库里面的swap()进行交换,swap(/*this,tmp),函数内部还是使用的赋值符号,会再去调用赋值,所以这里会死循环,造成栈溢出。
//重载[] 返回pos位置的字符的引用 //常量的调用 ,只能读,不能写 const char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; } //普遍调用, 可以读,可以写 char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; }重载[ ]非常简单,直接复用数组的[ ]即可。
//扩容 void reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } } //开空间并初始化 void resize(size_t n,char ch='/0') { //1.比当前的空间大 if (n>_size) { reserve(n); for (size_t i = _size; i < n; i++) { _str[i] = ch; } _str[n] = '/0'; _size = n; } else { //2.比当前的空间小 _str[n] = '/0'; _size = n; } }扩容函数,如果string内部已经有内容的话,要把数据拷贝到新开辟的空间里面去。开辟空间的时候要多留一个字节给"/0"。
//插入一个字符串 void push_back(char cn) { if (_size == _capacity) { //如果空间满了,要扩容 //如果string容器里面没有数据,开4个字节的空间。 reserve(_capacity == 0 ? 4 : _capacity * 2); } _str[_size] = cn; _size++; _str[_size] = '/0'; } //插入一个字符串 void append(const char* str) { size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } strcpy(_str + _size, str); //strcat(_str,str) 也可以使用这个函数追加,不过要找/0,效率低 _size += len; } void append(const string& str) { append(str._str); } void append(size_t n, char ch) { for (size_t i = 0; i < n; i++) { push_back(ch); } } string& operator+=(char ch) { push_back(ch); return *this; } string& operator+=(const char* str) { append(str); return *this; }插入字符或者字符串,实现其实比较简单,主要是注意判断如果数组的容量占满了要重新开辟空间。
//插入字符 string& insert(size_t pos, char ch) { assert(pos <= _size); if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } size_t end = _size + 1; while (end > pos) { _str[end] = _str[end - 1]; end--; } _str[pos] = ch; _size++; return *this; } //插入字符串 string& insert(size_t pos, const char* str) { assert(pos <= _size); size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } size_t end = _size + len; while (end >= pos + len) { _str[end] = _str[end - len]; end--; } strncpy(_str + pos, str, len); _size += len; return *this; }在指定位置插入时,要把插入位置后面的数据往后挪动。这里不要用strcpy()会把/0也拷贝进去,所以这里使用了strncay();
size_t end = _size; while (end >= pos) { //这里pos==0,end减到最后变为-1的时候,会变得非常大,造成越界。 _str[end + 1] = _str[end]; end--; } size_t end = _size+1; while (end > pos) { _str[end] = _str[end-1]; end--; }PS:这里挪动数据有两种写法,最好使用下面的写法,因为上面在开头插入数据时,最后end会变为-1,而这里end是size/_t类型的数据,会变得非常大,从而出错。
void push_back(char ch) { insert(_size,ch); } void append(const char* str) { intsert(_size,str); }其实也可以先实现insert(),然后复用实现push/_back()和append()。
//删除 void erase(size_t pos, size_t n = nops_t) //如果不写,那么全删除了 { assert(pos < _size); //如果没给参数,或者给的参数过大,则pos后面的全部删除 if (n == nops_t || n + pos > _size) { _str[0] = '/0'; _size = 0; } else { //把要删除的数据,用后面的数据覆盖 strcpy(_str + pos, _str + pos + n); _size -= n; } } void clear() { _str[0] = '/0'; _size = 0; }删除函数比较简单没什么要注意的地方。
//查找字符 size_t find(char ch, size_t pos = 0) { assert(pos < _size); for (size_t i = pos; i < _size;i++) { if (ch==_str[i]) { return i; } } return nops_t; } //查找字符串 size_t find(const char* sub, size_t pos = 0) { assert(pos < _size); const char* ret = strstr(_str + pos,sub); if (ret == nullptr) { return nops_t; } else { return ret - _str; } }这里使用了库里面的串匹配函数,这里也可以自己写个KMP算法来查找,不过其实效率也差不多。
这里返回查找到字符串位置的写法比较巧妙,使用的是指针减指针的知识点,获得的是中间元素的个数,而/_str又是数组的起始地址,所以这里就返回的就是查找到的位置。
bool operator>(const string& str)const { //大于0就是真 return strcmp(_str, str._str) > 0; } bool operator==(const string& str)const { return strcmp(_str, str._str) == 0; } bool operator<=(const string& str)const { return !(*this > str); } bool operator>=(const string& str)const { return *this > str || *this == str; } bool operator<(const string& str)const { return !(*this >= str); } bool operator!=(const string& str)const { return !(*this == str); }C语言里面字符串是可以比较大小的,所以这里也要重载一下,这里直接复用strcmp(),以后比较字符串的大小的时候,就可以直接使用运算符,很方便也容易理解。
string substr(size_t pos, size_t len = nops_t)const { assert(pos < _size); size_t realLen = len; //如果没给参数,或者给的参数过大,则pos后面的全部返回为子串 if (len == nops_t || pos + len > _size) { realLen = _size - pos; } string sub; for (size_t i = 0; i < realLen;i++) { sub += _str[pos + i]; } return sub; }把主串中的一段数据作子串返回。分割字符串的时候用的比较多。
//重载流提取和流插入 //实现成全局函数,避免和this指针的位置问题。 std::ostream& operator<<(std::ostream& out, const string& str); std::istream& operator>>(std::istream& in, string& str); std::ostream& STR::operator<<(std::ostream& out, const string& str) { for (size_t i = 0; i < str.size(); i++) { out << str[i]; } return out; } //这种写法+=多次,扩容多次 std::istream& STR::operator>>(std::istream& in, string& str) { char ch; //in>>ch 不能使用这个,这个无法检查到空格 ch = in.get(); while (ch != ' ' && ch != '/n') { str += ch; ch = in.get(); } return in; } std::istream& STR::operator>>(std::istream& in, string& str) { str.clear(); char ch; const size_t N = 32; char buff[N]={0}; size_t i = 0; ch = in.get(); //其思路是,buff满了,再追加到str里面 while (ch != ' ' && ch != '/n') { buff[i++] = ch; if (i == N-1) { buff[i] = '/0'; str += buff; i = 0; } ch = in.get(); } buff[i] = '/0'; str += buff; return in; }这里流提取比较好理解,但是流插入要注意一下,流插入是可以连续对多个对象进行插入的。
但是使用cin>>ch;这种写法不能接收到空格和换行符,因为它本身检查到空格和换行就是输入的间隔,会造成这里退出不了循环,所以这里使用了cin里面自带的get()函数。
如果输入的内容很长,使用+=的效率很低。所以这里使用一个数组先接收一定的数据,数组的数据满了才添加到对象里面(和缓冲区的概念相似)。
//迭代器 typedef char* iterator; //const 迭代器 typedef const char* const_iterator; iterator begin() { return _str; } iterator end() { return _size + _str; }迭代器的实现也比较简单,因为string就是用数组来存储的字符串,用指针也可以访问,这里只不过是把指针进行了宏定义,但是这种写法也是可以使用范围for的。
学习资源
如果你是也准备转行学习网络安全(黑客)或者正在学习,这里开源一份360智榜样学习中心独家出品《网络攻防知识库》,希望能够帮助到你
**读者福利 |**CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享**(安全链接,放心点击)**
知识库由360智榜样学习中心独家打造出品,旨在帮助网络安全从业者或兴趣爱好者零基础快速入门提升实战能力,熟练掌握基础攻防到深度对抗。
1、知识库价值
深度: 本知识库超越常规工具手册,深入剖析攻击技术的底层原理与高级防御策略,并对业内挑战巨大的APT攻击链分析、隐蔽信道建立等,提供了独到的技术视角和实战验证过的对抗方案。
广度: 面向企业安全建设的核心场景(渗透测试、红蓝对抗、威胁狩猎、应急响应、安全运营),本知识库覆盖了从攻击发起、路径突破、权限维持、横向移动到防御检测、响应处置、溯源反制的全生命周期关键节点,是应对复杂攻防挑战的实用指南。
实战性: 知识库内容源于真实攻防对抗和大型演练实践,通过详尽的攻击复现案例、防御配置实例、自动化脚本代码来传递核心思路与落地方法。
2、 部分核心内容展示
360智榜样学习中心独家《网络攻防知识库》采用由浅入深、攻防结合的讲述方式,既夯实基础技能,更深入高阶对抗技术。
360智榜样学习中心独家《网络攻防知识库》采用由浅入深、攻防结合的讲述方式,既夯实基础技能,更深入高阶对抗技术。
内容组织紧密结合攻防场景,辅以大量真实环境复现案例、自动化工具脚本及配置解析。通过策略讲解、原理剖析、实战演示相结合,是你学习过程中好帮手。
1、网络安全意识
2、Linux操作系统
3、WEB架构基础与HTTP协议
4、Web渗透测试
5、渗透测试案例分享
6、渗透测试实战技巧
7、攻防对战实战
8、CTF之MISC实战讲解
3、适合学习的人群
一、基础适配人群
- 零基础转型者:适合计算机零基础但愿意系统学习的人群,资料覆盖从网络协议、操作系统到渗透测试的完整知识链;
- 开发/运维人员:具备编程或运维基础者可通过资料快速掌握安全防护与漏洞修复技能,实现职业方向拓展或者转行就业;
- 应届毕业生:计算机相关专业学生可通过资料构建完整的网络安全知识体系,缩短企业用人适应期;
二、能力提升适配
1、技术爱好者:适合对攻防技术有强烈兴趣,希望掌握漏洞挖掘、渗透测试等实战技能的学习者;
2、安全从业者:帮助初级安全工程师系统化提升Web安全、逆向工程等专项能力;
3、合规需求者:包含等保规范、安全策略制定等内容,适合需要应对合规审计的企业人员;
因篇幅有限,仅展示部分资料,完整版的网络安全学习资料已经上传CSDN,朋友们如果需要可以在下方CSDN官方认证二维码免费领取【保证100%免费】
5、渗透测试案例分享*
6、渗透测试实战技巧
7、攻防对战实战
8、CTF之MISC实战讲解
3、适合学习的人群
一、基础适配人群
- 零基础转型者:适合计算机零基础但愿意系统学习的人群,资料覆盖从网络协议、操作系统到渗透测试的完整知识链;
- 开发/运维人员:具备编程或运维基础者可通过资料快速掌握安全防护与漏洞修复技能,实现职业方向拓展或者转行就业;
- 应届毕业生:计算机相关专业学生可通过资料构建完整的网络安全知识体系,缩短企业用人适应期;
二、能力提升适配
1、技术爱好者:适合对攻防技术有强烈兴趣,希望掌握漏洞挖掘、渗透测试等实战技能的学习者;
2、安全从业者:帮助初级安全工程师系统化提升Web安全、逆向工程等专项能力;
3、合规需求者:包含等保规范、安全策略制定等内容,适合需要应对合规审计的企业人员;
因篇幅有限,仅展示部分资料,完整版的网络安全学习资料已经上传CSDN,朋友们如果需要可以在下方CSDN官方认证二维码免费领取【保证100%免费】