• C++基础——对于C语言缺点的补充(2)


           上篇文章中说到,为了解决C语言会出现人为定义的函数和库函数出现重定义的错误,C++引入了一个新的概念,即命名空间,通过认为定义命名空间,来解决上述问题。

           在本篇文章中,将继续介绍C++相对于C语言不足来进行的补充,例如:缺省参数,重载函数等等。

    1. 缺省参数:

    1.1 缺省参数的定义以及单个缺省参数简单应用:

           缺省参数的定义大致如下:缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。例如下面的函数:

    1. void PRINT(int a = 0)
    2. {
    3. cout << a << endl;
    4. }
    5. int main()
    6. {
    7. PRINT(0);
    8. PRINT(10);
    9. return 0;
    10. }

           不难发现,相比C语言中的函数书写方法,C++中,对于函数的形式参数给定了一个初始值0,该值就是上面定义中所说的缺省值。对于这种带有缺省值的函数,其调用结果如下:

           这也证实了定义中的一句话,如果没有对函数的实参进行指定赋值,则在函数执行时默认采用形参,反之采用实参。

    1.2 多个缺省参数的应用:

           在上面的情况中,函数只有单一的缺省值。当函数的缺省值数量大于1时,函数的调用方式会更加多样,例如:

    1. void PRINT(int a = 10, int b = 20, int c = 30)
    2. {
    3. cout << a << endl;
    4. cout << b << endl;
    5. cout << c << endl;
    6. }
    7. int main()
    8. {
    9. PRINT();
    10. PRINT(1);
    11. PRINT(1, 2);
    12. PRINT(1, 2, 3);
    13. return 0;
    14. }

     运行结果为:

           通过上面的例子可以看到,针对这种缺省值数量大于1的情况下,函数的调用会因为实参的给定而给出不用的效果。

           但是对于实参数值的给定,需要注意,实参的给定必须是连续的,中间不能有跳跃的情况,例如:

    PRINT(, 2, );

    1.3 全缺省与半缺省:

          全缺省参数就是上一部分的代码所展示,即所有的形参都给定了缺省值。对于半缺省,则是对于形式参数,不全部给定缺省值,例如:

    void PRINT(int a, int b = 20, int c = 30)

    但是对于半缺省参数的给定需要注意以下两点:

    1. 缺省值必须从右向左给定,且必须是连续的。

    2. 当函数的定义和声明分离时,缺省参数不能同时在声明和定义中出现。一般是将缺省参数给定到函数声明中

    2. 函数重载:

    2.1 函数重载的定义及类型:

           函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。例如下面给出的例子:

    1. int ADD(int num1, int num2)
    2. {
    3. cout << "int ADD(int num1, int num2) = ";
    4. return num1 + num2;
    5. }
    6. double ADD(double num1, double num2)
    7. {
    8. cout << " double ADD(double num1, double num2) = ";
    9. return num1 + num2;
    10. }
    11. int main()
    12. {
    13. cout << ADD(1, 2) << endl;
    14. cout << ADD(1.1, 2.2) << endl;
    15. }

           在上述代码中,建立两个函数,且这两个函数函数名相同,都是ADD,但是这两个函数的返回值类型,以及参数类型都不同,此时符合上方函数重载的定义,构成了函数重载。

          对于上述代码,运行结果如下:

             

           上面的例子展示了函数的类型不同,以及其参数类型不同的情况下可以构成重载。当函数的参数数量不同时,即:

    1. void fun()
    2. {
    3. cout << "fun()" << endl;
    4. }
    5. void fun(int a)
    6. {
    7. cout << "fun(a)" << endl;
    8. }
    9. int main()
    10. {
    11. fun();
    12. fun(1);
    13. }

          此时符合重载定义中参数数量不同这一特性,程序依旧可以正常运行,运行结果如下:

        但是上述函数有一种特殊情况需要注意,即:将函数的参数给定一个缺省值,例如:

    1. void fun()
    2. {
    3. cout << "fun()" << endl;
    4. }
    5. void fun(int a = 0)
    6. {
    7. cout << "fun(a)" << endl;
    8. }
    9. int main()
    10. {
    11. fun();
    12. fun(1);
    13. }

          运行程序,编译器会报错。这是因为在存在一个不含参数的函数与另一个有参数,且参数有缺省值的情况下,同时运行两个函数会造成二义性,即:编译器不能判断在main函数中,调用函数时,调用的函数是上面两个函数哪一个函数。

        对于函数重载,还有一种方式,即类型的顺序不同,具体情况如下方代码所示:

    1. void fun(int a, char b)
    2. {
    3. cout << "void fun(int a, char b)" << endl;
    4. }
    5. void fun(char a, int b)
    6. {
    7. cout << "void fun(char a, int b)" << endl;
    8. }
    9. int main()
    10. {
    11. fun(2, 1.1);
    12. fun(2.2, 1);
    13. return 0;
    14. }

    3. 引用:

    3.1 引用的概念以及基本使用:

           在C++中,引用是基于C语言中指针的不足,为解决这些不足创建的。引用的概念如下:引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

          对应代码如下:

    1. int main()
    2. {
    3. int a = 1;
    4. int& c = a;
    5. }

         main函数中,第一行代码创建了一个名为a的整型变量,第二行则是引用,即给已有的整型变量a取了一个别名叫做c

         在上面的概念中提到,编译器不会为引用变量开辟内存空间,引用与其引用的变量共用一块内存空间。因此,加入对引用进行更改,相应的变量也会更改,例如对于下面代码:

    1. int main()
    2. {
    3. int a = 1;
    4. int& c = a;
    5. cout << "更改前 a=" << a << endl;
    6. cout << "更改前 c=" << c << endl;
    7. c++;
    8. cout << "更改后 a=" << a << endl;
    9. cout << "更改后 c=" << c << endl;
    10. return 0;
    11. }

    运行结果为:

    引用不光可以用在已有的变量上,也可以对引用进行引用,即:

    1. int main()
    2. {
    3. int a = 1;
    4. int& c = a;
    5. int& e = c;
    6. cout << "a = " << a << endl;
    7. cout << "c=" << c << endl;
    8. cout << "e=" << e << endl;
    9. return 0;
    10. }

     运行结果为:

            对于引用而言,其最大的价值适用于做参数,例如在之前文章中提到到交换函数:

    1. void Swap(int* a, int* b)
    2. {
    3. int tmp = *a;
    4. *a = *b;
    5. *b = tmp;
    6. }
    7. int main()
    8. {
    9. int x = 5;
    10. int y = 1;
    11. Swap(&x, &y);
    12. printf("%d %d", x, y);
    13. return 0;
    14. }

            由于函数形参只是实参的一份临时拷贝,形参的改变并不会影响实参,所以在传递参数时,需要传递变量的地址。

            如果利用引用的特性,即:引用的改变会引起变量的改变,来实现交换函数,则可以一定程度上化简上述函数,即:
     

    1. void Swap(int& a, int& b)
    2. {
    3. int tmp = a;
    4. a = b;
    5. b = tmp;
    6. }
    7. int main()
    8. {
    9. int x = 5;
    10. int y = 1;
    11. Swap(x, y);
    12. printf("%d %d", x, y);
    13. return 0;
    14. }

          运行结果如下:

    3.1 使用引用的注意事项: 

    3.1.1 引用与初始化:

           在指针这部分中,如果一个指针只是被创建但是不被初始化,则这个指针称为野指针。不过指针的初始化并不是必然的,但是对于引用来说,必须被初始化,例如:

    1. int main()
    2. {
    3. int a = 0;
    4. int& b;
    5. return 0;
    6. }

    由于引用没有被初始化,因此程序错误。

    3.1.2 引用能不能被改变指向:

            在数据结构关于的链表的文章中,如果需要在链表中插入一个新的结点,需要改变链表中指针的指向,但是对于引用而言,不能被改变指向,例如:

    1. int main()
    2. {
    3. int a = 0;
    4. int& b = a;
    5. int c = 1;
    6. printf("%p %p %p", &a, &b, &c);
    7. printf("\n");
    8. b = c;
    9. cout << "a = " << a << "b = " << b << "c = " << c << endl;
    10. printf("%p %p %p", &a, &b, &c);
    11. return 0;
    12. }

    运行结果如下:


           通过结果不难发现,在执行了b=c这一条代码后,b的地址依旧与a的地址相同,说明没有改变其指向。只是改变了a,b的值。

          因此,引用虽然在一定程度上弥补了指针的不足,但是由于引用并不能改变指向,因此引用并不能完全替代指针。

    3.2 引用返回:

    3.2.1 引用返回的概念:

    在解释本节标题的内容之前,首先需要了解一个概念:

    1. int fun()
    2. {
    3. int n = 0;
    4. n++;
    5. return n;
    6. }
    7. int main()
    8. {
    9. int ret = fun();
    10. return 0;
    11. }

           图中给定的程序的函数的返回值返回n,在函数外部创建了一个名为ret的变量来接收函数返回值,但是,由于在函数fun运行结束后,该函数对应的栈帧全部销毁,因此,ret并不是直接接收函数的返回值,而是返回值先被存到寄存器中,ret通过寄存器来间接接收这个值。

          此时,如果将函数返回的返回值改为下方代码所示,即:

    1. int& fun()
    2. {
    3. int n = 0;
    4. n++;
    5. return n;
    6. }
    7. int main()
    8. {
    9. int ret = fun();
    10. return 0;
    11. }

    此时再去运行程序,编译器会给出警告。这是因为在这种返回类型下,返回值并不能确定,如果编译器在函数结束后直接清理掉栈帧,则返回空,如果编译器在函数结束后,并不会立即清理掉栈帧,则返回还能有返回值。

    (注:由于VS并不会立刻清理掉栈帧,因此运行该程序,返回值会返回1

           通过上面的例子,可以得出一个结论,即:在函数运行结束后,即离开函数的作用域后,此时的返回对象被销毁,因此不能采用引用返回。

    但是,对于静态变量和利用malloc类似方式创建的变量,在函数结束后不会销毁,可以采用引用返回。

    3.2.2 引用返回的价值:

           由于用返回的价值主要体现在C++的后续内容,因此,本文在此部分指给出结论,原理会在后续的文章中进行阐述。

    1. 提高返回速度  

    2. 可以修改返回对象

    3.3 常引用:

    对于常引用,具体可以有下面代码反应:

    1. int main()
    2. {
    3. const int a = 1;
    4. int& b = a;
    5. return 0;
    6. }

            从上面代码可以看到,变量a是一个被const修饰的常变量,下面对变量a进行了一次引用。当去运行上述代码时,编译器会显示错误。

          但是如果将上述代码进行修改,即:

    1. int main()
    2. {
    3. const int a = 1;
    4. const int& b = a;
    5. return 0;
    6. }

           运行此代码时,并没有错误,不难观察到,上下的给的代码唯一的差距就是第二次给出的代码的引用前加了const。因此,在进行常引用的过程中,一定不能涉及变量权限的放大。例如上面运行失败的代码中,原本变量a的权限是不能修改,但是在引用后,由于引用的修改会引起原变量的修改,因此,引用的权限超过了原变量的权限,造成了权限放大。所以导致运行错误。这里需要注意的时,只是不能放大变量的权限,对于变量权限的平移的缩小均无影响,例如:
     

    1. int main()
    2. {
    3. int c = 2;
    4. const int& d = c;
    5. return 0;
    6. }

          上述代码在引用后,缩小了变量的权限,可以正常运行。

          另外,对于一个常量进行常饮用也是允许的,即:

    const int& e = 10;

    令外,当引用与被引用的对象存在类型转换的情况时,即:

    1. int j = 10;
    2. double h = 15;
    3. const double& k = j;

    由于类型转化的过程中,会额外生成一个常量作用于该过程,引用时k接收的值并不是i,而是i通过一个常量完成类型转换间接传给k,由于常量的常性,因此需要加上const

    4. 指针与引用的练习与区别:

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

      10.在引用这一小节开头的时候说到,引用是针对一个对象起的别名,与被引用对象共用一块空间。在汇编层上,引用是利用指针来实现的,因此也需要开辟空间。不过,在日常运用时,一般认为引用是不会开辟新空间的。

    5. 内联函数:

           对于一般的函数,在函数作用时需要开辟栈帧,而内联函数不需要开辟栈帧,而是在运行中原地展开。由于内联函数不需要开辟栈帧,因此提高了程序的运行效率。

          内敛函数的书写,只需要在函数声明的开头加上inline即可,例如:

    1. inline int Add(int x, int y)
    2. {
    3. int z = x + y;
    4. return x + y;
    5. }
    6. int main()
    7. {
    8. int x = 5;
    9. int y = 1;
    10. int ret = Add(x, y);
    11. return 0;
    12. }

                但是需要注意,内联函数并不能完全替代普通函数,并且,当一个函数的代码函数>10行时,不建议采用内联函数。这是因为,内联函数每次调用时都会原地展开,当调用的次数较大时,例如调用10000次,则一个代码行数为50行的内联函数完全展开后为500000行,延缓程序的运行速度。

  • 相关阅读:
    【源码+文档+调试】springboot文化传承小程序的设计与实现小程序源码分享
    Rust Rocket简单入门
    数仓4.0
    Fuzz:内存模糊测试
    apt安装yum
    Marin说PCB之国产电源芯片方案 ---STC2620Q
    软件开发项目文档系列之三如何撰写项目招标文件
    【面试题精讲】什么是websocket?如何与前端通信?
    Linux学习——进程状态
    回流重绘零负担,网页加载快如闪电
  • 原文地址:https://blog.csdn.net/2301_76836325/article/details/134226893