【编程语言】一些主流编程语言的共有特性区别整理

有一段时间没有发博客了,从笔记里摘录一些发两篇。

自动类型转换

C、C++、Java都有自动的"整型提升"、“算术转换”、“赋值转换”。但golang没有。

golang不允许不同类型的变量参与计算和赋值,需要手动强转。例如:

var ia int32 = 5
var fb int32 = 60
var sc int8 = 2
var result int32 = int32(float32(ia)*float32(fb)*0.01) + int32(sc)

编译运行

编译时需要指定的文件(包名对于编译的意义)。Golang、Java,有"包名"的概念。C/C++没有包名的概念,这意味着什么呢?

  • Golang、Java有包名,因此它们能够根据import <包名>,找到对应的函数、类/结构体的代码,然后进行编译。编译只需要知道入口文件(main函数文件)的路径就行了,其它使用到的文件都根据import <包名>来寻找。另外在编码时,包名也起到了命名空间的作用。

  • C/C++没有包名的概念、没有import,因此必须在编译器的文件列表中指明所有需要参与编译的源文件,才能成功进行编译,如gcc a.c b.c c.c ...。在使用Makefile或CMake配置编译规则时,也需要把这些参与编译的文件配置到Makefile或CMakeLists.txt文件中。

    特殊的:C/C++有#include <xxx>预处理指令,该指令和import有一点类似,它们都有能"寻找所需文件"的作用。#include一般的用法是用来包含头文件(根据头文件的搜索路径规则寻找),头文件会在预处理期间被展开到包含它的文件中。因此,使用#include包含的文件(通常就是头文件),是不需要在编译器(或Makefile、CMakeLists.txt)的文件列表中指明的。


线程/协程

JavaGolangC语言
线程/协程的守护性默认是非守护式线程(用户线程)。
可以调用Thread#setDaemon()设置为守护式线程。
守护式的,主/父协程挂掉后子协程会结束。
可以在父协程使用channelsync.WaitGroup阻塞等待子协程结束再结束。
标准C库没有线程,需要自己实现或使用第三方库。
C11引入了线程thread.h

挂起和恢复

  • Golang:利用select case <- channel的方式传递信号,以实现挂起和恢复协程。还可以用recover()恢复出现了panic的协程。
    只能在协程函数的内部实现挂起和恢复逻辑,但可以在外部传递channel信号。
  • Java:使用线程对象Thread提供的suspend()挂起指定线程,resume()恢复指定线程。
    可以在线程函数的外部调用挂起和恢复。
  • Lua:使用协程表coroutine提供的coroutine.yield()挂起当前协程,coroutine.resume()恢复指定协程。
    只能在协程函数的内部调用挂起,在外部调用恢复 (在内部调用恢复没有语法错误,但逻辑上是行不通的,因为协程都挂起了,就无法执行恢复代码)。

Go和Lua的协程

  • Lua的协程(coroutine),是单纯的/真正意义上的协程(也叫用户态线程),多协程本质上是在一个单线程进程上执行的,因此不具有并行能力,也因此不存在竞态条件。不过协程是支持并发的,只是不能并行而已。
  • Go的协程——goroutine,并不是单纯的协程(coroutine),goroutine是能够运行在多个线程上的,具有并行能力。

标识符命名规则

变量名、包名、常量名、函数名等等,统称为标识符,通常享有一样的规则。但可能有不同偏好的规范,如常量名通常都采用大写。
每个语言推荐的规范,以及每个团队的规范都不一样。用的语言多了之后,可能不会严格按照每个语言推荐的规范来,而是根据自己的经验,怎么舒服怎么来。

虽然一般只说“支持字母”,但实际上都支持中文,以及其它国家的文字。
实际上,根本区别是对特殊符号(#@_!等)的不同支持:都支持_

区分大小写:目前我已知的语言都区分大小写,即大写的标识符和小写的标识符是不同的标识符。

印象中有关于不区分大小写的东西是:windows的文件系统不区分大小写,MacOS的文件系统区分大小写。

Java (naming-conventions)

  • 只能包含字母、数字、_$;不能以数字开头。
    (内部类编译的.class文件名以$+数字为后缀)
  • "起始类"名必须与存放该类的文件名相同。

Golang (names)

  • 只能包含字母、数字、_;不能以数字开头。
  • 文件名对编译器是透明的,想怎么起怎么起。编译器主要关心的是go文件第一行声明的包名,如package main
  • 文件所在的目录名也不是实际的代码包名,实际的包名是go文件第一行的包声明,如package main。因此同一目录下所有go文件的package <package_name>要一样。

C语言 (naming-conventions)

  • 只能包含字母、数字、_;不能以数字开头。

C++ (names)

  • 只能包含字母、数字、_;不能以数字开头。

运算符

三目运算符

C、C++、Java、Js有三目运算符

Golang、Lua没有

位运算符

位运算符有:&按位与、|按位或、~按位取反、^按位异或、>>右移、<<左移

C、C++、Java都支持这些。

除此之外,Java还支持>>>无符号右移(逻辑右移)。

  • >> 右移(算术右移):考虑原有的符号位,高位补符号位。即正数补0,负数补1。
  • >>> 无符号右移(逻辑右移):不考虑原有的符号位,高位补0。无论正数还是负数,都不补0。

Golang不支持~符号,但支持按位取反:Golang的^运算符,作为一元运算符时就是按位取反,作为二元运算符时就是按位异或。

Golang还支持&^按位清除,该运算符的实际操作是&(^)。如:c = a &^ b 等价于 a & (^b)
所谓的按位清除,就是把b中的1位清除:表示如果b中的位为1,则c对应的位为0,否则对应a中的位的值。

01010110101  b
--------------
10101001010  ^b  (golang的^一元是取反)
11001010110  a
--------------
10001000010  a&(^b)  -- a&^b

&|^运算符的特殊含义

它们都是位运算符,但在不同语言中还具有特殊的含义

| 语言 | & | | | ^ |
| -------- | ---------------- | ------ | ------------------------------ |
| Java、JS | 按位与 | 按位或 | 按位异或 |
| C、C++ | 按位与、取地址符 | 按位或 | 按位异或 |
| Golang | 按位与、取地址符 | 按位或 | 按位异或(二元)、按位取反(一元) |

用于逻辑运算

bool值的本质是01,这在C/C++中体现的最为明显(bool值打印出来就是01,可以用01给bool变量赋值)。

因此,C/C++、Java、Js都支持把&|^位运算符用于逻辑运算,相对于&&||,位运算符实现的逻辑运算是非短路的


static关键字

static是静态的意思。C语言和Java都有此关键字,都是与内存管理有关的。但使用上有很大区别。

C语言:static可以修饰 全局变量(静态全局变量)、局部变量(静态局部变量)、函数(静态函数)。

  • 静态变量的生命周期:第一次调用函数的时候,开辟空间赋初值;函数结束后,不释放此变量;以后再调用函数的时候,就不再为其开辟空间,也不再重新赋值相当于不执行定义语句,用的是第一次的那个变量。
  • 作用域:静态变量和静态函数只能在定义它们的.c文件内部使用。

Java:static可以修饰 类成员变量(静态变量)、类的方法(静态方法)、代码块(静态块)、内部类(静态内部类)。
不能修饰局部变量。

  • 生命周期:只为它们分配一次内存。静态变量和静态方法可以直接通过类名调用(不需要实例化对象)。
  • 作用域:不影响作用域。

静态成员的继承问题

ChatGPT-"为什么无法继承静态成员"

在许多编程语言中,包括 C++、Java 和 C#,静态成员(也称为类成员或类属性)是与类本身关联,而不是与类的实例(对象)关联的。因为静态成员属于类级别,它们不依赖于类的实例化。因此,无法在子类中继承或重写静态成员,因为这些成员是直接与类关联的,而不是与对象关联的。

这里是为什么无法在子类中继承静态成员的几个原因:

  1. 静态成员是类级别的:静态成员是在类级别上定义的,而不是在对象级别上。它们在内存中只有一个副本,不会随着类的实例化而创建多个实例。
  2. 继承是对象级别的⭐️:继承是指子类从父类派生,获取父类的属性和方法。继承通常用于实例级别的特性。由于静态成员是类级别的,它们在继承的上下文中没有实际的对象实例,因此无法像实例成员一样被继承。
    (《C++ Primer Plus》13.1.2构造函数:访问权限的考虑:创建派生类的时候,程序首先创建基类对象。)
  3. 避免混淆和歧义:如果静态成员能够在子类中继承,可能会导致混淆和歧义。子类和父类可能都有相同名称的静态成员,这会增加代码的复杂性并引发潜在的错误。

如果你想在子类中使用类似于静态成员的特性,可以考虑以下方法:

  1. 在子类中定义自己的静态成员:子类可以独立定义自己的静态成员,这些成员在子类中是独立存在的。
  2. 使用类方法(非静态成员函数):在许多情况下,可以使用非静态的类方法来实现类似的功能。这些方法可以在子类中进行重写,并且可以根据对象的类型来调用。

总之,静态成员是类级别的属性,不会被继承到子类。子类可以独立定义自己的静态成员,或者通过类方法来实现类似的功能。


数组

C/C++数组

  • 一维数组:

    // 格式:数据类型 数组名[数组长度]
    // 所有的定义写法
    // int a[]; // 不能这么定义(会报错),这属于未知的大小,因为定义的时候就会为数组变量分配内存。
    int a[3]; // 只要定义就会分配内存。现在a占4*3=12字节。
    int a[3] = {};
    
    // 初始化赋值
    int a[3] = {1,2,3};
    int a[] = {1,2,3}; // 通过初始化值确定数组长度
    

    C/C++中定义数组的中括号为什么要放在变量名的后面?

  • 二维数组(多维数组):

    int a[3][3];
    int a[3][3] = {{1,2,3},{1,2,3},{1,2,3}};
    
    // 可以省略第一维长度,能够自动根据初始化赋值的行数确定行数。但不能省略第二维度的长度。
    int a[][3] = {{1,2,3},{1,2,3},{1,2,3}};
    
    // 二维数组每个元素的内存地址都是连续的,因此可以用一个大括号逐个初始化赋值。会一维一维地按顺序赋值。
    int a[2][3] = {2,5,4,2,3,4};
    

    C/C++的多维数组的每个维度的长度都是相同的,缺少赋值的位置会被填充默认值0(对数值型数组)或空字符\0(对字符型数组)。

  • 堆数组

    • C的堆数组,用mallocint *array = (int *)malloc(size * sizeof(int));
    • C++的堆数组,用newint array = new int[size];

Golang数组

  • 一维数组:

    // 格式:var 数组名 [数组长度]数据类型
    // go的语法设计理念是认为应该把更重要的元素放在前边定义,比如变量放在类型前。同理,数组标识(方括号)也被认为是比类型更重要的元素,因此也放在了类型前。
    // 所有的定义写法
    var arr []int
    var arr [3]int
    var arr [3]int = [3]int{}
    var arr = [3]int{}
    arr := [3]int{} // 短声明写法,只能用于函数内的局部变量。
    
    // 初始化赋值。等号前的类型标识可以不写,等号后的类型标识必须写。
    var arr [3]int = [3]int{1,2,3}
    var arr = []int{1,2,3} // 通过初始化值确定数组长度。
    var arr = [...]int{1,2,3} // 通过初始化值确定数组长度。`...`等同于不写长度
    
  • 二维数组(多维数组):

    var arr [3][3]int
    var arr = [3][3]int{{1,2,3}, {4,5,6}, {7,8,9}}
    var arr = [...][3]int{{1,2,3}, {7,8,9}} // 第 2 纬度不能用`...`
    var arr [2][3] = [...][3]int{{1,2,3}, {7,8,9}} // 前边定义了第一维是2,后边写`...`没有作用,但可以这么写。
    var arr = [][]int{{1,2,3}, {7,8,9}} // 其实我感觉`...`有点多余,不写长度本来就可以根据初始化赋值确定数组长度。
    

    golang的二维数组的第二维度(“列”)的长度可以不一样
    golang的多维数组的每个维度的存储空间和C语言一样也是连续的。但不能像C一样用一个大括号逐个赋值。

    可以通过打印数组元素的地址观察出来。–见二维数组的内存布局(Go、C++、Java)

Java数组

  • 一维数组:

    // 格式:数据类型[] 数组名 = new 数据类型[数组长度]
    // 所有的定义写法
    int[] arr;
    int[] arr = new int[3];
    int[] arr = new int[]{}; // 只要写了大括号,就表示要初始化时赋值,就不能再指定数组长度。
    
    // 初始化赋值。若要初始化赋值,就不能再指定数组长度。程序会根据初始化赋值的长度来确定数组长度。
    int[] arr = new int[]{1,2,3};
    int[] arr = {1,2,3}; // 省略写法
    
    // 还可以把方括号写在变量名后边 ---这是为了迎合C语言程序员的习惯。
    int arr[] = new int[3];
    
  • 二维数组(多维数组):

    int[][] arr = new int[3][2];
    int[][] arr = new int[3][]; // 可以不指定第二维,那么第一维的每个元素值都是null
    // 初始化赋值。
    int[][] arr = new int[][]{
      new int[]{1,2,3},
      new int[]{1,2,3},
      new int[]{1,2,3
    };
    int[][] arr = {{1,2,3},{1,2,3},{1,2,3}}; // 省略写法
    
    // 因为Java允许把方括号写在变量名后边,所以就出现了这种奇怪的写法。。。
    int[] arr[] = new int[3][3]; // 等效于 int[][] arr = new int[3][3]; 
    

    Java的二维数组的第二维度(“列”)的长度可以不一样
    Java实际上是不支持多维数组的 – Java没有真正的多维数组。Java的二维数组被看做是数组的数组,即二维数组是一个特殊的一维数组,其每个元素又是一个一维数组。因此Java的多维数组的每个维度的存储空间并不是连续的


指针

Golang和C的指针的*&运算符

  • 意思都一样:&取地址,*两个意思,一个是根据地址取值(解引用),一个是修饰变量为指针变量。
  • *作为修饰符时的语法位置稍微不一样:go的*修饰类型,放在类型前面,如var p *int;C的*修饰变量,放在变量名前面,如int *p
    这就像是定义数组时,C也会把方括号放在变量名后边int a[3];。感觉C更偏向于"装饰"变量名,而不是类型名。

声明和定义

C语言中有"声明"和"定义"的区分。因为它的源代码编译的时候是从上往下编译的,上边的代码如果用到了下面定义的函数或变量,会报错。因此需要把函数原型和变量名声明到使用它们的上边。

其它语言的源代码或许也是从上往下编译的(不然呢?),但他们都很好地避免了C的这种问题。
这或许和C的历史有关,早期的C甚至必须要把变量定义在代码块的最顶部,不能把变量和代码块内的其它逻辑代码穿插在一起。

其它语言里,"声明"和"定义"的说法是混用的。


C/C++/Golang的结构体内存管理

这三个语言中的结构体和类(C++有类),都是"值类型的",结构体/类的内存开辟在栈上

在函数之间传递时,如果需要主调函数需要知道被调函数中对结构体的修改,就要传递结构体/类的指针。说白了就是要给结构体进行动态内存分配,使其保存在堆区

  • Golang直接对结构体取地址,传递结构体的指针。此时结构体会内存逃逸,数据存储到堆中。

  • C (研究代码见c-study/wushu/09_结构体的传递)

    • 主调函数中创建结构体,传递给被调函数:直接对结构体取地址,传递结构体的指针。

      因为结构体是在主调函数创建的,而主调函数的生命周期包含了被调函数,只要主调函数不结束,栈上的结构体内存就不会被释放。

    • 从被调函数中获取结构体:要用malloc为结构体开辟内存,用指针接收结构体指针。

      因为结构体是在被调函数创建的,被调函数一结束,栈上的结构体内存就会被释放,传递出去的指针就变成了野指针。

  • C++:同C,但不是使用malloc而是使用new


多态

可参考:ChatGPT-"C++和Java多态的区别"

C++的多态,默认调用的是父类的方法,要通过virtual声明虚函数实现调用子类的方法。

Java的多态,默认调用的是子类的方法(如果子类重写了这个方法)。(也就是说,Java的默认就是虚函数)

人 p1 = new 理发师();
人 p2 = new 外科医师();
人 p3 = new 演员();
p1.cut(); //剪发
p2.cut(); //开刀
p3.cut(); //停止表演

C++和Java的多态,通过基类都只能访问基类中有的成员,想访问子类中独有的成员,都需要向下转型(强制类型转换)。


数据结构

set集合

  • C++的set会自动排序,是红黑树(二叉平衡搜索树)。
  • Golang没有set,可以这样实现:var set = make(map[interface{}]struct{}),用空结构体作为value,空结构体的内存占用为0

map集合

  • C++的map会根据key自动排序,是红黑树。

true & false

条件表达式,C/C++和Lua的条件表达式可以是任意类型值。

  • C/C++:没有真正的boolean类型,0值是false,非0值是真。NULL0
  • Lua:nil值和false值是false,其它是true。特别需要注意的是,数字0和空字符串也被认为是真。

在这三个语言中,经常利用这种特性把非boolean值作为条件表达式。例如Lua会利用这个判断空对象(如果没有指向则为nil,即为false)。


函数式编程支持

如果一个语言的函数是一等公民(如Golang、Js),这个语言就支持函数式编程

本来函数不是一等公民的语言(如C++、Java),通过引入Lambda表达式,也支持了函数式编程

  • Lambda表达式是定义匿名函数的一种语法,便于进行函数式编程。
  • Lambda表达式是一等公民。有个不严格的变相说法是:在引入Lambda表达式之前,函数可能不是一等公民(如C++/Java);但当引入Lambda后,函数也被视为一等公民(函数通过Lambda间接的成为了一等公民)。

函数式编程的一个明显特征是可以在局部定义函数(局部函数)。Go原本就支持,而C++和Java通过Lambda表达式实现支持。

各语言的支持情况

  • C语言:函数不是一等公民,不支持函数式编程。(只能通过函数指针来尽量模拟)

  • C++:函数不是一等公民(和C一样),但C++11引入了Lambda表达式后,才支持了函数式编程。

  • Java:函数不是一等公民,但Java8引入Lambda了表达式后,才支持了函数式编程。

  • Golang/Js:函数是一等公民,支持函数式编程。

    • Golang并没有引入Lambda,因为Go函数原本就是一等公民,支持函数式编程。
    • Js后来引入了Lambda,但Js函数原本就是一等公民,Lambda只是让函数式编程更加简洁。
  • Lua:函数是一等公民,支持函数式编程。Lua5.1之后引入了匿名函数语法,让函数式编程更加简洁。

    Lua5.1引入的匿名函数语法,官方就叫它匿名函数,而不是lambda。但看到网上有些文章叫它lambda,可以理解,但确实不对,属于以讹传讹了。

  • 32
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值