面向C++程序员的Rust教程(一)

综上所述,笔者以《面向C++程序员的Rust教程》进行开题,作为本身就是C++程序员兼Rust学习者的身份,相信能给读者带来切身体会和不一样的理解感悟。

从Hello World说起

按照世界级惯例,程序语言教程都由一个Hello World开始。一来我们可以以最少的篇幅来编写一段可运行的程序,让我们建立对这门语言的主观印象;二来也可以从这个最简单的程序里看到很多端倪,这有助于我们接下来的学习和研究。

fn main() {
  println!("Hello, World!");
}

算上大括号也就只有短短的3行,但其实能看出不少东西的,我们一一来说。

首先,整体上来说,Rust也采用了大括号和分号的代码风格,这一点跟C++一样,而不同于Python和Go。函数体是用大括号包裹的,并且每行语句都以分号结尾,那么这种风格通常来说都是对缩进和空格、空行不敏感的,因此这里的缩进、空行等都是一种让代码可读性更高的编码规范,而不是语法本身要求。

其次,这里出现了我们熟悉的main函数,作为程序的入口。函数是可以直接单独存在的,不需要强行包裹在类中,这一点与Java不同,说明Rust至少是「可以」做面向过程编程的。

最后,我们可以看到main函数是空参数空返回值的,这一点与C/C++不同,也就是说当Rust程序作为应用程序时,其与OS或父进程之间的交流应当有其他专有的方式,而不是通过传参。这种限制其实有他自己的独特优势的,这点我们在后面章节再详细介绍。

其他呈现的细节还有比如说双引号表示字符串,而单引号表示字符,这与C++表现一致,不同于js、Python、shell等。这些都等后面章节我们再一一诉说。

这里希望读者可以有一个主观印象,Rust程序大致就是长这个样子哒~。

类型说明符

在C语言以及其衍生语言(如C++、Java、Objective-C等)中,有一个非常大的特点(甚至说是缺陷),就是「行为」被隐含在了「类型」说明符之中。

举个例子来说:

int a;

这里的int首先是表达了「创建变量」的含义,其次才是表示「变量类型是整型」。「创建变量」这种表示动作的语义是隐藏在里面的,根据类型说明符的位置、上下文、组合等不同来区分。比如上面单独一个类型符号表示创建变量,而如果加上小括号就表示「函数声明」的动作:

int f();

这里的int()共同表示「函数声明」,而int又承担了「函数返回值类型」的类型说明的含义。

而在一些「仅需要表示动作,不需要类型」的场合,C++的这种语法就显得很奇怪和诡异,比如说:

template <typename T1, typename T2>
auto f(T1 a, T2 b) -> decltype(a + b) {}

这里的auto并不表示任何类型,因为返回值类型在箭头后面的表达式,因此这里只是需要一个占位符,跟小括号一起表达「函数定义」这样的动作语义,所以强行塞了一个auto

而对于Rust来说,这个问题就得到了非常好的解决,那就是把「动作」和「类型」这两件事分来,比如说:

let a: i32;

我们看到这里有两个关键字,其中let表示「定义变量」这个动作,而i32表示类型,也就是32位有符号整型。

那么,当遇到只需要动作描述,不需要类型描述的场景,这个语法就很自然,只要去掉类型描述符就好了,比如说:

let a = 5;

这时a的类型就会由其初始化的值类型来推导,整体语法上会比C++的auto占位符自然得多。

再比如说定义函数,也是用fn关键字,与类型的符号分开表示。

另一点就是,在C/C++当中,我们推荐使用intlong这种视架构而定的类型,比如说long在32位环境下是4字节,在64位环境下是8字节。原因是在CPU的寄存器原理上。匹配寄存器长度的数据读写是效率最高的。但这种缺点也很明显,因为你不能确定它的具体长度,自然也就不能确定它的值域范围。所以我们看到其实更多情况下int32_tint64_t使用得更广泛。这其实就是一个编程思维问题,如果你是底层思维,那你考虑更多的是硬件的位宽,但如果你是上层思维,你更多考虑的是类型的值域。

Rust当中虽然也有匹配架构的isizeusize类型,但是显然更被推荐的是i32u16等这种指定长度的类型,其实这也说明了Rust设计之初,是希望你更加聚焦到程序逻辑上,而不是底层框架上的。

引用/指针

在C++当中,引用和指针是独立的两套语法和语义,但底层实现上又非常相似,大多数场景下用指针或是引用都不会有太大差别,但这也就造成了另一个问题,就是我们可能难以区分「值传递」和「引用传递」。

举个例子来说:

int a = 5;
f(a); // 但从这里是看不出来a是值传递还是引用传递的

C++引入的这种「引用传递」的语义,原本的目的也是为了屏蔽「函数调用栈之间的地址传递」这样的信息,让你感觉就是真的把变量本身传进去了一样。但与之带来的就是它无法与真正的值传递进行区分,对于上例来说void f(int);void f(int &)的函数类型就会有不同的表现。

有写团队为了区分这种情况,会有规定类似于「出参必须使用指针」这样的方法,希望能在调用时对出入参进行区分,比如说:

void f(int in, int \*out);
void Demo() {
  int a = 5;
  int b = 0;
  f(a, &b); // 加了&的是出参
}

但这样做局限性也很大,首先我们还是区分不了入参是值传递和引用传递,比如说:

void f1(Test t);
void f2(const Test &t);

void Demo() {
  Test t;
  f1(t); // 入参,但会触发拷贝构造
  f2(t); // 入参,但是单纯引用传递
}

另一方面就是说,对于「指针类型的入参」,可能还是会添加取地址符,但本质上却作为了入参,比如说:

// 这个函数是为了打印指针的值,因此p并不是出参
void PrintPtr(const void \*p) {
  printf("%p", p);
}

void Demo() {
  int a = 5;
  PrintPtr(&a); // 其实并不是把a当做出参,而是把「&a」当做了入参
}

当然,像C++这种同时保留指针和引用语义的语言也是屈指可数的,大多数语言都只会保留其一,比如说Go当中,仅保留指针语义而没有引用语义。

// 只有指针语法
func f(a \*int) {
  \*a = 5 // 解指针后操作出参
}

func main() {
  var a = 3
  f(&a) // 显式取地址
  fmt.Println(a)
}

另一种就是像Java这种的,完全取消了引用和指针,只是会根据实际传参时的数据类型来决定使用值传递还是引用传递:

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前在阿里

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化的资料的朋友,可以点击这里获取!

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化的资料的朋友,可以点击这里获取!

  • 29
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值