当前位置: 首页 > news >正文

C++基础知识梳理<2>(引用、内联函数、auto关键字) [入门级】

目录

一、引用

1. 引用概念

2. 引用特性

2.1 引用在定义时必须初始化

2.2 一个变量可以有多个引用

2.3 引用一旦引用一个实体,再不能引用其他实体 

3. 常引用 

3.1 取别名的规则

3.2 权限放大error

3.3 权限不变 

3.4 权限缩小 

4. 引用原理与拓展

4.1 如何给常量取别名?

4.2 临时变量具有常性 

4.3 权限控制的用处

5. 引用的使用场景

5.1 做参数 

5.2 做返回值

6. 引用的使用场景

二、内联函数 

1. 概念

2. 特性 

3. 经典面试题 

三、auto关键字(C++11)

1. auto简介

2. auto的使用细则 

3. auto不能推导的场景 

4. auto的实际应用价值

5. 基于范围的for循环(C++11) 

5.1 范围for的语法

5.2 范围for的使用条件 

四、指针空值nullptr(C++11)


一、引用

1. 引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

  • 类型& 引用变量名(对象名) = 引用实体。我们看下面代码:
int main()
{
	int a = 10;
	int& b = a;
	int& c = a;
	int& d = b;
}

上述代码中定义一个变量a,a这块空间占据4个字节,接下来我们又给它取了二个名字叫做b和c,然后又给b取了一个名字叫做d,也就是说a,b,c,d同时可以访问且修改变量a的这块空间,并且a,b,c,d的地址都是一样的。

2. 引用特性

引用具有三大特性:

  • 引用在定义时必须初始化。
  • 一个变量可以有多个引用。
  • 引用一旦引用一个实体,再不能引用其它实体。

接下来我们将其特性进行一一讲解。

2.1 引用在定义时必须初始化

 假如我们写出如下的引用会发生什么呢?

int& d;

我们可以看到已经发生错误,所以引用在定义的时候必须进行初始化。 

2.2 一个变量可以有多个引用

比如我有个变量a,你可以给其取个别名b,也可以取个别名c,甚至给别名c再取别名d都可以,并且这些别名和a的地址均是一样的,我改变其中一个,其它的也会随之改变。

2.3 引用一旦引用一个实体,再不能引用其他实体 

在这段代码中,我们已经给a取别名b,随后把e的值赋给b,这里可不是对e取别名了,通过编译即可看出来,b的地址同引用的a的地址,而不同于e的地址。

3. 常引用 

3.1 取别名的规则

我们在取别名的时候不是在所有的情况下都是可以随便取的,要在一定的范围内。对原引用变量,权限只能缩小,不能放大。

3.2 权限放大error

我们都知道在C语言中有个const,而在C++的引用这一块也是有const引用的,假如我们现在有个被const修饰过的变量X,现在我们想对X取别名,我们还能用下面的方式吗??

这个就是典型的权限放大,变量X被const修饰只可以进行读,不能进行修改。而此时我们对X引用成y,并且是int型的,此时y是可读可写的,不满足X的只读条件。

那怎样才能对x进行引用呢?我们只需要确保权限不变即可。见下文: 

3.3 权限不变 

想要使权限不变,我们只需要对x引用的同时加上const修饰,让变量y也只是只读。

//权限不变
const int x = 0;
const int& y = x;

那如果变量没有加const修饰,但是在引用时加上const可以吗??这就是权限缩小。

3.4 权限缩小 

//权限缩小
	int c = 10;
	const int& d = c;

这里变量C是可读可写的,我们创建引用变量d进行const修饰,那么d就只为可读,权限缩小,不报错。

4. 引用原理与拓展

4.1 如何给常量取别名?

我们可以用下面方法给常量取别名吗?

int& c=20; //err

对于常量我们不可以直接进行取别名,我们需要加上const修饰才可以。

const int& c=20; //right

4.2 临时变量具有常性 

我们看如下代码:

double d = 2.2;
int& e = d;

编译一下看看:

很明显e不能成为d的别名。但是如果我们加上const,发现它竟然不会出错!!

 该如何解释上述现象呢??这就需要我们先回顾一下C语言的类型转换。C++本身就是建立在C语言之上,C语言在相似类型是允许隐式类型转换的。大的数据类型给小的会发生截断,小的给大的会提升。我们看如下代码:

 这里的丢失数据其实就是会丢失精度。

⭐原理:

这里把d赋值给f并不是直接赋值的,会先把d的整数部分取出来赋值给一个临时变量,该临时变量大小为4个字节,随后再把这个临时变量赋给f

  • 临时变量具有常性,就像被const修饰了一样,不能被修改。

 谈到这里我想大家应该就可以理解为什么下面的这段代码需要加上const才能编译通过:

double d = 2.2;
const int& e = d;

这里e引用的是临时变量,临时变量具有常性,不能直接引用否则就是放大了权限,加上const才能保证其权限不变。

  • 既然是这样,那为何下面这段代码在赋值的时候不加上const呢??
double d = 2.2;
int f = d;
  • 上述加const是在引用的基础上加的,如果不加const那么就是放大权限。而在此段代码,对f的改变并不会影响到临时变量。普通的变量不存在权限放大或者缩小。
  • 那么下列代码中的e还是对d的引用吗? 
double d=2.2
const int& e=d;

当然不是,此时的e是对临时变量的引用,是临时变量的别名我们可以通过编译来验证:

我们可以看到变量e和d的地址都不一样。 

4.3 权限控制的用处

这里简单的提一下,例如下面的传参问题。

如若函数写成普通的引用,那么很多参数可能会传不过去:

观察上面代码我们发现只有a能够正常传过去,后面的均传不过去,因为后面传的参数均涉及权限放大。 但如果我们在函数的形参上加const修饰呢?

加上const修饰后编译器就不会报错了。 

5. 引用的使用场景

引用的使用场景分为两个:

  • 做参数
  • 做返回值

接下来我们将会详细讲解一下。

5.1 做参数 

比如我们现在想写一个Swap函数,以前是用指针传参:

//指针版
void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

int main()
{
	int a = 10;
	int b = 5;
	Swap(&a, &b);
	return 0;
}

而现在,我们可以巧用引用来完成Swap函数

//引用版
void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

//支持函数重载
void Swap(double& x, double& y)
{
	double tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	//交换整数
	int a = 0, b = 1;
	Swap(a, b);
	//交换浮点数
	double c = 1.1, d = 2.2;
	Swap(c, d);
	return 0;
}
  • 在输出型参数里面用引用非常的方便:
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    //……
}

我们可以将上述代码改为下面的代码:

int* preorderTraversal(struct TreeNode* root, int& returnSize) {
    //……
}
int main()
{
    preorderTraversal(tree, size);
}

加上引用在调用函数时省去了写&,更加方便理解,同时也减少了对指针的使用。

用引用做参数的好处如下:

  • 输出型参数
  • 减少拷贝,提高效率

5.2 做返回值

 我们先看下面一段代码:

输出结果为1、2、3。 

  • 这里可能有人会提问为什么不是1、1、1呢?注意这里使用了静态区的变量只会初始化一次也就是说我static int n = 0这行代码在编译时只有第一次会跳到这,其余两次均不会走这一行代码,你每次进去的n都是同一个n。

⭐传值返回:

int Count()
{
	int n = 0;
	n++;
	return n;
}

int main()
{
	int ret = Count();
	return 0;
}

在传值返回的过程中会产生一个临时变量(类型为int),如果这个临时变量小会用寄存器进行替代,如果大就不会用寄存器替代。

具体返回的过程中是先把函数的n拷贝给临时变量,再把临时变量拷贝给ret。

  •  main函数里有个变量ret,汇编时会call一个指令跳到函数Count,Count里有一个变量n。这里不能把n直接传给ret,因为函数调用完后函数的栈帧就销毁了,这里会产生一个临时变量保存n的值,再把n的值传给ret。

我们如何证明这个过程中产生了临时变量呢?

我们只需要加一个引用。

这里很明显发生了编译错误。 这里ret之所以出错就是因为其引用的是临时变量,临时变量具有常性,只读不可修改,直接引用则会出现上文所述的权限放大问题。 所以这就很巧合的验证了此函数调用中途会产生临时变量。

想要解决此类问题,我们只需要使其保持权限不变即可,即加上const修饰:

 ⭐传引用返回:

我们对上述代码进行微调:

int& Count()
{
	int n = 0;
	n++;
	return n;
}

int main()
{
	int ret = Count();
	return 0;
}

这里加上了引用&后,中间也会产生一个临时变量,只是这个临时变量的类型是int&我们把这个临时变量假定为tmp那么此时tmp就是n的别名,再把tmp赋值给ret。这个过程不就是直接把n赋给ret吗这里区分于传值返回的核心就在于传引用的返回就是n的别名

如何证明传引用返回的是n的别名?

我们还可以通过打印法来验证:

这里的ret和n的地址都是一样的,这也就意味着ret其实就是n的别名。 综上,传值返回和传引用返回的区别如下:

  • 传值返回:会有一个拷贝
  • 传引用返回:没有这个拷贝了,返回的直接就是返回变量的别名 

这里又存在一个问题:上述代码有没有其他错误??

我传引用返回后,ret就是n的别名,出了函数出了这个作用域变量n就销毁。但空间的销毁并不代表空间不在了。空间的归还就好比你退房,虽然你退房了,但是这个房间还是在的,只是说使用权不是你的了。但是假说你在不小心的情况下留了一把钥匙,你依旧是可以进入这个房间,不过你这个行为是非法的。这个例子也就足矣说明了上述的代码是有问题的。是一个间接的非法访问。

我们看下面代码和运行结果:

这里第一次打印ret的值为1,打印完后函数栈帧销毁,此时ret里面存储的是随机值。 

  • 那如果我们非要引用返回,该怎样做呢?

加上static即可:

int& Count()
{
	static int n = 0;
	n++;
	cout << "&n: " << &n << endl;
	return n;
}
int main()
{
	int& ret = Count();
	cout << ret << endl;
	cout << "&ret: " << &ret << endl;
	cout << ret << endl;
	return 0;
}

加上了static后n就被放入了静态区,出了作用域不会被销毁。

⚠: 如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。否则就可能会出越界问题。

看下面代码:

int& Add(int a, int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int& ret = Add(1, 2);
	Add(3, 4);
	cout << "Add(1, 2) is :" << ret << endl;  //7
	return 0;
}

此段代码的执行结果ret的值为7,首先我Add(1,2)。调用完后返回C的别名给ret,调用完后Add栈帧销毁,当我第二次调用函数c的值就被修改成7。

正常情况下我们应该加上static:

 

 加上static后这里ret的值就是3了,因为加上了static初始化只有一次。此时c在静态区了,销毁栈帧它还在。

  • 我们再演示一下其被覆盖的情形:

正常情况:

不加static发生覆盖:

  ⭐传值、传引用效率比较:

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

  • 作为参数比较
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
	A a;
	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc2(a);
	size_t end2 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
	TestRefAndValue();
}

  • 作为返回值比较
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();
	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();
	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
	TestReturnByRefOrValue();
}

6. 引用的使用场景

引用和指针的不同点:

  • 引用概念上定义一个变量的别名,指针存储一个变量地址
  • 引用在定义时必须初始化,指针没有要求
  • 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  • 没有NULL引用,但有NULL指针
  • 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  • 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  • 有多级指针,但是没有多级引用
  • 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  • 引用比指针使用起来相对更安全

⭐:其实在底层实现上,引用是按照指针方式来实现的。下面我们来看下引用和指针的汇编代码对比:

二、内联函数 

1. 概念

inline修饰的函数叫做内联函数编译时C++编译器会在调用内联函数的地方展开没有函数压栈的开销,内联函数提升程序运行的效率。

我们以Add函数为例:

int Add(int x, int y)
{
	int z = x + y;
	return z;
}

上述的Add函数如若被频繁调用,则在效率上会存在一定的损失。假如我们调用10次,那么就需要建立十次栈帧。建立栈帧又要保存寄存器,压参数等一系列操作都会造成效率损失。

  • 在我们先前学过的C语言中就给出了解决方案:
#define ADD(x,y) ((x)+(y))
  • 既然用宏能够解决,那C++为何要引出inline?

使用宏确实可以帮助我们避免调用栈帧而造成效率损失,但是宏的写法上欠妥,我们需要注意结尾不能加分号,要注意优先级带来的问题而频繁加括等等一系列问题C++为了填补宏书写规则麻烦的坑,引出了内敛函数的概念(inline)。内敛函数的特点:

 1.解决宏函数晦涩难懂,容易写错的问题

 2.解决宏不支持调试,不支持类型安全的检查等问题

  • 内敛函数(inline)如何使用?

我们只需要在函数前面加上inline即可:

inline int Add(int x, int y)
{
	int z = x + y;
	return z;
}

此时我们再调用函数就不会再建立栈帧了,函数直接会在调用的地方展开。

  • inline的好处如下:

  1.debug支持调试

  2.不容易写错,就是普通函数的写法

2. 特性 

  1. inline是一种以空间换时间的做法省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
  2. inline对于编译器而言只是一个建议编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
  3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

3. 经典面试题 

  • 问题1:宏的优缺点

优点:

  1. 增强代码的复用性
  2. 提高性能

缺点:

  1. 不方便调试宏(因为预编译阶段进行了替换)
  2. 导致代码可读性差,可维护性差,容易误用
  3. 没有类型安全的检查
  • 问题2:C++有那些技术替代宏
  1. 常量定义,换用const
  2. 函数定义,换用内联函数

三、auto关键字(C++11)

1. auto简介

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?

C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

简单来说:先前定义变量要在变量前指定类型,使用auto可以不指定类型,让右边赋的值进行推导,如示例:

int a = 10;
auto b = a;
auto c = 'a';

这里a的类型是整型,那么就自动推出b的类型为int,而'a'为char类型,自然c就是char类型。

  • 补充:

这里补充一个知识点:typeid().name。它是专门用来输出一个变量的类型,返回的是一个字符串。

  • 代码演示:
int TestAuto()
{
	return 10;
}
int main()
{
	const int a = 10;
	auto b = a;
	auto m = &a;
	auto c = 'a';
	auto d = TestAuto();
	cout << typeid(b).name() << endl; // int
	cout << typeid(m).name() << endl; // int const *
	cout << typeid(c).name() << endl; // char 
	cout << typeid(d).name() << endl; // int
	//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
	return 0;
}
  • 结果如下:

  • 注意: 

使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

2. auto的使用细则 

  • auto与指针和引用结合起来使用

用auto声明指针类型时,用auto和auto*没有任何的区别,但用auto声明引用类型时则必须加&

int main()
{
	int x = 10;
	auto a = &x;
	auto* b = &x;
	auto& c = x;
	cout << typeid(a).name() << endl; // int*
	cout << typeid(b).name() << endl; // int*
	cout << typeid(c).name() << endl; // int
	*a = 20;
	*b = 30;
	c = 40;
	return 0;
}

  •  在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

void TestAuto()
{
	auto a = 1, b = 2;
	auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

3. auto不能推导的场景 

  • auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
  • auto不能做返回值
auto Test()
{
	return 10; // err
}
  • auto不能直接用来声明数组
void TestAuto()
{
	int a[] = { 1,2,3 };
	auto b[] = { 4,5,6 }; //err 错误
}
  • 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法。
  • auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。

4. auto的实际应用价值

  • 类型很长时,懒得写,我们可以让其自动推导

以后在我们学到容器的时候,我们会写出这样的代码:

#include<map>
#include<string>
int main()
{
	std::map<std::string, std::string>dict;
	dict["sort"] = "排序";
	dict["string"] = "字符串";
//auto意义之一:类型很长时,懒得写,可以让它自动推导
	std::map<std::string, std::string>::iterator it = dict.begin();
	auto it = dict.begin();
	return 0;
}

我们使用auto就可以简化前面定义过长类型的代码,使其自动判断类型

  • 基于范围的for循环

基于范围的for循环我们单独来讲,看下文。

5. 基于范围的for循环(C++11) 

5.1 范围for的语法

我们平常写打印一串数组中的数据的时候我们可以这样写:

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (int i = 0; i < sizeof(array) / sizeof(int); ++i)
		array[i] *= 2;
	for (int i = 0; i < sizeof(array) / sizeof(int); ++i)
		cout << array[i] << " "; // 2 4 6 8 10
}

对于一个有范围的集合而言,我们可以利用基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

因此在C++中我们可以这样写:

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (int i = 0; i < sizeof(array) / sizeof(int); ++i)
		array[i] *= 2;
	for (auto e : array)
		cout << e << " "; // 2 4 6 8 10
}

此段代码就是范围for,它可以自动遍历,会依次取数组中的数据赋值给e,自动判断结束

  • 可如果现在我想对数组进行修改,使数组中的每一个数字除以2,我们该怎么做呢?是如下这样吗?

我们可以看到上述方法是错误的,其并没有起到修改的作用。我们注意看范围for的规则:依次取数组中的数据赋值给e这也就说明e是数组中每个值的拷贝,e的改变并不会影响到数组 此时就需要我们用到引用了,当我们给其取别名时,e的修改就会影响到原数组。

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (int i = 0; i < sizeof(array) / sizeof(int); ++i)
		array[i] *= 2;
	for (auto e : array)
		cout << e << " "; // 2 4 6 8 10
	cout << endl;
	for (auto& e : array)
		e /= 2;
	for (auto e : array)
		cout << e << ' ';
}
  • 补充:

1、范围for里的auto也可以写成int,不过最好还是写成auto,毕竟auto可以自动推出数组的类型嘛,不用auto还要自己手动设置。把e改成其它的变量也是可以的,不强求。

2、与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。

5.2 范围for的使用条件 

  • for循环迭代的范围必须时确定的

对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

注意:以下代码就有问题,因为for的范围不确定

void TestFor(int array[])
{
	for (auto& e : array)
		cout << e << endl;
}

用范围for必须是数组名,C语言有规定参数传递的过程中不能是数组,这里的形参是指针,自然不能用范围for的规则了。

  • 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲)

四、指针空值nullptr(C++11)

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:

void TestPtr()
{
	int* p1 = NULL;
	int* p2 = 0;
	// ……
}

但是在C++中,我们推荐这样写:

int* p3=nullptr;

前者中,NULL和0在C++其实是等价的,都不规范。NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

如果没有定义宏,如果在cplusplus里,NULL被定义成0。可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

  • 注意:
  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr

相关文章:

  • Halcon图像分割总结
  • 学习笔记-WinRM
  • Java内存模型与volatile
  • LIO-SAM中的mapOptmization
  • Pandas数据处理可视化
  • NA of optical fiber(光纤的数值孔径)
  • 花了整整一天,总结了C语言所有常用的文件操作
  • 2022 年十大 Python Web 开发框架
  • Go语言学习(五)-- 函数和闭包
  • 【数据结构】链表其实并不难 —— 手把手带你实现单链表
  • LeetCode每日一题——754. 到达终点数字
  • 有一个是对的,就是坚持去做难的事情。
  • httpClient同步、异步性能对比
  • 吴峰光杀进 Linux 内核
  • 朋友离职了,一周面试了20多场,我直呼内行
  • 创建 MQTT 连接时如何设置参数?
  • C#基础知识
  • 云原生之快速使用Nacos Spring Cloud
  • 什么是GPIO的推挽输出和开漏输出
  • Java 模拟实现 定时器 和 线程池