引用概念
引用概念:引用不是新定义一个变量,而是个已存在变量取了一个别名(外号),编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间 比如:李逵,在家称为"铁牛",江湖上人称"黑旋风" (李逵=="铁牛" =="黑旋风" )
语法:
类型说明符& 引用变量名(对象名)=引用实体(不能是常量) 引用类型必须和引用实体是同种类型
void TestPef()
{
int a=10;
int& pa=a;//pa是a的别名
printf("&a==%p\n", &a);
printf("&pa==%p\n", &pa);
//从地址上,可以得出它和它引用的变量共用同一块内存空间
}
这就说明了修改对象a的也会影响对象pa,同理反之。
void TestPef()
{
int a=10;
int& pa=a;//pa是a的别名
a++;
printf("a==%d pa==%d\n", a,pa);//11 11
pa++;
printf("a==%d pa==%d\n", a,pa);//12 12
}
引用特性:
-
引用在定义时必须初始化(否则在编译阶段会报错)
-
引用一旦引用一个实体,再不能引用其他实体
-
一个变量可以有多个引用,引用变量也可以取别名
int main()
{
// 1.引用必须初始化
int a=10;
int& b;//(否则在编译阶段会报错)
b=a;
//2.引用定义后,不能改变指向(这一点很重要)
int& b=a;
int c=10;
b=c;//不是改变指向,而是b(a)赋值为10
//3.一个变量可以有多个别名,别名也有别名
int &d=b ;//d是b的别名,b是a的别名,则d是a的别名
return 0;
}
常引用(权限可以缩小,但是不能放大)
void TestConstRef()
{
int a=0;
int& b=a;
//支持-权限缩小
const int& c=a;
//不支持-权限放大
const int x=10;
int& y=x;//此时的x只有读权限,没有写权限
const int& y=x;
//表达式的返回值是临时对象,而临时对象具有常性
int& n=a+x//这里是属于权限放大
const int& n=a+x;
}
引用的使用场景:
一、做参数(a、输出型参数 b、对象比较大,减少拷贝,提高效率)
如果参数是指针类型,使用时需要对其解引用操作,但是使用引用可以避免解引用操作
void Swap(int* x,int* y)
{
int tmp=*x;
*x=*y;
*y=tmp;
}
void Swap(int& x,int& y)
{
int tmp=x;
x=y;
y=tmp;
}
void PushBack(struct Node**pphead,int x)
{
*pphead=newndoe;
}
void PushBack(struct Node* & pphead,int x)
{
pphead=newndoe;
}
在调用效率方面也有了很大的提升
#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;
return 0;
}
二、做返回值(a、修改返回对象 b、减少拷贝提高效率)
首先先说明,以是引用做返回值的错误用法
int& func()
{
int a = 0;
return a;
}
int main()
{
int ret = func();//第一种
int ret = func();//第二种
cout << ret << endl;
return 0;
}
解释:第一种,当func()函数运行结束,函数内存将被系统收回,int&做为返回值,返回的是a的别名,ret取出了这块空间的值,但是这空间到底是什么?可能有两种结果,编译器是否对这块函数栈帧清空,对此这样子操作是不具备安全性的。
第二种,如果再通过引用接收,ret就是指向这块已经被回收的空间,这样属于野引用,访问这块空间,好比成租房子,当房子合同到期,如果非法进入会出现不安全的影响。
关于编译器是否对该函数栈帧清空,可以看一个有意思的东西
解释:对于ret值为0,说明编译器没有对函数栈帧清空,当调用fx()函数时,ret值为随机值,函数栈帧已经清空过。但是当引用做返回值,ret的值神奇等于了b的值,以为函数申请的空间是可以复用的。
结论:返回变量出来函数的作用域就生命周消耗(局部变量要消耗,就不能引用返回)
下面是引用做返回值的正确用法
返回值不采用局部变量,那么可以使用全局变量、静态变量、堆上变量等返回。(类/结构体中可以直接访问变量,不同传递this指针,临时变量具有常性)
比如实现修改顺序表任意位置的数值,需要两个步骤通过下标找到数据,并对数据进行修改,但是通过引用可以一步找到数据返回直接修改。
值和引用的作为返回值类型的性能比较
//这里对于将变量和函数定义在结构体中,这样子的话变量出了函数的作用域生命周期也不会销毁
引用和指针的区别
在语法概念上,引用是一个别名,没有独立空间,同其引用实体共用同一块空间,但是在底层实现上,实际引用是有开辟空间的,因引用是按照指针方式实现的。
int main()
{
int a=10;
int& ra=a;
cout<<"&a"<<&a<<endl;
cout<<"&ra"<<&ra<<endl;
return 0;
}
可以引用和指针的汇编代码对比下
指针和引用的功能类似的,有重叠的
C++的引用,对于指针使用比较复杂的场景进行一些替换,让代码更简单易懂,但是不能完全替换指针:引用定义后,不能改变指向
引用和指针的不同点
1.引用概念上定义一个变量的别名,指针存储一个变量地址
2.引用在定义时必须初始化,指针没有要求
3.引用在初始化引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4.没有NULL引用,但是有NULL指针
5.在sizeof中含义不同:引用结果为引用类型的大小,但是指针始终是地址空间所占字节个数(32位平台下占4个字节)
6.引用自加既引用的实体增加1,指针自加即指针后偏移一个类型的大小
7.有多级指针,但是没有多级引用
8.访问实体方式不同,指针需要显示解引用,引用编译器自己处理
9.引用比指针使用起来相对更安全
内敛函数
内敛函数的概念
内敛函数:以关键字inline修饰的函数,编译时C++编译器会在调用(内敛)函数的地方展开。对此没有函数调用建立栈帧的开销,内敛函数可以提升程序运行的效率,但是内敛函数也是需要代价的。
注:这本身是一种代码优化行为,可以在release模式下,参考call Add函数地方,是否被展开。如果需要在debug模式下,需要对编译器进行设置,否则不会展开(在debug模式下,编译器默认不会对代码进行优化,可以通过解决任务管理器->配置->C/C++->常规(程序数据库)->优化->内联函数扩展->只适用于_inline)
内敛函数的特性;
1.inline是一种以空间换时间的做法,如果编译器将函数当成内敛函数处理,在编译阶段,会用函数体代替函数调用(优势:避免了函数调用开销,提高程序运行效率,缺陷:可能会使目标文件变大)
2.inline对于编译器而言只是一个建议/请求,编译器可以选择忽略这个建议/请求,不同编译器关于inline实现机制可能不同。
3.inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址,链接中就会找不到,推荐声明和定义一起放在头文件中。
//F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
第一点:内敛函数展开,编译程序空间变大(指令变多),空间是编译好的可执行程序,并不是我们理解的内存。这就导致个问题,当1000个行需要调用swap函数,如果swap不是内敛函数,合计是10+1000(这里10是swap内部行数),如果swap是内敛函数,合计是10*1000。对此就出现了可能使得目标文件变大。
第二点;不同编译器对内联函数展开行数限定不同,但是内敛是有限制的,大函数和递归是不会展开,而是调用该函数使用
第三点:
问题:为什么需要声明和定义分离
在声明这里定义Add函数,在预处理阶段会被展开,那么两个.cpp文件都有一份Add函数,就会出现命名冲突。对此需要声明和定义分离
那么如果我就想要在.h定义Add函数怎么办呢?可以使用static关键字,使用函数的外部链接属性转为内部链接属性,只在当前文件可见,简单来说就是不进符号表,就没有命名冲突了。
同时也可以使用内敛函数解决,如果在.cpp定义内敛函数,因为内敛函数不需要调用函数,就没有函数的地址,简单来说也是不会进符号表,对于需要在其他文件中需要使用该函数,会发生链接错误。对此,需要将内敛函数的声明和定义放在一块,在头文件展开并且链接时不会出现命名冲突。
对于三种方法的使用推荐:
1.如果是大函数,可以使用声明和定义分离、static修饰2.如果是小函数,可以使用内敛。
【面试题】
宏的优缺点?
优点:
增加代码的复用性
提高性能
缺点:
不方便调式宏(预处理阶段进行了替换)
导致代码可读性差,可维护性差,容易误用
没有类型安全的检查
C++有哪些技术替代宏?
常量定义换用const enum
短小函数定义 换用内敛函数
auto关键字(C++11)
前文:
由于程序中使用到的类型也越来越复杂,导致了类型难于拼写,含义不明确导致容易出错。
比如:std::map<std::string, std::string>::iterator 是一个类型,但是该类型太长了,特别容易写错。当然可以使用typedef给类型取别名,简化代码。同时这里也有属于typedef的不足。
typedef的陷阱
typedef char * pstring;
int main()
{
const pstring p1//编译成功还是失败?-->失败
const pstring* p2//编译成功还是失败?-->成功
return 0;
}
首先需要提出一个问题:“const pstring”是否等于const char *。答案是这两个是不等于的,这里const pstring中的const给予了整个指针本身常性,并且形成了常量指针(char * const).对此,在无论什么时候,只要指针声明typedef,那么在typedef名称加一个const使得指针本身是常量。对于p2来说,const pstring* p2转化后是char* const *p2,那么p2是个二级指针,所以p2不初始化也是可以的。
关于typedef不足
1.如果使用typedef过程中,给const扯上关系,使用上容易出现问题,这里不是简单的字符串替换
2.虽然 typedef 并不真正影响对象的存储特性,但在语法上它还是一个存储类的关键字,就像 auto、extern、static 和 register 等关键字一样
typedef static int INT_STATIC;
不可行的原因是不能声明多个存储类关键字,由于 typedef 已经占据了存储类关键字的位置,因此,在 typedef 声明中就不能够再使用 static 或任何其他存储类关键字了
auto简介:
在早期C/C++中auto的含义是:使用auto修饰的变量,该变量具有自动存储器的局部变量,但是没有人使用它,因为意义不大,这里指向的是局部变量,那么当函数结束,局部变量出了作用域,生命周期结束,变量会自动销毁,对此使用没有意义。
C++11中,auto被赋予了新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译中推导而得。
打印变量类型办法(了解即可)
typeid(对象名).name()可以得到变量的类型
auto的使用(自动识别类型)
使用auto定义变量时,该便两个必须对其进行初始化,在编译阶段编译器需要根据变量初始化表示来推导auto的实际类型,因此auto并非是一种“类型”的声明,而是一个类型声明的“占位符”,编译器在编译期间会将auto替换为变量实际的类型,从右到左,推导变量的类型。
auto与指针和引用结合起来使用。当auto声明指针类型时,用auto和auto*没有任何区别(如果使用auto *,则表示指向的变量是指针类型)但用auto声明引用类型时,则必须加&
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto d = TestAuto();
auto d=&a;
return 0;
}
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际上只会对第一个类型进行推导,然后用推导出来的类型定义其他变量。
在使用的过程中,需要知道右边上是什么类型的,如果不熟悉,会影响可读性,在auto做返回值有所体现。
对于上面这样子使用auto是没有价值的,主要还是对于类型很烦很长,使用auto进行自动推导类型。
auto不能推导的场景
1.auto不能作为函数的参数
2.auto不能直接用来声明数组
3.为了避免与C++98中auto混淆,C++11只保留了auto作为的类型指示符的用法
4.auto在实际中最常见的优势用法,就是跟新试for循环,lambda表达式等进行配合使用。
auto不能作为函数的参数,但是可以做函数的返回值,但是需要慎用auto去做返回值,跟可读性有关系,以下为例,如果多层嵌套函数,返回值都是auto,那么得到返回值的类型,需要一个个函数去检查。
10.指针空值
在C/C+良好的编程习惯中,对于未初始化的指针,一个没有合法的指向的指针,基本会进行初始化。int *p=NULL\0
。对此在C头文件<stddef.h>中,可知NULL实际是一个宏。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
对此NULL可能定义有两种(字面常量0,无类型指针(void*)的常量),cpp中使用NULL指针空值,可能会遇到一些问题。
void f(int)
{
cout << "haha" << endl;
}
void f(int *)
{
cout << "hehe" << endl;
}
int main()
{
f(0);
f((int*)NULL);
f(NULL);
return 0;
}
按照预期,传NULL应该调用int *的函数,但是NULL被定义成0,对此预期和结果不匹配。对此为了区分Cpp和C,cpp引出了nullptr关键字代替NULL的使用。
在C++98中,字面常量0既可以是一个整型数字,也可以是无类型的指针(void *)常量,但是编译器默认情况下将其看成是一个整型常量,如果要将其按照指针方式来使用,必须强制类型转换。
注:
1.nullptr是C++11作为新关键字引入的,在使用过程中,不需要包含头文件
2.在C++11中,sizeof(nullptr)与sizeof((void *)0)所占的字节数相同
3.为了提高代码的健壮性,在后续cpp使用中最好使用nullptr表示指针空值
- 点赞
- 收藏
- 关注作者
评论(0)