从Java 转 C++

6,022 阅读12分钟

本文建立再对java语言熟练的基础之上。

本文讲解的是 java到C++的思维模式,并且从中使用几个例子来加以作证,具体的代码 ,如果可能还需要 具体 查询

java 建立再虚拟机之上, 有人问过我,java 编译到可执行 之中发生了什么? 一脸懵逼的我,选择了 百度。

这里面涉及一堆的JVM的概念。 一切皆有 jvm 帮你负重前行。在你点击 运行 之后, 编译 连接 这些 按下不表。【感兴趣的可自行百度,以后在讲吧,毕竟这篇不是讲 java底层知识的】

说在前面

所有的语言其实都是工具,越高级的语言越像一堆的语法糖。

正文

先从环境部署说起

正常写个java程序,入门很低,因为你只需要 装个 java的环境【Oracle已经帮你搞定了,下载JRE, 一键安装,再装个IDE即可】

而写C/C++ 程序,对于不熟悉的人来说,可能就比较复杂了,因为没有 虚拟机的概念,在 语言层面 是一致的,而深入平台相关的 api则不一样了(比方说:线程API),目前主流的三大操作系统:windows linux macos 多多少少都存在一点区别。

C/C++ 搭建环境其实也不难,现在win10 出了 subsystem 可以帮助开发者 快速搭建linux的子系统,方便了很多人。

windows 的安装 需要下载 mingw 或者 VistualStudio 2019 ,然后配套安装即可。

linux 分为不同的 发布版本,又有不一样的命令: apt-get yum 等, 不过下载环境也是很简单的 apt-get install g++ 一行就搞定了

macos 不知道是不是因为装了 xcode 的缘故,发现啥都有了,连环境都不用配置,真给力。

编译器

再实际的开发过程中,会遇到很多 不同的编译链,linux 的编译链,每个不同平台的编译链,这么多的编译链就导致了我们为了保证程序的健壮性,必须知道自己 程序关于 底层的更细节的 认知。

主流 :clang /clang++ gcc/g++ cl/vcvarsall.bat【windows】

编译器的不同,影响关于 编译出来的 库 在 内存,指令集,等更为底层的区别

语言层面

最困扰 java程序员的,无非就是下面几种情况

指针

指针,而指针的形式又千奇百怪,导致看上去很郁闷罢了。

例如:

void* test(void*);

我是这么理解指针的,java中的传引用参数就是指针。

内存

写两个例子,申请变量,

Object a = new Object();
void * a = malloc(sizeof(void*));

对于java 来说,因为存在 垃圾收集器,申请的变量,放置在堆还是栈上,其实 可以不用那么在意,只有在做性能分析 , OOM 的时候才有用。

而对于C/C++ 来说,

  1. 你应该堆你申请的每一个直接都有个清晰的认知,申请一个字节就可以为何申请4个字节,
  2. 申请的内存,何时释放
  3. 赋值操作导致拷贝,从而发生内存的增加,如何避免 大内存的拷贝。
  4. 如果 一个三方库,提供申请内存函数,理论上也会提供 销毁内存的回调,不要忘记调用

总的来说 C++程序员更关注 内存的布局 以及性能对于程序的影响

windows 之 c

在windows 开发 简单的c 只需要下载mingw即可,将所有关联的base 库全部下载好,默认会在c:/mingw/bin 底下就有相关的可执行文件,将可执行文件添加到path中即可在任何命令行窗口进行使用。

如果 安装了 微软的visualstudio 则可以找到 vcvarsall.bat 运行即可。

我的路径是【默认的】: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build

vcvarsall.bat x64  # 指定平台 为x64平台
cl CybAuthor_Server_test.c /I. /link libCybAuthorServer.lib
关于linux 的 头文件

/usr/include/ 这是基本上所有相关头文件的查找路径。

如果需要查看 access中的 F_OK代表什么值,则可以通过查看 man access 找到 包含的头文件,再进入该头文件进行查找。

类型

java程序员说: INT32 INT32_t size_t 等等 这些看不懂 。

其实 这些就是java的 基础类型罢了。

在C/C++ 里面 因为要关注平台 cpu架构等,就不得不对 每个字节都了解的一清二楚, 换句话说 long a =0 这个东西占用多少个字节,java程序员 也许会说: 8个字节。 而对于C/C++程序来说,可能4个也可能8个。

那如何让每个人都清楚多少个字节呢?

int32_t 等出现了 表明32位 即 4个字节。

当然这些基础类型是 某个类型的 别名罢了

typedef signed int __int32_t;
typedef __int32_t int32_t;

当然只要你乐意,你也可以将类型改成你想要的

typedef __int32_t  mine_int;
mine_int a = 0;

所以在c++里面其实也无非就是哪几种基本类型 和java 是一样的。只是c/c++为了更好的表明内存大小以及有无符号,建立了很多别名,更易于读懂罢了。 毕竟 c/c++ 程序员的 宗旨就是,对自己程序的每个字节都一清二楚。

头文件 + 源文件

对于java来说,一个 .java 就可以完成的事情,为啥要定义两个文件 .h .c 实在没有必要。

我是这么理解的,头文件 表明申明了 一些列的方法 或者函数,调用者不用关系 函数的具体实现,就可以调用【听到这个,是不是很像java的接口】。

C/C++ 的头文件还支持提供给内部使用,提供给外部使用,比如我有两个头文件

//inA.h
void sayHello();
void say(const char *,size_t size);

而对于外部就只提供 如下的函数,但是外部是不知道原来还有一个say的,当然高手除外【使用别的方式也是可以间接获取的,此处按住不表】

//inA.h
void sayHello();
模板

可能接下来就会涉及到每种语言都不一样的地方了,不论是C/C++ 其他的语言的模板的书写方式都不一样,但是大同小异。

这里提供一个最简单的例子,[C++里面模板仅允许出现在 头文件中]

template<typename T>
void print(const T& t) {
    cout << t <<endl;
}
<T> void print(T t) {
    System.out.println(t);
}
宏定义

这个东西在java里面,没有相似的东西

#define HELLO 1
#define hello HELLO
初值

一定要为 c/c++ 程序附上初值,否则可能出现异常哦!切记

    char  w_data[256] ={0};
    memset(w_data,0,256); // 设置 w_data 后面 256 个字节 全为0
&(w_data[i]) vs w_data++

clang 编译器对于 前者的写法 有 编译校验的功能

拼接

snprintf

    int snprintf(char *str, size_t size, const char *format, ...);
    int sprintf(char *str, const char *format, ...);
    void  print_0xbyte(u8* data,size_t len){
        char* strBuf = (char*)malloc(len*2+1);
        for(size_t i = 0;i<len;++i)
        {
            snprintf(&(strBuf[i*2]),3,"%02x",data[i]);
        }
        //sprintf()//xxxx
        printf("%s",strBuf);
        free(strBuf);
    }

memcpy

#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
    char a1[6] = "hello";
    char b[sizeof(a1)] = {0};
    memcpy(b, a1, sizeof(a1));
    printf("%s\n", b);

strcpy & strncpy

#include <string.h>

char *strcpy(char *dest, const char *src); // 遇到\0 则结束
char *strncpy(char *dest, const char *src, size_t n);
    char a2[6] = "world";
    char b2[6] = {0};
    strcpy(b2, a2);
    printf("%s\n",b2);
\0 结束符

在c/c++ 里面 \0 结束符 是作为字符串,流等结束的标志,在java 里面没有这么严格的标志

    char a[10] = "hello123";
    a[5] = '\0';
    printf("%s", a); # 打印为 hello
判断char *长度
char a [10]="helloworld";
a[5] = '\0';
printf("strlen = %d, sizeof = %d\n", strlen(a), sizeof(a));

输出 strlen = 5, sizeof = 10

打印

在c/c++ 里面 \n 是作为换行的依据,在java里面其实也有只是封装好了 print println

打印 16进制 [c/c++ 中对于字符的处理明显比其他语言来的更多。16进制可以做为唯一码]

printf("%02x",'a');// 打出 a的 16进制 ,可以通过 ascii码进行查看,02 表明 不足2位补0

char str[3] ="ab";
print("%02x%02x\n",str[0],str[1]);
for(size_t i = 0 ; i<sizeof(str);++i){
    print("%02x",str[i]);
}

上述 比方说 'a' 用16 进制打印:61

上述 比方说 'a' 用10 进制打印:97

所以如果你遇到了 696a6b6c 这样的字符串,其实就是通过16进制转换过来的,翻译过来就是 "ijkl"

通过 查询 ascii 码 即可得知其中的数据 查询地址

这里还要说下: 关于C/c++ 中的格式化打印,[不全,但常用]

  • %p 打印地址,用在 校验地址是否 发生变化。再强转的场景出现问题时可用
  • %x 打印16进制
  • %l 打印 long类型,因为 c/c++ 对于 位数的要求比其他语言高的多。所以清楚字节数是很重要的。
  • %d 打印 int类型
声明与定义
char a[3] = "ab";// 自带 /0
char b[5] = "hello"; // 编译报错,因为是6个字节
如果你要改变a的值 只能使用 strcpy 或者 memcpy 之类的函数

    const char *b3 = "bcd";
    strcpy(a, b3);
    printf("%s\n", a);
const

在c/c++里面 const 为非常重要的作用,决定入参是否可变, 申请的变量是否可变,地址是否可变等等。

const char* p ="xxx";// 常量
char* const p ="xxx";// 不可变指针
void hello( char* const a){} //入参为指针不可变更
二级指针

用处 在入参的地方

void hello(void**a,int len);
//调用
char * a =NULL;// 此时为空指针,最好赋初值,因为不同的平台,可能默认初始值不一致
hello(&a,2);// 这就是调用,一般用作在函数内部进行申请空间与赋值

    void * dupP = NULL;
    printf("void* sizeof=%d\n",dupP==NULL);
    hello(&dupP,3);
    printf("void* sizeof=%d\n",dupP==NULL);

void* sizeof=1

void* sizeof=0

引用形参

在java 里面想要修改一个入参为 基本数据类型 的是不可能的,但是在c/c++里面其实就可以

public void swap(int a ,int b){}
void swap(int*a ,int*b){} // 相当于是java的类对象Integer
堆栈
    // 再栈上面申请的内存
    char  w_data[256] ={0};
	memset(w_data,0,256);
	
	// 再堆上面 申请了256 字节
	char * w_data_p = malloc(256);
	memset(w_data,0,256);// 申请完了 记得要 清空内存,这个之前有说过
	free(w_data_p);
字节

c/c++ 里面关于字节的 处理 是很多的,而且很方便

    char  data[256] ={0};
    
    // 从文件里面读取 数据
	FILE *fp = NULL;
    fp = fopen(CONFIG_ROOT_PATH"txz_pop.txt", "r");
    if (!fp) { // 可能 打不开,fp 就为NULL
        printf("read open file err.\n");
        return -1;
    }else{
        printf("read open file success.\n");
    }
    // 读出数据
    int ret = fread(fp,data,256);  //这里举了一个例子 256 足够了
    //fdelete(fp); // 每个平台关于删除文件的方式可能不一致
	fclose(fp); // 写任何语言的 文件都应该有开有关,语法糖 帮你做了 关的操作而已,但是你也应该对其内部的原理有个了解。
	
	// 字节操作,第一个字节为 长度,后面按照指定长度进行读取指定的数据
	size_t size  = data[0]; //取出第一个字节,最大 为2的八次方 即256。
	char result [256] ={0};
	memset(result ,0 ,256);
	result[0]='Y';// 为第一个字节附上值
	memcpy(result+1,&(data[1]),size);// 取出data数据第一个字节开始后面size 个字节 到result 从第一个字节开始的位置
	// 上面就是关于字节的操作,要熟练掌握 memcpy memset 等
大小端
uint8_t len = 1;//这里一定要 用u8 确定是8位
FILE* f = fopen(push_file_name_out, "wb");
fwrite(&len,1, 1, f); // 大小端问题

思考: 如果将上述的len 写成 int 会发生什么? 在某些情况下会发生 写入 和预期是不一致的。

其他

说了这么多,我还是不知道,怎么写具体的代码,比方说: 文件读写,网络请求,消息队列。

其实 知道了原则 有些api还是 需要 在实际应用中摸索。就如同 了解了 java语言,还是要去了解 网络请求怎么写一样【调用httpurlconnection】,当用多了也就自然 会了。

关于源码阅读

以前不管看什么源码,都容易局限于 代码的具体实现而没有宏观的概念,比方说,在看某一个功能点的时候,一个功能点可能扯出很多我没有了解过的api,然后我就深入一个一个的api,去看怎么实现的,【这其实是不对的】。比方说,你再了解网络框架的整体流程,【记住 函数名称一般 就能告诉你 大概的东西,比方进程间通讯,脑子里面应该有个基础的概念,就是进程间通讯有哪几种,管道啊 共享内存啊,socket 啊等等,然后看下名字大概也就能猜个八九不离十了。这时候继续往下看,不要纠结到底是采用哪种方式进行通讯的,协议的话另说。等整个框架看完之后在进行深入的研究。】

比方说,我看ffmpeg ,先不要深究每个具体函数里面是怎么实现的,或者 看函数内部的实现是为了让你理解 当前大概在做什么,而不是怎么做。 先理解 这大概是解包,这大概是解帧,这大概是解封装,这大概是在发送http请求。等把整个流程梳理完毕之后,你再深究,解帧是怎样将文件的字节区间进行如何如何的等等。

我 平常的工作中,有遇到方案商给到的sdk[经过隐藏具体实现的,只有函数声明的]。此时就要大概猜测对方给的这个sdk的工作原理,这快 大概做啥,这块大概干啥。一定不能深究,当然看源码一定要建立在你对基础知识有个大概的构建,才可以。否则你不懂什么叫 进程间通讯的实现方式, 消息队列的实现方式, 锁等。

举例
  // 再栈上面申请的内存
    char  w_data[256] ={0};
	memset(w_data,0,256);  // 
	
	// 数据的拼接,没错他竟然没有 像java 的 + 作为字符串的拼接
	char * uuid = "uuid";
	char * mid = "&&";
	char* token = "123";
	memcpy(w_data,uuid,strlen(uuid));
	memcpy(w_data+strlen(uuid),mid,2);
	memcpy(w_data+strlen(uuid)+2,token,strlen(token));