ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++:类和对象(下)

C++:类和对象(下) 1.在探构造函数2.类型转换3.static成员4.友元5.内部类6.匿名对象7.对象拷贝时的编译器优化1.在探构造函数1.1初始化列表初始化成员变量可用初始化列表。1.冒号开始接一个以 分隔的数据成员列表后跟初始值/表达式2.每个成员变量在初始化列表中只能出现一次语法理解上初始化列表是每个成员变量定义初始化的地方3.引用成员变量const成员变量没有默认构造的类类型变量必须放在初始化列表位置进行初始化否则会编译报错4.C11支持在成员变量声明的位置给缺省值该值是给没有显示在初始化列表初始化的成员使用的5.尽量使用初始化列表初始化不在初始化列表初始化的成员也会走初始化列表6.没显示在初始化列表初始化的内置类型成员是否初始化取决编译器。7.没显示在初始化列表初始化的自定义类型成员调用该类型的默认构造函数没有则报错。8.初始化列表中按成员变量在类中声明顺序进行初始化跟成员在初始化列表出现的的先后顺序无关。最好声明顺序和初始化列表顺序⼀致class Time { public: Time(int hour) :_hour(hour) { cout Time() endl; } private: int _hour; }; class Date { public: Date(int rx, int year 1, int month 1, int day 1) :_year(year) , _month(month) , _day(day) , _n(1) ,_ref(rx) ,_t(10) { // _year year; // ... // _n 1; // 报错 } void Print() const { cout _year - _month - _day endl; } private: // 声明 int _year; int _month; int _day; const int _n; int _ref; Time _t; }; int main() { int x 0; Date d1(x, 2026, 8, 8); x; return 0; }这是一个简单的初始化列表这里要注意一定要加括号。1.用const修饰的成员变量只能在定义时初始化。2.成员变量是引用那么也只能在定义时初始化。3.而如果是一个成员变量是自定义类型类类型并且没有默认构造函数也要在初始化列表初始化并调用它的构造函数结论∶初始化列表是每个成员变量定义的地方class Stack { public: Stack(int n) { } }; class MyQueue { public: MyQueue(int n 4) :_st1(n) ,_st2(n) {} private: Stack _st1; Stack _st2; }; int main() { MyQueue q; return 0; }这里Stack写了一个带参数构造系统就不会生成默认构造这就导致我们需要在初始化列表给st1和st2定义定义初始值。通过这两张图我们发现如果我们初始化内置类型会出现随机值在C11新增在成员变量声明时给缺省值。这张图我们发现我们没在初始化列表中写_size但是它还是出现在了初始化列表这就体现了没在初始化列表中定义的成员变量也会走初始化列表。这里我们发现我们没在初始化列表初始化st1和st2但是我们可以通过给它们缺省值来调用构造函数。我们可以通过这张图了解一下成员变量走初始化列表的逻辑class Array { public: Array() { // 有些初始化逻辑还必须在函数体完成 if (_ptr nullptr) { printf(malloc fail\n); return; } for (int i 0; i _n; i) { _ptr[i] 1; } } private: int* _ptr (int*)malloc(sizeof(int)*10); int _n 10; }; int main() { Array a; return 0; }这里就体现了函数体的作用它可以帮助我们判断初始化是否为空还可以帮助我们帮忙给已经开好的空间赋值。总结∶能放到初始化列表做的简单初始化优先放初始化列表带有循环、if分支、复杂业务逻辑的代码只能写在函数体里面。这里我们就发现在初始化列表中成员变量初始化是按照声明的顺序初始化而不是初始化列表顺序初始化。看一下这段代码class A { public: A(int a) :_a1(a) , _a2(_a1) {} void Print() { cout _a1 _a2 endl; } private: int _a2 2; int _a1 2; }; int main() { A aa(1); aa.Print(); return 0; }输出结果如下我们发现a2为随机值这是因为虽然它们都有缺省值但在初始化列表中定义了它们而声明时先声明了a2后声明a1。这就导致初始化列表先初始化a2后初始化a1而a2初始化的参数是a1此时未初始化。这就导致a2为随机值而a1参数为1初始化就是1。2.类型转换2.1隐式类型转换C支持内置类型隐式类型转换为类类型对象需要有相关内置类型为参数的构造函数。接下来我们看下在C语言中类型是如何转换的int类型的i可以转换成char类型。会出现转换的原因是它们都表示数据大小这里int类型的i可以强行转换int*类型但在64位情况下需要补位在32位情况下正常运行double类型的d不能强行转换成int*类型因为它们无关class A { public: // 构造函数explicit就不再支持隐式类型转换 //explicit A(int a1) A(int a1) :_a1(a1) { cout A(int a1) endl; } private: int _a1 1; int _a2 2; }; int main() { // 构造 A aa1(1); // 类型转换 // 构造拷贝构造优化-构造 A aa2 1; }这里aa1是构造而aa2涉及类型转换就会生成临时变量在通过拷贝构造给aa2接下来我们看下类型转换的作用class Array { public: void PushBack(const A a) { } }; int main() { const A aa3 1; Array arr; A aa4(4); arr.PushBack(aa4); arr.PushBack(4); return 0; }这里必须用const引用才能实现类型转换因为这样才能接收临时变量。对于PushBack我们想调用它时还必须传一个Array类型的对象但还可以直接通过类型转换传一个实参来调用这样大大提高效率。如果这里的构造函数有两个参数该怎么用呢A(int a1, int a2) :_a1(a1) ,_a2(a2) { cout A(int a1, int a2) endl; } int main() { A aa5 { 5, 5 }; const A aa6 { 5, 5 }; arr.PushBack(aa5); arr.PushBack({ 6,6 }); return 0; }这里传参时就要加上来括住参数同时引用时也要加const调用函数时既可以通过对象调用也可以通过类型转换调用多个参数都需要括起来2.2explicit构造函数前面加explicit就不再支持隐式类型转换这里在构造函数前加上explicit就不能实现类型转换了。2.3类与类类类型的对象之间也可以隐式转换需要相应的构造函数支持int Get() const { return _a1 _a2; } class B { public: B(const A aa) :_b(aa.Get()) {} private: int _b 1; }; int main() { A aa1(1); B bb1 aa1; // const B bb2 aa1; return 0; }这里的Get函数要写在A里面为了能实现在A里面得到成员变量同时还需要在B里面写带const引用A类型为参数的构造才能实现类之间进行转换。3.static成员1..用static修饰的成员变量一定在类外进行初始化2.静态成员变量为所有类对象所共享存放在静态区不属于某个具体的对象不存在对象中3.用static修饰的成员函数静态成员函数没有this指针4.静态成员函数中可访问其他静态成员但不能访问非静态的(无this指针)5.非静态的成员函数可以访问任意的静态成员变量和静态成员函数。6.突破类域可以访问静态成员通过类名::静态成员或对象.静态成员来访问静态成员变量和静态成员函数。7.静态成员也是类的成员受public、protected、private 访问限定符的限制8.静态成员变量不能在声明位置给缺省值初始化因为缺省值是个构造函数初始化列表的静态成员变量不属于某个对象不⾛构造函数初始化列表class A { public: A(int a 1) :_a(a) { _scount; } A(const A aa) :_a(aa._a) { _scount; } // static成员函数没有this指针 static int GetCount() { // _a; // 对非静态成员“A::_a”的非法引用, 没有this return _scount; } // 5:23 private: int _a; //public: static int _scount; // 声明 }; int A::_scount 0; int main() { A aa1(1); A aa2 2; // public //cout A::_scount endl; //cout aa1._scount endl; // private cout aa1.GetCount() endl; cout A::GetCount() endl; return 0; }这个代码是为了计算调用构造函数的次数。我们利用static是为了让它在类里面使其为所有成员共享同时我们想要得到它有两种方式一种是把它放在public中直接指定类域就可以得到它另一种是通过成员函数和对象调用来得到这个值还有就是通过static修饰的成员函数没有this指针和类名直接得到这个值3.1结合题目求123…n要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句A?B:Cclass Sum { public: Sum() { _ret _i; _i; } static int GetRet() { return _ret; } private: static int _i; static int _ret; }; int Sum::_i 1; int Sum::_ret 0; class Solution { public: int Sum_Solution(int n) { // 变长数组调用n次构造函数 Sum arr[n]; return Sum::GetRet(); } };这里通过变长数组调用n次构造而又利用i去记录1到n用ret去记录最终值在利用类去访问静态的函数GetRet来返回得到最终值。同时这里的i和ret通过static使其成为类中对象共享的不会出现每次调用都出现一个新的i和ret4.友元1.友元是一种突破类访问限定符封装的方式分为友元函数友元类在函数/类声明的前面加friend并且把友元声明放到一个类的里面2.友元函数声明可在类任意地方,不受类访问限定符限制(增加耦合度破坏封装)3.外部友元函数可访问类的私有和保护成员友元函数只是声明他不是类的成员函数4.一个函数可以是多个类的友元函数。5.友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员但是友元类的关系是单向的不具有交换性也不能传递//前置声明 class B; class A { // 友元声明 friend void func(const A aa, const B bb); private: int _a1 1; int _a2 2; }; class B { // 友元声明 friend void func(const A aa, const B bb); private: int _b1 3; int _b2 4; }; void func(const A aa, const B bb) { cout aa._a1 endl; cout bb._b1 endl; } int main() { A aa; B bb; func(aa, bb); return 0; }这里因为友元声明时要向上查找BC、C规定只能向上查找目的为了提高效率但是B在下面所以我们要在A前面前置声明B。同时这里的Func函数体现了一个函数可以是多个类的友元函数上面Func是全局函数。下面这个代码Func是B的成员函数class B; class A { friend class B; // B是A的友元B可以访问A的私有保护 private: int _a1 1; int _a2 2; }; class B { // friend class A; public: void func1(const A aa) { cout aa._a1 endl; cout _b1 endl; } void func2(const A aa) { cout aa._a2 endl; cout _b2 endl; } private: int _b1 3; int _b2 4; }; int main() { A aa; B bb; bb.func1(aa); bb.func1(aa); return 0; }这里两个Func不能通过对象访问A的私有就可以把其定义为A的友元但需要声明和定义分离。但这段代码作用的是把B定义为A的友元这就可以访问A的私有。A不能访问B的友元。这就体现了友元是单向的。5.内部类1.内部类:一个类定义在另一个类的内部。2.内部类是独立的类受外部类类域限制和访问限定符限制故外部类定义的对象中不包含内部类。3.内部类默认是外部类的友元类4.内部类也是⼀种封装当A类跟B类紧密关联实现A类就是给B类用那A可设为B的内部类如果放到private/protected位置那么A类就是B类的专属内部类其他地方不行class A { private: static int _k; int _h 1; //public: // 内部类 class B // B默认就是A的友元 { public: void foo(const A a) { cout _k endl; //OK cout a._h endl; //OK } private: int _b 1; }; }; int main() { cout sizeof(A) endl; //A::B b; return 0; }这里计算A的字节大小是4字节也就是一个int类型h的大小它不包含静态的k也不包含内部类B独立的类。5.1使用案例class Solution { // 内部类是Solution的友元类 class Sum{ public: Sum(){ _ret _i; _i; } }; public: int Sum_Solution(int n) { Sum arr[n]; // 调用n次构造函数 return _ret; } private: static int _i; static int _ret; }; int Solution::_i 1; int Solution::_ret 0;这是3.1题目的修改版本这里把Sum写成Sulution的内部类同时把i和ret放在了外部类。这样Sum是外部类的友元就可以访问静态的i和ret并进行修改返回时可以直接返回ret不用在通过函数调用来返回ret了6.匿名对象1.用类型(实参) 定义出来的对象叫做匿名对象相比之前我们定义的类型对象名(实参) 定义出来的叫有名对象2.匿名对象生命周期只在当前一行一般临时定义一个对象当前用一下即可就可以定义匿名对象。class A { public: A(int a 0) :_a(a) { cout A(int a) endl; } ~A() { cout ~A() endl; } private: int _a; }; int main() { // 有名对象 A aa1; A aa2(12); // 匿名对象--声明周期只在当前一行 A(); A(11); return 0; }这里有名对象无参时不能加括号匿名对象无参时必须加括号class Solution { public: int Sum_Solution(int n) { //... return n; } }; int main() { Solution s1; cout s1.Sum_Solution(10) endl; cout Solution().Sum_Solution(10) endl; return 0; }这里如果想调用一次成员函数还需要创建对象来调用成员函数但我们还可以用匿名对象来直接调用成员函数。并且有名对象调用的生命周期很长匿名对象的生命周期只在这一行。结论∶匿名对象用于一次性使用用完即销毁省去定义有名对象的步骤适合只调用一次函数的场景7.对象拷贝时的编译器优化现代编译器会为了尽可能提高程序的效率在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。如何优化C标准并没有严格规定各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化有些更新更激进的编译器还会进行跨行跨表达式的合并优化。class A { public: A(int a 0) :_a1(a) { cout A(int a) endl; } A(const A aa) :_a1(aa._a1) { cout A(const A aa) endl; } A operator(const A aa) { cout A operator(const A aa) endl; if (this ! aa) { _a1 aa._a1; } return *this; } ~A() { cout ~A() endl; } private: int _a1 1; }; void f1(A aa) {} int main() { // 构造拷贝构造-》优化直接构造 A aa1 1; // 传值传参 -》无优化 A aa1(1); f1(aa1); cout endl endl; // 构造拷贝构造-》优化直接构造 f1(A(1)); cout endl endl; f1(1); return 0; }这里的类型转换就是将编译器优化的结果本来是产生临时变量后构造和拷贝构造但经过优化后可以直接构造。而这里传值传参就不会优化因为这里如果合二为一会影响正确性各自有各自的用途。但我们如果传匿名的对象又会出现优化。而这里我们如果写一个隐式类型转换也会优化linux下可以将下面代码拷贝到test.cpp文件编译时用g test.cpp -fno-elideconstructors 的方式关闭构造相关的优化。上面这张图是没有关闭编译器优化时的结果下面是关闭的结果。我们就发现这里存在编译器优化A f2() { A aa; return aa; } int main() { // 传值返回 // 返回时一个表达式中连续拷贝构造拷贝构造-优化一个拷贝构造 vs2019 debug // 一些编译器会优化得更厉害进行跨行合并优化直接变为构造。vs2022 debug f2(); cout endl; // 返回时一个表达式中连续拷贝构造拷贝构造-优化一个拷贝构造 vs2019 debug // 一些编译器会优化得更厉害进行跨行合并优化直接变为构造。vs2022 debug A aa2 f2(); cout endl; // 一个表达式中连续拷贝构造赋值重载-无法优化 A aa3; aa3 f2(); cout endl; return 0; }上面这个VS2019 debug版本这里就把连续的拷贝构造优化为一个拷贝构造。下面这个VS2022 debug版本直接合并优化成构造了。这是因为f2没有接收返回值觉得拷贝也是白拷贝就不拷贝了。这就体现了编译器进化并优化的效果。通过最上面这张图我们发现如果用一个对象接收返回值。将会出现两次拷贝构造。而下面这张图体现了VS2019和2022在debug版本下的优化。而2022的优化逻辑是将aa变成aa2的引用aa不管怎么变aa2都跟aa相同。VS2019 debug版本这里aa3已经创建出来了就无法在合并优化了。还有就是这里aa3在赋值前可能存在其他操作所以它没有优化。而VS2022则局部aa直接构造到外层的返回临时对象的内存空间上直接消除return aa的临时对象的拷贝构造。总结传参时尽量不要用有名对象多用匿名对象或类型转换的方式传参。传值返回接收返回值推荐直接定义对象在拷贝构造的方式
返回列表