小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
c++系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
目录
一、引用
1.概念讲解
引用并不是新定义一个新的变量,引用是给已存在的变量取一个别名,在语法上,编译器并不会为引用变量新开辟一块内存空间,而是它和它引用的变量共用一块内存空间
同一个人有不同的称谓,即不同的名表示的是同一个人,那么这就构成了引用
例如:刘勇,别名王者荣耀听安,别名边路之怪,别名安天帝,别名边路之猴,别名大勇,这些别名表示的都是刘勇这个人,这些别名对刘勇这个人构成引用
2.使用方法
类型加&加引用变量名加=加引用实体
- 引用类型必须和引用实体类型相同
#include <iostream>
using namespace std;
int main()
{
int a = 0;
int& ra = a;//引用的定义
cout << a << endl;
cout << ra << endl;
cout << &a << endl;
cout << &ra << endl;
return 0;
}
观察运行结果,ra是对a的引用,ra和a的值都是0,并且ra和a的地址都相同,ra是a的别名,共用一块内存空间
3. 引用特性
- 引用在定义的时候必须初始化
- 一个变量可以有多个引用,正如一个人可以有多个别名
- 引用一旦引用一个实体,就不能引用其它实体
int main()
{
int a = 0;
//int& b;//引用在定义的时候如果不初始化,编译器会报错
int& ra = a;
int& rra = a;
int& c = a;
cout << a << " " << & a << endl;
cout << ra << " " << &ra << endl;
cout << rra << " " << &rra << endl;
cout << c << " " << &c << endl;
int x = 11;
ra = x;
cout << &a << endl;
cout << &x << endl;
cout << &ra << endl;
return 0;
}
- 观察a和ra和rra和c的值和地址都相同,即一个变量a可以有多个别名ra,rra,c
- 我们定义一个x变量,并且使用a的别名ra尝试,尝试让ra指向x,在ra引用完实体a后,再次引用实体x,运行代码,使用赋值尝试让ra进行引用其它实体x失败,ra的地址仍然和a的相同,和x的地址不同,即ra仍然是a的引用,即引用一旦引用实体后,不能再引用其它实体了
- 但是这里的操作就变成了赋值,ra是a的别名,就相当于将x的值11赋值给ra,ra的值改变成为11,而ra又是a的别名,ra和a共用一块内存空间,两者的值改变任意一个都会造成值的改变
4.const常引用
涉及到这一块的引用通常都会有权限的放大或缩小或平移的问题
对于常量的权限是只读的,对于变量的权限是可读可写的
在引用的过程中,权限只能平移或缩小,切记不可以权限的放大
int main()
{
//int& a = 10;
const int& ra = 10;
const int& b = 10;
//int& c = b;
const int& rb = b;
double d = 1.1;
//int& e = d;
const int& rd = d;
return 0;
}
错误演示
错误讲解以及正确做法讲解
4. 引用的使用场景
1. 做参数
使用引用做参数的典型用例就为Swap交换函数,通过引用做参数的方法,使Swap函数的参数ra,rb分别作为函数外的变量a,b的别名,通过函数内部的操作即可影响函数外面的变量,这种参数称为输出型参数
- 引用做参数的好处
- 减少拷贝,提高效率(大对象/深拷贝类对象,这个请关注小编后续文章更新)
- 可以作为输出型参数
void Swap(int& ra, int& rb)
{
int c = ra;
ra = rb;
rb = c;
}
int main()
{
int a = 0;
int b = 1;
cout << a << endl;
cout << b << endl;
Swap(a, b);
cout << a << endl;
cout << b << endl;
return 0;
}
2. 做返回值
常规的int作为返回值通常都是会有一个临时拷贝当作返回值进行返回
这里使用int&作为返回值即返回一个c的别名,这个c的别名和c共用同一块内存空间
- 引用做返回值的好处
- 减少拷贝,提高效率(大对象/深拷贝类对象,这个请关注小编后续文章更新)
- 修改返回值、获取返回值
- 正确的引用返回
int& Add(int a, int b)
{
static int c = a + b;
return c;
}
int main()
{
int a = 11;
int b = 22;
int& c = Add(a, b);
cout << c << endl;
return 0;
}
- 错误的引用返回
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int a = 11;
int b = 22;
int& c = Add(a, b);
cout << c << endl;
return 0;
}
5.引用和指针的区别
在语法含义上,引用是变量的别名,没有独立的空间,和引用实体共用同一块空间
但是在底层的实现上,引用是有空间的,引用的实现是按照指针的方式来实现的
//引用
int main()
{
int a = 0;
int& ra = a;
return 0;
}
//指针
int main()
{
int b = 0;
int* rb = &b;
return 0;
}
引用和指针的区别
引用 | 指针 |
---|---|
引用是一个变量的别名 | 指针是存储一个变量的地址 |
引用在定义时必须初始化 | 指针在定义时可初始化也可不初始化 |
引用在初始化时引用了一个实体就不能引用其它实体了 | 指针可以在任何时候可以指向任意一个同类型的实体 |
引用没有NULL引用 | 指针有NULL指针 |
在sizeof中引用求得的是引用类型的字节数大小 | 在sizeof中指针求得是地址空间所占的字节数大小 |
续 | (32位平台下是4个字节,64位平台下是8个字节) |
引用自加为引用实体元素加一 | 指针自加为地址向后偏移一个类型的大小 |
没有多级引用 | 有多级指针 |
引用访问实体需要编译器处理 | 指针访问实体需要进行解引用操作 |
引用不存在NULL的可能也就变相的避免了类似野指针非法访问内存(引用相对指针来讲安全) | 指针存在NULL的可能会出现数组越界/野指针/非法访问内存(指针相对引用来讲不安全) |
二、内联函数
1.概念介绍
使用inline修饰的函数被称为内联函数,内联函数会在调用的地方直接展开,没有调用函数栈帧的开销,提高程序效率
2. 内联函数的使用
- 当Add函数没有inline修饰的时候,我们将程序转到反汇编,有类似于call+Add(地址)的形式即为调用了函数,此时需要开辟栈帧,压栈,寄存器等等操作会有消耗
#include <iostream>
using namespace std;
int Add(int a, int b)
{
return a + b;
}
int main()
{
cout << Add(1, 2);
return 0;
}
2. 当Add函数有inline修饰的时候,我们将程序转到反汇编,没有有类似于call+Add(地址)的形式,即调用函数的地方直接展开了,没有开辟栈帧
#include <iostream>
using namespace std;
inline int Add(int a, int b)
{
return a + b;
}
int main()
{
cout << Add(1, 2);
return 0;
}
观察我们程序的反汇编,没有出现call+Add(地址)的形式,即我们调用内联函数Add其已经在我们调用的地方直接展开了
3. 内联函数的特点
- 内联函数是一种以空间换时间的方法,在编译阶段,编译器将函数当作内联函数直接展开,采用函数体代替了调用函数,减小了调用函数开辟栈帧的消耗,提高了程序的效率
- 但是内联函数会导致代码膨胀的问题,即最后生成的可执行程序.exe变大
3. 所以为了避免这样的情况,当函数过长的时候,我们不要将其设置成为内联函数,这个过长衡量的标准是10行代码以内,短小且经常调用的函数,我们可以将其设置成内联函数
4. 那么如果说我们的代码量较长比如15行,我们将其设置成为内联函数再使用编译器进行调用,那编译器还会不会将我们调用内联函数的地方直接展开呢?
- 例如我们使用下面这种场景来测试一下
#include <iostream>
using namespace std;
inline int Add(int a, int b)
{
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
cout << "hello cpp" << endl;
return a + b;
}
int main()
{
cout << Add(1, 2);
return 0;
}
当我们设置的内联函数的代码行数长,为了避免可能程序中有多次调用内联函数而造成代码膨胀的风险,编译器并没有按我们的代码行指令将我们本设置成为内联函数的Add函数在调用的地方进行展开,所以我们设置的内联函数对编译器来说仅仅是一种建议,是否采用取决于编译器
- 通常来讲内联函数不适合递归,代码行过长的函数,适用于代码行较短(小于10行)且在程序中频繁调用的函数
- 对于预处理、编译、汇编、链接不了解的友友请点击<—
- 对于内联函数,声明和定义不能分离,如果仅仅是声明有内联inline,由于各文件是分开独自进行编译链接,那么在编译期间,遇到内联函数,编译器会将其展开,展开后函数就没有了地址,那么在函数名也就不会被汇总,进而函数名也就不会进入符号表,也就不会进行链接,也就不会进行调用,所以内联函数我们通常是声明和定义不分离
对比宏,inline的优缺点都有什么
- 宏
- 优点,代码的复用性高,直接替换效率高
- 缺点,复杂易出错,不能调试,没有类型安全的检查
- 在c++中短小且频繁调用的函数通常我们都使用inline替代宏
- 优点,可以调试,直接替换效率高
- 缺点,可能会导致代码膨胀
三、auto关键字(c++11)
在c++11中,标准委员会规定,auto不再是一个储存类型提示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译阶段推得
1.auto能使用的场景
- auto的使用方法是auto加变量名加=加要赋值的变量
- auto在使用时必须初始化, 在编译阶段编译器需要根据auto的初始化表达式推导auto的实际类型,因此auto并非类型的声明,而是一种类型声明的占位符,在编译阶段编译器会将auto替换为变量实际的类型
- auto声明指针类型的时候使用auto和auto*都可以,但是在声明引用类型的时候必须要使用auto&才可以
- auto可以推导调用完函数之后函数返回的值的类型
- auto同一行声明多个变量的时候,必须保证变量的类型相同,因为实际上编译器只对第一个变量的类型进行推导,推导完成后使用第一个变量类型推导的结果去初始化全部变量的类型
- 当某些类型过长的时候,我们也可以使用auto进行替代,例如后续将要学习的vector的反向迭代器的类型
- auto同样可以搭配范围for来进行使用,下一个模块小编就会讲解范围for
#include <vector>
int Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int a = 0;
auto b = a;
auto c1 = &a;
auto* c2 = &a;
auto d = 'a';
auto e = Add(1, 2);
auto f = 1, g = 2, h = 3;
auto& i = a;
vector<int> v;
vector<int>::reverse_iterator rit1 = v.rbegin();
auto rit2 = v.rbegin();
cout << typeid(b).name() << endl;
cout << typeid(c1).name() << endl;
cout << typeid(c2).name() << endl;
cout << typeid(d).name() << endl;
cout << typeid(e).name() << endl;
cout << typeid(f).name() << endl;
cout << typeid(g).name() << endl;
cout << typeid(h).name() << endl;
cout << typeid(i).name() << endl;
cout << typeid(rit1).name() << endl;
cout << typeid(rit2).name() << endl;
return 0;
}
这里我们可以使用typeid(变量).name()来获取变量的类型进行打印
auto Add(double a, double b)
{
return a + b;
}
int main()
{
cout << Add(1.1, 2.2) << endl;
return 0;
}
- 运行代码,auto可以用于推导函数的返回值
2.auto不能进行推导的场景
1. auto不能用作函数参数
2. auto不能用作推导数组
//小编是在演示错误,请读者友友们在实际不要这样使用auto
int Add(auto a, auto b)
{
return a + b;
}
int main()
{
auto arr[] = {
1,2,3,4 };
return 0;
}
四、范围for循环的简单使用(c++11)
- 范围for循环迭代范围必须确定
如何使用? - 通常来讲,范围for是搭配auto来进行使用的,由于我们可能要循环遍历各种类型的数组,这样使用auto推导数组中变量的类型可以以不变应万变
- for(auto加空格加变量名加:加数组名)
{
内容
} - 特别注意,如果要进行修改数组的值,对于使用auto推导的应该变成auto&推导引用
int main()
{
int arr[5] = {
1,2,3,4,5 };
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
for (auto& e : arr)//对数组进行修改应该传引用
{
e *= 2;
}
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
return 0;
}
五、nullptr空指针
- 观察如下代码,我们想使用Func(NULL)去调用void Func(int* a),那么会不会成功呢?
void Func(int a)
{
cout << "void Func(int a)" << endl;
}
void Func(int* a)
{
cout << "void Func(int* a)" << endl;
}
int main()
{
Func(0);
Func(NULL);
return 0;
}
我们本意是想使用Func(NULL)去调用void Func(int* a),观察程序运行结果,却去调用了void Func(int a),这是因为在c语言中NULL其实是被定义为宏即#define NULL 0或者#define NULL (void*)0,但是通常来讲,编译器处理NULL进行常量求值的结果都为0的整形常量表达式,所以在编译期间NULL会被宏替换为0,在编译器运行后就会被调用成void Func(int a),所以调用失败
- 那么在c++中,通常在可以使用NULL的地方我们都是使用nullptr进行替换
- 观察如下代码,我们想使用Func(nullptr)去调用void Func(int* a),那么会不会成功呢?
void Func(int a)
{
cout << "void Func(int a)" << endl;
}
void Func(int* a)
{
cout << "void Func(int* a)" << endl;
}
int main()
{
Func(0);
Func(NULL);
Func(nullptr);
return 0;
}
使用使用sizeof求出字节数sizeof(nullptr)和sizeof((void*)0)的字节数相同
小编是在是64位环境下进行的测试,指针的大小位8个字节
调用成功,因为使用使用sizeof求出字节数sizeof(nullptr)和sizeof((void*)0)的字节数相同
所以在调用的时候Func(nullptr)就去调用了void Func(int* a)
- 所以在c++中,为了避免类似上述情况发生或其它异常情况,我们通常都是使用nullptr来替代NULL
总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!