1.函数重载
1.1 概念
我们在封装功能相同、用法相同的函数的时候,只是因为参数的不同,就需要定义多个版本的函数,且函数名不能重名,使用起来很不方便,并且函数重载允许定义重名的函数。
1.2 函数重载的要求
函数名相同,形参列表必须不同:个数或者类型不同都可以,函数重载对返回值没有要求。
1.3 重载函数的调用
调用的时候会根据,实参的不同,自动选择调用哪个函数。编译器在编译的过程中,其实已经根据类型的不同,生成了不同名字的函数。
1.4 函数的默认参数
1.C语言中函数的形参的值必须由实参传递过来;而C++中,允许给函数的形参一个默认值,有了默认值之后:调用函数时,使用了实参,用的就是实参的值调用函数时,没有使用实参,用的就是默认值;
2.因为函数传参的时候遵循靠左原则,所以在给定默认参数的时候要遵循靠右原则,否则会有歧义,报错;
3.函数的默认参数只能写在声明处,不能写在定义处。因为调用函数时,先看到的是函数的声明;
4.函数重载和默认参数同时出现时,会报错。
2. 哑元
在定义函数的时候,可以指定某个或某几个参数只有类型,没有参数名,这个类型只起到一个占位的作用。
哑元可以用于实际开发的过程中代码升级的操作:
如原本函数实现某个功能需要3个参数,升级后只需要2个参数即可,这是只需要修改函数的形参,将不需要的参数改成哑元,调用处都无需修改
注意:这种用法虽然支持,但是一般都不使用
必须使用哑元的场景:
自增、自减运算符重载的时候,会用到哑元作为占位。
例:int my_add(int x, int , int z)
3. 内联函数-inline
内联函数会建议编译器将函数在调用处展开,从而减少函数跳转的时间开销,执行效率更高,但是内联的代价就是可执行文件会变大。
定义内联函数:
在定义函数前,需要使用 inline 关键字修饰
定义内联函数的要求:
1.内联函数只能定义在.h中,不能定义在.cpp中
2.内联函数要求函数体比较小,逻辑比较单一
具体是否在调用处展开,由编译器决定,我们决定不了。
3.1 内联函数和带参宏的区别
1.宏定义是在预处理阶段完成替换的,内联函数是在编译阶段处理的
2.内联函数本质也是函数,有函数的属性,会对参数的类型做检查,而宏定义只是简单的无脑替换。
4. 构造函数
4.1 功能
在类实例化对象的过程中,给成员申请空间,如分配内存、打开文件等操作,完成对成员的初始化。
4.2 格式
(1)构造函数与类同名
(2)构造函数没有返回值
(3)构造函数一般受public权限控制
4.3 调用时机
在类实例化对象的过程中,会自动调用构造函数。
注意:构造函数不能手动调用,并且根据在栈区和堆区的不同,调用时机不同:
栈区:
类名 对象名(构造函数的实参表);//调用构造函数
堆区:
类名 *指针名; //这个过程不会调用构造函数
指针名 = new 类名(构造函数的实参表); //这个过程才会调用构造函数
例:
class Student{
private:
string name;
int *age;
public:
Student(string n, int a){
cout << "我是构造函数,正在初始化" << n <<endl;
name = n;
age = new int(a);
}
};
int main(){
//栈区实例化对象
Student s1("zhangsan", 10);//调用构造函数
s1.show();
//堆区实例化对象
Student *p1;
p1 = new Student("lisi", 20);//调用构造函数
p1->show();
//malloc不会调用构造函数
Student *p2;
p2 = (Student *)malloc(sizeof(Student));
return 0;
}
4.4 构造函数支持重载
默认构造函数:
如果类型没有显性的定义构造函数,编译器会提供一个默认的构造函数,形参列表为void,函数体为空,用来给实例化对象的过程使用。
如果类中显性的定义了构造函数,那么编译器就不再提供默认的版本了,所以,在这种场景下,如果想要使用无参的构造函数,也需要显性手动定义。
4.5 构造函数的初始化列表
可以在定义构造函数的时候,使用冒号的方式引出构造函数的初始化列表。
格式:
类名(构造函数的形参表):成员1(初值1),成员2(初值2){ 构造函数的函数体; }
例:
class Student{
private:
string name;
int *age;
public:
Student(string n, int a):name(n), age(new int(a)){
cout << "我是有参构造函数,正在初始化" << n <<endl;
}
};
4.5.1 必须使用初始化列表的场景
1) 当成员变量名和构造函数的形参名重名时--也可以使用this指针解决;
2) 类中有引用作为成员的时候;
3) 类中有const修饰的成员变量时;
4) 当类中有成员子对象(其他类的对象)时;
例:
class Student{
private:
string name;
int age;
public:
Student(){cout << "Student 无参构造函数" <<endl;}
Student(string n, int a):name(n), age(a){
cout << "Student 有参构造函数" <<endl;
}
void show(){
cout<<name<<" "<<age<<endl;
}
};
class Teacher{
private:
string name;
int age;
Student stu;
public:
Teacher(){cout<<"Teacher 无参构造函数"<<endl;}
//当类中有成员子对象时,需要在构造函数的初始化中调用成员子对象的构造函数
//并传参完成对成员子对象的初始化,如果没有调用成员子对象的构造函数
//默认会调用成员子对象的无参构造函数
Teacher(string n1, int a1, string n2, int a2):\
name(n1), age(a1), stu(n2, a2){//stu(n2, a2)表示调用Student类的有参构造并传参
cout<<"Teacher 有参构造函数"<<endl;
//name = n1;
//age = a1;
//stu.Student(n2, a2);//错误的 构造函数不能手动调用
}
void show(){
cout<<name<<" "<<age<<" "<<endl;
//cout<<stu.name<<" "<<stu.age<<endl;//错误的 name 和 age 是 Student类中的私有的
stu.show();
}
};
5. 析构函数
5.1 作用
在对象消亡的时候,用来做释放空间等善后工作的。
5.2 格式
~类名(void){} //析构函数是没有参数的 所以不能重载
5.3 调用时机
对象消亡时,自动调用。 (析构函数可以手动调用 但是一般不这样做)
栈区:声明周期结束时
堆区:手动调用delete时
5.4 默认析构函数
如果类中没有显性的定义析构函数,编译器会默认提供一个函数体为空的析构函数,用来消亡对象使用的,如果显性定义了,默认的版本就不提供了。
例:
class Student{
private:
string name;
int *age;
public:
//类内定义的写法
~Student(void){
cout<<"析构函数"<<endl;
if(age != NULL){
delete age;
age = NULL;
}
}
};
5.5 构造函数和析构函数调用的顺序
1.对于堆区的对象:
先new哪个对象,就先构造哪个对象,先delete哪个对象,就先析构哪个对象,所以,一般不考虑堆区的构造函数和析构函数的调用顺序
2.对于栈区的对象:
构造函数的调用顺序:按顺序调用
析构函数的调用顺序:逆序调用
----先构造的后析构,栈的顺序
6. 拷贝构造函数
6.1 格式
函数名:与类同名
返回值:没有返回值
形参: const 类名 &
类名(const 类名 &other){
}
6.2 调用时机
用一个已经初始化的类对象,去初始化新对象时,会自动调用拷贝构造函数
类名 对象1(构造函数的实参表); // 有参构造函数
类名 对象2(对象1); //拷贝构造函数
类名 对象3 = 对象1; //拷贝构造函数
类名 *指针名 = new 类名(对象3); //拷贝构造函数
类名 对象2(对象1);
6.3 C++中浅拷贝和深拷贝的区别
浅拷贝:
如果类中没有显性的定义拷贝构造函数,编译器会提供一个默认的拷贝构造函数,这个默认的拷贝构造函数,只完成成员之间的简单赋值,如果类中没有指针成员,只用这个默认的拷贝构造函数,是没有问题的。
深拷贝:
如果类中有指针成员,并且使用浅拷贝,指针成员之间也是只做了简单的赋值,相当于两个对象的指针成员指向的是同一块内存空间,调用析构函数的时候,就会出现 double free 的问题,此时,需要在类中显性的定义拷贝构造函数,并且,给新对象的指针成员分配空间,再将旧对象的指针成员指向的空间里的值拷贝一份过来。