编程语言(2)—类型和运算符
程序=数据+计算。类型确定了数据的定义、数据支持的计算。是程序的基础。
类型可分为静态类型和动态类型。绝大多数类型是静态类型,编译期确定(常见的如C++的模版类型、sizeof(int)、typeid(int))。动态类型特指接口和多态, 对象的类型需运行时确定。例如C++具有虚函数的多态类, golang的interface{}和通过接口访问struct对象, 以及java的普通类(java的类普通方法均需通过虚函数表调用)。
本文介绍C、C++、JAVA、Go、Python的类型和运算符。
C语言
C语言类型主要有
- 整型int
- 浮点型float, double
- 字符型char
- 枚举类型enum
- 数组
int[]
- 指针
int*
,包含函数指针int (* p)(int, int)
- 结构体struct
- 共用体Union,
- 空类型void
C 语言支持的运算符,按优先级从高到低排列
() [] -> .
,括号、数组下标、成员访问! ~ ++ -- - +
,逻辑非、按位取反、自增、自减、取负、正号;* & (type) sizeof
解引用、取地址、类型转换、大小计算* / %
乘法、除法、取模+ -
加法、减法<< >>
左移、右移< <= > >=
小于、小于等于、大于、大于等于== !=
等于、不等于&
按位与^
按位异或|
按位或&&
逻辑与||
逻辑或?:
= += -= *= /= %= <<= >>= &= ^=
赋值及复合赋值,
逗号
具体类型
整型,整型几乎支持所有运算,包括加减乘除算数运算,左移右移,大于等于比较运算,按位运算,逻辑运算(0表示false,其余整型表示true)
1 | int //基本整型,4字节 |
浮点型,浮点型不支持左移右移、按位运算。支持算术、逻辑、比较运算。
1 | float // 单精度浮点型,4字节。 |
字符型,字符型实际等于8位无符号整型,整数支持的运算它都支持,运算等价于ACCSI码整数运算
1 | char // 字符型,1字节, |
枚举类型,C语言的枚举类型同样等价于整型,整数支持的运算它都支持
1 | enum Color {RED, GREEN, BLUE}; |
数组,数组本身支持[index]
索引查询。另外,数组名相当于数组第一个元素的地址
1 | int arr[10]; |
指针,指针本身是一个64位的整型。但不支持乘除、移位、按位运算。支持加减,逻辑运算。当然还支持*
解引用运算
1 | int *p; |
结构体, 结构体支持使用.
找到成员变量, 结构体指针则使用->
1 | struct Person { |
共用体,共享内存。共用体的size是size最大的成员变量的size。共用体运算同结构体。
1 | union Data { |
void类型, 主要应用于void指针。void指针支持加减,逻辑运算。但不支持*
解引用运算
1 | void *ptr; |
函数指针。函数指针除了支持指针运算,还支持使用()
执行函数
1 | int add(int a, int b) { |
转型
C语言没有RTTI运行时类型信息, 因此C语言没有动态类型。如果函数参数需要传接口, C语言一般选择void*
。所有指针先转为void*传给函数, 在函数内再转型回来。
C语言支持隐式转型和强制转型。强制转型和隐式转型都发生在编译期。
C语言运算符的两侧必须是相同类型,如果不是,就会尝试隐式转型。隐式转型规则,隐式转型主要针对整型和浮点型。隐式转型规则是size小的转为size大的,例如char转为int,int 转换为 unsigned/float, float 转换为 double
1 |
|
C语言的转型有很复杂,比如下面的例子。因此尽量保证类型的统一。,使用stdint.h库里的int32_t、uint32_t
1 | int i = -2; |
C++
C++兼容C语言的类型、操作符和转型,另外,还支持class定义、类重载运算符、动态类型和静态模版类型
- class 定义, C++ 通过定义class,实现了面向对象(封装、继承、多态)。通过拷贝构造和移动构造, 合并了对象创建和初始化(相比C的struct, 配合new和delete关键字)。通过重载操作符,实现类型比较、赋值等功能。通过虚函数和虚函数表, 实现RTTI和动态类型。
- 静态模版类型。C++模版大大增强了编译期类型能力, 除了虚函数调用和动态类型操作(如dynamic_cast, typeid(动态对象指针)), C++的类型信息和操作完全在编译期完成, 相比C语言无性能损失、无额外空间占用。
类型定义举例
1 |
|
C++ 支持bool型,bool型基本等同于1bit的整型,支持加减、移位等运算符,默认false(0)
C语言只能通过malloc申请进程堆内存,把地址给指针,并通过free释放内存。C++提供了new关键字在堆内存创建对象,new后面可以跟int, float, char, 数组等基本类型,也可以跟自定义类型。对应的释放关键字是delete。new相比malloc,可以通过调用构造函数实现对象初始化。
在值和指针类型之上,C++增加了引用类型。引用相当于const指针, 但简化了使用。初始化引用时即实现对原值的绑定,对引用的修改等价于对原值的修改。C++的指针和引用使用规则
- 若某函数不创建对象,不具有对象的所有权,只修改对象,函数参数传裸指针
- 若函数不修改对象,只读对象,传const&
- 若函数创建对象同时具有对象的所有权, 使用std::unique_ptr和make_unique。若要转移所有权, 使用std::move(unique_ptr)。
- 如果某对象只要创建一次, 需要被共享访问, 尤其是多线程共享, 对象无法预知何时不再被使用时, 使用std::shared_ptr和make_shared。
shared_ptr 例如
1 | class DataProcessor { |
C++提供右值引用,函数的右值引用参数表示要接收对象的所有权。例如某函数执行std::move(unique_ptr)将所有权转移, 函数参数传右值引用获得所有权。右值引用只能在所有权转移时使用,常见的是赋值(旧对象成员的所有权转移到新建对象), 容器构造(对象所有权从本函数转移到容器)。std::move是将左值转为右值, 目的是转移左值的所有权。
在C语言的隐式和显示转换之上,C++增加了四个显示类型转换
- static_cast,编译期间类型转换,基于类型信息
- dynamic_cast,运行时接口向下转型, 接口指针转为对象类型指针
- const_cast,将const* 指针转为普通指针, 从而可以通过指针修改对象。 编译期行为
- reinterpret_cast,等价于运行时指针强制转换
对象创建和初始化
运行期局部变量在栈上创建,全局变量和静态变量在静态区创建,new 产生的变量在堆上创建。栈变量的作用域等于函数作用域,全局变量和静态变量的作用域是进程执行期,new产生的堆变量可能逃逸,需要用户自行管理生命周期和内存释放。
确定所有权是管理堆变量生命周期的推荐方式,具有对象所有权的负责创建和析构堆对象,而其他的只有对象的读写权限,不负责生命周期管理。难以明确所有权的,使用引用计数管理,也就是shared_ptr。
std::weak_ptr弱引用,不增加引用计数;必须通过lock()方法获取临时的shared_ptr才能访问对象。可以作为观察者, 通过尝试lock判断对象是否存在,然后访问对象。weak_ptr可用来处理循环引用,也就是如果想父对象引用子对象,子对象也引用父对象,就会造成循环引用,可以通过父对象通过weak_ptr来引用子对象, 当调用子对象时, 通过lock()判断子对象是否存在。
1 |
|
初始化
- 局部变量,未初始化的为不确定值(即垃圾值)
- 静态变量,自动初始化为 0 或 NULL(指针类型),静态变量初始化只执行一次
- 全局变量,自动初始化为 0 或 NULL
C++类变量规则
- 普通成员变量, 必须在构造函数中显式初始化,未初始化的普通成员变量值是未定义的(包含随机值)
- 静态成员变量是类级别的,需要在类外初始化,自动初始化为 0 或 NULL(指针类型),静态变量初始化只执行一次
- const变量必须在构造函数的初始化列表中初始化,初始化后不可修改
- C++11 允许为 非静态成员变量 提供类内初始值。静态成员变量仍需在类外初始化。
- 成员变量按 声明的顺序 初始化,与初始化列表的顺序无关。
修饰符
static, 静态变量。一个编译单元(.cpp)文件一个静态变量, 静态变量不可跨编译单元共享,因此不能作为全局变量。全局变量(外部变量)跨.cpp文件,链接多文件共享;
- 静态函数的定义一般放到头文件里, 如果放到.cpp文件,头文件被其他.cpp引用时, 由于静态函数无法变链接,其他.cpp文件会发生函数链接错误。
const,表示常量,不可改变量。
- 修饰函数表示函数不可修改类成员变量, 可通过mutable关键字修改。
volatile,告诉编译器不要对该变量进行优化。常用于 多线程编程 或 硬件编程,避免编译器对变量的访问进行优化。
extern,声明变量或函数在其他文件中定义。它不分配存储空间,只是告诉编译器该变量在其他地方定义过。链接时全局变量在多个文件共享。
mutable 用来修饰类中的成员变量,即使类的对象是 const 类型,成员变量仍然可以被修改。
C++ 编译类型推导
C++ 编译期静态类型推导
C++ 14 允许 Lambda 参数使用 auto 类型推导
1 | auto lambda = [](auto x, auto y) { return x + y; }; |
支持返回类型推导, decltype(auto)
1 | // 返回类型自动推导 |
constexpr,修饰函数和表达式,尝试在编译期就给出计算结果
1 | class Circle { |
结构化绑定
1 | std::pair<int, std::string> data{42, "Alice"}; |
C++ 模版编程
既然讲到类型, 就离不开C++模版编程, 编译期类型
::value 和::type
::value
和 ::type
是两种常见的成员访问形式,用于从 类型特性(Type Traits) 或 元函数(Metafunctions) 中提取信息
::value
用于从 类型特性 中提取一个 编译时常量值(通常是布尔值),表示某种类型或条件的静态属性。
- 类型检查:检查类型是否符合特定条件(如是否是整数、指针、类类型等)。
- 数值提取:获取类型关联的静态数值(如数组维度、对齐值等)。
1 |
|
::type
用于从 元函数 中提取一个 类型,常用于类型转换或条件编译。
- 类型转换:生成与输入类型相关的新类型(如移除引用、添加常量等)。
- 条件分支:通过
std::enable_if
控制模板的启用或禁用。
1 |
|
在模板中访问 依赖名称(Dependent Name) 的 ::type
必须 使用 typename
关键字,以明确告知编译器这是一个类型而非静态成员。
1 | template <typename T> |
模版匹配规则
类模板的匹配优先级为:
- 完全特化版本(全参数指定)。
- 部分特化版本(部分参数特化)。
- 主模板。
1 | // 主模板 |
函数匹配规则
- 非模板函数(如果有精确匹配)。
- 最特化的模板函数(通过参数推导和约束判断)。
- 通用模板函数。
1 | // 通用模板 |
C++20 引入 概念(Concepts),允许更直观地约束模板参数,改变匹配逻辑:
1 | template <typename T> |
SFINAE(替换失败并非错误
SFINAE(替换失败并非错误)是C++模板元编程中的核心原则,其核心思想是:在模板实例化过程中,如果替换(Substitution)模板参数导致无效代码,编译器不会报错,而是将该候选从重载集中剔除,继续寻找其他合法候选。
- 当调用一个函数或使用类模板时,编译器会尝试推导模板参数,并生成具体的代码(实例化)。
- 如果替换模板参数后生成的代码无效(例如访问不存在的成员、类型不兼容),编译器不会抛出错误,而是静默忽略该候选,继续尝试其他可能的重载或模板。
- 若替换成功但实例化后的代码在函数体内报错(如无效操作),编译器仍会报错。
可以直接设置根据不同类型提供不同的实现
1 |
|
模版特化
1 | template <typename T, typename Enable = void> |
std::enable_if
std::enable_if的作用是条件编译,
基于 SFINAE(Substitution Failure Is Not An Error)原则。它允许根据类型特性或编译时条件启用或禁用特定的函数重载或模板特化。
std::enable_if<Condition, T>::type
:如果 Condition
为 true
,则 std::enable_if
的 type
成员是 T
;否则,std::enable_if
没有 type
成员,这会导致模板实例化失败。
1 |
|
编译时和运行时的一个优势是,编译期能知道类型信息。
C++ type_traits提供std::is_integral
是用于在编译时检查某个类型是否为 整型(整数类型)。
以下类型std::is_integral返回tree
1 | 有符号整型:signed char, short, int, long, long long |
std::is_floating_point
:检查浮点类型
std::is_arithmetic
:检查算术类型(整型 + 浮点型)。
std::is_signed
:检查类型是否有符号。
模版实例化失败
- 类型不满足模版约束
1 | template <typename T> |
使用 static_assert
提前验证类型:
1 | template <typename T> |
使用 C++20 概念(Concepts) 约束模板参数:
1 | template <typename T> |
- 模版参数不匹配
1 | template <typename T, int N> |
- 依赖名称未正确解析
1 | template <typename T> |
使用 typename
明确依赖名称
1 | typename T::value_type data; |
模版头文件
JAVA
JAVA所有类型和函数都是写到类中,类名和文件名相同。main函数是在主类中定义的static函数。JAVA没有全局变量,因为所有变量都是在类中。
JAVA基本类型变量存储在栈中,引用类型变量存储在堆中。引用类型变量使用new创建,jvm的垃圾回收模块负责对象回收。JAVA的变量都会初始化,不会有随机值。
JAVA类最大的特点是动态加载(按需加载),类静态变量和静态模块随着类加载而初始化和执行,如果某类无须被加载,则不会执行该类中的静态变量和静态块。**
- 类的加载通常是在访问类的静态成员(如静态字段、静态方法)或创建类的实例时触发的。
- 类加载器会在需要时加载类,不会提前加载。
JAVA运算符基本和C++一致,除了
- 取消指针和引用相关运算符,包括
*
,->
,&
JAVA类型默认值是0, JAVA的boolean型不再和整型互通
基本数据类型, 存储值本身, 参数会选择传值本身
1 | 数据类型 存储大小(字节) |
引用类型, 存储对象的引用。引用类型默认值是null, 参数会选择传引用
1 | // 类 |
JAVA提供包装类型为基本数据类型提供引用, 默认值是null
1 | 基本数据类型 包装类型 |
JAVA包装类型的常用函数
1 | Integer.valueOf(int i) // 返回表示基本类型的Integer对象 |
JAVA 通过(type)实现显式转换
1 | class Animal {} |
JAVA中,所有类都直接或间接继承自 Object 类。Object类提供的成员方法, 也就是任何都有以下方法,也可以重载以下方法
1 | public boolean equals(Object obj) |
变量访问控制和修饰
Java中,如果需要使用其他包中的类,必须利用import显式地导入它们。import 语句只能导入类,而不能导入方法、字段
Java 的访问控制包括四种权限:
- public:类或成员可以被任何类访问。
- protected:类或成员可以被同一个包中的类或子类访问。
- default(无修饰符):类或成员只能被同一个包中的类访问。
- private:类或成员只能在当前类内部访问。
包名通常采用 小写字母,以避免与类名的命名冲突。
包名通常使用 反向域名,例如 com.example.myapp。这种做法避免了不同公司或组织的包名冲突。
除了类,java的变量和函数也有访问修饰符
1 | // public:变量可以被任何其他类访问,无论它们在同一包中还是不同的包中。 |
static,声明为 static 为静态变量,属于类。
final,声明为 final 的变量是常量,初始化之后不能再修改。final修饰函数表示该函数不能被重写
volatile 变量会告诉 Java 虚拟机(JVM)每次访问该变量时都要从内存中读取,而不是从缓存中读取。通常用于多线程编程中,确保变量的可见性。
synchronized 主要用于方法,确保同一时刻只有一个线程能执行该方法。
native 关键字用于声明本地方法,这些方法通过 JNI(Java Native Interface)调用非 Java 语言(如 C、C++)编写的代码。native通常用于方法声明。
反射
由于java是动态加载类, 这让java 具有利用反射动态加载类, 这催生了spring ioc框架。如果要根据字符串的内容匹配对应的类/函数,C++和go都要维护一个(string->函数)的map,但java可以直接通过类名来动态加载类。java利用反射动态加载类分两步1. 获取class类 2.加载class类
获取class对象
1 | // 类名.class |
利用反射调用方法,修改变量
1 | Method method = clazz.getDeclaredMethod("methodName", parameterTypes); |
Go
Golang的运算符和C++的类似,区别主要是指针类型。Go指针类型不支持算术运算,只支持引用和解引用运算。Go指针零值是nil, 获取成员变量用.
golang可以使用 new 分配内存,并返回指针。对切片、map、channel、interface,golang可以使用make函数分配内存和初始化。
golang可以使用var xxx type
声明变量,var xxx = value
或xxx := value
声明变量并赋值
当变量的生命周期仅限于当前函数时(不用new构造的对象),Go 会尽量将其分配到栈上。堆内存,通常分配给引用类型(slice, map, channel, pointer)或者使用new创建变量且变量的地址被传递到函数外部。
Go 变量的访问控制。全局定义的小写字母只能包内访问,大写字母可以跨包访问。访问其他包的变量需要先import。golang 提供struct组合代替继承(C++继承内部的实现就是组合)。
- 匿名嵌入的结构体方法会被提升(Promote)到外层结构体。
- 若外层结构体定义了同名方法,会覆盖嵌入结构体的方法
golang支持的类型
1 | // 以下是值类型 |
Go中只有强制类型转换
1 | var a int = 42 |
golang 可以通过直接赋值将struct转为接口, 同时通过类型断言将接口转换为具体类型(动态类型)。Golang中,所有struct都实现interface{}
空接口
1 | package main |
反射
golang 反射的目的是运行时获取对象的类型信息。一方面,golang不支持模版编程, 对象的静态类型信息也需要运行时获得(C++编译期就可以获得静态类型信息)。另一方面,动态类型信息同样需要运行时获得。(golang的这个反射不支持根据字符串动态获取对象, 和C++ 的动态类型差不多)
golang 的struct 统一实现了interface{}。interface{}包含了类型信息和对象指针。因此golang静态类型和动态类型信息都是借助interface获得。
1 | // 获取类型信息 |
golang的多态依赖于interface{},包括静态多态。例如json序列化提供的统一接口就是interface{}
1 | func Marshal(v interface{}) ([]byte, error) |
Python
python类型比较特别, 把类型分为两类,可变类型和不可变类型。
如果修改一个不可变对象,Python 将会创建一个新的对象。
- 不可变类型包括整数(int)、浮点数(float)、字符串(str)、元组(tuple)等。
- 可变类型的值可以原地修改。可变对象包括列表(list)、字典(dict)、集合(set)等, class定义的类默认是可变类型
不可变类型函数参数可以认为是值传递,可变类型函数参数可以认为是引用传递
Python变量的类型是在运行时决定的,不需要显式声明。Python 不允许隐式类型转换。
1 | def modify(x): |
Python的类型如下
1 | 类型 描述 |
类型检查和转换
1 | print(type(42)) # <class 'int'> |
Python局部变量区域可以访问全局变量,但无法修改全局变量
1 | x = 10 |
if __name__ == "__main__"
函数里可以正常修改全局变量(其余函数不行
1 | x = 10 |
python无法对变量进行修饰,但可以通过@注解对函数进行修饰。例如@staticmethod,@classmethod
常见@注解
1 | @classmethod 装饰器定义类方法,允许方法访问类本身,而不是实例。 |
python的操作符按照优先级排序为
- 小括号
()
- 索引运算符
x[i] 或 x[i1: i2 [:i3]]
- 属性访问
x.attribute
- 乘方
**
- 按位取反
~
- 符号运算符
+(正号)、-(负号)
- 乘除
*、/、//、%
- 加减
+、-
- 位移
>>、<<
- 按位与
&
- 按位异或
^
- 按位或
|
- 比较运算符
==、!=、>、>=、<、<=
- is 运算符
is、is not
- in 运算符
in、not in
- 逻辑非
not
- 逻辑与
and
- 逻辑或
or
- 逗号
exp1, exp2
类似JAVA,在python中,object是所有类的基类。object类的方法
1 | __init__(self) 初始化方法,用于对象创建后初始化属性。 |
本文标题:编程语言(2)—类型和运算符
文章作者:Infinity
发布时间:2024-11-30
最后更新:2025-06-02
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!