C++引用
引言
本贾尼·斯特劳斯特卢普(Bjarne Stroustrup)可能嫌弃指针用起来麻烦,经常会出现潜在的错误和其本身的复杂性,于是就创建了引用,引用的符号为&,学过C语言的都知道,&这个符号在C语言中表示取地址,但&在C++中又增添了一个新定义,就是引用, 它在一定意义上代替了指针,但又和指针有所区别
引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
引用特性
- 引用会改变引用变量的值
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;
b = 5;
cout << "a = " << a << " " << "b = " << b;
return 0;
}
- 引用在定义时必须初始化
#include<iostream>
using namespace std;
int main()
{
int a;
//int& c;//没有初始化,编译会报错
int& b = a;
return 0;
}
- 一个变量可以有多个引用
#include<iostream>
using namespace std;
int main()
{
int a;
//int& c;//没有初始化,编译会报错
int& b = a;
int& c = a;
return 0;
}
- 引用一旦引用了一个实体,就不能引用其他实体
常引用
const修饰的变量在称为常量,存储在常量区,在对变量进行引用的时候,需要注意变量是否具有常性,以及是否创建了临时变量,因为临时变量具有常性
- 如果一个不具有常性的变量对另一个具有常性的变量取别名就会报错,这是权限的放大
#include<iostream>
int main()
{
const int a = 10;
int& b = a;//不可以,是权限的放大,由可读可写变为可读,b对a的值可以进行改变
int c = 10;
const int* p1 = &c;
int* p2 = p1;//不可以,是权限的放大
}
- 如果一个具有常性的变量对另一个不具有常性的变量取别名就不会报错
#include<iostream>
int main()
{
int a = 10;
const int& b = a;//可以是权限的缩小
int* p1 = &a;
const int* p2 = p1;//可以,是权限的缩小
}
- 类型转换会创建一个临时变量,临时变量具有常性
#include<iostream>
int main()
{
double a = 1;
int& b = a;
//不可以,类型转换时,
//a就会创建一个临时变量空间来存储值,临时变量具有常性,
//而b不具有常性,是权限的放大
}
- 表达式求值会创建一个临时变量,临时变量具有常性
#include<iostream>
int main()
{
int b = 10;
int c = 20;
int& a = b + c;//不可以,b+c要先创建一个临时变量存储,临时变量具有常性,转换为int& ,是权限的放大,不可以
}
- 赋值不考虑是否具有常性
#include<iostream>
int main()
{
const int a = 10;
int p = a;//可以,这里是拷贝,不会影响a的值
}
引用的底层
引用在汇编层面其实使用指针来实现的,但其在语法上并不是指针
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;
return 0;
}
- 00801A15 lea eax,[a]
lea 是 “Load Effective Address” 的缩写,用于将一个内存地址加载到寄存器中。
eax 是一个常用的32位寄存器,用于存储操作的结果。
[a] 表示一个内存地址,这里 a 是一个标签,代表某个具体的内存位置。
这条指令的作用是将标签 a 所指向的内存地址加载到 eax 寄存器中。 - 00801A18 mov dword ptr [b],eax
mov 是 “Move” 的缩写,用于数据传送。
dword ptr 表示操作的对象是一个双字(32位)的内存位置。
[b] 表示另一个内存地址,这里 b 是另一个标签,代表另一个具体的内存位置。
eax 是源数据,即要传送的数据。
这条指令的作用是将 eax 寄存器中的值复制到标签 b 所指向的内存位置。
使用场景
- 做参数
比如用引用来实现Swap函数
#include<iostream>
using namespace std;
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int a = 10;
int b = 5;
Swap(a, b);
cout << "a = " << a << " " << "b = " << b;
return 0;
}
- 做返回值
#include<iostream>
using namespace std;
int& Count()
{
static int n = 0;
cout << n << endl;
// ...
return n;
}
int main()
{
int& a = Count();
a = 5;
Count();
return 0;
}
注意事项:
#include<iostream>
using namespace std;
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;
return 0;
}
以上代码是错误的,如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
传值和传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
//传引用的效率比传值的效率高
#include<iostream>
using std::cout;
using std::endl;
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
int main()
{
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;
}
指针和引用的区别
- 引用是对变量取别名,而指针存储变量的地址
- 引用必须初始化,而指针没有要求
- 引用不存在空引用,而指针存在空指针
- 引用在初始化引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- sizeof的含义不同,引用结果为引用类型大小,但指针是地址空间所占字节个数(和平台有关)
- 引用自加,实体加一,指针自加,向后偏移一个类型的大小
- 有多级指针,没有多级引用
- 访问的实体方式不同,指针需要显示解引用,引用需要编译器自己处理
- 引用比指针使用起来相对安全:
9.1 指针对初始化没有要求,会造成野指针,不能野指针进行访问,
而引用一旦初始化引用一个对象就不能被改变,这种特性使其访问更加安全
9.2 不存在空引用,引用必须与合法的存储单元关联,而指针可以是NULL,对空指针进行解引用,会造成程序崩溃,而尝试解引用一个未初始化的引用就会导致编译错误,从而避免运行是错误
9.3 在二进制层面,引用一般是通过指针实现,但编译器自动会帮我们完成转换,使得引用更加安全
内联函数
内联函数概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
内联函数展开:
内联函数特点
-
inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
-
inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
#include<iostream>
using std::cout;
using std::endl;
inline int Add(int left, int right)
{
int ret = left + right;
cout << ret << endl;
cout << ret << endl;
cout << ret << endl;
cout << ret << endl;
cout << ret << endl;
cout << ret << endl;
cout << ret << endl;
cout << ret << endl;
cout << ret << endl;
cout << ret << endl;
return left + right;
}
int main()
{
int ret = 0;
ret = Add(1, 2);
std::cout << ret;
return 0;
}
这里并不会展开,内联展开太长,编译器不会展开。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到。
test.cpp
#include<iostream>
#include"test.h"
int main()
{
int ret = 0;
ret = Add(1, 2);
std::cout << ret;
return 0;
}
test.h
inline int Add(int left, int right);
stack.cpp
#include"test.h"//包含头文件才会发生链接错误
int Add(int left, int right)
{
return 1;
}
由于内联函数没有地址,在进行链接的过程中就会发生链接错误,所以inline建议声明和定义不分离,写到一起。
在C语言中也有类似于内联的宏
宏的优缺点:
1. 优点:
1.增强代码复用性
2,提高性能
2. 缺点:
1.不方便调试宏。(因为在预编译阶段进行了替换)
2.代码可读性差,维护性差,容易误用
3.没有类型安全的检查
用宏实现Swap函数
#include<iostream>
using namespace std;
//第一种方法
#define Swap(a,b,type) do {\
type (tmp) = (a);\
(a) = (b);\
(b) = (tmp);\
} while (0);
//\表示续行符
//第二种方法
//#define Swap(a,b) (a) ^= (b) ^= (a) ^= (b)
//第三种方法
//#define Swap(a,b) ((a) = (a) + (b),(b) = (a) - (b),(a) = (a) - (b))
int main()
{
int a = 2, b = 98;
//Swap(a, b ,int);
Swap(a, b);
cout << a << " " << b;
return 0;
}
aotu关键字
auto的含义
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一
个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
auto使用
typeid使用来判断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定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
auto使用细则
- auto与指针和引用结合起来使用用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
#include<iostream>
using namespace std;
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = &a;
//指定的必须是指针
//auto* e = a;//编译错误
auto* e = &a;
auto f = (float)1.02;
cout << typeid(b).name() << endl;//用来判断auto类型
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
cout << typeid(e).name() << endl;
cout << typeid(f).name() << endl;
}
- 在同一行定义多个变量当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0;//编译错误
return 0;
}
- auto不能作为函数的参数
void test(auto i)
{}
int main()
{
test(1);
return 0;
}
- auto不能用来直接声明数组
int main()
{
int a[] = { 1,2,3 };
auto b[] = { 4,5,6 };
return 0;
}
基于范围的for循环
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因
此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范
围内用于迭代的变量,第二部分则表示被迭代的范围。
#include <iostream>
using namespace std;
int main()
{
int a[] = { 1,2,3,4,5 };
for (auto& e : a)//改变a的值
{
e *= 2;
}
for (auto e : a)
{
cout << e << " ";
}
return 0;
}
范围for的使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供
begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
#include<iostream>
using std::cout;
void test(int a[])
{
for (auto e : a)//编译错误,数组传参传的是数组首元素的地址,范围不确定
{
cout << e << " ";
}
}
int main()
{
int a[] = { 1,2,3,4,5 };
test(a);
return 0;
}
- 迭代的对象要实现++和==的操作
空指针nullptr
在C/C++中NULL其实就是一个宏,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
这就可能会遇到一点麻烦,比如:
#include<iostream>
using namespace std;
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)是调用的是第一个函数,编译器把NULL当做0。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器
默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void
*)0。
为了弥补这个漏洞,在C++11中nullptr作为新关键词引入,用来表示指针空值
在C++11中,sizeof(nullptr)与sizeof((void)0)所占字节数相同
为了提高代码的健壮性,后续表示指针空值时建议最好使用nullptr*