这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

系列文章见:

CGO是什么

简单点来讲,如果要调用C++,C写的库(动态库,静态库),那么就需要使用Cgo。其他情况下一般用不到,只需要知道Go能调用C就行了,当然C也可以回调到Go中。

使用Cgo有2种姿势:

  1. 直接在go中写c代码
  2. go调用so动态库(c++要用extern “c”导出)

为了熟悉CGO,我们先介绍第一种方法,直接在Go中写C代码。

入门,直接在Go中写C代码

引用:Command cgo

首先,通过import “C”导入伪包(这个包并不真实存在,也不会被Go的compile组件见到,它会在编译前被CGO工具捕捉到,并做一些代码的改写和桩文件的生成)

import "C"

  

然后,Go 就可以使用C的变量和函数了, C.size_t 之类的类型、诸如 C.stdout 之类的变量或诸如 C.putchar 之类的函数。

func main(){
cInt := C.int(1) // 使用C中的int类型
fmt.Println(goInt) ptr := C.malloc(20) // 调用C中的函数
fmt.Println(ptr) // 打印指针地址
C.free(ptr) // 释放,需要 #include <stdlib.h>
}

  

如果“C”的导入紧跟在注释之前,则该注释称为序言。例如:

// #include <stdio.h>
/* #include <errno.h> */
import "C"

  

序言可以包含任何 C 代码,包括函数和变量声明和定义。然后可以从 Go 代码中引用它们,就好像它们是在包“C”中定义的一样。可以使用序言中声明的所有名称,即使它们以小写字母开头。例外:序言中的静态变量不能从 Go 代码中引用;静态函数是允许的。

所以,你可以直接在/**/里面写C代码(注意,C++不行!):

package main

/*
int add(int a,int b){
return a+b;
}
*/
import "C"
import "fmt" func main() {
a, b := 1, 2
c := C.add(a, b)
}

 

编译下,会出现下面的问题( fmt.Println(C.add(1, 2)) 能编译通过,思考下为什么? ):

./main.go:20:12: cannot use a (type int) as type _Ctype_int in argument to _Cfunc_add
./main.go:20:12: cannot use b (type int) as type _Ctype_int in argument to _Cfunc_add

  

为什么呢?因为C没有办法使用Go的类型,必须先转换成CGO类型才可以,改成这样就行了:

func main() {
cgoIntA, cgoIntB := C.int(1), C.int(2)
c := C.add(cgoIntA, cgoIntB)
fmt.Println(c)
}

  

运行后输出:

3

  

CGO基础类型

就像上面的代码一样,Go没有办法直接使用C的东西,必须先转换成CGO类型,下面是一个基础类型对应表。

C类型 CGO类型 GO类型
char C.char byte
signed char C.schar int8
unsigned char C.uchar uint8
short C.short int16
unsigned short C.ushort uint16
int C.int int32
unsigned int C.uint uint32
long C.long int32
unsigned long C.ulong uint32
long long int C.longlong int64
unsigned long long int C.ulonglong uint64
float C.float float32
double C.double float64
size_t C.size_t uint

如果直接在C中 #include <stdint.h>,则类型关系就比较一致了,例如:

C类型 CGO类型 GO类型
int8_t C.int8_t int8
int16_t C.int16_t int16
uint32_t C.uint32_t uint32
uint64_t C.uint64_t uint64

字符串、数组和函数调用

那么,在Go要如何传递字符串、字节数组以及指针? CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换:

// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char // Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer // C string to Go string
func C.GoString(*C.char) string // C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string // C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

 

字符串,可以通过C.CString()函数(别忘记通过free释放):

// 通过C.CString,这里会发生内存拷贝,cgo通过malloc重新开辟了一块空间,使用完需要释放,否则内存泄露
imagePath := C.CString("a.png")
defer C.free(unsafe.Pointer(imagePath))

 

字节数组,直接使用go的数组,然后强制转换即可:

// 只能使用数组,无法使用切片用作缓冲区给C使用
var buffer [20]byte
// &buffer[0]: 数组在内存中是连续存储的,取首地址
// unsafe.Pointer():转换为非安全指针,类型是*unsafe.Pointer
// (*C.char)():再强转一次
cBuffer := (*C.char)(unsafe.Pointer(&buffer[0]))
 

对应类型的指针,直接使用Cgo类型,然后&取地址即可:

bufferLen := C.int(20)
cPoint := &bufferLen // cPoint在CGO中是*C.int类型,在C中是*int类型。

  

假如ocr识别函数如下:

int detect(const char* image_path, char * out_buffer, int *len);

  

有3个参数:

  • image_path:指示了要识别的图片路径。
  • out_buffer:识别到的文字输出到这里,是一个char字节数组。
  • len:指示输出字节缓冲区大小,调用成功后,值变成字符串长度,便于外界读取。

在go中调用方式如下:

imagePath := C.CString("a.png")
defer C.free(unsafe.Pointer(imagePath)) var buffer [20]byte
bufferLen := C.int(20)
cInt := C.detect(imagePath, (*C.char)(unsafe.Pointer(&buffer[0])), &bufferLen)
if cInt == 0 {
fmt.Println(string(buffer[0:bufferLen]))
}

 

分离Go和C代码

为了简化代码,我们可以把C的代码放到xxx.h和xxx.c中实现。

有以下结构:

├── hello.c
├── hello.h
└── main.go

  

hello.h的内容:

#include <stdio.h>

void sayHello(const char* text);

  

hello.c:

#include "hello.h"

void sayHello(const char* text){
printf("%s", text);
}

  

main.go中调用hello.h中的函数:

#include "hello.h"
import "C" // 必须放在导入c代码活头文件的注释后面,否则不生效 func main() {
cStr := C.CString("hello from go")
defer C.free(unsafe.Pointer(cStr))
C.sayHello(cStr)
}

  

常用cgo编译指令

如果我们把h和c文件放到其他目录,则编译会报错:

├── main.go
└── mylib
├── hello.c
└── hello.h

Undefined symbols for architecture x86_64:
  "_sayHello", referenced from:
      __cgo_7ab15a91ce47_Cfunc_sayHello in _x002.o
     (maybe you meant: __cgo_7ab15a91ce47_Cfunc_sayHello)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

  

这里应该可以使用#cgo预编译解决(CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS) :

// #cgo CFLAGS: -DPNG_DEBUG=1 -I ./include
// #cgo LDFLAGS: -L /usr/local/lib -lpng
// #include <png.h>
import "C"
  • CFLAGS:-D部分定义了宏PNG_DEBUG,值为1。-I定义了头文件包含的检索目录
  • LDFLAGS:-L指定了链接时库文件检索目录,-l指定了链接时需要链接png库

通常实际的工作中遇到要使用cgo的场景,都是调用动态库的方式,所以这里没有继续往下深究上面的错误如何解决了。

调用C静态库和动态库

目录结构如下:

├── call_shared_lib
│ └── main.go
├── call_static_lib
│ └── main.go
└── mylib
├── hello.c
├── hello.h
├── libhello.a
└── libhello.so

1)静态库

把上面的hello.h 和 hello.c 生成为静态库(需要安装gcc,省略):

# 生成o对象
$ gcc -c hello.c
# 生成静态库
$ ar crv libhello.a hello.o
# 查看里面包含的内容
# ar -t libhello.a
# 使用静态库
#gcc main.c libhello.a -o main

  

Go中调用C静态库:

package main

/*
#cgo CFLAGS: -I ../mylib
#cgo LDFLAGS: -L ../mylib -lhello
#include <stdlib.h>
#include "hello.h"
*/
import "C"
import "unsafe" // 请先按照README.md 生成libhello.a 静态库文件
func main() {
cStr := C.CString("hello from go")
defer C.free(unsafe.Pointer(cStr)) C.sayHello(cStr)
}

  

2)动态库

生成

# 生成o对象
$ gcc -fPIC -c hello.c
# 生成动态库
$ gcc -shared -fPIC -o libhello.so hello.o
# 使用动态库
#gcc main.c -L. -lhello -o main

  

调用代码和上面一样的,LDFLAGS加上-lstdc++:

#cgo LDFLAGS: -L ../mylib -lhello -lstdc++

注意,生成的so文件一定的是libhello.so,然后在Go中只需要写-lhello即可,不是libhello,linux下会自动增加lib前缀。

唯一不同的是,静态库需要指定so文件的搜索路径或者把so动态库拷贝到/usr/lib下,在环境变量中配置:

$ export LD_LIBRARY_PATH=../mylib
$ go run main.go
# 也可以在goland中在Run -> Edit Configurations -> Environment 配置 LD_LIBRARY_PATH=../mylib ,方便调试

  

更多关于静态库和动态库的区别:segmentfault.com/a/119000002…

调用C++动态库

本质上和调用c动态库在Go的写法上是一样的,只是需要导出成C风格的即可:

#ifdef __cplusplus
extern "C"
{
#else // 导出C 命名风格函数,函数名字和定义的一样,C++因为支持重载,所以导出的函数名被编译器改变了 #ifdef __cplusplus
}
#endif

CGO的缺陷

cgo is not Go中总结了cgo 的缺点:

  1. 编译变慢,实际会使用 c 语言编译工具,还要处理 c 语言的跨平台问题
  2. 编译变得复杂
  3. 不支持交叉编译
  4. 其他很多 go 语言的工具不能使用
  5. C 与 Go 语言之间的的互相调用繁琐,是会有性能开销的
  6. C 语言是主导,这时候 go 变得不重要,其实和你用 python 调用 c 一样
  7. 部署复杂,不再只是一个简单的二进制

这篇文章描述了CGO通过go去调用C性能开销大的原因:blog.csdn.net/u010853261/…

  • 必须切换go的协程栈到系统线程的主栈去执行C函数
  • 涉及到系统调用以及协程的调度。
  • 由于需要同时保留C/C++的运行时,CGO需要在两个运行时和两个ABI(抽象二进制接口)之间做翻译和协调。这就带来了很大的开销。

《GO原本》中进一步通过runtime源码解读了原因

所以,使用的时候,自己灵活根据场景取舍吧

CGO最佳使用场景总结

CGO的一些缺点:

  1. 内存隔离

  2. C函数执行切换到g0(系统线程)

  3. 收到GOMAXPROC线程限制

  4. CGO空调用的性能损耗(50+ns)

  5. 编译损耗(CGO其实是有个中间层)

CGO 适合的场景:

  1. C 函数是个大计算任务(不在乎CGO调用性能损耗)

  2. C 函数调用不频繁

  3. C 函数中不存在阻塞IO

  4. C 函数中不存在新建线程(与go里面协程调度由潜在可能互相影响)

  5. 不在乎编译以及部署的复杂性

更多可以阅读:

Ocr实战

1.chineseocr_lite介绍

GitHub: github.com/DayBreak-u/…

Star: 7.1 K

介绍:超轻量级中文ocr,支持竖排文字识别, 支持ncnn、mnn、tnn推理 ( dbnet(1.8M) + crnn(2.5M) + anglenet(378KB)) 总模型仅4.7M。

这个开源项目提供了C++、JVM、Android、.Net等实现,没有任何三方依赖,经作者实践,识别效果中等,越小的图片越快。

比如识别一个发票号码,只需要50ms左右:

复杂的图片识别大概500-900ms左右:

表格识别效果一般

所以,适合格式一致的识别场景。比如发票的某个位置,身份证,银行卡等等

2.编译chineseocr_lite

按照 chineseocr_lite/cpp_projects/OcrLiteOnnx 中的README.md文档编译即可,推荐在Linux下,我再windows和Macos没编译通过。

然后需要改造成动态库,我改动的内容有:

  • 默认生成动态库,给ocr_http_server使用
  • 去掉jni的支持
  • 增加ocr.h,导出c风格函数

3.导出c函数

ocr.h

/** @file ocr.h
* @brief 封装给GO调用
* @author teng.qing
* @date 8/13/21
*/
#ifndef ONLINE_BASE_OCRLITEONNX_OCR_H
#define ONLINE_BASE_OCRLITEONNX_OCR_H
#ifdef __cplusplus
extern "C"
{
#else
// c
typedef enum{
false, true
}bool;
#endif const int kOcrError = 0;
const int kOcrSuccess = 1;
const int kDefaultPadding = 50;
const int kDefaultMaxSideLen = 1024;
const float kDefaultBoxScoreThresh = 0.6f;
const float kDefaultBoxThresh = 0.3f;
const float kDefaultUnClipRatio = 2.0f;
const bool kDefaultDoAngle = true;
const bool kDefaultMostAngle = true; /**@fn ocr_init
*@brief 初始化OCR
*@param numThread: 线程数量,不超过CPU数量
*@param dbNetPath: dbnet模型路径
*@param anglePath: 角度识别模型路径
*@param crnnPath: crnn推理模型路径
*@param keyPath: keys.txt样本路径
*@return <0: error, >0: instance
*/
int ocr_init(int numThread, const char *dbNetPath, const char *anglePath, const char *crnnPath, const char *keyPath); /**@fn ocr_cleanup
*@brief 清理,退出程序前执行
*/
void ocr_cleanup(); /**@fn ocr_detect
*@brief 识别图片
*@param image_path: 图片完整路径,会在同路径下生成图片识别框选效果,便于调试
*@param out_json_result: 识别结果输出,json格式。
*@param buffer_len: 输出缓冲区大小
*@param padding: 50
*@param maxSideLen: 1024
*@param boxScoreThresh: 0.6f
*@param boxThresh: 0.3f
*@param unClipRatio: 2.0f
*@param doAngle: true
*@param mostAngle: true
*@return 成功与否
*/
int ocr_detect(const char *image_path, char *out_buffer, int *buffer_len, int padding, int maxSideLen,
float boxScoreThresh, float boxThresh, float unClipRatio, bool doAngle, bool mostAngle); /**@fn ocr_detect
*@brief 使用默认参数,识别图片
*@param image_path: 图片完整路径
*@param out_buffer: 识别结果,json格式。
*@param buffer_len: 输出缓冲区大小
*@return 成功与否
*/
int ocr_detect2(const char *image_path, char *out_buffer, int *buffer_len); #ifdef __cplusplus
}
#endif
#endif //ONLINE_BASE_OCRLITEONNX_OCR_H

  

ocr.cpp

/** @file ocr.h
* @brief
* @author teng.qing
* @date 8/13/21
*/
#include "ocr.h"
#include "OcrLite.h"
#include "omp.h"
#include "json.hpp"
#include <iostream>
#include <sys/stat.h>
using json = nlohmann::json;
static OcrLite *g_ocrLite = nullptr;
inline bool isFileExists(const char *name) {
struct stat buffer{};
return (stat(name, &buffer) == 0);
}
int ocr_init(int numThread, const char *dbNetPath, const char *anglePath, const char *crnnPath, const char *keyPath) {
omp_set_num_threads(numThread); // 并行计算
if (g_ocrLite == nullptr) {
g_ocrLite = new OcrLite();
}
g_ocrLite->setNumThread(numThread);
g_ocrLite->initLogger(
true,//isOutputConsole
false,//isOutputPartImg
true);//isOutputResultImg
g_ocrLite->Logger(
"ocr_init numThread=%d, dbNetPath=%s,anglePath=%s,crnnPath=%s,keyPath=%s \n",
numThread, dbNetPath, anglePath, crnnPath, keyPath);
if (!isFileExists(dbNetPath) || !isFileExists(anglePath) || !isFileExists(crnnPath) || !isFileExists(keyPath)) {
g_ocrLite->Logger("invalid file path.\n");
return kOcrError;
}
g_ocrLite->initModels(dbNetPath, anglePath, crnnPath, keyPath);
return kOcrSuccess;
}
void ocr_cleanup() {
if (g_ocrLite != nullptr) {
delete g_ocrLite;
g_ocrLite = nullptr;
}
}
int ocr_detect(const char *image_path, char *out_buffer, int *buffer_len, int padding, int maxSideLen,
float boxScoreThresh, float boxThresh, float unClipRatio, bool doAngle, bool mostAngle) {
if (g_ocrLite == nullptr) {
return kOcrError;
}
if (!isFileExists(image_path)) {
return kOcrError;
}
g_ocrLite->Logger(
"padding(%d),maxSideLen(%d),boxScoreThresh(%f),boxThresh(%f),unClipRatio(%f),doAngle(%d),mostAngle(%d)\n",
padding, maxSideLen, boxScoreThresh, boxThresh, unClipRatio, doAngle, mostAngle);
OcrResult result = g_ocrLite->detect("", image_path, padding, maxSideLen,
boxScoreThresh, boxThresh, unClipRatio, doAngle, mostAngle);
json root;
root["dbNetTime"] = result.dbNetTime;
root["detectTime"] = result.detectTime;
for (const auto &item : result.textBlocks) {
json textBlock;
for (const auto &boxPoint : item.boxPoint) {
json point;
point["x"] = boxPoint.x;
point["y"] = boxPoint.y;
textBlock["boxPoint"].push_back(point);
}
for (const auto &score : item.charScores) {
textBlock["charScores"].push_back(score);
}
textBlock["text"] = item.text;
textBlock["boxScore"] = item.boxScore;
textBlock["angleIndex"] = item.angleIndex;
textBlock["angleScore"] = item.angleScore;
textBlock["angleTime"] = item.angleTime;
textBlock["crnnTime"] = item.crnnTime;
textBlock["blockTime"] = item.blockTime;
root["textBlocks"].push_back(textBlock);
root["texts"].push_back(item.text);
}
std::string tempJsonStr = root.dump();
if (static_cast<int>(tempJsonStr.length()) > *buffer_len) {
g_ocrLite->Logger("buff_len is too small \n");
return kOcrError;
}
*buffer_len = static_cast<int>(tempJsonStr.length());
::memcpy(out_buffer, tempJsonStr.c_str(), tempJsonStr.length());
return kOcrSuccess;
}
int ocr_detect2(const char *image_path, char *out_buffer, int *buffer_len) {
return ocr_detect(image_path, out_buffer, buffer_len, kDefaultPadding, kDefaultMaxSideLen, kDefaultBoxScoreThresh,
kDefaultBoxThresh, kDefaultUnClipRatio, kDefaultDoAngle, kDefaultMostAngle);
}

  

ocr_wrapper.go

package ocr

// -I: 配置编译选项
// -L: 依赖库路径 /*
#cgo CFLAGS: -I ../../../OcrLiteOnnx/include
#cgo LDFLAGS: -L ../../../OcrLiteOnnx/lib -lOcrLiteOnnx -lstdc++ #include <stdlib.h>
#include <string.h>
#include "ocr.h"
*/
import "C"
import (
"runtime"
"unsafe"
) //const kModelDbNet = "dbnet.onnx"
//const kModelAngle = "angle_net.onnx"
//const kModelCRNN = "crnn_lite_lstm.onnx"
//const kModelKeys = "keys.txt" const kDefaultBufferLen = 10 * 1024 var (
buffer [kDefaultBufferLen]byte
) func Init(dbNet, angle, crnn, keys string) int {
threadNum := runtime.NumCPU() cDbNet := C.CString(dbNet) // to c char*
cAngle := C.CString(angle) // to c char*
cCRNN := C.CString(crnn) // to c char*
cKeys := C.CString(keys) // to c char* ret := C.ocr_init(C.int(threadNum), cDbNet, cAngle, cCRNN, cKeys) C.free(unsafe.Pointer(cDbNet))
C.free(unsafe.Pointer(cAngle))
C.free(unsafe.Pointer(cCRNN))
C.free(unsafe.Pointer(cKeys))
return int(ret)
} func Detect(imagePath string) (bool, string) {
resultLen := C.int(kDefaultBufferLen) // 构造C的缓冲区
cTempBuffer := (*C.char)(unsafe.Pointer(&buffer[0]))
cImagePath := C.CString(imagePath)
defer C.free(unsafe.Pointer(cImagePath)) isSuccess := C.ocr_detect2(cImagePath, cTempBuffer, &resultLen)
return int(isSuccess) == 1, C.GoStringN(cTempBuffer, resultLen)
} func CleanUp() {
C.ocr_cleanup()
}

  

3.环境变量设置

路径包含库所在目录,或者直接把动态库拷贝到/usr/lib中,推荐后者:

export LD_LIBRARY_PATH=../mylib

  

4.运行

效果如下

参考

CGO入门和OCR文字识别(非第三方API,有源码,效果好)实战的更多相关文章

  1. 如何精准实现OCR文字识别?

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由云计算基础发表于云+社区专栏 前言 2018年3月27日腾讯云云+社区联合腾讯云智能图像团队共同在客户群举办了腾讯云OCR文字识别-- ...

  2. OCR文字识别笔记总结

    OCR的全称是Optical Character Recognition,光学字符识别技术.目前应用于各个领域方向,甚至这些应用就在我们的身边,比如身份证的识别,交通路牌的识别,车牌的自动识别等等.本 ...

  3. 云+社区分享——腾讯云OCR文字识别

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由云+社区运营团队发布在腾讯云+社区 前言 2018年3月27日腾讯云云+社区联合腾讯云智能图像团队共同在客户群举办了腾讯云OCR文字识 ...

  4. 我的AI之路 —— OCR文字识别快速体验版

    OCR的全称是Optical Character Recoginition,光学字符识别技术.目前应用于各个领域方向,甚至这些应用就在我们的身边,比如身份证的识别.交通路牌的识别.车牌的自动识别等等. ...

  5. Android OCR文字识别 实时扫描手机号(极速扫描单行文本方案)

    身份证识别:https://github.com/wenchaosong/OCR_identify 遇到一个需求,要用手机扫描纸质面单,获取面单上的手机号,最后决定用tesseract这个开源OCR库 ...

  6. 怎么给OCR文字识别软件重编文档页面号码

    ABBYY FineReader Pro for Mac OCR文字识别软件处理文档时,在FineReader文档中,页面的加载顺序即是页面的导入顺序,完成导入之后,文档的所有页面均会被编号,各编号会 ...

  7. 对OCR文字识别软件的扫描选项怎么设置

    说到OCR文字识别软件,越来越多的人选择使用ABBYY FineReader识别和转换文档,然而并不是每个人都知道转换质量取决于源图像的质量和所选的扫描选项,今天就给大家普及一下这方面的知识. ABB ...

  8. 怎么提高OCR文字识别软件的识别正确率

    在OCR文字识别软件当中,ABBYY FineReader是比较好用的程序之一,但再好的识别软件也不能保证100%的识别正确率,用户都喜欢软件的正确率高一些,以减轻识别后修正的负担,很多用户也都提过这 ...

  9. OCR文字识别软件许可文件被误删了怎么办

    使用任何一款软件,都会有误操作的情况发生,比如清理文件时一不小心删除了许可文件,对于ABBYY FineReader 12这样一款OCR文字识别软件,因失误错误删除了许可文件该怎么办呢?今天就来给大家 ...

随机推荐

  1. MindSpore模型精度调优实战:常用的定位精度调试调优思路

    摘要:在模型的开发过程中,精度达不到预期常常让人头疼.为了帮助用户解决模型调试调优的问题,我们为MindSpore量身定做了可视化调试调优组件:MindInsight. 本文分享自华为云社区<技 ...

  2. 【知识点】H264, H265硬件编解码基础及码流分析

    前言 音视频开发需要你懂得音视频中一些基本概念,针对编解码而言,我们必须提前懂得编解码器的一些特性,码流的结构,码流中一些重要信息如sps,pps,vps,start code以及基本的工作原理,而大 ...

  3. 怀疑前端组件把我的excel文件搞坏了,怎么证明

    背景 我在做个需求,用户通过excel上传文件,文件中,每一行就是一条数据,后台批量处理:但是呢,用户填的数据可能有问题,所以我后台想先做个检查,然后在每一行中加一列,来指出这一行存在的问题. 我本来 ...

  4. VirtualBox 修改Android x86虚拟机的分辨率

    首先说明一下,本人使用的是Windows下的VirtualBox,android x86使用的是9.0-r2版本 一.查看virtualbox中已有的分辨率 启动虚拟机后,连续按两次E键,进入下面页面 ...

  5. Debian9 无线网卡驱动安装

    https://wiki.debian.org/iwlwifi sudo vi /etc/apt/sources.list --------- ..... deb http://httpredir.d ...

  6. 【洛谷P1795 无穷的序列_NOI导刊2010提高(05)】模拟

    分析 map搞一下 AC代码 #include <bits/stdc++.h> using namespace std; map<int,int> mp; inline int ...

  7. c++ 的父类 new 重载, 子类new 对象的时候会调用父类的operater new

    子类在new 对象的 时候  父类的new 进行了重载,那么会调用父类的operater new() 函数 #include <iostream> #include <string& ...

  8. CMS垃圾收集器——重新标记和浮动垃圾的思考

    <深入理解java虚拟机 第二版 JVM高级特性与最佳实践>里面提到 CMS 垃圾收集器. CMS 垃圾收集器的垃圾回收分4个步骤: 初始标记(initial mark) 有 STW 并发 ...

  9. SQL SERVER 雨量计累计雨量(小时)的统计思路

    PLC中定时读取5分钟雨量值,如何将该值统计为小时雨量作为累计?在sql server group by聚合函数,轻松实现该目的. 1.编写思路 数据库中字段依据datetime每五分钟插入一条语句, ...

  10. 高德开放平台实现批量自定义marker和信息窗体显示

    上篇博客提到云图无法实现文本标签标记marker,这篇博客着重实现在marker点文本标记以及自定义按钮窗体显示. 1.效果: 2.代码实现 <!doctype html> <htm ...