• c++入门(二)



    前言

    继续进行c++的入门学习,因为c++的细节比较多,所以想要入门也要花多点时间,学好c++就更要花功夫去理解专研那些难的语法和用法;学习不是一蹴而就的,要懂得静下心来,一步一步走,才走得稳,为后面的走得快打好基础!!!

    在这里插入图片描述


    一、引用

    1.1 引用的概念

    1. 引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
    2. 当然,这个是概念,我们这么理解就行了,但是其实底层实现还是用了指针的,我们后面会提到;
    3. 比如,我们有自己的小名,自己的外号,自己的身份证上的名字,但是其实都是指我们自己,只是很多是外号而已,而引用也是一样的;
    4. 类型& 引用变量名(对象名) = 引用实体;
    5. 注意:引用类型必须和引用实体是同种类型的;
    void TestRef()
    {
      int a = 10;
      int& ra = a;//<====定义引用类型
      printf("%p\n", &a);
      printf("%p\n", &ra);//其实两个打印出来的地址是一样的,同一个;
    }
    

    1.2 引用的特性

    1. 引用在定义时必须初始化
    2. 一个变量可以有多个引用
    3. 引用一旦引用一个实体,再不能引用其他实体
    void TestRef()
    {
     int a = 10;
     // int& ra;  // 该条语句编译时会出错
     int& ra=a;//
     int& rra = ra;//这个也是可以的哦!
     int& rrra = a;
     printf("%p %p %p %p\n", &a, &ra, &rra, &rrra); //全部都是一样的地址
    }
    

    1.3 常引用

    1. 我们引用的权限不能变大,但是可以变小;也就是说如果一开始的变量是用const修饰了的,我们的引用也要用const修饰才行,不然会报错,因为如果不用const修饰,我们就可以随意更改值,但是原意是用了const 本来就不行被改变,但是不加const的引用就可以更改值了,所以不能进行权限放大;但是原来是没有const修饰,但是引用加了const修饰是可以的,而且和原变量是互不干扰的,原来的还可以进行修改;
    2. 常量相当于用const修饰了的,所以引用是不可以有修改的,也要用const修饰才行;注意:强制类型转换是要开拷贝变量在进行赋值的,而拷贝变量相当于常量,所以强制类型转换也是要引用加const修饰才行的哦!!!包括函数传参的时候也要注意权限的大小哈~~
    void TestConstRef()
    {
      const int a = 10;
      //int& ra = a;  // 该语句编译时会出错,a为常量
      const int& ra = a;
      // int& b = 10; // 该语句编译时会出错,b为常量
      const int& b = 10;
      double d = 12.34;
      //相当于进行强制类型转换,int temp=12;
      //const int& rd=temp;
      //int& rd = d; // 该语句编译时会出错,类型不同
      const int& rd = d;
    }
    

    1.4 引用的使用场景

    1. 做参数
    void Swap(int& left, int& right)
    {
     int temp = left;
     left = right;
     right = temp;
    }
    
    1. 做返回值
    int& Count()
    {
     static int n = 0;
     n++;
     // ...
     return n;//注意:传引用是可以改原值的;
    }
    
    //下面代码输出什么结果?为什么?
    int& Add(int a, int b)
    {
      int c = a + b;
      return c;
    }
    int main()
    {
      int& ret = Add(1, 2);
      Add(3, 4);
      cout << "Add(1, 2) is :"<< ret <<endl;
      return 0;
    }
    
    1. 这里的答案是不确定的,因为ret是传引用返回的,相当于ret就是Add函数中临时变量c的别名,但是Add函数调用完后被操作系统回收了,而变量也随之销毁了,虽然ret还是c的别名,但是不知道c中放的是什么值了。可能是7,因为后面又调了一次Add函数,位置还是一样的,因为空间可以重复利用的,所以c值改成7了,而ret也可能改成7了;也可能是其它值,因为这个空间不受保护了,可以被别人用,可以被别人改值,ret也可能随之改变;
    2. 故答案不确定;
    3. 注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。也就是在不在函数栈帧里的东西嘛;函数栈帧里的出了作用域是会被操作系统回收的;

    1.5 传值、传引用效率比较

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

    #include 
    struct A{ int a[10000]; };
    void TestFunc1(A a){}
    void TestFunc2(A& a){}
    void TestRefAndValue()
    {
    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;
    }
    

    1.6 值和引用的作为返回值类型的性能比较

    #include 
    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;
    }
    

    通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。可以体现出用引用还是有优势的哈;

    1.7 引用和指针的区别

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

    int main()
    {
    int a = 10;
    int& ra = a;
    cout<<"&a = "<<&a<<endl;
    cout<<"&ra = "<<&ra<<endl;//会发现地址都是相同的
    return 0;
    }
    

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

    int main()
    {
    int a = 10;
    int& ra = a;
    ra = 20;
    int* pa = &a;
    *pa = 20;
    return 0;
    }
    

    我们来看下引用和指针的汇编代码对比:

    在这里插入图片描述

    会发现汇编代码是一样的。证明其实底层实现是用指针实现的,只是在语法上看是一个变量的另一个别名而已;
    引用和指针的不同点:

    1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
    2. 引用在定义时必须初始化,指针没有要求,但是用的时候要注意,不要变成野指针哦!!!
    3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
    4. 没有NULL引用,但有NULL指针
    5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
    6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
    7. 有多级指针,但是没有多级引用
    8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
    9. 引用比指针使用起来相对更安全,因为容易有野指针产生嘛;

    二. 内联函数

    2.1 内联函数的概念

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

    在这里插入图片描述

    注意:要转到反汇编去观察汇编代码哦!!!

    在这里插入图片描述

    在这里插入图片描述

    第二张图片就是内联函数展开的汇编代码;这里没有调用函数的汇编代码;

    2.2 内联函数特性

    1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运
      行效率。
    2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建
      议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不
      是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
    3. 下图为《C++prime》第五版关于inline的建议:

    在这里插入图片描述

    1. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。因为在链接的时候会合并形成的符号表,而且只有声明的源文件的符号表会去找对应函数地址,要是找不到就报链接错误;
      这里的话内联函数都不会将真正的函数地址放进符号表中,所以链接错误;
    // F.h
    #include 
    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: 无法解析的外部符号 "void __cdecl
    //f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
    

    【面试题】
    宏的优缺点?
    优点:

    1. 增强代码的复用性。
    2. 提高性能。
      缺点:
    3. 不方便调试宏。(因为预编译阶段进行了替换)
    4. 导致代码可读性差,可维护性差,容易误用。
    5. 没有类型安全的检查 。

    C++有哪些技术替代宏?

    1. 常量定义 换用const 和 enum;
    2. 短小函数定义 换用内联函数;

    三. auto关键字(C++11)

    3.1 类型别名思考

    随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:

    1. 类型难于拼写
    2. 含义不明确导致容易出错
    #include 
    #include 
    int main()
    {
    std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange",
    "橙子" },
     {"pear","梨"} };
    std::map<std::string, std::string>::iterator it = m.begin();
    while (it != m.end())
    {
    //....
    }
    return 0;
    }
    

    看不懂没关系,我也看不懂;
    std::map::iterator 是一个类型,但是该类型太长了,特别容易写错。聪明的同学可能已经想到:可以通过typedef给类型取别名,比如:

    #include 
    #include 
    typedef std::map<std::string, std::string> Map;
    int main()
    {
    Map m{ { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} };
    Map::iterator it = m.begin();
    while (it != m.end())
    {
    //....
    }
    return 0;
    }
    

    使用typedef给类型取别名确实可以简化代码,但是typedef有会遇到新的难题:

    typedef char* pstring;
    int main()
    {
    const pstring p1;   // 编译成功还是失败?
    const pstring* p2;  // 编译成功还是失败?
    return 0;
    }
    '
    运行

    在这里插入图片描述

    上面的代码其实对于解读还是不容易的,没有易读性,可读性;
    在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义。

    3.2 auto简介

    在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?
    C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
    说实话,上面一堆的介绍废话;具体运用是赋值的时候右值是有类型的值,然后左边用auto赋值,左边的变量就不用类型,用auto就行了,auto会自动识别右边的类型;

    int TestAuto()
    {
    	return 10;
    }
    int main()
    {
    	//typeid(auto修饰的变量).name()用法
    	int a = 10;
    	auto b = a;
    	auto c = 'a';
    	auto d = TestAuto();
    	cout << typeid(b).name() << endl;//typedi(auto修饰的变量).name()就是用来识别该变量的类型的
    	cout << typeid(c).name() << endl;
    	cout << typeid(d).name() << endl;
    	//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
    	return 0;
    }
    

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

    3.3 auto的使用细则

    1. auto与指针和引用结合起来使用用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&;
    int main()
    {
      int x = 10;
      auto a = &x;
      auto* b = &x;
      auto& c = x;
      cout << typeid(a).name() << endl;
      cout << typeid(b).name() << endl;
      cout << typeid(c).name() << endl;
      *a = 20;
      *b = 30;
      c = 40;
      return 0;
    }
    

    在这里插入图片描述

    1. 在同一行定义多个变量当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
    void TestAuto()
    {
      auto a = 1, b = 2;
      auto c = 3, d = 4.0;  // 该行代码会编译失败,因为c和d的初始化表达式类型不同
    }
    

    3.4 auto不能推导的场景

    1. auto不能作为函数的参数
    // 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
    void TestAuto(auto a)
    {
    }
    
    1. auto不能直接用来声明数组
    void TestAuto()
    {
      int a[] = {1,2,3};
      auto b[] = {456};
    }
    
    1. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
    2. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有
      lambda表达式等进行配合使用。

    4. 基于范围的for循环(C++11)

    4.1 范围for的语法

    在C++98中如果要遍历一个数组,可以按照以下方式进行:

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

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

    void TestFor()
    {
    int array[] = { 1, 2, 3, 4, 5 };//用了上面auto的用法,然后e其实表达的就是每一个元素,每次循环都是一个array里的元素;
    for(auto& e : array)
      e *= 2;
    for(auto e : array)
      cout << e << " ";
    return 0;
    }
    

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

    4.2 范围for的使用条件

    1. for循环迭代的范围必须是确定的
      对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供
      begin和end的方法,begin和end就是for循环迭代的范围。
      注意:以下代码就有问题,因为for的范围不确定
    void TestFor(int array[])
    {
      for(auto& e : array)
        cout<< e <<endl;
    }
    
    1. 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在提一下,没办法讲清楚,现在大家了解一下就可以了)

    五. 指针空值nullptr(C++11)

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

    void TestPtr()
    {
    int* p1 = NULL;
    int* p2 = 0;
    // ……
    }
    

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

    #ifndef NULL
    #ifdef __cplusplus
    #define NULL  0
    #else
    #define NULL  ((void *)0)
    #endif
    #endif
    

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

    void f(int)
    {
    cout<<"f(int)"<<endl;
    }
    void f(int*)
    {
    cout<<"f(int*)"<<endl;
    }
    int main()
    {
    f(0);
    f(NULL);
    f((int*)NULL);
    return 0;
    }
    

    在这里插入图片描述

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

    1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入
      的。//注意:是关键字哦
    2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
    3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

    总结

    提示:这里对文章进行总结:
    例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

  • 相关阅读:
    记录--使用Vue开发Chrome插件
    java毕业设计税源管理系统源码+lw文档+mybatis+系统+mysql数据库+调试
    PyScada(三)后端应用
    使用 NVIDIA CUDA-Pointpillars 检测点云中的对象
    C语言理论--笔试面试基础稳固
    【chrome基础】Chrome、Chromium、libcef、electron版本关系大揭秘!
    当PBlaze6 6920 Raid阵列遇到FC SAN
    B032-服务器 Tomcat JavaWeb项目 Servlet
    Spring Boot 如何使用Liquibase 进行数据库迁移
    SpringBoot保姆级教程(五)SpringBoot注册web组件
  • 原文地址:https://blog.csdn.net/qq_68844357/article/details/127041862