引用
int& pa = a; //c++的引用,就是起别名,a或ra都能访问这块空间
//类型& 引用变量名(对象名) = 引用实体
//引用类型必须和引用实体是同种类型;
int* pa = &a; //c的指针,符号一样,要区分开
初始化
- 引用在定义时必须初始化;
int& ra;//err
- 一个变量可以有多个引用
int& x = a; int& y = a; int& pa = a;
- 引用一旦引用一个实体,再不能引用其他实体(记住对引用变量进行赋值时,改的是引用和引用指向内容的值,而不是它的指向,因为引用不能再 引用其他实体)【
int a=10; int& ra=a; int b=20; ra=b;
这个代码其实是a=b;
】;
使用场景
有些场景有引用就可以不用指针,相当于指针的简化,但引用不能取代指针。
//1、做参数
void swap(int& m, int& n)//把实参的地址传给形参
{
int tmp = m;
m = n;
n = tmp;
}
int main()
{
int x = 10, y = 20;
swap(x, y);//m是x的别名,n是y的别名
}
//2、做返回值 引用返回,需为static
int& Count()
{
static int n = 0;//n在静态区,如果n不用static修饰却引用返回就会出错
n++;
return n;
}
int main()
{
int ret = Count();//main函数栈帧中接收返回值的变量ret会建立Count函数栈帧【栈是向下增长的】,ret是n的别名
cout << ret << endl;
cout << ret << endl;
return 0;
}
//注意:引用返回:临时变量的值是int&,没有额外开辟空间,相当于直接返回了n,此时函数栈帧内创建的变量需为static放在静态区的,不然普通的变量在函数栈帧里直接返回就会出现越界访问
//传值返回:传值调用的过程是 把n的值交给一个寄存器,寄存器再把这个值给ret。因为当n不是静态变量时就是存在于函数栈帧里的,当函数栈帧销毁时n不存在了无法直接给ret,想要给ret只能进入main栈帧,但是不能同时在两个栈帧里,所以必须要用到临时变量(寄存器或者返回值所在的函数栈帧)临时变量的类型是int,开辟了一个空间。可以不用static
//这个是返回值引用,主函数用引用接收
int& Count()
{
static int n = 0;//而n在静态区
n++;
return n;
}
int main()
{
int& ret = Count();//相当于n的引用再引用,会访问2次n的空间,第一次是Count返回时,第二次是ret初始化时。如若后续要重新开辟栈帧,这块空间就会被初始化。而n是在静态区,生命周期是整个程序
cout << ret << endl;
return 0;
}
内存空间销毁,意味着 空间还在,只是没有使用权,所存的数据不被保护,还能访问,只是我们读写的数据都是不确定的。故出了函数作用域,返回变量存在,才能引用返回。
请考虑一下单链表的尾插当时是使用二级指针来完成的,现在就可以用引用
void SLitstPushBack(struct SListNode** pphead, int x);
void SLitstPushBack(struct SListNode*& phead, int x);//引用
引用返回的意义:减少拷贝,提高效率;修改返回值。
关于常引用
1、使用时要注意指针和引用的权限可以平移和缩小,但不能放大。
int a = 10;
int& ra = a;//权限的平移
const int a = 10;
int& ra = a;//err,权限的放大
int a = 10;
const int& ra = a;//权限的缩小
const int a = 10;
const int& ra = a;//权限的平移
使用场景:假如在函数传参时,只想减少拷贝,不用修改实参值,就可以在函数声明时写成常引用。
2、类型不同时要加const
int main()
{
double b = 12.13;
const int& rb = b;
cout << "rb=" << rb << endl; // rb=12
cout << "&rb " << &rb << endl; // &rb 012FFE5C
cout << "b=" << b << endl; // b=12.13
cout << "&b=" << &b << endl; // &b=012FFE74
return 0;
}
类型转换(比如截断、整型提升、强制类型转换)的过程中,均会产生一个临时变量。在上述代码,a在转换成int时,会产生一个int类型的临时变量,临时变量具有常性不可修改,故只能给rra,rra引用的是临时变量,所以b的地址和rb的地址是不一样的。
3、传值返回,用常引用接收
int Count();
int main()
{
const int& ret = Count();
return 0;
}
因为传值返回,返回的是个临时变量,具有常性.
4、语法和底层
语法上引用不开空间,实际上底层实现上是开空间的,是使用指针实现的。
引用和指针的区别
- 引用概念上定义一个变量的别名,指针存储一个变量地址;
- 引用在定义时必须初始化,指针没有要求;
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
- 没有NULL引用,但有NULL指针;
- 在
sizeof
中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节); - 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
- 有多级指针,但是没有多级引用;
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理;
- 引用比指针使用起来相对更安全。
引用构成重载,但是实际用的时候用不了,会引起歧义。
auto
自动识别类型,自动判断结束,自动迭代。
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
//场景1
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange",
"橙子" },{"pear","梨"} };
std::map<std::string, std::string>::iterator it = m.begin();
//auto it = m.begin(); // 当类型过长,直接用auto
//场景2 范围for遍历
//以前写的
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
cout << array[i] << " ";
cout << endl;
// 用auto写的,依次取array中数据赋值给e 自动判断结束,自动迭代
for (auto e : array)
{
cout << e << " ";
}
cout << endl;
typeid(a).name()
可以拿到变量的类型,以字符串形式输出。
使用细则
1、auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须
加&
int x = 10;
auto a = &x; // a是int*
auto* b = &x; // b是int*
auto& c = x; // c是int
2、auto不能做参数,auto不能直接用来声明数组
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
3、在同一行对同一类型的变量同时进行初始化
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
内联函数
以inline
修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方用函数体替换函数的调用。没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
与宏的区别
在c语言中可以用宏函数;在cpp中用inline函数。但是宏的缺点是无法调试,没有类型安全的检查,容易写错(优先级,传表达式等都可能存在问题)。
C++有哪些技术替代宏?
- 常量定义 宏常量用const enum替换;
- 短小函数定义 用内联函数替换。
特性
1、inline是一种以空间(这里的空间指的是编译出来的可执行程序的大小)换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运
行效率。
2、inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建
议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不
是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
3、在debug版本下,内联不会展开,此时转成反汇编视图,也会看到call(建立函数栈帧的过程)。因为debug模式下,编译器默认不会对代码进行优化。
4、inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到。预处理和编译都能过。正常使用的内联函数,在调用时不会去call内联函数的地址,故内联函数定义所在的cpp文件中,看到inline,编译器认为这个内联函数在当前文件会直接展开,所以符号表里就不会存内联函数的地址。
预编译后,头文件在各文件展开。在链接前,各cpp文件不会相互联系。主文件里知道这个函数是inline,但是主文件里没有该函数的定义,想展开也展不开。那只能链接的时候去找,但是链接的时候没有该函数的地址也找不到它,所以会报链接错误。有声明没定义才会去找链接。
所以内联函数直接在.h文件定义即可。
指针空值nullptr
NULL实际是一个宏,在传统的C头文件(stddef.h)中
#ifndef NULL
#ifdef __cplusplus
#define NULL 0 // NULL被定义为字面常量0
#else
#define NULL ((void *)0) // 被定义为无类型指针(void*)的常量
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦。
故C++11引入nullptr
来解决问题。在C++11中,sizeof(nullptr)
与 sizeof((void*)0)
所占的字节数相同。
nullptr
就是 (void*)0