阜阳网站建设云平台珠海免费网站建设
阜阳网站建设云平台,珠海免费网站建设,dw软件下载安装教程,外贸网站如何选择域名C类和对象下篇学习笔记
1. 类的默认成员函数
类和对象学到后面#xff0c;一个绕不过去的点就是默认成员函数。所谓默认成员函数#xff0c;就是用户自己没有显式实现时#xff0c;编译器可能会自动生成的一组特殊成员函数。
通常会接触到 6 个默认成员函数#xff1a;
构造…C类和对象下篇学习笔记1. 类的默认成员函数类和对象学到后面一个绕不过去的点就是默认成员函数。所谓默认成员函数就是用户自己没有显式实现时编译器可能会自动生成的一组特殊成员函数。通常会接触到 6 个默认成员函数构造函数析构函数拷贝构造函数赋值运算符重载取地址运算符重载const取地址运算符重载真正最重要的是前 4 个后面两个了解即可。学默认成员函数时建议始终从两个角度去理解如果自己不写编译器默认生成的行为是什么如果默认行为不满足需求应该怎么自己实现2. 构造函数构造函数本质上是对象初始化阶段的入口。它的作用就是在对象创建时把对象调整到一个可用状态。构造函数有几个基本特征函数名和类名相同没有返回值也不用写void对象实例化时会自动调用可以重载例如classDate{public:Date(intyear1,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}voidPrint(){cout_year/_month/_dayendl;}private:int_year;int_month;int_day;};这里要注意一个很经典的坑Date d1;Dated2();第一行是定义对象第二行不是定义对象而是函数声明。Date d2();会被编译器解释成“声明一个返回值类型为Date的函数”。2.1 默认构造函数的几个结论如果类里没有显式写构造函数编译器会生成一个默认构造函数。这里有几个很容易混淆的点无参构造函数是默认构造函数全缺省构造函数也是默认构造函数自己不写由编译器生成的那个构造函数也叫默认构造函数也就是说只要不传参数就能调用的构造函数都可以叫默认构造函数。但这几种默认构造函数在同一个类里不能随便同时出现否则会导致调用歧义。2.2 编译器默认生成的构造函数做了什么如果自己不写构造函数编译器默认生成的构造函数对不同类型成员的处理是不一样的。对于内置类型成员一般不会主动初始化值可能是不确定的对于自定义类型成员会去调用该成员的默认构造函数如果这个成员没有默认构造函数就会编译报错所以当类里包含自定义类型成员时默认构造函数能不能正常工作取决于成员对象本身是否支持默认构造。3. 析构函数析构函数和构造函数正好相反。构造函数负责对象创建时的初始化析构函数负责对象销毁时的资源清理。它的基本特征如下函数名是在类名前加~没有参数没有返回值一个类只能有一个析构函数对象生命周期结束时会自动调用比如栈这种内部申请了动态资源的类就必须认真写析构函数classStack{public:Stack(intn4):_capacity(n),_top(0),_a((int*)malloc(sizeof(int)*n)){if(!_a){perror(malloc failed);}}~Stack(){free(_a);_anullptr;_top_capacity0;}private:int*_a;size_t _capacity;size_t _top;};如果类中没有管理资源比如一个纯粹保存日期的Date类析构函数通常可以不显式写直接使用编译器生成的默认析构函数即可。3.1 默认析构函数的行为如果自己不写析构函数编译器默认生成的析构函数同样会区分成员类型。对于内置类型成员不做额外处理对于自定义类型成员会自动调用它们各自的析构函数还有一个很容易忽略的点即使自己显式写了析构函数自定义类型成员的析构函数仍然会在对象销毁时自动调用不需要手动再调一遍。另外局部作用域中如果定义了多个对象析构顺序是后定义的先析构。4. 拷贝构造函数拷贝构造函数本质上是用一个已经存在的对象去初始化另一个正在创建的对象。典型形式如下classA{public:A(constAother);};常见调用形式有两种MyClassobj1(10);MyClass obj2obj1;MyClassobj3(obj1);这里的obj2 obj1虽然看起来像赋值但本质上是在定义对象时用已有对象初始化所以调的是拷贝构造不是赋值运算符重载。4.1 拷贝构造函数的特点拷贝构造函数有几个关键点它是构造函数的一种重载第一个参数必须是同类对象的引用一般建议写成const引用如果用传值方式接收参数会导致无限递归写成引用是因为如果传值那么为了传参又要先拷贝一个对象出来而这个拷贝过程又要调用拷贝构造逻辑就陷入死循环了。所以平时记成一句话就行拷贝构造的参数必须是同类对象的引用最好再加const。4.2 编译器默认生成的拷贝构造如果自己不写拷贝构造编译器也会生成一个默认的版本。默认拷贝构造的行为可以理解为成员逐个拷贝对内置类型成员执行值拷贝也可以理解成浅拷贝对自定义类型成员去调用该成员自己的拷贝构造函数这个默认行为在某些类上是够用的但在某些类上会出问题。4.3 浅拷贝和深拷贝如果一个类里只是一些普通内置类型比如Date这种只有年、月、日默认拷贝构造一般就够用了。但如果类里有指针成员并且指针指向动态资源比如顺序表、栈、链表这类结构默认拷贝构造就可能出问题。因为默认拷贝构造做的是浅拷贝也就是直接把指针值拷过去。这样两个对象里的指针会指向同一块空间后果主要有两个一个对象修改数据另一个对象也会被影响两个对象析构时都会释放同一块空间导致重复释放这时就需要自己实现深拷贝。深拷贝的核心思想是新对象自己重新申请资源把原对象中的内容复制过来两个对象最终各自管理各自的资源所以像Date这种类通常不需要自己写拷贝构造像Stack、链表、字符串模拟实现这类带资源管理的类就往往必须自己写。4.4 传值传参和传值返回C 中自定义类型对象发生传值传参、传值返回时通常都会触发拷贝构造。这也是为什么很多场景下更推荐传引用可以减少拷贝开销避免临时对象带来的额外成本但引用返回也不是随便用的。如果返回的是当前函数内部的局部对象引用函数结束后对象已经销毁返回出去的引用就成了野引用这和野指针本质上是一样的危险。所以可以记一个原则能不能返回引用关键看函数结束后那个对象是否还存在。5. 赋值运算符重载赋值运算符重载解决的是“两个已经存在的对象之间如何赋值”的问题。它和拷贝构造的最大区别在于拷贝构造发生在对象创建时赋值运算符重载发生在对象已经存在之后典型形式如下classA{public:Aoperator(constAother){if(this!other){// 处理赋值逻辑}return*this;}};这里有几个点要记住参数通常写成const A避免不必要拷贝返回值通常写成A方便连续赋值一定要考虑自赋值问题也就是a a如果类中管理了动态资源赋值运算符重载同样不能直接依赖默认行为否则也可能出现浅拷贝问题。5.1 运算符重载的基本认识运算符重载的本质就是函数重载。函数名的形式是operator加运算符符号。例如operatoroperatoroperator它可以写成成员函数也可以写成全局函数。但有些运算符不能重载比如::..*?:sizeof还有几个运算符必须写成成员函数[]()-像这种运算符还要区分前置和后置。后置会额外加一个int形参作为区分标记Aoperator();// 前置Aoperator(int);// 后置6.14 再谈构造函数学到后面会发现构造函数真正重要的地方不只是“会写”而是要分清初始化和赋值。对象创建时成员变量首先进入初始化阶段。构造函数体里的语句本质上更接近赋值而不是严格意义上的初始化。因为初始化只能发生一次赋值则可以发生很多次。所以初始化列表才是成员初始化的正式位置classDate{public:Date(intyear,intmonth,intday):_year(year),_month(month),_day(day){}private:int_year;int_month;int_day;};6.15 初始化列表必须掌握的细节初始化列表这部分有三个细节必须掌握引用成员变量必须在初始化列表中初始化const成员变量必须在初始化列表中初始化没有默认构造函数的自定义类型成员必须通过初始化列表初始化另外还要特别注意初始化顺序。成员变量的实际初始化顺序只和它们在类中的声明顺序有关和初始化列表中的书写顺序无关。如果一个成员依赖另一个成员的值就一定要让被依赖的成员在类里先声明。6.16 explicit 关键字单参数构造函数或者除了第一个参数外其余参数都有默认值的构造函数可能会触发隐式类型转换。例如classA{public:A(intx){}};A aa10;这段代码在没有限制的情况下是可能成立的因为编译器会尝试用10构造一个临时对象。如果不希望发生这种隐式转换可以用explicit修饰构造函数。它的意义很直接就是禁止隐式类型转换让代码语义更清晰。6.17 static 成员static成员体现的是类级别的共享属性而不是对象级别的私有属性。用static修饰的成员变量属于整个类所有对象共享同一份数据通常存放在静态区。一个很经典的例子就是统计对象个数classA{public:A(){_scount;}A(constA){_scount;}~A(){--_scount;}staticintGetACount(){return_scount;}private:staticint_scount;};intA::_scount0;这里要记住几个结论静态成员变量属于类不属于某个具体对象静态成员变量必须类外定义和初始化静态成员函数没有this指针静态成员函数不能直接访问非静态成员非静态成员函数可以访问静态成员6.18 友元友元是一种突破封装的机制。它能带来便利但也会提高耦合度所以不要滥用。友元分为友元函数和友元类。最典型的友元函数场景就是重载operator和operator。它们如果写成成员函数左操作数位置就不对因此更适合写成类外函数再通过友元获得访问私有成员的权限。classDate{friendostreamoperator(ostreamout,constDated);friendistreamoperator(istreamin,Dated);public:Date(intyear1900,intmonth1,intday1):_year(year),_month(month),_day(day){}private:int_year;int_month;int_day;};关于友元有三个边界要特别清楚友元关系是单向的友元关系不能传递友元关系不能继承6.19 内部类如果一个类定义在另一个类的内部这个类就叫内部类。内部类虽然写在外部类里面但它本身依然是一个独立的类不属于外部类对象的一部分。内部类有几个关键特性可以定义在外部类的任意访问区域内部类天然是外部类的友元内部类可以访问外部类的私有成员外部类并不会天然成为内部类的友元内部类的定义不会增加外部类对象大小classA{private:staticintk;inth;public:classB{public:voidfoo(constAa){coutkendl;couta.hendl;}};};6.20 匿名对象匿名对象就是没有名字的临时对象它的生命周期通常只到当前语句结束。A();这种对象最大的特点就是短命一般用于临时调用某个成员函数不需要长期保存。例如Solution().Sum_Solution(10);6.21 拷贝对象时的一些编译器优化对象按值传参、按值返回时理论上会产生拷贝构造、赋值等过程但编译器在很多情况下会做优化减少不必要的中间对象。真正要理解的是下面这几个概念的区别构造拷贝构造赋值析构其中最重要的是分清初始化和赋值不是一回事。对象创建时发生的是初始化对象已经存在之后再接收别的对象内容才是赋值。6.22 再次理解类和对象这一部分更偏理解层面但其实很重要。类不是对象类是对一类事物的抽象是模板是类型对象则是类实例化出来的具体个体。可以把整个过程理解成这样先从现实对象里抽取属性和行为再把这些特征组织成类类形成后它就成了一种新的自定义类型最后再通过类实例化出对象后面学继承、多态、模板、STL本质上都是在这个基础上继续往上搭。6.23 高频易错点总结这一部分细节特别多下面这些点很容易写错Date d2();是函数声明不是对象定义不要把构造函数体中的赋值误认为初始化引用成员、const成员、无默认构造的成员必须走初始化列表成员初始化顺序只看声明顺序不看初始化列表顺序带资源管理的类默认拷贝构造和默认赋值往往不够用自定义类型传值传参、传值返回通常都会触发拷贝构造返回局部对象的引用会产生野引用问题静态成员变量必须类外定义静态成员函数不能直接访问非静态成员友元关系单向、不能传递、不能继承6.24 练习建议如果想把这部分真正学扎实可以多练这几类内容日期类的输入输出重载日期加减天数、日期差值这类综合题带动态资源的栈、顺序表、字符串模拟实现自己实现一版深拷贝和赋值运算符重载调试观察对象传值、返回值时构造和析构的顺序6.25 总结类和对象下篇真正难的不是语法量而是对象模型有没有理解透。这一部分要抓住几条主线默认成员函数到底是编译器怎么生成、什么时候该自己写构造、析构、拷贝构造、赋值运算符重载分别解决什么问题浅拷贝和深拷贝为什么会直接影响程序正确性初始化列表、explicit、static、友元、内部类这些机制分别在解决什么问题把这些点理顺之后后面再学内存管理、模板和 STL很多细节就不会再觉得乱。