文章目录
- 自动类型转换
- 编译运行
- 线程/协程
- 标识符命名规则
- 运算符
- 三目运算符
- 位运算符
- `&`、`|`、`^`运算符的特殊含义
- `static`关键字
- 静态成员的继承问题
- 数组
- 指针
- 声明和定义
- C/C++/Golang的结构体内存管理
- 多态
- 数据结构
- true & false
- 函数式编程支持
有一段时间没有发博客了,从笔记里摘录一些发两篇。
自动类型转换
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)的文件列表中指明的。
线程/协程
Java | Golang | C语言 | |
---|---|---|---|
线程/协程的守护性 | 默认是非守护式线程(用户线程)。 可以调用 Thread#setDaemon() 设置为守护式线程。 | 守护式的,主/父协程挂掉后子协程会结束。 可以在父协程使用 channel 或sync.WaitGroup 阻塞等待子协程结束再结束。 | 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
值的本质是0
和1
,这在C/C++中体现的最为明显(bool值打印出来就是0
或1
,可以用0
和1
给bool变量赋值)。
因此,C/C++、Java、Js都支持把&
、|
、^
位运算符用于逻辑运算,相对于&&
和||
,位运算符实现的逻辑运算是非短路的。
static
关键字
static是静态的意思。C语言和Java都有此关键字,都是与内存管理有关的。但使用上有很大区别。
C语言:static可以修饰 全局变量(静态全局变量)、局部变量(静态局部变量)、函数(静态函数)。
- 静态变量的生命周期:第一次调用函数的时候,开辟空间赋初值;函数结束后,不释放此变量;以后再调用函数的时候,就不再为其开辟空间,也不再重新赋值,相当于不执行定义语句,用的是第一次的那个变量。
- 作用域:静态变量和静态函数只能在定义它们的
.c
文件内部使用。
Java:static可以修饰 类成员变量(静态变量)、类的方法(静态方法)、代码块(静态块)、内部类(静态内部类)。
不能修饰局部变量。
- 生命周期:只为它们分配一次内存。静态变量和静态方法可以直接通过类名调用(不需要实例化对象)。
- 作用域:不影响作用域。
静态成员的继承问题
ChatGPT-"为什么无法继承静态成员"
在许多编程语言中,包括 C++、Java 和 C#,静态成员(也称为类成员或类属性)是与类本身关联,而不是与类的实例(对象)关联的。因为静态成员属于类级别,它们不依赖于类的实例化。因此,无法在子类中继承或重写静态成员,因为这些成员是直接与类关联的,而不是与对象关联的。
这里是为什么无法在子类中继承静态成员的几个原因:
- 静态成员是类级别的:静态成员是在类级别上定义的,而不是在对象级别上。它们在内存中只有一个副本,不会随着类的实例化而创建多个实例。
- 继承是对象级别的⭐️:继承是指子类从父类派生,获取父类的属性和方法。继承通常用于实例级别的特性。由于静态成员是类级别的,它们在继承的上下文中没有实际的对象实例,因此无法像实例成员一样被继承。
(《C++ Primer Plus》13.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的堆数组,用
malloc
:int *array = (int *)malloc(size * sizeof(int));
- C++的堆数组,用
new
:int array = new int[size];
- C的堆数组,用
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
值是真。NULL
是0
。 - 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,可以理解,但确实不对,属于以讹传讹了。