C++-Rust-一次性掌握两门语言
简介
本文主要是通过介绍C++和Rust的基础语法达成极速入门两门开发语言。
C++是在C语言的基础之上添加了面向对象的类、重载、模板等特性和大量标准库以达到让使用者更高效地进行开发工作,其适用场景主要是游戏应用、游戏引擎、数据库等底层架构开发(而C更适合于系统内核、云搜索等算法和内存管理要求极高的程序)。
Rust则是吸取了Typescript、Python、C++、Go等各类前辈的语法特色创建出来的现代化语言,最重要的特点就是所有权与借用的特性,使之让很多内存问题在编译阶段甚至在编写代码时就能实时告知开发者,避免了众多运行时问题。
特色
C++最大的特点就是使用指针*和引用&来自由地操作内存地址与值,但开发者使用不当也很容易造成内存泄漏。
shell
void main() {
// 直接在栈上创建实例,好处是随着作用域结束而自动销毁,无需手动管理
User user;
// 在堆上创建实例并拿到指针,好处是将随着程序一直存在,当不使用后需要手动进行销毁
User *user = new User();
// 手动销毁,否则若程序一日未结束,则该内存块一直被占据
delete user;
}
Rust最大的特点是所有权与借用的概念,Rust中大部分变量要不通过引用来指向同个地址,要不是直接移动而非复杂变量,通过严格控制来减少内存泄漏的可能性。
rust
fn main() {
/// 直接在栈上创建实例
let mut user = User {};
/// 将执行移动,使user变得不可用
let user2 = user;
/// 提供一个只读引用指向user2相同的内存块,两者都可用,但user3只是可读,不可写
let user3 = &user2;
/// 提供一个可写引用指向user2相同的内存块,这会让user2暂时失效,直到user4的作用域结束,同一时间只能存在一个可写引用
let mut user4 = &mut user2;
/// 不允许存在另一个可写引用
// let mut user5 = &mut user2;
}
可以简单地理解为Rust做的各种赋值操作背后都是采用C++的std::move()函数将右值放入左值,或者将左值移动到左值,而C++自身如果不使用std::move()函数的话,赋值几乎都是直接复制(浅拷贝/深拷贝)一个新的,这样在释放内存时可能因为多次释放导致空悬指针,进而报错。
Rust还有另一特点是没有Null值,而是使用枚举Option::Some/None来指代结果可能为空的情况,类似于C++的std::optional可选值,Rust大量使用Option和基于此的Result来指定参数与返回值的可选性。
以下函数展示一个第二个值可能为空,根据情况进行运算并返回结果:
rust
use std::error::Error;
fn plus_val(value: i32, plus: Option<i32>) -> Result<i32, Error> {
match plus {
Option::Some(v) => {
if v == 1 {
return Error("plus can't be 1.")
}
value * v
},
Option::None => value * 2,
}
}
fn test() {
let result1 = plus_val(2, Option::None); // 2 * 2 = 4
let result2 = plus_val(2, Option::Some(4)); // 2 * 4 = 8
// 可以用match语法搭配枚举值处理
match result1 {
Ok(r1) => println!("{}", r1),
Err(error) => panic!("r1 error!")
}
// 也可以直接链式处理(此处只处理成功情况)
result2.and_then(|r2| println!("{}", r2));
}
数据类型
两者的数据类型相差不多,甚至可以说大部分开发语言的数据类型都是这些。
类型\语言 | C++ | Rust | 说明 |
---|---|---|---|
布尔型 | bool | bool | 可取值只有true和false,用于指代一些状态开关 |
有符号短整型 | short int | i32 | Rust使用字母i加上比特位数来表示有符号的整数类型,整型变量的默认类型 |
更短的有符号整型 | i8,i16 | ||
有符号整型 | int | i64 | C++最常使用的整型变量类型 |
有符号长整型 | long int | i128 | |
无符号整型 | unsigned int | u64 | |
无符号短整型 | unsigned short int | u32 | |
无符号长整型 | unsigned long int | u128 | |
更短的无符号整型 | u8, u16 | ||
字符 | char | char | 注意C++的字符是1~2个字节,而Rust的字符是1~4个字节,占据更大空间也意味着嫩表示更广的字符集或者更多字符 |
字符串(不可变) | const char* | &str | C++的不可变是通过指定一个常量字符指针指向字面量所在地址,而Rust的定义类似,是定义一个引用指向字面量地址,只不过依旧可以修改引用指向的地址,即重新赋值 |
字符串 | std::string | String | 可变字符串,可随时对内部字符进行增删改,注意Rust的String实际是对vec的封装 |
空值 | void | unit | 表示没有值、空值、无值的情况,常用于函数的返回值 |
定长数组 | int[4] 或者 std::array<int,4> | [i32;4] | 用于存放固定数量的相同类型值,这里都定义了固定数量4,类型为整型。由于长度在编译期就能确定,意味着可以预先分配好 |
元组 | std::tuple<T1,T2,...,Tn> | (T1,T2,...,Tn) | 用于存放若干个不同类型但相同用途的值,类似于数组版的结构体 |
其他比较常用的内置类型:
- 可变长度数组:C++为std::vector,Rust为vec,这是这类开发语言中更常用的数组类型,可变长度意味着更适用于实际场景根据输入进行长度变动。
- 哈希表:C++为std::unorder_map<KeyType, ValueType>,Rust为HashMap<KeyType, ValueType>,大部分算法或者要求唯一键名的用途都会大量使用哈希表。
声明常量、变量
C++属于传统的强类型编程语言,由于认为类型是必须在编译前就定好(静态类型),因此类型都是先于变量名、函数名之前指定的,如声明一个变量并给其赋予初始值:
shell
// 声明常量,需要在类型前面加上const关键字,类型为int
const int num1 = 1;
// 声明变量,类型为int
int num2 = 1;
Rust吸收了现代语言的特性,虽然依旧认为类型是必须的,但也尽可能通过自动推断减少开发者多余的类型定义工作:
rust
fn variable() {
// 声明常量,使用let关键字,此处自动推断类型为i32
let num1 = 1;
// 声明变量,在let关键字后添加mut关键字,此处自动推断类型为i32
let mut num2 = 1;
}
以上声明等同于let num1: i32 = 1;
,let mut num2: i32 = 1;
判断与循环
C++使用传统的if、else、switch作为判断,使用for、while作为循环:
shell
void handler(std::vector<unsigned int>& list) {
unsigned int max = 0;
for (auto *it = list.begin(); p != list.end(); p++) {
unsigned int temp = *it;
switch (temp) {
case 1:
temp = 2;
break;
case 3:
temp = 4;
break;
default:
break;
}
if (temp > max)
max = temp;
}
}
Rust除了有if、else、while、for之外,将switch换成了match减少了多余的语法,还有永久循环loop,并且都可以直接获取到返回值:
rust
fn handler(list: &[u32]) {
let max = 0;
for &item in list {
// 注意temp可以直接拿到返回值
let temp = match item {
1 => 2,
3 => 4,
_ => item
};
max = if temp > max { temp } else { max };
}
}
函数
C++定义函数是返回值类型在前,函数名称在后:
shell
void main() {
// 函数体
}
Rust遵从现代语言方式,用关键字表示这是一个函数声明,并将类型放在后面:
rust
fn main() -> unit {
// 函数体
}
抽象化的对象:类与接口
C++拥有struct结构体和class类,但没有interface接口,且这两者实际上除了一个默认声明为public公开,一个为private隐藏之外,并无其他区别。
C++一般是在.h头文件中声明结构体和类作为抽象化的用途。
头文件中定义结构体和类声明:
h
struct User {
std::string name;
bool enable;
unsigned int level;
};
// 定义接口
struct IUserController {
private:
User* user;
public:
virtual void init();
virtual User& getInfo();
};
cpp文件中实现类方法和使用结构体:
shell
// 实现接口,在类里如果没有说明,默认都是隐藏
class UserController: public IUserController {
User* user;
public:
void init() override {
delete user;
user = new User{ "Way",true, 1 };
}
User& getInfo() override {
return *user;
};
}
cpp文件中使用类:
shell
void main() {
UserController uCtrler;
uCtrler.init();
std::cout << uCtrler.getInfo() << std::endl;
}
Rust中虽然有结构体,但并没有类,结构体是同时作为结构体和类来使用的,同时提供了trait特征作为类似接口的存在提供各种抽象化。由于没有头文件的概念,需要自己分隔声明、实现、调用的文件结构。
定义结构体和类声明:
rust
// 声明结构体
struct User {
name: String,
enable: bool,
level: u64,
}
// 定义接口
pub trait GetInfo {
fn get_info() -> &User;
}
// 声明类
struct UserController {
user: User,
}
// 实现接口
impl GetInfo for UserController {
fn get_info(&self) -> &User {
&self.user
}
}
// 定义类方法
impl UserController {
fn init_user_info(&mut self) {
self.user = User { name: String::from("Way"), enable: true, level: 1 };
}
}
使用类:
rust
fn main() {
let u_ctrler = UserController { user: User { name: "Way", enable: true, level: 1 } };
u_ctrler.init();
println!("{}", u_ctrler.getInfo());
}
要注意特征和接口有不一样的地方,就是特征只能定义方法,不能定义属性/状态,即变量,是因为Rust更期望做好内存管理,如果这么定义那每个实现特征时都有大量额外内存开辟,不符合Rust对内存的严格管理。
而C++更自由地让开发者直接操作内存,主要也和C++的接口实际是通过抽象类与头文件声明来间接实现的,只要开发者知道自己在做什么C++就允许。
枚举
两者差别不大,不过C++的枚举是忽略枚举名称的,使用时是直接使用属性名:
shell
enum PlayerStatus {
PLAYER_MOVE,
PLAYER_STOP
}
void check_status(PlayerStatus status) {
std::cout << status == PLAYER_MOVE << std::endl;
}
Rust除了遵循枚举名::属性名的使用方式之外,还允许在内部继续定义结构体,类似Kotlin的密封类,这样可以让枚举完成更多更复杂的功能,方便封装状态操作等。
rust
enum PlayerStatus {
MOVE,
STOP,
OTHER {
name: String,
value: i8
}
}
impl PlayerStatus::OTHER {
fn equal(&self, &value: i8) -> bool {
self.value == value
}
}
fn check_status(status: PlayerStatus) {
println!("{}", status == PlayerStatus::MOVE);
}
模板与泛型
C++中只有一种类似于泛型但更加强大的特性:template模板,其不仅可以装填类型,还能附加常量值,使之能用于创建多个不同版本的类或函数。
以下例子使用模板创建一个包含长度为7的整形数组的类,并传递名称:
shell
template<T, Size>
class TheTemplate {
T *arr[Size];
std::string name;
public:
TheTemplate(std::string *_name, T *_arr[Size]): name(*_name), arr(_arr) {};
}
void create() {
TheTemplate week<int, 7>{"星期", new int[7]{1,2,3,4,5,6,7}};
TheTemplate month<int, 12>{"月份", new int[7]{1,2,3,4,5,6,7,8,9,10,11,12}};
}
注意该类是在编译期就根据已确定的模板创建多个副本类,抹除模板的存在,类似这样:
shell
class TheTemplate1 {
int *arr[7];
std::string name;
public:
TheTemplate1(std::string *_name, int *_arr[7]): name(*_name), arr(_arr) {};
}
class TheTemplate2 {
int *arr[12];
std::string name;
public:
TheTemplate1(std::string *_name, int *_arr[12]): name(*_name), arr(_arr) {};
}
void create() {
TheTemplate1 week{"星期"};
TheTemplate2 month{"月份"};
}
而Rust中泛型除了用于装载类型外,还能用于装载生命周期注解,并广泛用于创建不同类型的类与函数。
生命周期注解是Rust特有的具象化生命周期概念,其用单引号加小写字母的形式表示如'a,'b。生命周期注解并不会直接改变变量、参数、返回值本身的生命周期,而是类似类型一样对其本身的生命周期进行要求和约束。
以下同样是创建一个与上述例子相同的泛型结构体,并使用生命周期'a来约束入参与返回值应该具有的生命周期:
rust
struct TheGenerator<T> {
arr: [T],
name: String,
}
impl<T> TheGenerator<T> {
fn getWelcome<'a>(&self, word: &'a str) -> &'a String {
self.name + word
}
}
fn create() {
// 注意这里的泛型也可以不用填写,Rust可以根据初始值自动推断
let week = TheGenerator::<i8> {
arr: [1, 2, 3, 4, 5, 6, 7],
name: "星期",
};
let month = TheGenerator {
arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
name: "月份",
};
// 符合规则时不需要显示传递生命周期注解
let result = month.getWelcome("hello");
println!("{}", result);
}
以上的getWelcome方法中约束了出参的生命周期必须达到与入参一样长,要注意出参的生命周期并没有被缩短,还是按照原本的规则(这里的规则是出参使用了结构体的name属性,因此出参与结构体实例化后的生命周期一致,直到
Rust的泛型也是和C++的模板一样会在编译期就生成多个结构体副本,抹除泛型的存在。
Rust还有一个简化版的泛型约束,先看看原本约束方式:
rust
fn SeeMe<T: Add>(a: T, b: &T) -> T {
a + b
}
fn useMe() {
SeeMe(1, 2);
}
以上实现了对函数参数类型约束,要求两个参数都为T类型并且实现了Add相加的特征,也可以简写为以下方式:
rust
fn SeeMe(a: impl Add) -> impl Add {
a + 1
}
fn useMe() {
SeeMe(2);
}
要求泛型同时具有多种类型,除了直接使用加号<T: Add + Copy>
的方式外,还可以使用where从句:
rust
use std::fmt::Display;
fn some_fn<T, U>(a: &T, b: &U) -> i32 where T: Add + Copy, U: Add + Display + PartialEq {
if a > b {
println!("{}", *b);
}
a + b
}
Lambda匿名函数表达式
C++版,采用,类型可以使用std::function<ParamType,ReturnType>或者直接用auto表示:
shell
void lambda(int value) {
auto func = [](int)=>{ std::cout << x << std::endl; };
func(value);
}
Rust版,采用竖线开始并用于分隔参数列表和函数体:
rust
fn lambda(value: &i32) {
let func = |x: &i32| println!("fn!{}", x);
func(value)
}