C++初阶1--2

简介: C++初阶1--2

C++初阶1--1https://developer.aliyun.com/article/1424493


三,函数重载

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


       函数重载的注意要点其实主要根据编译器的识别。当函数名相同时,只要当我们运用重名函数时能够区分两者的不同即可通过。如当传参时参数的类型不同或个数不同或顺序不同。如下:


例一:

#include <iostream>
using namespace std;
int Func(int a = 10, int b = 20, int c = 30) {
    return a + b + c;
}
int Func(int a = 1, int b = 2) {
    return a + b;
}
int main() {
    Func();//系统将报错,因为此时两个重名函数都满足条件,编译器无法区分用哪个
    Func(2);//系统报错,同理
    Func(1, 2);//系统报错,同理
    Func(1, 2, 3);//参数数量不同,可以区分,正常运行
    return 0;
}

例二:


#include <iostream>
using namespace std;
void Func(int a = 10, int b = 20, int c = 30) {
    cout << a + b + c << endl;
}
int Func(int a = 1, int b = 2, int c = 3) {
    return a + b;
}
int main() {
    Func(1, 2, 3);//函数类型虽不同,但当调用时两者仍都满足,所以出错
    return 0;
}


四,引用

4-1,引用的使用

      在C++中出现了与指针效果相同的引用概念,引用与指针不同的是引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间,即可通过引用来改变变量,与指针一样。


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


void TestRef()
{    
        int a = 10;
        int& b = a;//定义引用类型
        //输出两者的地址一样,即b的改变也会影响a的改变    
        printf("%p\n", &a);
        printf("%p\n", &b);
}


图解:

1008616d279a4139b262bcf1bdb1d18b.png



其中引用的使用要有以下注意要点:


       1,引用类型必须和引用实体是同种类型的,指针可以不同。


       例如:int a = 5;chr &b = a;将会出错,因为变量a的类型为int,指向a的引用b的类型也必须为int。


       2,引用在定义时必须初始化。


       例如:int &c;将会出错,因为引用没有初始化


       3,一个变量可以有多个引用。


       例如:int a = 5;int &b = a;int &c = a;也就是说一个变量可有多个别名,当a、b、c其中一个改变时,这些别名和a的值都会被改变。


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


       例如:int a = 7, b = 6;int &c = a;&c = b;错误,因为引用c已经指向了一个实体,不能改变引用的指向,但是类似于c = b,c = 9等操作是赋值操作,没有改变引用的指向,是可以的。


4-2,常引用

       常引用通常是引用常量的,也可引用变量。具体定义:const 类型& 引用变量名(对象名) = 常量。


解析说明:


       首先,我们先要明白,无论什么变量,只要加上const关键字修饰后相当于将变量的权限缩小了,即此时的变量成为了常量,不可改变,而常引用就是将引用的权限缩小,即局限了引用的作用范围,当常引用指向变量时,是将别名的权限限制了,但变量名本身还可以改变。之所以常引用可以指向变量是因为在规定中,权限小的可以指向权限大的,权限大的不可指向权限小的,而变量的值可以改变,常量的值不可改变,因此变量的权限要比常量的权限大。之所以要如此规定是因为当大权限的引用一旦指向常量时,引用改变时常量也要改变,显示是错误的,而使用const修饰后的引用本身其实就是别名,当指向大权限变量时,其实只是将别名限制,跟变量本身权限无关。指针与此也是同理。


       还有,在平常我们变量赋值操作时,如果会发生类型转换,中间都会产生一个临时变量,赋值的操作不是变量之间直接赋值,是将这个临时变量赋值,而这种临时变量具有常性,也就是此时是将常量赋值。没有类型转换将不会发生此事情,将直接赋值。如下图:

12bf0c83a5604f94be0ae0b576f022a7.png



void TestConstRef()
{
    const int a = 10;
    //int& ra = a;// 该语句编译时会出错,a为常量,权限大的不可指向权限小的
    const int& ra = a;
    //int& b = 10;// 该语句编译时会出错,b为常量,权限大的不可指向权限小的
    const int& b = 10;
    //权限小的指向权限大的
    int c = 5;
    const int& e = c;//别名权限缩小
    cout << "c = " << e << endl;//输出5
    c = 6;
    cout << "c = " << e << endl;//输出6
    //产生临时变量的情况
    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同,系统将具有常属性的临时变量进行赋值
    const int& rd = d;//相当于const int& rd = 12,rd = 12。
}

4-3,引用的使用场景

       引用的使用场景其实跟指针的使用场景一样,唯一要注意的是引用作为函数返回值的情况。当引用作为函数的返回值时,请注意,不要返回局部变量的引用,因为局部变量是存放在内存中的栈区中,栈区的数据会随着函数栈帧的销毁而销毁,一旦栈帧销毁了局部变量的值将会随机,但是如果编译器没有及时销毁函数栈帧,这时里面的数据还存在,将不会出现错误。如下:


#include <iostream>
using namespace std;
int& ADD(int a, int b) {
    int c = a + b;
    return c;
}
int main() {
    cout << "ADD(2, 3): " << ADD(2, 3) << endl;
    cout << "ADD(2, 3): " << ADD(2, 3) << endl;
    cout << "ADD(2, 3): " << ADD(2, 3) << endl;
    return 0;
}


运行图:


8a6e2d63ab2446cc9ef9d7854ad3cf01.png


       可发现,本人的编译器没有及时销毁函数栈帧,即系统没有及时回收内存。但是有些编译器会及时回收内存,即销毁函数栈帧,所以,要想返回局部变量的引用,我们可用关键字static将其放入到静态区中存储,静态区中的数据只会在整个程序结束后才回收内存,不会随着函数栈帧的销毁而销毁。即如下:


#include <iostream>
using namespace std;
int& ADD(int a, int b) {
    static int c = a + b;
    return c;
}
int main() {
    cout << "ADD(2, 3): " << ADD(2, 3) << endl;
    cout << "ADD(2, 3): " << ADD(2, 3) << endl;
    cout << "ADD(2, 3): " << ADD(2, 3) << endl;
    return 0;
}


内部分析解说:


       当C++函数返回一个局部对象时,这个局部对象的值会被复制到调用函数的栈帧中,或者通过引用传递给调用函数,这个过程是通过值传递的方式进行的。在调用函数的栈帧中,系统会创建一个临时变量,这个临时变量的值就是局部对象的值。当函数返回时,局部对象的生命周期结束,但是临时变量的值会被复制到调用函数的栈帧中,或者通过引用传递给调用函数。也就是说,新的拷贝变量的存储位置是在调用函数的栈帧中。


4-4,引用的效率分析以及与指针的关系

引用的本质:


       引用的本质在C++内部实现是一个指针常量,说白了引用其实是按照指针方式来实现的,并且,在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。


1,变量引用的使用


int a = 10;
int& p = a;//系统内部自动转换为int* const p = &a;其中指针常量是指针的指向不可改,也就说明了为什么引用不可更改的原因
 p = 20;//内部发现p是引用,自动转化为*p = 20;

2,常引用的使用


const int& p = 10;//加上const修饰后,系统内部将代码转换为int a = 10;const int& p = a;


引用跟指针和普通数值之间的效率比较:


       首先,我们来分析以值作为参数或者返回值类型的情况,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。当我们运用指针时,指针可是要在内存中开辟空间,在运算时,指针在内存中要经过复杂转换和相关逻辑的连接,这些复杂运算很容易产生错误而且可读性也差,在传值和指针作为传参以及返回值类型上效率相差就很大。引用的使用可达到指针的相同效果,虽然当函数调用时仍要给形参分配存储空间,但是引用本身就不另开空间,运算时也是跟引用指向的实体运算一样,没有复杂的逻辑结构。综上所述,引用无论在效率上还是安全性上都要比指针高。


       引用虽然相对于指针比较安全和高效,但引用本身也不是绝对安全,如果一旦使用不当也将会产生危险性。


扩展补充:


       我们来观察以下 float* &c;此类型,其实它是一个指向 float 类型指针的引用。


       我们可以这样理解它的组成:


               1,float*:这是一个指向 float 类型的指针。也就是说,它是一个可以存储 float 类型         变量地址的变量。


               2,&c:这是对一个变量(在这里是 c)的引用。引用是一种变量,它提供了对另一个变          量的直接访问。使用引用的效果和使用该变量的名字的效果是一样的。


       所以,float* &c 定义了一个名为 c 的引用,该引用指向一个指向 float 类型的指针。通常在某些情况下,你可能需要一个引用指向指针,比如在函数参数中,如果你希望改变指针的值(而不是仅仅传递指针的值),那么你需要传递一个指向指针的引用。这些东西我们先做了解,后面文章讲解深入C++程序时会进行深入说明。


五,内联函数

      在讲解内联函数之前我们先回顾下C语言中的宏。宏的用法跟函数类似,它比函数高效的是宏的使用不用建立函数栈帧,但宏本身也存在很多问题,我们先观察以下代码:


#define ADD(x, y) x + y;
//int a = ADD(1, 2) * 5;转换后a = 1 + 2 * 5 = 11,不符合我们的要求
#define ADD(x, y) (x + y)
//int a = ADD(1 | 2, 1 & 2); 转换后a = (1 | 2 + 1 & 2),不符合我们的要求
#define ADD(x, y) (x) + (y)
//int a = ADD(1, 2) * 5;转换后a = 1 + 2 * 5 = 11,不符合我们的要求
#define ADD(x, y) ((x) + (y));
//写法正确


       不难发现,宏的使用虽然在效率上比函数高,但本身存在很多细节观念,复杂的宏使用很容易掉坑,所以,宏有以下优缺点:


       优点: 1.增强代码的复用性。


                           2.提高性能。


       缺点: 1.不方便调试宏。(因为预编译阶段进行了替换,也就是不能调试,难以发现错误)


                   2.宏导致代码可读性差,可维护性差,容易出错。


                   3.没有类型安全的检查 。


       由于宏的这些原因,C++使用了以下两中方法进行改进:


               1,使用常量定义,换用const enum(即枚举)


               2,使用短小函数的内联函数


       枚举在这里我们先不讨论,我们先观察内联函数。C++中用的关键字inline修饰的函数叫做内联函数,编译时,C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,从这方面可看出,内联函数提升了程序运行的效率。


解析:


       1,inline其实是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,系统会用函数体替换函数调用,这样一来可能就会使目标文件变大,但少了调用开销,提高了程序的整体运行效率。


       2,inline虽然几乎解决了宏的所有缺陷,但是inline的使用使空间消耗太大了,所以,当函数规模较小时,编译器才会决定使用内联函数,当函数规模较大或运用了递归时,编译器将直接忽略了这种请求,即内部还是没有使用内联函数。笔者建议,对于函数规模较小且频繁调用时采用inline修饰。


       3,不同编译器内联函数的实现机制可能有所不同,具体还要根据编译器的内部功能而定。


       4,inline函数不支持声明和定义分离开,因为编译器一旦将一个函数作为内联函数处理,就会在调用位置展开,即该函数是没有地址的,也不能在其他源文件中调用,故一般都是直接在源文件中定义内联函数的。总的来说就是inline不支持头文件的定义。


       5,可以在同一个项目的不同源文件定义函数名相同但实现功能不同的内联函数。


举例如下:


//内部规模小,系统将展开
inline int Add(int x, int y) {
    int c = x + y;
    return c;
}
//内部规模较大,系统不会展开
inline int Add(int x, int y) {
    int c = x + y;
    int c1 = x + y;
    int c2 = x + y;
    int c3 = x + y;
    int c4 = x + y;
    int c5 = x + y;
    int c6 = x + y;
    int c7 = x + y;
    int c8 = x + y;
    int c9 = x + y * c8;
    int c10 = x + y;
    int c11 = x + y;
    return c1 + c10 - c9;
}
//运用了递归,系统将不会展开
typedef struct Node {
    int val;
    struct Node* left;
    struct Node* right;
}Node;
inline int Tree(Node* root) {
    if (!root) {
        return 0;
    }
    int leftsize = Tree(root->left);
    int rightsize = Tree(root->right);
    return leftsize + rightsize;
}


       内联函数的运用虽然具有局限性,但是它的优点不可忽视,在后期的运用也是很重要的。它可以调试,运用效率高,语法也简单,更不用建立栈帧,大大的提升了效率。



相关文章
|
4月前
|
存储 编译器 C++
【C++ 初阶路】--- 类和对象(下)
【C++ 初阶路】--- 类和对象(下)
22 1
|
4月前
|
存储 编译器 C语言
【C++初阶路】--- 类和对象(中)
【C++初阶路】--- 类和对象(中)
26 1
|
4月前
|
安全 编译器 程序员
【C++初阶】--- C++入门(上)
【C++初阶】--- C++入门(上)
29 1
|
4月前
|
存储 编译器 C++
【C++初阶】—— 类和对象 (中)
【C++初阶】—— 类和对象 (中)
34 3
|
4月前
|
编译器 C++
【C++初阶】—— 类和对象 (下)
【C++初阶】—— 类和对象 (下)
24 2
|
4月前
|
C语言 C++
【C++初阶】—— C++内存管理
【C++初阶】—— C++内存管理
28 1
|
4月前
|
存储 编译器 C语言
【C++初阶】—— 类和对象 (上)
【C++初阶】—— 类和对象 (上)
33 1
|
4月前
|
C语言 C++ 容器
【C++初阶学习】第十二弹——stack和queue的介绍和使用
【C++初阶学习】第十二弹——stack和queue的介绍和使用
41 8
|
4月前
|
存储 C++
C++初阶学习第九弹——探索STL奥秘(四)——vector的深层挖掘和模拟实现
C++初阶学习第九弹——探索STL奥秘(四)——vector的深层挖掘和模拟实现
42 8
|
4月前
|
存储 C++
C++初阶学习第十一弹——探索STL奥秘(六)——深度刨析list的用法和核心点
C++初阶学习第十一弹——探索STL奥秘(六)——深度刨析list的用法和核心点
48 7