从C、C++的视角来看Rust
原文链接:https://ovea-y.cn/looking_at_rust_from_the_perspective_of_c_and_cpp/
本篇文章用于有C、C++基础的读者快速入门Rust语言。
一、工具链
编译工具 gcc/clang、g++/clang++ -> rustc
构建系统 make/cmake/Bazel -> cargo cargo用于管理和辅助创建一个rust项目,它通过Toml配置文件进行项目管理。 它具备将上一次构建成功的状态记录到Cargo.lock的功能,这对于第三方crate(类似库)的版本管理非常有帮助,这意味着你在发布的每个版本只要存在Cargo.lock,就可以按照当时构建成功的配置进行构建(反例:Gradle、Maven和JDK、SDK、NDK甚至cmake等工具链存在不兼容的情况)。
编辑器增强 clangd/IntelliSense -> rust-analyzer 用于提供IDE、编辑器对RUST语言的支持能力,提供代码补全、跳转、重构等能力。
二、项目结构
一个Rust项目称为一个Projects,它包含package、crate、module结构。 一个package包含多个crate、但是只能有一个library crate(可以有无数多个binary crate)。 一个crate可以是library crate(特点包含lib.rs),也可以是binary crate(包含函数入口),也可以两者都是。它可以实现多个module。
三、基本语法
3.1 数据类型及定义方式
3.1.1 基本数据类型
其中C/C++的long、long int、long double、wchar_t类型存储的数据长度和操作系统及其位宽相关。
- Rust如果通过debug构建,默认带有溢出检查,当发生数值溢出后会panic终止程序。
- 如果希望在release版本也启用检查,需要overflow-checks = true配置。
- Rust程序中,一旦发生数组越界,就会触发panic终止程序。
Length | Signed | Unsigned | C/C++ Signed | C/C++ Unsigned |
---|---|---|---|---|
8-bit | i8 |
u8 |
char/signed char | unsigned char |
16-bit | i16 |
u16 |
short int/signed short int | unsigned short int |
32-bit | i32 |
u32 |
short int/signed int | unsigned int |
64-bit | i64 |
u64 |
long/long int/signed long int/long long | unsigned long |
128-bit | i128 |
u128 |
||
arch | isize |
usize |
||
32-bit | f32 |
float | ||
64-bit | f64 |
double | ||
128-bit | long double | |||
32-bit | char |
wchar_t | ||
8-bit | bool |
bool |
Rust默认整型数据类型为i32,默认浮点数数据类型为f64。
// 可以在定义数据类型的时候不指定数据类型(此处默认是i32类型)
let a = 32;
// 也可以指定其对应的数据类型
let a: i64 = 32;
let z: char = 'ℤ';
3.1.2 复合数据类型
3.1.2.1 元组(Tuple)类型
该数据类型是Rust特有,C/C++不具备的数据类型。 元组可以将多种不同的数据类型组合形成一个复合结构,一旦定义后就无法再增长或缩减。
// 可以让编译器自动推断数据类型
let tup = (500, 6.4, 1);
// 也可以自己写明数据类型
let tup: (i32, f64, u8) = (500, 6.4, 1);
// 拆解元组的方式
let (x, y, z) = tup;
// 元组的使用方式
let five_hundred = x.0;
3.1.2.2 数组(Array)类型
数字可以存储一系列同数据类型的数据,定义后具有固定的长度。
// 定义和初始化
let a = [1, 2, 3, 4, 5];
// 指定数据类型和长度,并初始化
let a: [i32; 5] = [1, 2, 3, 4, 5];
// 初始化缩写写法,相当于let a = [3, 3, 3, 3, 3];
let a = [3; 5];
下面是C/C++定义数组的方式
int a[5] = {1, 2, 3, 4, 5};
// 动态数组定义,需要通过delete进行删除
int *a = new int[100];
delete []a;
// 初始化前两个元素,其他元素全部默认为0
int *a = new int[100]{ 3, 5 };
// 初始化所有元素为0,但是并不是所有编译器都支持
int a[5] = { 0 };
3.2 宏定义
rust的宏和C/C++的宏一样,都是在编译前进行展开和替换,然后再进行编译步骤。 #define -> macro_rules!
C/C++的宏定义写法如下,定义一个字符串字面量
#define TASK_STATUS_TEMPLATE "/proc/{}/status"
{
printf("%s\n", TASK_STATUS_TEMPLATE);
}
Rust的写法如下,定义一个字符串字面量
macro_rules! TASK_STATUS_TEMPLATE { () => { "/proc/{}/status" } }
{
println!("{}", TASK_STATUS_TEMPLATE!());
}
我们可以注意到,println!也是一个内置宏,它会在编译前进行展开,转换成方法调用。
#[macro_export]
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), rustc_diagnostic_item = "print_macro")]
#[allow_internal_unstable(print_internals)]
macro_rules! print {
($($arg:tt)*) => {{
$crate::io::_print($crate::format_args!($($arg)*));
}};
}
#[macro_export]
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), rustc_diagnostic_item = "println_macro")]
#[allow_internal_unstable(print_internals, format_args_nl)]
macro_rules! println {
() => {
$crate::print!("\n")
};
($($arg:tt)*) => {{
$crate::io::_print($crate::format_args_nl!($($arg)*));
}};
}
3.3 方法定义
rust的入口函数默认是main。
通过std::env::args()
可以获取到程序传入参数,通过skip函数跳过了第一个路径参数。
- Rust定义函数的关键词是
fn
- Rust的函数定义可以在任何位置,不必像C/C++一样,调用之前,必须先声明函数。
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
// {:?}可以将args的信息详细输出,以debug的格式
println!("{:?}", args);
another_function();
print_labeled_measurement(five(), 'h');
}
fn another_function() {
println!("Another function.");
}
// 定义方法的参数,必须有明确的数据类型
fn print_labeled_measurement(value: i32, unit_label: char) {
// rust格式化输出有两种方式,一种是{value}这样直接使用变量名
// 另一种是("{}", value)这种方式,其中{}中可以添加格式化的方式
// 比如{:.3},它的作用是输出浮点数时保留小数点后三位
println!("The measurement is: {value}{unit_label}");
}
// 定义方法的返回参数,也必须有明确的数据类型
// 返回值类型写在->后面
// 这里可以发现,5后面没有`;`,在rust中,最后一条语句没有分号,那么它的值将会被返回
// 也可以通过return 5;返回
fn five() -> i32 {
5
}
C/C++的方式
void another_function() {
printf("Another function.\n");
}
int five() {
return 5;
}
void print_labeled_measurement(int value, char unit_label) {
printf("The measurement is: %d%c\n", value, unit_label);
}
int main(int argc, char *argv[]) {
for(int i = 0; i < argc; ++i) {
std::cout << "Argument [" << i << "] is " << argv[i] << std::endl;
}
another_function();
print_labeled_measurement(five(), 'h');
}
3.4 流程控制
- 需要注意,rust的流程控制条件必须是bool类型,其他数据类型均会编译出错。
3.4.1 if
和C/C++的使用方式类似,但是rust有其特有的方式,if - else也可以返回数据,并且赋值给变量。 if - else的返回值必须是同数据类型,否则无法通过编译。
下面是rust的方式
let n = if condition { 5 } else { 6 };
if number < 5 {
println!("condition was true");
} else if number % 3 != 0 {
println!("condition was false");
}
下面是C/C++的方式
// 这种使用方式,rust不支持
int n = condition ? 5 : 6;
if (number < 5) {
printf("condition was true\n");
} else if (number % 3) {
printf("condition was false\n");
}
3.4.2 loop
Rust具备其特有的循环关键词loop
,loop也可以返回值赋值给变量,也可以进行跳转。
loop {
println!("again!");
}
// 在循环中满足状态后返回,并将值传递给变量
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
// 在循环中,还可以使用类似C/C++ goto的语法
// 在break的时候,可以跳到对应的label位置。
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
C/C++的方式
while (true) {
printf("again!\n");
}
int result = 0;
for (;;) {
counter += 1;
if (counter == 10) {
result = counter * 2;
break;
}
}
counting_up:
while (1) {
printf("count = %d\n", count);
int remaining = 10;
for (;;) {
printf("remaining = %d\n", remaining);
if (remaining == 9) {
break;
}
if (count == 2) {
goto counting_up;
}
remaining -= 1;
}
count += 1;
}
3.4.3 while
rust的写法。rust禁止使用number–的语法。
while number != 0 {
println!("{number}!");
number -= 1;
}
C/C++的写法
while (number) {
printf("%d!\n", number);
number--;
}
3.4.4 for
Rust的for可以遍历数组、vec等数据结构。
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
C/C++的写法:
int a[] = {10, 20, 30, 40, 50};
for (auto element : a) {
printf("the value is: %d\n", element);
}
for (size_t number = 3; number >= 1; number--) {
printf("%d!\n", number);
}
printf("LIFTOFF!!!\n");
3.5 结构体 - struct
Rust的结构体,可以存储一系列数据结构,类似于C++中的struct(默认权限public)或class(默认权限private)。
下面是定义一个用于描述矩形的struct
// 加入#[derive(Debug)]之后的struct,可以通过{:?}输出debug信息。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// 此处表明我们需要为Rectangle结构体添加一个新方法
// 可以多处进行impl进行添加,无需放在一起
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let scale = 2;
// struct在初始化后,默认存放在栈中。
let rect1 = Rectangle {
// dbg宏使用后,会在终端输出相关变量的调试信息,不会中断程序执行
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
println!( "The area of the rectangle is {} square pixels.", rect1.area() );
}
以下是C++的第一种写法。
class Rectangle {
public:
unsigned int width;
unsigned int height;
Rectangle(unsigned int w, unsigned int h) : width(w), height(h) {};
unsigned int area() const {
return width * height;
}
bool can_hold(const Rectangle& other) const {
return width > other.width && height > other.height;
}
};
int main() {
int scale = 2;
Rectangle rect1(30 * scale, 50);
std::cout << "width: " << rect1.width << ", height: " << rect1.height << std::endl;
std::cout << "The area of the rectangle is " << rect1.area() << " square pixels.\n";
return 0;
}
以下是C++的第二种写法
struct Rectangle {
unsigned int width;
unsigned int height;
// 构造函数
Rectangle(unsigned int w, unsigned int h) : width(w), height(h) {}
// 计算面积的成员函数
unsigned int area() const {
return width * height;
}
// 判断是否可以容纳另一个Rectangle的成员函数
bool can_hold(const Rectangle& other) const {
return width > other.width && height > other.height;
}
};
int main() {
int scale = 2;
Rectangle rect1(30 * scale, 50);
std::cout << "width: " << rect1.width << ", height: " << rect1.height << std::endl;
std::cout << "The area of the rectangle is " << rect1.area() << " square pixels.\n";
return 0;
}
3.6 枚举 - enum
Rust的枚举类型和struct一样,也可以拥有自己的方法,并且还可以存储不同的数据类型。 同时可以通过match关键词,来对枚举数据类型进行自动匹配。 而C++不行。
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn describe(&self) {
match self {
Message::Quit => {
println!("Quit");
}
// 结构体需要解构
Message::Move {x, y} => {
println!("Move to {}, {}", x, y);
}
// 其他成员可以直接匹配
Message::Write(text) => {
println!("Text message: {}", text);
}
// `_`表示其他类型
_ => {
println!("Other");
}
}
}
}
// Rust常用的Option和Result也是枚举类型
// 此处的T代表范型
enum Option<T> {
None,
Some(T),
}
pub enum Result<T, E> {
Ok(T),
Err(E),
}
C/C++的枚举类型,默认都是整形。
enum MyEnum {
A = 1,
B = 2,
// C会自动沿用前一个枚举类型的值+1
C,
D = 10
};
3.7 trait
Rust中的trait代表的是一类相同的行为,类似于“接口”。 一个trait可以定义一组方法,并且可以具有默认实现,这些方法可以在任何数据类型上实现。 我们可以为任意数据结构实现一个trait包含的方法,该方法会替代默认实现被使用。
Rust通过trait实现类似C++运算符重载的机制(但是Rust本身没有重载机制)。
Display
、Copy
、PartialOrd
等是Rust标准库中定义的一些trait,在Rust中有特殊的含义和作用。
-
Display
:这个trait提供了一个方式来格式化输出到用户的字符串。例如,当你想打印一些值或是把这个值转換成一个字符串来显示给用户,就必须实现Display
trait。 -
Copy
:这个trait表示一种类型的值可以进行“堆栈复制”,即这个类型的值可以用简单的按位复制来製作新的实例,并且這並不會使原值失效。所以一旦我们使用了这个值,它就会产生一份拷贝。原生类型(例如i32、f64等)都是可复制的,自定义类型需要显式实现Copy
trait。 -
PartialOrd
:这个trait用于比较两个值的大小。它定义了一个partial_cmp
方法,比较自身和另一个同类型的值,返回一个Option<Ordering>
实例。这个实例可以是Some(Less)
、Some(Equal)
或Some(Greater)
,也可以是None
(表示两个值不能进行比较)。大多数类型都实现了PartialOrd
trait,以便于我们进行比较操作。例如,所有整型和浮点型都实现了这个trait。 -
Debug
:这个 trait 用于支持调试输出,它提供了一个格式化输出至开发者的字符串。如果你使用{:?}
或{:#?}
进行输出,那你的类型就需要实现Debug
trait。 -
Eq
和PartialEq
:PartialEq
trait 用于比较两个类型的各个实例是否相等(使用==
或!=
符号)。可以看作一种 relaxed 的等价观念,因为它也可以用于 float 类型,比如 NaN 是不等于 NaN 的。而Eq
trait 则表示一种更强的“等价”观念,如果 a == b 和 b == a 总是相等,那一般就实现它,它是一个“空”(marker)trait,表示满足某种性质。 -
Ord
:这个 trait 是PartialOrd
的超集,用于完全排序的类型,它通过cmp
方法返回一个Ordering
(而不是Option<Ordering>
)。也就是说,对于实现了Ord
的类型的任意两个值,我们总是可以确定一个明确的排序关系。 -
Default
: 用于创建一个类型的默认值。 -
Clone
: 这个trait表示一个对象可以准确复制一份出来,和Copy
不同之处在于,当Copy
发生时原始值仍然有效,而Clone
通常和所有权、生命周期有关。显式调用clone()
方法可以获取类型的一个全新对象,这通常发生在复杂类型,比如Vec
、String
或者拥有 heap 内存空间等。 -
Deref
andDerefMut
:这两个trait用于重载解引用运算符(*),分别用于不可变和可变的引用。 -
除此之外还有很多其他的trait可以用,比如实现std::ops::Add将可以覆盖
+
的行为。
这些trait的存在使得Rust的类型系统更加灵活,我们可以通过实现不同的trait对自定义类型进行定制,使得这些类型具有更丰富的功能。
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
// 可以为一个数据结构,同时实现多个trait,此处没有进行实现。
// 此处是通过trait进行限制,要求范型T实现了Display和PartialOrd这两个trait,才允许执行cmp_display函数
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
四、所有权和生命周期
Rust和其他语言很多不同之处,其中最大最重要的一点就是其所有权机制和生命周期的概念。
在Rust中,堆上的数据,都有指向其的指针存储在栈上,而指针没有实现Copy trait,因此赋值是move语义。一旦指针的生命周期结束,存放在堆上的数据将会被自动释放。
4.1 可变性
在Rust语言中,所有的变量默认都是不可变的。 如果想在后面对其进行变更,就需要mut这个关键词修饰。
基本数据类型,都实现了Copy这个trait,所以当发生赋值的时候,所有权并不会转移,而是创建一个新的变量并赋值
如果数组存放的数据类型是实现了Copy trait的,那么它的再赋值也是拷贝,而不是移动。
// 不可变变量在后面无法再被赋值,发生赋值行为会导致编译检查失败
let apples = 5; // immutable
// 可变变量可以在后面重新赋值
let mut bananas = 5; // mutable
// 此时apples依旧可以被使用,它并未发生所有权转移。
let oranges = apples;
let a = [0; 100];
// 此时数组a依旧可以被使用,因为其存储元素实现了Copy trait,因此赋值给b是创建新的数组并复制了一份。
let b = a;
4.2 堆和栈的使用
上面提到了实现了Copy trait的数据类型数组赋值的时候,会创建一个新的数组,并进行数据拷贝。
数组默认是存储在栈上的。
但是有些情况下(比如减少内存使用),我们并不希望它进行数据拷贝,而是进行转移(move)。
在这种情况下,我们可以将数据存储到堆上。
在Rust中,所有在堆上的数据,都有指向其的指针存储在栈上,而指针没有实现Copy trait,因此赋值是move语义。
一旦指针的生命周期结束,存放在堆上的数据将会被自动释放。
我们可以通过Box将数组分配到堆中。
C/C++中自身不具备自动释放堆上内存的能力,但是可以通过标准库中的智能指针实现(std::unique_ptr
和 std::shared_ptr
)。
4.3 遮蔽(Shadowing)
遮蔽可以让一个同名变量重复被使用。
fn main() {
let x = 5;
// 此处赋值后,上面的x将会失效
let x = x + 1;
{
// 此处x在离开花括号后失效
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}
C/C++也有遮蔽的功能,但是不允许在同一个作用域中进行遮蔽。 下面这种行为是不允许的。
int main() {
int x = 5;
// 此处会编译报错
int x = x + 1;
}
4.4 引用和借用(References and Borrowing)
Rust在进行方法调用时,也会发生所有权的转移,比如下面这段代码。
fn main() {
let m1 = String::from("Hello");
let m2 = String::from("world");
greet(m1, m2);
let s = format!("{} {}", m1, m2); // Error: m1 and m2 are moved
}
fn greet(g1: String, g2: String) {
println!("{} {}!", g1, g2);
}
当后面像通过format宏进行格式化的时候,m1和m2的所有权已经在转移到了greet函数内。其生命周期也因此被重新限制在了greet函数内,并在执行完成后被释放了。
如果我们想要m1、m2对应的堆数据不被释放,那么我们需要在函数执行结束后将所有权再转移回去。
fn main() {
let m1 = String::from("Hello");
let m2 = String::from("world");
let (m1_again, m2_again) = greet(m1, m2);
let s = format!("{} {}", m1_again, m2_again);
}
fn greet(g1: String, g2: String) -> (String, String) {
println!("{} {}!", g1, g2);
(g1, g2)
}
这种方式无法保证传入变量的生命周期,不受到方法影响。 因此我们能否将变量的值借用给方法,而不将所有权传递给方法呢? 这里就可以用到Rust的引用了。
fn main() {
let m1 = String::from("Hello");
let m2 = String::from("world");
greet(&m1, &m2); // note the ampersands
let s = format!("{} {}", m1, m2);
}
fn greet(g1: &String, g2: &String) { // note the ampersands
println!("{} {}!", g1, g2);
}
&m1 这样的语法,就是创建一个m1的引用(或借用),我们可以看到greet方法对形参中借用的声明是&String,关键是
&
。
C/C++中可以通过传递指针或传递引用的方式给一个函数。 指针和引用的区别只有几点
- 指针赋值后,可以被修改,但是引用一旦赋值,就无法再被修改了。
- 引用不能是空值。
- 指针可以算数运算,而引用不行。 除此之外,使用方式区别不大。
4.5 解引用指针访问堆上数据
和C++一样,可以通过*
对一个指针解引用,访问其堆上数据。
let mut x: Box<i32> = Box::new(1);
let a: i32 = *x; // *x 读取堆上的值, 因此 a = 1
*x += 1; // *x 通过x直接修改堆上的数值,此时x指向的内存数据变成2
let r1: &Box<i32> = &x; // r1 在栈上指向x
let b: i32 = **r1; // 我们对r1进行两次解引用,才可以访问到堆上内存
let r2: &i32 = &*x; // r2 直接指向堆上内存
let c: i32 = *r2; // 因此r2进行单次解引用就可以访问堆上的内存
4.6 借用引起访问权限变化
Rust中,借用检查器的核心思想是变量存在三种权限。
- Read (R): 数据可以被拷贝到其他位置
- Write (W): 数据可以被直接修改
- Own (O): 数据可以被移动或丢弃 默认情况下,Rust中的变量默认具有RO权限,如果添加了mut关键词,将会具备RWO属性。
4.6.1 非可变借用
- 第一行v复制后,获得RWO权限
- 第二行v中的数据被num借用,此时v将失去WO权限,它后面只能被读取。
- 变量num获取到RO权限,但是num不可写,因为没有mut关键词修饰。
- 第三行*num在println宏使用完之后,后续没有再使用到num的地方了,因此num的借用权限被取消,同时num重新获得WO权限。
- 第四行执行结束后,v变量不再被使用了,因此它将会被丢弃,并且失去所有的权限。
4.6.2 可变借用
和不可变借用不同的是,当使用可变借用之后,被借用数据结构将失去所有权限。在借用使用结束之前,都无法被读写。
4.7 切片(Slice)
let s = String::from("hello world");
let hello: &str = &s[0..5];
let world: &str = &s[6..11];
let s2: &String = &s;
切片后的权限变化。
五、常用Collections
5.1 Vec
类似动态数组
let v: Vec<i32> = Vec::new();
let v = vec![1, 2, 3];
v.push(5);
v.push(6);
v.push(7);
v.push(8);
for n_ref in &v {
// n_ref has type &i32
let n_plus_one: i32 = *n_ref + 1;
println!("{n_plus_one}");
}
5.2 String
字符串类型,string默认存储的是UTF-8类型
let mut s = String::new();
let data = "initial contents";
let s = data.to_string();
// the method also works on a literal directly:
let s = "initial contents".to_string();
let s = String::from("initial contents");
5.3 HashMap
存储键值对的方式。
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
六、智能指针
Box<T>
: 分配数据在堆上Rc<T>
: 引用技术类型的指针,允许多个所有权Ref<T>
和RefMut<T>
,或者RefCell<T>
七、示例项目
linux: https://github.com/luoqiangwei/LinuxProcessTrace
android: https://github.com/luoqiangwei/AndroidProcessTrace
参考链接
https://rust-book.cs.brown.edu/experiment-intro.html
原文链接:https://ovea-y.cn/looking_at_rust_from_the_perspective_of_c_and_cpp/