C++语言学习02

发布时间 2023-08-29 10:12:26作者: 优秀还天籁

一、函数重载
1、什么是函数重载
在同一作用域下,函数名相同,参数列表不同的函数构成重载关系
函数重载与返回值的类型、参数名无关
与作用域是否相同,以及参数列表的数量、参数类型、常属性不同等有关

2、C++是如何实现函数重载的?
    通过g++ -S 的方式生成汇编代码可以知道,编译器会把函数的参数类型进行缩写
    后追加到函数名的末尾形成新的函数名,既是进行了换名操作,从而确保编译结果
    函数名不重复
3、extern "C"   
    因为通过C++编译函数、调用语句时,会变成换名后的名字,但是项目中有可能需要
    去调用已经通过C编译器编译好的函数,它们是没有换名的,因此会调用失败。
    因为大部分情况下C编译好的文件无法重新使用C++编译器编译,所以只能想办法让
    C++编译器在调用C编译的函数时不能换名字
    通过extern "C"{xxx} 让C++编译器按照C语言的格式编译函数,从而让调用语句与
    被调用函数名同名,这样就可以正确的调用C标准库、系统函数

4、重载和隐藏
    重载:在同一作用域下,函数名相同,参数列表不同的函数构成重载关系
    隐藏:在不同作用域下,同名函数构成隐藏

5、参数类型不匹配
    当调用函数时,编译器会优先调用类型最匹配最精确的版本函数,如果没有最正确的,则
    会先做类型转换后在匹配,但如果没有能够进行类型转换匹配的版本最后也会报错。
    但是,不建议通过类型转换去调用函数,因为转换的情况与编译器有关

二、默认形参
1、什么是默认形参
在C++中可以给函数的参数设置默认值,当函数被调用时如果调用者提供了实参则使用实参
调用,如果没有提供则使用默认值调用
2、默认形参要靠右
如果函数有多个参数,并设置了默认形参,要遵循从右往左依次设置默认形参
3、只能在函数声明中设置默认形参
如果函数声明与定义分开实现,只能在函数声明中设置默认形参,否则语法错误
4、默认形参可能会影响函数重载
如果对函数进行了重载,又设置了默认形参,调用时可能会有冲突和歧义,调用失败
因此对于重载过的函数设置默认形参时一定要谨慎

三、内联函数
1、普通函数
普通函数会被编译成二进制指令存储在代码段中,调用语句会生成一条跳转指令,当程序运行到调用
语句时,会跳转该函数在代码段中对应的位置执行,执行结束后会返回
2、什么是内联函数
内联函数也会被翻译成二进制指令,但调用语句不会生成跳转指令,而是直接把内联函数的二进制指令进行
替换,就没有跳转和返回,而是直接执行二进制指令,这种函数称为内联函数
3、显式内联和隐式内联
显示内联:
在函数的返回值前加 inline 该函数就以内联函数的机制调用

    隐式内联:
        在结构、联合、类中的成员函数会自动被当作内联函数处理
        注意:如果在结构、联合、类中声明成员变量,但是在外面定义,
        则不会当作内联函数处理
    
    注意:函数是否被内联由编译器以及它的优化等级决定,加inline 只是有可能影响它的决定

    注意:g++默认优化等级 -O -O1下所有的内联函数都会当成普通函数处理
        在-O2 -O3的优化级别下,甚至普通函数都可能会被当作内联函数处理

    注意:c99也支持 inline

4、内联的适用条件
    优点:节约了函数传参、跳转、返回的时间,提高代码的运行速度
    缺点:当被多位位置调用时,那么二进制会被拷贝多份,产生了冗余,
    导致可执行文件明显增加

    适用条件:
        1、适合内容简单且同一位置频繁调用的函数
        2、不适合内容多、且多个位置、较少调用的函数,因为节约的时间
        还弥补不过牺牲的空间
        3、带有递归属性的函数无法内联、编译器会自动忽略

5、内联函数和宏函数的相同点和不同点

    相同点:都是采用以空间换时间的策略来提高程序的运行速度,减少函数调用
    跳转的耗时

    不同点:
    1、宏函数不是真正的函数,只是语句的替换,不会对参数进行类型检查,没有
    返回值、安全性低
    2、内联函数是真正的函数、严格检查参数类型、有返回值、安全性高

四、引用
什么是引用:引用就是一种取别名的机制
为什么要使用指针:
1、跨函数共享变量(输出型参数),引用可替代
2、提高传参效率,引用可替代,效率比指针还高,不拷贝字节
3、配合堆内存使用,只能使用指针
4、配合字符串使用,string类可以替代

引用使用格式:
    类型名& 别名 = 数据;

什么情况下使用引用:
    1、跨函数共享变量,引用比指针更安全(无需直接操作地址空间、不存在空引用,也极少
    出现野引用问题)、也比指针更加方便(无需取地址,解引用)
    2、提高传参效率,引用的效率比指针还高,指针最起码还要传递4/8字节的地址编号,但是
    引用是一个字节都不需要传递,但是引用和指针一样都有被修改的风险,因此为了保护目标
    需要增加const

使用引用需要注意的问题:
    1、引用必须初始化,所以不存在空的引用
    2、可以引用右值,但是必须使用const修饰引用
    3、引用不能中途更改引用的目标
    4、函数的返回值类型可以是引用类型,但是不能返回局部变量的引用

常考面试题:指针与引用的相同点和不同点
相同点:
    1、都可以跨函数共享内存,都可以提高函数传参效率、也需要const保护。
    2、可以定义数组指针,也可以定义数组引用

    int arr[5] = {}; 
    int (*arrp)[5] = &arr;
    int (&hehe)[5] = arr;

    3、可以定义函数指针,可以定义函数引用

    void (*fp)(void) = func;
    fp();
    void (&hehe)(void) = func;
    hehe();

不同点:
    1、引用是一种取别名的机制,指针是一种数据类型
    2、引用不需要额外的存储空间,指针需要4/8字节用于存储内存地址
    3、指针可以不初始化,但是引用是必须初始化
    4、指针有空指针,但是没有空引用
    5、指针可以更改指向的目标,但是引用不可以
    6、指针可以配合堆内存使用,但是引用不行
    7、可以定义指针数组,但是不能定义引用数组

五、C++的强制类型转换
C语言原来的强制类型转换依然可以在C++中继续使用
(新类型)数据
强制类型转换都是得到一个临时结果,数据原来的类型
不会发生改变
为什么C++要重新设计强制类型转换?
因为C语言的强制类型转换虽然自由度高,但是非常危险
为什么C++之父设计强制类型转换设计的很复杂,使用很麻烦?
因为他认为只有在程序设计不合理的情况下才需要强制类型转换,
之所以设计复杂就是不想让程序员使用,而是去反思,重新设计自己的代码

//要背出来这四个
1、静态类型转换
static_cast<目标类型>(原数据)
目标类型和原数据的类型之间必须有一个方向能够自动类型转换,否则出错

2、动态类型转换
dynamic_cast<目标类型>(原数据)
目标类型和原数据之间必须存在继承关系,并且目标类型必须是指针类型或引用类型,否则出现错误

3、去常类型转换
const_cast<目标类型>(原数据)
目标类型必须是指针或引用,且除了const属性不同外,其他都要相同,否则出现报错

4、重解释类型转换
reinterpret_cast<目标类型>(原数据)
目标类型只能把整数转换成指针,或者把指针转换成整数,否则会出错

六、面向对象和面向过程
面向过程:
关注如何解决问题,以及解决问题的步骤

面向对象:
    关注的是解决问题的"人",-"对象",以及实现能解决问题的"对象"

    抽象:先找出(想象)能解决问题的"对象",分析该对象解决问题所需要的属性(成员变量)和行为(成员函数)

    封装:把抽象的结果封装成一个类类型(类似结构),并给予类的成员变量、成员函数设置相对应的访问控制权限
    (public\private\protected)

    继承:
        1、在封装类前先考虑现有的类是否能解决部分问题,如果有则可以通过继承,只需要在此基础上扩展即可
        从而缩短解决问题的时间
        2、把一个复杂的大问题拆分成若干个不同的小问题,给每个小问题设计一个类去解决,最后把这些类通过
        继承合并成一个能解决大问题的类,从而降低问题的难度

    多态:
        发出一个指令,系统会根据实际情况执行不同的操作,这种特征称为多态(一个指令多种形态)
        比如:重载过的函数、当调用函数时会根据参数的不同调用不同的版本,具体调用哪个版本在
        编译期间可以确定,称为编译时多态

注意:面向对象的细节的本质上还是面向过程,因此面向对象不是解决问题的捷径,而是以更高的维度去思考问题

七、类和对象
什么是类和对象
类是由程序员设计一种数据类型,里面包含有成员变量、成员函数而对象就是类的实例化,可以理解为使用类
类型创建的变量,创建的过程叫做实例化

类的设计和对象的实例化:
    class 类名
    {
        成员变量;  //类中默认属性是private
    pubilc:
        成员函数;
    };
    对象的实例化:
    方法1:类名 类对象名;
    方法2:类名* 类对象名_p = new 类名;

类的声明、实现、使用:
    1、在头文件中声明:
        class 类名
        {
            成员变量;
        public:
            返回值 成员函数名(参数列表);
        }
    2、在源文件中实现成员函数
    返回值 类名::成员函数名(参数列表)
    {
        //在成员函数中可以直接使用成员变量、成员函数
        //不需要使用 . ->来表示访问
    }
    注意:如果类的内容不多,也可以直接在头文件中把成员函数来实现

八、访问控制限定符
private
私有的,被它修饰的成员,只能在类内访问,这是类的默认访问属性
设置为私有的是为了对类进行保护,一般会把成员变量设置私有

public
    公开的,被它修饰的成员可以在任意位置访问,一般会把成员函数设置公开

protected
    保护的,被修饰的成员只能在本类内和他的子类中访问,但不能在类外访问

九、构造函数
构造函数就是与类名同名的成员函数,当实例化对象时他会自动执行。
当构造函数执行结束后,对象才完成实例化
任务:一般负责对类对象进行初始化,以及资源的分配
class 类名
{
int* p;
public:
类名(参数)
{
p = new int;
}
}

1、构造函数必须是public,否则无法实例化对象
2、构造函数可以重载,可以有多个版本的构造函数(无参、有参)
3、带参数的构造函数的调用
    类名 对象名(实参);   //使用实参调用有参构造
    类名* 对象名 = new 类名(实参);   //使用实参调用有参构造
4、默认情况下编译器会自动生成一个什么都不干的无参构造函数,但是
一旦显式地实现了构造函数,就不再自动生成该无参构造函数了

5、如果给有参构造设置了默认形参,实例化对象时可以出现类似调用无参
构造的语句,但实际是调用有参构造
6、构造函数没有返回值
7、不能使用malloc给类对象分配内存,因为它不会调用构造函数

十、析构函数
任务:析构函数一般负责对类对象内容进行收尾工作,例如:释放资源
保存数据等
当对象销毁时会自动执行
class 类名
{
int* p;
public:
类名(参数)
{
p = new int;
}
~类名(参数)
{
delete p;
}
};
1、析构函数也必须是public
2、析构函数没有参数、返回值、不能重载
3、当类对象生命周期完结,被操作系统自动释放(栈),或者通过
delete手动释放(堆)才会调用析构函数
4、构造函数必定执行,但析构函数不一定执行
5、不能使用free销毁类对象,因为不会调用析构函数
6、如果没有显式地实现析构函数,编译器也会自动生成一个什么都不做的析构函数

十一、初始化列表
初始化列表是属于构造函数的一种特殊语法,只能在构造函数中使用
class 类名
{
const 成员1;
成员2;
public:
类名(参数) :成员1(val),成员2(val) //这才是初始化语句 val可以是常量、变量
{
成员1 = val;属于赋值语句,不是初始化 代const属性的成员就无法赋值
}
};

注意:
1、初始化列表是先于构造函数执行,初始化列表执行时类对象还没有实例化完成,
因此它是一种给const属性成员变量初始化的最佳方案
2、当参数名与成员名相同时,初始化列表可以分辨出来,可以同名
3、当成员中有类类型,该成员的有参构造函数可以在初始化列表中调用