文章目录
前言
继上篇探索C++中的类与对象:构建程序的基石(上)
探索C++中的类与对象:构建程序的基石(中)
在编程的世界里,C++以其强大的灵活性和高效性,在众多编程语言中占据了举足轻重的地位。它不仅继承了C语言的底层操作能力和高效执行速度,还引入了面向对象编程(OOP)的概念,极大地提升了代码的可维护性、可扩展性和重用性。其中,类和对象是C++面向对象编程的核心,它们为程序员提供了一种组织代码、模拟现实世界实体以及实现复杂数据结构的有效方式。
💫1. 再谈构造函数
1.1 函数体内赋值
在构造函数体内进行赋值,即对象的成员变量先通过默认构造函数创建,随后在构造函数体内被赋值。
class Date{ public: Date(int year, int month, int day){ _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
缺点:
- 效率较低:在构造函数体内赋值时,成员变量已经经历了一次默认初始化,之后再进行赋值。这会导致两步操作,特别是对于复杂类型对象,可能导致不必要的性能损耗。
- 无法处理某些成员类型:对于
const
成员、引用类型、以及没有默认构造函数的类成员,无法使用这种方式赋值,必须使用初始化列表。
1.2 初始化列表
初始化列表是在构造函数的声明后,紧跟着冒号
:
的一部分。它在对象创建时,直接调用成员变量的构造函数或对其进行初始化。class Date{ public: Date(int year, int month, int day) : _year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; };
【注意】
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
const
成员变量- 自定义类型成员(且该类没有默认构造函数时)
class A{ public: A(int a) :_a(a) {} private: int _a; }; class B{ public: // 初始化列表:对象的成员定义的位置 B(int a, int& ref) : _aobj(a) , _ref(ref) , _n(10) {} private: A _aobj; // 没有默认构造函数 // 特征:必须在定义的时候初始化 int& _ref; // 引用 const int _n; // const };
【注意】
在B类的初始化列表中,为什么使用
int& ref
而不是int ref
,原因是ref如果是局部变量,那么出了作用域就销毁了,此时_ref
就相当于野引用,所以这里应该是int&
。优点:
- 效率高:初始化列表直接在对象创建时初始化成员变量,避免了先默认构造再赋值的额外步骤。
- 强制初始化:某些类型(如
const
和引用
)必须通过初始化列表进行初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序
class A{ public: A(int a): _a1(a), _a2(_a1){} void Print(){ cout << _a1 << " " << _a2 << endl; } private: int _a2; int _a1; }; int main() { A aa(1); aa.Print(); return 0; } // A. 输出1 1 // B. 程序崩溃 // C. 编译不通过 // D. 输出1 随机值
💫2. Static
静态成员
2.1 静态成员变量
静态成员变量(也称为类变量)是指在面向对象编程中,属于类而不是某个特定对象的变量。它的特性是在类的所有实例之间共享,即无论创建了多少个对象,静态成员变量在内存中只有一个副本,所有实例对这个变量的修改都会反映在所有其他实例中。
- 属于类本身:静态成员变量是类级别的,不能通过对象直接定义,而是通过类定义。
- 共享性:所有对象共享同一个静态成员变量,修改这个变量时,所有的实例都会感知到修改的值。
- 生命周期长:静态成员变量随着类的加载而存在,类卸载时才会消失。
- 访问方式:可以通过类名直接访问,而不需要实例化对象
#include<iostream> using namespace std; class MyClass { public: static int staticVar; void display() { cout << "Static Variable: " << staticVar << endl; } }; int MyClass::staticVar = 10; // 初始化静态成员变量 int main() { MyClass obj1, obj2; obj1.display(); // 输出:Static Variable: 10 obj2.display(); // 输出:Static Variable: 10 obj1.staticVar = 20; // 修改静态变量 obj1.display(); // 输出:Static Variable: 20 obj2.display(); // 输出:Static Variable: 20 return 0; }
在这个例子中,
staticVar
是MyClass
的静态成员变量。即使obj1
和obj2
是不同的实例,但它们都共享同一个staticVar
变量。当obj1
修改了staticVar
的值,obj2
也会看到同样的变化。【注意】
- 静态成员变量的初始化必须在类定义外进行。
- 不能通过对象直接初始化静态成员变量。
2.2 静态成员函数
静态成员函数是与类相关联的函数,而不是与类的具体实例关联。它属于类本身,而不是类的某个对象。静态成员函数在使用时无需实例化对象,可以直接通过类名调用。不依赖对象:静态成员函数是类级别的函数,不依赖于类的具体对象。它可以在没有实例化类对象的情况下直接调用。
不能访问非静态成员变量和非静态成员函数:由于静态成员函数不依赖于对象,它不能直接访问类的非静态成员变量或非静态成员函数,因为这些成员变量和成员函数是依赖于具体对象的。
可以访问静态成员变量:静态成员函数可以访问静态成员变量,因为静态成员变量同样是类级别的,与对象无关。
常用于工具函数或与实例无关的逻辑:静态成员函数常用于执行与具体对象无关的任务,比如全局计数、工具函数等。
#include<iostream> using namespace std; class MyClass { public: static int staticVar; // 静态成员变量 // 静态成员函数 static void staticFunction() { cout << "Static Variable: " << staticVar << endl; } // 非静态成员函数 void nonStaticFunction() { cout << "Non-static function can access staticVar: " << staticVar << endl; } }; int MyClass::staticVar = 10; // 初始化静态成员变量 int main() { // 调用静态成员函数,不需要创建对象 MyClass::staticFunction(); // 输出:Static Variable: 10 // 修改静态成员变量的值 MyClass::staticVar = 20; MyClass::staticFunction(); // 输出:Static Variable: 20 // 创建对象,调用非静态成员函数 MyClass obj; obj.nonStaticFunction(); // 输出:Non-static function can access staticVar: 20 return 0; }
【解释】
- 静态成员变量
staticVar
:这是一个静态成员变量,属于整个类。无论创建多少个对象,它的值在所有对象间是共享的。- 静态成员函数
staticFunction
:可以通过类名MyClass::staticFunction()
调用,无需创建对象。它能访问静态成员变量staticVar
。- 非静态成员函数
nonStaticFunction
:它可以访问静态成员变量staticVar
,因为静态成员变量对整个类可见。【使用静态成员函数的场景】
- 与对象无关的操作:当函数的逻辑不依赖具体的对象时,可以使用静态成员函数,例如工具类中的数学计算方法。
- 访问或操作静态成员变量:静态成员函数常用于操作静态成员变量,例如维护类实例的全局计数等。
- 工厂模式:静态成员函数可以用于返回类的实例,如工厂模式中常用的“创建对象”的函数。
【注意事项】
- 静态成员函数无法直接调用非静态成员变量和非静态成员函数。如果需要访问,必须传递对象实例或者将非静态成员变量变为静态成员变量。
- 静态成员函数虽然不依赖于对象,但是它们同样遵守类的访问控制(如
private
、protected
)。
💫3. 友元
友元的本质: 友元打破了 C++ 封装的严格限制,使得指定的外部函数或类能够访问类的私有成员和保护成员。友元并不是类的成员,它是一种特殊的外部“访问权限声明”。
3.1 友元的类型:
- 友元函数:普通的函数可以通过在类内声明为友元,从而可以访问该类的私有和保护成员。
- 友元类:一个类可以将另一个类声明为友元,这样友元类的所有成员函数都能访问该类的私有和保护成员。
- 友元成员函数:某类的特定成员函数可以被声明为友元,只对该特定函数开放访问权限。
3.2 友元的应用场景:
- 操作符重载:特别是像
<<
和>>
这样的输入输出运算符重载,通常需要通过友元函数来访问类的私有数据。- 调试和日志:通过友元,某些调试类可以直接访问目标类的内部状态,用于日志记录或状态检查。
示例 - 操作符重载中的友元函数:
class Complex { private: double real, imag; public: Complex(double r, double i) : real(r), imag(i) {} // 声明友元函数 friend std::ostream& operator<<(std::ostream& out, const Complex& c); }; // 友元函数定义 std::ostream& operator<<(std::ostream& out, const Complex& c) { out << c.real << " + " << c.imag << "i"; return out; }
在这个例子中,
<<
运算符被声明为Complex
类的友元函数,从而能够访问Complex
的私有成员real
和imag
。
💫4. 内部类
内部类的本质: 内部类是一个类的成员,存在于另一个类的定义内部。它与外部类存在某种逻辑关系,但不会自动继承访问权限。通常,内部类用于表示外部类的一个组成部分,封装复杂的数据结构或功能。
4.1 内部类的访问规则:
- 内部类和外部类之间的访问权限是独立的,除非明确声明为友元。
- 外部类不能直接访问内部类的私有成员,反之亦然。
- 内部类可以访问外部类的公有和保护成员。
4.2 内部类的应用场景:
- 实现细节封装:内部类经常用来封装外部类的实现细节,隐藏复杂的内部逻辑。
- 模块化设计:内部类可以用于实现更清晰的模块化设计,将一个大类拆分成多个小的内部类来管理不同的功能。
示例 - 内部类与外部类的交互:
class Outer { private: int outerValue; public: Outer(int val) : outerValue(val) {} // 内部类声明 class Inner { public: void displayOuter(Outer& o) { std::cout << "Outer class value: " << o.outerValue << std::endl; // 访问外部类的私有成员 } }; };
在这个例子中,
Inner
类是Outer
类的内部类,Inner
类的displayOuter
函数可以访问Outer
类的私有成员。
💫5. 匿名对象
匿名对象的本质: 匿名对象是未被命名的对象,它通常是在表达式中临时生成的,生命周期极短。匿名对象常见于临时对象的创建和函数返回值中。匿名对象的好处是避免了不必要的命名和生命周期管理,简化了代码逻辑。
5.1 匿名对象的特点:
- 自动销毁:匿名对象在使用完后立即销毁,不占用额外的资源。
- 适用于短期操作:非常适合在函数调用中返回临时对象,避免了拷贝和对象管理的复杂性。
5.2 匿名对象的应用场景:
- 临时计算结果:某些场景下,使用匿名对象来计算临时结果非常常见。
- 返回值优化:在函数返回值时,匿名对象与返回值优化(RVO)结合,能有效减少拷贝。
示例 - 匿名对象作为返回值:
class Example { public: Example() { std::cout << "Constructor called!" << std::endl; } ~Example() { std::cout << "Destructor called!" << std::endl; } }; Example createExample() { return Example(); // 返回匿名对象 } int main() { Example e = createExample(); // 用匿名对象初始化 e }
在这里,
Example()
是匿名对象,它的生命周期仅限于函数调用,它的构造和析构顺序也表明了其生命周期的短暂性。
💫6. 拷贝对象时的一些编译器优化
编译器在处理对象拷贝时,会进行一些常见的优化以提高性能。以下是几种主要的优化技术:
6.1 返回值优化(
RVO
)
RVO
是一种编译器优化,它避免了在函数返回时临时对象的拷贝构造。编译器在函数返回时直接在目标位置创建对象,消除了拷贝的开销。示例:
class A { public: A() { std::cout << "Constructor" << std::endl; } A(const A&) { std::cout << "Copy Constructor" << std::endl; } }; A createA() { return A(); // RVO,避免拷贝 } int main() { A a = createA(); // RVO 使得没有调用拷贝构造函数 }
6.2 移动语义
C++11 引入了移动语义,通过移动构造函数和移动赋值运算符,能够有效避免深拷贝的开销。移动语义将对象资源的所有权转移,而不是进行拷贝。
示例:
class B { public: B() { std::cout << "Constructor" << std::endl; } B(B&&) { std::cout << "Move Constructor" << std::endl; } }; B createB() { return B(); // 移动构造 }
此例中,移动语义会避免不必要的深拷贝,大大提升性能。
6.3 拷贝省略
在某些情况下,C++ 标准允许编译器跳过某些不必要的拷贝操作,比如在函数返回时,编译器直接在调用者的上下文中构造返回对象,避免了临时对象的创建和拷贝。
💫7. 再次理解封装
封装的本质: 封装是面向对象编程(
OOP
)的核心原则之一。它通过将对象的状态(数据)和行为(方法)封装在类中,限制外部对类内部实现的直接访问。封装的目的是保护对象的完整性,并通过控制访问权限实现信息隐藏。7.1 封装的三种访问控制:
- Public(公有):外部可以自由访问,表示开放给外部的接口。
- Private(私有):外部无法访问,只有类的内部成员函数可以访问。
- Protected(保护):子类可以访问,但外部类无法访问。
7.2 封装的优势:
- 数据安全性:通过私有和保护成员变量,封装可以保护数据的完整性,避免外部直接修改数据,确保程序的稳定性和安全性。
- 灵活性:通过封装,内部实现可以随时更改,而不影响外部代码,因为外部只能通过公开接口与对象交互。
- 降低耦合:封装可以减少类之间的依赖和耦合,提高代码的可维护性和可扩展性。
示例 - 封装的好处:
class BankAccount { private: double balance; // 私有数据成员,外部无法直接访问 public: BankAccount(double initBalance) : balance(initBalance) {} void deposit(double amount) { if (amount > 0) { balance += amount; } } void withdraw(double amount) { if (amount > 0 && amount <= balance) { balance -= amount; } } double getBalance() const { return balance; } };
通过封装,
balance
变量不会被外部代码直接修改,外部只能通过deposit
和withdraw
函数来
结语
通过本文的学习,相信你已经对C++中的类和对象有了全面而深入的理解。类和对象不仅是C++面向对象编程的基础,更是现代软件开发不可或缺的工具。它们教会我们如何以更加抽象和模块化的方式思考问题,将复杂的系统分解成简单、可管理的部分。掌握这一技能,你将能够设计出更加灵活、健壮的软件系统,应对日益复杂的编程挑战。
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!