【C++】引用、内联函数、auto关键字、基于范围的for循环、指针空值nullptr

发布时间:2024年01月03日


前言

提示:这里可以添加本文要记录的大概内容:

C++是一门强大而灵活的编程语言,拥有许多特性和语法糖,让程序员能够更高效地编写代码。在本博客中,我们将探讨一些C++中常用的特性,包括引用、内联函数、auto关键字、基于范围的for循环以及指针空值nullptr。通过深入了解这些特性,我们可以写出更简洁、高效且易于维护的C++代码。


提示:以下是本篇文章正文内容,下面案例可供参考

引用

引用概念

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

这就好比一个人的名字,身份证上是你的名字,你可能也有乳名或者外号,但是这都代表的是你本人!!!

使用:类型& 引用变量名(对象名)= 引用实体;

看一段程序
在这里插入图片描述

你会发现,pa和a的地址是同一个地址,这也证明了变量和引用变量共享同一块内存空间

注意:引用类型必须和引用实体是同种类型的。扩展点:引用的本质是指针,但是语法上引用变量不会开辟空间

引用特性

  • 引用在定义的时候必须初始化
int& b;//这种写法编译器会报错
  • 一个变量可以有多个引用
int a=0;
int& ra=a;
int& rb=a;
int& rra=ra;
  • 引用一旦引用一个实体,再也不能引用其他实体
int a=0;
int& ra=a;
int x=0;
ra=x;

这里最后是赋值操作,并不是ra重新引用x变量

常引用

先看一段代码,思考一下为什么???

#include <iostream>
using namespace std;
int main()
{
	int a = 0;
	int& b = a;

	const int c = 0;
	int& d = c;
	return 0;
}

在这里插入图片描述
这里很明显上面代码没错,下面代码报错了。结论:这是因为在指针和引用中,权限只能缩小,不能放大!!!
在上面代码中,a变量是可读可写的,b也是可读可写的,这里没问题,但是下面c是只读的,d是可读可写的,这里就会出大问题,问题就在于,他们两个共用一块内存,变量c自身都是只读的,被你引用后你怎么能修改我呢??这就是权限只能缩小,不能放大的由来

还有一种情况就是类型不一致的情况

在这里插入图片描述
这里编译器也报错了,报错原因是无法用 “double” 类型的值初始化 “int &” 类型的引用(非常量限定) ,这里又是为什么呢?接下来我讲用画图板来给大家解释。
在这里插入图片描述
类型不同时,隐式类型转换和引用都会中间产生一个临时变量,而临时变量具有常性(相当于被const修饰),因此int&b的引用相当于权限的放大,这里加上const之后就没问题了

修改后,程序编译通过

在这里插入图片描述

注意:变量之间的赋值没有权限的的缩小和放大问题,只有引用和指针有,因为变量赋值不是共用一块内存空间

在这里插入图片描述
在这里插入图片描述

使用场景

  1. 作参数
#include <iostream>
using namespace std;
void swap(int& num1, int& num2)//引用做参数
{
	int tmp = num1;
	num1 = num2;
	num2 = tmp;
}
int main()
{
	int num1 = 10;
	int num2 = 20;
	swap(num1, num2);
	cout << num1 << " " << num2 << endl;
	return 0;
}

图解
在这里插入图片描述

  1. 作返回值
int& test()//引用做返回值
{
	static int n = 0;
	n++;
	return n;
}
int main()
{
	/*int num1 = 10;
	int num2 = 20;
	swap(num1, num2);
	cout << num1 << " " << num2 << endl;*/
	cout << test() << endl;
	cout << test() << endl;
	cout << test() << endl;
	return 0;
}

注意:如果函数返回时,出了函数作用域,如果返回对象还未还给操作系统,则可以使用引用返回,如果已经还给系统,则必须使用传值返回

总结:通常情况下,如果引用返回的是局部变量,那么很可能会出现潜在的问题,返回的变量中的值可能是随机值,所以一般情况下,引用返回的是全局变量或者被static修饰等,那么原因是什么??

图解

在这里插入图片描述
总的来说,传值返回发生了一次拷贝,多开辟了一次空间,而传引用返回仅仅是返回变量的别名,没有开辟新的空间!!

传值、传引用效率对比

传值与传引用效率对比示例:

在C++中,函数参数的传递方式对性能有一定影响。传值方式会将实参的值复制到形参,而传引用方式直接传递实参的地址。以下是一个简单的例子,展示了传值与传引用的效率对比:

#include <iostream>
#include <chrono>

// 传值方式
void passByValue(int a, int b) {
    // 一些操作
    int result = a + b;
}

// 传引用方式
void passByReference(const int& a, const int& b) {
    // 一些操作
    int result = a + b;
}

int main() {
    // 测试传值方式
    auto startValue = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        passByValue(42, 23);
    }
    auto endValue = std::chrono::high_resolution_clock::now();
    auto durationValue = std::chrono::duration_cast<std::chrono::microseconds>(endValue - startValue);
    std::cout << "传值方式耗时: " << durationValue.count() << " 微秒" << std::endl;

    // 测试传引用方式
    auto startReference = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        passByReference(42, 23);
    }
    auto endReference = std::chrono::high_resolution_clock::now();
    auto durationReference = std::chrono::duration_cast<std::chrono::microseconds>(endReference - startReference);
    std::cout << "传引用方式耗时: " << durationReference.count() << " 微秒" << std::endl;

    return 0;
}

示例说明:

在上述例子中,通过std::chrono库计算了通过传值方式和传引用方式调用函数的耗时。通常情况下,传引用方式相对于传值方式可能具有更好的性能,因为它避免了值的复制操作。

注意事项:

  • 对于小型数据或内置类型,传值可能更为合适。在大型数据结构或自定义类型时,传引用可以减少复制开销。
  • 优化建议:在实际应用中,应该根据具体情况选择合适的传递方式,并根据实际性能需求进行优化。

引用和指针的区别

在语法概念上,引用就是一个别名,没有自己独立的空间,和其引用实体共用同一块空间。

int main()
{
 	int a = 10;
 	int& ra = a;
 
 	cout<<"&a = "<<&a<<endl;
 	cout<<"&ra = "<<&ra<<endl;
	 return 0;
}

但是在底层实现上,引用实际是有空间的,因为引用是按照指针的方式来实现的。

我们来看下引用和指针的汇编代码对比

在这里插入图片描述
在这里插入图片描述

引用和指针的不同点:

1. 定义和声明:

  • 引用: 引用是变量的别名,必须在定义时初始化,并且初始化后不能再引用其他变量。
    int x = 10;
    int& ref = x;  // 引用的初始化
    
  • 指针: 指针是一个变量,存储另一个变量的地址,可以在任何时候指向其他变量。
    int x = 10;
    int* ptr = &x;  // 指针的初始化
    

2. 内存操作:

  • 引用: 引用在内部被视为被引用变量的别名,没有自己的内存地址。
  • 指针: 指针有自己的内存地址,存储其他变量的地址。

3. 空值(Null):

  • 引用: 不存在空引用的概念,必须在初始化时指向有效的对象。
  • 指针: 可以具有空指针值,即指向空地址(nullptr)。

4. 重指向:

  • 引用: 一旦引用被初始化,就无法更改其引用对象。
  • 指针: 可以通过重新分配来更改指针所指向的对象。
    int x = 10;
    int* ptr = &x;  // 指向 x
    int y = 20;
    ptr = &y;       // 指向 y
    

5. 访问方式:

  • 引用: 使用引用时,不需要使用解引用运算符*,直接使用引用变量即可。
  • 指针: 使用指针时,需要通过解引用运算符*来访问指针所指向的值。
    int x = 10;
    int& ref = x;    // 使用引用
    int* ptr = &x;   // 使用指针
    std::cout << ref << std::endl;  // 不需要解引用
    std::cout << *ptr << std::endl; // 需要解引用
    

6. 大小:

  • 引用: 引用在内存中通常不占用额外空间。
  • 指针: 指针在内存中占用一定的空间,通常是机器的字长。

示例说明:

#include <iostream>

int main() {
    int x = 10;
    
    // 引用的使用
    int& ref = x;
    std::cout << "引用值:" << ref << std::endl;

    // 指针的使用
    int* ptr = &x;
    std::cout << "指针值:" << *ptr << std::endl;

    // 重指向
    int y = 20;
    ptr = &y;
    std::cout << "重指向后的指针值:" << *ptr << std::endl;

    return 0;
}

上述示例展示了引用和指针的基本用法以及它们之间的不同点。根据具体的场景和需求,选择引用或指针将有助于更清晰和有效地编写代码。

内联函数

概念

inline修饰的函数叫内联函数,编译时,C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升了程序运行的效率。
在这里插入图片描述
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式:

  1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add
  2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化.

特性

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

宏的优缺点?
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
C++有哪些技术替代宏?

  1. 常量定义 换用const
  2. 函数定义 换用内联函数

auto关键字

auto概念

早期C++语法中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但是可惜的是一直没有人去使用,大家可以去网上搜寻一下是为什么?
C++11中,赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto申明的变量必须由编译器在编译时期推导而得

int TestAuto()
{
	return 10;
}
int main()
{
	int a = 10;
 	auto b = a;
 	auto c = 'a';
 	auto d = TestAuto();
 
 	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
 	cout << typeid(d).name() << endl;
 
 //auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
 return 0;
}

上述代码中使用的auto关键字来推导接收到的值是什么类型,并使用typeid().name()来判断对象或者变量所属的类型是什么

注意:使用auto定义的变量必须对其初始化(这里和引用一样),在编译阶段编译器需要根据初始化表达式来推导auto的实际类型,因此auto并非是一种“类型的”声明,而是一个类型声明时的“占位符”,编译器在编译期间会将auto替换成变量实际的类型

auto的使用细则

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

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

int a = 10;
auto b=&a;
auto* c=&a;
auto& d=a;
  • 在同一行,使用auto定义多个变

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

auto i=10,j=20;
auto x=10,y='1';// 该行代码会编译失败,因为x和y的初始化表达式类型不同

auto不能推导类型的场景

  • 在函数形参中不能使用
void test(auto a)
{}
  • auto不能直接用来声明数组
void test()
{
	auto arr[]={1,2,3,4,5,6,7,8,9,10}
}
  • auto在实际中最常见的用法就是跟以后C++11提供的新式for循环,还有lambda表达式等进行配合使用。

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

范围for的语法形式

对于一个有范围的集合来说,由程序员自己来说明循环的范围是非常多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号:分成两个部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围

#include <iostream>
using namespace std;
void test()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    for (auto& e : arr)
    {
        e*=2;
    }
    for (auto e : arr)
    {
        cout << e << " " ;
    }
}
int main()
{
    test();
    return 0;
}

注意:和普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环

范围for的使用条件

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

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

以下代码就存在问题,因为for的范围不确定

void test(int arr[])
{
	for(auto&e : array)
	{
		cout<<e<<" ";
	}
}

因为这里形参的数组,已经退化成指针,所有形参中的arr并不能获得数组的范围

  • 迭代的对象要实现++和==的操作。(这里牵涉到后期的知识,留给大家思考一下吧哈哈)

指针空值nullptr的出现

相信一个拥有良好编程习惯的同学,一定会在声明一个变量的时候给他初始化,否则可能就会出现使用未初始化的变量的错误,比如未初始化指针等等。在C语言中,如果指针没有一个合法的指向,我们基本都是按照下面的方式对其进行初始化的:
在这里插入图片描述
NULL实际上是一个宏,在传统的C头文件(stddef.h)中,可以看到以下代码:
在这里插入图片描述
这段代码是对NULL宏的定义,用于在C和C++中表示空指针的常量。

  1. 条件编译:

    • #ifndef NULL:如果NULL未定义(即未被之前的代码或头文件定义过)。
    • #ifdef __cplusplus:如果是C++环境。
  2. 宏定义:

    • #define NULL 0:在C++环境下,将NULL宏定义为整数0,表示空指针。
  3. 备选定义:

    • #else#define NULL ((void *)0):如果不在C++环境下,则将NULL宏定义为一个空指针,即(void *)0

这段代码的目的是确保在C和C++中都有一个合适的空指针表示。在C++中,空指针通常表示为整数0,而在C中,可以用(void *)0表示空指针。这种做法是为了保持对旧代码的兼容性,以及确保在不同编译环境下都能正确使用NULL

在这个宏定义中,可以看到,NULL可能被定义成字面常量0,或者在cpp中被定义为(void*)0。但是不管采取何种定义,在使用空值指针的时候,都不可避免的会遇到一些麻烦

void f(int)
{
 	cout<<"f(int)"<<endl;
}
void f(int*)
{
 	cout<<"f(int*)"<<endl;
}
int main()
{
	f(0);
 	f(NULL);
 	f((int*)NULL);
 	return 0;
}

程序结果
在这里插入图片描述
程序本意是想把f(NULL)调用指针版本的 f(int)。但是由于NULL被定义为字面常量0,所以程序输出恰好相反。
在C++98中,字面常量0既可以是一个整型数字,也可以是无类型的指针 (void
)0 ,但是编译器默认情况下会将其看成是一个整型常量,如果要将其按照指针方式来使用,必须要对其进行强转(void*)0**

注意:建议在中统一使用nullptr代替NULL++

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

总结

C++作为一门现代编程语言,提供了许多方便的语法糖和特性,使得程序员能够更好地应对各种编程场景。引用为我们提供了更直观的数据操作方式,内联函数优化了程序的执行效率,auto关键字简化了变量声明,基于范围的for循环使代码更具可读性,而指针空值nullptr则更安全地表示空指针。通过充分利用这些特性,我们能够在C++中编写出更加优雅和高效的代码,提升编程体验和代码质量。

文章来源:https://blog.csdn.net/m0_68002296/article/details/135357616
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。