C++ 项目中的extern “C”

1、 C++ 项目中的extern “C” {}

// 在用C++的项目源码中,经常会不可避免的会看到下面的代码:

#ifdef __cplusplus
extern "C" {
#endif
/*...*/
#ifdef __cplusplus
}
#endif

它到底有什么用呢,你知道吗?而且这样的问题经常会出现在面试 or 笔试中。

下面我就从以下几个方面来介绍它

1、#ifdef _cplusplus/ #endif _cplusplus 及发散

2、extern “C”

2.1、extern关键字
2.2、”C”
2.3、小结extern “C”
3、C和C++互相调用
3.1、C++的编译和连接
3.2、C的编译和连接
3.3、C++中调用C的代码
3.4、C中调用C++的代码
4、C和C++混合调用特别之处函数指针

1、#ifdef _cplusplus/#endif _cplusplus及发散

在介绍 extern "C" 之前,我们来看下 #ifdef _cplusplus / #endif _cplusplus 的作用。
很明显 #ifdef / #endif 、#ifndef/#endif用于条件编译,#ifdef _cplusplus/#endif _cplusplus
—--—表示如果定义了宏 _cplusplus,就执行 #ifdef/#endif 之间的语句,否则就不执行。
在这里为什么需要 #ifdef _cplusplus/#endif _cplusplus呢?因为 C 语言中不支持 extern "C" 声明,
如果你明白 extern "C" 的作用就知道在 C 中也没有必要这样做,这就是条件编译的作用!
在 .c 文件中包含了 extern "C" 时会出现编译时错误。
既然说到了条件编译,我就介绍它的一个重要应用——避免重复包含头文件。
还记得腾讯笔试就考过这个题目,给出类似下面的代码(下面是我最近在研究的一个开源 web 服务器 : Mongoose 的头文件 mongoose.h 中的一段代码):

#ifndef MONGOOSE_HEADER_INCLUDED
#define    MONGOOSE_HEADER_INCLUDED
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
/*...................  * do something here  *................... */
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* MONGOOSE_HEADER_INCLUDED */
然后叫你说明上面宏#ifndef/#endif的作用?
为了解释一个问题,我们先来看两个事实:
这个头文件 mongoose.h 可能在项目中被多个源文件包含(#include "mongoose.h"),而对于一个大型项目来说,
这些冗余可能导致错误,因为一个头文件包含类定义或inline函数,在一个源文件中mongoose.h可能会被 #include 两次
(如,a.h 头文件包含了 mongoose.h,而在 b.c文件中 #include a.h和 mongoose.h)----这就会出错(在同一个源文件中一个结构体、类等被定义了两次)。
从逻辑观点和减少编译时间上,都要求去除这些冗余。然而让程序员去分析和去掉这些冗余,不仅枯燥且不太实际,最重要的是有时候又需要这种冗余来保证各个模块的独立。

为了解决这个问题,上面代码中的
#ifndef MONGOOSE_HEADER_INCLUDED
#define    MONGOOSE_HEADER_INCLUDED
/*……………………………*/
#endif /* MONGOOSE_HEADER_INCLUDED */
就起作用了。
如果定义了 MONGOOSE_HEADER_INCLUDED,#ifndef/#endif 之间的内容就被忽略掉。
因此,编译时第一次看到 mongoose.h 头文件,它的内容会被读取且给定 MONGOOSE_HEADER_INCLUDED 一个值。
之后再次看到 mongoose.h 头文件时,MONGOOSE_HEADER_INCLUDED 就已经定义了,mongoose.h 的内容就不会再次被读取了。

2、extern “C”

首先从字面上分析 extern "C",它由两部分组成----extern 关键字、"C"。下面我就从这两个方面来解读extern "C"的含义。

2.1、extern关键字

在一个项目中必须保证函数、变量、枚举等在所有的源文件中保持一致,除非你指定定义为局部的。
首先来一个例子:
//file1.c:     int x=1;     int f(){do something here}
//file2.c:     extern int x;     int f();     void g(){x=f();}
在 file2.c 中 g() 使用的 x 和 f() 是定义在 file1.c 中的。
extern 关键字表明 file2.c 中 x,仅仅是一个变量的声明,其并不是在定义变量 x,并未为x分配内存空间。
变量 x 在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。
但是可以声明多次,且声明必须保证类型一致,如:
//file1.c:     int x=1;     int b=1;     extern c;
//file2.c:     int x;// x equals to default of int type 0     int f();     extern double b;     extern int c; 
在这段代码中存在着这样的三个错误:
x被定义了两次
b两次被声明为不同的类型
c被声明了两次,但却没有定义

回到 extern 关键字,extern 是 C/C++ 语言中表明函数和全局变量作用范围(可见性)的关键字,
该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。
例如,模块 B 欲引用模块 A 中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块 B 中调用模块 A 中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;
它会在连接阶段中从模块A编译生成的目标代码中找到此函数。
与 extern 对应的关键字是 static,被它修饰的全局变量和函数只能在本模块中使用。
因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

2.2、”C”

典型的,一个 C++ 程序包含其它语言编写的部分代码。
类似的,C++ 编写的代码片段可能被使用在其它语言编写的代码中。
不同语言编写的代码互相调用是困难的,甚至是同一种编写的代码但不同的编译器编译的代码。
例如,不同语言和同种语言的不同实现可能会在注册变量保持参数和参数在栈上的布局,这个方面不一样。
为了使它们遵守统一规则,可以使用 extern 指定一个编译和连接规约。
例如,声明 C 和 C++ 标准库函数 strcpy(),并指定它应该根据C的编译和连接规约来链接:
extern "C" char* strcpy(char*,const char*);
注意它与下面的声明的不同之处:
extern char* strcpy(char*,const char*);

下面的这个声明仅表示在连接的时候调用strcpy()。
extern "C" 指令非常有用,因为 C 和 C++ 的近亲关系。
注意:
   extern "C" 指令中的 C,表示的一种编译和连接规约,而不是一种语言。

C 表示符合 C 语言的编译和连接规约的任何语言,如 Fortran、assembler 等。
还有要说明的是,extern "C" 指令仅指定编译和连接规约,但不影响语义。

例如在函数声明中,指定了 extern "C",仍然要遵守 C++ 的类型检测、参数转换规则。
再看下面的一个例子,为了声明一个变量而不是定义一个变量,你必须在声明时指定 extern 关键字,但是当你又加上了"C",它不会改变语义,但是会改变它的编译和连接方式。
如果你有很多语言要加上 extern "C",你可以将它们放到extern "C"{ }中。

2.3、小结extern “C”

通过上面两节的分析,我们知道 extern "C" 的真实目的是实现类 C 和 C++ 的混合编程。
在 C++ 源文件中的语句前面加上 extern "C",表明它按照类 C 的编译和连接规约来编译和连接,而不是C++ 的编译的连接规约。
这样在类 C 的代码中就可以调用 C++ 的函数 or 变量等。
(注:我在这里所说的类 C,代表的是跟 C 语言的编译和连接方式一致的所有语言)

3、C和C++互相调用

我们既然知道 extern "C" 是实现的类 C 和 C++ 的混合编程。
下面我们就分别介绍如何在 C++ 中调用 C 的代码、C 中调用 C++ 的代码。
首先要明白 C 和 C++ 互相调用,你得知道它们之间的编译和连接差异,及如何利用 extern "C" 来实现相互调用。

3.1、C++的编译和连接

C++ 是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。
为了支持函数重载的这个特性,C++编译器实际上将下面这些重载函数:

void print(int i);
void print(char c);
void print(float f);
void print(char* s);
编译为:
_print_int
_print_char
_print_float
_pirnt_string
这样的函数名,来唯一标识每个函数。
注:
    不同的编译器实现可能不一样,但是都是利用这种机制。
所以当连接是调用 print(3)时,它会去查找 _print_int(3) 这样的函数。
下面说个题外话,正是因为这点,重载被认为不是多态,多态是运行时动态绑定(“一种接口多种实现”),如果硬要认为重载是多态,它顶多是编译时“多态”。
C++中的变量,编译也类似,如全局变量可能编译g_xx,类变量编译为c_xx等。连接是也是按照这种机制去查找相应的变量。

3.2、C的编译和连接

C 语言中并没有重载和类这些特性,故并不像 C++ 那样 print(int i),会被编译为_print_int,而是直接编译为_print等。
因此如果直接在 C++ 中调用 C 的函数会失败,因为连接是调用 C 中的 print(3) 时,
它会去找 _print_int(3)。因此 extern "C"的作用就体现出来了。

3.3、C++中调用C的代码

假设一个 C 的头文件 cHeader.h 中包含一个函数 print(int i),为了在C++中能够调用它,
必须要加上 extern 关键字(原因在 extern 关键字那节已经介绍)。
它的代码如下:
#ifndef C_HEADER
#define C_HEADER
    extern void print(int i);
#endif
C_HEADER 相对应的实现文件为 cHeader.c 的代码为:
#include <stdio.h>
#include "cHeader.h"
void print(int i)
{
    printf("cHeader %d\n",i);
}

现在 C++ 的代码文件 C++.cpp 中引用 C 中的 print(int i) 函数:
extern "C"{
    #include "cHeader.h"
}
int main(int argc,char** argv)
{
    print(3);
    return 0;
}

3.4、C 中调用 C++ 的代码

现在换成在 C 中调用 C++ 的代码,这与在 C++ 中调用 C 的代码有所不同。
如下在 cppHeader.h 头文件中定义了下面的代码:

#ifndef CPP_HEADER
#define CPP_HEADER
    extern "C" void print(int i);
#endif //CPP_HEADER
相应的实现文件cppHeader.cpp文件中代码如下:
#include "cppHeader.h"
#include <iostream>
using namespace std;
void print(int i)
{
    cout<<"cppHeader "<<i<<endl;
}
在 C 的代码文件 c.c 中调用print函数:
extern void print(int i);
int main(int argc,char** argv)
{
    print(3);
    return 0;
}
注意在 C 的代码文件中直接 #include "cppHeader.h" 头文件,编译出错。
而且如果不加 extern int print(int i) 编译也会出错。

4、C和C++混合调用特别之处函数指针

当我们 C 和 C++ 混合编程时,有时候会用一种语言定义函数指针,而在应用中将函数指针指向另一种语言定义的函数。
如果 C 和 C++ 共享同一种编译和连接、函数调用机制,这样做是可以的。
然而,这样的通用机制,通常不会假定它存在,因此我们必须小心地确保函数以期望的方式调用。
而且当指定一个函数指针的编译和连接方式时,函数的所有类型,包括函数名、函数引入的变量也按照指定的方式编译和连接。如下例:

typedef int (*FT) (const void* ,const void*);//style of C++
extern "C" {
  typedef int (*CFT) (const void*,const void*);  //style of C
  void qsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
}
void isort(void* p,size_t n,size_t sz,FT cmp);//style of C++
void xsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
//style of C
extern "C" void ysort(void* p,size_t n,size_t sz,FT cmp);
int compare(const void*,const void*);//style of C++
extern "C" ccomp(const void*,const void*);//style of C
void f(char* v,int sz)
{
    //error,as qsort is style of C
    //but compare is style of C++
    qsort(v,sz,1,&compare);
    qsort(v,sz,1,&ccomp);//ok
    isort(v,sz,1,&compare);//ok
    //error,as isort is style of C++
    //but ccomp is style of C
    isort(v,sz,1,&ccopm);
}
注意:
    typedef int (*FT) (const void* ,const void*),表示定义了一个函数指针的别名FT,
这种函数指针指向的函数有这样的特征:
    返回值为int型、有两个参数,参数类型可以为任意类型的指针(因为为void*)。

最典型的函数指针的别名的例子是,信号处理函数signal,它的定义如下:
typedef void (*HANDLER)(int);
HANDLER signal(int ,HANDLER);
上面的代码定义了信函处理函数signal,它的返回值类型为HANDLER,有两个参数分别为int、HANDLER。
这样避免了要这样定义signal函数:
void (*signal (int ,void(*)(int) ))(int)
比较之后可以明显的体会到typedef的好处。

[工程中 C 文件调用 C++ 函数](https://www.cnblogs.com/cfzhang/p/25bbffa718da778a213c94ebd6971528.html)

1、描述

C调用C++链接库:
 1.编写C++代码,编写函数的时候,需要加入对C的接口,也就是extern “c"
 2.由于C不能直接用"class.function”的形式调用函数,所以C++中需要为C写一个接口函数。例如本来要调用student类的talk函数,就另外写一个cfun(),专门建一个student类,并调用talk函数。而cfun()要有extern声明
 3.我在练习中就使用在C++头文件中加extern ”c”的方法。而C文件要只需要加入对cpp.h的引用
 4.详细见如下代码:
  student是一个类,里边有talk函数,就输出一句话而已
  cpp.cpp与cpp.h是两个C++代码,包含对C的接口
  最后用C代码:helloC.c来测试结果

代码

1、student.cpp

#include <iostream>
using namespace std;
#include "student.h"
void student::talk()
{
    cout<<"I am Kenko"<<endl;
}

### 2、student.h
```c
#ifndef _STUDENT_
#define _STUDENT_
class student {
    public:
        void talk();
};
#endif

3、cpp.h

#ifndef _CPP_
#define _CPP_
    #include "student.h"

#ifdef __cplusplus
    extern "C" {
#endif
    void helloCplusplus();
#ifdef __cplusplus
}
#endif
#endif

4、cpp.cpp

#include <iostream>
using namespace std;
#include "cpp.h"
student stu;
void helloCplusplus() {
    cout << "hello C++" << endl;
    stu.talk();
}

void helloCplusplus2() {
    cout << "hello C++" << endl;
}

5、helloC.c

#include <stdio.h>
#include "cpp.h"
int main() {
    helloCplusplus();
    return 0;
}

3、怎样编译运行

linux 编译:
  g++ -fPIC -shared -g -o libcccall.so cpp.cpp student.cpp
  g++ -g helloC.c ./libccall.so -o main

cmder

一、用自己习惯的方式启动

1、研究它保存配置文件存放位置

对 cmder 的风格,不同风格操作上有所区别。
界面看上去效果也不同,最好能根据需要自由启动。

启动方式,通过 cmder 的settings-->> startup 最下方,
在cmder:cmder时,它给出的提为:
cmd /c cmder\vender\init.bat

当然 bash:bash / minitty 等不同风格,启动命令也各不相同

保存风格后,发现cmder\vendor\conemu-maximus5\ConEmu.xml 修改时间发生改变,OK,那就是它了。

2、简单修改,方便启动

A、修改配置文件

然后将相应的配置分别重名为:
ConEmu_minitty.xml
ConEmu_minitty_admin.xml
ConEmu_cmder_cmder.xml
ConEmu_cmder_cmder_admin.xml
ConEmu_bash_bash.xml
ConEmu_bash_bash_admin.xml

B、建立几个启动文件

这里仅仅列出一个为例:
bash_cmder_admin.bat 内容如下:
copy cmderPath\vender\conemu-maximus5\ConEmu_cmder_cmder_admin.xml cmderPath\vender\conemu-maximus5\ConEmu.xml

cmd /c cmderPath\cmder.exe

二、别人提供的修改方法

https://www.jianshu.com/p/979db1a96f6d

三、同类工具

mobaxterm
guake
secureCRT
putty/mtputty

c

python输出内容,然后用C将其读出

python脚本: a.py

# coding =utf-8
import os,sys,time,glob,traceback,gc,re,string,stringgrep
import time,socket,random,unittest

reload(sys)
sys.setdefaultencoding("utf-8")
os.system("echo 3900/48412 line\(44.7\) Branch\(43.9%\)>out.txt"

C 代码

#include <stdio.h>
#include <stdlib.h>
int main()
{
    char CoverageOut[128] = {0};
    system("python2.7.exe a.py");
    FILE *pFile = fopen("out.txt", "r");
    if (NULL == pFile)
    {
        printf("failed to open out.txt\n");
        return 0;
    }
    fread(CoverageOut, 1024, 1, pFile);
    printf("Coverage Out is %s\n", CoverageOut);
    if (pFile)
    {
        fclose(pFile);
    }
    return 0;
}

alarm 函数的应用

linux代码段

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_alarm(int sigNo)
{
    printf("in sig_alarm, paraIn = %d\n", sigNo);
    system("date");   //注意退出时,打印的时间间隔
    return;
}

int main(void)
{
    signal(SIGALARM, sig_alarm); //注册函数,时间到则调用此函数
    system("data"); //程序启动时,先打印一个基准时间
    printf("alarm(4) = %d\n", alarm(4)); //如果没有下面的sleep(1)和alarm(8)两行代码,则过4秒打印并跳过pause()继续执行
    sleep(1);  //这个时间被统计进alarm()延时中,需要加上sleep值
    alarm(8); //如果同时开启alarm(4) 和alarm(8),则以长的时间为准
    pause();//在C语言中,如果没有alarm()语句,则会一直卡在这里
    printf("print before end ...\n"); // 在9秒之后,继续从pause的地方执行
}
在这个场景中,如果所注册的 sig_alarm函数中,调用了exit(0)函数,则程序提前退出。同样如果同时调用多个alarm()函数,则以时间最长的为准。

最能体现代码执行时序的例子

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler() {  printf("hello!\n");
void main()
{
    int i = 0;
    signal(SIGALRM,handler);
    alarm(3);
    for (i = 1; i < 5; i ++)
    { printf("sleep %d ...\n", i); sleep(1);}
}

编译动态链接库,示例项目
a.c:  int a= 1;
b.c:  int b = 1;
c.c : int c = 1;
main.c:
#include <stdio.h>
extern int a;
extern int b;
extern int c;
int main()
{
    printf("a=%d b=%d c=%d\n", a, b, c);
    return 0;
}

第一步:编译动态库
     gcc -shared -fpic a.c b.c c.c
     gcc -shared -fpic -o libprog.so  a.o b.o c.o
   或者将两行合并成一行写
     gcc -shared -fpic a.c b.c c.c -o libprog.so
 第二步,编译可执行文件
    gcc main.c -o main -L./   ./libprog.so  ==>>运行成功
    gcc main.c -o main -L./   libprog.so  ==>>运行失败???
另外,链接关键字 -fpic 换成 -fPIC 同样运行成功,具体原理看过,没完全理解,现在又忘了

其它写法,能编译成功,但是运行失败:
    gcc main.c -o main -L ./ -lprog ==>>多标准的写法,运行不了</stdio.h>

C语言解析CSV的正确打开方式

char *pBuff = NULL;
char fileBuff[4*1024*1024] = {0};
FILE *fp = fopen("./a.csv", "r");
//以字符为单位:1,最多读取4*1024*1024个长度
ret = fread(fileBuff, 1, 4*1024*1024, fp);
pBuff = strtok(fileBuff, "\t");  //将csv copy出来,它是以tab间隔的,所以要以"\t"来分割
pBuff = strtok(fileBuff, ","); //csv原文件是以逗号间隔的,所以要以","来分割
pBuff = strtok(fileBuff, "\r\n"); //csv原文件一般包含多列,最后一列肯定要以换行符来结束
if (pBuff)
{
    strcpy(yourBuff, pBuff);
    printf("yourBuff is : %s\n", yourBuff);
}
else
{  //一般都包含多行,所以需要在一个循环中处理
    break;
}
而且第一个元素需要从总buffer中读取,后续所有的strtok操作,第一个参数都是NULL.

gcc 中的 weak 关键字

1、当没有遇到“强”函数时的效果

aa.c:
#include <stdio.h>
__attribute__((weak)) void fun1(int a)
{
    printf("a=%d\n", a);
}
int main()
{
    fun1(3);
    return 0;
}

此时,执行: gcc aa.c -o a.out ; ./a.out
得到的输出是a=3

2、当遇到“强”函数时,立即失效,被别人代替

bb.c:
#include <stdio.h>
void fun1(int a)
{
    printf("new a=%d\n",a*3);
}

此时执行: gcc aa.c bb.c -o a.out; ./a.out
得到的输出是 new a=9

#栈,调试

1、backtrace

一些内存检测工具如Valgrind,调试工具如GDB,可以查看程序运行时函数调用的堆栈信息,有时候在分析程序时要获得堆栈信息,借助于backtrace是很有帮助的,其原型如下:
#include <execinfo.h>
int backtrace(void **buffer, int size); 
char **backtrace_symbols(void *const *buffer, int size);
void backtrace_symbols_fd(void *const *buffer, int size, int fd);
头文件“execinfo.h”提供了三个相关的函数,简单的说,backtrace函数用于获取堆栈的地址信息, backtrace_symbols函数把堆栈地址翻译成我们易识别的字符串, backtrace_symbols_fd函数则把字符串堆栈信息输出到文件中

backtrace:该函数用于获取当前线程的函数调用堆栈,获取的信息将存放在buffer中,buffer是一个二级指针,可以当作指针数组来用,数组中的元素类型是void*,即从堆栈中获取的返回地址,每一个堆栈框架stack frame有一个返回地址,参数 size 用来指定buffer中可以保存void* 元素的最大值,函数返回值是buffer中实际获取的void*指针个数,最大不超过参数size的大小。 
backtrace_symbols:该函数把从backtrace函数获取的信息buffer转化为一个字符串数组char**,每个字符串包含了相对于buffer中对应元素的可打印信息,包括函数名、函数的偏移地址和实际的返回地址,size指定了该数组中的元素个数,可以是backtrace函数的返回值,也可以小于这个值。需要注意的是,backtrace_symbols的返回值调用了malloc以分配存储空间,为了防止内存泄露,我们要手动调用free来释放这块内存。 
backtrace_symbols_fd:该函数与backtrace_symbols 函数功能类似,不同的是,这个函数直接把结果输出到文件描述符为fd的文件中,且没有调用malloc。 
在使用以上三个函数时,还需要注意一下几点: 
(1)如果使用的是GCC编译链接的话,建议加上“-rdynamic”参数,这个参数的意思是告诉ELF连接器添加“-export-dynamic”标记,这样所有的符号信息symbols就会添加到动态符号表中,以便查看完整的堆栈信息。 
(2)static函数不会导出符号信息symbols,在backtrace中无效。 
(3)某些编译器的优化选项对获取正确的函数调用堆栈有干扰,内联函数没有堆栈框架,删除框架指针也会导致无法正确解析堆栈内容。
下面是一个简单的例子

//backtrace_ex.cpp
#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h> 
void my_backtrace()
{
    void *buffer[100] = { NULL };
    char **trace = NULL;
    int size = backtrace(buffer, 100);
    trace = backtrace_symbols(buffer, size);
    if (NULL == trace) {
        return;
    }
    for (int i = 0; i < size; ++i) {
        printf("%s\n", trace[i]);
    }
    free(trace);
    printf("----------done----------\n");
}
void func2()
{     my_backtrace(); 
} 
void func()
{     func2();
} 
int main()
{     func();     return 0;
}
编译执行上面的文件
g++ backtrace_ex.cpp
./a.out
./a.out() [0x400811]
./a.out() [0x400baf]
./a.out() [0x400bba]
./a.out() [0x400bc5]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f2473cf5ec5]
./a.out() [0x400709]
----------done----------

咦!堆栈信息虽然打出来了,但是函数调用栈并不是很明确,原因是少了“-rdynamic”参数,重新编译执行如下:
g++ -rdynamic backtrace_ex.cpp
./a.out
./a.out(_Z12my_backtracev+0x44) [0x400b11]
./a.out(_Z5func2v+0x9) [0x400eaf]
./a.out(_Z4funcv+0x9) [0x400eba]
./a.out(main+0x9) [0x400ec5]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f006bdfbec5]
./a.out() [0x400a09]
----------done----------
加了“-rdynamic”参数后就很好了,我们可以看到函数名称,由于不同的平台、编译器有不同的编译规则,所以用backtrace解析出来的函数名形式是不同的,以“./a.out(_Z4funcv+0x9) [0x400eba]”为例说明,重点在于圆括号中的内容,“_Z”是个函数名开始标识符,后面的“4”表示函数名长度,接着便是真正的函数名“func”,后面的“v”表示函数参数类型为void,随后的“+0x9”是偏移地址。虽然有一定的编译规则,但可读性还不是很好,我们可以用下面介绍的方法demangle来解析这些符号

2、demangle

demangle即符号重组,函数原型如下:
#include <cxxabi.h>
char* __cxa_demangle(const char* __mangled_name,
                     char* __output_buffer,
                     size_t* __length,
                     int* __status);

cxxabi.h是一个C++函数运行时库,要用g++编译链接,gcc会有问题。__mangled_name即原符号信息,是个字符串,以空字符结尾,__output_buffer用来保存符号重组后的信息,长度为__length,__status表示demangle结果,为0时表示成功,返回值指向符号重组后的字符串首地址,字符串以空字符结尾。 
我们使用demangle来改进上面的例子:(把my_backtrace替换为my_backtrace2)
void my_backtrace2()
{
    void *buffer[100] = { NULL };
    char **trace = NULL;
    int size = backtrace(buffer, 100);
    trace = backtrace_symbols(buffer, size);
    if (NULL == trace) {
        return;
    }
    size_t name_size = 100;
    char *name = (char*)malloc(name_size);
    for (int i = 0; i < size; ++i) {
        char *begin_name = 0;
        char *begin_offset = 0;
        char *end_offset = 0;
        for (char *p = trace[i]; *p; ++p) { // 利用了符号信息的格式
            if (*p == '(') { // 左括号
                begin_name = p;
            }
            else if (*p == '+' && begin_name) { // 地址偏移符号
                begin_offset = p;
            }
            else if (*p == ')' && begin_offset) { // 右括号
                end_offset = p;
                break;
            }
        }
        if (begin_name && begin_offset && end_offset ) {
            *begin_name++ = '\0';
            *begin_offset++ = '\0';
            *end_offset = '\0';
            int status = -4; // 0 -1 -2 -3
            char *ret = abi::__cxa_demangle(begin_name, name, &name_size, &status);
            if (0 == status) {
                name = ret;
                printf("%s:%s+%s\n", trace[i], name, begin_offset);
            }
            else {
                printf("%s:%s()+%s\n", trace[i], begin_name, begin_offset);
            }
        }
        else {
            printf("%s\n", trace[i]);
        }
    }
    free(name);
    free(trace);
    printf("----------done----------\n");
}
结果如下:
g++ -rdynamic backtrace_ex.cpp
./a.out
./a.out:my_backtrace2()+0x44
./a.out:func2()+0x9
./a.out:func()+0x9
./a.out:main()+0x9
/lib/x86_64-linux-gnu/libc.so.6:__libc_start_main()+0xf5
./a.out() [0x400a09]
----------done----------
可以看出来,demangle后函数名已清晰地显示出来了,没有那些奇奇怪怪的符号了。

C 语言正则表达式

1、先上一段代码

#include <stdio.h>
#include <sys/types.h>
#include <regex.h>
#include <memory.h>
#include <stdlib.h>

int main(){
    char *bematch = "hhhericchd@gmail.com";
    char *pattern = "h{3,10}(.*)@.{5}.(.*)";
    char errbuf[1024];
    char match[100];
    regex_t reg;
    int err,nm = 10;
    regmatch_t pmatch[nm];

    if(regcomp(®,pattern,REG_EXTENDED) < 0){
        regerror(err,®,errbuf,sizeof(errbuf));
        printf("err:%s\n",errbuf);
    }

    err = regexec(®,bematch,nm,pmatch,0);

    if (err == REG_NOMATCH){
        printf("no match\n");
            exit(-1);
    }
    else if(err) {
        regerror(err,®,errbuf,sizeof(errbuf));
        printf("err:%s\n",errbuf);
        exit(-1);
    }

    for(int i=0;i<10 && pmatch[i].rm_so!=-1;i++) {
        int len = pmatch[i].rm_eo-pmatch[i].rm_so;
        if(len) {
            memset(match,'\0',sizeof(match));
            memcpy(match,bematch+pmatch[i].rm_so,len);
            printf("%s\n",match);
        }
    }
    return 0;
}

2、编译运行效果

[Administrator.WINDOWS-LGJ801D] ➤ gcc regExp.c -o ppc
[Administrator.WINDOWS-LGJ801D] ➤ ./ppc
hhhericchd@gmail.com
ericchd
com

3、代码理解及验证

A、重复字符匹配

char *pattern = "h{3,10}(.*)@.{5}.(.*)";
显然是核心,这是一个正则表达式语法.
h{3,10} ==>>表示至少有三个'h'重复,最多10个,以此为开始标记,第11个'h'将被看作普通字符

将 char *bematch = "hhhericchd@gmail.com"; 改为两个'h'开头:"hhericchd@gmail.com"
重新编译运行:
[Administrator.WINDOWS-LGJ801D] ➤ gcc regExp.c -o ppc && ./ppc.exe
no match

再改为5个'h'开头"hhhhhericchd@gmail.com":
[Administrator.WINDOWS-LGJ801D] ➤ gcc regExp.c -o ppc && ./ppc.exe
hhhhhericchd@gmail.com
ericchd
com

再改为10个以上连续'h'开头:"hhhhhhhhhhhhhhhhericchd@gmail.com"
[Administrator.WINDOWS-LGJ801D] ➤ gcc regExp.c -o ppc && ./ppc
hhhhhhhhhhhhhhhhericchd@gmail.com
hhhhhhericchd
com

B、指定个数字符,不作为匹配输出

显然,输出中 ".gmail." 被 ".{5}." 取代

C、输出匹配的内容规则
[0] 表示全匹配字符串
[1] 表示第一个 "(.*)" 匹配成功的内容
[2] 表示第二个 "(.*)" 匹配成功的内容
"尤其注意:
    必须用括号括起来的,才能作为子匹配项。
"

但是有一个神奇的事情,空格的字符串长度竟然为0

D、加戏,增加多场景匹配

一个神奇的事情,空格的字符串长度竟然为0
    char *bematch  = "hhhhhhhhhhhhhhhhericchd@gmail.com";
    char *bematch2 = "hhhhhhhhhhhericchd @gmail.com";
    char *bematch3 = "hhhhhhericchd  @gmail.com";
    char *pattern = "h{3,10}(.*)([ ]*)@.{5}.(.*)";
想要匹配'@'、' @'、'  @'即任意空格和'@'连接的情况,第三个将输出也改一下:
    for(int i=0;i<10 && pmatch[i].rm_so!=-1;i++) {
        int len = pmatch[i].rm_eo-pmatch[i].rm_so;
//        if(len) {
            memset(match,'\0',sizeof(match));
            memcpy(match,bematch+pmatch[i].rm_so,len);
            printf( "match %d: %s\n",i , match);
//        }
    }
则编译运行效果如下:

[Administrator.WINDOWS-LGJ801D] ➤ gcc regExp.c -o ppc && ./ppc
match 0: hhhhhhhhhhhhhhhhericchd@gmail.com
match 1: hhhhhhericchd
match 3: com
match 0: hhhhhhhhhhhhhhhhericchd@gmail
match 1: hhhhhheri
match 3: ail
match 0: hhhhhhhhhhhhhhhhericchd@g
match 1: hhhhhhhhh
match 2:
match 3: d@g

4、在经历了一段时间项目经验后,认识更全面,经验更老道一点

1、排除空格,高风险地方

比如有如下脚本:
  [root]$ ssh 127.0.0.1 -l userName -p 2222
要提取出 `ip / userName / 2222`这三个关键字,可以这样写
char *pattern = "ssh[ ]{1,}(.*)[ ]{1,}-l[ ]{1,}(.*)[ ]{1,}-p[ ]{1,}(.*)[ ]{0,}";
讲解:[ ]{1,} ==>> 表示它后面至少有一个空格,可能有多个
     (.*)    ==>> 是最终
     [ ]{0,} ==>> 最后的一个,是为了清除空格,不被(.*)匹配上
     但是这种写法有缺陷,某些时候,不知道为什么最后的空格没有清除掉,被匹配进最后一个项里面

改进写法:
   char *pattern = "ssh[ ]{1,}([^ ]+)[ ]{1,}-l[ ]{1,}(.*)[ ]{1,}-p[ ]{1,}([^ ]+)";
讲解:用 `([^ ]+)` 代替`(.*)`,可完美避开空格,尤其是最后一项,可以不用考虑末尾的空格影响

带上小括号的,就是将被匹配上的内容,唯一例外就是全字符,就是如果匹配
成功,全字符将被告放入匹配数组 match[0],其它依次放入
match[1]/match[2]

2、开关匹配情况

如:
  [root]$ status on
  [root]$ status off
两种情况都应该被匹配上,正确写法是
char *pattern = "status[ ]{1,}(on|off)[ ]{0,}";
这样 match[0]为全字符,match[1]为 on 或者 off

5、其它人的总结,如下内容不全正确,可以批判接受

字 符 意 义 示 例
* 任意长度的字符串。 a* 表示: 空字符串、aaaa、a…
? 长度为0或者1的字符串。 a? 表示: 空字符串和a。
+ 长度为一个或者多个的字符串。 a+表示:a、aa、aaaaaa…
. 任意字符。 a. 表示:a后跟任意字符。
{} 代表上一规则重复数目
{1,1,s}包含一组匹配花括号,里面有两个数字和一个字符,表示在指定次数范围内找到字符。 
a{3}表示:三个a、
a{1,3}表示:一个到三个a、
a{3,} 表示:大于等于三个a、
{3,7,a}表示在3到7次重复范围内匹配字符a。
[] 集合,代表方括号中任意一个字符。 
[ab] 表示:a或者b都可以
[a-z] 表示:从a到z的字符。
() 组,代表一组字符。 (ab){2}表示:abab。
a/b 同时满足。 a/b表示:字符串a后跟字符串b才能满足要求。
a|b 并列,代表符合a或者符合b都可以,this|that表示: 字符串this或者字符串that都满足要求。
^ 如果放在开头表示代表该规则必须在字符串的开头,其他位置代表字符本身。
  如果放在[]中的开头表示对该集合取反,其他位置代表字符本身。
^a表示:a必须在字符串的开头
[^a]表示:除了a以外的其他字符。
$ 如果放在最后表示该规则必须放在最后,其他位置代表字符本身。 a$表示:a必须在字符串最后。
/:s 正则表达式用 /:s 表示空格。 a/:sb 匹配 a b。
  ==>>根据本人实践,/:s代表空格失败,直接采用[ ]表示空格是成功的
/:a 正则表达式用 /:a 表示字符与数字。 a/:a 匹配 ab、a6 等。
/:c 正则表达式用 /:c 仅表示字符。 a/:c 匹配 ac等,不匹配a1等。
/:p 正则表达式用 /:p 表示可打印字符。
/:D 正则表达式用 /:d 仅表示数字。 a/:c 匹配 a1等,不匹配ac等。
/:x00 正则表达式用 /:x00 表示ASCII字符。
/:r 正则表达式用 /:r 表示回车。
/:N 正则表达式用 /:d 表示换行。

C/C++ 中的单子节对齐问题

有两种方式:

1、pragma

2、attribute((packed))

一般为了阅读方便,代码美观,会这样处理:
#define MPACK __attribute__((packed))
typedef struct
{
    char c;
    int i;
}MPACK myStruct;
  这样就实现了单子节对齐

编译静态库和动态库

编译静态库

#正常情况下,就直接将 object 文件链接一起编译生成可执行程序就行了
gcc -c funTest.c
gcc -o testMain testMain.c funTest.o //库文件形式下
gcc -c funTest.c //编译生成funa.o
ar -rsv libfunTest.a funTest.o //ar指令, 编译生成静态库文件
gcc -o testMain testMain.c -L./ -lfunTest //链接静态库文件,生成可执行文件

动态链接库的编译与使用

// 动态库编译生成
gcc -o libtest.so -fPIC -shared funa.c
gcc -o testMain testMain.c ./libtest.so //直接指定动态库位置

C 语言彩色打印

彩色定义

#define NONE "\033[m"
#define RED "\033[0;32;31m"
#define LIGHT_RED "\033[1;31m"
#define GREEN "\033[0;32;32m"
#define LIGHT_GREEN "\033[1;32m"
#define BLUE "\033[0;32;34m"
#define LIGHT_BLUE "\033[1;34m"
#define DARY_GRAY "\033[1;30m"
#define CYAN "\033[0;36m"
#define LIGHT_CYAN "\033[1;36m"
#define PURPLE "\033[0;35m"
#define LIGHT_PURPLE "\033[1;35m"
#define BROWN "\033[0;33m"
#define YELLOW "\033[1;33m"
#define LIGHT_GRAY "\033[0;37m"
#define WHITE "\033[1;37m"

简单应用示例

#include <stdio.h>
#define RED    "\033[0;32;31m"
#define NONE   "\033[m"
#define YELLOW "\033[1;33m"
int main()
{
    printf(RED" the red!\n"NONE);
    printf(YELLOW" the yello!\n"NONE);
    return 0;
}

对 printf 参数进行封装
printf(YELLOW" he is %d years old!\n"NONE, 99);

用法用法:
char buff[512]={0};
sprintf(buff,"this is a print content,num=%d", 99);
colorPrint(YELLOW, buff);

最后封装
typedef enum {
    eRED,
    eLIGHT_RED,
    ...
    eLIGHT_GRAY,
    eWHITE,
}eColor;
int colorPrint(eColor color, const char *format, ...)
{
    char content[512 + 1] = {0};
    va_list argptr;
    memset((char *)&argptr, 0, sizeof(va_list));
    va_start(argptr, format);
    vsnprintf(content, 512, format, argptr);
    va_end(argptr);
    if (eRED == color)
    {
        printf(RED "%s" END, content);
    }
    else if(eLIGHT_RED == color)
    {
        printf(LIGHT_RED "%s" END, content);
    }
    ...
    else if (eWHITE == color)
    {
        printf(WHITE "%s" END, content);
    }
    else
    {
        printf("%s", content);
    }
    return 0;
}
用法:
colorPrint(eRED, "myname:%s,age:%d",name, 99);

怎样输出百分号:‘%’
int main()
{
    printf("%%%s%%\n","xx");
    return 0;
}

C 语言中调用shell语句(linux C)

1、亲自编写的代码

#define ERROR_CODE (1<<8)
void test()
{
    char cmdBuff[512] = {0};
    sprintf(cmdBuff, "grep -rni %d ${PWD%%KEY*}/KEY/file.c || exit 1");
    if (ERROR_CODE ==system(cmdBuf))
    {
        return 1;
    }
    return 0;
}

int ret =  system("exit 1");
则 ret=(1<<8),0x100,256

int ret =  system("exit 2");
则 ret=(2<<8),0x200,512

int ret =  system("exit 3");
则 ret=(1<<8),0x300,768

但是126、127有特殊用途,一般不返回这两个值

2、转链接

bat

一、dos下计算两个%time%时间差

1、场景一(此脚本基本调试通过)

bat语句中计算两个时间差,可以先将时间转换成秒数,然后,将两个时间数进行相减即可,参考代码(此代码中问题多多):
@echo off
set ns=0
rem 显示开始时间
set time1=%time%
echo 当前时间是%time1%
call :time2sec %time1%
set t1=%ns%
pause

rem 显示结束时间
set time2=%time%
echo 当前时间是%time2%
call :time2sec %time2%
set t2=%ns%
rem 计算时间差,计算中,最好把表达式用引号括起来
set /a "tdiff=(%t2%-%t1%)"
echo diff %time1% from %time2% is %tdiff% seconds.
pause
goto :eof

:time2sec
rem 将时间转换成秒数,保存到ns中
rem %t1 是函数的第一个入参
set tt=%1
rem 从第一(0)个字符开始,取两个长度
rem 这个用法其实有问题,当小时不是两位数的时候,取值发生错位
set hh=%tt:~0,2%
set mm=%tt:~3,2%
set ss=%tt:~6,2%
rem 表达式需要用引号,否则问题报错,
rem 说括号不配对,花了很长时间才解决
set /a "ns=(%hh%*60+%mm%)*60+%ss%"
goto :eof

2、场景二(脚本尚未调试)

@echo off
title 同一月份下的耗时计算
::获取起始月份、起始日期、起始小时和起始分钟
set startmonth=%date:~5,2%
set startday=%date:~8,2%
set starthour=%time:~0,2%
set startmin=%time:~3,2%

echo.&echo 修改系统日期和时间为未来同一个月份下的某日某月某时某分 以便测试脚本
echo 完成修改后 按任意键继续
pause >nul
::获取终止月份、终止日期、终止小时和终止分钟
set endmonth=%date:~5,2%
set endday=%date:~8,2%
set endhour=%time:~0,2%
set endmin=%time:~3,2%  
::初始化间隔日期变量、间隔小时变量和间隔分钟变量
set intday=0
set inthour=0
set intmin=0
::初始化耗时变量
set inttime=0 
::如果结束月份和起始月份不在同一月 则调用calc4标签
if %endmonth% NEQ %startmonth% (call:calc4 & goto :finalresult)
::如果结束日期等于起始日期 则调用calc1标签
if %endday% EQU %startday% (call:calc1 & goto :finalresult)
::如果结束日期大于起始日期 则调用calc2标签
if %endday% GTR %startday% (call:calc2 & goto :finalresult)
::如果结束日期小于起始日期 则调用calc3标签
if %endday% LSS %startday% (call:calc3 & goto :finalresult)
::备注:没有规避同一天内结束小时小于起始小时、以及同一小时内结束分钟小于起始分钟的情况,因为在脚本运行过程中,逻辑上一般不会出现这两种情况。
::显示耗时
:finalresult
echo 耗时:%inttime%
exit /b
::同一天内的耗时计算 需考虑到结束分钟小于起始分钟的时候 从终止小时借位的情况
:calc1
if /i %endmin% LSS %startmin% (set /a intmin=endmin+60-startmin & set /a endhour-=1) else (set /a intmin=endmin-startmin)
set /a inthour=endhour-starthour
set /a intday=endday-startday
set inttime=%intday%天%inthour%小时%intmin%分钟
goto :eof
::同一月份但不同天内的耗时计算 需考虑到结束分钟小于起始分钟的时候 从终止小时借位的情况 需考虑到结束小时小于起始小时的时候 从终止日期借位的情况
:calc2
if /i %endmin% LSS %startmin% (set /a intmin=endmin+60-startmin & set /a endhour-=1) else (set /a intmin=endmin-startmin)
if /i %endhour% LSS %starthour% (set /a inthour=endhour+24-starthour & set /a endday-=1) else (set /a inthour=endhour-starthour)
set /a intday=endday-startday
set inttime=%intday%天%inthour%小时%intmin%分钟
goto :eof
::同一月份下 结束日期逻辑上不能小于起始日期 抛出错误
:calc3
set inttime=错误!结束日期小于起始日期!
goto :eof
::跨月份的情况忽略不计
:calc4
set inttime=跨月份忽略耗时计算
goto :eof

二、延时指定时间,任意时长都可以设置

@echo of
echo this window will disappear 3 seconds later
‘自动生成一个VB脚本,参数0表示第一个参数
'应该是特殊字符的转意字符
‘1000毫秒是一秒
echo if wscript.argument(0)^>0 then \
    wscript.Sleep(wscript.argument(0)*1000):end if>"%Temp%\delay01.vbs"
'执行脚本
cscript "%Temp\delay01.vbs" 3
‘执行结束后,删除脚本
del "%Temp\delay01.vbs"

三、找到并多杀死进程

1、找到进程

tasklist | findstr /i frps.exe

2、杀死进程

taskkill /f /im frps.exe

四、win7 快速进入开机设置目录

win+R 然后输入: shell:startup

五、想在DOS命令行下,查看内存情况

c:> systeminfo
然后,会看到各种详细信息

c:> systeminfo | findstr 内存

centos

一、RPM 资源

1、网络资源

http://rpmfind.net/linux/rpm2html/

2、下载到本地后,安装方法

rpm -ivh ab.rpm

3、一位老兄详细的文档,还讲解了怎样将windows目录挂载到linux

https://blog.csdn.net/annicybc/article/details/1133899
https://blog.csdn.net/weixin_40973138/article/details/103724390

值得程序员关注的开源项目

1.WebBench

Webbench 是一个在 linux  下使用的非常简单的网站压测工具。它使用 fork ()模拟多个客户端同时访问我们设定的 URL,测试网站在压力下工作的性能,最多可以模拟 3  万个并发连接去测试网站的负载能力。Webbench 使用C语言编写, 代码实在太简洁,源码加起来不到 600 行。

2. Tinyhttpd

tinyhttpd 是一个超轻量型 Http Server,使用C语言开发,全部代码只有 502 行(包括注释),附带一个简单的 Client,可以通过阅读这段代码理解一个 Http Server 的本质。

3. cJSON

cJSON 是C语言中的一个 JSON 编解码器,非常轻量级,C文件只有 500 多行,速度也非常理想。cJSON 也存在几个弱点,虽然功能不是非常强大,但 cJSON 的小身板和速度是最值得赞赏的。其代码被非常好地维护着,结构也简单易懂,可以作为一个非常好的C语言项目进行学习。

4. CMockery

cmockery 是 google 发布的用于C单元测试的一个轻量级的框架。它很小巧,对其他开源包没有依赖,对被测试代码侵入性小。cmockery 的源代码行数不到 3K,你阅读一下 will_return 和 mock 的源代码就一目了然了。主要特点:免费且开源,google 提供技术支持;轻量级的框架,使测试更加快速简单;避免使用复杂的编译器特性,对老版本的编译器来讲,兼容性好;并不强制要求待测代码必须依赖 C99 标准,这一特性对许多嵌入式系统的开发很有用

5. Libev

libev 是一个开源的事件驱动库,基于 epoll,kqueue 等 OS 提供的基础设施。其以高效出名,它可以将 IO 事件,定时器,和信号统一起来,统一放在事件处理这一套框架下处理。基于 Reactor 模式,效率较高,并且代码精简(4.15 版本 8000 多行),是学习事件驱动编程的很好的资源。

6. Memcached

Memcached 是一个高性能的分布式内存对象缓存系统,用于动态 Web 应用以减轻数据库负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提供动态数据库驱动网站的速度。Memcached 基于一个存储键/值对的 hashmap。Memcached-1.4.7 的代码量还是可以接受的,只有 10K 行左右。

7. Lua

Lua 很棒,Lua 是巴西人发明的,这些都令我不爽,但是还不至于脸红,最多眼红。让我脸红的是 Lua 的源代码,百分之一百的 ANSI C,一点都不掺杂。在任何支持 ANSI C 编译器的平台上都可以轻松编译通过。我试过,真是一点废话都没有。Lua 的代码数量足够小,5.1.4 仅仅 1.5W 行,去掉空白行和注释估计能到 1W 行。

8. SQLite

SQLite 是一个开源的嵌入式关系数据库,实现自包容、零配置、支持事务的 SQL 数据库引擎。 其特点是高度便携、使用方便、结构紧凑、高效、可靠。足够小,大致 3 万行C代码,250K。

9. UNIXv6

UNIX V6 的内核源代码包括设备驱动程序在内约有 1 万行,这个数量的源代码,初学者是能够充分理解的。有一种说法是一个人所能理解的代码量上限为 1  万行,UNIX V6 的内核源代码从数量上看正好在这个范围之内。看到这里,大家是不是也有“如果只有 1 万行的话没准儿我也能学会”的想法呢?另一方面,最近的操作系统,例如 Linux 最新版的内核源代码据说超过了 1000 万行。就算不是初学者,想完全理解全部代码基本上也是不可能的。

10. NETBSD

NetBSD  是一个免费的,具有高度移植性的 UNIX-like 操作系统,是现行可移植平台最多的操作系统,可以在许多平台上执行,从 64bit alpha  服务器到手持设备和嵌入式设备。NetBSD 计划的口号是:”Of course it runs  NetBSD”。它设计简洁,代码规范,拥有众多先进特性,使得它在业界和学术界广受好评。由于简洁的设计和先进的特征,使得它在生产和研究方面,都有卓越的表现,而且它也有受使用者支持的完整的源代码。许多程序都可以很容易地通过

可以把玩的项目

1、seq2seq-couplet

人工智能对对联,自由自逍遥游
这个项目基于深度学习技术来实现自动对对联,比如上面这个对联,就是AI对出的对联。
直接输入上联点击后就可以得到下联。如果对内在技术感兴趣,也可以在项目内查看。

其实这个系统更擅长古风的对联比如:
“殷勤怕负三春意 ,潇洒难书一字愁。
“如此清秋何吝酒,这般明月不须钱。”
你能看出这是AI对出来的对联吗?还不收藏起来,以后你就是对联之王~
该项目已有 2.5k 星。

2、webchat-shelter

对PC版微信进行伪装,变成有道笔记,大胆在公司摸鱼

3、nocode

无需任何更改,即可在任意平台、任意版本的IDE上运行。
是史上最伟大的工程

4、logoly

可以为自己定制出P站 log风格的图片,可以调节字体颜色,大小。
适合自己私下定制logo

5、autoPiano

只要有电脑,只要会打字,从今天开始你就可以用钢琴弹出一首动人的歌了.
这个项目是作者用Vue + Tone.js做的一款web应用,快来和朋友们一起弹小星星,成为键盘钢琴家吧!

6、在线动力编辑

项目名:sorry
该项目已有4.7k星。
上面项目不仅非常有意思,而且甚至不需要你会编程就能用起来。但是不得不说这些仅仅是GitHub中优秀项目的沧海一粟,当你精通某一名语言之后,你才能发现真正的GitHub,真正的新世界。用双手敲打出的代码,创造属于你的世界!