首页 / 资讯中心 / 文章详情

【C++修炼】类和对象(下):再探构造、类型转换、友元函数、static成员、内部类、匿名对象

【C++修炼】类和对象(下):再探构造、类型转换、友元函数、static成员、内部类、匿名对象 ★ FEATURED ARTICLE
目录类和对象1. 再探构造函数2. 类型转换3. static成员4. 友元函数5. 内部类6. 匿名对象类和对象1. 再探构造函数之前我们实现构造函数时初始化成员变量主要使⽤函数体内赋值例如带参构造在函数体内用形参为成员变量赋值。但其实还有一种初始化成员变量的⽅式且更推荐这种方式——初始化列表。使用初始化列表的⽅式是在函数体{}前形参列表后以冒号:开始以逗号,分隔显式写出需要初始化的成员变量后跟括号括号内是需要初始化的值或表达式。每个成员变量在初始化列表中只能出现⼀次语法理解上初始化列表被认为是每个成员变量定义与初始化的地⽅同一作用域下变量自然只能定义一次而类体内是成员变量声明的地方。代码示例1.1#includeiostream using namespace std; class Date { public: // 构造函数初始化成员变量的方式第二种——显式使用初始化列表 // 书写格式如下成员变量可分多行写也可一行全部写完 Date(int year, int month, int day) :_year(year) // 成员变量在构造函数的初始化列表处定义并开空间 ,_month(month) // 成员变量也可以在此处初始化 ,_day(day) ,_num( (10 * 2) 1 ) // 括号内可以是表达式用表达式的返回值进行初始化 ,_ptr( malloc( sizeof(int) * 4 ) // 函数表达式malloc的返回值作初始值 { // 走完初始化列表也可以在函数体内执行其他语句 // 例如判断malloc是否成功开辟空间失败则_ptr被初始化为空指针 if (nullptr _ptr) { perror(malloc fail); return; } } void Print() { cout _year 年 _month 月 _day 日 endl; cout _num: _num endl; } private: // 成员变量在类体内声明未实例化不开空间 int _year; int _month; int _day; int _num; int* _ptr; }; int main() { // 对象的定义调用带参构造走带参构造的初始化列表进行成员变量的定义与初始化 Date d(2022, 2, 2); d.Print(); return 0; }运行结果这里额外说明一下以下三个概念需要特别区分声明、定义与初始化。它们经常容易混淆但其实是三种不同的行为只是通常关系密切。类中只声明了成员变量如果不实例化创建对象就不会自动调用构造函数更不会走构造函数的初始化列表对成员变量进行定义与初始化。不定义那么变量就不存在没有物理内存空间就不能存储数据更别谈初始化成员变量。我们只需要牢记在语法逻辑上成员变量是在类中声明是在初始化列表中定义与初始化只是部分变量也可以不初始化或没有初始化的需求因此不是非要走初始化列表初始化可以后续在函数体内赋值。尽量使⽤初始化列表初始化因为⽆论是否显式写初始化列表每个构造函数都有初始化列表⽆论是否在初始化列表显式初始化成员变量每个成员变量都要⾛初始化列表初始化。引⽤成员变量、const成员变量、没有默认构造的类类型成员变量都是定义就必须初始化的变量因此只能在初始化列表初始化不能在函数体内初始化会编译报错。额外注明const将变量从普通变量变为常量一经定义不可修改因此必须在定义时就初始化引用同理类类型成员变量在定义时一定自动调用构造函数进行初始化如果没有默认构造就必须手动传参否则无法调用手动传参就需要在初始化列表显式去写。代码示例 1.2#includeiostream using namespace std; class Time { public: // 类Time没有默认构造已有构造则编译器不能生成构造且没有无参构造、全缺省构造 // 未显式写初始化列表但也有隐式的初始化列表 Time(int hour, int minute, int second) { // 即使在函数体内赋值初始化也要先走初始化列表再执行函数内的语句 _hour hour; _minute minute; _second second; } void Print() { cout _hour h _minute m _second s endl; } private: int _hour; int _minute; int _second; }; class Date { public: // 引⽤成员变量const成员变量没有默认构造的类类型成员变量必须在初始化列表初始化 Date(int year, int month, int day) :_year(year) ,_month(month) ,_day(day) ,_ref(_year) ,_n(100) ,_t(0, 0, 0) // Time类对象_t的定义Time类只有带参构造必须传参才能调用 { // ... } void Print() { cout _year 年 _month 月 _day 日 endl; cout _ref: _ref endl; cout const: _n endl; cout _t:; _t.Print(); } private: int _year; int _month; int _day; // 必须在初始化列表初始化 int _ref; // 引用类型成员变量 const int _n; // const成员变量 Time _t; // 类类型成员变量 }; int main() { Date d(2022, 2, 2); d.Print(); return 0; }运行结果若不初始化_ref、_n、_t运行结果C11⽀持在成员变量声明的位置给缺省值和函数的缺省参数不同不可混为一谈这个缺省值主要是给没有显式在初始化列表初始化的成员变量使⽤的。未显式写初始化列表时若给了成员变量缺省值初始化列表会⽤缺省值初始化成员变量若未给成员变量缺省值对于内置类型成员变量初始化列表是否初始化取决于编译器不确定随机值若未给成员变量缺省值对于自定义类型成员初始化列表调⽤该成员类型的默认构造若没有默认构造则编译报错。代码示例 1.3#includeiostream using namespace std; class Date { public: // 先走初始化列表未显式实现则使用缺省值初始化 Date(int year, int month, int day) { // 走完初始化列表成员变量初始化完毕再执行函数 // 判断一下初始化情况 if (nullptr _ptr) { perror(malloc fail); return; } // 用ptr初始化时动态申请的空间存储数据 _ptr[0] 123; _ptr[1] 456; _ptr[2] 789; } ~Date() { free(_ptr); _ptr nullptr; _year _month _day 0; } void Print() { cout _year 年 _month 月 _day 日 endl; cout _ptr[0]: _ptr[0] endl; cout _ptr[1]: _ptr[1] endl; cout _ptr[2]: _ptr[2] endl; } private: // 成员变量声明的位置给缺省值注意此处不是赋值 int _year 1; int _month 1; int _day 1; int* _ptr (int*)malloc(sizeof(int) * 3); // 缺省值也可以是一个表达式 }; int main() { // 传参调用带参构造使用带参构造的初始化列表定义与初始化成员变量 Date d(2022, 2, 2); // 202222分别传给函数形参 d.Print(); return 0; }运行结果需要注意缺省值与缺省参数是两个完全不同的东西我们可以从几点去区分缺省值在类中成员变量声明处给成员变量缺省参数在函数形参列表内给形参缺省参数不止可以在成员函数中也可以在类外声明定义的函数中缺省值在初始化列表使用未显式实现初始化列表时用于初始化成员变量缺省参数在函数形参列表使用未指定传实参时用于给形参一个默认值。缺省值可以是值或表达式缺省参数只能是值。代码示例 1.4#includeiostream using namespace std; class Date { public: // 1、缺省值用于初始化列表初始化列表未显式写的成员变量则用缺省值初始化 // 2、缺省参数用于函数形参列表给形参默认值。形参可用于初始化列表初始化成员变量 // 也可用于函数体内 Date(int year 1, int month 1, int day 1) { cout 初始化列表初始化后 endl; cout _year: _year endl; cout _month: _month endl; cout _day: _day endl; // 如果不主动使用形参赋值缺省参数与成员变量就是无关的 // 缺省值才是与成员变量的初始化逻辑相关联的 _year year; _month month; _day day; cout 函数体内使用形参赋值后 endl; cout _year: _year endl; cout _month: _month endl; cout _day: _day endl; } private: // 成员变量声明的位置给缺省值注意此处不是赋值 int _year 2; int _month 2; int _day; // 内置类型不给缺省值它的初始化是不确定的 }; int main() { // 调用全缺省构造 Date d; return 0; }运行结果因此成员变量的初始化首先看初始化列表显式初始化了哪些成员变量接着是未显式初始化的成员变量再看缺省值最后看构造函数体内是否赋值。在初始化列表初始化成员变量成员变量的初始化顺序按声明顺序跟成员在初始化列表出现的先后顺序⽆关。因此建议声明顺序和初始化列表顺序保持⼀致。下⾯程序的运⾏结果是什么A. 输出1 1 B. 输出2 2 C. 编译报错D. 输出1 随机值 E. 输出1 2 F. 输出2 1#includeiostream using namespace std; 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(); }分析回答这题选D。首先对象aa的空间包括它的成员变量的空间在main函数栈帧创建时就开辟好了再调用构造函数随函数栈帧的创建形参a的空间也开好了接着走初始化列表成员变量依次定义。因此对象或变量的定义是没有任何问题的不会发生未开空间未定义就使用的情况。这里主要是初始化问题调用带参构造实参1传给形参a。接着是初始化列表根据声明顺序初始化成员变量。因此应该是先初始化_a2再是_a1。于是_a2初始化时_a1是随机值故_a2的值为随机值。_a1初始化用的形参a也就是1故_a1的值为1。初始化列表总结如下2. 类型转换类型转换的使用C⽀持内置类型隐式类型转换为类类型对象需要有相关内置类型为参数的构造函数。代码示例 2.1#includeiostream using namespace std; class A { public: // 带参构造 A(int a1) :_a1(a1) {} void Print() { cout _a1 endl; } private: int _a1; }; int main() { // 正常方式定义对象aa1 A aa1(1); aa1.Print(); // 隐式类型转换方式定义对象aa2直接用参数赋值需要有对应的带参构造支持 A aa2 2; aa2.Print(); // 注意与拷贝构造、赋值重载进行区分 // 隐式类型转换直接使用参数赋值对象 // 拷贝构造拷贝已存在的同类型对象初始化aa2如A aa2 aa1 // 赋值重载已存在对象之间的赋值aa2还在定义与初始化 return 0; }运行结果隐式类型转换在C语言中就已经存在而C将其延展到了类和对象。通过实现对应的带参构造函数支持参数到对象的类型转换。如代码示例2.1中整型2被赋值给对象aa2其实是调用了构造函数将2传给了函数形参从而构建一个隐式的临时对象再将该临时对象赋值给对象aa2这里又要调用一个拷贝构造来初始化aa2。这就是参数能够赋值给对象的直接原因这其中的关键角色是临时对象相当于中转站先用参数构造临时对象再用临时对象通过拷贝构造初始化aa2。将参数2换成2.2也可以浮点数可以转换成整型再调用带参构造构建临时对象再调用拷贝构造即可。如果有匹配的形参为浮点型的构造函数就不用先转换成整型。运行结果warning C4244: “参数”: 从“double”转换到“int”可能丢失数据再将参数换成字符串abc编译报错结果语法意义上类型转换必须要先构造⼀个临时对象再⽤这个临时对象拷⻉构造初始化aa2。但实际情况下出于性能考虑编译器会对理论过程进行优化。当遇到这种连续构造构造 拷⻉构造的情况可能忽略临时对象直接用2构造aa2省去拷贝构造。我们可以用两句代码来验证一下C的强制类型中临时对象的存在并尝试运行程序查看一下在自己使用的平台下是如何优化的。代码示例 2.2#includeiostream using namespace std; class A { public: // 带参构造 A(int a1) :_a1(a1) { cout A(int a1) endl; } // 拷贝构造 A(const A aa) :_a1(aa._a1) { cout A(const A aa) endl; } void Print() { cout _a1 endl; } private: int _a1; }; int main() { // 定义A类对象aa1 // 在构造函数和拷贝构造中添加一行被调用的信息输出有优化就没有拷贝构造的调用信息 cout A aa1 3的调用情况: endl; A aa1 3; // 引用aa1 A raa1 aa1; // 直接引用参数33需要先强制类型转换成临时对象临时对象有常性必须用const引用 // 如果不使用const使用普通引用A会报错E0461非常量引用的初始值必须为左值 cout const A raa2 3的调用情况: endl; const A raa2 3; return 0; }运行结果类型转换的用途类型转换主要用途在哪里呢以函数调用为例传实参时如果形参是类类型对象我们就必须传类类型对象所以我们需要提前定义、构造并初始化一个有名对象。如果有类型转换我们就可以直接传参数参数强制类型转换成临时对象一行函数调用的代码就可以搞定。代码示例 2.3#includeiostream using namespace std; class A { public: // 带参构造 A(int a1) :_a1(a1) { cout A(int a1) endl; } // 拷贝构造 A(const A aa) :_a1(aa._a1) { cout A(const A aa) endl; } void Print() { cout _a1 endl; } private: int _a1; }; // 一个简化的栈 class Stack { public: // 压栈方法 void Push(const A aa) // 形参可能接收临时对象需要const引用 { // ... } private: int arr[10]; // 栈空间 int _top; // 栈顶 }; int main() { // 定义一个栈对象st Stack st; // 一般情况下函数调用逻辑先定义有名对象再传对象给函数 A aa1 1; st.Push(aa1); // 有类型转换直接传参免去对象的定义参数直接类型转换成匿名的临时对象 st.Push(1); return 0; }类类型的对象之间也可以隐式转换只需要有对应的带参构造⽀持。也可以支持多参数的玩法只需要为参数加一对花括号{}即可同样需要有对应的带参构造支持。如果不想发生这种隐式类型转换构造函数前⾯加关键字explicit就可以禁用。代码示例 2.4#includeiostream using namespace std; class A { public: // 构造函数explicit就不再⽀持隐式类型转换 // explicit A(int a1) A(int a1) :_a1(a1) {} //explicit A(int a1, int a2) A(int a1, int a2) :_a1(a1) ,_a2(a2) {} void Print() { cout _a1 _a2 endl; } int Get() const { // 返回相加之和 return _a1 _a2; } private: int _a1 1; int _a2 2; }; // 用B对象存储A对象中的两个成员变量之和 class B { public: // explicit B(const A a) B(const A a) // 形参需要接收一个A对象 :_b(a.Get()) // 获取A对象成员变量之和并初始化 {} void Print() { cout _b endl; } private: int _b 0; }; int main() { // 多参数的玩法有对应带参构造支持即可参数在花括号内 // 括号内的参数依次传给构造函数的形参列表通过构造函数构建临时对象 // 直接这样写是不行的必须用花括号A aa1 5, 6; A aa1 { 5, 6 }; // 或 A aa1({ 5, 6 }); aa1.Print(); // 类类型对象之间的转换A对象——B对象 B bb1 aa1; // 或 B bb1(aa1); bb1.Print(); return 0; }运行结果3. static成员⽤static修饰的成员变量称之为静态成员变量静态成员变量⼀定要在类外进⾏初始化。一个类有静态成员变量这个类定义的所有对象共享静态成员变量。静态成员变量不存在某个对象中而是区别于普通成员变量单独存放在静态区。静态成员变量不能在声明位置给缺省值初始化因为缺省值是用于构造函数初始化列表的静态成员变量不属于某个对象其定义与初始化不⾛初始化列表。代码示例 3.1#includeiostream using namespace std; class A { public: // 静态成员变量在类⾥⾯声明 // 静态成员变量是不能用缺省值的static int _scount 1; // 缺省值用于初始化列表静态成员变量不存在对象中单独存在静态区不走初始化列表 static int _scount; }; // 必须在类外初始化不需要加static声明已经加了 int A::_scount 0; int main() { // sizeof计算类型或对象在内存中所占的字节数 // 验证说明静态成员变量不在类和对象中单独存放 A aa1; cout sizeof(A) endl; // 1个字节以占位标识 cout sizeof(aa1) endl; // 1个字节以占位标识 // A类定义的所有对象aa1、aa2共享静态成员变量 A aa2; // aa1._scount、aa2._scount与A::_scount其实都是同一个scount cout aa1._scount aa2._scount A::_scount endl; return 0; }运行结果其实我们可以将静态成员变量理解为静态的全局变量只是受类域和访问限定符的限制。静态成员变量和全局变量一样运行程序后在main函数之前就已经创建好了到程序结束才生命周期结束。静态成员变量受类域限制类和对象中可以任意使用但成员变量通常为私有受访问限定符限制无法任意访问。因此对于静态成员变量的类外访问问题我们通常会借助静态成员函数成员函数可以是公有间接访问私有成员变量。静态成员也是类的成员受public、protected、private访问限定符的限制。公有的情况下突破类域就可以访问静态成员可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数。⽤static修饰的成员函数称之为静态成员函数静态成员函数没有 this 指针。静态成员函数中可以访问其他的静态成员但是不能访问⾮静态的因为没有this指针。⾮静态的成员函数可以任意访问类中的成员无论是静态成员还是普通成员成员包括成员变量和成员函数。代码示例 3.2#includeiostream using namespace std; class A { public: // 静态成员函数 static int GetACount() { // 需要牢记静态成员函数没有this指针成员函数是通过this指针来访问成员的 // 静态成员函数不可以访问非静态的成员变量或成员函数 // this-_a1 2; // 不可访问 // 通过公有的静态成员函数间接访问私有的静态成员变量 return _scount; } // 普通成员函数任意访问类中成员 void func() { // 普通成员函数有this指针可以访问其他成员变量或成员函数 _a1 2; cout void func() endl; cout _a1 endl; cout _scount endl; cout GetACount() endl; } private: int _a1 1; // 静态成员变量 static int _scount; }; int A::_scount 0; int main() { // 可以用 类域操作符:: 调用 cout A::GetACount() endl; // 也可以用当前类的对象调用 A aa1; cout aa1.GetACount() endl; aa1.func(); return 0; }运行结果静态成员的作用是什么我们知道静态成员变量除了单独存放在静态区具有更长的生命周期外更重要的是它属于类的所有对象。因此对象可以共享静态成员变量存储一些共同数据。以牛客网编程题——JZ64 求123...n 为例不能循环不能递归但我们知道类定义一个对象会自动调用构造函数而对象之间共享静态成员变量利用于此我们可以定义n个对象进行计数从而算出1到n之和。// 求和类Sum利用Sum的构造函数与静态成员变量求和 class Sum { public: Sum() { _ret _i; // 求和 _i; // 下一个数 } static int GetRet() { // 返回求和总值 return _ret; } private: static int _i; // 当前第n个数 static int _ret; // n个数的总和 }; int Sum::_i 1; // 初始化为1从1开始到n int Sum::_ret 0; // 初始化为0从1开始求和 class Solution { public: // 12...n int Sum_Solution(int n) { // 需要编译器支持C99的变长数组 // 定义n个Sum类的数组元素对象 // Sum arr[n]; // 或者使用C的方式new创建对象 Sum* arr new Sum[n]; // 返回求和的值 return Sum::GetRet(); } };4. 友元函数友元是⼀种突破类访问限定符封装的⽅式友元分为友元函数和友元类。对于类外的非成员函数或其他类实现友元需要在函数声明或者类声明前加friend并且把友元声明放到类的⾥⾯。类外部的友元函数可访问类的私有和保护成员友元函数仅仅是⼀种声明不代表它是类的成员函数。友元函数可以在类定义的任何地⽅声明不受类访问限定符限制。⼀个函数可以是多个类的友元函数。代码示例 4.1#includeiostream using namespace std; // 前置声明避免类A中使用了类B或其对象但不认识 class B; class A { // 友元声明——声明前加friend并放到类中任意位置通常放在最上面统一放置 friend void func(const A aa, const B bb); private: int _a1 1; int _a2 2; }; class B { private: int _b1 3; int _b2 4; // 友元声明——声明前加friend并放到类中任意位置 // 声明为友元函数但不代表成为成员函数 friend void func(const A aa, const B bb); }; // 类外的普通函数 void func(const A aa, const B bb) { // 需要在函数内访问A类、B类的私有成员变量故需要声明为友元函数 // 通过友元突破访问限定符的封装 cout aa._a1 endl; cout bb._b1 endl; } int main() { A aa; B bb; func(aa, bb); return 0; }运行结果对于友元类它的成员函数都是另⼀个类的友元函数都可以访问另⼀个类中的私有和保护成员。友元类的关系是单向的不具有交换性⽐如A类是B类的友元但B类不是A类的友元。代码示例 4.2#includeiostream using namespace std; class A { // 友元声明 friend class B; void funcB(const B bb) { // 友元关系是单向的B在A中声明为友元类B中可以访问A的私有 // 但A没在B中声明为友元A中不可以访问B的私有成员 // cout bb._b1 bb._b2 endl; } private: int _a1 1; int _a2 2; }; class B { public: // B类的成员函数中需要访问A类的私有成员变量 // 直接声明B为A的友元类B类的成员函数就都是A类的友元函数 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.func2(aa); return 0; }运行结果友元类的关系不能传递如果A是B的友元B是C的友元但是A不是C的友元。就像我们不一定认识我们朋友的朋友有时提供了便利。但是友元会增加耦合度破坏了封装所以友元不宜多⽤。但该用的时候大胆用。5. 内部类可以在类内部定义另⼀个类内部的这个类叫做内部类。内部类是⼀个独⽴的类跟定义在全局相⽐它只是受外部类类域限制和访问限定符限制所以外部类定义的对象中不包含内部类及其成员变量。内部类默认是外部类的友元类内部类可以访问外部类的私有成员。内部类本质也是⼀种封装当A类跟B类紧密关联A类实现出来主要就是给B类使⽤那么可以考虑把A设计为B的内部类如果放到private/protected位置那么A类就是B类的专属内部类其他地⽅都⽤不了。代码示例 5.1#includeiostream using namespace std; // Sum类就是给Solution类使用的因此可以将Sum设为Solution的内部类 class Solution { // class没有访问限定符默认为私有 // 私有的内部类专属于外部类默认为外部类的友元类 class Sum { public: Sum() { // 成员变量放在外部类内部类可以直接使用 _ret _i; _i; } // 无需get函数访问私有成员变量了私有成员变量在外部类 /*static int GetRet() { return _ret; }*/ }; static int _i; static int _ret; public: int Sum_Solution(int n) { // 可以直接访问原本Sum类的现在Solution的私有成员变量了 Sum* arr new Sum[n]; return _ret; // return Sum::GetRet(); } }; int Solution::_i 1; int Solution::_ret 0; int main() { // 内部类是独立的类外部类对象不存储内部类及其成员变量 // 计算Solution类对象的大小时不包括Sum类的大小 Solution s; // 内部类不仅受类域影响同样受访问限定符影响类外其他部分不可访问私有内部类 // Solution::Sum s; }6. 匿名对象定义对象时可以省去对象名即定义一个匿名对象。相⽐之前正常定义的类型对象名(实参)定义出来的叫有名对象。匿名对象⽣命周期只在当前⼀⾏用完下一行立马销毁。⼀般临时定义⼀个对象当前⽤⼀下即可就可以定义匿名对象。代码示例 6.1#includeiostream using namespace std; class A { public: // 默认构造——全缺省构造 A(int a 0) :_a(a) { cout A(int a 0) endl; } // 析构 ~A() { cout ~A() endl; } private: int _a; }; class Solution { public: int Sum_Solution(int n) { //... return n; } }; int main() { // 定义对象使用默认构造初始化可以不用传参如下 // 调用一次构造程序结束再调用一次析构 A aa1; // 但不能这么定义对象因为编译器⽆法识别下⾯是⼀个函数声明还是对象定义不传参不用写括号 // A aa1(); // 但可以这样定义匿名对象匿名对象可以不⽤取名字 // 但它的⽣命周期只有⼀⾏调用一次构造后下一行立马销毁调用析构 A(); // 也可以给匿名对象传参 A(1); // 一次构造一次析构 // 正常定义的有名对象 A aa2(2); // 一次构造程序结束一次析构 // 匿名对象在这样的场景下就很好⽤当然还有⼀些其他使⽤场景这个我们以后遇到了再说 // 一次构造一次析构 Solution().Sum_Solution(10); // 直接用 类类型括号() 临时定义一个匿名对象调用成员函数 // 正常调用定义一个有名对象再调用成员函数 // Solution s; // s.Sum_Solution(10); // 程序结束两个有名对象两次析构 return 0; }运行结果
阅读完成 · 觉得有帮助?
咨询建站