C++ 面向对象编程的三大特性之一:封装
目录
一:什么是封装?
封装是面向对象编程(OOP)中的一个重要概念,它指的是将数据和操作数据的方法(函数)封装在一个单元中,以便对外隐藏实现的细节,只暴露必要的接口。
在C++中,封装通过类来实现,类将成员属性和成员函数组合在一起,形成一个新的完成的数据类型。
封装的目的是提供更好的抽象、隔离和代码组织,从而增强代码的可维护性、可扩展性和安全性。
二:封装的优势:
1. 信息隐藏和保护: 允许将类的内部数据隐藏起来,只暴露必要的接口给外部。这样可以防止外部直接访问和修改内部数据,从而保护数据的完整性和安全性。
2. 代码隔离和模块化: 将对象的数据和行为封装在一起,形成独立的模块。这种模块化的设计使得代码更具可维护性和可扩展性,因为修改一个模块不会影响到其他模块。
3. 代码复用: 促进了代码的复用。一个良好封装的类可以在不同的上下文中重复使用,从而减少了重复编写代码的工作量。
4. 降低耦合性: 可以降低代码之间的耦合度。 类的内部细节隐藏起来,不同的类之间可以通过定义良好的接口进行通信,从而降低了代码之间的依赖性,使得系统更加灵活。
5. 简化接口: 允许将复杂的内部实现细节隐藏起来,向外部提供更简洁的接口。 这样使用类的其他部分的开发人员不需要了解类的全部实现细节,只需要知道如何使用接口即可。
6. 版本管理和演化: 可以让类的实现细节在不影响接口的情况下进行修改和优化,从而使得软件系统的演化更加容易,不会影响到已经使用该类的其他部分。
三:封装的基本实现:
1. 封装的基本语法:
class 类名{访问权限: 成员属性/成员函数};
代码示例:
class Person // Person类
{
public: // 访问权限 公共权限
int m_Age; //成员属性
Person(int age) :m_Age(age) {}; // 有参构造函数
};
2. 三种访问权限:
- public(公有) :在类的外部和内部都可以访问公有成员。公有成员可以被任何函数、类或对象访问。
- protected(保护) :在类的外部无法直接访问保护成员,但在派生类中可以访问。保护成员不能被外部函数或一般对象访问,只能被派生类中的成员函数访问。主要体现在继承关系:派生类(子类)可以访问基类(父类)的该权限内容。
- private(私有):私有成员只能被定义该成员的类中的函数访问,外部函数和派生类都无法直接访问私有成员。私有成员用于隐藏类的实现细节,实现封装性。
代码示例:
class MyClass {
public: // 公有成员
int publicVar; // 类内可以访问,类外也可以访问
protected: // 保护成员
int protectedVar; // 类内可以访问,类外不可以访问,派生类可以访问
private: // 私有成员
int privateVar; // 类内可以访问,类外不可以访问,派生类也无法访问
};
class son :public MyClass {
void print() {
cout << protectedVar << endl; //基类的 protected权限可以访问
//cout << privateVar << endl; 基类的 private权限不可访问
}
};
关于继承的更多内容:【C++】 面向对象编程的三大特性之一:继承
3. struct 和 class 的区别:
不同之处:
struct 默认成员访问权限为public。
class 默认成员访问权限为private。
相同之处:
继承方式,成员函数、成员变量、构造函数、析构函数、友元函数等的定义和使用在struct和class中是相同的。
代码示例:
// 使用 struct 定义类
struct MyStruct {
int publicVar; // 默认 public
void PublicMethod() {
// ...
}
};
// 使用 class 定义类
class MyClass {
int privateVar; // 默认 private
void PrivateMethod() {
// ...
}
public:
void PublicMethod() {
// ...
}
};
int main() {
MyStruct s;
s.publicVar = 10;
s.PublicMethod();
MyClass c;
c.PublicMethod(); // 可以访问
// c.PrivateMethod(); // 无法访问
return 0;
}
4. 访问器(get)和修改器(set):
访问器和修改器即是成员属性私有化(private),然后通过公有权限(public),提供只读(get)和只写(set)的权限来控制成员属性的输入和输出。
主要有以下作用:
- 可以控制成员属性的读写权限。
- 对于写可以检测出数据的有效性。
代码示例:
class Person {
private: // 将成员属性私有化
string m_Name;
int m_Age;
public:
Person(string name,int age) :m_Name(name), m_Age(age) {};
void GetName() // 给出相应的接口来实现读写权限的操作
{
cout << "姓名:" << m_Name << endl;
}
void SetName(string name) {
m_Name = name;
}
//只读
void GetAge()
{
cout << "年龄:" << m_Age << endl;
}
//只写
void SetAge(int age) {
if(age<0 || age>140){ // 可以对输入的年龄进行检测判断是否合理
cout << "输入不合理!" << endl;
return;
}
m_Age = age;
}
};
5. 构造函数:
(1)基本概念:
- 主要作用在于创建对象时,为对象的成员属性赋值,编译器自动调用,用于初始化操作;
- 可以有参数,因此可以支持重载;
- 函数名与类名相同:类名( ){ };
- 没有返回值,不写void,只会调用一次;
- 如果没有显式定义任何构造函数时,编译器将会自动生成的默认构造函数(空实现)。
代码示例:
class Person {
private:
string name;
int age;
public:
// 若没有写任何默认构造函数,将会是编译器提供的空实现: Person(){};
// 默认构造函数 -- 构造函数用于初始化操作
Person() {
name = "Unknown";
age = 0;
}
// 带参数的构造函数 参数重载
Person(const string& n, int a) {
name = n;
age = a;
}
// 默认拷贝构造函数
Person(const Person& p1) // 默认是值传递
{
this->age = p1.age;
this->name = p1.name;
}
// 打印信息
void display() { // 提供接口,实例化对象后可以调用该接口来打印信息
cout << "Name: " << name << ", Age: " << age << endl;
}
};
int main() {
// 使用默认构造函数创建对象
Person person1;
person1.display();
// 使用带参数的构造函数创建对象
Person person2("Alice", 25);
person2.display();
return 0;
}
关于深拷贝和浅拷贝的问题,请看本文的析构函数内容中的解析和解决办法
(2)调用方式:
括号法(常用):
// 使用默认构造函数创建对象
Person person1; // 默认构造函数调用
person1.display();
// 使用带参数的构造函数创建对象
Person person2("Alice", 25); // 有参构造函数调用
person2.display();
Person person3(p2); // 默认拷贝构造函数调用
person3.display();
显示法:
Person person1; // 默认构造函数调用
person1.display();
Person person2 = Person("Alice", 25); // 有参构造函数调用
person2.display();
Person person3 = Person(p2); // 默认拷贝构造函数调用
person3.display();
隐式转换法:
Person person2 = {"Alice", 25}; // 有参构造函数调用
person2.display();
Person person3 = p2; // 默认拷贝构造函数调用
person3.display();
(3)调用规则:
默认构造函数 -> 有参构造函数 -> 拷贝构造函数
如果用户定义了有参构造函数,则C++不再提供默认无参构造,但是会提供默认拷贝构造。
如果用户定义了拷贝构造函数,C++就不会再提供其他构造函数。
(4)explicit 关键字:
explicit 关键字主要用于修饰类的任何参数构造函数,特别是单参数构造函数。它的作用是防止隐式类型转换,即防止编译器在需要进行类型转换的地方自动调用构造函数来完成转换,从而提高代码的安全性和可读性。
代码示例:
class MyClass {
public:
explicit MyClass(int value) {
// 构造函数实现
}
};
MyClass obj = 10; // 这将会报错,不能隐式地将 int 转换为 MyClass
MyClass obj2(20); // 这是合法的,因为是显式调用构造函数
(5)初始化列表:
C++ 中用于在构造函数中对成员变量进行初始化的一种特殊语法。它出现在构造函数的函数体之前,用冒号 : 开头,然后在冒号后面按成员变量的顺序列出成员变量的初始化表达式。
它可以直接在对象构造时对成员变量进行初始化,而不是先调用默认构造函数然后再通过赋值语句赋值。
代码示例:
class MyClass {
private:
int m_Age;
string m_Name;
public:
explicit MyClass(int age,string name):m_Age(age),m_Name(name) { // 初始化列表
// 构造函数实现
}
};
以下情况下特别有用:
- 对于成员变量是 const 或引用类型的情况,因为它们必须在构造函数开始时初始化,而不能在构造函数体内进行赋值操作。
class Car {
private:
const int maxSpeed;
Engine& engine;
public:
Car(int speed, Engine& e) : maxSpeed(speed), engine(e) {
// 在构造函数初始化列表中初始化 const 成员变量和引用成员变量
}
};
- 对于成员变量是类对象或自定义类型的情况,可以避免不必要的临时对象的创建和销毁,提高效率。
class Address {
private:
string city;
public:
Address(const std::string& c) : city(c) {}
void Display() const {
cout << city << endl;
}
};
class Person {
private:
string name;
int age;
Address address;
public:
// 使用初始化列表初始化成员变量
Person(const string& n, int a, const Address& addr) : name(n), age(a), address(addr) {}
void DisplayInfo() const {
cout << "Name: " << name << ", Age: " << age << endl;
cout << "Address: ";
address.Display();
}
};
int main() {
Address addr("City");
Person person("Alice", 25, addr);
person.DisplayInfo();
return 0;
}
6. 析构函数:
(1)基本概念:
- 主要作用在于对象销毁前系统自动调用,执行一些清理工作 (堆区释放);
- 析构函数不可以有参数,因此不能重载;
- 函数名与类名相同,在名称前加:类名( ){ };
- 没有返回值,不写void,只会调用一次;
代码示例:使用Person类来进行构造和析构操作
class Person {
public:
// 默认构造函数(空实现)
Person() {
cout << "默认构造函数" << endl;
}
// 默认析构函数(空实现),主要是对堆区进行释放操作
~Person(){
cout << "默认析构函数" << endl;
}
}
(2)析构函数通常执行的任务:
1. 释放堆内存:
如果在对象的生命周期内分配了堆内存(使用 new 操作符),析构函数应该在对象销毁时释放这些堆内存,以防止内存泄漏。
代码示例:使用一个类来动态分配内存,并在析构函数中释放这些内存
class DynamicMemory {
private:
int* data; // 指针(堆区数据)
public:
DynamicMemory(int size) { // 构造函数
data = new int[size];
}
~DynamicMemory() { // 析构函数
delete[] data; // 释放堆区数据
}
};
int main() {
DynamicMemory dynamicArray(5);
return 0;
}
在这个示例中,DynamicMemory类在构造函数中动态分配一个整数数组的内存,在析构函数中使用delete[]释放这块内存。这确保了dynamicArray对象超出范围并且销毁时,分配的内存会被释放,防止内存泄漏。
2. 关闭文件和网络连接:
如果对象在构造函数中打开了文件、网络连接或其他资源,析构函数应该关闭这些资源,以确保资源的正确释放。
代码示例:使用一个类来打开文件并在析构函数中关闭文件
#include <iostream>
#include <fstream>
using namespace std;
class FileHandler {
private:
ofstream file;
public:
FileHandler(const string& filename) {
file.open(filename);
if (!file.is_open()) {
// cerr 用于输出错误信息的输出流对象,通常用于向标准错误流输出内容。
cerr << "Failed to open file: " << filename << endl;
}
}
~FileHandler() {
if (file.is_open()) {
file.close();
}
}
void write(const string& content) {
if (file.is_open()) {
file << content;
}
}
};
int main() {
FileHandler file("example.txt");
file.write("Hello, world!");
return 0;
}
FileHandler类在构造函数中打开文件,在析构函数中关闭文件。当FileHandler对象超出范围时(例如在main函数末尾 FileHandler对象超出了其作用域),析构函数会被调用,从而确保文件被正确关闭,即使在发生异常的情况下也能正常关闭文件。
3. 清理临时数据(记录日志):
如果对象在其生命周期内产生了临时数据或状态,析构函数应该清理这些数据,以确保对象被销毁后不会留下任何不需要的状态。
记录一些状态信息或日志,有助于调试和跟踪对象的生命周期。
代码示例:模拟的日志记录类,它在对象析构时将临时保存的日志内容写入日志文件中。
class Logger {
private:
ofstream logFile;
string temporaryLog;
public:
Logger(const string& filename) : logFile(filename) {
if (!logFile) {
cerr << "Error opening log file" << endl;
}
}
void log(const std::string& message) {
temporaryLog += message + "\n";
}
~Logger() {
// Write temporary log to the log file
if (logFile) {
logFile << "Temporary Log:\n" << temporaryLog << "\n";
logFile.close();
cout << "Log file closed" << endl;
}
}
};
int main() {
Logger logger("log.txt");
logger.log("Log entry 1");
logger.log("Log entry 2");
// Logger object goes out of scope, destructor is called
return 0;
}
Logger 类在构造函数中打开一个日志文件并创建一个临时日志字符串。通过 log 函数,我们可以将要记录的日志信息添加到临时日志中。在 Logger 对象被销毁时(即作用域结束时),析构函数会将临时日志内容写入日志文件中,然后关闭文件。
4. 解除绑定:
如果对象与其他对象或资源有关联,析构函数可能需要解除这些关联,以避免悬挂指针或引用。
代码示例:使用智能指针,它在对象超出范围时会自动解除绑定并释放资源
#include <iostream>
#include <memory>
class Resource {
public:
Resource() {
cout << "Resource acquired" << endl;
}
~Resource() {
cout << "Resource released" << endl;
}
};
int main() {
shared_ptr<Resource> resourcePtr = make_shared<Resource>();
cout << "Exiting the program" << endl;
return 0;
}
shared_ptr 是一个智能指针,它在对象超出范围时自动调用析构函数,从而释放资源并解除绑定。这种方式更加安全和方便,可以有效地管理资源和防止内存泄漏。
(3)深拷贝和浅拷贝问题的解决:
如果一个类的成员属性包含了指向堆区的数据(例如动态分配的内存),默认拷贝构造函数会简单地复制指针,而不是复制指针所指向的实际数据。这可能会导致多个对象共享同一块堆内存,从而在析构时可能造成重复释放和悬挂指针等问题。
代码示例:
#include<iostream>
using namespace std;
class Person {
private:
int* m_Salary;
int m_Age;
public:
Person() {}
Person(int age,int salary){
cout << "构造函数" << endl;
this->m_Age = age;
m_Salary = new int(salary);
}
Person(const Person& p) { // 默认拷贝构造函数,浅拷贝
m_Salary = p.m_Salary;
m_Age = p.m_Age;
}
void display(){
cout << "年龄:" << m_Age << " 薪水:" << *m_Salary << "w" << endl;
}
~Person(){
cout << "析构函数" << endl;
if (m_Salary != NULL){
delete m_Salary;
m_Salary = NULL;
}
}
};
int main(){
Person p1(25,20);
p1.display();
Person p2(p1);
p2.display();
}
以上代码会出现异常,因为默认拷贝函数是值传递,所以Person p2(p1);会出现 p1 和 p2 的m_Salary 指针都是指向同一地址,进行析构函数进行释放堆区内存时,会出现重复释放该内存的操作,因此出现异常。
解决办法,重写拷贝函数,重新申请一块内存来记录数据,然后新对象指针指向新内存的地址。
Person(const Person& p) {
//m_Salary = p.m_Salary;
m_Age = p.m_Age;
m_Salary = new int(*p.m_Salary);
//重新申请一块内存,指向新内存,析构函数释放时就不会出现释放同一块内存空间了
}
四:总结:
以上就是C++ 面向对象编程的三大特性之一封装的全部内容了,本文主要阐述了封装的用法以及构造函数和析构函数的主要使用途径和注意事项。关于类和对象的其他内容:友元friend,运算符重载等等,将会在其他文章中详细解析,敬请期待!
希望大家多多支持我,码字不易,若有错误请指出,祝大家阅读愉快!
欢迎关注🔎点赞👍收藏⭐️留言📝