尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

c++类与对象(下)

c++类与对象(下) 文章目录前言一、再谈构造函数1、构造函数体赋值2、初始化列表3、explicit关键字二、static成员1、概念2、特征三、友元1、友元函数2、友元类四、内部类五、匿名对象六、拷贝对象时的一些编译器优化1、函数传参的优化2、传值返回的优化3、总结前言一、再谈构造函数1、构造函数体赋值在创建对象时编译器通过调用构造函数给对象中各个成员变量一个合适的初始值。虽然我们之前写的构造函数调用之后对象中已经有了一个初始值但是不能将其称为对对象中成员变量的初始化构造函数体中的语句只能将其称为赋初值而不能称作初始化。因为初始化只能初始化一次而构造函数体内可以多次赋值。class Date{public:Date(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};当我们定义类的成员时我们可能会定义const修饰的成员变量而使用const修饰的变量必须在定义的位置就完成初始化。而我们在下面的代码中虽然在声明时给了_x缺省值但以后_x变量就不可以改变了而且有时候我们并不想给_x赋值为缺省值。所以必须给每个成员变量找一个定义的位置然后在这个位置将这些声明的变量统一进行初始化。所以c中在构造函数中还有个初始化列表的概念。class A{private://成员变量的声明int_a1;int_a2;//这只是给_x的缺省值这样定义以后_x变量的值就不能再改变了constint_x1;};intmain(){A aa;//const修饰的变量必须在定义的位置初始化constinti0;return0;}2、初始化列表哪个对象调用构造函数初始化列表是它所有成员变量定义的位置。初始化列表以一个冒号开始接着是一个以逗号分隔的数据成员列表每个成员变量后面跟一个放在括号中的初始值或表达式。有了初始化列表上述我们遇到的问题就可以解决即我们可以在初始化列表中对const修饰的成员变量进行初始化。并且不管是否显示的在初始化列表中对类成员变量进行初始化编译器都会在初始化列表中对这些变量进行定义初始化所以说初始化列表才是真正对类成员变量进行初始化的地方。class A{public:A()//初始化列表位置在该位置对const修饰的_x进行初始化:_x(5),_a1(2)//此时_a1的缺省值就不会生效了//虽然在下面没有写 _a2(4)来给_a2初始化但是_a2在声明时有缺省值//所以此时编译器会在初始化列表自动对_a2进行初始化为缺省值//即 _a2(1){}private://成员变量的声明int_a11;int_a21;//这只是给_x的缺省值这样定义以后_x变量的值就不能再改变了constint_x;};intmain(){A aa;//const修饰的变量必须在定义的位置初始化constinti0;return0;}1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)2. 类中包含以下成员必须放在初始化列表位置进行初始化(1). 引用成员变量(2) .const成员变量(3). 自定义类型成员(且该类没有默认构造函数时3. 尽量使用初始化列表初始化因为不管你是否使用初始化列表对于自定义类型成员变量一定会先使用初始化列表初始化。class B{public://此时类B的构造函数为有参数的构造函数而默认构造是不需要传参就可以调用的那一个构造函数//所以类B没有默认构造函数此时如果在A类中声明了B类类型的对象_bb就需要在A的初始化列表中将_bb初始化B(inta):_b(0){coutB()endl;}private:int_b;};class A{public:A()//初始化列表位置在该位置对const修饰的_x进行初始化:_x(5),_a1(2),_ref(_a1),_bb(2)//_a1(3) //每个成员变量在初始化列表只能初始化一次所以该语句会报错//虽然在下面没有写 _a2(4)来给_a2初始化但是_a2在声明时有缺省值//所以此时编译器会在初始化列表自动对_a2进行初始化为缺省值//即 _a2(1){}private://成员变量的声明int_a11;int_a21;//const成员变量引用成员变量自定义类型成员变量(并且该类没有默认构造函数时)都必须放在初始化列表位置进行初始化constint_x;int_ref;B _bb;};intmain(){A aa;//const修饰的变量必须在定义的位置初始化constinti0;return0;}4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序与其在初始化列表中的先后次序无关。例如下面的代码中虽然在初始化列表中的顺序为先对_a1初始化再对_a2初始化但是因为在声明时是先声明的_a2然后再声明的_a1。所以其实在初始化列表中先执行的是_a2(_a1)然后再执行_a1(a)所以该程序的结果为_a1为1_a2为随机值。class A{public:A(inta):_a1(a),_a2(_a1){}voidPrint(){cout_a1 _a2endl;}private:int_a2;int_a1;};intmain(){Aaa(1);aa.Print();return0;}其实类中声明的所有成员变量(包括内置类型和自定义类型)都会走初始化列表因为那里才是成员变量定义的地方。内置类型成员虽然不需要显示的在初始化列表出现但是它们的定义也是在初始化列表中定义为随机值自定义类型成员也会在初始化列表中定义即会自动调用该自定义类型的默认构造函数(无参构造函数)如果没有默认构造函数就需要显示的在初始化列表中调用自定义类型的有参构造函数。3、explicit关键字构造函数不仅可以构造与初始化对象对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数还具有类型转换的作用。我们知道当内置类型发生隐式类型转换时其实在中间生成了一个临时变量。例如下面的例子中。那么当我们将自定义类型进行这样的隐式类型转换时是不是也会像内置类型一样中间生成临时对象呢答按逻辑来说应该是这样的但是我们可以分析一下如果生成临时对象的话需要先调用构造函数创建一个临时对象tmp然后又调用一次拷贝构造函数将tmp拷贝给aa2对象即发生了两次函数的调用。所以其实编译器会对这个过程进行优化即会将构造拷贝构造直接优化为构造。即优化为A aa2(1)这样的效果。但是需要注意的是只有类的构造函数为单个参数或者除第一个参数无默认值其余均有默认值时才具有类型转换的作用(c98语法)。A类的构造函数为单个参数时。A类的构造函数中除第一个参数无默认值其余均有默认值。而当我们不想让构造函数具有类型转换功能时就可以使用explicit关键字修改构造函数此时如果再像上面那样直接使用A aa2 1来创建对象时就会发生错误因为此时构造函数不可以进行类型转换了。即会直接报出不存在从int转换到A的适当构造函数的错误。即用explicit修饰构造函数将会禁止构造函数的隐式转换。上面的语法其实是C98的语法C98语法中只支持单参数隐式类型转换但是在C11中支持了多参数隐式类型转换。即如果构造函数的参数不为单参数也可以进行隐式类型转换。二、static成员1、概念声明为static的类成员称为类的静态成员用static修饰的成员变量称之为静态成员变量用static修饰的成员函数称之为静态成员函数。静态成员变量一定要在类外进行初始化。我们可以想一下这个问题实现一个类可以计算出该程序中共创建了多少个类对象我们看到这个问题的第一个想法应该就是创建一个全局变量然后在该类的构造函数和拷贝函数中都对这个全局变量进行1。但是这样写虽然可以得到这个程序中创建了多少个类对象但是num为全局变量是可以随意修改的所以这个办法是很不安全的。intnum0;class A{public:A(inta0){num;}A(constAaa){num;}};voidfunc(A a){}intmain(){Aaa1(5);Aaa2(aa1);A aa3aa1;func(aa1);coutnumendl;return0;}2、特征1. 静态成员为所有类对象所共享不属于某个具体的对象存放在静态区。2. 静态成员变量必须在类外定义定义时不添加static关键字类中只是声明。3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问。4. 静态成员函数没有隐藏的this指针不能访问任何非静态成员。5. 静态成员也是类的成员受public、protected、private 访问限定符的限制。所以上面的代码我们就可以使用static将num设为静态成员。此时静态成员num不属于某个对象它属于所有的对象属于整个类。但是将num设置为静态成员的话还会遇到一个问题即如果将num声明为private时外面就不可以访问到num变量而如果将num声明为public时此时num又变得不安全即可以通过对象.num或者A::num修改num的值。class A{public:A(inta0){num;}A(constAaa){num;}//private://静态成员变量不属于某个对象属于所有对象属于整个类即创建出来的所有对象都共享这一个静态成员变量staticintnum;};//A类中的静态成员变量定义初始化//静态成员变量一定要在类外进行初始化intA::num0;voidfunc(A a){}intmain(){Aaa1(5);Aaa2(aa1);A aa3aa1;func(aa1);//因为静态成员num属于所有对象属于整个类所以A::num也可以访问numcoutaa1.numendl;coutA::numendl;//此时并没有对nullptr进行解引用A*ptrnullptr;coutptr-numendl;return0;}为了解决上面的问题我们可以再定义一个GetNum()函数用来返回num的值但是此时如果我们想到的num的值就还需要实例化一个对象然后再调用该对象的GetNum()方法得到num的值这又显得很不方便。所以我们可以将GetNum()函数也定义为静态成员函数即static int GetNum()。这样不但可以通过对象.GetNum()来调用GetNum()函数也可以通过A::GetNum()来调用GetNum()函数。需要注意的是静态成员函数中不能访问非静态成员变量也不能调用非静态成员函数因为函数内没有this指针。非静态成员函数中可以访问静态成员变量也可以调用静态成员函数class A{public:A(inta0){//非静态成员函数中可以访问静态成员变量也可以调用静态成员函数GetNum();num;}A(constAaa){//非静态成员函数中可以访问静态成员变量num;}voidPrint(){cout_a1endl;}//静态成员函数 -- 没有this指针staticintGetNum(){//静态成员函数中不能访问非静态成员变量也不能调用非静态成员函数因为函数内没有this指针。//_a1;//Print();returnnum;}private://静态成员变量不属于某个对象属于所有对象属于整个类即创建出来的所有对象都共享这一个静态成员变量staticintnum;//声明int_a1;};//A类中的静态成员变量定义初始化//静态成员变量一定要在类外进行初始化intA::num0;voidfunc(A a){}intmain(){Aaa1(5);Aaa2(aa1);A aa3aa1;func(aa1);coutaa1.GetNum()endl;coutA::GetNum()endl;//此时并没有对nullptr进行解引用A*ptrnullptr;coutptr-GetNum()endl;return0;}三、友元友元提供了一种突破封装的方式有时提供了便利。但是友元会增加耦合度破坏了封装所以友元不宜多用。友元分为友元函数和友元类1、友元函数友元函数我们在前面实现Date日期类时其实已经用到了在我们写流插入符和流提取符的重载函数时因为因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置所以我们需要将这两个符号的重载函数设置为全局函数而设置为全局函数后又不能访问Date类中的私有成员变量。所以我们用到了友元函数即在Date类中将这两个符号的重载函数设置为了友元函数这样就可以在这两个函数中访问Date类中的私有成员变量了。class Date{public:Date(intyear,intmonth,intday):_year(year),_month(month),_day(day){}// d1 cout; - d1.operator(d1, cout); 不符合常规调用// 因为成员函数第一个参数一定是隐藏的this所以d1必须放在的左侧ostreamoperator(ostream_cout){_cout_year-_month-_dayendl;return_cout;}private:int_year;int_month;int_day;};友元函数可以直接访问类的私有成员它是定义在类外部的普通函数不属于任何类但需要在类的内部声明声明时需要加friend关键字。友元函数可访问类的私有和保护成员但不是类的成员函数友元函数不能用const修饰(因为友元函数中没有this指针)友元函数可以在类定义的任何地方声明不受类访问限定符限制一个函数可以是多个类的友元函数友元函数的调用与普通函数的调用原理相同class Date{friend ostreamoperator(ostream_cout,constDated);friend istreamoperator(istream_cin,Dated);public:Date(intyear1900,intmonth1,intday1):_year(year),_month(month),_day(day){}private:int_year;int_month;int_day;};ostreamoperator(ostream_cout,constDated){_coutd._year-d._month-d._day;return_cout;}istreamoperator(istream_cin,Dated){_cind._year;_cind._month;_cind._day;return_cin;}intmain(){Date d;cind;coutdendl;return0;}2、友元类友元类的所有成员函数都可以是另一个类的友元函数都可以访问另一个类中的非公有成员。友元关系是单向的不具有交换性。比如Time类和Date类在Time类中声明Date类为其友元类那么可以在Date类中直接访问Time类的私有成员变量但想在Time类中访问Date类中私有的成员变量则不行。友元关系不能传递如果C是B的友元 B是A的友元则不能说明C时A的友元。class Time{friend class Date;// 声明日期类为时间类的友元类则在日期类中就直接访问Time类中的私有成员变量public:Time(inthour0,intminute0,intsecond0):_hour(hour),_minute(minute),_second(second){}private:int_hour;int_minute;int_second;};class Date{public:Date(intyear1900,intmonth1,intday1):_year(year),_month(month),_day(day){}voidSetTimeOfDate(inthour,intminute,intsecond){// 直接访问时间类私有的成员变量_t._hourhour;_t._minuteminute;_t._secondsecond;}private:int_year;int_month;int_day;Time _t;};四、内部类概念如果一个类定义在另一个类的内部这个内部类就叫做内部类。内部类是一个独立的类它不属于外部类更不能通过外部类的对象去访问内部类的成员。外部类并不能越界访问内部类的私有成员。可以在下面的例子中看到A类类型对象中并没有B类的成员b这时因为A和B在空间上是独立的。内部类B写在A里面和写在全局没有区别只是受A的类域限制所以此时如果想要定义一个B类就要去A类的类作用域里面找。如果去全局找的话是找不到B类的。如果将B类在A类中定义为私有则外面就无法访问到B也不能创建B对象即此时B类就只能在A类中创建。注意内部类就是外部类的友元类内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。即内部类B天生就是A的友元类所以在B中可以访问A的私有成员但A不能访问B的私有成员。特性1. 内部类可以定义在外部类的public、protected、private都是可以的。2. 注意内部类可以直接访问外部类中的static成员不需要外部类的对象/类名。3. sizeof(外部类)外部类和内部类没有任何关系。class A{public:voidPrint2(){B bb;//但A不能访问B的私有成员。//cout bb.b endl;}private:inth1;staticintnum;//内部类class B{public:voidPrint(constAa){//内部类B天生就是A的友元类所以在B中可以访问A的私有成员但A不能访问B的私有成员。couta.hendl;// 内部类可以直接访问外部类中的static成员不需要外部类的对象/类名。coutnumendl;}private:intb2;};};intA::num0;intmain(){A aa;coutsizeof(aa)endl;//定义一个内部类B需要标明B类在A类的类作用域中//A::B bb;return0;}五、匿名对象临时对象和匿名对象都具有常型匿名对象的生命周期只在这一行。class A{public:A():_a1(2),_a2(4){num;}~A(){cout~A()endl;}intGetNum(){returnnum;}private:int_a1;int_a2;staticintnum;};intA::num0;intmain(){//不能这样定义对象因为编译器无法识别下面是一个函数声明还是对象定义//A aa1();//但是我们可以这么定义匿名对象匿名对象的特点不用取名字// 但是他的生命周期只有这一行我们可以看到下一行他就会自动调用析构函数A aa2;//当我们想查看创建了多少对象时我们还需要创建一个对象然后通过该对象调用GetNum()方法来得到numcoutaa2.GetNum()endl;//我们也可以创建一个匿名对象该匿名对象的生命周期只在这一行coutA().GetNum()endl;return0;}六、拷贝对象时的一些编译器优化1、函数传参的优化class A{public:A(inta10):_a(a){cout构造函数endl;}A(constAa){_aa._a;cout拷贝构造函数endl;}~A(){cout析构函数endl;}private:int_a;};voidfunc1(A aa){}voidfunc2(constAaa){}intmain(){A aa11;//构造 拷贝构造 - 优化为直接构造func1(aa1);//无优化//按逻辑应该是先将2构造为A类型的临时对象tmp然后调用func1时调用拷贝构造将tmp拷贝给对象aafunc1(2);//构造 拷贝构造 - 优化为直接构造func1(A(3));// 构造 拷贝构造 - 优化为直接构造cout----------------------------endl;//因为为传引用传参所以不会进行拷贝函数的构造所以就不会进行优化func2(aa1);//无优化func2(2);//无优化func2(A(3));//无优化return0;}2、传值返回的优化传值返回在返回时会调用拷贝构造函数但是编译器进行了优化。class A{public:A(inta10):_a(a){cout构造函数endl;}A(constAa){_aa._a;cout拷贝构造函数endl;}~A(){cout析构函数endl;}private:int_a;public:intb;};Afunc3(){A aa;//构造函数aa.b3;//没有优化returnaa;//此时返回aa对象会调用拷贝构造生成aa对象的拷贝然后当作返回值返回}Afunc4(){returnA();//构造匿名对象}intmain(){func3();//func3return时会调用一次拷贝构造函数A aa1 tmp时会调用一次拷贝构造函数。但是编译器会将两次拷贝构造函数优化为一次拷贝构造函数A aa1func3();// 会将两次拷贝构造函数优化为一次拷贝构造函数。func4();//构造拷贝构造 - 优化为构造A aa2func4();//构造拷贝构造拷贝构造 - 优化为构造return0;}3、总结函数传参总结尽量使用const 传参。对象返回总结(1). 接收返回值对象尽量拷贝构造方式接收不要赋值接收。A aa1func3();//而不要分开写因为分开写就不会进行优化了A aa1;aa1func3();(2). 函数中返回对象时尽量返回匿名对象。Afunc4(){//分开写不会进行优化//A aa;//reutrn aa;//一起写才会进行优化returnA();}
返回列表