Rust实战(4):防御简单C++ vector容器的越界问题例子分析

C++版本1

这是一个C++的简单vector容器的越界问题的小例子:

func1.h

//设立一个简单vector容器的越界问题
#ifndef STRING_VECTOR_H
#define STRING_VECTOR_H
#include <vector>
#include <string>
using namespace std;
class StringVector {
public:
    StringVector();
    void addElement(const string& element);
    bool popElement();
    //bool modifyElement(const string& oldElement, const string& newElement);
    vector<string> getValues();
    string getElement(size_t index);
private:
    size_t cur_len;
    size_t max_len;
    vector<string> values;

};
#endif

func1.cpp

#include "func1.h"

StringVector::StringVector() {
    max_len = 10;
    cur_len = 0;
    // 设置容器长度为max_len
    values.resize(max_len);
}

void StringVector::addElement(const string& element){
    // if(cur_len>=max)
    //     return;
    // 往尾巴插入元素,第一次会在第11的位置插入元素
    values.emplace_back(element);
}

bool StringVector::popElement(){
    // if(cur_len<=0)
    //     return false;
    values.pop_back();
    cur_len--;
    return true;
}

string StringVector::getElement(size_t index){
    return values[index];
}

上面的代码是刻意制造出来的,用一个测试代码说明问题:

#include "func1.h"
#include <random>
#include <string>
#include <iostream>

int main(){
    // 正常运行的代码
    StringVector sv;
    sv.addElement("This is question1");
    std::string ret1 = sv.getElement(10);
    std::cout<<"ret1:"<<ret1<<std::endl;
    //sv.popElement();
    

    // 有BUG的代码
    sv.popElement();
    std::string ret2 = sv.getElement(11);

    std::cout<<"ret2:"<<ret2<<std::endl;
    return 0;
}

程序输出:

ret1:This is question1
ret2:

这个代码在Mac OS 系统上 make 后运行没有崩溃,但是测试代码里std::string ret2 = sv.getElement(11); 实际上发生了数组越界。

Rust版本1

在rust环境下创建一个比对小项目:

首选,创建一个测试目录:

mkdir rust

其次,添加一个Cargol.toml工程配置文件,添加空配置:

[workspace]

members = [

]

接着,创建一个类库项目some:

cargo new --lib some

接着,创建一个主程序main:

cargo new main

最后,配置两个子项目到根配置Cargo.toml里

[workspace]

members = [
    "some",
    "main"
]

此时目录结构这样:

├── Cargo.toml
├── main
│   ├── Cargo.toml
│   └── src
│       └── main.rs
├── some
│   ├── Cargo.toml
│   └── src
│       ├── lib.rs

现在,在some/src 目录下新增str_vec.rs,代码如下:

use core::fmt;

pub struct StringVector {
    cur_len: usize,
    max_len: usize,
    values: Vec<String>,
}

impl fmt::Display for StringVector {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "cur_len: {}, max_len:{}, values:{:?}",
            self.cur_len, self.max_len, self.values
        )
    }
}

impl StringVector {
    pub fn new(cur_len: usize, max_len: usize) -> Self {
        let mut vec_string: Vec<String> = Vec::with_capacity(max_len);
        for _i in 0..max_len {
            vec_string.push(String::from(""));
        }

        StringVector {
            cur_len: cur_len,
            max_len: max_len,
            values: vec_string,
        }
    }

    pub fn add_element(&mut self, element: String) {
        if self.cur_len >= self.max_len {
            return;
        }

        self.values.push(element);
        self.cur_len += 1;
    }

    pub fn pop_element(&mut self) -> bool {
        if self.cur_len == 0 {
            return false;
        }
        self.values.pop();
        return true;
    }

    pub fn get_values(&self) -> Vec<String> {
        self.values.clone()
    }

    pub fn get_element(&self, index: usize) -> String {
        self.values[index].clone()
    }
}

在 some/src/lib.rs 里添加测试代码

mod str_vec;

pub use str_vec::*;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let max_len = 10;
        let mut vec = str_vec::StringVector::new(0, max_len);

        // 正常代码
        vec.add_element(String::from("你好"));
        let ret1 = vec.get_element(10);
        println!("ret1:{:}", ret1);
        vec.pop_element();

        // 有BUG代码
        let ret2 = vec.get_element(11);

        println!("ret2:{:}", ret2);
    }
}

执行cargo的测试命令:cargo test -- --nocapture,输出:

warning: some crates are on edition 2021 which defaults to `resolver = "2"`, but virtual workspaces default to `resolver = "1"`
note: to keep the current resolver, specify `workspace.resolver = "1"` in the workspace root's manifest
note: to use the edition 2021 resolver, specify `workspace.resolver = "2"` in the workspace root's manifest
   Compiling some v0.1.0 (/Users/feilong/Desktop/test/mtest_cases/rust/some)
    Finished test [unoptimized + debuginfo] target(s) in 0.21s
     Running unittests src/main.rs (target/debug/deps/main-90bdc1be522c03e6)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/lib.rs (target/debug/deps/some-911f1fc75a0fadd8)

running 1 test
ret1:你好
thread 'tests::it_works' panicked at 'index out of bounds: the len is 10 but the index is 10', some/src/str_vec.rs:55:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test tests::it_works ... FAILED

failures:

failures:
    tests::it_works

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `-p some --lib`

可以看到Rust的测试输出了详细的数组越界信息:thread 'tests::it_works' panicked at 'index out of bounds: the len is 10 but the index is 10', some/src/str_vec.rs:55:9

在数组越界上,如果采用和C++一样的数组下标直接索引,Rust代码在运行时会明确的Panic。

Rust版本2

但是Rust对类型的处理可以更好,稍微改造下get_element的实现:

use core::fmt;

pub struct StringVector {
    cur_len: usize,
    max_len: usize,
    values: Vec<String>,
}

impl StringVector {
    pub fn new(cur_len: usize, max_len: usize) -> Self {
        let mut vec_string: Vec<String> = Vec::with_capacity(max_len);
        for _i in 0..max_len {
            vec_string.push(String::from(""));
        }

        StringVector {
            cur_len: cur_len,
            max_len: max_len,
            values: vec_string,
        }
    }

    pub fn add_element(&mut self, element: String) {
        if self.cur_len >= self.max_len {
            return;
        }

        self.values.push(element);
        self.cur_len += 1;
    }

    pub fn pop_element(&mut self) -> bool {
        if self.cur_len == 0 {
            return false;
        }
        self.values.pop();
        return true;
    }

    pub fn get_values(&self) -> Vec<String> {
        self.values.clone()
    }

    pub fn get_element(&self, index: usize) -> Option<&String> {
        // 使用get返回Option<&String>
        self.values.get(index)
    }
}

改造下对应的测试代码:

mod str_vec;

pub use str_vec::*;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let max_len = 10;
        let mut vec = str_vec::StringVector::new(0, max_len);

        // 正常代码
        vec.add_element(String::from("你好"));
        let ret1 = vec.get_element(10);
        println!("ret1:{:?}", ret1.unwrap());
        vec.pop_element();

        // 有BUG代码
        let ret2 = vec.get_element(10);

        println!("ret2:{:?}", ret2);
    }
}

执行测试命令cargo test -- --nocapture

输出:

warning: some crates are on edition 2021 which defaults to `resolver = "2"`, but virtual workspaces default to `resolver = "1"`
note: to keep the current resolver, specify `workspace.resolver = "1"` in the workspace root's manifest
note: to use the edition 2021 resolver, specify `workspace.resolver = "2"` in the workspace root's manifest
   Compiling some v0.1.0 (/Users/feilong/Desktop/test/mtest_cases/rust/some)
warning: unused import: `core::fmt`
 --> some/src/str_vec.rs:1:5
  |
1 | use core::fmt;
  |     ^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: `some` (lib) generated 1 warning (run `cargo fix --lib -p some` to apply 1 suggestion)
warning: `some` (lib test) generated 1 warning (1 duplicate)
    Finished test [unoptimized + debuginfo] target(s) in 0.23s
     Running unittests src/main.rs (target/debug/deps/main-90bdc1be522c03e6)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/lib.rs (target/debug/deps/some-911f1fc75a0fadd8)

running 1 test
ret1:"你好"
ret2:None
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

可以看到ret2:None 此时数组超出范围,返回的是None,这个时候,可以使用Rust的 match 语法做自然的错误处理:

mod str_vec;

pub use str_vec::*;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let max_len = 10;
        let mut vec = str_vec::StringVector::new(0, max_len);

        // 正常代码
        vec.add_element(String::from("你好"));
        let ret1 = vec.get_element(10);
        println!("ret1:{:?}", ret1.unwrap());
        vec.pop_element();

        // 有BUG代码
        let ret2 = vec.get_element(10);

        // 使用match 语法
        match ret2 {
            // 放心使用
            Some(value) => println!("ret2:{:?}", String::from("world") + value),
            None => println!("do nothing"),
        }
    }
}

输出:

warning: some crates are on edition 2021 which defaults to `resolver = "2"`, but virtual workspaces default to `resolver = "1"`
note: to keep the current resolver, specify `workspace.resolver = "1"` in the workspace root's manifest
note: to use the edition 2021 resolver, specify `workspace.resolver = "2"` in the workspace root's manifest
warning: unused import: `core::fmt`
 --> some/src/str_vec.rs:1:5
  |
1 | use core::fmt;
  |     ^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: `some` (lib) generated 1 warning (run `cargo fix --lib -p some` to apply 1 suggestion)
warning: `some` (lib test) generated 1 warning (1 duplicate)
    Finished test [unoptimized + debuginfo] target(s) in 0.00s
     Running unittests src/main.rs (target/debug/deps/main-90bdc1be522c03e6)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/lib.rs (target/debug/deps/some-911f1fc75a0fadd8)

running 1 test
ret1:"你好"
do nothing
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests some

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

可以看到安全地有 do nothing 输出。实际上,如果此时不用match,直接写:

println!("ret2:{:?}", String::from("world") + ret2);

执行cargo build,编译器就提示类型不匹配:

error[E0308]: mismatched types
  --> some/src/lib.rs:22:55
   |
22 |         println!("ret2:{:?}", String::from("world") + ret2);
   |                                                       ^^^^ expected `&str`, found `Option<&String>`
   |
   = note: expected reference `&str`
                   found enum `Option<&String>`

所以你就强制要解决类型匹配问题,要么用unwrap,但是一旦你用了unwrap,就意味着如果是None会Panic,此处需要做match获取其他错误处理(以后展开):

println!("ret2:{:?}", String::from("world") + ret2.unwrap());

C/C++数组越界麻烦的根本原因,Rust首先做对的是什么?

Q1:c/c++和rust在数组越界上的处理差异是?
A1
C/C++ 和 Rust 在处理数组越界方面存在一些关键差异。

在 C/C++ 中,数组越界通常会导致不确定的行为。这意味着程序可能会崩溃、生成垃圾数据或以其他意想不到的方式工作。数组越界很难调试,因为它可能很难确定导致问题的索引。

Rust 在数组越界方面更加安全。如果您试图访问数组越界索引,Rust 将抛出一个错误。这使得在运行时更容易发现和修复数组越界问题。

以下是 C/C++ 和 Rust 在数组越界方面处理差异的一些示例:

// C++
int arr[10];
arr[11] = 1; // 数组越界,程序可能崩溃或生成垃圾数据

// Rust
let mut arr = [1, 2, 3, 4, 5];
arr[5] = 6; // 数组越界,Rust 抛出一个错误

Rust 在数组越界方面更加安全,因为它可以帮助您在编译时或运行时发现和修复数组越界问题。这可以帮助您编写更可靠的程序。

Q2:rust编译时检查出数组越界的例子
A2
好的,以下是 Rust 编译时检查出数组越界的例子:

fn main() {
    let mut arr = [1, 2, 3, 4, 5];
    arr[5] = 6; // 数组越界,Rust 编译时检查出
}

编译该代码会得到以下错误:

error[E0659]: index out of bounds: the len is 5 but the index is 5
 --> src/main.rs:4:5
  |
4 |     arr[5] = 6;
  |     ^^^^^^^ index out of bounds
  |

Rust 在编译时检查出数组越界,可以帮助您避免在运行时发生数组越界错误。

更多

Rust的在错误处理上的支持还有更多,可以慢慢展示,这些只是Rust现代语言设计带来的基本便利,Rust并不是小补丁式的解决问题。

关于Option的问题,也可以看这篇以前翻译的文章:翻译(3): NULL-计算机科学上最糟糕的失误: https://vector.blog.csdn.net/article/details/119341261?spm=1001.2014.3001.5502

解决问题要用系统性的方式解决。Rust是替代C++的代规模底层软件开发的首选。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值