• C++入门基础(下)


    目录

    引用

    引用概念

    引用特性

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

    2.一个变量可以有多个引用

    3.引用一旦引用一个实体,再不能引用其他实体.

    常引用

    使用场景

    1.作为参数使用

    2.作为返回值使用

    引用和指针的区别

    内联函数

    内联函数的概念

    内联函数特性

    宏的优缺点

    auto关键字

    auto简介

    auto使用的细则

    auto不能使用的场景

    基于范围的for循环

    语法

    使用条件

    指针空值nullptr

    C++98中的指针空值
     

    引用

    引用概念

    引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间.

    一个人可以有多种称呼

    比如:李逵 ,在家被称为“铁牛”,在江湖上被称为:”黑旋风“.

    但无论哪一种称呼都是指李逵本身这个人.

    用法如下:

    类型& 引用变量名 = 引用实体

    举个例子:

    输入以下代码:

    1. #include<iostream>
    2. using namespace std;
    3. int main()
    4. {
    5. int a = 5;
    6. int& ra = a;//对a取了个别名 ra,它们公用一块内存空间
    7. cout << &a << endl;//输出a的地址
    8. cout << &ra << endl;//输出b的地址
    9. return 0;
    10. }

    注意:引用类型必须和实体类型是同种类型的.

    我们看输出结果:

    94a62932b9be4373a825b200fa01fd5f.png

    它们的地址一样,说明它们确实指向了同一块内存空间,内容相同.

    所以说如果改变ra的值,a的值也会随之改变.

    引用特性

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

    2.一个变量可以有多个引用

    3.引用一旦引用一个实体,再不能引用其他实体.

    我们逐个来解释说明.

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

    我们平常定义变量的时候,例如int a;char c;int* p...等等,都可以不用初始化,就是说不用给初值,但是引用初始化必须给初值.

    1. int main()
    2. {
    3. int a = 5;
    4. int& ra = a;
    5. int& rb;//错误,没有给初值
    6. return 0;
    7. }

    2.一个变量可以有多个引用

    意思是一个人可以有多个外号,就像上面举的那个例子.

    变量也是同样的道理,一个变量可以有多个别名.

    1. int main()
    2. {
    3. int a = 5;
    4. int& ra = a;
    5. int& rb = a;
    6. int& rc = a;
    7. cout << ra << endl;
    8. cout << rb << endl;
    9. cout << rc << endl;
    10. return 0;
    11. }

    输出结果如下:

    aa11d95bf95d4436a0636f55f33cbfe6.png

    3.引用一旦引用一个实体,再不能引用其他实体.

    这个意思是比如你给李逵起了一个黑旋风的外号,这个外号以后就只能属于李逵他自己了,不能再把黑旋风这个外号给别人.

    例如有两个变量a和变量b,我们给a起一个别名是ra,这个时候你就不能再把ra这个外号给b了.

    1. int main()
    2. {
    3. int a = 5;
    4. int b = 10;
    5. int& ra = a;
    6. ra = b;//千万注意!这里不是将b变成a的别名ra,而是将ra的值即a赋值为b.也就是说别名没有变,依然是a的别名,但是a(和别名)的内容变化了,变成了b
    7. &ra = b;//按道理来说,这样才是修改a的别名,但这样并无法编译,所以无法修改别名
    8. return 0;
    9. }

    常引用

    这里会涉及一些权限的平移、放大与缩小问题.

    我们知道,被const修饰的变量不可以被修改,相当于变成了只读权限了,不可以被写了.相当于权限变小了.

    而我们平常不被const修饰的变量,既可以被修改,也可以被读取。所以它的权限比较大.

    权限只可以被缩小和平移,不可以被放大!!!

    这里还需要补充一点:在我们发生类型转化的时候,比如 int b = 0;double a = b;

    编译器会先产生b的一份临时拷贝tmp(通过整型提升,类型为double),而临时常量具有常性,相当于tmp的类型为const double,再将tmp的值赋值给a.

    1. int main()
    2. {
    3. int a = 5;
    4. int& ra = a;//a的类型为int,ra的类型也为int,权限没有变化,可以平移,所以没有问题
    5. const int b = 10;
    6. //int& rb = &b;//错误b的类型为const int,而rb的类型为int,由于int权限大于const int,权限不可以被放大,所以错误
    7. const int& rb = b;//正确,此时rb的类型也为const int,权限可以平移
    8. int d = 15;
    9. double& rd = d;//错误,d先产生一份const double类型的临时变量,由于double权限 > const double,所以权限放大,错误
    10. const double& rdd = d;//正确
    11. return 0;
    12. }

    使用场景

    1.作为参数使用

    1. void Swap(int& left, int& right)
    2. {
    3. int temp = left;
    4. left = right;
    5. right = temp;
    6. }

    因为参数是引用,所以他就是相当于实参的别名,所以如果交换,就切切实实交换了两个实参的值,而不是形参.

    2.作为返回值使用

    传引用返回:实质上是返回返回对象的别名

    1. int& Count()
    2. {
    3. static int n = 0
    4. n++;
    5. // ...
    6. return n;
    7. }

    对于静态或全局变量,返回值可以用引用作为返回值,直接返回它本身,而不用再产生一份拷贝了.

    但若不是全局变量会出现什么问题呢?

    1. int& Count()
    2. {
    3. int n = 0;
    4. n++;
    5. // ...
    6. return n;
    7. }
    8. int main()
    9. {
    10. int ret = Count();
    11. cout << ret << endl;
    12. }

    先来看结果:

    814c33cd4b8140409c47c08a1c98cf35.png

    有的同学就说了,这是1啊,没有问题啊.

    其实这是一种侥幸,进入函数之后,n++,此时n变成1.返回n的别名给ret

    但仔细想一下,当把n的别名给ret的时候,是不是函数已经结束了!函数结束是不是临时变量就被回收了!

    但是为什么结果是1呢?

    因为此时编译器还没有清理或者修改增加别的新的内容. 里面的内容依然是1.

    我们拿个例子来解释一下:你去住酒店,要住一晚上,然后第二天走了之后发现你的钱包和一些东西落在里面了,这个时候你赶紧返回去拿,发现钱包什么的都还在,你就拿到了它.这就是侥幸.

    如果其中来了其他人或者保洁阿姨清理了这些东西呢?这个时候你也就拿不到了,所以说刚才编译器那次拿到这个1也是“侥幸”.

    那么这次就没这么”侥幸“了.

    1. int& Add(int a, int b)
    2. {
    3. int c = a + b;
    4. return c;
    5. }
    6. int main()
    7. {
    8. int& ret = Add(1, 2);
    9. Add(3, 4);
    10. cout << "Add(1, 2) is :" << ret << endl;
    11. return 0;
    12. }

    我们来看输出结果:

    93083351fda745f8a7708dc57a7a2a6e.png

    诶?ret不是Add(1,2),是1和2相加结果不应该是3吗?怎么会是7呢?

    这次就没这么侥幸了.

    首先第一次Add(1,2)返回了3,即把这个3留在了酒店.但此时3的主人已经走了.

    后面又来了一个Add(3,4)即7,替代了这个3

    这个时候3的主人回来再取,取到的已经不是原来它的东西了,只能是7了.这就造成了错误.

    所以注意:

    注意:如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

    引用和指针的区别

    语法概念上,引用就是一个别名,没有独立空间。和其引用实体公用一块空间.

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

    ⭐引用和指针不同点:

    1.引用在定义时必须初始化,指针没有要求

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

    3.没有NULL引用,但有NULL指针

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

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

    6.有多级指针,但没有多级引用.

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

    8.引用比指针使用起来更加安全.

    内联函数

    内联函数的概念

    以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,从而不会有函数压栈的开销,提高运行效率.

    看下图:

    在Debug模式下,我们转到反汇编来看一下

    6751cdbee8a2493fa5cf292f87145f1f.png

     可以看到call这个命令,这个其实就是在调用函数,说明此时并没有展开.

    在Release模式下我们再试一下:

    4778da746770446aa0fd9fe5a6ddd31a.png

     可以发现Add函数被展开了,并没有call函数.直接进行相加操作了.

    内联函数特性

    1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数

    2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。(这条很重要,意味着你写inline但编译器不一定会展开,会根据代码的长短进行决断)

    3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

    下面这段代码演示了这个定义与声明分离的问题:

    1. //F.h
    2. #include
    3. using namespace std;
    4. inline void f(int i);
    5. // F.cpp
    6. #include "F.h"
    7. void f(int i)
    8. {
    9. cout << i << endl;
    10. }
    11. // main.cpp
    12. #include "F.h"
    13. int main()
    14. {
    15. f(10);
    16. return 0;
    17. }
    18. /// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

    所以最好声明和定义写在一起.

    宏的优缺点

    优点:1.提高代码的复用性

    2.提高性能

    缺点:

    1.不方便调试(预编译阶段进行了替换)

    2.使代码可读性变差,可维护性差,容易误用

    3.没有安全类型检查        

    C++有哪些技术可以替换宏?

    1. 常量定义 换用const
    2. 函数定义 换用内联函数

    auto关键字

    auto简介

    在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?
    C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

    总而言之就是:auto会自动推导变量的类型,而不用自己手动去写.通常类型名较长的时候用auto替代(或者自己也不知道变量是什么类型(doge))

    看以下代码:

    1. int main()
    2. {
    3. auto a = 1;
    4. auto b = 1.5;
    5. auto c = 'c';
    6. cout << typeid(a).name() << endl;
    7. cout << typeid(b).name() << endl;
    8. cout << typeid(c).name() << endl;
    9. return 0;
    10. }

    运行结果如下:

    f70707ef41bd41ccba66433ac9d2f510.png

    可以看到auto已经成功推导出来了变量的类型. 

    需要注意的是:

    使用auto定义变量时必须对其进行初始化在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型

    auto使用的细则

    1. auto与指针和引用结合起来使用
    用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

    d9d71104cb2c4e47baee3965bc65b4b5.png

    可以看到,无论auto加不加*,对于指针类型,结果都是一样的.

    但引用必须加上&

     2. 在同一行定义多个变量
    当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量

    363c20748e3046078d19e5f35ef5f65b.png

     可以看到同一行类型不同并不能编译通过.

    auto不能使用的场景

    1.auto不能作为函数的参数

    1. // 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
    2. void TestAuto(auto a)
    3. {}

    2. auto不能直接用来声明数组

    bfbace9a0c204f20a89ae631d90a9f64.png

    3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
    4. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用

    基于范围的for循环

    语法

    如果想要遍历一个数组,我们有以下两种方式

    1. void TestFor()
    2. {
    3. int array[] = { 1, 2, 3, 4, 5 };
    4. for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
    5. array[i] *= 2;
    6. for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
    7. cout << *p << endl;
    8. }

    对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围

    可以改为以下代码:

    1. void TestFor()
    2. {
    3. int array[] = { 1, 2, 3, 4, 5 };
    4. for(auto& e : array)
    5. e *= 2;
    6. for(auto e : array)
    7. cout << e << " ";
    8. return 0;
    9. }

    是不是非常简便.

    注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环

    使用条件

    1. for循环迭代的范围必须是确定的
    对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
    注意:以下代码就有问题,因为for的范围不确定

    1. void TestFor(int array[])
    2. {
    3. for(auto& e : array)
    4. cout<< e <<endl;
    5. }

    2. 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后我会说明,现在大家了解一下就可以了)

    指针空值nullptr

    C++98中的指针空值

    在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:

    1. void TestPtr()
    2. {
    3. int* p1 = NULL;
    4. int* p2 = 0;
    5. // ……
    6. }

    NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

    1. #ifndef NULL
    2. #ifdef __cplusplus
    3. #define NULL 0
    4. #else
    5. #define NULL ((void *)0)
    6. #endif
    7. #endif

    可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如

    1. void f(int)
    2. {
    3. cout<<"f(int)"<<endl;
    4. }
    5. void f(int*)
    6. {
    7. cout<<"f(int*)"<<endl;
    8. }
    9. int main()
    10. {
    11. f(0);
    12. f(NULL);
    13. f((int*)NULL);
    14. return 0;
    15. }

    程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
    在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

    需要注意的是:

    1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的.

    2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
    3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr.

  • 相关阅读:
    MindManager22全新版思维导图软件工具
    特斯拉2021年自动驾驶,特斯拉自动驾驶技术专利
    变量与常量
    模板 template<typename T> 和 template<class T>区别
    谷粒商城----缓存与分布式锁
    神秘的Java集合与UML
    搜维尔科技提供电影和动画的动作捕捉解决方案
    【Axure高保真原型】曲线图组和堆叠曲线图
    yocto开发-常见的概念
    QT之QListView的简介
  • 原文地址:https://blog.csdn.net/weixin_47257473/article/details/127471037