前提补充知识
- 在c++中,类的成员函数是不占据类空间的,一个类的成员函数是统一存储在一个固定的地方。当需要调用成员函数的时候,到该区域查找使用。
- 类中的静态变量也是不占类的空间的,在类中的静态变量实际上是放在静态区中,生命周期个全局变量类似。但是与全局变量不同的是在使用上,如果这个静态变量是类的私有成员,是不能在外部直接使用这个变量,需要静态函数从类中获取静态变量。
- 静态函数:静态函数的形参中是没有成员函数默认的this指针,不能访问类的普通的成员变量,但是能够访问类的静态变量。
- 默认函数:实际上就是不需要我们明确写出,编译器也会自动生成的函数。
- 重载函数:在c++中,是允许出现同名函数的,只要同名函数满足:参数的类型,参数的个数和参数的顺序至少有一个不同,这样的同名函数就成为重载函数。带调用函数的时候,编译器是会根据参数的类型,个数和顺序与相对应的重载函数进行匹配。注意:函数的返回值是不能作为同名函数是否是重载函数的判断标准的。
- 对象的实例化:对象的申明,实际上没有开辟任何空间;当对象定义时,开辟空间,这就是对象的实例化。
- 缺省值:在没有自定义值时,编译器会自动使用缺省值;当有了自定义的值时,是不会使用缺省值的。
演示:
1.上面第一点和第二点的演示
2.上面第五点的演示
初始化和清除
初始化——构造函数
构造函数:是在成员变量定义时,用来初始化成员变量的函数,在一个对象的生命周期中只会调用一次,就是在该对象顶的时候。
构造函数的特征
- 构造函数的函数名和类名是一致的。
- 无返回值,但是不需要在函数的前面加上void。
- 在对象的实例化时,自动调用构造函数。
- 构造函数是可以重载的,可以有多个构造函数。
- 类中没有构造函数时,编译器是会自动生成的,但是一但有,编译器一般是不生成的。
类中的成员变量
- 内置类型
- 自定义类型
默认构造:不需要传参就可以直接调用的构造函数
构造函数的调用规则
编译器生成的构造函数在对象实例化时,对于内置类型是没有明确说明是否需要初始化(这个具体版编译器),对于自定义类型,是会调用这个自定义类型的默认构造进行初始化,没有默认构造就不会进行初始化。
总结
- 由于初始化的多样性,一般构造函数是需要我们显示地完成。
- 只有极少数的情况下,不需要显示地写构造函数。如:成员变量都时自定义类型时,在实例化时会调用这些定义类型的默认构造。
- 其实不难看出,我们写的构造函数实际上是为了初始化内置类型的,当遇到自定义类型都是调用其的默认构造函数。
注意事项:在自定义类中,无参数构造,全缺省构造和编译器默认生成的构造只能有一个,这个三个函数虽然满足函数的重载规则,但是在实际的调用中,不传参数,编译器是无法确定调用哪个函数的。
构造函数——进阶知识
问题:刚刚上述所讲到构造函数会在对象实例化时调用,对象的实例化在哪?
这个问题就关系到了初始化列表。
用例分析:如上图,第一图中使用构造函数时显示标错,但是在第二张图中时正常的,不难发现时成员变量有区别,一个有const修饰,一个没有,有什么区别呢?int类型的变量,是可以在定义时不舒适化,之后通过赋值修改变量的值,但是const修饰之后,变量的值是不可以修改的,也就是在定义时必须初始化,这就意味着,上述报错就说明:在构造函数的函数体内部实际上,成员变量已经被定义了,分配空间里。那就引述下一个问题,那成员变量的定义在哪儿?
答:实际上在函数体的前面变量已经被定义,初始化,那就引出初始化列表,变量就是在初始化列表时,被定义,也就是开辟空间。当在有些情况下,定义和初始化必须要同时进行时,进必须在初始化列表来完成。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式。
如下图:
当const修饰类中的成员变量时,只能够在初始化列表时进行初始化。
//使用类比来理解初始化列表 int a; //这个语句其实就是变量的初始化,只是没有给初始值,变量开辟空间(这个与构造函数中的初始化列表类似) a=5; //这个其实是在变量已经开辟空间,初始化完成之后,对变量进行一个赋值(这个与构造函数中的函数体类似) //下面就是开辟空间和初始化同时完成的 int b=5; //这个就是变量开辟空间时,给初始值,这些同样也可以在初始化列表中完成
注意
- 每一个成员在初始化列表中只能出现一次
- 类中出现一些特定的成员变量时,必须放在初始化列表位置进行初始化
* 引用成员变量
* const成员变量
* 自定义类型成员(该类型没有默认构造时) - 初始化列表,就算没有写,每一个成员变量都会走一遍,缺省值是会在初始化列表使用。
* 内置类型的成员变量会使用缺省值,没有就看编译器的处理
* 自定义类型成员变量是会调用它的默认构造
* 构造函数的构成:初始化列表+函数体 - 初始化列表的初始化顺序是和申明的顺序是一致的,不是按变量在初始化列表中的顺序决定的
- 隐示类型转换
* 当赋值符号左右两边的类型不同时,会发生隐示类型转换,一般会发生连续构造+拷贝构造,这是编译器会直接优化为直接构造
* 在类的构造函数前面添加explicit关键字,就不会发生隐示类型转换(单参数和多参数都适用)
小细节:
缺省值的的使用,当变量在初始化列表没有出现的时候,才会使用缺省值,当变量在初始化列表出现时,即使没有初始化值,也不会使用缺省值。在vs中这种情况默认是0。
清除——析构函数
析构函数:析构函数与构造函数相反,析构函数不是对对象本身进行销毁,而是对对象中申请的资源进行清除,销毁对象的工作只有编译器来完成的。
需要处理资源的情况:申请空间,打开文件,链接数据库等情况
析构函数的特征
- 不销毁对象,而是清除对象中申请的资源,对象是在生命周期结束时,有编译器清除的。
- 函数名是类名前面加上~。没有参数返回,不需要写返回值,在对象结束前自动调用
- 和构造函数一样,析构函数是不会处理内置类型的,当有申请资源时,是需要自己写相应的析构函数,会处理自定义类型,是会调用自定义类型的析构函数。
- 对象在离开作用域是自动调用的,所有程序结束时,会先析构局部变量后析构全局变量,在同一区域内后定义的对象先析构。
//析构函数的举例 class A { public: ~A(); //析构函数的申明 private: int x; int y; } ~A() //析构函数的定义,函数体中是一段打印是为了显示调用了析构函数 { cout<<~A()<<endl; }
拷贝和复制
拷贝——拷贝构造函数
拷贝构造函数:也是一种构造函数,只有一个形参,该形参是对本类对象的一个引用,一般需要加const修饰,在已存在类类型对象创建新的对象是有编译器自动调用。
//拷贝构造函数的举例 class A { public: A(A&x); //拷贝构造函数的申明 private: int x; int y; } A(A&a) //拷贝构造函数的定义,函数的形参是一个同类的引用 { x=a.x; y=a.y; cout<<A(A&a)<<endl; }
拷贝构造函数的特征
- 通过引用实现,该函数的形参是一个引用
* 原因:如果不是使用引用的话,形参和实参是两个独立的部分,需要先执行是形参对实参的拷贝,那就又要调用拷贝构造,那就会重复上面的步骤,无穷递归且不是停止,这就是什么需要使用引用。(其实也可以使用指针,引用的底层其实就是指针) - 浅拷贝/值拷贝:按字节拷贝,编译器自动生成的拷贝构造函数就是这个。
- 深拷贝:申请空间,需要显示地去完成
* 浅拷贝和深拷贝的不同:比如使用内存管理函数申请空间,对象中有一个指针指向该空间,如果只是浅拷贝,拷贝出来的对象中指针指向空间和原对象指针指向空间是同一个空间,但是深拷贝就是,拷贝出来的对象中指针指向空间和原对象指针指向空间不是同一个空间,也就是说是两个空间,但是这两个空间中的内容一样。
拷贝构造函数的使用场景
- 使用已存在的对象创建新的对象
- 函数参数类型为类类型对象
- 函数返回值为类类型对象
注意:
- 如果对象中没有申请资源,就不需要显示的完成拷贝构造函数,编译器自动生成的拷贝构造函数就够用。
- 对象中都是自定义成员或者内置成员没有指向资源的,那就可以使用编译器生成的拷贝构造函数。
对拷贝构造函数的个人理解:
前面不是介绍了初始化列表,实际上初始化列表存在于每一个构造函数。当然拷贝构造函数也存在初始化列表,学习的时候,我有所疑问是不是所有的拷贝在初始化列表就可以完成?其实我觉得可以将编译器生成的构造函数就认为是初始化列表,因为初始化列表就可以完成浅拷贝,而不能完成深拷贝,深拷贝那些需要我们在函数体中完成。(仅个人认为)
复制——赋值运算符的重载
运算符重载
c++为了增肌可读性,运算符重载是具有特殊名的函数。例如:“=”符号在C语言中只能适用于内置类型,但是是不能用于自定义类型,c++将这些符号进行重载使它们能够使用于自定义类型。
函数名字:operator + 需要重载的运算符符号
函数原型:返回值+函数名+函数的参数列表
特征
- 不能创建新的操作符号(是能重载c++中已有的符号)
- 重载操作符号的函数参数列表中必须要有一个自定义类型
* 原因:因为没有如果没有自定义类型就说明,函数的参数列表是内置类型,一个原因是可能会改变原来操作符号的意识,另一个原因是参数列表没有改变,是不会满足重载函数的要求的。 - 用于内置类型的运算符号,重载为自定义类型的运算符号时,其含义最好不要改变
- 下面的运算符不允许重载
* /.*/::/sizeof/? :/./ (所有的/为分隔符,不与其他符号组成运算符) - 运算符重载中,参数顺序和操作数顺序一致
class A { public: A(int a) { x=a; y=a; } int operator>(A a) //函数的参数顺序为:*this,a { if(x>a.x&&y>a.y) return 1; else return 0; } private: int x; int y; } int main() { A a1(2); A a2(3); a1>a2; //操作数顺序:a1 a2 //上面的*this对应于下面的a1,这就是参数顺序于操作数顺序一致 }
补充知识
- 在取成员函数的地址,需要加上&符号
- 重载为全局,无法访问私有成员(c++中类的封装)下面是解决方案
* 提供这些成员的get和set函数(这些函数是用来获取类成员)
* 友元
* 重载为成员函数 - 重载符号会优先在类里面查找,类中没有编译器就会到全局中查找,还是没有就会报错
- 重载符号,前缀一元运算符和后缀一元运算符在生命的时候是有区别的,后缀运算符在申明和定义的时候参数列表中需要加上int
赋值运算符
赋值运算符:其实就是在类中对“=”进行重载,是操作符重载的一种
前提:两个存在的对象
赋值运算符的使用规则
可以有编译器默认生成,内置成员是会直接实现值拷贝(也就是浅拷贝),但是自定义类型是调用它的赋值重载函数
小细节:
- 引用返回对象是一个局部变量或者是临时变量,当出了函数的作用域,是会被析构的,这是这个引用就成了野指针
- 出了函数作用,变量还在的情况(如静态变量),那就可以使用引用返回
* 引用返回是可以节省拷贝的 - 两个变量在赋值时,是不是产生临时变量的
- 转换,运算等都是会产生临时变量的
取地址重载
const修饰
将const修饰的“成员函数”成为“const成员函数”
成员函数中都是隐藏了一个this指针,const修饰的其实是 *this,这是为了,在调用时,不允许修改对象的成员变量。
class A { public: void print()const { cout<<x<<y<<endl; //因为加上了const,所以 x=2 这样的操作是不允许的 } private: int x; int y; }
取地址以及const取地址(两个取地址重载)
取地址以及const取地址:这两个函数也是操作符函数的一种,是&操作符的两个重
class Date { public : Date* operator&() { return this ; } const Date* operator&()const { return this ; } private : int _year ; // 年 int _month ; // 月 int _day ; // 日 };
上面的两个重载函数的区别就是,在参数列表this指针是否有const修饰,如果有this指针修饰就说明不能通过this来修改成员变量,返回值当然也就需要使用const修饰,这样能避免权限扩大的问题。
函数特征:
这两个函数其实正常情况下使用系统默认生成的函数就已经够用了,如果有特殊情况才需要显示地完成(比如在调用函数时,不希望返回真是的地址)
结语:上面我介绍了C++类和对象中的6个默认函数,这6个默认函数使得自定义类型也可以使用的像内置类型一样方便,希望我写的这篇文章能给你一些帮助。由于篇幅的问题,可能有些问题没有特别详细的讲到,可以私信我,大家一起探讨进步。