C++入门篇2

文章目录

  • C++入门篇2
    • 1、函数重载
      • 1.1、函数重载概念
      • 1.2、 C++支持函数重载的原理 -- 名字修饰(name Mangling)
    • 2、引用
      • 2.1、引用概念
      • 2.2、引用特性
      • 2.3、常引用
      • 2.4、使用场景
      • 2.5、传值、传引用效率比较
      • 2.6、引用和指针的区别
    • 3、内联函数
      • 3.1、内联函数概念
      • 3.2、内联函数特性
    • 4、auto关键字(C++11)
      • 4.1、auto简介
      • 4.2、auto的使用
      • 4.3、auto不能使用的场景
    • 5、基于范围的for循环(C++11)
      • 5.1、范围for的语法
      • 5.2、范围for的使用条件
    • 6、指针空值nullptr(C++11)

img

C++入门篇2

1、函数重载

1.1、函数重载概念

  • 函数重载是指在同一作用域内使用相同的函数名定义多个函数,这些函数的参数列表(参数的个数或类型)必须不同。函数重载是一种多态的表现形式,常见于面向对象的编程语言如C++和Java。

    #include 
    
    using namespace std;
    
    int Add(int a, int b) {
        return a + b;
    }
    
    float Add(float a, int b) {
        return a + b;
    }
    
    double Add(double a, int b) {
        return a + b;
    }
    
    int Add(double a, double b) {
        return a + b;
    }
    
    //函数重载
    int main() {
        cout << Add(1, 1) << endl;
        cout << Add(1.1f, 1) << endl; //1.1f数据类型为float
        cout << Add(1.1, 1) << endl;//1.1 默认数据类型为double
        cout << Add(1.1, 1.1) << endl;
        std::cout << "Hello, World!" << std::endl;
        return 0;
    }
    
    

1.2、 C++支持函数重载的原理 – 名字修饰(name Mangling)

为什么C++支持函数重载,而C语言不支持函数重载呢?

在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接

这里我们以gcc编译器和g++编译器来演示C语言和C++的链接规则(过程好演示一点)。

这里涉及到C语言链接的时候是根据符号表去根据函数名去找相应的函数,当一个文件开始运行时,符号表会收集所有的函数名,这里函数名不加修饰,由于是根据函数名来找函数,所以函数名不能重复,不然不知找哪个!

而C++也是有符号表的,但是这个符号表记录的函数名和C语言不一样,这里的函数名会加以修饰,修饰规则是:==_Z+函数长度

+函数名+类型首字母==。

  • 采用C语言的编译后结果

    C++入门篇2_第1张图片

    C++入门篇2_第2张图片

    C++入门篇2_第3张图片

    结论在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变


  • 采用C++语言的编译后结果

    C++入门篇2_第4张图片

    C++入门篇2_第5张图片

    C++入门篇2_第6张图片

    结论在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中

  • 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
  • 如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。

2、引用

2.1、引用概念

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

  • 语法:类型& 引用变量名(对象名) = 引用实体;

    int main() {
        int a = 1;
        int &b = a;
    
        cout << &b << endl << &a << endl;//地址一样
    
        return 0;
    }
    

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


2.2、引用特性

  • 引用在定义时必须初始化

  • 一个变量可以有多个引用,这些引用可以指向同一个实体,也可以指向不同的实体。

  • 引用一旦引用一个实体,就不能再引用其他实体。

    int main() {
        int a = 1, b = 0;
        int& c = a;
        int& d = a;
        //int& c = b;//报错,一个变量只能引用一个变量
        cout << &a << endl << &c << endl << &d << endl;//地址一样
        return 0;
    }
    

2.3、常引用

  • 除了普通的引用外,C++还提供了常引用。所谓常引用,就是一旦一个常引用被初始化指向一个对象后,它就不能被重新指向另一个对象

  • 引用类型必须和引用实体是同种类型的。但是仅仅是同种类型,还不能保证能够成功引用。如果用一个普通引用类型去引用其对应的非常量类型对象,那么编译器将会报错。我们不可以可以将一个安全的类型(const修饰的类型)交给一个不安全的类型(可被修改的类型)。对于一个非常量引用,它可以引用一个非常量对象,也可以引用一个常量对象。但是对于常量对象,我们不能改变它的值也不能改变它的类型(即权限不能放大,但是权限可以缩小)

    int main() {
        //权限不能放大
        const int a = 10;
        //int &b = a;//报错
        const int &b = a;
        const int &c = b;
    		//c = 1;//报错
      
        //权限可以缩小
        int d = 20;
        const int &e = d;
    
        //可以给常数取别名
        const int &f = 10;
    
        int i = 1;
        double j = i;
        //double &rj = i;
        const double &rj = i;//i先int提升为double,提升后会有一个常变量记录这个提升后的i(i的原始值不变),所以必须使用常量来引用
    
        return 0;
    }
    

2.4、使用场景

  • 做参数:引用的一个常见用途就是在函数中作为参数使用。在函数中,我们可以通过引用来传递参数,而不是直接传递参数的值。这样可以避免数据的复制,提高了效率。

    void Swap(int &a, int &b) {
        int tmp = a;
        a = b;
        b = tmp;
    }
    
    
    int main() {
        int a = 1, b = 3;
        cout << a << " " << b << endl;//1 3
    
        Swap(a, b);
        cout << a << " " << b << endl;//3 1
        return 0;
    }
    
  • 做返回值:与做参数类似,我们也可以使用引用来返回函数的结果。这样可以避免数据的复制,提高了效率。在返回值是大型对象时特别有用。

    int &Add(int a, int b) {
        static int c = a + b;//只初始化一次
        return c;
    }
    
    
    int main() {
        int &ret = Add(1, 2);
        cout << ret << endl;//3
        
        Add(4, 5);
        cout << ret << endl;//3
        return 0;
    }
    
    • 思考一个问题,为什么第二次输出ret的值也是3?因为在Add函数里,变量c是静态变量,第一次调用Add函数的时候它存在静态区(堆区),第二次调用的时候static int c = a + b;不会执行,因为静态变量c只会被初始化一次,那么第二次返回的就是堆区的c,也就还是3

2.5、传值、传引用效率比较

在C++中,传值和传引用都可以用来传递参数给函数,但它们在执行效率和内存使用上有一些不同。

传值是通过复制实参的值给形参,在函数内部,形参是实参的一个副本,改变形参的值不会影响实参。这种方式不会避免对大型对象的复制操作,缺点是会占用额外的内存来存储副本。

传引用是通过将实参的内存地址给形参,在函数内部,形参可以直接访问实参的内存地址。这种方式可以直接访问并修改实参,优点是可以避免对大型对象进行昂贵的复制操作。

在执行效率方面:

  • 传值对于小型对象来说效率较高,因为复制操作相对较快。
  • 传值对于大型对象来说效率较低,因为复制操作需要花费更多的时间和内存。
  • 传引用对于小型对象和大型对象来说效率都较高,因为不需要需要使用额外的内存来存储形参。

在内存使用方面:

  • 传值需要额外内存来存储副本,如果传递的对象很大,会导致内存占用增加。
  • 传引用不需要额外内存来存储副本,但是需要使用指针来访问对象,可能会导致指针错误或者空指针问题。

在实际编程中,选择传值还是传引用取决于具体的情况。如果传递的对象较大或者需要修改对象,使用传引用可以提高效率;如果只是需要传递对象的值而不是修改对象,使用传值可能更加合适。

  • 值和引用的作为函数参数类型的性能比较

    #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;
    }
    
    int main(){
        TestRefAndValue();//传引用效率更高
        return 0;
    }
    
    
  • 值和引用的作为返回值类型的性能比较

    #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;
    }
    
    int main(){
        TestReturnByRefOrValue();//传引用效率更高
        return 0;
    }
    

2.6、引用和指针的区别

传引用不需要对大型对象进行复制操作。因为在C++中,当一个大型对象被传递给函数时,使用引用参数可以提高参数传递的效率。引用并不产生对象的副本,也就是说,在参数传递时,对象不需要被复制。因此,通过引用,函数可以直接访问并修改实参的内存地址,而不需要像传值那样将实参的值复制给形参,也不需要像传指针那样需要使用额外的内存来存储指针。因此,传引用对于大型对象来说可以提高效率。

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

  • 底层实现上实际是有空间的,因为引用是按照指针方式来实现的。但是我们一般都认为引用是没开空间的!

    int main()
    {
        int a = 10;
        int& ra = a;
        ra = 20;
        int* pa = &a;
        *pa = 20;
        return 0;
    }
    
    • 通过反编译可以对比引用和指针:

      C++入门篇2_第7张图片

  • 引用和指针的不同点:

    1. 基本性质:指针是一个变量,其内部存储的是指向内存存储单元的地址,而引用是原变量的一个别名,它们在内存中占用的存储单元是同一个
    2. 赋值行为:在对引用进行第一次赋值时,起的是绑定作用,即给引用赋予一个新的名字,它和原变量是同一个东西。此后对引用的赋值,将会改变绑定变量的值。而对于指针,每次赋值都是赋给它自身的地址。
    3. 初始化要求:引用在初始化时必须绑定一个已存在的变量,而指针则无需初始化
    4. 空值情况:指针可以为空(NULL),但引用不能为空。
    5. 可修改性:指针的值在初始化后可以修改,而引用在初始化后就不能修改了,即指针一个变量只能引用一个变量。
    6. 级别限制:指针可以有2级或2级以上,但是引用只能有一级。
    7. 安全性:使用引用比使用指针更安全。例如:引用的生命周期与所绑定的对象的生命周期是相同的,因此无需担心内存泄漏问题。
    8. 内存占用:在大多数情况下,指针所占内存为4字节,Linux下是8字节,而64位则不确定,引用所占内存与原变量所占内存大小相同

    总结起来,指针和引用虽然都是C++中重要的概念,但在使用上、功能上有明显的差异。


3、内联函数

3.1、内联函数概念

  • 内联函数是指用inline关键字修饰的函数,或者在类体内定义的成员函数

  • 内联函数在编译时,会被嵌入到每一个调用处,而不是在调用时发生控制转移。这使得内联函数可以消除函数调用时的时间开销,通常用于频繁执行的函数。

    C++入门篇2_第8张图片

    如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。

    如下图:

    C++入门篇2_第9张图片

3.2、内联函数特性

  • 需要注意的是,递归函数不能被定义为内联函数。此外,内联函数一般适合于不存在复杂的结构(如while和switch等)且只有1~5条语句的小函数。如果一个内联函数有多个执行路径(如在if-else结构中),编译器可能会把它视为普通函数

    C++入门篇2_第10张图片

  • 另外,使用内联函数时要注意:内联函数只能先定义后使用;不能对内联函数进行异常的接口声明即内联函数不能声明和定义分离,分离会导致链接错误。因为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 中被引用
    

4、auto关键字(C++11)

4.1、auto简介

在C++中,auto关键字用于自动类型推断,即让编译器根据变量的使用上下文自动确定其类型。auto在C++11及其后的版本中引入,用于简化代码并减少手动指定变量类型的需求。

使用auto关键字可以使代码更简洁,并可以减少因手动指定类型而产生的错误。例如,假设有一个复杂的表达式或变量,手动指定其类型可能会很麻烦或容易出错。使用auto可以让编译器自动推断出正确的类型

  • auto可以自动识别类型。如下:

    #include 
    
    int main() {
        int a = 1;
        auto b = &a;
        auto c = a;
        auto d = b;
        std::cout << typeid(a).name() << std::endl << typeid(b).name() << std::endl << typeid(c).name() << std::endl << typeid(d).name() << std::endl;// i Pi i Pi
      	//i表示int ,Pi表示Pointer int,也就是int*
        return 0;
    }
    
  • 但是auto不是用在这么简单的类型上,在后面我们学的越来越多,越来越深后,类型的长度也会有更长的。比如迭代器

    #include 
    #include 
    
    int main() {
        std::map m{{"apple",  "苹果"},
                                             {"orange", "橙子"},
                                             {"pear",   "梨"}};
        std::map::iterator it = m.begin();
        while (it != m.end()) {
            //....
        }
        return 0;
    }
    
    • 这里的变量it的类型是std::map::iterator,这里我们用auto关键字就会使得代码简单很多。
    #include 
    #include 
    
    int main() {
        std::map m{{"apple",  "苹果"},
                                             {"orange", "橙子"},
                                             {"pear",   "梨"}};
        auto it = m.begin();
        while (it != m.end()) {
            //....
        }
        return 0;
    }
    
  • 注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

4.2、auto的使用

  • 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;
    }
    
  • 在同一行定义多个变量
    当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

    void TestAuto()
    {
        auto a = 1, b = 2;
        //auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
    }
    

4.3、auto不能使用的场景

  • auto不能作为函数参数

    // 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
    void TestAuto(auto a)
    {
      //考虑一个问题,如果这个函数定义了,但是没调用,那么auto推导的类型是什么?所以肯定不行吧
    }
    
  • auto不能用来声明数组

    void TestAuto() {
        int a[] = {1, 2, 3};
        //auto b[] = {4,5,6};//auto不能用来声明数组
    }
    
  • 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法

  • auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环(基于范围的for循环),还有lambda表达式等进行配合使用。


5、基于范围的for循环(C++11)

5.1、范围for的语法

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

    for(用于迭代的变量:被迭代的范围){}

    //传统for
    void TestFor1() {
        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;
    }
    
    //新型for
    void TestFor2() {
        int array[] = {1, 2, 3, 4, 5};
        for (auto &e: array)
            e *= 2;
        for (auto e: array)
            cout << e << " ";
        return 0;
    }
    

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

5.2、范围for的使用条件

  • for循环迭代的范围必须是确定的
    对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循迭代的范围。

  • 注意:以下代码就有问题,因为for的范围不确定

    void TestFor(int array[]) {
        for (auto &e: array)
            cout << e << endl;
    }
    
  • 迭代的对象要实现++和==的操作,我们看到这里for循环里使用auto &e,在范围for循环中使用auto&可以引用容器或数组中的元素,并对这些元素进行下一步操作。


6、指针空值nullptr(C++11)

  • 简而言之nullptr就是用来解决NULL的在某些方面的不好。

    • NULL有可能被定义为字面常量0,也可能被定义为无类型指针(void*)0的常量。

      void f(int) {
          cout << "f(int)" << endl;
      }
      
      void f(int *) {
          cout << "f(int*)" << endl;
      }
      
      int main() {
          f(0);//输出f(int)
          f(NULL);//输出f(int)
          f((int *) NULL);输出f(int*)
          return 0;
      }
      
    • 但是nullptr就纯纯是无类型指针(void*)0的常量。

      void f(int) {
          cout << "f(int)" << endl;
      }
      
      void f(int *) {
          cout << "f(int*)" << endl;
      }
      
      int main() {
          f(0);//输出f(int)
          f(nullptr);//输出f(int*)
          f((int *) NULL);输出f(int*)
          return 0;
      }
      
  • 注意:

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

    • 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。

    • 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。


OKOK,C++入门篇2就到这里。如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。

Xpccccc的github主页

你可能感兴趣的:(C/C++,c++,java,开发语言)