CPP封装

封装

一、封装

封装的核心思想是隐藏对象的内部实现细节,对外提供公共的接口(即公共成员函数),限制对内部数据的直接访问。

从类的角度来看,封装就是把对象的全部属性和操作结合起来,形成一个整体。并且,隐藏具体的实现细节,只暴露必要的接口给外部使用。

封装最大好处在于提升了代码的内聚性,提高代码的安全性,减少耦合度,从而提高代码的可复用性和可维护性。

cpp 复制代码
class Person {
public:
    int age;
    std::string name;
    //打印学生信息
    void print(){
        std:::cout << "age:" << age << "name:" << name << std:endl;
    } 
}

int main(){
    Person p;
    p.age = -20; //public属性,类外可以直接访问
    p.name = "张三";
    p.print();
           
    return 0;
}
1、封装的实现

封装主要通过以下机制实现的。

(1)访问修饰符(Access Modifiers)

C++中实现封装,需要通过权限访问修饰符publicprivateprotected关键字来说明类中的成员是公有的、私有的,还是受保护的。

  • public(公有的):类外可以访问;
  • private(私有的):类外不可访问,继承类不可访问;
  • protected(受保护的):类外不可访问,继承类可访问。

继承类(子类) 是指 从父类 (基类) 继承成员 的类,继承类可以访问父类的公有(public)和受保护(protected)成员。

(2)公共方法 (public Methods)

给私有成员变量提供公共访问方法,比如:getter() 和 setter()

cpp 复制代码
class Person {
private: // 私有数据成员
    int age;
    std::string name;
   
public: // 公共接口方法
    void setName(const std::string& newName) {
        name = newName;
    }
    void setAge(int newAge) {
        if(newAge >= 0) { // 简单的验证
            age = newAge;
        } else {
            std::cout << "Invalid age value." << std::endl;
        }
    }
    std::string getName() const {
        return name;
    }
    int getAge() const {
        return age;
    }
};

类中name、age被定义为私有成员变量,只能通过公有成员函数(set和get函数)来进行操作和访问。这种封装的方式可以保证在外部代码中无法直接访问和修改私有成员,从而提高代码的安全性。

2、访问类成员

通过公有成员函数来访问私有成员。

cpp 复制代码
int main() {
    Person s;
    s.setName("张三");
    s.setAge(18);
    cout << s.getName() << endl;
    cout << s.getAge() << endl;
    return 0;
}

二、this指针

this 指针 是 C++ 中 隐含的指针 ,指向当前对象的地址。它的作用是在类的成员函数中,标识 当前调用该成员函数的对象,以便你可以访问当前对象的属性和方法。

cpp 复制代码
class Person {
public:
    string name;

    void setName(string name) {
        this->name = name;  // 使用 this 指针访问当前对象的成员变量
    }

    void printName() {
        cout << "Name: " << this->name << endl;
    }
};

int main() {
    Person p1;  // 创建第一个对象
    p1.setName("Alice");  // 调用 setName 设置 p1 的 name 为 "Alice"
    p1.printName();  // 输出 p1 的 name

    Person p2;  // 创建第二个对象
    p2.setName("Bob");  // 调用 setName 设置 p2 的 name 为 "Bob"
    p2.printName();  // 输出 p2 的 name

    return 0;
}

在这个例子中:

  • p1p2 是两个不同的 Person 对象。
  • 当你调用 p1.setName("Alice"); 时,this 指针指向 p1 ,也就是当前调用 setName 的对象。
  • 当你调用 p2.setName("Bob"); 时,this 指针指向 p2 ,也就是当前调用 setName 的对象。

this指针有几个关键的用途和特点:

1、访问成员变量

this指针可以用来访问当前对象的成员变量和调用成员函数。

cpp 复制代码
class Student
{
public:
    string name;
    void methodA(){
        this->methodB();
    }
    void methodB() {}
    void showName()
    {
        cout << this->name << endl;
    }
};

在每个成员函数(非静态成员函数)中都隐含包含一个this指针作为函数参数; 它是指向被调用对象的指针,并在函数调用时将对象自身的地址隐含作为实际参数传递。

例如,以showName()成员函数为例,编译器将其定义为:

cpp 复制代码
void showName(Student* this) //隐含添加this指针
{
     cout << this->name << endl;
}

在对象调用成员函数时,传递对象的地址到成员函数中。例如:

cpp 复制代码
int main()
{
    Student s1;
    s1.showName();
    Student s2;
    s2.showName();
}

编译器将其解释为:

cpp 复制代码
int main()
{
    Student s1;
    s1.showName(&s1);
    Student s2;
    s2.showName(&s2);
}
2、解决参数和成员变量同名冲突
cpp 复制代码
class Student
{
public:
    Student(string name)
    {
        this->name = name;
    }
private:
    string name;
}
3、作为返回值

this 还可以用于支持链式编程。通过 this 返回当前对象本身,使得多个成员函数的调用可以连在一起。例如:

cpp 复制代码
class Student {
public:
    string name;
    int age;

    Student* setName(string name) {
        this->name = name;
        return this;  // 返回当前对象的指针,支持链式调用
    }

    Student* setAge(int age) {
        this->age = age;
        return this;  // 返回当前对象的指针,支持链式调用
    }
};

int main() {
    Student student;
    student.setName("Tom")->setAge(20);  // 链式调用
}

三、静态成员

静态类成员使用static关键字修饰,静态成员分为静态成员变量,静态成员函数。

1、静态成员变量

类的静态成员变量与函数的静态变量类似,静态成员变量的生命周期与整个程序的生命周期相同。一旦程序开始运行,静态成员变量就会被创建并分配内存,直到程序结束运行时,静态成员变量才会被销毁并释放内存。

(1)静态成员变量必须在类内声明、类外初始化,定义变量前要加static关键字

MyClass.h

h 复制代码
#pragma once
class MyClass
{
public:
    static int val;
};

MyClass.cpp

cpp 复制代码
#include "MyClass.h"
int MyClass::val = 200;

(2)静态成员变量可以通过类名直接访问,也可以通过对象名访问 ,但是推荐使用类名直接访问

cpp 复制代码
#include <iostream>
#include "MyClass.h"
using namespace std;
int main()
{
    //通过类名直接访问
    cout << MyClass::val << endl;
}

(3)静态成员变量被类的所有实例共享

静态成员变量属于类级别的变量,不属于任何一个对象,被类的所有实例共享。无论创建多少个类的实例,都共享一个静态成员变量。

cpp 复制代码
int main()
{
    MyClass m1;
    MyClass m2;
    m1.val = 10;
    cout << m2.val << endl;
    m2.val = 3;
    cout << m1.val << endl;
}

内存:静态成员变量在内存中只有一份,不会随着每个对象的创建而复制。它存储在静态数据区。

共享:所有类的实例共享同一个静态成员变量,任何一个对象对静态变量的修改都会影响其他对象。

访问:静态成员变量可以通过类名或者对象访问。类名访问是直接访问类级别的数据,通常更清晰明了。

2、静态成员函数

(1)静态成员函数与静态成员变量一样,可以通过类名直接访问。也可以通过对象名访问

在定义静态成员函数时,如果函数的实现代码处于类体之外,则在函数的实现部分不能再标识static关键字。

cpp 复制代码
#pragma once
class MyClass
{
public:
    static void func();
}
cpp 复制代码
#include "MyClass.h"
#include <iostream>
using namespace std;
void MyClass::func()
{
    cout << "static func" << endl;
}
cpp 复制代码
#include "MyClass.h"
int main() {
    
    MyClass::func();
}

(2)静态成员函数中只能访问静态成员变量

由于静态成员是在程序开始执行就分配内存,并初始化;此时类的对象还没有实例化出来,因此不能访问类中的成员变量,成员变量是属于对象的,在对象创建的过程中才分配内存并初始化。

cpp 复制代码
// MyClass.h
#pragma once
class MyClass
{
public:
    static void func();  // 静态成员函数声明
    static int val;      // 静态成员变量声明
};
cpp 复制代码
// MyClass.cpp
#include "MyClass.h"
#include <iostream>
using namespace std;

int MyClass::val = 100;  // 静态成员变量初始化

void MyClass::func() {
    cout << "static func" << endl;
    cout << "静态成员变量:" << val << endl;  // 访问静态成员变量
}
cpp 复制代码
// main.cpp
#include "MyClass.h"

int main() {
    MyClass::func();  // 直接通过类名调用静态成员函数
}

首先,理解为什么 静态成员函数 只能访问 静态成员变量 ,关键点在于静态成员和非静态成员之间的 生命周期和内存分配 区别。

  • 静态成员函数 属于 类级别的函数 ,它在内存中只存在 一份 ,并且和 类的实例无关 。也就是说,静态成员函数可以在没有创建任何对象的情况下调用。因此,它不依赖于任何具体的对象实例,也没有 this 指针来指向当前对象。
  • 静态成员变量 也属于 类级别的变量,它在程序启动时分配内存,并且属于类的所有实例共享(即静态变量只有一份内存)。
  • 非静态成员变量 属于 对象实例,每个对象都有一份独立的副本。

总结:因为静态成员函数没有 this 指针 ,而非静态成员变量需要通过对象实例(this)来访问。

四、const修饰成员函数

1、常函数

成员函数后加const修饰后,称之为常函数。常函数不能修改非静态成员属性

✅ 作用:
  • const修饰的是函数的 this指针
  • 默认:MyClass* const this
  • 常函数:const MyClass* const this → 变成了 指向常对象的指针
✅ 限制:
  • 因为 thisconst MyClass*,所以不能通过它修改非 mutable 的成员变量。
✅ 特殊说明:
  • 常函数可以读成员变量
  • 常函数 不能修改非静态成员变量
  • 可以修改 mutable 修饰的成员变量
  • 可以访问静态成员变量(因为静态成员不依赖对象,不需要this指针)
cpp 复制代码
#pragma once
class MyClass
{
private:
    int num  = 100;
public:
    void show() const; //常函数

};
cpp 复制代码
#include "MyClass.h"
#include <iostream>
using namespace std;
void MyClass::show() const
{
    cout << "num:" << num << endl;
    //num = 1;// 常函数不可以修改成员属性
}

int main()
{
    MyClass myClass;
    myClass.show();
}

如果要修改,需要在成员属性声明时加mutable关键字。

cpp 复制代码
#pragma once
class MyClass
{
private:
    mutable int num  = 100;
    
public:
    void show() const;

};
cpp 复制代码
#include "MyClass.h"
#include <iostream>
using namespace std;
void MyClass::show() const
{
    num = 1;
    cout << "num:" << num << endl;
}
int main()
{
    MyClass myClass;
    myClass.show();
}
2、常对象

声明对象前加const关键字,则该对象称为常对象

  • 通常只会调常函数,但也能调静态函数
  • 可以修改静态属性,能调普通成员但不能修改其值,除非用mutable修饰
✅ 限制:
  • 常对象只能调用 常成员函数
  • 不能调用非 const 的成员函数(因为它们可能修改对象状态)
  • 不能修改普通成员变量
  • 可以访问或修改 mutable 成员变量
  • 可以访问/修改静态成员变量
cpp 复制代码
#pragma once
class MyClass
{
    
private:
    mutable int num  = 100;
    
public:
    void show() const;
    void modify(); 

};
cpp 复制代码
#include "MyClass.h"
#include <iostream>
using namespace std;
void MyClass::show() const
{
    num = 1;
    cout << "num:" << num << endl;
}
void MyClass::modify()
{
}
int main()
{
    const MyClass myClass;
    myClass.show();//常对象只能调用常函数
    //myClass.modify();
}

3、静态成员与常函数/常对象的关系

✅ 说明:
  • 静态成员属于类,不属于对象
  • 所以不受 const 限制,常对象也可以访问和修改它
cpp 复制代码
class MyClass {
public:
    mutable int count = 0;
    static int sharedCount;

    void modify() {
        count++;
    }

    void access() const {
        count++; // OK,因为是mutable
        // sharedCount++; // OK,静态成员不受常函数限制
    }
};

int MyClass::sharedCount = 0;

int main() {
    const MyClass obj;
    obj.access(); // OK:常对象只能调用常函数
    // obj.modify(); // ❌ 错误:不能调用非常函数
}

五、友元

友元是C++中一种特殊的访问权限控制机制,它允许一个类、函数或函数模板绕过常规的访问限制,直接访问另一个类的私有(private)和受保护(protected)成员。

友元关系打破了面向对象编程中的封装原则,但有时出于特定的设计需求,这种灵活性是有必要的。

1、友元函数
定义:

友元函数 是一个定义在类外部的函数,但它可以访问该类的 私有受保护 成员。尽管它是类外部的非成员函数,友元关系使得它可以绕过封装性限制,直接访问类的私有和受保护数据。

使用方法:
  • 在类内声明该函数为友元函数:使用 friend 关键字。
  • 友元函数 不属于类,它是类外部的函数。

友元函数使用friend关键字声明:

MyClass.h

cpp 复制代码
// MyClass.h
#pragma once
class MyClass
{
    friend void print(MyClass& myClass);  // 声明 print 为友元函数
public:
    MyClass(int data);
private:
    int data;  // 私有成员变量
};

// MyClass.cpp
#include "MyClass.h"
#include <iostream>
using namespace std;

MyClass::MyClass(int data): data(data) {}

void print(MyClass& myClass) {
    cout << "访问私有成员:" << myClass.data << endl;  // 友元函数可以访问私有成员
}

// main.cpp
#include "MyClass.h"

int main() {
    MyClass myClass(100);  // 创建对象
    print(myClass);  // 调用友元函数,访问私有成员
    return 0;
}

解释:

  • print()MyClass 类的友元函数,尽管它不是 MyClass 类的成员,但它可以直接访问 MyClass私有成员data)。
  • 友元函数的声明在 MyClass 类内部,使用 friend 关键字
2、友元类

友元类(Friend Class): 如果类A声明类B为友元,那么类B可以直接访问类A的私有和受保护成员。友元关系是单向的,即类A声明类B为友元并不意味着类B自动成为类A的友元

友元类使用friend class 来声明:

cpp 复制代码
// MyClassA.h
#pragma once
#include "ClassB.h"

class MyClassA
{
public:
    friend class MyClassB;  // 类 B 是类 A 的友元类
    MyClassA(int data);
private:
    int data;  // 私有成员变量
};

// MyClassA.cpp
#include "MyClassA.h"

MyClassA::MyClassA(int data):data(data) {}

// MyClassB.h
#pragma once
#include "MyClassA.h"

class MyClassB
{
public:
    void print(MyClassA& classA);  // 成员函数可以访问 MyClassA 的私有成员
};

// MyClassB.cpp
#include "MyClassB.h"
#include <iostream>
using namespace std;

void MyClassB::print(MyClassA& classA) {
    cout << classA.data << endl;  // 友元类 MyClassB 可以访问 MyClassA 的私有成员
}

// main.cpp
#include <iostream>
#include "MyClassB.h"

int main() {
    MyClassA classA(100);  // 创建 MyClassA 对象
    MyClassB classB;       // 创建 MyClassB 对象
    classB.print(classA);  // 调用 MyClassB 的 print(),访问 MyClassA 的私有成员
    return 0;
}

六、C++内存分配

1、C++内存区域
  • C++ 程序的内存分为几个区域,具体如下:
    • 代码区(Code Area)
      • 存放程序的 机器代码(即编译后的指令),由系统自动管理。
      • 该区域是只读的,防止程序篡改自己的代码。
    • 数据区(Data Area)
      • 已初始化的全局变量和静态变量。这些变量在程序启动时被初始化并分配内存。
      • 例如:static int staticGlobal = 200;int global = 100;
    • BSS 区(Block Started by Symbol)
      • 存放 未初始化的全局变量和静态变量。这些变量的值默认为零。
      • 例如:int temp;static int staticTemp;
    • 堆区(Heap Area)
      • 用于 动态内存分配 ,由开发者通过 newmalloc 等操作符分配,通过 deletefree 释放。
      • 堆内存的生命周期由程序控制,可以动态分配任意大小的内存空间。
    • 栈区(Stack Area)
      • 存放 函数的局部变量函数参数返回数据等,由编译器自动管理。
      • 每次函数调用时,栈会自动分配内存,函数结束时,栈内存会被自动释放。
2. 静态存储区域和动态存储区域
  • 静态存储区域

    • 在编译阶段就分配并初始化的内存区域,程序运行时无法修改大小。
    • 包含代码区、数据区(已初始化的全局变量和静态变量)、BSS区(未初始化的全局变量和静态变量)。
  • 动态存储区域

    • 动态分配的内存区域,程序运行时可以根据需要分配和释放。

    • 包含堆区和栈区。

    • 栈区由编译器自动管理,不需要手动干预。

    • 堆区 由程序员通过 newmalloc 等操作符手动分配内存,并通过 deletefree 等释放内存。

      高地址

      ±-------------------+

      | 栈(Stack) | ← 向低地址增长

      ±-------------------+

      | 空闲区 |

      ±-------------------+

      | 堆(Heap) | ← 向高地址增长

      ±-------------------+

      | BSS段(未初始化)|

      ±-------------------+

      | 数据段(已初始化) |

      ±-------------------+

      | 代码段(Text) |

      ±-------------------+

      低地址

2、动态内存管理(数组,动态对象)

deletedelete[] 是用于释放由 newnew[] 分配的动态内存的运算符。它们之间的区别主要在于处理数组和单个对象的方式上。

  1. delete

    • delete 运算符负责释放之前通过 new 运算符动态分配的内存,在释放内存之前,delete 会先调用该对象的析构函数。

    • 析构函数完成,delete 将调用底层的内存管理机制,来释放之前由 new 分配的那块内存,使得这块内存可以被操作系统再次使用。

    • delete 运算符并不会修改指针本身。指针指向的内存区域被认为是无效的,再次使用这个指针(除了将其设为 nullptr 或重新分配)会导致未定义行为。

    cpp 复制代码
    MyClass *p = new MyClass;// 分配单个对象
    delete p; // 释放p指向的内存
    p = nullptr;// 防止悬空指针
  2. delete[] (数组指针)

    delete[] 则专门用于释放由 new[] 分配的数组内存。当使用 new[] 创建一个数组时,必须使用 delete[] 来释放这个数组的所有元素的内存。

    delete[] 不仅释放内存,还会依次调用数组中每个元素的析构函数,从最后一个元素开始向前调用,确保所有资源得到正确清理。

cpp 复制代码
MyClass* arr = new MyClass[10];  // 分配数组
delete[] arr;  // 释放数组的内存,并调用每个元素的析构函数
arr = nullptr;  // 防止悬空指针