类与对象(4)
类与对象(3)-CSDN博客讲解了赋值运算符重载和取地址运算符重载
1. 再探构造函数
构造函数是每个类中非常重要的部分,负责在对象创建时初始化对象的状态。前面讲解了如何通过构造函数的函数体内赋值来初始化成员变量,但在 C++ 中,构造函数还有一种非常重要的初始化方式——初始化列表。
1.1 什么是初始化列表?
初始化列表是构造函数的一部分,在构造函数体之前,用冒号开始,并列出所有成员变量的初始值或表达式,成员变量之间用逗号分隔。与在构造函数体内赋值不同,初始化列表是在对象构建时就为每个成员变量提供值。
class MyClass {
private:int a;double b;public:MyClass(int x, double y) : a(x), b(y) {// 构造函数体可以为空,成员变量已经在初始化列表中初始化了}
};
这里,a(x)
和 b(y)
就是初始化列表中的初始化,它们直接在对象创建时被初始化,而不是在构造函数体内赋值。
1.2 为什么使用初始化列表?
-
性能提升:
- 对于一些类类型成员,使用初始化列表比在构造函数体内赋值要高效。因为在构造函数体内赋值时,成员变量会先被默认初始化(可能是一些不必要的默认值),然后再被显式赋值,而使用初始化列表直接初始化时,这个成员变量就从一开始就拥有正确的值,避免了不必要的初始化。
例如,
const
类型和引用类型成员变量,如果不通过初始化列表初始化,你会遇到编译错误。 -
必要条件:
- 引用类型成员:引用类型成员必须在初始化列表中初始化,因为引用类型不能在构造函数体内赋值。它们必须在创建时与某个对象关联。
class MyClass {
private:int& ref;
public:MyClass(int& r) : ref(r) {} // 必须在初始化列表中初始化引用类型成员
};
const
成员变量:const
成员变量必须在初始化列表中进行初始化,不能在构造函数体内赋值。
class MyClass {
private:const int num;
public:MyClass(int n) : num(n) {} // 必须在初始化列表中初始化 const 成员变量
};
1.3 没有默认构造的类类型成员:
- 如果类成员没有默认构造函数(即没有无参构造函数),则必须通过初始化列表来显式调用其构造函数进行初始化,否则编译会报错。
class A {
public:A(int x) {} // 有参构造函数
};class MyClass {
private:A a; // 类型 A 没有默认构造函数
public:MyClass(int x) : a(x) {} // 必须在初始化列表中调用 A 的有参构造函数
};
1.4 初始化列表的作用及优化
初始化列表让你能够显式地控制成员的初始化顺序(虽然它们在类中的声明顺序上被初始化)。它能提升效率并避免一些潜在的错误,特别是在涉及到不可赋值的成员(如 const
、引用类型和类类型成员)时。如果你没有使用初始化列表而是在构造函数体内赋值,那么可能会遇到无法编译的错误。
1.5 C++11的新特性:默认值的支持
C++11 引入了在类成员声明中指定默认值的功能。这样,如果构造函数没有显式地在初始化列表中给出某个成员的初始化值,那么会使用该默认值。
class MyClass {
private:int x = 10; // 默认值为 10double y = 3.14; // 默认值为 3.14public:MyClass(int x_val) : x(x_val) {} // 构造函数只初始化 x,y 使用默认值
};
这里,x
会被构造函数中的参数初始化,而 y
将使用默认值 3.14
。
这种方法适用于没有显示在初始化列表中初始化的成员变量,但只能用于内置类型或可以通过默认构造函数初始化的类类型成员。如果没有显式给定默认值,C++ 对于内置类型的成员变量是否初始化并没有明确规定,因此依赖于编译器。
1.6 初始化列表与成员顺序一致性
尽管可以在初始化列表中按任何顺序列出成员变量,建议初始化列表中成员变量的顺序应与它们在类中的声明顺序保持一致。这不仅有助于代码的可读性,也可以避免潜在的错误。
为什么呢? 因为初始化的顺序实际上是按照成员变量在类中声明的顺序来执行的,即使在初始化列表中写的顺序不一样,成员变量依旧按声明顺序被初始化。如果初始化顺序不一致,可能会引入潜在的错误或造成难以发现的bug。
class MyClass {
private:int a;double b;public:MyClass(int x, double y) : b(y), a(x) {} // 不推荐:初始化顺序和声明顺序不一致
};
虽然编译不会报错,但这种写法会看不懂啊!易读性也是关键,更好的做法是:
class MyClass {
private:int a;double b;public:MyClass(int x, double y) : a(x), b(y) {} // 推荐:初始化顺序和声明顺序一致
};
来个小题
#include<iostream>
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();
}
选a选a啊
类
A
的构造函数使用初始化列表对_a1
和_a2
进行了初始化。在初始化列表中,_a1
被初始化为传入的参数a
(即 1),然后_a2
初始化为_a1
的值。所以即使在成员变量声明中_a2
和_a1
默认值为 2,实际初始化顺序仍然根据初始化列表的顺序进行。由于a
的值是 1,最终_a1
和_a2
都被初始化为 1,结果打印1 1
。
2. 类型转换
C++ 支持将内置类型隐式地转换为类类型对象,但前提是该类中有一个接受内置类型为参数的构造函数。如果类定义了这样的构造函数,编译器会自动进行类型转换,允许直接使用内置类型的值来初始化类类型的对象。
然而,如果构造函数前加上 explicit
关键字,C++ 将不再允许这种隐式类型转换。在这种情况下,只有显式地调用构造函数,才能进行类型转换。
#include<iostream>
using namespace std;class MyClass {
public:// 默认构造函数MyClass(int x) {value = x;}void print() {cout << "Value: " << value << endl;}private:int value;
};int main() {// 隐式类型转换:自动将 int 转换为 MyClass 对象MyClass obj1 = 10; // 隐式调用 MyClass(int) 构造函数obj1.print();// 如果构造函数是 explicit 的,就会报错// MyClass obj2 = 20; // 编译错误return 0;
}
- 在
MyClass
类中,我们定义了一个接受int
类型参数的构造函数。这样,编译器允许将int
类型的值隐式地转换为MyClass
类型。- 在
main()
函数中,我们可以直接将10
赋值给obj1
,编译器会自动调用MyClass(int)
构造函数,将10
转换为MyClass
对象。- 如果构造函数前加上
explicit
,则隐式类型转换将被禁止,类似于MyClass obj2 = 20;
会导致编译错误,必须显式调用构造函数来创建对象。
也还有另一种情况
使用 explicit
class MyClass {
public:explicit MyClass(int x) {value = x;}void print() {cout << "Value: " << value << endl;}private:int value;
};int main() {// 显式类型转换MyClass obj1(10); // 正确,显式调用构造函数obj1.print();// 隐式类型转换不再允许,会报错// MyClass obj2 = 20; // 编译错误return 0;
}
- 隐式类型转换: 如果没有
explicit
,编译器可以自动进行类型转换。- 显式类型转换: 使用
explicit
,必须显式地调用构造函数进行类型转换。
3. static成员
在 C++ 中,static
关键字用于修饰类的成员,标识该成员是静态的。静态成员与普通的成员变量或成员函数不同,它们的生命周期是整个程序的生命周期,而非特定对象的生命周期。静态成员在类的所有对象之间共享,而不是每个对象各自拥有一份。
静态成员变量
-
共享特性: 静态成员变量是所有类对象共享的。也就是说,所有的对象对静态成员变量的访问都是共享的,这意味着静态成员变量不会在每个对象中创建独立副本。它们属于类本身,而不是类的某个具体对象。这样,静态成员变量存储在程序的静态内存区域中。
-
初始化:静态成员变量必须在类外进行初始化。尽管在类中可以给出初始值(但这不是强制的),静态成员变量的实际初始化发生在类外部。
class MyClass { public:static int count; // 声明 };// 类外初始化静态成员变量 int MyClass::count = 0;
-
内存位置:静态成员变量存放在静态存储区,而不是类实例的内存中。因此,静态成员变量与对象的生命周期无关,通常它在程序开始时初始化,直到程序退出才会销毁。
-
共享性:静态成员变量是所有类实例共享的,意味着它只有一份副本,不论类实例化多少次。例如:
class MyClass { public:static int count;MyClass() { ++count; } }; int MyClass::count = 0;int main() {MyClass a, b, c;cout << MyClass::count << endl; // 输出 3 }
由于静态成员变量是类级别的,修改一个对象的静态成员变量会影响到所有的对象。
-
生命周期: 静态成员变量的生命周期贯穿整个程序的执行过程,它们在程序开始时初始化,在程序结束时销毁。
class MyClass { public:static int count; // 静态成员变量声明MyClass() {count++; // 每创建一个对象,count 增加} };// 静态成员变量初始化 int MyClass::count = 0;int main() {MyClass obj1;MyClass obj2;MyClass obj3;cout << "Object count: " << MyClass::count << endl; // 输出 3return 0; }
静态成员函数
-
定义:静态成员函数没有
this
指针,因为它不作用于任何特定对象。它只能访问类的静态成员(静态变量和其他静态函数),无法访问类的非静态成员(即实例化对象的成员变量和函数)。 - 使用:静态成员函数通常用于对静态成员变量的操作,或者执行不依赖于特定对象的任务。例如:
class MyClass {
public:static int count;static void increment() {count++;}
};
int MyClass::count = 0;int main() {MyClass::increment();cout << MyClass::count << endl; // 输出 1
}
- 没有
this
指针:由于静态成员函数没有this
指针,它无法直接访问类的非静态成员。例如,不能在静态函数中直接访问this
指向的对象成员。静态函数只能操作静态成员。
访问静态成员
-
静态成员(变量或函数)可以通过两种方式访问:
-
通过类名访问:推荐使用
类名::静态成员
来访问静态成员,这样更清晰地表明静态成员是与类相关,而不是某个对象。
MyClass::count = 5; // 使用类名访问静态成员
- 通过对象访问:尽管可以通过对象访问静态成员,但这通常会引发编译器警告,因为静态成员并不依赖于任何对象,而是类级别的。
MyClass obj;
obj.count = 5; // 可以编译通过,但不推荐
静态成员的生命周期
静态成员变量的生命周期与程序的生命周期相同:在程序启动时初始化,直到程序结束才销毁。即使没有创建任何对象,静态成员变量仍然会存在,并保持其值。
静态成员变量的初始化顺序
在多文件程序中,静态成员变量的初始化顺序可能导致问题。这种问题通常称为静态初始化顺序问题,它发生在静态成员变量的定义顺序不明确时。为了避免此问题,最好将静态成员变量初始化放在单一的 .cpp
文件中,并且避免跨文件间依赖静态成员的初始化顺序。
静态成员函数与非静态成员函数的区别
-
静态成员函数:只能访问其他静态成员,不能访问非静态成员。静态成员函数没有
this
指针,它是独立于对象存在的,因此不依赖于对象的状态。 -
非静态成员函数:可以访问静态成员和非静态成员。非静态成员函数有
this
指针,它是作用于某个特定对象的,因此可以访问该对象的所有成员(静态的和非静态的)。
静态成员的内存管理
静态成员的内存分配和普通成员变量不同。它们并不随着对象的创建和销毁而分配和释放内存,而是由系统在程序运行时静态分配。静态成员的内存直到程序结束时才会释放。
静态成员变量的作用
静态成员变量的主要作用是存储类级别的数据,所有对象共享这些数据。例如,类的实例化计数、全局状态等都可以使用静态成员来管理。
静态成员变量的缺省值
静态成员变量不能在声明时给定缺省值。默认值的赋值只能在类外部进行,因为它不属于任何对象。
// 错误的做法,不能在类内初始化静态成员变量
class MyClass {
public:static int count = 10; // 错误:静态成员变量不能在声明时赋值
};
总结一下,静态成员变量和静态成员函数都是类级别的,它们与特定对象无关,所有对象共享一个静态成员。静态成员函数可以访问静态成员变量,但不能访问非静态成员,而非静态成员函数则可以访问所有的静态成员和非静态成员。在设计类时,静态成员非常有用,比如用于计数、缓存或者全局状态管理等,而不必依赖具体的对象实例。
也跟上面一样来个小题
A:D B A C
B:B A D C
C:C D B A
D:A B D C
E:C A B D
F:C D A BC c;
int main()
{
A a;
B b;
static D d;
return 0;
}
- C c; — 先构造
C
类对象c
,在构造C
类对象时,C
类的构造函数会调用其基类的构造函数(如果有的话)。这涉及到继承链。- A a; — 然后构造
A
类对象a
,没有基类的构造。- B b; — 再构造
B
类对象b
,同样构造其基类(如果有的话)。- static D d; — 最后构造
static D
类对象d
,静态对象在程序结束时销毁,但其构造顺序依赖于全局静态对象的初始化顺序。每个类的构造函数调用顺序是根据类的顺序来执行,并且
C
继承自D
,D
继承自B
,B
继承自A
所以是F哦
下一章讲解友元、内部类、匿名对象和对象拷贝时的编译器优化类与对象(5)-CSDN博客