【C++】类和对象(下)
文章目录
- 上文链接
- 一、再探构造函数
- 1. 初始化列表
- 2. 深入理解初始化列表
- 二、类型转换
- 1. 单参数类型转换
- 2. 多参数类型转换
- 3. explicit
- 4. 类型转换的意义
- 三、static成员
- 1. 静态成员变量
- 2. 静态成员函数
- 3. 静态成员总结
- 4. OJ 练习
- 四、友元
- 1. 引入
- 2. 友元
- 五、内部类
- 1. 内部类的定义
- 2. 内部类的特点
- 六、匿名对象
- 七、对象拷贝时编译器优化
- 1. 一般场景
- 2. 传值返回
- 3. 拷贝构造 + 赋值重载
上文链接
【C++】类和对象(中)——默认成员函数详解(万字)
一、再探构造函数
1. 初始化列表
之前在类和对象(中)里我们讲了构造函数的大部分内容,还有一部分内容需要我们作进一步的探索。
之前我们实现构造函数时,初始化成员变量主要是使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是以逗号分隔的数据成员列表,每个“成员函数”后面跟着一个放在括号中的初始值或表达式。
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; // 这是我们之前写的构造函数 } Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) // 初始化列表方式 { // 函数体内也可以初始化,我们上面写的构造函数就是在这里进行初始化的。 } Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) { _day = day; // 我们还可以一部分用初始化列表,一部分在函数体内初始化 } private: int _year; int _month; int _day; };
2. 深入理解初始化列表
- 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
前半句很好理解,一个成员变量不能在初始化列表中出现多次。那么后半句该如何理解?
首先我们知道,像 const 这样的变量我们是必须要在定义的地方就初始化的。
const int x = 1; // OK const int x; int x = 1; // ERROR
所以说如果我们的类中的成员变量有一个 const 类型,如果我们还像之前那样的方式写构造函数,那么它的行为就相当于我们上面代码的 ERROR 的那一种,是会报错的。
那么这个时候我们就必须要用到初始化列表的方式,它的初始化行为就相当于就在定义的地方将变量初始化,等同于我们上面代码中 OK 的那一种。
class Date { public: Date(int year = 1, int month = 1, int day = 1) :_day(day) // _day 成员是一个 const 类型,必须放在初始化列表中 { _year = year; _month = month; } private: int _year; int _month; const int _day; };
除了 const 类型,引用也是必须放在初始化列表中的,因为引用在定义时也必须进行初始化。另外,还有一种类型也必须放在初始化列表中,就是没有默认构造函数的类对象。
我们之前在讲 Stack 类和 MyQueue 类的时候就提到过说,因为 MyQueue 类中的成员变量是自定义类型,初始化 MyQueue 对象时如果它没有显式写构造函数那么编译器就会去调用 Stack 类中的默认构造函数。但是如果 Stack 类中没有默认构造函数,我们就需要在 MyQueue 中自己去写一个构造函数。
由于我们的类对象是在创建的时候就被初始化的,相当于也是在定义的地方初始化这样的逻辑,比如 Date d1(2025, 4, 22)。所以说我们在显式地在 MyQueue 中实现构造函数时,必须将它的两个 Stack 类的成员变量放在初始化列表中,才符合我们的逻辑,就跟我们普通实例化类对象一样。
class Stack { public: Stack(int n) // 这里需要我们传参,因此它不是默认构造函数,我们需要在 MyQueue 中自己写构造函数 { // ... } private: int* _a; int _capacity; int _top; }; class MyQueue { public: MyQueue(int n = 4) :_pushst(n) // 必须用初始化列表的方式进行初始化 , _popst(n) {} private: Stack _pushst; Stack _popst; }; int main() { MyQueue q; return 0; }
所以总结一下:
- const 成员变量,引用成员变量,没有默认构造函数的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。其他类型的成员变量可以放在初始化列表位置进行初始化,也可以放在函数体内进行初始化。
- C++11 支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显式在初始化列表初始化的成员使用的。
也就是说我们平时在类中定义成员变量的时候是只写了声明,比如 int _year;,而现在我们可以在这个声明的地方给它一个缺省值,比如 int _year = 1;,注意这里不是初始化!这个缺省值是给初始化列表用的。如果初始化列表没有显式初始化,默认就会用这个缺省值初始化。
class Date { public: Date(int year = 1, int month = 1, int day = 1) :_day(day) // 这里显式地实现了初始化列表,所以缺省值就不起作用了。如果没有写,_day就是缺省值 { _year = year; _month = month; } private: int _year; int _month; int _day = 1; // 给了一个缺省值,注意这里不是初始化! }; int main() { Date d1(2025, 4, 22); // d1 -> 2025/4/22 return 0; }
还有像之前的 MyQueue 类,如果 Stack 类已经有了默认构造函数,但是 MyQueue 类中还有一个内置类型,我们就可以使用下面这样的缺省值,更加方便。
class MyQueue { public: private: Stack _pushst; Stack _popst; int _n = 4; // 给了一个缺省值,没有在 MyQueue 中写构造函数那么 _n 就是缺省值 };
除此之外,我们还可以给 const 变量、类对象等一个缺省值,并且缺省值可以是常量,也可以是一个表达式。
class Test1 { public: Test1(int t) { // ... } private: int _t; }; class Test2 { private: int _x = 0; const int _n = 1; Test1 a = 1; // 给一个类对象一个缺省值 int* _ptr = (int*)malloc(12); // 缺省值为一个表达式 };
当我们给一个类对象一个缺省值时,就等同于我们在这个类中以初始化列表的方式对它进行初始化。
class Test2 { public: Test2() :a(1) {} private: Test1 a; };
- 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显式地在初始化列表初始化地内置类型是否初始化取决于编译器,C++ 并没有规定。对于没有显式地在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造函数就会造成编译错误。
对于上图中还需要补充一点:如果引用成员变量 / const 成员变量 / 没有默认构造函数的成员变量如果没有显式地在初始化列表中初始化,那么给缺省值也可以。
上面我们说能用初始化列表进行初始化就用初始化列表,但是也有一些我们需要在函数体中进行初始化,比如说我们在初始化列表中定义了一个数组,开了一块空间,我们需要对这个数组每个单元进行赋值,那么就需要在函数体中写一个循环之类的。
- 初始化列表中按照成员变量在类中的声明顺序进行初始化,跟成员变量在初始化列表出现的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。
关于这一点,下面有一道题考考大家:
下面程序的运行结果是什么( )
A. 输出 1 1
B. 输出 2 2
C. 编译报错
D. 输出 1 随机值
E. 输出 1 2
F. 输出 2 1
class A { public: A(int a) :_a1(a) ,_a2(_a1) {} void Print() { cout A aa(1); aa.Print(); } public: A(int a1) :_a1(a1) {} private: int _a1 = 1; }; public: A(int a1) :_a1(a1) { cout cout A aa1(10); A aa2 = 10; return 0; } public: A(int a1) :_a1(a1) {} int Get() const { return _a1 + _a2; } private: int _a1 = 1; int _a2 = 2; }; class B { public: B(const A& a) :_b(a.Get()) {} private: int _b = 0; }; int main() { A aa1 = 10; B bb1(aa1); // 普通走构造 B bb2 = aa1; // 隐式类型转换 return 0; } public: A(int a1) :_a1(a1) {} A(int a1, int a2) :_a1(a1) ,_a2(a2) {} private: int _a1 = 1; int _a2 = 2; }; A aa1(10, 20); // 普通走构造函数 // C++11 A aa2 = { 10, 20 }; // 隐式类型转换 return 0; } public: explicit A(int a1, int a2) :_a1(a1) ,_a2(a2) {} private: int _a1 = 1; int _a2 = 2; }; int main() { A aa1 = { 10, 20 }; // 会报错 return 0; } public: A(int a1) :_a1(a1) {} private: int _a1 = 1; }; class Stack { public: void push(const A& aa) { // ... } }; int main() { Stack st; // 以前我们的写法 A aa10(10); st.push(aa10); // 有了类型转换之后 st.push(10); return 0; } public: A(int a1) :_a1(a1) {} int Get() const { return _a1 + _a2; } private: int _a1 = 1; int _a2 = 2; }; class B { public: B(const A& a) :_b(a.Get()) {} private: int _b = 0; }; class Stack { public: void push(const A& aa) { // ... cout // ... cout Stack st; A aa1 = 10; B bb1 = aa1; // 隐式类型转换 st.push(aa1); return 0; } public: A(int i = 0) { ++_scount; } A(const A& t) { ++_scount; } ~A() { --_scount; } private: static int _scount; // 声明 }; int A::_scount = 0; // 定义 public: // ... static int _scount; // 声明 }; int A::_scount = 0; // 定义 int main() { A a1, a2; A a3(a1); A a4 = 1; // 只适用于公有的情况 cout public: // ... int GetCount() { return _scount; } private: static int _scount; }; int A::_scount = 0; int main() { A a1, a2; A a3(a1); A a4 = 1; cout public: // ... static int GetCount() // 静态成员函数(没有this指针) { return _scount; } private: static int _scount; }; int A::_scount = 0; void Func() { cout A a1, a2; A a3(a1); A a4 = 1; cout A a[10]; Func(); return 0; } public: Sum() { _ret += _i; _i++; } static int GetSum() { 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) { Sum arr[n]; // 变长数组 return Sum::GetSum(); } }; public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } ostream& operator out Date d1(2025, 4, 28); cout // 友元声明 friend ostream& operator _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; ostream& operator out Date d1(2025, 4, 28); cout // 友元声明 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 public: class B { public: void f(const A& a) // B 默认就是 A 的友元类 { cout A aa; // 创建一个 A 类的对象 A::B bb; // 创建一个 B 类的对象,受外部类类域的限制 return 0; } public: class Sum { public: Sum() { _ret += _i; _i++; } }; int Sum_Solution(int n) { Sum arr[n]; return _ret; } private: static int _i; static int _ret; }; int Solution::_i = 1; int Solution::_ret = 0; A aa1(1); // 有名对象 const A& aa2 = 1; // 类型转化产生的临时对象 A(2); // 匿名对象 return 0; } // 正常情况下我们要创建一个对象,然后再调用函数 Solotion s; s.Sum(10); // 我们还可以使用匿名对象来调用,因为我们这个函数只用一次 Solution().Sum(10); // 在这一行匿名对象已经销毁了 return 0; } const A& aa1 = 1; const A& aa2 = A(3); // 这两种情况依次是临时对象和匿名对象 // 它们的生命周期被延长,跟着 aa1 和 aa2 走 // aa1 和 aa2 被销毁时它们才被销毁 return 0; } public: A(int a = 0) :_a(a) { cout cout cout } int main() { // 构造临时对象,临时对象再拷贝构造aa1 - 优化为直接构造 A aa1 = 10; cout A aa; return aa; // 传值返回 } int main() { func2(); return 0; } A aa; return aa; // 传值返回 } int main() { // 返回一个表达式中,连续拷贝构造+拷贝构造-优化一个拷贝构造 (vs2019 debug) // 一些编译器优化得更厉害,进行跨行合并优化,直接变为构造 (vs2022 debug) A ret = func2(); return 0; } A aa(6); return aa; } int main() { A aa1 = 1; cout