C++的面向对象实现方案
OO 控制:核心是访问权限管控
C 语言结构体成员直接可见,无访问限制;C++ 的 OO 控制仅聚焦访问权限,通过
private等限定符隔离,不改变成员本质,仅语法层面限制访问范围。
-
private 的 “不可见” 原理
private阻止外部调用的核心是编译时检查:外部访问私有成员时,编译器直接报错终止编译,从源头拦截非法访问,属于语法校验而非内存隔离。
多文件组织与成员存储
类多文件组织需用::明确成员所属类,存储特性如下:
- 成员变量:每个对象独占独立内存,互不干扰;
- 成员函数:所有对象共享,固定存储在 DATA 区;
- 头文件放成员函数:需用
inline(仅适用于简单函数),可降低调用开销。
关键认知
OO 机制仅是程序概念上的实现,未改变底层内存模型,本质是 C++ 新增的语法规则与编译检查,实现封装的逻辑抽象。
构造函数
在学习构造函数前需明确:程序运行时内存分为全局静态区、栈空间、堆空间,不同内存区域的分配 / 释放规则不同,而构造函数的核心作用,就是对对象(无论存储在哪个区域)进行初始化。
构造函数:对象的 “初始化器”
构造函数是类中用于初始化对象的特殊成员函数,核心规则如下:
- 命名规则:与类名完全相同,无返回类型(连
void都不能写); - 调用方式:对象创建时自动调用,无法在代码中直接显式调用;
- 核心特性:支持重载,可定义多个参数不同的构造函数,适配不同初始化场景。
- 默认构造函数的关键规则
- 生成条件:当类中未自定义任何构造函数时,编译器会自动生成无参数的默认构造函数;
- 这里的参数在静态区默认为0,堆栈则随机
- 失效场景:一旦自定义构造函数(无论是否带参数),编译器不再自动生成;
- C++11 扩展:可用
=default显式要求编译器生成默认构造函数,用=delete禁用指定构造函数。
- 生成条件:当类中未自定义任何构造函数时,编译器会自动生成无参数的默认构造函数;
构造函数的访问权限
- 常规用法:定义为
public,允许外部代码直接创建对象; - 特殊场景:定义为
private时,外部无法直接创建对象,需通过类的静态成员函数等方式接管创建(如单例模式)。
委托构造:代码复用的简化方式
委托构造允许一个构造函数调用同类的其他构造函数,核心目的是复用初始化逻辑,语法简洁:例:C():C(1){}(无参构造函数委托给带参的C(1),复用其初始化逻辑)。
TDate() : TDate(2000) { // 委托给TDate(int y)
cout << "调用了无参构造函数(委托)" << endl;
}
成员初始化表:解决特殊成员的初始化难题
构造函数体内的赋值操作有局限性:对于const int(常量成员)、int&(引用成员)这类必须“定义时立即绑定值”的成员,构造函数体内赋值无效——它们要求在对象创建的同时完成初始化,而非后续赋值。此时必须使用成员初始化表,在构造函数参数列表后、函数体前完成初始化。
-
核心规则与优势
- 执行顺序:严格按照类中成员的声明次序执行(而非初始化表中的书写顺序),这是易踩坑点;
- 效率优势:直接对成员进行初始化,仅需一次操作;而构造函数体内赋值是“先默认初始化,再赋值”,多一次开销。
-
C++11 扩展特性
- 就地成员初值:用
=给成员指定默认值(如int a = 10;),本质是“默认匹配初始化”,与成员初始化表作用一致; - 统一初始化:用
{}初始化(支持窄化检查,如int b{3.14}会编译报错),语法更统一,类似“批量/逐元素初始化”(类比for-each的遍历逻辑)。
- 就地成员初值:用
-
四特性联动示例
class Test { private: int x = 5; // 就地成员初值(C++11) const int y; // 常量成员,必须初始化 int& z; // 引用成员,必须绑定 int arr[3]; // 数组成员 public: // 成员初始化表 + 统一初始化联动,覆盖所有特性 Test(int& ref, int y_val) : y(y_val), z(ref), arr{1,2,3} {} // 说明:x用就地初值5初始化,y通过初始化表赋值,z绑定外部引用,arr用统一初始化{}赋值 }; int main() { int num = 20; Test t(num, 100); // 联动效果:x=5(就地)、y=100(初始化表)、z绑定num、arr=[1,2,3](统一初始化) return 0; }注:示例中联动了“const成员、引用成员、就地初值、统一初始化、成员初始化表”,既体现了初始化表的核心作用,也展示了C++11特性的配合使用,同时规避了“声明次序≠初始化表顺序”的坑。
析构函数
析构函数:对象的“资源清理者”
析构函数是类的特殊成员函数,核心作用是在对象消亡时自动调用,清理对象所持有的资源——这是C++基于RAII(资源获取即初始化)思想的核心实现,确保资源释放的确定性。
与Java GC的核心区别
| 特性 | C++ 析构函数 | Java GC |
|---|---|---|
| 触发时机 | 对象生命周期结束时自动调用(确定) | 不定时触发(依赖虚拟机调度) |
| 清理范围 | 内存资源 + 非内存资源(如文件句柄、网络连接) | 仅回收内存资源 |
| 并发问题 | 无(确定性调用) | 可能引发并发冲突 |
简单说:GC是“被动、局限的内存回收”,而析构函数是“主动、全面的资源清理”。
资源释放的关键规则
析构函数中需手动释放堆内存(堆资源由程序员申请,需手动回收),但无需释放栈内存和静态内存(栈内存随函数栈帧销毁,静态内存随程序结束释放):
class FileHandler {
private:
FILE* file; // 堆分配的文件资源(非内存资源)
int* data; // 堆内存资源
public:
// 构造函数:获取资源
FileHandler(const char* filename) {
file = fopen(filename, "r"); // 打开文件(非内存资源)
data = new int[100]; // 申请堆内存
}
// 析构函数:释放资源
~FileHandler() {
if (file != nullptr) fclose(file); // 释放非内存资源(文件句柄)
delete[] data; // 手动释放堆内存
// 无需释放栈/静态内存:data是指针(栈上),指向的堆空间才需释放
}
};
private析构函数:自主控制对象存储分配
析构函数可定义为private,此时外部无法直接通过对象生命周期触发析构(编译器禁止),核心作用是强制自主控制对象的存储分配:
- 外部不能定义栈对象(栈对象生命周期结束时会自动调用析构,而外部无访问权限,编译报错);
- 只能在堆中创建对象,通过类提供的public接口(如静态成员函数)手动触发销毁。
示例:通过private析构实现“堆对象专属”的类:
class HeapOnly {
private:
int val;
~HeapOnly() { // 私有析构:外部无法直接调用
cout << "释放HeapOnly对象" << endl;
}
public:
HeapOnly(int v) : val(v) {}
// 提供public静态接口:创建和销毁堆对象
static HeapOnly* create(int v) {
return new HeapOnly(v); // 堆上创建对象(析构由类自主控制)
}
static void destroy(HeapOnly* ptr) {
delete ptr; // 类内部可访问private析构,手动释放堆对象
}
};
// 外部使用:
int main() {
// HeapOnly obj(10); // 编译报错:外部无法创建栈对象(析构不可访问)
HeapOnly* p = HeapOnly::create(10); // 只能通过接口创建堆对象
// 业务逻辑...
HeapOnly::destroy(p); // 手动调用接口销毁,触发析构
return 0;
}
核心总结
析构函数的核心是“确定性释放资源”,RAII思想让资源的获取(构造)和释放(析构)强绑定;private析构则通过限制析构调用权限,强制对象只能在堆上分配,实现存储分配的自主控制。
拷贝构造函数
拷贝构造函数是C++中特殊的构造函数,核心用途是用同类已存在的对象,初始化新创建的对象——它不是“赋值操作”,而是“创建新对象时的初始化行为”。
核心语法格式
类名(const 类名& 源对象) {
// 拷贝逻辑(默认或自定义)
}
- 参数必须是“同类对象的const引用”(避免无限递归拷贝,const保证源对象不被修改);
- 是构造函数的一种,无返回类型,名称与类名一致。
默认拷贝构造的行为
当类中未自定义拷贝构造函数时,编译器会自动生成默认拷贝构造函数,其行为是:
- 执行浅拷贝:对每个成员变量递归调用其自身的拷贝构造函数(基本类型直接复制值,自定义类型调用对应拷贝构造);
- 可通过
=delete显式禁用默认拷贝构造(如类名(const 类名&) = delete;),禁止对象按值拷贝。
3. 自定义拷贝构造:深拷贝示例
默认浅拷贝在成员包含堆内存时会出问题(如重复释放内存),此时需自定义拷贝构造实现深拷贝:
class String {
private:
char* str; // 堆内存成员
public:
// 普通构造:申请堆内存
String(const char* s) {
str = new char[strlen(s) + 1];
strcpy(str, s);
}
// 自定义拷贝构造:深拷贝(避免浅拷贝的堆内存共享问题)
String(const String& other) {
// 为新对象重新申请堆内存,复制源对象内容
str = new char[strlen(other.str) + 1];
strcpy(str, other.str);
}
// 析构函数:释放堆内存
~String() { delete[] str; }
// 禁用默认拷贝构造(若需禁止拷贝,直接写:String(const String&) = delete;)
};
// 使用:深拷贝确保两个对象的str指向独立堆内存
String s1("hello");
String s2 = s1; // 调用自定义深拷贝构造,s2.str是新申请的内存
4. 拷贝构造的调用场景
只有“用已有对象初始化新对象”时才会调用,常见形式:
A a;
A b = a; // 拷贝构造(初始化新对象b)
A c(a); // 拷贝构造(与上等价,更直观的初始化写法)
// 注意:A d; d = a; 这是赋值操作,不调用拷贝构造!
5. 关键坑点:子对象的显式拷贝
若类中包含“子对象”(即其他类的成员对象),且自定义了拷贝构造函数,编译器不会自动递归调用子对象的拷贝构造,而是会调用子对象的默认构造函数——这会导致子对象未按源对象拷贝,需在成员初始化表中显式调用子对象的拷贝构造:
// 子对象类
class SubObj {
public:
int val;
SubObj(int v) : val(v) {}
SubObj(const SubObj& other) : val(other.val) { // 子对象的拷贝构造
cout << "SubObj拷贝构造调用" << endl;
}
};
// 包含子对象的主类
class MainObj {
private:
SubObj sub; // 子对象
public:
// 自定义MainObj拷贝构造:必须显式调用SubObj的拷贝构造
MainObj(const MainObj& other) : sub(other.sub) { // 初始化表显式拷贝子对象
// 若不写sub(other.sub),会调用SubObj的默认构造(而非拷贝构造)
}
MainObj(int v) : sub(v) {} // 普通构造:初始化子对象
};
// 使用:
MainObj m1(10);
MainObj m2 = m1; // 调用MainObj拷贝构造,同时显式触发SubObj拷贝构造
核心总结
拷贝构造的核心是“初始化拷贝”,默认浅拷贝适用于无堆内存的简单类;含堆内存时必须自定义深拷贝;自定义拷贝构造时,子对象需在初始化表显式调用其拷贝构造,避免编译器误用默认构造。
移动构造函数
先理清基础:左值、右值与引用绑定
要理解移动构造,首先得明确左值、右值及引用的绑定规则:
-
左值:可直接访问、有明确内存地址的对象(如变量、数组元素);
-
右值:临时产生、无法通过代码直接访问的对象(如字面量、函数返回的临时变量);
-
左值引用(
&):仅能绑定左值; -
右值引用(
&&):仅能绑定右值,但绑定后自身会变成左值; -
特殊规则:常量左值引用(
const T&)可绑定左值或右值(万能绑定)。 -
简单示例:
int a = 10; // a是左值(可访问、有地址) int& ref1 = a; // 左值引用绑定左值(合法) // int& ref2 = 20; // 错误:左值引用不能绑定右值(20是右值) int&& ref3 = 20; // 右值引用绑定右值(合法) // int&& ref4 = a; // 错误:右值引用不能绑定左值 const int& ref5 = a; // 常量左值引用绑定左值(合法) const int& ref6 = 20; // 常量左值引用绑定右值(合法,万能绑定)
移动构造函数:解决临时对象的拷贝浪费
拷贝构造函数(无论浅拷贝、深拷贝)都会为新对象重新分配资源(如new申请内存),但对于函数返回的临时对象(右值),这种“完整拷贝”完全是浪费——临时对象用完就销毁,与其拷贝资源,不如直接“接管”它的资源。
移动构造的核心语法
参数必须是右值引用,核心逻辑是“资源转移”(窃取原对象的资源),而非“拷贝”:
class MyArray {
private:
int* arr;
int size;
public:
// 普通构造:申请堆内存
MyArray(int n) : size(n) {
arr = new int[size];
cout << "普通构造:申请内存" << endl;
}
// 移动构造函数:参数为右值引用
MyArray(MyArray&& other) : size(other.size), arr(other.arr) {
other.arr = nullptr; // 关键:原对象指针置空,避免析构时重复释放
cout << "移动构造:接管资源" << endl;
}
// 析构函数:释放内存
~MyArray() {
if (arr) delete[] arr;
}
};
2. 移动构造的调用场景
当用右值初始化新对象时自动调用,常见两种情况:
- 函数返回的临时对象(天然右值);
- 用
std::move()显式将左值转换为右值。
3. 完整示例(含调用场景)
// 生成临时对象(返回右值)
MyArray generateArray() {
return MyArray(10); // 返回临时对象(右值)
}
int main() {
// 场景1:临时对象初始化新对象(触发移动构造)
MyArray arr1 = generateArray();
// 输出:普通构造→移动构造(接管临时对象的资源,无额外拷贝)
// 场景2:std::move转换左值为右值(触发移动构造)
MyArray arr2(5); // 普通构造
MyArray arr3 = std::move(arr2); // 显式转换为右值,触发移动构造
// 注意:arr2的arr已被置空,后续不可再使用
return 0;
}
核心总结
移动构造的本质是“资源转移”,针对右值(临时对象)避免冗余拷贝,提升效率;其参数是右值引用,调用时机为“右值初始化新对象”(含std::move转换的左值),核心步骤是“窃取资源+原对象置空”,防止重复释放。
动态对象和动态对象数组
动态对象指在堆内存上创建的对象(区别于栈对象——栈对象随作用域结束自动销毁),C++通过
new(创建)和delete(销毁)运算符管理,核心是“手动分配内存+自动调用构造/析构”,实现对象生命周期的自主控制。
new运算符:动态对象的创建
new是C++专属的动态创建运算符,不仅负责堆内存分配,还会自动调用构造函数初始化对象,支持基本类型和自定义类型,执行步骤固定:
- 分配堆内存;
- 调用构造函数(自定义类型)或直接初始化(基本类型);
- 返回对象的内存地址(需用指针接收)。
-
new的三种核心语法(附示例)
// 1. 基本类型(无初始化) int* p1 = new int; // 堆上分配int内存,默认值不确定 // 2. 带初始化的基本类型 int* p2 = new int(10); // 初始化值为10 int* p3 = new int{20}; // C++11统一初始化,效果同上 // 3. 类对象(带/不带参数构造) class Test { public: Test() { cout << "默认构造" << endl; } Test(int a) { cout << "带参构造:" << a << endl; } }; Test* t1 = new Test; // 调用默认构造 Test* t2 = new Test(30); // 调用带参构造 Test* t3 = new Test{40}; // 统一初始化语法 -
new的关键特点
-
动态对象是匿名对象,无法直接访问,必须通过指针间接操作;
-
指针大小仅与系统位数有关(32位系统4字节,64位8字节),与指向对象的大小无关;
-
支持重载:可自定义new的内存分配逻辑(极简示例):
// 全局重载new(仅演示语法,实际需处理内存分配) void* operator new(size_t size) { cout << "自定义new分配" << size << "字节" << endl; return malloc(size); // 底层仍可调用malloc } Test* t4 = new Test; // 调用自定义new,输出"自定义new分配4字节"(假设Test占4字节)
-
delete运算符:动态对象的销毁
delete是new的配套销毁运算符,核心是“先销毁对象,再释放内存”,执行步骤:
- 调用析构函数(自定义类型,释放对象持有的资源);
- 释放堆内存(归还给操作系统);
- 建议将指针置空(
p = nullptr),避免悬空指针。
-
核心示例:
// 销毁基本类型(无需析构,直接释放内存) delete p1; p1 = nullptr; // 置空避免悬空 // 销毁类对象(自动调用析构) delete t1; t1 = nullptr; delete t2; t2 = nullptr;
动态对象数组:创建与销毁的关键规则
动态对象数组的创建语法与普通数组类似,但销毁时必须用delete[],不可省去[]!
-
数组的创建与销毁示例
// 创建:动态数组(10个Test对象,均调用默认构造) Test* arr = new Test[10]; // 销毁:必须用delete[],不可写delete arr! delete[] arr; arr = nullptr; -
为什么不能省去[]?(底层原理)
动态对象数组的内存布局中,编译器会在数组指针
arr的前侧额外分配一块空间,存储数组的对象个数(长度)。
- 用
delete[]时:编译器读取这块“长度空间”,遍历所有对象调用析构函数,再释放整个数组内存; - 省去
[](用delete arr)时:编译器不会读取长度信息,仅调用数组第一个对象的析构函数,释放的也只是第一个对象的内存——导致内存泄漏+析构不完整(后续9个对象未调用析构,资源无法释放)。
- 用
核心总结
- new/delete是成对使用的:new创建→delete销毁,new[]创建→delete[]销毁,不可混用;
- new的核心优势:比C的malloc多了“自动调用构造函数”,delete比free多了“自动调用析构函数”;
- 动态对象数组的
[]是“析构遍历开关”,省去则会导致严重的内存和资源问题,是高频易错点。
Const成员
const成员变量与const成员函数:对象的“只读控制”
const成员变量的初始化规则
const成员变量(如const int x)必须在对象创建时完成初始化,且初始化后不可修改:
- 老版C++中,需通过成员初始化表完成初始化(无法在构造函数体内赋值);
- 新版C++支持就地成员初值(如
const int x = 10;),语法更便捷,但核心原则不变——const成员必须在定义时绑定值。
示例(初始化表方式,兼容所有版本):
class A {
const int x; // const成员变量,必须初始化
public:
A(int c) : x(c) {} // 唯一合法方式:初始化表中绑定值
};
const成员函数:“不修改对象”的语法承诺
const成员函数是类中承诺“不会修改对象普通成员变量”的特殊成员函数,核心语法是在函数声明后加const。
-
底层实现原理:this指针的const修饰
成员函数的底层都隐含
this指针参数,const成员函数的本质是对this指针的双重限制:- 非const成员函数的
this:A* const this(指针本身不可改,但可修改指向的对象成员,如this->x = 1); - const成员函数的
this:const A* const this(指针本身不可改,且指向的对象成员也不可改,禁止this->x = 1)。
- 非const成员函数的
-
核心调用规则(高频考点)
class A { int x, y; public: void f() { x = 1; y = 1; } // 非const成员函数:可修改成员 void show() const { cout << x << y; } // const成员函数:不可修改成员 }; int main() { const A a(0, 0); // const对象 a.f(); // 错误:const对象只能调用const成员函数(this不匹配) a.show(); // 正确:const对象匹配const成员函数的this A b(0, 0); // 非const对象 b.f(); // 正确:非const对象可调用所有成员函数 b.show(); // 正确:非const对象也可调用const成员函数 }总结规则:
- const对象 → 仅能调用const成员函数;
- 非const对象 → 可调用const/非const成员函数;
- const成员函数内 → 不可修改普通成员变量(受
const A* this限制)。
mutable修饰符:突破const的“君子协定”
若需在const成员函数中修改某个成员(且该修改不影响对象对外行为,如内部缓存),可使用mutable修饰该成员——这是C++的“君子协定”:允许修改内部状态,但对外仍保持“只读”形象。
示例(内部缓存场景):
struct Fib {
int n_;
mutable bool cached = false; // mutable:可在const函数中修改
mutable int cache = 0; // mutable:可在const函数中修改
int value() const { // const成员函数:对外承诺“不修改对象”
if (!cached) {
cache = fib(n_); // 允许修改mutable成员(内部缓存)
cached = true; // 允许修改mutable成员(缓存标记)
}
return cache;
}
};
核心用途:适用于内部缓存、计数等“外部不可见”的成员,既保持const函数的只读承诺,又支持必要的内部状态更新。
新的Const
这里主要讲的是constexpr和constval,这两个修饰符会把运行期的计算提前到编译期,提升效率、更早暴露错误。
constexpr:可在编译期 / 运行期求值
-
规则:
constexpr修饰的变量 / 函数,若参数是常量表达式,则在编译期求值;否则在运行期求值; -
适用场景:数组大小、模板参数、
switch的case标签等 “必须是编译期常量” 的场景。 -
示例(对应 PPT Example1/2):
// Example1:constexpr运算符,编译期计算BAD|EOF constexpr int operator|(Flags f1, Flags f2) { return Flags(int(f1)|int(f2)); } // Example2:constexpr对象/数组,编译期初始化 constexpr Point origo(0,0); constexpr Point a[] = {Point(0,0), Point(1,1)}; // 编译期创建数组
consteval(C++20):必须在编译期求值
- 规则:只能修饰函数,调用时参数必须是常量表达式,否则编译报错(强制编译期计算);
- 区别于
constexpr:constexpr是 “可选编译期”,consteval是 “强制编译期”。
示例(对应 PPT Example3):
consteval int pow2(int n) { return 1 << n; }
constexpr int M = pow2(8); // 正确:8是常量表达式,编译期计算
// int r = pow2(y); // 会报错误:y是变量,不是常量表达式
静态成员
静态成员:类级别的数据共享方案
C语言中无“类共享数据”的原生支持,C++的静态成员(静态成员变量+静态成员函数)核心目标,就是解决“多个对象如何安全共享同一数据”的问题——既避免普通成员变量“每个对象独有一份”的隔离性,又规避全局变量“数据无保护、命名污染”的缺陷,让共享数据归属于“类本身”,同时遵循类的访问控制规则。
静态成员变量:所有对象共享的“类级变量”
-
核心规则:声明与初始化
-
基础语法:类内声明,类外定义初始化(必须遵循,否则会报链接错误): 注意:不可在头文件中进行类外定义,否则多次包含头文件会导致变量重复定义。
class A { private: static int shared_val; // 类内声明(需加static关键字) }; // 类外定义+初始化:必须写类名::限定,且只能放在.cpp文件中(避免重复定义) int A::shared_val = 0;
-
-
C++17简化方案:
inline static直接类内初始化,编译器自动处理类外定义逻辑:class A { private: inline static int shared_val = 0; // C++17支持,无需类外额外定义 }; -
关键特点
- 存储特性:所有对象共享同一份数据(全局唯一拷贝),存储在全局静态区,生命周期贯穿程序运行全程(早于对象创建,晚于对象销毁);
- 访问控制:遵循类的
public/private规则(如private static变量仅类内可访问,外部无法直接修改,比全局变量更安全)。
静态成员函数:操作静态成员的专属函数
- 规则与限制
-
语法要求:类内声明时加
static,类外定义时不能再加static:class A { public: static void show_val(); // 类内声明加static }; // 类外定义:无static关键字,需用类名::限定 void A::show_val() { cout << shared_val << endl; // 仅能访问静态成员变量 } -
核心限制:无
this指针(因为属于类而非对象),因此只能访问静态成员变量/静态成员函数,不能访问非静态成员(无法定位具体对象的非静态成员)。
-
- 调用方式(推荐直接用类名调用)
- 类名直接调用(更直观,体现“类级操作”):
A::show_val(); - 对象调用(语法允许,但不推荐,掩盖了静态特性):
A a; a.show_val();
- 类名直接调用(更直观,体现“类级操作”):
静态成员的核心价值
- 安全的共享数据:比全局变量多了访问控制,避免数据被随意修改;
- 无需创建对象即可调用:静态成员属于类,程序启动后无需实例化对象就能使用(适合工具类功能、单例模式等场景);
- 对象状态统计:轻松实现“类的对象总数”等跨对象统计功能。
典型应用场景示例
-
统计类的对象数量
class Counter { private: static int obj_count; // 静态变量:统计对象总数 public: Counter() { obj_count++; } // 构造时计数+1 ~Counter() { obj_count--; } // 析构时计数-1 static int get_obj_num() { // 静态函数:返回当前对象数 return obj_count; } }; int Counter::obj_count = 0; // 类外初始化 // 使用: Counter c1, c2; cout << Counter::get_obj_num(); // 输出2(当前2个对象) { Counter c3; cout << Counter::get_obj_num(); // 输出3(局部对象c3创建) } cout << Counter::get_obj_num(); // 输出2(c3析构,计数减少) -
单例模式(全局唯一实例)
class Singleton { private: static Singleton* instance; // 静态指针:存储唯一实例 Singleton() {} // 私有构造:禁止外部创建对象 Singleton(const Singleton&) = delete; // 禁用拷贝构造,防止多实例 public: // 静态函数:获取唯一实例(不存在则创建) static Singleton* get_instance() { if (instance == nullptr) { instance = new Singleton; } return instance; } static void destroy() { // 静态函数:销毁实例 delete instance; instance = nullptr; } }; Singleton* Singleton::instance = nullptr; // 类外初始化 // 使用:全局仅能通过get_instance()获取一个实例 Singleton* p1 = Singleton::get_instance(); Singleton* p2 = Singleton::get_instance(); cout << (p1 == p2); // 输出true(同一实例) Singleton::destroy();
核心总结
静态成员的本质是“类级别的成员”,而非“对象级别的成员”:
- 静态成员变量:类所有对象共享,类外初始化(C++17用
inline static简化); - 静态成员函数:无
this指针,仅操作静态成员,可通过类名直接调用; - 核心价值:安全共享数据、无需实例化调用,典型应用是对象统计、单例模式等。
友元
就像上面我们需要在const成员函数中改变其中的成员变量会用到mutable修饰符,在这里我们也会需要在外部用到内部的private变量的情况 友元机制:封装与访问灵活性的折中方案
友元的三类形式(类内用friend声明)
友元的核心是“精准授权”,根据授权范围分为三类,均需在被访问的类内部用friend关键字声明:
-
友元函数:授权全局函数访问私有成员
当某个全局函数需要频繁访问多个类的
private成员时,可将其声明为这些类的友元函数,直接操作私有数据,无需通过public接口。示例(矩阵与向量相乘):
class Matrix { private: int* p_data; // 私有成员:存储矩阵数据 public: // 声明multiply为友元函数,允许其访问private成员 friend void multiply(Matrix& m, Vector& v, Vector& res); }; class Vector { private: int* p_data; // 私有成员:存储向量数据 public: // 同样声明multiply为友元函数 friend void multiply(Matrix& m, Vector& v, Vector& res); }; // 全局函数:可直接访问Matrix和Vector的private成员p_data void multiply(Matrix& m, Vector& v, Vector& res) { // 直接操作私有数据,避免多次接口调用的冗余 for (int i = 0; i < 3; i++) { res.p_data[i] = m.p_data[i*3] * v.p_data[0] + m.p_data[i*3+1] * v.p_data[1] + m.p_data[i*3+2] * v.p_data[2]; } } -
友元类:授权整个类的所有成员函数访问
若类B需要频繁访问类A的
private成员,可将类B声明为类A的友元类——此时类B的所有成员函数都能直接访问类A的私有成员,授权范围比友元函数更广。示例:
class A { private: int x; // 私有成员 public: friend class B; // 声明B为友元类,授权B的所有成员函数 }; class B { public: void setA(A& a, int val) { a.x = val; // B的成员函数可直接修改A的private成员x } int getA(A& a) { return a.x; // 直接访问A的private成员x } }; -
友元类成员函数:精准授权单个成员函数
比友元类更精细的授权方式:仅允许其他类的某个特定成员函数访问当前类的
private成员,避免过度开放权限。核心注意:需先做前置声明(告知编译器目标类存在),否则会因“未定义类”报错。示例(含前置声明):
// 前置声明:告诉编译器“类C存在”(不完整声明,仅用于友元声明) class C; class A { private: int x; // 私有成员 public: // 仅声明C的成员函数f()为友元,其他函数无访问权限 friend void C::f(A& a); }; // 定义类C,此时编译器已知晓C的结构 class C { public: void f(A& a) { a.x = 10; // C::f()可直接访问A的private成员x } void g(A& a) { // a.x = 20; // 错误:仅f()是友元,g()无访问权限 } };
友元的关键注意点(高频易错点)
- 前置声明不可少:若友元涉及未定义的类/函数,必须先做前置声明(如
class C;),否则编译器无法识别目标实体,会报“未声明标识符”错误——尤其适用于类之间互相引用、友元成员函数的场景。 - 友元不具有传递性:若A是B的友元,B是C的友元,A并不自动成为C的友元。友元是“单向、精准授权”,不传递、不继承,避免滥用导致封装失效。
- 友元是“单向授权”:类A声明类B为友元,仅表示B能访问A的私有成员,A不能直接访问B的私有成员——授权是单向的,需双向授权需各自声明。
友元的使用原则:不滥用,坚守封装
友元是“必要时才使用的工具”,而非常规方案:
- 日常开发优先通过
get/set等public接口访问成员,保持类的接口“最小化且完备”,避免直接暴露私有数据; - 仅在“接口访问效率极低”“需跨类协同操作私有数据”时使用友元(如数学运算、工具类协作),遵循迪米特法则(最少知识原则),不滥用友元破坏封装的安全性。
核心总结
友元的核心价值是“精准授权访问”,解决封装与灵活性的矛盾:
- 三类形式:友元函数(全局函数)、友元类(整个类)、友元类成员函数(单个函数),均通过
friend声明; - 关键规则:需前置声明、无传递性、单向授权;
- 使用原则:坚守封装,仅在必要时使用,避免过度开放权限。
921

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



