rust也是一门编译语言,而且很新,是二零一几年出的一个语言。但是rust的内存管理非常的安全,而且有堪比C++的运行效率
该rust学习笔记不适合纯新生或者是刚入门的新手,本人学习rust的主要目的是学习一下rust逆向和rust的pwn,更偏向于其内存管理等底层机制
rust环境
一开始入门不打算用IDEA开发效率高的环境,想一行一行敲,感觉这样基础更牢,所以就在Linux下搭建了rust环境。
在Linux下安装rust环境感觉是比在windows上容易的多,直接就是输入指令
1 2 3 sudo apt update sudo apt install rustc sudo apt install cargo
1 2 rustc --version cargo --version
先创建并编写一个简单的rust程序,先创建main.rs
文件,然后再编写如下内容
1 2 3 4 5 6 7 8 9 fn main (){ let a = 12 ; println! ("a is {0}" ,a); println! ("a is {},a again is {}" ,a,a); println! ("a is {0},a again is {a}" ,a); println! ("a is {{}}" ); println! ("this is a \n huan hang fu" ); println! ("look" ); }
使用rustc
编译,rustc main.rs
,然后运行编译后的文件
Cargo的使用
cargo介绍
Cargo是Rust编程语言的包管理系统和构建系统。它用于管理Rust项目的依赖项、编译代码、运行测试、构建项目和发布库。可以类似于Python的pip
包管理器
接下来介绍一下cargo的一些常用的命令
cargo new
1 2 3 4 5 6 7 cargo new 项目名 --bin //创建一个可执行程序 cargo new hello_world --bin cargo new 项目名 //编写一个可以被其他项目引用的库和包 cargo new hello_world
使用cargo new hello_world --bin
,创建一个可执行程序它的目录如下
其中Cargo.toml
,是由Cargo
生成的配置文件,用于定义项目的元数据、依赖性、编译配置等。
src
:里面存放的是源代码,src
里面会生成一个main.rs
文件,.rs
后缀的文件就是rust
的源代码文件
1 2 3 4 hello_world --Cargo.toml(文件) --src(目录) --main.rs
接下来查看一些cargo.toml
文件里面的具体信息,使用vim cargo.toml
命令查看该文件里面的内容
name
:表示包的名字
version
:项目版本号
edition
:Rust的版本
1 2 3 4 5 6 7 8 [package] name = "hello_world1" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies]
注意 :使用cargo new hello_wold
命令生成的项目,目录也是与上方一样
cargo build
cargo build
命令是编译使用cargo new
所创建的项目
直接在所创建的项目内使用cargo build
命令编译即可
1 2 3 4 5 myheart@aaa ~/c/r/hello_world1 (master)> ls Cargo.toml src myheart@aaa ~/c/r/hello_world1 (master)> cargo build Compiling hello_world1 v0.1.0 (/home/myheart/code/rust/hello_world1) Finished dev [unoptimized + debuginfo] target(s) in 0.92s
使用cargo build
命令后,项目的目录结构会发生改变,编译后基本上就是这样的目录,具体的了解即可。重点就是为可执行二进制文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 hello_world --Cargo.lock(文件) --Cargo.toml(文件) --src(目录) --main.rs(源代码) --target(目录) --CACHEDIR.TAG(文件) --debug(目录) --build(目录) --deps(目录) --hello_world-65c478c245451323 --hello_world-65c478c245451323.d --examples(目录) --hello_world(可执行二进制文件) --hello_world.d(文件) --incremental(目录) --hello_world-2raufob2y547t(目录) --s-h1czdyejl1-16wthvs-37ja4u7549aastc5b8qe1nrmn(目录) --24qepgxx259x3ntg.o --4hcybahg0o19qhm4.o --2i7b77ao4v5unib8.o --540zf6nxv88tkrze.o --3ohs3puklkwxl3d3.o --c4bpplywo62iusm.o --dep-graph.bin --query-cache.bin --work-products.bin --s-h1czdyejl1-16wthvs.lock(文件)
rust hello_world
内容
1 2 3 fn main (){ println! ("hello world!" ); }
使用cargo build
,将该源代码编译,也可以直接使用rustc
编译编译,使用rustc ./main.rs
编译后运行即可就会出现如下输出:
1 2 myheart@aaa ~/c/r/h/src (master)> ./main hello world!
接下来分析一下该程序:
首先fn main()
fn
定义函数的关键字
main()
,就代表main
函数,当运行该程序的时候,该程序就会从main函数这边开始运行
如果声明函数的要参数的话,该参数必须在()
内部被声明
{}
花括号就是函数体,rust要求所有函数体都要被花括号包裹起来
然后println!()
函数:这个是输出函数,即打印函数,会将字符给打印到屏幕中。
在这里需要强调:rust风格使用四个空格进行缩进,而不是使用Tab键
然后println!
是一个宏,如果单纯调用一个函数,那么就不会有!
,即正常函数是这样的println()
rust
中所有以!
结尾的调用都意味着是使用宏而不是普通的函数
然后rust
代码行都会以分号来结尾。
1 println!(“hello world!”);
总结
关于rust编程风格
rust要使用fn
关键字定义函数
rust要使用{}
来表示函数体,rust要求所有函数都要被花括号包裹起来
rust中缩进为四个空格
,而不是Tap
键
rust代码行都会以分号来结尾;
rust中程序以main函数为程序的入口点
补充一点:rust
语言也是使用//
表示单行注释,
项目-猜数字
创建项目
然后我们先使用cargo run
命令将该项目编译运行一下,该命令可以用于快速迭代一个项目
编写项目
第一部分 基本结构
cargo run
运行完之后就可以,可以使用编写程序了,编写main.rs
该程序实现的是一个猜数字游戏 ,这是该游戏的第一部分,该部分会请求用户进行输入,并检查输入是否满足预期的格式。
1 2 3 4 5 6 7 8 9 10 11 12 13 use std::io;fn main () { println! ("Guess the number!" ); println! ("Please input your guess." ); let mut guess = String ::new (); io::stdin ().read_line (&mut guess).expect ("Failed to read line" ); println! ("You guessed: {}" ,guess); }
第一部分解释
导入包
use std::io
,就是导入std
标准库中的io
模块,可以类比于Python的import io
(虽然类比于C++会更合适)。
rust一开始会导入一些模块,该模块的作用域为每一段程序。这个导入的模块只包含了一小部分。
但是有些函数或者类没有在该模块中我们就要使用use
语句,导入其他的库和模块,就比如use std::io
。
std::io
这个模块在该部分的作用是获得用户输入的数据,如果我们没有use std::io
这个语句,当我们使用io::stdin
时就不能这么写,而是要写成std::io::stdin
声明变量
let mut guess
:该语句是声明一个可变的变量,let
是声明一个变量guess
,mut
(mutable)表示这个guess
变量是可变的。并且程序会在栈内存中开辟一个空间,来存储guess
变量的值。
这里我们深刻来理解一下mut
,这个使得变量可变的关键字
当我们使用let foo = 5
时,该变量是不可变的,即之后的代码中,我们不能再将foo
赋值给其他值。这里我们进行一个lab。当我们使用rustc lab1.rs
编译时,会出现如下的报错
1 2 3 4 fn main (){ let a = 5 ; a = 6 ; }
当我们使用let mun a=5
时就可以对a
进行修改
1 2 3 4 5 fn main () { let mut a = 5 ; a = 6 ; println! ("a 的值是: {}" , a); }
这时我们再编译就只会出现警告,而不是报错,可以正常编译,编译后也能正常运行
创建实例
let mut guess = String::new
,该语句中String::new
,会创建一个String
的实例。
String
是标准库中的一个字符串类型,它在内部使用UTF-8
格式编码,并可以按需求扩展自己的大小
new
是一个关联函数,并不是独立函数,它的作用创建某一类型的一实例。new
会开辟一个堆内存空间,使用该函数开辟的堆内存空间是被自动管理的。并不需要我们手动管理
String::new
,就是将字符串类型与new函数关联起来,开辟一个字符串类型的堆内存空间,返回的是一个字符串对象。可以用处理字符串的方法来处理该对象。
而let mut guess = String::new
,表示guess
指向一个字符串对象,调用该变量可以打印出该字符串对象里面的字符串。
注意:可以简单理解guess指向该对象,但是并不是String::new
返回值是单纯的堆地址,面向对象的内存管理抽象化了,是自动管理内存的。并不是像C语言那样将内存具体化,让开发者手动管理内存。(可以试着打印一些guess的值,它不会打印出地址,而是在该对象没有任何内容的时候会输出空。)
let mut guess = String::new
,运行该语句的内存运行机制。面向对象的,可以看做是对结构体的一种扩展和运用。对于字符串这个对象,在rust
语言中会是如下的结构体。而guess
变量是指向一个字符串实例,所以guess
变量存储着一个指针。
而我们在一个对象中有特定的方法可以操作该对象,可以理解为有专门的函数可以处理该结构体。
1 2 3 4 5 struct String { ptr: *mut u8 , len: usize , capacity: usize }
输入处理
io::stdin
会返回std::io::stdin
的实例,该实例中保存了许多方法来处理终端的标准输入。比如.read_line
这个方法就是用来读取用户的一行标准输入
io::stdin().read_line(&mut guess)
,这行代码中.read_line(&mut guess)
里面的&mut guess
,是引用传参,&
意味着当前参数是一个引用。
引用操作的是与传递的参数同一块的内存,而不是像普通传参一样,复制一份对我们所复制的进行修改
注意 :引用的参数与变量一样默认情况下是不可变的,所以我们需要使用mut
关键字,表示该引用的参数是可变的
异常处理
io::stdin().read_line(&mut guess).expect("Failed to read line");
这行代码其实是这么长
当我们使用.read_line
方法的时候,使用结束后该方法会返回io::Result
的值,Result
是一个枚举类型,拥有Ok
(字母O)和Err
两个变体。执行成功Result的值为Ok
,而在执行过程中出错,就会Result的值就为Err
,并且附带失败的原因
而expect
方法就是用来处理Result
的这个枚举类型,当expect
接收到为Err
,那么就会中断程序,并将传入的字符串参数显示出来。如果接收到为OK
那么就会提取OK中附带的值,并返回给用户。
注意 :如果没有expect
这种异常处理,仍然可以通过编译,但是编译器有警告提示
println!占位符
最后这句println!("a 的值是: {}", a)
可以将存储的用户输入打印出来。
第一个参数是用来格式化字符串的,而花括号就相当于占位符{}
,可以将后面的参数值插入用{}
所占的位置,第一个花括号对应的第一个参数,第二个花括号对应的是第二个参数,以此类推。
接下来简单再来演示一下花括号的占位符,这个的字符串输出结果就会被变量a
、b
、a+b
的值给取代了。格式化字符串输出这个内部原理,可以上网了解一下C语言printf函数的内部实现。
1 2 3 4 5 fn main(){ let a = 10; let b = 20; println!("{}+{}={}",a,b,a+b); }
第二部分 随机数生成
为了增加游戏的可玩性,我们将使用随机数来生成一个数字,然用户去猜测。
在rust中,并没有把随机数生成函数,内置到标准库中,而是作为外部的rand
包提供给用户
rust中的包(crate)代表了一系列源代码文件的集合
我们当前正在构建的项目是一个用于生成可执行程序的二进制包
rand
包是一个用于重复功能的库包
Cargo
之前有说过类似于python中的pip
,是用来帮助我们管理和使用第三方库和包,所以在使用rand
这个包的时候就先要使用Cargo
导入rand
包。
我们先在该目录下对Cargo.toml
这个文件进行编辑
在dependencies
下方导入rand
包,即添加一行rand="0.3.14"
。
rand="0.3.14"
表示任何与0.3.14版本公共API相兼容的版本
修改完保存后,不要修改任何rust代码,直接使用cargo build
重构这个项目,Cargo
就会下载rand
包到本地中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 use std::io;use rand::Rng;fn main () { println! ("Guess the number!" ); let secret_number = rand::thread_rng ().gen_range (1 ,101 ); println! ("The secret number is: {}" , secret_number); println! ("Please input your guess." ); let mut guess = String ::new (); io::stdin ().read_line (&mut guess).expect ("Failed to read line" ); println! ("You guessed: {}" ,guess); }
第二部分解释
use rand::Rng
:这里的Rng
是一个train(特征) ,它定义了随机数生成的方法集合,这使得我们可以访问随机数生成的方法。
let secret_number = rand::thread_rng().gen_range(1,101)
,这段代码中
rand::thread_rng()
会返回一个特定的随机数生成器,位于本地线程空间,并通过操作系统获得随机数种子,之后就利用gen_range
这个方法生成随机数
gen_range
这个方法是接收俩个数字做为参数,并生成包含下限但是不包含上限的随机数,在本代码中参数为(1,101)
就会生成1~100
之间的随机数
运行该代码后就会出现如下结果:
第三部分 比较和隐藏数字
既然是猜数字,就不能让被猜的数字显示出来
注意该代码编译时会报错,无法编译出二进制程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 use std::io;use std::cmp::Ordering;use rand::Rng;fn main () { println! ("Guess the number!" ); let secret_number = rand::thread_rng ().gen_range (1 ,101 ); println! ("The secret number is: {}" , secret_number); println! ("Please input your guess." ); let mut guess = String ::new (); io::stdin ().read_line (&mut guess).expect ("Failed to read line" ); println! ("You guessed: {}" ,guess); match guess.cmp (&secret_number){ Ordering::Less => println! ("Too small!" ), Ordering::Greater => println! ("Too big!" ), Ordering::Equal => println! ("You win!" ), } }
第三部分解释
use std::cmp::Ordering
,该段代码从标准库引入了std::cmp::Ordering
的类型。Ordering
也是枚举类型,有Less
、Greater
、Equal
3个变体,用于表示比较数字后的结果
match guess.cmp(&secret_number)
:
guess.cmp
中cmp
方法,该方法接收secret_number
的引用,然后返回secret_number
与guess
比较后的结果。返回的结果就是Ordering
枚举类型
match
表达式就用来匹配返回的结果,从而决定下一步应该执行什么代码
假设我们要猜的数是38
,而我们猜50
,比较返回的结果就是Ordering::Greater
变体。
match就会去按顺序匹配对应变体,先匹配Ordering::Less
,不是该变体就不执行println!("Too small!")
然后就会接下去匹配Ordering::Greater
变体,匹配成功就执行println!("Too big!")
,执行完后就退出匹配了
如果要隐藏代码就直接注释掉println!("The secret number is: {}", secret_number);
,在该段代码的前面添加//
尝试编译第三部分代码,发现我们有一处错误,就是secret_number
是整型,而guess
是字符串对象,他们无法比较
第四部分 类型转换
rust有一个静态强类型系统,同时还有自动进行类型推导的能力。
当我们使用let guess = String::new()
时,rust会自动将变量推导为String
而secret_number
可能是i32
(32位整数)、u32
(32位无符号整数)、i64
(64位整数),rust会默认将secret_number
视作i32
但是i32
和String
无法直接对比,所以编译器会报错
所以为了正常比较,应该将guess
转成u32
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 use std::io;use std::cmp::Ordering;use rand::Rng;fn main () { println! ("Guess the number!" ); let secret_number = rand::thread_rng ().gen_range (1 ,101 ); println! ("The secret number is: {}" , secret_number); println! ("Please input your guess." ); let mut guess = String ::new (); io::stdin ().read_line (&mut guess).expect ("Failed to read line" ); let guess : u32 = guess.trim ().parse ().expect ("Please type a number!" ); println! ("You guessed: {}" ,guess); match guess.cmp (&secret_number){ Ordering::Less => println! ("Too small!" ), Ordering::Greater => println! ("Too big!" ), Ordering::Equal => println! ("You win!" ), } }
第四部分解释
第五部分 循环
rust中,loop关键字会创建一个无限循环,加入程序代码中即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 use std::io;use std::cmp::Ordering;use rand::Rng;fn main () { println! ("Guess the number!" ); let secret_number = rand::thread_rng ().gen_range (1 ,101 ); println! ("The secret number is: {}" , secret_number); loop { println! ("Please input your guess." ); let mut guess = String ::new (); io::stdin ().read_line (&mut guess).expect ("Failed to read line" ); let guess : u32 = guess.trim ().parse ().expect ("Please type a number!" ); println! ("You guessed: {}" ,guess); match guess.cmp (&secret_number){ Ordering::Less => println! ("Too small!" ), Ordering::Greater => println! ("Too big!" ), Ordering::Equal => println! ("You win!" ), } } }
编译运行后就可以进行了,但是会出现不想玩时无法正常退出游戏,或者是在猜测成功后无法正常退出
这时我们就要添加一个break
的语句,从而在猜对后退出循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 use std::cmp::Ordering;use rand::Rng;fn main () { println! ("Guess the number!" ); let secret_number = rand::thread_rng ().gen_range (1 ,101 ); println! ("The secret number is: {}" , secret_number); loop { println! ("Please input your guess." ); let mut guess = String ::new (); io::stdin ().read_line (&mut guess).expect ("Failed to read line" ); let guess : u32 = guess.trim ().parse ().expect ("Please type a number!" ); println! ("You guessed: {}" ,guess); match guess.cmp (&secret_number){ Ordering::Less => println! ("Too small!" ), Ordering::Greater => println! ("Too big!" ), Ordering::Equal => { println! ("You win!" ); break ; } } } }
第六部分 异常处理输入
当我们在输入的时候输入了非数字,而是输入了字母就会导致程序异常退出,非常不好
这时我们就要对parse()
方法的返回值做处理,这里使用match
进行处理,而不使用expect
进行异常处理
这种处理是根据parse()
返回值这个Result
的枚举进行匹配,这里Err(_)
中_
是一个通配符,表示匹配所有的Err()
值
continue
语句会使得程序跳转到下一次循环
最后再删除println!("The secret number is: {}", secret_number);
即可得到完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 use std::io;use std::cmp::Ordering;use rand::Rng;fn main () { println! ("Guess the number!" ); let secret_number = rand::thread_rng ().gen_range (1 ,101 ); loop { println! ("Please input your guess." ); let mut guess = String ::new (); io::stdin ().read_line (&mut guess).expect ("Failed to read line" ); let guess : u32 = match guess.trim ().parse (){ Ok (num) => num, Err (_) => continue , }; println! ("You guessed: {}" ,guess); match guess.cmp (&secret_number){ Ordering::Less => println! ("Too small!" ), Ordering::Greater => println! ("Too big!" ), Ordering::Equal => { println! ("You win!" ); break ; } } } }
使用cargo run
编译运行一下就可以开始玩游戏了
Cargo的使用(全)
之前的Cargo
命令是简单使用,为了就是快速入门Cargo
然后,较快的上手rust
,现在就归纳一下cargo
的常用命令,Cargo
虽然学习编程可能不怎么能体现他的重要性,但是在编写项目的便可以体现出cargo
的优势
cargo new
1 2 3 4 5 6 7 cargo new 项目名 --bin //创建一个可执行程序 cargo new hello_world --bin cargo new 项目名 //编写一个可以被其他项目引用的库和包 cargo new hello_world
使用cargo new hello_world --bin
,创建一个可执行程序它的目录如下
其中Cargo.toml
,是由Cargo
生成的配置文件,用于定义项目的元数据、依赖性、编译配置等。
src
:里面存放的是源代码,src
里面会生成一个main.rs
文件,.rs
后缀的文件就是rust
的源代码文件
1 2 3 4 hello_world --Cargo.toml(文件) --src(目录) --main.rs
接下来查看一些cargo.toml
文件里面的具体信息,使用vim cargo.toml
命令查看该文件里面的内容
name
:表示包的名字
version
:项目版本号
edition
:Rust的版本
1 2 3 4 5 6 7 8 [package] name = "hello_world1" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies]
注意 :使用cargo new hello_wold
命令生成的项目,目录也是与上方一样
cargo build
cargo build
命令是编译使用cargo new
所创建的项目
直接在所创建的项目内使用cargo build
命令编译即可
1 2 3 4 5 myheart@aaa ~/c/r/hello_world1 (master)> ls Cargo.toml src myheart@aaa ~/c/r/hello_world1 (master)> cargo build Compiling hello_world1 v0.1.0 (/home/myheart/code/rust/hello_world1) Finished dev [unoptimized + debuginfo] target(s) in 0.92s
使用cargo build
命令后,项目的目录结构会发生改变,编译后基本上就是这样的目录,具体的了解即可。重点就是为可执行二进制文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 hello_world --Cargo.lock(文件) --Cargo.toml(文件) --src(目录) --main.rs(源代码) --target(目录) --CACHEDIR.TAG(文件) --debug(目录) --build(目录) --deps(目录) --hello_world-65c478c245451323 --hello_world-65c478c245451323.d --examples(目录) --hello_world(可执行二进制文件) --hello_world.d(文件) --incremental(目录) --hello_world-2raufob2y547t(目录) --s-h1czdyejl1-16wthvs-37ja4u7549aastc5b8qe1nrmn(目录) --24qepgxx259x3ntg.o --4hcybahg0o19qhm4.o --2i7b77ao4v5unib8.o --540zf6nxv88tkrze.o --3ohs3puklkwxl3d3.o --c4bpplywo62iusm.o --dep-graph.bin --query-cache.bin --work-products.bin --s-h1czdyejl1-16wthvs.lock(文件)
cargo run
该cargo
命令就是编译并运行该rust程序,这里我们先使用cargo new hello_world2
然后使用然后我们使用cd
命令,进入hello_world2
这个目录
再输入cargo run
这个命令,即可编译并运行该cargo
这个文件
1 2 3 4 5 6 myheart@aaa ~/c/r/hello_world2 (master)> cargo run Compiling hello_world2 v0.1.0 (/home/myheart/code/rust/hello_world2) Finished dev [unoptimized + debuginfo] target(s) in 0.62s Running `target/debug/hello_world2` Hello, world! myheart@aaa ~/c/r/hello_world2 (master)>
导入包
当我们要引入外部包或者库的时候我们就要使用cargo
这个包管理器
我们先使用cargo new
创建了一个新的项目,然后再Cargo.toml
这边导入包
导入包后马上执行cargo build
,这样就可以导入包了。cargo
会先在本地寻找相关包,如果没有相关的包就会远程下载该包,然后放入本地中。
之后的项目如果再使用该包就会在本地中导入,无需再次下载