C++入门(lesson2)(上)

简介: C++入门(lesson2)

一、引用🐅


1.1 引用概念🐱


引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间(理论上,实际并非如此,后面细说),它和它引用的变量共用同一块内存空间。

引用,就好像给一个人取了个外号,就像古代的人有名和字,都是指代他这个人。

引用的使用: 类型& 引用变量名(对象名)=引用实体。

1669254088862.jpg

引用就是如此,它们指代的都是同一块空间,所以地址肯定是相同的。


不过需要注意的是:引用类型必须和引用实体是同种类型的。


1.2 引用特性🐱


1、引用在定义时必须初始化。


2、一个变量可以有多个引用。(一个人可以有很多的外号)


3、引用只能指向一个空间,不能改为其他空间。


4、引用没有开辟空间,如果对它赋值,是改变了引用指向空间的内容。


5、不能利用引用去做链表,因为引用不能更改指向,在C++中,引用还没完全取代指针,而Java是可以脱离指针的。


1.3引用的使用🐱


①做参数😀

在C语言阶段我们要交换两个数的值,常常需要传他们的地址,通过地址来改变他们的值,在提出引用之后,我们不需要传地址了。

void Swap(int& left, int& right)
{
  int temp = left;
  left = right;
  right = temp;
}

同样的,通过使用引用,我们有些地方可以没必要使用二级指针,从而使代码简化。

void SListPushBack(Node**pphead,int x)
void SListPushBack(Node*&pphead,int x)//这种方式

②做返回值😀

int& Count1()
{
  int n = 0;
  n++;
  return n;//引用返回
}
//传值返回
int& Count2()
{
  static int n = 0;
  n++;
  return n;//传值返回
}

Count1和Count2函数不同的区别:


在于Count1中n是局部变量,在栈上开辟的,而Count2中n是在静态区开辟的,不会随着函数的结束而销毁。


我们再来看一下它们被调用后可以观察到什么


1669254148528.jpg


这样的结果是意料之内的,因为Count1中的n随着函数结束而销毁,也就是它的空间不归我们使用,归还给了操作系统,我们可以通过引用找到那个空间,但是它可能已经被操作系统使用而值发生了变化,所以我们读写的数据是不确定的。所以在这里我们要注意,当使用引用做返回值的时候,返回的数不能在栈区,不能是局部变量,它可以在静态区或者堆。


在这里,我们可以再深入一点,看一下传值返回和传引用返回有什么不一样。


1669254159570.jpg


Count1返回的是引用,我们惊奇地发现,用int和int&接受它们的值也是不一样的,这似乎超乎我们的意料,不过如果我们知道函数的栈帧,再一分析就很好理解了。


为什么出现不一样的结果?


因为函数结束也就是栈帧销毁,会生成一个临时变量,如果临时变量比较小,就会存放再寄存器中,如果比较大,比如结构体,就会开辟一块空间,因为不可能直接传,因为空间已经被释放还给操作系统了。


所以这里ret接受的是n的拷贝,那么还有一个问题,int&类型的ret接受的是不是拷贝呢?其实也是拷贝,那为什么,它能找到那个空间呢?因为这里,最本质上是传了地址!如果诸位不相信,我们可以在编译器内部看一下引用和指针。


1669254168618.jpg


我们惊奇的发现真的好相似,所以引用在底层上实际也是指针实现的!


1.4Const引用和临时变量🐱


①引用权限的放大和缩小😀

权限的放大:


1669254191088.jpg


我们来看这段代码,为什么int& rb=b会出错呢?


这就是典型的引用的权限的放大,这里rb引用b的别名,b已经被const限制,而rb不被限制,rb是b的别名,rb可修改而b不可修改是不是不合理?


反之,如果合理,那么我给rb赋值,那么rb所指向的空间的内容被修改,而这个空间就是b,但是b被const限制,不能修改,是不是就起冲突了?


权限的缩小


1669254199364.jpg


我们再来看一下这段代码,这段代码中,被const限制的rra不能被修改,但是它可以引用a,只不过权限受限,而a不受限制。


启示:


1、指针和引用赋值中,权限可以平移,可以缩小,但是不能放大!类似于上级给你权力,它可以给你和他相同的,或者小于他的,但是不能高于他,这是一样的道理。


2、权限的放大和缩小,仅限于指针和引用,因为这两种都会影响到原来的数据。


3、如果不更改原来的数据,一般引用或者用指针做参数,一般都是用const加限制引用!


②临时变量的讲解😀

我们还是通过代码来看一下:


int main()
{
  double d = 10.28;
  int i = d;
  cout << (int)d << endl;
  return 0;
}

看了这段代码,我想问几个问题:


1、把浮点型d赋值给整型i,是把d强转为int后再赋值给i吗?


2、输出强转成整型的d,是真的把d给强转截断了吗?


显然不是,这里就涉及到了临时变量的概念,这里无论是把d赋给i,还是把d强转,都产生了临时变量,它对原来的数据进行拷贝,在int i=d中,临时变量就等于把d强转后的数,然后再赋给i。


1669254219669.jpg


这里为什么会出错呢?这要说到临时变量的一个特性,也就是常量性,临时变量是不可修改的,它是被const限制的,这里的赋值出错就是因为,临时变量对于d强转整型后的拷贝是常量,他不能被赋值给变量。


1669254228272.jpg


当我们用const加限制,使它变为常量后,就运行成功了!


总结一下临时变量的特点:


1、隐式类型转换、强制类型转换、截断、提升、函数栈帧的销毁都会产生临时变量。


2、临时变量具有常量性,不能被修改!


1.5 传值、传引用效率的比较🐱


以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
  // 以值作为函数的返回值类型
  size_t begin1 = clock();
  for (size_t i = 0; i < 100000; ++i)
  TestFunc1();
  size_t end1 = clock();
  // 以引用作为函数的返回值类型
  size_t begin2 = clock();
  for (size_t i = 0; i < 100000; ++i)
  TestFunc2();
  size_t end2 = clock();
  // 计算两个函数运算完成之后的时间
  cout << "TestFunc1 time:" << end1 - begin1 << endl;
  cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
  TestReturnByRefOrValue();
  return 0;
}


1669254251510.jpg


我们很轻易就看出,传引用的效率要远高于传值的。


1.6 引用和指针的区别🐱


在 语法概念上 引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。但是,上面博主就验证过了,实际并非如此。

在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。


1669254263462.jpg


引用和指针的不同点:


1. 引用概念上定义一个变量的别名,指针存储一个变量地址。

2. 引用 在定义时 必须初始化 ,指针没有要求,但是为了避免野指针,我们也要初始化指针。

3. 引用 在初始化时引用一个实体后,就 不能再引用其他实体 ,而指针可以在任何时候指向任何 一个同类型实体。

4. 没有 NULL 引用 ,但有 NULL 指针。

5. 在 sizeof 中含义不同 : 引用 结果为 引用类型的大小 ,但 指针 始终是 地址空间所占字节个数 (32 位平台下占4 个字节 )。

6. 引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小。

7. 有多级指针,但是没有多级引用。

8. 访问实体方式不同, 指针需要显式解引用,引用编译器自己处理

9. 引用比指针使用起来相对更安全。


二、内联函数🐅


2.1概念🐱


以 inline修饰的函数叫内联函数,编译时C++编译器会在 调用内联函数的地方展开, 没有函数调用栈帧的开销,内联函数提升程序运行的效率。

不知道读者在看到对于内联函数的介绍时有没有感觉到很熟悉,对,它确实和我们在C语言阶段所学习的宏定义函数很相似。那么,可能有的读者比较困惑,为什么有了宏, 他也能替换,也能不调用函数,不消耗栈帧,那么引入内联函数的意义是什么呢?

我们在C语言阶段,学习的宏它确实有不少优点,比如说:它不需要调用,比函数运行更快;它直接替换;它是类型无关的等等。但是他也有不少缺点:

1、不能调试;

2、没有类型安全检查;

3、容易写出带有副作用的宏参数,比如x++等等;

4、写宏的时候经常需要考虑运算符优先级问题,会加上不少的括号,很容易出错。

内联函数就是C++提出解决这些问题的。


2.2内联函数在编译器的表现🐱


1669254286466.jpg


看到这里,可能有的读者就要发出疑问了,我们不是使用了内联函数吗,为什么还和普通函数一样需要call(调用)呢?


这是因为这是在debug模式下,编译器为了方便我们调试,而不发生替换。


如果在release模式下,编译器生成的汇编代码是不会存在call Add的。


在debug模式下,我们需要对编译器进行设置,否则不会展开(因为在debug模式下,编译器默认不会对代码进行优化)。


设置方式(vs 2019):

1669254302203.jpg


1669254311859.jpg

设置之后我们再进行调试,就可以发现没有调用函数,而是直接发生了替换:


1669254321628.jpg



相关文章
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
47 2
C++入门12——详解多态1
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
89 1
|
2月前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
32 0
|
2月前
|
自然语言处理 编译器 C语言
【C++打怪之路Lv1】-- C++开篇(入门)
【C++打怪之路Lv1】-- C++开篇(入门)
39 0
|
2月前
|
分布式计算 Java 编译器
【C++入门(下)】—— 我与C++的不解之缘(二)
【C++入门(下)】—— 我与C++的不解之缘(二)
|
2月前
|
编译器 Linux C语言
【C++入门(上)】—— 我与C++的不解之缘(一)
【C++入门(上)】—— 我与C++的不解之缘(一)
|
2月前
|
编译器 C++
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
41 0
|
2月前
|
程序员 C++
C++入门11——详解C++继承(菱形继承与虚拟继承)-1
C++入门11——详解C++继承(菱形继承与虚拟继承)-1
44 0
|
2月前
|
存储 算法 C++
C++入门10——stack与queue的使用
C++入门10——stack与queue的使用
48 0
|
2月前
|
存储 C++ 容器
C++入门9——list的使用
C++入门9——list的使用
22 0