1. 前言
在面向对象的世界中,类和对象是构建程序的基石。从构造函数到析构函数,从对象的创建到拷贝,每一步都充满了细节和优化的学问。在上一篇中,我们探讨了类和对象的基础概念,而在这一篇中,我们将深入挖掘类的更高级特性:初始化列表、隐式类型转换、static成员、友元函数、内部类、以及匿名对象。这些概念不仅帮助我们编写更高效的代码,还能更好地理解 C++ 的设计哲学。让我们一起来探索吧!
2. 初始化列表
2.1 初始化列表的意义与使用场景
假设我们有以下代码
class Student
{
public:
Student(int age, string name)
{
_age = age;
_name = name;
}
private:
int _age;
string _name;
}
class Management
{
public:
Management(int age, string name)
{}
private:
Student st1; // 类成员
}
类
Student中没有默认构造函数(即没有无参构造函数),因此Management的构造函数无法正常编译。对于自定义类型,C++ 会尝试使用该类型的默认构造函数进行初始化,但Student中没有默认构造函数。
如果我们需要使用该类型的带参构造函数来初始化自定义成员变量呢,这时候就必须使用初始化列表进行显式初始化。
class Management
{
public:
Management(int age, string name)
: st1(age, name) // 使用初始化列表
{}
private:
Student st1;
};
此时大家可能会产生疑问:既然会默认尝试使用自定义类型的默认构造函数进行初始化,那我直接在
Student中添加一个默认构造函数不就可以了吗?
让我们看以下的对比:
未使用初始化列表的代码
Management(int age, string name)
{
st1 = Student(age, name); // 假设Student类有默认构造函数, 调用默认构造后再赋值
}
假设
st1和st2有默认构造函数,这样写会:
- 先调用默认构造函数构造
st1。- 再通过赋值操作重新赋值,调用拷贝赋值运算符。
- 结果是额外的开销,且代码冗余。
小结
在构造函数中初始化类成员时,特别是在成员变量是另一个类的对象时,推荐使用初始化列表。他可以直接调用目标构造函数,避免默认构造和赋值的多余操作。
2.2 初始化列表的特性
1. 每个成员变量只能在初始化列表中出现一次
初始化列表可以看作是为每个成员变量提供初始化值的地方,每个成员变量在初始化时只能被指定一次。
public:
Student(int age, string name)
:_age(age)
,_name(name)
,_name(name) // 编译报错 成员Student::_name已被初始化
{}
2. 必须使用初始化列表初始化的成员变量
以下三种类型的成员变量必须在初始化列表中初始化,否则会导致编译错误:
- 引用成员变量 (
&):引用类型必须在定义时初始化。 const成员变量:常量成员变量只能在初始化列表中赋值。- 没有默认构造函数的类类型成员:如果成员变量的类型没有默认构造函数,就必须显式通过初始化列表初始化。
3. 初始化顺序遵循成员声明的顺序
成员变量的初始化顺序与其在类中声明的顺序一致,而与初始化列表中出现的顺序无关。
class Student
{
public:
Student(int age, string name)
:_name(name)
,_age(age) // 实际初始化顺序是 _age -> _name
{}
private:
int _age;
string _name;
}
为避免混乱,建议初始化列表的顺序与成员声明的顺序保持一致。
4. 优先使用初始化列表
初始化列表是初始化成员变量的最佳方式,它会显式覆盖声明时的默认值,并避免依赖编译器对内置类型成员变量的默认初始化行为。推荐始终使用初始化列表进行显式初始化。
3. 隐式类型转换
1 内置类型到类类型对象
C++ 支持将内置类型隐式转换为类类型对象,只要该类定义了一个以该内置类型为参数的构造函数。
class Student
{
public:
Student(int age)
:_age(age)
{}
private:
int _age;
}
Student st = 20; // 隐式调用 Student(int)
2.explicit 关键字
如果在构造函数前加上 explicit 关键字,隐式类型转换将不再被支持,必须显式调用构造函数。
class Student
{
public:
explicit Student(int age)
:_age(age)
{}
private:
int _age;
}
Student st = 20; // 编译错误
Student st(15); // 必须显式调用
3. 类类型对象之间的隐式转换
类类型的对象之间也可以进行隐式转换,但需要相应的构造函数或类型转换运算符支持。
class A {
public:
A(int value)
: _value(value)
{}
// 成员函数:返回成员变量 _value 的值
int getA() {
return _value;
}
private:
int _value;
};
class B {
public:
// 构造函数:接收一个 A 类型对象,将其成员变量值传递给 B 的成员变量
B(A a)
: _value(a.getA()) // 初始化列表,调用 A 的成员函数 getA() 获取值
{}
private:
int _value; // 整型成员变量,用于存储从 A 类型对象中获取的值
};
A aa(10);
B bb = aa; // 隐式调用 B(A a) 构造函数,将 A 类型对象 aa 转换为 B 类型对象
4. static成员
用static修饰的成员变量,称之为静态变量
4.1 基本概念
static成员是属于类本身的,而不是属于某个具体对象的。static成员变量在内存中只会有一份共享的副本,所有对象共享这一份数据。static成员函数只能访问static成员变量,不能直接访问非静态成员。
class Example
{
public:
static void func()
{
// 错误,无法访问非静态成员
cout << value << endl;
}
private:
int value;
}
4.2 使用注意
1. 静态成员变量的初始化:
静态成员变量必须在类外进行定义和初始化,否则会导致链接错误。
class Example
{
private:
static int value; // 声明
}
int Example::value = 0; // 初始化
2. 访问方式
静态成员可以通过类名访问:
cout << Example::value << endl;
当然也可以通过对象访问,但推荐使用类名以提高可读性。
3. 内存管理:
静态成员变量的生命周期与程序相同,从类加载到程序结束,不依赖于对象的创建和销毁。
4.3 使用示例
使用静态变量统计类的个数
class Example {
public:
// 每创建一个对象,_count 自增 1
Example() {
_count++;
}
// 每销毁一个对象,_count 自减 1
~Example() {
_count--;
}
// 返回当前的对象计数
static int getCount() {
return _count;
}
private:
// 创建静态变量用于存储对象的计数
static int _count;
};
// 静态变量在类外初始化
int Example::_count = 0;
int main() {
Example obj; // 创建一个对象,_count 自增 1
cout << Example::getCount() << endl; // 输出1
return 0;
}
计数器使用静态的原因是静态成员变量被所有对象共享,能够用于跟踪所有对象的全局状态
4.4 静态成员的访问
我们来看以下代码:
class Example
{
private:
static int value;
}
int Example::value = 0;
int main
{
cout << Example::value << endl; // 输出0
return 0;
}
这时候也许大家会有疑问: value不是私有的吗,为什么还是能够直接访问呢
虽然 value 是私有的,但它仍然可以通过 Example::value 直接访问。这是因为:
private 的限制是指只能在类的内部访问。当我们直接通过 Example::value 修改静态成员变量的值时,其实仍然是在类的作用域中操作。因为静态成员变量的声明是在类中完成的,所以类自身有权直接访问它,即使在 main 函数中这样写:
Example::value = 1;
本质上其实是在使用类的权限操作静态变量。
静态成员变量虽然声明在类中,但它的定义(
int Example::value = 0;)在类外部,这让它的存储空间是独立于类的对象的。这是静态成员的特殊设计,使得Exmple::value这样的访问方式成为可能。
5. 友元
友元是 C++ 中一种打破类访问限定符的机制,可以让外部函数或类访问类的私有和受保护成员。友元分为:
1. 友元函数:通过在函数声明前加 friend 关键字,将函数声明为某个类的友元。
class A
{
friend void func();
}
友元类:通过在类声明前加 friend 关键字,将某个类声明为另一个类的友元。
class A
{
friend class B;
}
class B
{}
5.1 友元函数的特点
- 友元函数可以访问类的私有和受保护成员。
- 友元函数不是类的成员函数,仅仅是一个声明。
- 友元函数可以在类的任何位置声明,不受访问限定符(
private、protected、public)的限制。 - 一个函数可以是多个类的友元。
class Example2;
class Example1
{
// 友元函数不是类的成员函数,仅仅是一个声明
friend void func(Example1& obj1, Example2& obj2);
private:
int value1;
}
class Example2
{
// func可以是多个类的友元
friend void func(Example1& obj1, Example2& obj2);
private:
int value2;
}
void func(Example1& obj1, Example2& obj2)
{
// 友元函数可以访问类的私有成员
cout << obj1.value1 << endl;
cout << obj2.value2 << endl;
}
在
class Example1的定义时,编译器尚未遇到class Example2的定义,所以需要先告诉编译器class Example2是一个类,这就是前置声明的作用。
5.2 友元类的特点
- 友元类中的所有成员函数都可以访问被声明为友元的类的私有和受保护成员。
- 友元关系是单向的:如果类
A是类B的友元,B不能自动成为A的友元。 - 友元关系是不可传递的:如果
A是B的友元,B是C的友元,那么A不是C的友元。
class A {
private:
int value; // A 的私有成员
};
class B {
// 声明 A 是 B 的友元类
friend class A;
};
class C {
// 声明 B 是 C 的友元类
friend class B;
};
int main() {
B b;
C c;
// 错误示例 1: B 并不是 A 的友元,因此无法访问 A 的私有成员 value
// cout << b.value << endl;
// 错误示例 2: C 也不是 A 的友元,友元关系不具有传递性,因此无法访问 A 的私有成员 value
// cout << c.value << endl;
return 0;
}
友元打破了封装性,增加类与类之间的耦合度,滥用会导致代码维护困难,不建议频繁使用。
5.3 友元的使用示例
当我们重载流插入和提取运算符时,是否曾有过这样的疑问:究竟应该将它们重载为成员函数,还是全局函数呢?
重载为成员函数可以直接访问类的私有成员,但流运算符的左操作数通常是标准流对象如cin或cout,这使得成员函数重载不够直观。相反,重载为全局函数更为自然,但由于无法直接访问类的私有成员,这时候就需要借助友元机制来解决。
代码示例: Student类
class Student
{
public:
// 声明友元函数,使其可以访问类的私有成员
friend ostream& operator<<(ostream& os, Student& s);
friend istream& operator>>(istream& is, Student& s);
private:
int _age;
string _name;
}
// 全局函数重载流插入运算符(输出)
// 友元函数的作用是允许直接访问 Student 的私有成员
ostream& operator<<(ostream& os, Student& s)
{
os << s._age << " " << s._name << endl;
return os;
}
// 全局函数重载流提取运算符(输入)
istream& operator>>(istream& is, Student& s)
{
cout << "请输入学生的年龄和姓名: " << endl;
is >> s._age >> " " >> s._name;
return is;
}
6. 内部类
内部类是定义在另一个类内部的类。通常用来实现逻辑上的紧密绑定,强调内部类的作用是为了辅助外部类的功能,且不希望在外部类之外被单独使用。
6.1 内部类的概念
如果一个类定义在另一个类的内部,这个类就叫内部类(Nested Class)。
内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象访问内部类的成员。
需要注意的是,外部类对内部类没有任何特殊的访问权限。
特别注意
- 内部类被认为是外部类的友元类(Friend Class)。
- 外部类不是内部类的友元,因此外部类无法直接访问内部类的成员。
6.2 内部类的特性
1. 内部类的访问修饰符
内部类可以定义在外部类的 public、protected 或 private 部分,这取决于设计需求。
2. 访问外部类的成员
- 内部类可以直接访问外部类中的
static成员,不需要外部类的对象或类名。 - 如果需要访问外部类的非静态成员,则需要通过外部类对象。
3. 内存占用关系
- 内部类和外部类的内存占用是独立的。
sizeof(外部类)的值与内部类无关。
6.3 内部类使用示例
使用内部类解决求1+2+3+...+n问题
class Solution
{
public:
class Sum
{
// 每次创建一个 Sum 对象时,都会将 _val 的值累加到 _ret。
Sum()
{
_ret += _val; // 访问外部类的静态成员变量
}
};
// 外部类的公共接口,用于获取累加结果
int getRet(int n)
{
Sum sum[n]; // 创建 n 个 Sum 对象
return _ret; // 返回累加的结果
}
private:
static int _ret;
static int _val;
};
int Solution::_ret = 0; // 静态成员变量在类外进行初始化。
int Solution::_val = 1;
7. 匿名对象
匿名对象是指在定义时没有显式提供名称的对象,通常直接通过 类型(实参) 的形式创建,用于临时性的任务。
-
定义方式:
匿名对象通过类型(实参)的形式创建。
class Example
{}
int main()
{
Example(); // 创建匿名对象
return 0;
}
-
生命周期:
匿名对象的生命周期仅限于它所在的表达式,执行完后立即销毁。
class Example
{
Example()
{
cout << "Example()" << endl;
}
~Example()
{
cout << "Example()" << endl;
}
}
int main()
{
Example(); // 创建匿名对象
cout << "main()" << endl;
return 0;
};
输出:
Example()
~Example()
main()
当我们只是想调用一个类里的某个函数时,使用匿名对象会很方便
class Calculator
{
public:
// 使用初始化列表初始化成员变量
Calculator(int a, int b)
:_a(a)
,_b(b)
{}
int add()
{
return _a + _b;
}
private:
int _a;
int _b;
};
int main()
{
// 创建匿名对象并调用 add 方法
cout << Calculator(10, 20).add() << endl;
return 0;
}
相比匿名对象,有名对象因生命周期更长,在这种场景下反而会导致更高的资源消耗。
8. 结语
在本篇文章中,我们深入探讨了类和对象的高级特性,从初始化列表到匿名对象,每一个概念都揭示了 C++ 设计哲学的深度与精妙。希望这篇文章能为您的学习提供启发,感谢您的阅读,期待与您在下一篇继续探索 C++ 的精彩世界!
:深入特性与优化&spm=1001.2101.3001.5002&articleId=144520185&d=1&t=3&u=04fbc726cd2e4a9ba4fae4c9204e5828)
1646

被折叠的 条评论
为什么被折叠?



