在 C++ 编程中,类和对象是面向对象编程(OOP)的核心基石,封装、继承、多态三大特性均围绕其展开。本文将从类的定义与实例化、默认成员函数、高级特性等维度,结合实战代码,系统梳理类和对象的关键知识点,帮助开发者夯实 OOP 基础。
一、类的基础认知:定义、访问控制与实例化
1.1 类的定义格式
C++ 使用class关键字定义类(struct也可定义类,兼容 C 语言用法且支持成员函数),类体包含成员变量(属性)和成员函数(方法),结束时必须加分号 。为区分成员变量与局部变量,惯例是给成员变量加前缀(如_)或后缀,例如_year、m_month。
class Date {
public:
// 成员函数:初始化日期
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
private:
// 成员变量:加前缀_区分
int _year;
int _month;
int _day;
}; // 分号不可省略
1.2 访问限定符:封装的核心实现
访问限定符控制成员的访问权限,是封装特性的直接体现:
public:类外可直接访问(通常暴露接口函数);private/protected:类外不可直接访问(通常隐藏成员变量,继承时二者有差异);- 访问权限作用域:从限定符出现位置到下一个限定符结束,
class默认private,struct默认public。
1.3 类域与成员函数分离
类定义了独立的作用域,类外实现成员函数时需用::作用域操作符指明所属类,否则编译器会视为全局函数。
// 类内声明,类外实现
void Date::Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
1.4 类的实例化
类是对象的 "设计图",仅声明成员变量(未分配空间),实例化是通过类创建对象并分配物理内存的过程。一个类可实例化多个对象,每个对象拥有独立的成员变量存储空间,成员函数则共享(存储在代码段)。
int main() {
Date d1; // 实例化对象d1,分配内存
d1.Init(2024, 10, 1); // 调用成员函数初始化
return 0;
}
1.5 对象大小计算
对象仅存储成员变量,大小遵循内存对齐规则(与结构体一致),目的是提高访问效率:
-
第一个成员偏移量为 0;
-
其他成员对齐到 "对齐数"(编译器默认值与成员大小的较小值,VS 默认 8)的整数倍;
-
总大小为最大对齐数的整数倍;
-
无成员变量的类对象大小为 1 字节(占位标识对象存在)。
class A {
private:
char _ch; // 1字节
int _i; // 4字节,对齐数4
};
// 内存对齐后大小:8字节(1+3填充+4)
cout << sizeof(A) << endl; // 输出8
二、this 指针:对象的 "隐藏身份标识"
2.1 核心问题
多个对象共享成员函数,函数如何区分操作的是哪个对象?例如d1.Init()和d2.Init(),函数需知道当前操作的是d1还是d2。
2.2 本质与特性
C++ 编译器在成员函数形参第一个位置隐含添加this指针(类型为类名* const),指向当前调用函数的对象,函数体内访问成员变量本质是通过this指针访问(可显式使用,不可显式声明)。
// 编译器优化后的Init函数原型
void Date::Init(Date* const this, int year, int month, int day) {
this->_year = year; // 显式使用this
_month = month; // 隐式使用this
}
// 调用时编译器自动传递对象地址
d1.Init(2024, 10, 1); // 等价于d1.Init(&d1, 2024, 10, 1)
2.3 经典面试题解析
// 题目1:编译运行结果?
class A {
public:
void Print() { cout << "A::Print()" << endl; }
private:
int _a;
};
int main() {
A* p = nullptr;
p->Print(); // 正常运行:Print未访问成员变量,无需解引用p
return 0;
}
// 题目2:编译运行结果?
class A {
public:
void Print() { cout << _a << endl; } // 访问成员变量,需解引用this
private:
int _a;
};
int main() {
A* p = nullptr;
p->Print(); // 运行崩溃:this为nullptr,解引用出错
return 0;
}
三、默认成员函数:编译器的 "自动实现"
当用户未显式定义时,编译器会自动生成 6 个默认成员函数,核心是前 4 个:构造、析构、拷贝构造、赋值重载。
3.1 构造函数:对象的 "初始化器"
-
功能 :替代
Init函数,对象实例化时自动调用,初始化成员变量; -
特性 :
- 函数名与类名相同,无返回值(无需写
void); - 可重载(无参、带参、全缺省);
- 无显式定义时,编译器生成默认构造(对内置类型不初始化,自定义类型调用其默认构造);
- 无参、全缺省、编译器默认生成的构造,统称 "默认构造"(不传参即可调用)。
class Date {
public:
// 全缺省构造(推荐,兼顾多种初始化场景)
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};// 实例化方式
Date d1; // 调用全缺省构造,默认1-1-1
Date d2(2024, 10, 1); // 调用带参构造 - 函数名与类名相同,无返回值(无需写
3.2 析构函数:对象的 "清理工"
-
功能 :替代
Destroy函数,对象生命周期结束时自动调用,释放资源(如堆内存); -
特性 :
- 函数名
~类名,无参数无返回值; - 一个类仅一个析构函数(不可重载);
- 无显式定义时,编译器生成默认析构(对内置类型不处理,自定义类型调用其析构);
- 有资源申请(如
malloc、new)时必须显式定义,否则内存泄漏。
class Stack {
public:
Stack(int n = 4) {
_a = (int*)malloc(sizeof(int) * n);
_capacity = n;
_top = 0;
}
// 显式定义析构,释放堆内存
~Stack() {
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _capacity;
size_t _top;
}; - 函数名
3.3 拷贝构造函数:对象的 "复制器"
-
功能 :用已有对象初始化新对象(如
Date d2 = d1); -
特性 :
- 第一个参数必须是
const 类名&(传值会引发无穷递归),后续参数可带默认值; - 无显式定义时,编译器生成默认拷贝构造(内置类型值拷贝 / 浅拷贝,自定义类型调用其拷贝构造);
- 浅拷贝问题:若成员变量指向堆内存(如
Stack的_a),会导致多个对象共享同一块内存,析构时重复释放崩溃,需显式实现深拷贝。
Stack::Stack(const Stack& st) {
// 深拷贝:为新对象分配独立内存
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr) perror("malloc fail");
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
} - 第一个参数必须是
3.4 赋值运算符重载:对象的 "赋值器"
-
功能 :两个已存在对象间的赋值(如
d1 = d2),区别于拷贝构造(初始化新对象); -
特性 :
- 必须重载为成员函数,函数原型
类名& operator=(const 类名&); - 返回
类名&支持连续赋值(如d1 = d2 = d3); - 需检查自赋值(
if (this != &d)),避免重复释放; - 无显式定义时,编译器生成默认赋值重载(浅拷贝,需资源的类需显式实现深拷贝)。
Date& Date::operator=(const Date& d) {
if (this != &d) { // 避免自赋值
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; // 支持连续赋值
} - 必须重载为成员函数,函数原型
四、高级特性:提升代码灵活性与效率
4.1 初始化列表:成员变量的 "初始化源头"
-
格式 :构造函数后加
:,后跟成员变量初始化表达式(成员变量(值), ...); -
必要性 :引用成员、
const成员、无默认构造的自定义类型成员,必须通过初始化列表初始化; -
注意:初始化顺序与类中成员声明顺序一致,与列表顺序无关。
class Date {
public:
// 初始化列表初始化
Date(int year, int month, int day, int& ref)
: _year(year)
, _month(month)
, _day(day)
, _ref(ref) // 引用必须初始化
, _n(10) // const成员必须初始化
{}
private:
int _year;
int _month;
int _day;
int& _ref; // 引用成员
const int _n; // const成员
};
4.2 static 成员:类的 "共享资源"
- 静态成员变量 :
- 用
static修饰,所有对象共享,存储在静态区; - 类内声明,类外初始化(
类型 类名::变量名 = 值); - 受访问限定符控制,可通过
类名::变量或对象.变量访问。
- 用
- 静态成员函数 :
- 用
static修饰,无this指针; - 仅可访问静态成员变量 / 函数,不可访问非静态成员。
- 用
应用场景:统计对象创建个数:
class A {
public:
A() { ++_scount; }
A(const A&) { ++_scount; }
~A() { --_scount; }
static int GetCount() { return _scount; } // 静态成员函数
private:
static int _scount; // 静态成员变量
};
int A::_scount = 0; // 类外初始化
int main() {
A a1, a2;
cout << A::GetCount() << endl; // 输出2
return 0;
}
4.3 友元:突破封装的 "特殊权限"
友元允许外部函数 / 类访问类的私有成员,分为友元函数和友元类,慎用(破坏封装)。
-
友元函数 :非成员函数,类内声明时加
friend; -
友元类:类 A 是类 B 的友元,则 A 的所有成员函数可访问 B 的私有成员(单向关系,不可传递)。
class Date {
// 友元声明:operator<<可访问私有成员
friend ostream& operator<<(ostream& out, const Date& d);
private:
int _year;
int _month;
int _day;
};// 全局函数实现
ostream& operator<<(ostream& out, const Date& d) {
out << d._year << "-" << d._month << "-" << d._day;
return out;
}// 使用
Date d(2024, 10, 1);
cout << d << endl; // 输出2024-10-1
4.4 匿名对象:临时使用的 "无名称对象"
-
格式 :
类名(实参),生命周期仅当前行; -
场景:临时调用成员函数,无需定义命名对象。
class Solution {
public:
int Sum(int n) { return n*(n+1)/2; }
};// 匿名对象调用函数
int ret = Solution().Sum(100); // 输出5050
五、C++ vs C 语言:封装的优势
以Stack为例,对比 C 语言与 C++ 的实现差异:
| 特性 | C 语言实现 | C++ 实现 |
|---|---|---|
| 数据与函数关系 | 分离(函数需显式传结构体指针) | 封装(数据 + 函数在类内,this 指针隐式传递) |
| 访问控制 | 无,可直接修改结构体成员 | 访问限定符控制,私有成员不可直接修改 |
| 初始化与清理 | 需手动调用Init/Destroy |
构造 / 析构函数自动调用,避免遗漏 |
| 代码简洁性 | 需 typedef,函数参数繁琐 | 类名直接作为类型,语法更简洁 |
六、总结
类和对象是 C++ 面向对象编程的核心,核心要点可概括为:
- 封装:通过类整合数据与函数,访问限定符控制权限;
- 默认成员函数:构造(初始化)、析构(清理)、拷贝构造(复制新对象)、赋值重载(对象赋值)是基础,需区分使用场景;
- 高级特性:初始化列表解决特殊成员初始化,static 成员实现共享资源,友元灵活访问私有成员(慎用),匿名对象简化临时操作;
- 内存与效率:理解 this 指针、内存对齐、编译器拷贝优化,避免内存泄漏和性能问题。