文章目录

  • 一、指针基础知识
    • 1、什么是指针
    • 2、指针变量的大小
    • 3、指针类型的意义
    • 4、指针的运算
    • 5、野指针的成因及规避方法
  • 二、指针进阶知识
    • 1、字符指针
    • 2、指针数组
    • 3、数组指针
    • 4、数组参数、指针参数
    • 5、函数指针
    • 6、函数指针的用途
    • 7、函数指针数组
    • 8、函数指针数组的用途
    • 9、指向函数指针数组的指针

一、指针基础知识

在开始我们指针的进阶内容之前,我们先来回顾一下与指针相关的基础知识:

1、什么是指针

  1. 指针是内存中一个最小单元的编号,也就是地址
  2. 我们一般口语中说的指针,通常指的是指针变量,也就是用来存放内存地址的变量。

2、指针变量的大小

  1. 在32位的机器上,地址由32个0/1组成二进制序列组成,所以地址需要用4个字节的空间来存储,则一个指针变量的大小就应该是4个字节。
  2. 在64位机器上,地址由64个0/1组成二进制序列组成,所以地址需要用8个字节的空间来存储,则一个指针变量的大小就应该是8个字节。
  3. 总结:在 X86 (32位平台) 环境下,一个指针变量的大小 (一个地址的大小) 是四个字节;在 X64 (64位平台) 环境下,一个指针变量的大小 (一个地址的大小) 是八个字节

3、指针类型的意义

  1. 指针的类型决定了指针进行解引用操作时的访问权限 (解引用时能向后访问几个字节的空间) ;
  2. 指针的类型决定了指针 ± 整数时候的步长 (+1跳过几个字节) ;

4、指针的运算

  1. 指针 ± 整数:指针移动整数个元素的大小;
  2. 指针 - 指针:得到指针之间元素的个数;
  3. 指针的关系运算:比较两个地址的大小;

5、野指针的成因及规避方法

野指针的成因

  1. 指针未初始化;
  2. 指针越界访问;
  3. 指针指向的空间被释放;

野指针的规避方法

  1. 使用已初始化的指针;
  2. 小心指针越界;
  3. 当指针指向的空间被释放的同时把该指针置为 NULL;
  4. 避免返回局部变量的地址 (离开该变量的生命周期该变量就会被销毁);
  5. 指针使用之前检查其有效性;

二、指针进阶知识

1、字符指针

什么是字符指针

顾名思义,字符指针就是用来存放字符地址的指针。

字符指针的两种使用方法

第一种:

int main()
{char ch = 'w';char *pc = &ch;*pc = 'w';return 0;
}

第二种:

int main()
{const char* pstr = "hello world";  //这里是把一个字符串放到pstr指针变量里了吗?printf("%s\n", pstr);return 0;
}

第一种使用方法很简单,这里我不再赘述;难点是第二种使用方法:在第二个例子中,我们并不是把 “hello world” 这整个字符串放到 pstr 指针变量中,而且 pstr 是指针变量,只能存放四个字节的内容,也存不下这整个字符串;

其实我们是把 “hello world” 这个字符串中首字符的地址,即 ‘h’ 的地址放入 pstr 中,然后我们可以以 %s 的形式把整个字符串打印出来;

同时,“hello world” 这样的字符串被我们称为常量字符串,它是存储在字符常量区的,我们可以通过 pstr 来访问它,但是不能修改它的内容 (因为它是常量),所以这里我们用 const 关键字来修饰 char*,防止有人误该 “hello world” 中的内容。

笔试题练习

下面程序的输出结果是什么?

int main()
{char str1[] = "hello world";char str2[] = "hello world";const char* str3 = "hello world";const char* str4 = "hello world";if (str1 == str2)printf("str1 and str2 are same\n");elseprintf("str1 and str2 are not same\n");if (str3 == str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0;
}

解析

str1 和 str2 是数组,数组空间在栈区上开辟,所以操作系统会给 str1 和 str2 分别分配一块空间,并把空间里的内容初始化为 “hello world”,同时数组名代表首元素地址,所以 str1 != str2;

对于 str3 和 str4 来说,由于 “hello world” 存放在字符常量区,所以 “hello world” 只会存在一份,只需要让它们同时指向 “hello world” 的空间即可,所以 str3 和 str4 其实存放的都是字符常量区中 “hello world” 中 字符 ‘h’ 的地址,所以 str3 == str4;

2、指针数组

指针数组是什么

顾名思义,指针数组是一个数组,而且是用来存放指针变量的数组;所以指针数组就是存放指针的数组。

指针数组的定义

int* arr[10];
# arr的类型:int* [10]   //去掉变量名剩下的就是变量类型
# arr先和[10]结合,表示arr是一个数组,数组里面有10个元素,每个元素的类型是int*char* str[10];
# str的类型 char* [10]   //去掉变量名剩下的就是变量类型
# str先和[10]结合,表示str是一个数组,数组里面有10个元素,每个元素的类型是char*

指针数组的使用

int main()
{int a = 10;int b = 20;int c = 30;int* arr[3] = { &a, &b, &c };int i = 0;for (i = 0; i < 3; i++){*(arr[i]) = i;}printf("%d %d %d\n", a, b, c);return 0;
}

3、数组指针

指针数组是什么

顾名思义,数组指针就是一个指针,这个指针指向的是一个数组;所以数组指针就是指向数组的指针。

数组指针的定义

int (*arr)[10];
# arr的类型:int (*)[10]  //去掉变量名剩下的就是变量类型
# arr首先和*结合,表示它是一个指针,然后和[10]结合,表示它指向的是一个数组,数组里面有10个元素,每个元素的类型是int;char* (*arr)[10];
# arr的类型:int* (*)[10]  //去掉变量名剩下的就是变量类型
# arr首先和*结合,表示它是一个指针,然后和[10]结合,表示它指向的是一个数组,数组里面有10个元素,每个元素的类型是char*;

数组名和&数组名的区别

#include <stdio.h>
int main()
{int arr[10] = { 0 };printf("arr = %p\n", arr);printf("&arr= %p\n", &arr);printf("arr+1 = %p\n", arr + 1);printf("&arr+1= %p\n", &arr + 1);return 0;
}

如上所示:数组名和&数组名所得到的地址的起始位置是相同的,但是数组名加1跳过的是一个整形,即4个字节;而&数组名加1跳过的是一个数组,即40个字节;

所以:数组名表示首元素的地址,+1 跳过一个数组元素;而&数组名表表示整个数组的地址,+1 跳过整个数组。

数组指针的使用

int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };int(*p)[10] = &arr;  //把整个数组的地址赋值给数组指针变量pint i = 0;for (i = 0; i < 10; i++){//*p找到整个数组,而数组名代表整个数组,所以*p相当于得到数组名,//而数组名又表示首元素的地址,所以*p最终的效果是得到数组首元素的地址//首元素的地址 +i 再解引用得到数组的各个元素printf("%d ", *(*p) + i);  }return 0;
}

虽然上面的使用是正确的,但是我们通常不这样用,因为我们可以直接用 arr[i] 来得到数组的每个元素;数组指针通常用于二维数组:

void print_arr(int(*arr)[5], int row, int col)
{int i = 0;int j = 0;for (i = 0; i < row; i++){for (j = 0; j < col; j++){//arr[i] 相当于 *(arr+i)//所以 arr[i][j] 相当于 *(*(arr+i)+j)//arr[i] 找到二维数组具体的某一行,而行号代表那一行,同时行号又表示那一行首元素的地址//所以 arr[i][j] 就可以找到二维数组中具体某一行的具体某一个元素printf("%d ", arr[i][j]);}printf("\n");}
}int main()
{int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };//数组名arr,表示首元素的地址//但是二维数组的首元素是二维数组的第一行//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址//一维数组的地址用数组指针来接收print_arr(arr, 3, 5);return 0;
}

练习题

下面的代码分别表示什么意思?

int arr[5];
# arr和[5]结合,表示arr是一个数组,数组里面有5个元素,每个元素的类型是int;所以这里表示正常的一维整形数组;int *parr1[10];
# parr1和[10]结合,表示parr1是一个数组,数组里面有10个元素,每个元素的类型是int*;所以这里表示指针数组;int (*parr2)[10];
# parr2首先和*结合,表示它是一个指针,然后和[10]结合,表示它指向一个数组,数组里面有10个元素,每个元素的类型是int;所以这里表示数组指针;int (*parr3[10])[5];
# parr3和[10]结合,表示这是一个数组,数组里面有10个元素,每个元素的类型是int (*)[5];所以这里是存放数组指针的数组;

4、数组参数、指针参数

我们在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?下面我们来探讨这个问题。

一位数组传参

void test(int arr[]) {};
# true 数组传参,用数组接受void test(int arr[10]) {};
# true 数组传参,用数组接受,数组元素个数可写可不写void test(int* arr) {};
# true 整形的地址,用整形指针来接收,上面的两种传参方式本质上是这种void test2(int* arr[20]) {};
# true 数组传参,用数组接受void test2(int** arr) {};
# true 整形指针的地址,用二级指针来接收int main()
{int arr[10] = { 0 };int* arr2[20] = { 0 };test(arr);  //数组名,首元素地址,整形的地址test2(arr2); //数组名,首元素地址,整形指针的地址return 0;
}

二维数组传参

void test(int arr[3][5]) {};
# true 二维数组传参,用二维数组来接收void test(int arr[][]) {};
# false 二维数组传参可以不指定行,但必须指定列void test(int arr[][5]) {};
# true 二维数组传参,用二维数组来接收,行号可写可不写void test(int* arr) {};
# false 一位数组的地址不能用整形指针来接收void test(int* arr[5]) {};
# false 一维数组的地址不能用指针数组来接收void test(int(*arr)[5]) {};
# true 一维数组的地址用数组指针来接收,数组里面有5个元素void test(int** arr) {};
# false 一位数组的地址不能用整形二级指针来接收int main()
{int arr[3][5] = { 0 };test(arr);  //二维数组的数组名代表第一行的地址,即一维数组的地址
}

一级指针传参

void print(int* p, int sz)  //一级指针用指针变量来接收
{int i = 0;for (i = 0; i < sz; i++){printf("%d\n", *(p + i));}
}int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9 };int* p = arr;  //数组名代表首元素地址,即整形的地址int sz = sizeof(arr) / sizeof(arr[0]);print(p, sz);  //将一级指针p传给函数return 0;
}

二级指针传参

void test(int** ptr)
{printf("num = %d\n", **ptr);
}int main()
{int n = 10;int* p = &n;  //一级指针变量int** pp = &p;  //二级指针变量test(pp);  //二级指针test(&p);  //一级指针的地址,即二级指针return 0;
}

5、函数指针

什么是函数指针

我们知道,任何类型的变量都是要在内存中占用空间的,而每个内存空间都会有自己的编号,也就是地址,那么对于函数来说,函数也会在内存中占用空间,所以函数也是有地址的;而函数指针就是用来存放函数地址的指针。

int Add(int x, int y)
{return x + y;
}int main()
{int a = 10;int b = 20;int c = Add(a, b);printf("%p\n", &Add);printf("%p\n", Add);return 0;
}

函数指针的定义

int (*p1)(int, int) = &Add;
int (*p1)(int, int) = Add;
# 我们上面已经知道了,函数名和&函数名都代表函数的地址,所以上面这两种写法其实是一样的,都是把Add函数的地址赋给了函数指针p1;
# p1的类型:int (*)(int, int)  //去掉变量名剩下的就是变量类型
# p1首先和*结合,表示它是一个指针,然后和(int, int)结合,表示它指向的是一个函数,函数的参数是int,int,返回值也是int;

函数指针的使用

int Add(int x, int y)
{return x + y;
}int main()
{int (*p1)(int, int) = &Add;//int (*p1)(int, int) = Add;int ret1 = Add(2, 3);int ret2 = (*Add)(2, 3);printf("%d %d\n", ret1, ret2);int ret3 = p1(2, 3);int ret4 = (*p1)(2, 3);int ret5 = (*********p1)(2, 3);printf("%d %d %d\n", ret3, ret4, ret5);return 0;
}

我们日常在调用函数的时候,都是直接函数名 + 函数参数,但是在学习了函数指针之后我们可能会有一个疑惑,既然函数名也代表函数的地址,那我们在调用函数的时候是不是应该先对函数进行解引用,然后再进行传参等操作?

其实对于函数来说,调用是不需要解引用的 (C语言就是这样设计的,大家当作一个特例记住就行,不用深究) ,当然,我们对它解引用编译器也不会报错,说白了对函数就行解引用只是为了让我们能够更好的理解指针,而编译器会自动忽略掉函数前面的*号;

就像我们上面例子中的 ret1 和 ter2,ret3 和 ret4 ,其实对于编译器来说他们都是一样的,甚至我们像 ret5 那样在函数前面加上若干个*号编译器也不会报错,因为编译器会将其忽略。

两段有趣的代码

第一段代码:

(*(void (*)())0)();
  • (void (*)())0:首先,0前面括号中的 void (*)() 是一个函数指针类型,该指针指向的函数的参数为空,返回值也为空;其次,我们知道,(int*)0 是把0强制类型转换为int*类型,即把0当作一个地址,该地址存放的是一个整数;所以 (void (*)())0 是把0强制类型转换为函数指针类型,即把0当作一个参数为空,返回值也为空的函数的地址。
  • (*(void (*)())0)():现在我们知道了(void (*)())0 代表的是0地址处的函数,且该函数的参数为空,所以我们现在调用该函数;首先用*对函数(不加*也行)解引用,然后传参(参数为空);
  • 所以实际上上面的代码完成的是一次函数调用,调用0地址处的函数。

上面这段出自于《C陷阱与缺陷》这本书的第二章,该书中对此问题的描述如下: 所以说,上面这段是有着实际意义的,并不是我们为了炫技而设计出的无用的代码。(上面提到的子例程是函数的意思)

第二段代码:

void (*signal(int, void(*)(int)))(int);
  • signal ( int, void (*) (int) ):对于这种复杂的语句,我们一般从函数名开始分析,我们发现,signal 是一个函数的函数名,该函数有两个参数,第一个参数是一个整形,第二个参数是一个函数指针,该指针指向的函数的参数是 int,返回值是 void;
  • void (*) (int):我们把 signal ( int, void (*) (int) ) 从代码中抽离出去就得到了函数的返回值,可以看到,该函数的返回值也是一个函数指针,该指针指向的函数的参数是 int,返回值是 void;
  • 所以实际上上面这段代码是一个函数声明,声明的函数的第一个参数是整形,第二个参数是参数为 int,返回值为 void 的函数指针,返回值也是参数为 int,返回值为 void 的函数指针。

上面这段也出自于《C陷阱与缺陷》这本书的第二章,紧挨着我们上面的函数调用:

《C陷阱与缺陷》这本书是十分经典的一本C语言书籍,里面提到了许多C语言中可能会出现的一些错误,特别是指针方面的错误,希望大家都能抽时间看看这本书,我把这本书的电子版放到了阿里云盘中,有需要的可以自取。

阿里云盘链接:https://www.aliyundrive.com/s/WHvpjWmFqDo
提取码: zg83

6、函数指针的用途

当我们学习了函数指针的相关知识过后,可能大家会有这样一个疑惑,既然我们可以在代码中直接来调用函数,那为什么还要通过函数指针来间接调用函数呢?这不是多此一举吗?我们说,存在即合理,其实只是我们没有还见过函数指针的真正用途而已,而并不能说函数指针没用;

实际上,函数指针是C语言中一种特别高明的存在,我们在用C语言完成比较大型的工程项目的时候,函数指针会被经常用到;而函数指针数组最常用的两个用途就是回调函数和转移表;回调函数是指通过函数指针来间接调用函数,转移表其实就是函数指针数组。

回调函数

回调函数就是一个通过函数指针调用的函数;如果我们把函数的指针(地址)作为参数传递给另一 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数;回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进 行响应。(C语言中的库函数 qsort 就是回调函数使用的经典例子)

下面我通过一个简易的计算器来具体体现函数指针的回调函数的用法:

int add(int a, int b)
{return a + b;
}int sub(int a, int b)
{return a - b;
}int mul(int a, int b)
{return a * b;
}int div(int a, int b)
{return a / b;
}void menu()
{printf("****************************\n");printf("*****   1. Add   2. Sub*****\n");printf("*****   3. Mul   4. Div*****\n");printf("*****      0. Exit     *****\n");printf("****************************\n");
}int main()
{int x, y;int input = 1;int ret = 0;do{menu();printf("请选择:");scanf("%d", &input);switch (input){case 0:printf("退出程序\n");break;case 1:printf("输入操作数:");scanf("%d %d", &x, &y);ret = add(x, y);printf("ret = %d\n", ret);break;case 2:printf("输入操作数:");scanf("%d %d", &x, &y);ret = sub(x, y);printf("ret = %d\n", ret);break;case 3:printf("输入操作数:");scanf("%d %d", &x, &y);ret = mul(x, y);printf("ret = %d\n", ret);break;case 4:printf("输入操作数:");scanf("%d %d", &x, &y);ret = div(x, y);printf("ret = %d\n", ret);break;default:printf("选择错误\n");break;}} while (input);return 0;
}

上面我们完成了一个简易的计算器,但是我们发现它存在一个问题,那就是case 1 、case 2、case 3、case 4 中的代码除了调用函数的那句不一样之外,其他三句完全一样,造成了代码冗余,我们设想能不能设计一个函数,把这句代码都放入一个函数中去,从而实现代码的复用,这时候就要用到我们的回调函数了。

经过改造后的代码如下:

int add(int a, int b)
{return a + b;
}int sub(int a, int b)
{return a - b;
}int mul(int a, int b)
{return a * b;
}int div(int a, int b)
{return a / b;
}void menu()
{printf("****************************\n");printf("*****   1. Add   2. Sub*****\n");printf("*****   3. Mul   4. Div*****\n");printf("*****      0. Exit     *****\n");printf("****************************\n");
}void calc(int(*pf)(int, int))
{int x = 0;int y = 0;int ret = 0;printf("输入操作数:");scanf("%d %d", &x, &y);ret = pf(x, y);printf("ret = %d\n", ret);
}int main()
{int input = 1;do{menu();printf("请选择:");scanf("%d", &input);switch (input){case 0:printf("退出程序\n");break;case 1:calc(add);break;case 2:calc(sub);break;case 3:calc(mul);break;case 4:calc(div);break;default:printf("选择错误\n");break;}} while (input);return 0;
}

上面代码中我们把前面冗余的代码全部封装到了 calc 函数中,通过把 calc 函数的参数设置为函数指针来实现了回调函数。

7、函数指针数组

什么是函数指针数组

顾名思义,函数指针数组就是用来存放函数指针的数组。

函数指针数组的定义

int (*parr1[10])(int);
# parr1的类型:int (*[10])(int)  //去掉变量名剩下的就是变量类型
# parr1和[10]结合,表示它是一个数组,数组里面有10个元素,每个元素的类型是一个函数指针,该指针指向的函数的参数为int,返回值为int;

函数指针数组的使用

int Add(int x, int y)
{return x + y;
}int Sub(int x, int y)
{return x - y;
}int Mul(int x, int y)
{return x * y;
}int Div(int x, int y)
{return x / y;
}int main()
{int (*parr[4])(int, int) = { Add, Sub, Mul, Div };  //将函数地址放入数组中int a = 10;int b = 20;int i = 0;for (i = 0; i < 4; i++){printf("%d\n", (parr[i])(a, b));  //知道需要函数地址,调用函数}return 0;
}

8、函数指针数组的用途

上面我们已经提到,函数指针数组用于转移表。

下面我们继续通过对计算器的改造来体现转移表的作用:经过改造后的代码如下

int add(int a, int b)
{return a + b;
}int sub(int a, int b)
{return a - b;
}int mul(int a, int b)
{return a * b;
}int div(int a, int b)
{return a / b;
}void menu()
{printf("****************************\n");printf("*****   1. Add   2. Sub*****\n");printf("*****   3. Mul   4. Div*****\n");printf("*****      0. Exit     *****\n");printf("****************************\n");
}int main()
{int x, y;int input = 1;int ret = 0;int(*p[5])(int x, int y) = { 0, add, sub, mul, div };  //转移表while (1){menu();printf("请选择:");scanf("%d", &input);if ((input > 0 && input <= 4)){printf("输入操作数:");scanf("%d %d", &x, &y);ret = (*p[input])(x, y);  //回调函数printf("ret = %d\n", ret);}else if (input == 0){printf("退出程序\n");break;}else{printf("输入错误\n");}}return 0;
}

上面我们把各个函数的地址存放到一个函数指针数组中去,实现了一个转移表,然后通过访问数组里面的元素,配合回调函数,从而达到了我们简化代码的目的。

9、指向函数指针数组的指针

指向函数指针数组的指针就是一个指针,指针指向的是一个数组,数组里面的每个元素是函数指针。

其定义和使用如下:

void test(const char* str)
{printf("%s\n", str);
}int main()
{//函数指针pfunvoid (*pfun)(const char*) = test;//函数指针的数组pfunArrvoid (*pfunArr[5])(const char* str);pfunArr[0] = test;//指向函数指针数组pfunArr的指针ppfunArrvoid (*(*ppfunArr)[5])(const char*) = &pfunArr;return 0;
}

深析C语言的灵魂 -- 指针相关推荐

  1. 指针是c语言的灵魂,C语言之灵魂 指针学习

    指针是c语言的难点 称之为c语言的灵魂一点也不为过,不过指针用好了能事半功倍,用不好bug满天飞. 一.指针的概念 指针也是变量只不过是特殊的变量,指针的值是另一个变量的地(也就是变量所在的内存地址) ...

  2. c语言c99标准_C语言的灵魂指针,配合这个新增的关键字,能够生成更高效的程序...

    正如我前面的文章提到的,C语言虽然已经比较成熟,但是近些年来也是有所发展的--比如增加了许多新特性.遗憾的是,可能因为C语言程序员的工资比不过互联网程序员,国内很多教材比较老旧,几乎不涉及近些年来C语 ...

  3. c语言浮点数能用八进制输出不,深析C语言浮点型数据的输入输出

    方星星 吕永强 摘  要 C语言的基本数据类型分为:整型.字符型和浮点型,大多C语言教材都概括了整型和字符型数据的编码及输入输出,但并未详细介绍浮点型数据的编码及输入输出,这导致很多学生不能灵活运用这 ...

  4. C语言的灵魂--指针(1)

    C程序设计中使用指针可以: 使程序简洁.紧凑.高效 有效地表示复杂的数据结构 动态分配内存 得到多于一个的函数返回值 取地址运算符& 格式:&变量名 含义:取出存放变量的地址 取地址值 ...

  5. 你真的理解C语言的灵魂 “ 指针 ” 吗?(初阶篇)

  6. 【语言的灵魂】C语言的灵魂 “ 指针 ”

  7. c语言指针转换成数组,浅议C语言中灵魂数组和指针的互操作(转)

    浅议C语言中灵魂数组和指针的互操作(转)[@more@]曾听好多朋友说,C是一种怀旧的语言,因为它的历史很久远,然而自从各种面向对象的编程语言的相续出现让它的影响力日减. 当然了,这是无可非议的,但是 ...

  8. C语言 const 修饰指针 - C语言零基础入门教程

    目录 一.const 简介 二.const 修饰指针 1.const int *p 2.int const *p 3.int * const p 4.const int * const p 三.猜你喜 ...

  9. maven和gradle深析

    maven和gradle深析 本质 gradle使用的是真正的脚本语言groovy(很关键一点就是它也是编译为class运行在jvm上的),maven使用的是标签语言xml(本质是就是符合预定的文本) ...

最新文章

  1. 用户数年增长 300%,BitMax如何把握数字资产时代机遇?
  2. 为什么 CPU 访问硬盘很慢
  3. .net mysql 参数,在MySQL .NET Provider中使用命名参数
  4. html常规的布局模版,html5/css3常规布局(示例代码)
  5. 亚稳态到底是什么呢?
  6. Docker 快速学习(一)
  7. gcc编译时rpath可以使用多个路径,用:分隔
  8. 遇到暴风影音 activeX 漏洞
  9. 各种常用的 Win32Api 汇总(持续更新中. . .)
  10. 用DDA算法绘制一条直线
  11. 使用Mac下的sequel Pro数据库错误MySQL said: Authentication plugin 'caching_sha2_password'
  12. Unity Shader 法线贴图原理解析
  13. 2017-2018 Petrozavodsk Winter Training Camp, Saratov SU Contest C.Cover the Paths 贪心+DFS
  14. 咸鱼Maya笔记—创建NURBS基本体
  15. [M1]Daily Scum 10.9
  16. 华为防火墙虚拟系统实验
  17. 《SysML精粹》学习记录--第四章
  18. VOLTE网络架构、接口与功能实体
  19. Servlet容器装载Servlet的三种情况
  20. 小米(绿米联创)39元 无线开关破解(NXP JN5169 zigbee 3.0开发实战)

热门文章

  1. elasticsearch启动报错:unable to install syscall filter: java.lang.UnsupportedOperationException: seccomp
  2. 多租户积分系统功能清单
  3. 计算机组成原理第二章例题解析(下)
  4. 【Python全栈100天学习笔记】Day41 Django快速上手
  5. Android音乐播放器开发(3)—注册
  6. Tex资料及问题解决方案汇总
  7. 海康摄像机如何用第三方域名/外网固定IP方式远程观看?--官方文档记录
  8. 一位资深程序员的亲身经历:跳槽国企要注意啥
  9. c语言打字游戏程序设计报告,打字游戏程序设计报告.doc
  10. 高通 MSM8K GPT异常导致无法开机问题分析