??个人主页:欢迎大家光临——>沙漠下的胡杨
? 各位大帅哥,大漂亮
?如果觉得文章对自己有帮助
?可以一键三连支持博主
?你的每一分关心都是我坚持的动力
?
??: 本期重点:C++引用和指针区别
??希望大家每天都心情愉悦的学习工作。?
? ? ? ? ? ? ? ? ? 目录
C++中的引用
引用的概念:
const引用:
引用的特性:
引用的使用场景:
引用和指针区别:
?内联函数:
内联函数的特性:
auto关键字:
空指针问题:
?
C++中的引用
引用的概念:
首先我们从语法角度来讲,引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。引用的使用方法如下:类型&? ?引用变量的名字(对象名) = 引用实体,其中这个引用实体就想当于我们的“大名”,而这个引用变量的名字就想当于我们的“别名”,其中我们的引用变量名和引用实体的类型必须相同。
#include <iostream>
2 int main ()
3 {
4 int a = 10;
5 int & b = a;
6 b++;
7 std::cout << a <<std::endl;
8 std::cout << b <<std::endl;
9 return 0;
10 }
?结果上显示我们的 a和b 都变成了11。
const引用:
我们怎么理解这句话呢?我们的引用变量名和引用实体的类型必须相同。
首先我们先看一个例子:
#include <iostream>
int main ()
{
const int a = 100;
int& b = a;
a++;
std::cout << a <<std::endl;
std::cout << b <<std::endl;
return 0;
}
?现在就是我们不能够正常是使用啦,这里就涉及一些权限的东西啦,我们在使用指针或者引用时,我们引用的对象是有const限定的。此时我们的变量的权限就是不可修改的;而我们使用一个没有const限定的引用变量对象进行接收,就相当于我们把不可修改的权限去掉了,也就是我们的权限放大了,这样是不可以的。所以权限是不可以放大的!
我们在看一个例子:
#include <iostream>
int main ()
{
int a = 100;
const int& b = a;
a++;
std::cout << a <<std::endl;
std::cout << b <<std::endl;
return 0;
}
我们发现此时居然可以正常使用,为什么呢?
首先我们引用实体的类型没有 const 来修饰,然后我们的引用对象的类型经过 const来修饰了,这就相当于说我们不能通过修改引用对象的值来改变引用实体的值,但是我们可以通过引用实体的值来改变引用对象的值。这个叫做权限的缩小,权限虽然不能够放大,但是权限是可以缩小的。
我们在看一个例子:
#include <iostream>
int main ()
{
int a = 100;
const double& b = a;
a++;
std::cout << a <<std::endl;
std::cout << b <<std::endl;
return 0;
}
?
这个是为什么呢?我们一个int 类型的对象,我们居然可以使用 const 修饰的 double 类型进行接收。我们先看下这个const 修饰的double型变量是怎么形成的吧。
所以我们的a修改不会影响 b,因为b是 const 修饰的。
所以我们引用前加 const ,具有非常强的接受度。我们再使用引用传参时,如果函数内不改变参数的值,我们尽可能的使用 const 引用传参。??? ?
引用的特性:
1. 引用在定义时必须初始化 2. 一个变量可以有多个引用 3. 引用一旦引用一个实体,再不能引用其他实体 ?
我们可以对一个引用实体有多个引用对象,我们还可以对引用对象进行引用,如下:
#include <iostream>
int main ()
{
int a = 100;
int& b = a;
int& c = a;
int& d = c;
a++;
std::cout << a <<std::endl;
std::cout << b <<std::endl;
std::cout << c <<std::endl;
std::cout << d <<std::endl;
return 0;
}
??
?但是我们的引用一旦引用一个实体,再不能引用其他实体。如下:
#include <iostream>
int main ()
{
int a = 100;
int c = 1000;
int& b = a;
b = c;
std::cout<<a<<std::endl;
std::cout<<b<<std::endl;
std::cout<<&a<<std::endl;
std::cout<<&b<<std::endl;
return 0;
}
?
我们有两个引用实体,其中我们先让b来引用a,然后我们再把 b = c,这个步骤就是赋值,不是引用,我们从最后的打印地址也可以看出,此时b的地址和a的地址是一样的,说明此时b还是a的引用。
引用的使用场景:
1.做参数(输出型参数)(大对象传参和节省效率和指针一样)
引用做参数和指针类似,指针需要手动解引用使用,引用直接使用即可。
#include <iostream>
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int main ()
{
int left = 111;
int right = 999;
std::cout<<"left:"<<left<<std::endl;
std::cout<<"right:"<<right<<std::endl;
Swap(left,right);
std::cout<<"left:"<<left<<std::endl;
std::cout<<"right:"<<right<<std::endl;
return 0;
}
??
2.引用做返回值。
(做输出型返回对象,调用者可以修改返回对象)(提高效率)
这里我们就要简单提一下函数栈帧了。参考文章:函数栈帧的创建和销毁
传值返回:我们函数结束时,函数栈帧是要销毁的,同样我们在函数体内部的临时对象也是要销毁的,我们一般传值返回时,我们是把值拷贝到寄存器中,然后返回的,也就是默认返回值也销毁了,这是不影响我们传值返回;
传引用返回:如果我们今天要是传引用返回了,函数体内的临时变量什么的都销毁了,我们传递回去的引用对象是属于越界,我们在访问这些变量会导致出错,访问的结果是未定义的。那么我们怎么使用传引用返回呢?
#include <iostream>
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
std::cout << "Add(1, 2) is :"<< ret <<std::endl;
return 0;
}
?我们此时在g++下,直接报错,说返回临时变量c ,就是认为返回 c 的引用,这种行为是未定义的,如果我们在变量 c 前加上static ,把这个变量存储在静态区,就可以啦。
也就是说,我们使用引用返回时,如果变量出了函数的作用域就销毁了,那么我们不能使用引用返回,一定要使用传值返回。
引用和指针区别:
在语言角度来看,我们的引用没有开辟额外的空间,指针开辟了4 或者 8字节的空间,在底层来讲,引用可以说是指针的一种封装,汇编代码来看是一样的·。
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。 2. 引用在定义时必须初始化,指针没有要求 3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体 4. 没有NULL引用,但有NULL指针 5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节) 6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小 7. 有多级指针,但是没有多级引用 8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理 9. 引用比指针使用起来相对更安全
我们还是要结合具体的使用场景和语法特性以及底层原理来区分他们。
?内联函数:
首先我们C语言中和内联函数很像的语法就是宏,我们C++是支持C语言的,为什么我们还要有内联函数呢?肯定是宏不太好。
宏的优缺点?
优点:
1.
增强代码的复用性。
2.
提高性能。
缺点:
1.
不方便调试宏。(因为预编译阶段进行了替换)
2.
导致代码可读性差,可维护性差,容易误用。
我们内联函数只要正常的写函数,让我们我们注意下内联函数的特性就可以啦,内联函数兼容了宏的优点。
内联函数的特性:
1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替 换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。我们一般的函数是要在使用处进行调用的,然而我们内联函数,会在调用处进行展开,这样我们就不用进行创建栈帧。
如下所示:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
inline void func(int a,int b)
{
cout << (a + b) << endl;
}
int main()
{
int num1 = 1;
int num2 = 2;
func(num1,num2);
return 0;
}
?
2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、频繁调用的函数 采用inline修饰,否则编译器会忽略inline特性。
如下所示:
?
#include <iostream>
using namespace std;
inline void func(int a,int b)
{
cout << (a + b) << endl;
cout << (a + b) << endl;
cout << (a + b) << endl;
cout << (a + b) << endl;
cout << (a + b) << endl;
}
int main()
{
int num1 = 1;
int num2 = 2;
func(num1,num2);
return 0;
}
?
此时我们虽然也把函数设置为内联函数,但是我们编译器不实现,因为此时我们编译器认为这个函数太大了,所以此时这个函数还是一个普通函数,还是要进行函数的调用。
3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会 找不到。
此事会导致链接错误·error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (? f@@YAXH@Z),该符号在函数 _main 中被引用
?
注意:
1. 我们如果想把这个函数设置为内联函数时,我们应该不能把声明和定义分离。
?
2.如果我们在函数前extern了内敛函数,我们编译器就不会把这个函数设置为内敛函数,即使它符合内联函数的所有特点。它也是一个普通函数。
auto关键字:
随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
1.
类型难于拼写
2.
含义不明确导致容易出错
我们虽然可是使用 typedef? 来进行类型重命名,但是一些特殊的地方我们使用 typedef 会编译出错,那么我们可是使用auto来进行自动推导变量的类型,来使我们的写的过程简单不容易出错。
在早期
C/C++
中
auto
的含义是:使用
auto
修饰的变量,是具有自动存储器的局部变量
,但遗憾的是一直没有 人去使用它,大家可思考下为什么?
因为auto定义的变量具有自动存储期的变量在进入声明该变量的程序块时被建立,它在该程序块活动时存在,退出该程序块时撤销。在函数内部定义的变量成为局部变量。我们一般定义的局部变量都是auto的,我们一般不写。
C++11
中,标准委员会赋予了
auto
全新的含义即:
auto
不再是一个存储类型指示符,而是作为一个新的类型
指示符来指示编译器,
auto
声明的变量必须由编译器在编译时期推导而得。
如下:
int main()
{
int x = 10;
auto a = x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(x).name() << endl;
return 0;
}
我们是可以推出相对应的类型的,但是这样的意义不大。
auto的使用场景:
1. 我们一般在使用长类型名时才会用到auto进行推导。
2. 我们可以在范围for中使用,来完成自动推导,自动提取数组元素,我们使用auto &还可以完成对数组中的元素进行修改的功能。
如下:
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
for (auto e : arr)
{
cout << e << ' ';
}
cout<<endl;
for (auto &e : arr)
{
e++;
cout << e << ' ';
}
cout << endl;
return 0;
}
? ?
?我们这样写就比较简单,并且我们也可以对数组元素进行修改,只不过没办法随机访问啦。
auto的使用细节:
1.
使用
auto
定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导
auto
的实际类
型
。因此
auto
并非是一种
“
类型
”
的声明,而是一个类型声明时的
“
占位符
”
,编译器在编译期会将
auto
替换为
变量实际的类型
。
2.?
用
auto
声明指针类型时,用
auto
和
auto*
没有任何区别,但用
auto
声明引用类型时则必须加
&,我们使用auto*时重点强调它是一个指针类型,而我们使用auto &时我们重点强调它是一个引用。
3.
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对
第一个类型进行推导,然后用推导出来的类型定义其他变量。
?
auto不能推导的场景:
?1.不能作为函数的参数
?2.auto不能直接来声明数组
空指针问题:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
这是NULL的定义,其中__cplusplus是C++的宏,c++中NULL就是0,在一些特殊的情况下,这个NULL会导致调用错误。
其中,我们NULL是空指针,应该调用显示出 int *的,但是由于上述NULL的定义时0,所以出现调用匹配错误。如何解决这个问题呢,我们应该使用 nullptr ,这个是C++引入的关键字,我们可以直接使用,nullptr就是? ?(void*)0 。?
|