文章目录
- 前言
- 引用
- 引用概念
- 引用特性
- 常引用
- 使用场景
- 传值、传引用效率对比
- 引用和指针的区别
- 内联函数
- 概念
- 特性
- auto关键字
- auto概念
- auto的使用细则
- auto不能推导类型的场景
- 基于范围的for循环(C++11)
- 范围for的语法形式
- 范围for的使用条件
- 指针空值nullptr的出现
- 总结
前言
提示:这里可以添加本文要记录的大概内容:
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之后就没问题了
修改后,程序编译通过
注意:变量之间的赋值没有权限的的缩小和放大问题,只有引用和指针有,因为变量赋值不是共用一块内存空间
使用场景
- 作参数
#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;
}
图解
- 作返回值
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关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式:
- 在release模式下,查看编译器生成的汇编代码中是否存在call Add
- 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化.
特性
- inline是一种以
空间换时间
的做法,省去调用函数的额外开销,所以代码很长或者有递归的函数不适宜使用作为内联函数。 inline对于编译器来说只是一个建议
,编译器会自动优化,如果定义为inline的函数体内有递归等等,编译器优化时会忽略内联。inline不建议声明和定义分离
,分离会导致链接错误,因为inline被展开了,就没有函数地址了,链接就会找不到。
宏的优缺点?
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
C++有哪些技术替代宏?
- 常量定义 换用const
- 函数定义 换用内联函数
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++中表示空指针的常量。
-
条件编译:
#ifndef NULL
:如果NULL
未定义(即未被之前的代码或头文件定义过)。#ifdef __cplusplus
:如果是C++环境。
-
宏定义:
#define NULL 0
:在C++环境下,将NULL
宏定义为整数0,表示空指针。
-
备选定义:
#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++中编写出更加优雅和高效的代码,提升编程体验和代码质量。