一、函数重载
1、什么是函数重载
在同一个作用域下,函数名相同、参数列表(参数个数和类型)不同的函数构成重载关系
函数重载与返回值类型、参数名无关
2、C++是如何实现函数重载的
通过g++ -s XXX.cpp 生成汇编代码得知,编译器会把函数的参数类型缩写后追加到函数名后面,也就是说编译时会给函数进行换名
3、extern "C"
因为C++编译器在编译函数调用语句时,会找换名后的函数调用,这样就无法调用到已经使用C编译器编译成功的函数了
使用 extern "C" 会让C++编译器按照C编译器的格式来翻译函数名,这样函数的声明与定义就匹配,就可以正确的调用C标准库、系统库函数
4、重载和隐藏
在同一作用域下同名不同参的函数构成重载关系
在不同作用域(父子类)下的同名函数遵循名字隐藏原则
5、参数的类型转换
当调用函数时,编译器会优先调用类型最精确的函数,如果没有则会做一定程度的类型提升或者是标准转换,而不是全部直接报错,
但具体优先级与编译器有关,因此选择最准确的参数即可,否则容易产生函数调用的二义性
二、默认形参
1、什么是默认形参
C++中可以给函数的参数设置默认值,当函数调用者提供了实参则使用实参,如果没有提供则使用默认值
2、默认形参要靠右
如果函数有多个参数,设置了默认形参,必须遵循从右往左依次设置,否则有二义性
3、只能在函数声明处设置默认形参
如果函数声明与定义分开实现,只能在函数声明时设置默认形参,否则语法错误
4、默认形参可能会影响函数重载的效果
如果对同名函数进行了重载,又设置了默认形参,则调用时可能会有冲突
因此为重载过的函数设置默认形参时一定要小心
三、内联函数
1、普通函数
普通函数会被翻译成二进制指令存储在代码段中,调用语句是会生成一句跳转指令,并让程序员跳转到该函数所在代码段处运行,运行结束再返回
2、内联函数
内联函数也会被翻译成二进制指令,调用语句不会生成跳转指令,而是直接把函数的二进制指令替换成调用语句,
这样既没有跳转也没有返回,而是直接往下执行被调用函数,这种函数称为内联函数
3、显示内联和隐式内联
显示内联:在函数的返回值前面加inline,则该函数就以内联机制进行调用,但并不是所有编译器都支持内联机制,我们现在的g++ gcc都不支持
隐式内联:结构、联合、类中的成员函数会自动被当做内联函数处理
4、内联函数的适用条件
内联的优点
节约了函数跳转、返回、传参的时间,提高了代码的运行速度
内联的缺点
当多处调用内联函数时,它的二进制指令会被拷贝多份,产生代码冗余,导致最终的可执行文件增大
适用条件
a、适合内容简单且一次调用多次执行的情况,因此不适合内容多且调用少的函数,因为这样节省的时间还弥补不了牺牲的内存
b、带有递归属性的函数无法内联,编译器会自动屏蔽inline关键
四、引用
什么是引用:引用是一种取别名的机制
为什么使用指针:
1、跨函数共享变量(输出型参数)时,引用可替代
2、提高传参效率,引用可替代,且效率更高
3、配合字符串时,string可以替代
4、配合堆内存使用,继续使用指针
什么情况下适合使用:
1、跨函数共享变量时,引用比指针安全(不存在空引用、极少出现野引用)、引用比指针更方便(不用取地址、解引用)、
2、提高传参效率,引用的传参效率比指针还高,因为指针还需要4/8字节用于存储内存地址,而引用一个字节都不需要,
但是引用与指针一样有被修改的风险,因此为了安全需要加const保护一下
指针与引用的相同点和不同点
相同点:
都需要跨函数共享变量、都可以提高函数传参效率、也都需要const保护
不同点:
1、引用是一种取别名的机制,指针是一种数据类型
2、引用不需要额外的存储空间,而指针需要4/8字节内存用于存储内存地址编号
3、引用不能更换指向目标,而指针可以更改指向
4、引用必须初始化,而指针可以不初始化
5、有空指针,但没有空引用
6、指针可以配合堆内存使用,引用不可以
7、可以定义指针数组,但是不能定义引用数组(可以定义数组指针,也可以定义数组引用,可以定义函数指针,也可以定义函数引用)
使用引用时需要注意的问题:
1、引用必须初始化、不存在空引用
2、可以引用右值,但必须使用const修饰
3、引用不可以修改目标
4、函数返回引用类型的数据时,不要返回局部变量的引用
五、强制类型转换
C语言中强制类型转换还能在C++中继续使用,因此C++中新的强制类型转换显得有点鸡肋
为什么C++要重新设计新的强制类型转换?
因为C语言中的强制类型转换太危险
为什么的强制类型转换设计得很复杂啊、使用麻烦?
因为C++之父认为只有在程序设计不合理的情况下才需要进行强制类型转换,之所以设计复杂就是不想让程序员使用,从而反思重新设计自己的代码
1、静态类型转换
static_cast<目标类型名>(原数据)
目标类型与原数据类型之间必须有个一方向可以自动类型转换
2、动态类型转换
dynamic_cast<目标类型名>(原数据)
目标类型与原类型之间必须存在一个方向可以自动类型转换,否则会出现错误
3、 去常类型转换
const_cast<目标类型名>(原数据)
目标类型和原类型必须是指针或引用,并且除了是否带const属性区别之外,其他类型都必须相同,否则出错
4、重解释类型转换
reinterpret_cast<目标类型名>(原数据)
只能是把整数数据转换成指针,或者把指针转换为整数
六、面向对象与面向过程
面向过程:
关注的是如何解决问题、以及解决问题的步骤
面向对象:
抽象:先找出(想象出)能解决问题的"对象",分析该对象解决问题所需要的属性(成员变量)和行为(成员函数)
封装:把抽象的结果封装成一个类(结构),并给类的成员变量、成员函数设置相应的访问权限(public\protected\private)
继承:
1、在封装类之前先考虑现有的类是否有能解决一部分问题,如果有则把现有的类继承过来,在此基础上进行扩展,以此来解决问题的时间
2、把一个复杂的问题分析拆解成若干个小问题,每个问题设计一个类去解决,最后把这些类通过继承合并到一个能解决最终问题的类中
多态:
发出一个指令,系统会根据实际情况执行不同响应的操作,这种特征称之为多态(统一命令多种形态)
例如:重载过的函数,当调用函数时,编译器会根据参数的类型调用对应的重载版本,这就是一种多态
而且具体调用哪个版本,如果在编译时就能确定下来,这种重载成为编译时多态
特别注意:面向对象的行为细节依然是面向过程,因此面向对象只是从更高的维度去思考解决问题,而不是去寻求解决问题的捷径
七、类和对象
什么是类和对象?
类是由程序员自己设计的一种数据类型,它里面包含了成员变量和成员函数两部分
而对象是类的实例化,其实可以理解为使用类创建的变量
类的设计和实例化:
class 类型
{
成员变量;/类中成员默认属性是private私有
public:
成员函数;
};
类的声明、定义和实例化
1、在头文件中声明类
class 类型
{
成员变量;/类中成员默认属性是private私有
public:
成员函数;
};
2、在源文件中定义类
返回值 类名::函数名(参数列表)
{
//成员函数内可以直接使用成员变量 不用. ->
}
注意:如果类的内容不多,可以考虑在头文件中权限实现出来
3、实例化:
方法1: 类名 类对象名;
方法2: 类名* p =new 类型;
八、访问控制限定符
private
私有的,被它修饰的成员只能在类内访问,是类的默认访问属性
public
公开的,被他修饰的成员可以在任何位置访问,一般把类的成员函数设为公开的
protected
保护的,被它修饰的成员可以在类内和子类中访问,但是不能在类外访问
九、构造函数
构造函数是类的同名成员函数,没有返回值,当实例化对象时他会自动执行,一般负责对类进行初始化、分配资源
class 类名
{
int* p;
public:
类名(参数)
{
p = new int;
}
}
1、构造函数必须是public 否则无法实例化对象
2、构造函数可以重载,可以有多个版本
3、带参数的构造函数的调用方法
类名 对象名(实参);
类名* 对象指针 = new 类名(实参);
4、默认情况下编译器会自动生成一个无参构造函数,该函数什么都不做,一旦显示地实现了构造函数,则编译器不生成该函数
类名 对象名; //调用无参构造,如果没有无参构造,则报错
5、也可以通过设置构造函数的默认形参达到无参构造的效果
6、构造函数没有返回值
7、不要使用malloc为类实例化对象分配内存,因为malloc不会调用构造函数
十、析构函数
析构函数负责对类对象进行首位工作,例如:释放类中的资源、保存数据等,当类对象销毁时会自动调用执行
class 类名
{
int* p;
public:
类名(参数)
{
p = new int;
}
~类名()
{
delete p;
}
}
1、析构函数必须是public
2、没有返回值、参数、不能重载
3、当类对象生命周期完结、或者使用delete销毁对象时会自动调用析构函数
4、如果没有显示的实现析构函数,编译器会自动生成一个什么都不干的析构函数
5、构造函数肯定会执行,但是析构函数不一定执行
6、不要使用free来销毁对象,因为不会执行析构函数
十一、初始化列表
初始化列表是构造函数的特殊语法,只能在构造函数中使用
class 类名
{
public:
类名(参数):成员名1(初始化数据),成员名2(初始化数据)...
{
//构造函数
}
}
1、类的成员变量在老编译标准中不可以设置初始值,而且在构造函数执行之前成员变量都已经定义完毕,
因此const属性的成员变量无法在构造函数内正常赋值初始化(新标准中可以直接设置初始值,但是也只能给常量,功能有限)
2、初始化列表先于构造函数执行的,初始化列表执行时类对象还没有构造完成,因此他是唯一一种能给const属性成员变量赋值的方法
3、当参数名与成员名相同时,初始化列表可以自动分辨成员名和参数名
4、当类中有类类型成员时,该成员的有参构造函数也可以在初始化列表中被执行
十二、对象的创建和销毁过程分析
1、对象的创建
a、给对象划分内存空间(栈、堆)
b、执行初始化列表
根据继承表顺序来调用父类的无参构造或有参构造
通过:父类类名(val)调用父类有参构造
根据成员的定义顺序调用类类型成员的无参构造或有参构造
通过:成员名(val)调用类类型成员有参构造
初始化普通成员
c、执行构造函数、申请资源
2、对象的销毁(创建的逆序)
a、执行自己的析构函数、释放资源
b、根据成员定义顺序,逆序调用类类型成员的析构函数
c、根据继承表顺序,逆序调用父类析构函数
d、释放对象的内存
十三、成员函数是如何区别调用它的对象
1、对象的内存只存储了它的成员变量,没有存储成员函数指针
2、当通过对象调用成员函数时,编译器会自动把对象的地址传递给该成员函数,也就是说成员函数中有一个隐藏的参数,叫做this指针参数,来接收对象的地址
3、this指针拿到了调用对象的地址,就可以访问该对象的成员,完成了区别对象的工作
4、虽然this指针是隐藏的,但是可以在成员函数内显示的使用它,但是正常情况下不写就等于写
十四、常函数
1、被const修饰过的成员函数,称为常函数
返回值 成员函数名(参数列表)const{}
2、已知当对象调用成员函数时,编译器会把对象的地址隐式的传递给成员函数的this指针
3、如果对象被const修饰过,就不能使用普通成员函数,编译会报错,
因为此时传递的对象地址带有const属性,而普通的成员函数隐藏的this指针不带const属性,所以把带const属性的指针赋值给不带const属性的指针变量,编译器不允许
4、如果成员函数被const修饰,本质上是修饰了隐式的this指针,这样this指针就带const属性,也就可以被带const属性的对象调用
5、带const属性的对象只能调用常函数,常函数也只能调用常函数
6、常函数可以重载成不带const属性但其他形参完全相同的成员函数
7、在常函数中不能修改成员变量的值,除非该成员变量定义时被mutable修饰
C语言中const与C++中的const有什么不同?
C和C++的const都是用来保护数据
不同点:
a、不同的是C++中的const会优化变量的取值过程,哪怕该变量的内存被强制修改,也不会改变const变量的值,这样的机制为了安全性考虑
b、C++中的const还可以用于修饰成员函数(隐藏的this指针),定义常函数
一个空的结构体对象在C语言和C++中各自占多少字节,为什么?
在C++结构中可以定义成员函数,且有默认隐藏的四个成员函数(构造、析构、拷贝、赋值),当创建结构对象时会调用成员函数,会把对象的地址自动传递给成员函数,
这种机制就要求结构体对象无论如何都要在内存中有一席之地,因此当结构中没有成员时,编译器也会让结构至少拥有不使用的1字节
十五、拷贝构造和赋值操作
拷贝构造是一种特殊的构造函数,格式为
类名(const 类名& that) //const不是必写的,加更好
{
}
什么时候会调用拷贝构造:当使用旧对象给新对象进行初始化时,会自动调用拷贝构造
Test t;
Test t1 =t;
拷贝构造的任务:
顾名思义拷贝构造负责把旧对象中的成员拷贝给新对象,且编译器会默认生成具备这样功能一个隐藏的拷贝构造函数
什么时候应该显示的实现拷贝构造:
普通情况下编译器自动生成的拷贝构造完全够用,但是当类中的成员有指针且指针分配了堆内存,默认的拷贝构造只会对指针的值进行了拷贝,
这样就导致两个对象的指针成员都指向同一块堆内存,在执行析构函数时会造成重复释放堆内存导致内存崩溃,此时必须要显示的拷贝构造
浅拷贝和深拷贝:
当类中有成员时指针类型且分配了堆内存,浅拷贝(默认的拷贝构造)只会拷贝指针变量的值,深拷贝不拷贝指针变量的值,而是申请新的内存,拷贝原内存中的内容到新内容中
赋值操作函数:
所谓的赋值操作,就是一个对象给另一个对象赋值(两对象都已经创建完毕),在C++中会把运算符当做函数处理,使用运算符相当于调用运算符函数
类名&
operator=(const 类名& that)
{
}
Test t1,t2;
t1=t2; //调用赋值操作
赋值运算符函数的任务:
它与拷贝构造的任务基本一致,默认下编译器也会自动生成具备浅拷贝功能的赋值操作函数,
但是当需要进行深拷贝时,不仅需要显示的实现拷贝构造,同时也需要显示的实现赋值运算符函数
实现赋值运算符函数需要注意的问题:
赋值运算符函数与拷贝构造符函数任务虽然接近,但是实现过程有所不同:
问题1:连个对象的指针都已经分配好内存
a、先释放被赋值者的指针变量所指向的原内存
b、再给被赋值者 的指针变量重现申请内存
c、把赋值者指针变量所指向内存的内容拷贝给赋值者新申请的内存中
问题2:有可能对象给自己赋值
需要判断this和赋值者的地址是否相同,如果相同立即结束,不相同才进行赋值操作
问题3:允许 n1=n2=n3连续赋值操作
因此赋值运算符函数的返回值要返回类对象的引用