C/C++ 存储类别
table {
margin: auto;
}
本文介绍 C/C++ 中的存储类别。所谓的“存储类别”究竟是什么意思? 存储类别主要指在内存中存储数据的方式,其大致牵涉到变量的三个方面 —— 作用域、链接性和存储期,也就是说这三个方面决定了存储类别。下面先解释这三个概念,再介绍在 C/C++ 中的表示形式。
存储类别定义
作用域 (scope) 描述程序中可访问变量的区域,主要有块作用域 (block scope) 变量和 文件作用域 (file scope) 变量,平常我们也分别用局部变量和全局变量来指代这两者。这里需要注意的是,在 C/C++ 中一个源文件通常包含一个或多个头文件 (.h 扩展名),在实际编译之前的预处理阶段会将头文件内容替换源文件中的
#include
指令,所以源代码文件和所有的头文件都被看成是一个包含信息的单独文件,这整个文件被称为翻译单元 (translation unit)。描述一个具有文件作用域的变量时,其实际可见范围是整个翻译单元。链接性 (linkage) 描述变量能否跨文件访问,而按上面的定义,更准确地讲应该是能否 “跨翻译单元访问”。主要分为三种:
- 外部链接(external linkage):可以在多个翻译单元中使用。
- 内部链接(internal linkage): 只能在一个翻译单元中使用,即只能在被声明的文件中使用。
- 无链接(no linkage): 只能被定义块所私有,不能被程序其他部分引用。比如函数定义由块包围,其内部的参数、变量都是无链接变量。
由此可见作用域和链接性共同描述了变量的可见性。
存储期 (storage duration) 指变量在内存中保留了多长时间。通常分为四种:
- 静态存储期 (static storage duration): 在程序执行期间一直存在。文件作用域变量都具有静态存储期。
- 线程存储期 (thread storage duration):具有线程存储期的对象,从被声明到线程结束一直存在。以关键字
_Thread_local
声明一个变量时,每个线程都获得该变量的私有备份。 - 自动存储期 (auto storage duration): 块作用域的变量通常具有自动存储期,当程序进入定义变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存。
- 动态分配存储期 (allocated storage duration): 使用
malloc
或new
函数创建而成的指针变量具有动态分配存储期,需要使用者手动使用free
或delete
释放占用的内存。任何可以访问该指针的函数均可以访问这块内存。例如,一个函数可以把这个指针的值返回给另一个函数,那么另一个函数也可以访问该指针指向的内存。
存储类别在 C/C++ 中的表示形式
在 C/C++ 中如何表示上述的这些概念? 一句话概括就是在合适的位置使用合适的关键字就可以了,这些关键字有 auto
、register
、extern
、static
、_Thread_local (C11)
,下表进行了总结:
存储类别 | 作用域 | 链接性 | 存储期 | 声明方式 |
---|---|---|---|---|
自动 | 块作用域 | 无链接 | 自动存储期 | 在块中 (可选使用关键字 auto ) |
寄存器 | 块作用域 | 无链接 | 自动存储期 | 在块中,使用关键字 register |
静态、外部链接 | 文件作用域 | 外部链接 | 静态存储期 | 在所有函数外部 |
静态、内部链接 | 文件作用域 | 内部链接 | 静态存储期 | 在所有函数外部,使用关键字 static |
静态、无链接 | 块作用域 | 无链接 | 静态存储期 | 在块中,使用关键字 static |
线程、外部链接 | 文件作用域 | 外部链接 | 线程存储期 | 在所有函数外部,使用关键字 _Thread_local |
线程、内部链接 | 文件作用域 | 内部链接 | 线程存储期 | 在所有函数外部,使用关键字 static 和 _Thread_local |
线程、无链接 | 块作用域 | 无链接 | 线程存储期 | 在块中,使用关键字 static 和 _Thread_local |
1、自动存储类别
属于自动存储类别的变量具有块作用域、无链接性和自动存储期,默认情况下,声明在块或函数头的任何变量都属于自动变量,可以使用 auto
关键字进行强调,也可以不使用,如下面的 a 和 b 都是自动变量:
int main(void)
{
int a;
auto int b;
}
注意 auto
关键字在 C++ 11 中有完全不同的用法。如果编写 C/C++ 兼容的程序,最好不要使用 auto
作为存储类别说明符。
2、寄存器存储类别
顾名思义,寄存器变量通常直接存储在 CPU 的寄存器中,那么其访问速度会比在普通内存中存储快很多。下面的存储器层次结构显示寄存器的访问速度远快于主存 (越往上速度越快):
当然使用 register
关键字声明并不意味着该变量一定会被存储在寄存器中。在实际中,编译器并不一定会这样做(比如,可能没有足够数量的寄存器供编译器使用)。实际上,现在性能优越的编译器能够识别出经常使用的变量,并将其放在寄存器中,而无需给出 register
声明。下面用一个例子测试 (编译需支持 C++ 11):
#include <iostream>
#include <chrono>
using namespace std;
using namespace chrono;
#define TIME 1000000000
int main()
{
register int a, b = TIME; /* 寄存器变量 */
int x, y = TIME; /* 自动变量 */
auto start1 = steady_clock::now();
for (a = 0; a < b; a++);
auto end1 = steady_clock::now();
auto dur1 = duration_cast<nanoseconds>(end1 - start1);
cout << "寄存器变量: " << double(dur1.count()) * nanoseconds::period::num / nanoseconds::period::den << "秒" << endl;
auto start2 = steady_clock::now();
for (x = 0; x < y; x++);
auto end2 = steady_clock::now();
auto dur2 = duration_cast<nanoseconds>(end2 - start2);
cout << "自动变量: " << double(dur2.count()) * nanoseconds::period::num / nanoseconds::period::den << "秒" << endl;
}
// 寄存器变量: 0.659秒
// 自动变量: 2.742秒
可以看到在这个例子中使用寄存器变量快了不少。
3、静态存储类别
使用 static
声明的变量都具有静态存储类别,所谓的 “静态” ,意思是变量在内存中的位置不变,而不是其值不变。静态存储类别都具有静态存储期, 即在程序执行期间一直存在,且只能被初始化一次,不论是局部变量还是全局变量。
3.1 块作用域的静态变量
声明局部变量时用 static
修饰,可以在函数调用之间保持该变量的值,而不需要在每次它进入和离开作用域时进行创建和销毁,即具有块作用域、无链接、静态存储期。
3.2 外部链接的静态变量
外部链接的静态变量具有文件作用域、外部链接和静态存储期。由于全局变量默认就是静态存储类别,所以不需要使用 static
关键字进行声明。因其具有外部链接,所以可以在别的文件中使用这个变量,但需要进行引用式声明,即在前面加上 extern
关键字。同样对于函数而言,也可以通过 extern
关键字使用别的文件的函数。
3.3 内部链接的静态变量
内部链接的静态变量具有文件作用域、内部链接和静态存储期,也就是用 static
修饰的全局变量,由于其内部链接属性,只能在同一个文件中使用。同样用 static
修饰的函数也是只能在同一文件中使用。容易让人混淆的是,这里的 static
改变的是全局变量的链接属性,而不是存储期。因为全局变量默认就是静态存储类别,不需要特意使用 static
修饰。
对于上述三种静态变量的区别,来看一个例子更清楚,机器学习中常用到的指定随机数种子进行训练初始化。有两个文件 ( main.c
和 rand.c
):
/******** main.c ********/
#include <stdio.h>
extern int count; // 声明外部变量
extern void srandom(unsigned int); // 声明外部函数
extern void generateRandom(int n); // 声明外部函数
int main(void)
{
unsigned int seed;
int n;
printf("请输入种子和数量,按 q 退出: \n");
while (scanf("%u %d", &seed, &n) == 2)
{
srandom(seed);
generateRandom(n);
printf("\n");
}
printf("总共生成了%d个随机数。\n", count);
}
/******** rand.c ********/
#include <stdio.h>
static unsigned int next = 1; // 内部链接的静态变量
int count = 0; // 外部链接的静态变量
static unsigned int random(void) // 内部链接的函数,该文件私有的私有函数
{
next = next * 1103515245 + 12345;
return (unsigned int) (next / 65536) % 32768;
}
void generateRandom(int n)
{
int subCount = 0;
static int round = 1; // 块作用域的静态变量
while (n--)
{
next = random();
printf("%u ", next);
subCount++;
}
printf("\n第%d轮生成了%d个随机数。\n", round, subCount);
round++;
count += subCount;
}
void srandom(unsigned int seed)
{
next = seed;
}
我们知道,计算机模拟出来的都不是真正的随机数,而是一种“伪随机数”,如果指定相同的随机数种子 (seed),那么每次都可以得到相同的结果:
gcc -o static main.c rand.c # 编译
./static # 运行
请输入种子和数量,按 q 退出:
1 5 # 种子1生成5个随机数
16838 14666 10953 11665 7451
第1轮生成了5个随机数。
1 8 # 种子1生成8个随机数
16838 14666 10953 11665 7451 26316 27974 27550
第2轮生成了8个随机数。
5 10 # 种子5生成10个随机数
18655 4557 22274 26675 10846 12206 7471 2634 16995 4072
第3轮生成了10个随机数。
q
总共生成了23个随机数。
可以看到,两次使用种子1生成的前几个随机数都是一样的,这里的关键是rand.c
中的 next
是一个静态全局变量,因而每次只要通过 srandom
函数将其设定成一样,就会生成一样的随机数。 rand.c
中的round
被声明为块作用域的静态变量,其不会像一般的局部变量那样每次进入和离开作用域时都会创建和销毁,所以能记录轮数。而 count
为外部链接的静态变量,所以可以在 main.c
中使用,前提是先用 extern
声明。
C/C++ 的内存管理
这一节来深究一下原理,先来关注存储期。本文开头定义了存储类别主要指在内存中存储数据的方式,那么具体究竟是什么样的方式?概括起来就类似于哲学的三大命题:
- 我存储于内存中的什么位置 (我是谁?)
- 我何时被创建于这个位置 (我从哪里来?)
- 我何时会从这个位置释放 (我到哪里去?)
首先看第一个问题,翻开任何一本操作系统教科书,都会告诉你,C/C++ 中内存大致分为这么几个区域:栈、堆、静态存储区,见下图:
那么存储在什么位置就明了了,自动存储期变量存放在栈中,动态分配存储期变量存放在堆中,静态存储期变量存放在静态存储区。而对于后面两个问题,则与这几个区域的运作方式有关,下面分别阐述:
1、栈 (stack): 栈被用以实现函数调用。在程序运行期间执行函数调用的时候,会向下增加栈帧 (stack frame),帧中存储局部变量 (自动变量) 以及该函数的返回地址。大部分编程语言都只允许使用位于栈中最下方的帧 ,而不允许调用其它的帧,所以在一个函数中无法调用其它函数的局部变量,这对应于块作用域的属性。在函数结束调用返回时,会从栈中弹出相应的栈帧,那么帧上的局部变量也会自动销毁,这对应了自动存储期的属性。所以对于第二第三个问题,自动存储期变量随着相应栈帧的出现而创建,又随着栈帧的销毁而释放,整个过程由操作系统控制,不需要使用者手动操作,这就是其“自动”的含义。
2、堆 (heap):和栈一样,堆 (heap) 也可以用来为变量分配内存,二者的一个不同点是栈上要分配的空间大小在编译时就已确定,而堆上分配的空间大小在运行时才会确定,这样的好处是当不知道需要多少空间时,不用在程序中预先指定大小。在堆中分配内存需要使用 malloc
或 new
函数,但使用完后不会自动释放,而是需要使用者手动使用 free
或 delete
释放占用的内存, 这就是动态分配存储期变量的创建和释放。
3、静态存储区: 堆和栈被统称为动态存储区,因为在运行期间都会动态地扩展和收缩,而与之相对的就是静态存储区,里面存放着全局变量和静态变量,这些变量在程序运行期间都会一直存在。静态存储区可细分为两个部分 —— .data
段用于存放已初始化的全局和静态变量,.bss
段用于存放未初始化的全局和静态变量。值得一提的是,静态存储区中的未显式初始化变量在运行时会被统一初始化为 0,而未显式初始化的 (在栈中的) 局部变量在运行时的初始值却是不确定的,来看一个例子:
#include<iostream>
using namespace std;
void first()
{
int a;
int c;
cout << "第一个函数中 a = " << a << endl;
cout << "第一个函数中 c = " << c << endl;
}
void second()
{
int b;
int c;
cout << "第二个函数中 b = " << b << endl;
cout << "第二个函数中 c = " << c << endl;
}
int main()
{
first();
second();
}
第一个函数中 a = 4201275
第一个函数中 c = 2686824
第二个函数中 b = 4201275
第二个函数中 c = 2686824
结果显示两个函数中不论变量的名称是啥,第一个变量 (a 和 b) 的默认初始化值都一样,第二个变量 (都是 c) 也是如此。这是因为函数结束调用后,虽然栈帧销毁了,但只要程序依然在运行,则栈也会一直存在,接下来调用其它函数时依然会向栈中同一片地址方向增长,未显式初始化的局部变量其默认值是之前分配给这同一个地址的值 (通常不会是 0),因而栈相当于一个可复用的暂存区。
666、线程局部存储:这种类型的变量具有线程存储期,顾名思义从被声明到线程结束一直存在于某个线程中,相当于和这个线程关联了起来。我们知道全局变量位于静态存储区,同一进程中各个线程都可以访问,因此它们存在多线程读写问题。而对于线程存储期变量,每个线程拥有其私有备份,不能被别的线程访问,这样可以有效避免竞争。从 C11
起要声明此类变量,需要添加 _Thread_local
关键字,下面使用 Pthreads API
创建一个例子,可以看到对于同样的变量 tl
,两个线程对其递增互不干扰,最后打印出不同的值:
#include <stdio.h>
#include <pthread.h>
_Thread_local static int tl = 0; // 声明为静态线程局部存储变量
struct arg
{
char * name;
int num;
};
void increase_tl(void * t)
{
struct arg * a = (struct arg *)t;
for (int i = 0; i < a->num; ++i)
tl++; // 每个线程获得tl的私有备份
printf("%s 中的 tl = %d\n", a->name, tl);
}
int main(void)
{
pthread_t thread1, thread2;
struct arg arg1 = {"线程1", 5};
struct arg arg2 = {"线程2", 10};
pthread_create(&thread1, NULL, (void *)&increase_tl, (void *)&arg1);
pthread_create(&thread2, NULL, (void *)&increase_tl, (void *)&arg2);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_exit(NULL);
}
// 线程1 中的 tl = 5
// 线程2 中的 tl = 10
实际上每个线程共享同一进程中的静态存储区、堆,但有各自独立的栈和线程存储期变量。综合上几段的描述,内存管理图也许可以更新成这样:
链接
上一节说的存储期和内存管理更多的是一种运行时概念,也就是说在程序实际运行时内存被划分为不同的区域,而正在运行的程序有个更为人熟知的名字,那就是进程 (process)。相对应的,链接 (linking) 是一个链接时概念,主要作用是将各种代码和数据片段收集和组合成为一个可执行文件。在第一节中提到有三种链接属性:外部链接、内部链接、无链接。外部链接的变量可以跨文件访问,内部链接的变量只能在文件内使用,而无链接变量只能在块中使用。既然有些变量可以在文件外访问,有些不能,那么文件中必然存在某种标示,标记了各变量的可访问区域。这里我们不过多牵涉链接的具体原理,而是重点关注不同链接属性的标示究竟是什么样的?
当然首先要说并不是任何文件都可以被用来链接的,可以用来链接的文件被称为 “可重定位目标文件”,通常以 .o
为扩展名。每个这类文件中都有一个符号表 (symbol table) ,记录了各种属性包括链接属性,下面用例子来说明,脱胎于上文中的随机数例子:
/******** rand.c ********/
#include <stdio.h> // gcc -c rand.c -o rand.o readelf -s rand.o
static unsigned int next = 1; // 第一个内部链接的静态变量
static unsigned int next2 = 1; // 第二个内部链接的静态变量
int count = 0; // 第一个外部链接的静态变量
int count2 = 0; // 第二个外部链接的静态变量
const int CONSTANT = 100; // 外部链接的常量
static const int CONSTANT2 = 100; // 内部链接的常量
void generateRandom(int n)
{
int subCount = 0;
static int round = 1; // 第一个块作用域的静态变量
static const int CONSTANT3 = 100; // 块作用域的内部链接的常量
const int CONSTANT4 = 100; // 无链接的常量
while (n--)
subCount++;
printf("\n第%d轮生成了%d个随机数。\n", round, subCount);
round++;
count += subCount;
}
void generateRandom222(int n)
{
int subCount = 0;
static int round = 1; // 第二个块作用域的静态变量(与generateRandom中的round同名)
static int round2 = 1; // 第三个块作用域的静态变量
while (n--)
subCount++;
printf("\n第%d轮生成了%d个随机数。\n", round, subCount);
round++;
count += subCount;
}
这里面的变量有点多,我特意加了几个常量,看它们的表示有何不同。下表进行了总结:
全局 (外部链接) | 内部链接静态 | 无链接静态 | 局部 (自动) | |
---|---|---|---|---|
变量 | count count2 | next next2 | round round2 | subCount |
常量 | CONSTANT | CONSTANT2 | CONSTANT3 | CONSTANT4 |
下面使用 gcc -c rand.c
生成名为 rand.o
的可重定位目标文件,再使用命令 readelf -s rand.o
可查看该文件中的符号表:
符号表中包含着丰富的信息,因为这里既有变量又有常量,所以要描述先得找个统一的称谓,正好在符号表的 Type
一列中显示这些量都是 OBJECT
(对象),这不是巧合,在 C 中这些占用内存的量确实被称为对象,所以后文也遵循这种名称,和面向对象编程里的对象是不一样的。
符号表中的对象按链接类型划分为三个部分,分别对应外部链接对象,内部链接对象和无链接对象,如下图。注意代码中的自动变量 (subCount
和 CONSTANT4
) 并不包含在符号表中,因为链接器对此类对象不感兴趣。
首先看 Bind
这一列,只有两种类型,LOCAL
和 GLOBAL
,可以看到外部链接的对象如 count, CONSTANT
都是 GLOBAL
属性,而内部链接对象和无链接对象则是 LOCAL
属性。自然从语义上看,GLOBAL
对应于所有文件都可以访问,LOCAL
对应于只有单个文件访问。
那么内部链接静态对象和无链接静态对象的区别是什么?从 Name
这一列可以看出,无链接的静态对象 (如 round, CONSTANT3
) 的名称后面都被加上了几个数字,如 round2.2304
,而且代码中显示,两个generateRandom
函数中实际上都定义了同名的 round
对象,而符号表中则在各自名称后面加上了不同的数字。这反映了两个 round
是不同的无链接静态对象,不能超出各自函数外访问它们。
从这里可以看出内部链接静态对象和无链接静态对象和 Java 中的静态变量有一些相似。Java 也通过 static
关键字在类中声明为静态变量,这些变量也只会被初始化一次,且只能在类内部起作用,这显示了其面向对象的特点,C++ 在这方面与之类似。而与之对应的是 C 中的内部链接静态对象只能在单个文件中使用,无链接静态对象只能在块中使用,二者和 Java 的静态变量一样会被存放在固定的区域。
最后看 Value
列,这里面的值可以理解为内存中的各对象的相对位置,同样用 16 进制表示。如果我们把这些对象的位置从小到大排列 (限于空间这里只取最后两位),就会发现一些有趣的现象。
对象 | next | next2 | round.2293 | round.2303 | round.2304 | .... | count | count2 |
---|---|---|---|---|---|---|---|---|
**Value **值 | 00 | 04 | 08 | 0c | 10 | .... | 00 | 04 |
0c 在十六进制中代表12, 10 代表16,那么这说明前 5 个对象的位置是连续的,因为都是 int
类型,所以是 4 个字节的间隔,而后两个 count
和 count
也是连续的。这说明内部链接静态对象和无链接静态对象是井然有序地存储在一起的,而外部链接静态对象则统一在另一片位置。另外几个常量的位置都很诡异,CONSTANT
和 count
的 Value 一样,而CONSTANT
和 next
的 Value 一样。因为 Value 值仅表示相对位置,所以这可能表示常量都存储在另外的一个区域,而且与全局/静态变量相隔较远。
/
C/C++ 存储类别的更多相关文章
- C语言杂谈(三)存储类别
本文讨论C语言中的存储类别,包括数据在内存的存储.变量的存储类别.函数的存储类别.生存周期.下图为计算机的存储空间,有寄存器和内存. 一.存储区域 1.寄存器:存放立即参加运算的数据. 2.系统区:存 ...
- C++变量的存储类别与作用域
总结一下C++中变量的存储类别以及变量的作用域. (1)标示符的存储类别决定了标示符在内存中存在的时间(我们可以理解标示符就是确定一个变量的符号,也就是我们所说的变量名) 二:存储类别 (1)静态存储 ...
- C语言变量的存储类别
我们知道,从变量的作用域(即从空间)角度来分,可以分为全局变量和局部变量. 从另一个角度,从变量值存在的作时间(即生存期)角度来分,可以分为静态存储方式和动态存储方式. 静态存储方式:是指在程序运行期 ...
- [C++程序设计]变量的存储类别
全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储单元,程序执行完毕就释放这些空间.在程序执行过程中它们占据固定的存储单元,而不是动态地进行分配和释放. 在动态存储区中存放以下数据: ...
- C语言_了解一下C语言中的四种存储类别
C语言是一门通用计算机编程语言,应用广泛.C语言的设计目标是提供一种能以简易的方式编译.处理低级存储器.产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言. C语言中的四种存储类别:auto ...
- c++ 变量的存储类别
c++的存储类别 首先我们得知道c++的变量存储方式:静态存储和动态存储两种,全局变量使用的是静态存储,函数的形参和局部变量是使用的动态存储. 当然在有的教程中又分为自动存储,静态存储,动态存储.相信 ...
- C语言中存储类别又分为四类:自动(auto)、静态(static)、寄存器的(register)和外部的(extern)。
除法运算中注意: 如果相除的两个数都是整数的话,则结果也为整数,小数部分省略,如8/3 = 2:而两数中有一个为小数,结果则为小数,如:9.0/2 = 4.500000. 取余运算中注意: 该运算只适 ...
- C++变量存储类别和内存四区
变量存储类别 变量声明/定义的一般形式: 存储类别 数据类型 变量名 存储类别指的是数据在内存中存储的方法.存储方法分为静态存储和动态存储两大类.标准C语言为变量.常量和函数定义了4种存储类型:ext ...
- c语言 变量的存储类别以及对应的内存分配?
<h4><strong>1.变量的存储类别</strong></h4>从变量值存在的角度来分,可以分为静态存储方式和动态存储方式.所谓静态存储方式指在程 ...
- C Primer Plus学习笔记(十一)- 存储类别、链接和内存管理
存储类别 从硬件方面来看,被储存的每个值都占用一定的物理内存,C 语言把这样的一块内存称为对象(object) 对象可以储存一个或多个值.一个对象可能并未储存实际的值,但是它在储存适当的值时一定具有相 ...
随机推荐
- Android开发学习3
学习内容: 1.复选框CheckBox 2.ImageView & 使用第三方库加载网络图片 3.列表视图ListView 4.网格视图GridView 5.ScrollView & ...
- 900A. Find Extra One#寻找与众不同的它(计数)
题目出处:http://codeforces.com/problemset/problem/900/A 题目大意:问删除一个点后,剩下的点能不能都在Y轴的同一边 #include<iostrea ...
- Python数据分析与展示第3周学习笔记(北京理工大学 嵩天等)
入门学习马上结束辽. 1.Pandas库 import pandas as pd 两个数据类型:Series,DataFrame Series类型:数据+索引 自定义索引 b = pd.Series( ...
- unittest如何在循环遍历一条用例时生成多个测试结果
引用自:http://blog.csdn.net/kaku21/article/details/42124593 参考网址:http://programmaticallyspeaking.com/te ...
- WebService客户端生成方法
1.使用JDK的wsimport.exe产生客户端代码 打开cmd命令框,输入如下格式命令: 格式:wsimport -s "src目录" -p "生成类所在包名&quo ...
- MySQL数据库优化、设计与高级应用
MySQL数据库优化主要涉及两个方面,一方面是对SQL语句优化,另一方面是对数据库服务器和数据库配置的优化. 数据库优化 SQL语句优化 为了更好的看到SQL语句执行效率的差异,建议创建几个结构复杂的 ...
- [LC] 404. Sum of Left Leaves
Find the sum of all left leaves in a given binary tree. Example: 3 / \ 9 20 / \ 15 7 There are two l ...
- 常用面试sql(1)
1:update qr_user_info set score =score+50 where level=3 2:delete from qr_user_info where level is nu ...
- vue element 关闭当前tab 跳转到上一路由
方法一 this.$store.dispatch('delVisitedViews', this.$route); this.$router.go(-1); 方法二 this.$store.state ...
- 数字签名和数字证书等openssl加密基本术语
openssl 算法基础 1.1 对称算法 : 密钥相同,加密解密使用同一密钥 1.2 摘要算法:无论用户输入的数据什么长度,输出的都是固定长度的密文:即信息摘要,可用于判断数据的完整性:目前常用的有 ...