C++ 的类(Class)和对象(Object)是面向对象编程的核心,它们将数据与操作封装为有机整体,实现了对现实世界的抽象建模。本文将从基础定义出发,逐步深入到内存布局、默认成员函数、高级特性等核心内容,结合丰富的代码示例和原理分析,帮助读者全面掌握这一关键知识体系。
一、类的基础定义与核心结构
1. 类的定义语法与访问控制
类是用户自定义类型,通过class
关键字定义,包含成员变量(属性)和成员函数(方法),以分号结束:
class Stack {
public:
// 公共接口:外部可直接调用
void Init(int n = 4); // 初始化栈
void Push(int x); // 入栈
int Top(); // 获取栈顶元素
void Destroy(); // 销毁栈
private:
// 私有数据:仅类内可见
int* array; // 存储数据的动态数组
size_t capacity; // 栈容量
size_t top; // 栈顶下标(初始为0)
};
- 访问限定符:
public
:公开成员,类外可直接访问(如st.Push(5)
)。private
:私有成员,仅类内函数或友元可访问(外部访问会编译报错)。protected
:保护成员,类内及派生类可访问(继承时使用)。
struct
与class
的区别:
struct
默认成员访问权限为public
,class
默认为private
,其他语法完全一致。通常用class
定义类,struct
定义简单数据结构。
2. 类域与作用域解析符::
类定义了独立的作用域,做声明和定义分离时,类外定义成员函数时需通过::
指定所属类域:
// 类外定义Init函数,属于Stack类域
void Stack::Init(int n) {
array = (int*)malloc(sizeof(int) * n); // 分配内存
if (!array) perror("malloc failed");
capacity = n;
top = 0;
}
若未指定类域,编译器会将Init
视为全局函数,导致找不到类成员而报错。
二、对象的实例化与内存布局
1. 实例化过程:从类到对象的具象化
类是模板(不开空间),对象是实例(开空间)。实例化对象时,内存为成员变量分配空间,成员函数存储在公共代码区,不占用对象内存:
Stack st1, st2; // 实例化两个栈对象
- 成员变量存储:每个对象独立拥有成员变量(如
st1.array
和st2.array
是不同指针)。 - 成员函数共享:所有对象共享同一套成员函数代码,通过
this
指针区分操作的对象(见下文)。
2. 对象大小计算:内存对齐规则
对象大小由成员变量占用的内存决定,需遵循内存对齐规则(提升 CPU 访问效率):
- 第一个成员从偏移量 0 开始。
- 后续成员对齐到编译器默认对齐数或者成员大小的整数倍位置(VS 默认对齐数为 8,GCC 为 4/8)。
- 总大小为最大对齐数的整数倍。
示例:
class A {
char ch; // 1字节,对齐到1的倍数(偏移0)
int i; // 4字节,对齐到4的倍数(偏移4,补3字节)
}; // 总大小:8字节(1+3+4=8,8是最大对齐数4的倍数)
class B {}; // 空类大小为1字节(占位符,区分对象存在)
- 指针成员:无论指向什么类型,均占 8 字节(64 位系统)。
- 忽略成员函数:对象大小仅计算成员变量,成员函数代码不占对象内存。
三、this 指针:对象的隐含管理者
成员函数通过隐含的this
指针识别当前操作的对象,由编译器自动传递,无需显式声明:
class Date {
public:
void SetDate(int year, int month, int day) {
_year = year; // 等价于 this->_year = year
_month = month; // this指向调用对象(如d1或d2)
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.SetDate(2024, 10, 1); // this指向d1
d2.SetDate(2024, 10, 2); // this指向d2
return 0;
}
- 类型:
const Date* const this
(指向当前对象的常指针,不可修改指向,也不可通过this
修改对象成员 )。 - 应用场景:
- 返回当前对象引用,支持链式调用:
Date& SetYear(int year) { _year = year; return *this; // d1.SetYear(2024).SetMonth(10); }
- 解决成员变量与参数名冲突(如
void Init(int capacity) { this->capacity = capacity; }
)。
- 返回当前对象引用,支持链式调用:
四、默认成员函数:编译器的 “自动助手”
当用户未定义时,编译器自动生成 6 个默认成员函数,核心是前 4 个,掌握它们的行为是正确使用类的关键。
1. 构造函数(Constructor):对象的初始化器
- 作用:在对象实例化时初始化成员变量,替代 C 语言中的
Init
函数,自动调用。 - 特性:
- 名称与类名相同,无返回值(连
void
也不写)。 - 支持重载:可定义无参、带参、全缺省构造函数,但默认构造函数只能有一个(无参或全缺省)。
- 默认生成规则:若用户未定义任何构造函数,编译器生成空构造函数(内置类型不初始化,自定义类型调用其默认构造)。若用户定义了任意构造函数,编译器不再生成默认构造函数。
- 名称与类名相同,无返回值(连
class Stack {
public:
Stack()
{
//...
} // 无参构造
Stack(int n)
{
//...
} // 带参构造
Stack(int n = 4)
{
//...
} // 全缺省构造(与无参构造冲突,不可同时存在)
};
- 初始化顺序:成员变量按类中声明顺序初始化,与初始化列表顺序无关(见下文 “初始化列表”)。
2. 析构函数(Destructor):对象的资源释放者
- 作用:对象生命周期结束时自动调用,释放资源(如堆内存、文件句柄等),防止内存泄漏。
- 特性:
- 名称以
~
开头,无参数和返回值,一个类只能有一个析构函数。 - 默认生成规则:用户未定义时,编译器生成空析构函数,但会为自定义类型成员调用其析构函数。
~Stack() { free(array); // 释放动态数组 array = nullptr; // 置空指针,避免野指针 capacity = top = 0; }
- 名称以
- 调用时机:
- 局部对象离开作用域时(如函数结束)。
- 动态创建的对象被
delete
时(如Stack* p = new Stack(); delete p;
)。
3. 拷贝构造函数(Copy Constructor):对象的复制器
- 作用:用已有对象初始化新对象,分为浅拷贝(默认生成,危险)和深拷贝(自定义,安全)。
- 特性:
- 参数必须为类类型的引用(
const T&
),若用值传递会引发无限递归(因为复制参数时需调用自身拷贝构造)。 - 默认行为:编译器生成的默认拷贝构造函数对内置类型执行值拷贝(复制指针地址,而非指向的内容),对自定义类型调用其拷贝构造函数。
- 必写场景:当类包含指针成员(管理动态资源)时,必须自定义拷贝构造函数,否则会导致多个对象指向同一资源,析构时重复释放。
- 参数必须为类类型的引用(
Stack(const Stack& st) //这里必须要用引用,否者会引发无穷递归
{
// 深拷贝:重新分配内存,复制数据
capacity = st.capacity;
top = st.top;
array = (int*)malloc(capacity * sizeof(int));
if (array)
memcpy(array, st.array, top * sizeof(int));
}
4. 赋值运算符重载(operator=
):对象的赋值器
- 作用:实现两个已有对象的赋值操作,需处理自赋值和资源释放。
- 特性:
- 成员函数形式,返回
T&
以支持链式赋值(如a = b = c;
)。 - 默认行为:与默认拷贝构造类似,对内置类型值拷贝,自定义类型调用其
operator=
。
Stack& operator=(const Stack& st) { if (this != &st) { // 避免自赋值(释放自身资源前检查) free(array); // 释放旧资源 // 深拷贝逻辑(同拷贝构造) capacity = st.capacity; top = st.top; array = (int*)malloc(capacity * sizeof(int)); if (array) memcpy(array, st.array, top * sizeof(int)); } return *this; }
- 成员函数形式,返回
五、初始化列表:更高效的成员初始化方式
在构造函数中,使用成员列表进行初始化,比函数体内赋值更高效,尤其适用于特殊成员:
class Data {
public:
// 初始化列表:直接初始化成员
Data(int x, int y)
: _x(x)
, _y(y)
{
}
// 等价于:Data(int x, int y) { _x = x; _y = y; }(但赋值方式效率更低)
private:
int _x;
int _y;
};
- 必须使用初始化列表的场景(也可以在定义处给缺省值):
- 引用成员:引用必须在定义时初始化,无法在函数体内赋值。
const
成员:常量必须在初始化时赋值,无法后续修改。- 无默认构造函数的自定义类型成员:若成员类型没有默认构造函数,必须在初始化列表显式调用其带参构造函数。
class RefData {
public:
RefData(int& ref)
: _ref(ref)
{
} // 正确,初始化列表初始化引用
// RefData(int& ref) { _ref = ref; } 错误,引用未初始化
private:
int& _ref;
};
- 初始化顺序:成员按类中声明顺序初始化,与初始化列表顺序无关。错误顺序可能导致未定义行为:
class A { public: A(int x) : b(a) , a(x) { } //成员a先声明,初始化顺序应为a→b,a和b的值应相等 private: int a; int b; };
成员变量走初始化列表的逻辑
六、静态成员:类级别的共享资源
用static
修饰的成员属于类本身,而非某个对象,适用于全局共享的数据或工具函数。
1. 静态成员变量
- 特性:
- 存储在静态区,所有对象共享,通过类名
::
变量或对像.变量访问。 - 必须在类外初始化(声明时不可赋值,否则重复定义):
class A { public: int GetCount() { return count; } private: static int count; // 类内声明 }; int A::count = 0; // 类外初始化,且需指定类域 int main() { A a1; cout<<a1.GetCount()<<endl; cout<<A::GetCount()<<endl; return 0; }
- 存储在静态区,所有对象共享,通过类名
- 应用场景:统计类的实例数量、全局配置参数等。
2. 静态成员函数
- 特性:
- 无
this
指针,只能访问静态成员(静态变量 / 静态函数),不能访问非静态成员。
class Math { public: static int Add(int a, int b) { return a + b; } // 静态函数,无this指针 int NonStaticAdd(int a, int b) { return a + b + x; } // 非静态函数,可访问非静态成员x private: static int x; // 静态变量 int y; // 非静态变量 };
- 无
- 调用方式:直接通过类名调用,无需实例化对象:
int sum = Math::Add(3, 5); // 直接调用静态函数
七、友元:突破封装的双刃剑
允许非类成员访问类的私有 / 保护成员,分为友元函数和友元类,但会破坏封装性,需谨慎使用。
1. 友元函数
- 声明:在类内用
friend
关键字声明外部函数,使其获得访问权限:class Date { friend ostream& operator<<(ostream& out, const Date& d); // 友元函数声明 private: int _year, _month, _day; }; // 外部定义友元函数,可访问私有成员 ostream& operator<<(ostream& out, const Date& d) { out << d._year << "-" << d._month << "-" << d._day; return out; }
- 特点:友元函数不是类的成员函数,不受访问限定符位置影响(可在类内任意位置声明)。
2. 友元类
- 声明:类 A 声明类 B 为友元,则 B 的所有成员函数均可访问 A 的私有 / 保护成员:
class A { friend class B; // B是A的友元类 private: int secret; }; class B { public: void AccessSecret(A& a) { a.secret = 100; // 合法访问A的私有成员 } };
- 单向性:友元关系不具有交换性(A 是 B 的友元,B 不一定是 A 的友元)。
八、内部类:嵌套的类结构
定义在另一个类内部的类,作为独立类存在,默认是外部类的友元(可访问外部类私有成员)。
1. 分类与特性
- 静态内部类:用
static
修饰,不依赖外部类实例,只能访问外部类的静态成员。 - 非静态内部类:依赖外部类实例,可访问外部类的所有成员(包括私有成员)。
class Outer {
public:
class Inner { // 非静态内部类,默认是Outer的友元
public:
void Print(Outer& o)
{
cout << o.privateData;
} // 访问外部类私有成员
};
private:
int privateData;
};
2. 应用场景
封装与外部类紧密相关的辅助类,如STL
容器的迭代器常作为内部类实现。
九、匿名对象与编译器优化
1. 匿名对象:临时存在的 “一次性” 对象
无名称的临时对象,用类名创建,生命周期仅当前语句,语句结束后立即析构:
class Printer {
public:
Printer(string msg) {
cout << msg << endl;
}
~Printer() {
cout << "Destroy Printer" << endl;
}
};
int main()
{
// 匿名对象示例
Printer("Hello, World!"); // 输出信息后立即销毁,打印"Destroy Printer"
return 0;
}
- 应用场景:临时调用某个类的方法,无需保留对象(如
Stack().Push(1);
入栈后销毁)。
2. 拷贝构造优化:编译器的 “偷工减料”
现代编译器会优化不必要的拷贝构造,提升性能,常见优化包括:
- RVO(返回值优化):当函数返回本地对象时,直接构造调用处的对象,避免临时对象创建。
A f() { return A(1); // 优化后,直接在调用处构造A(1),不调用拷贝构造。 }
- NRVO(命名返回值优化):对命名的本地对象(如
A aa; return aa;
)进行类似优化。 - 关闭优化:调试时可通过编译器选项(如
g++ -fno-elide-constructors
)关闭优化,观察真实拷贝行为。
十、类设计最佳实践与常见陷阱
1. 资源管理三原则
- Rule of Three:若类需要自定义析构函数(释放资源),则必须同时自定义拷贝构造函数和赋值运算符,避免浅拷贝导致的资源错误。
- Rule of Five(C++11 后):新增移动构造和移动赋值,处理右值引用时的高效资源转移。
2. 避免默认构造函数冲突
确保类有且仅有一个默认构造函数(无参或全缺省),否则会导致实例化时的歧义:
class BadClass {
BadClass() {} // 无参构造
BadClass(int = 0) {} // 全缺省构造(与无参构造冲突,编译报错)
};
3. const
成员函数:保护对象的 “只读” 接口
用const
修饰成员函数,确保函数内不修改对象成员,提高代码安全性:
class ConstDemo {
public:
void Print() const { // this指针为const ConstDemo* const
// _x = 10; 错误,不可修改成员
cout << _x << endl;
}
private:
int _x;
};
const ConstDemo obj;
obj.Print(); // 合法,调用const成员函数
总结:类与对象的核心价值
C++ 的类与对象通过封装(数据隐藏)、初始化与资源管理(构造 / 析构)、拷贝控制(深拷贝)等机制,实现了对复杂数据结构的安全高效管理。理解this
指针、默认成员函数、初始化列表等底层机制,是写出健壮代码的关键。同时,合理使用静态成员、友元、内部类等高级特性,能进一步提升代码的抽象能力和复用性。
在实际开发中,始终遵循 “封装优先” 原则,谨慎处理资源管理,避免浅拷贝陷阱,结合编译器优化与最佳实践,才能充分发挥 C++ 面向对象编程的强大能力。类与对象不仅是语法知识,更是一种建模思维,掌握它们是深入 C++ 世界的必经之路。
练习代码
//可根据该文件中的内容完善Date.cpp
//Date.h
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 析构函数
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
// 获取某年某月的天数
int GetMonthDay(int year, int month) {
assert(month > 0 && month < 13);
int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)) {
return 29;
}
return arr[month];
}
//检查日期合法性
bool CheckDate();
// >运算符重载
bool operator>(const Date& d)const;
// ==运算符重载
bool operator==(const Date& d)const;
// >=运算符重载
bool operator >= (const Date& d)const;
// <运算符重载
bool operator < (const Date& d)const;
// <=运算符重载
bool operator <= (const Date& d)const;
// !=运算符重载
bool operator != (const Date& d)const;
//赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 前置--
Date& operator--();
// 后置--
Date operator--(int);
// 日期-日期 返回天数
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
//Date.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
ostream& operator<<(ostream& out, const Date& d)
{
cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
while (1)
{
cout << "请输入日期:";
cin >> d._year >> d._month >> d._day;
if (!d.CheckDate())
{
cout << "日期非法,请重新输入" << endl;
}
else
break;
}
return in;
}
bool Date::CheckDate()
{
if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}
bool Date::operator>(const Date& d) const {
if (_year > d._year) {
return true;
}
else if (_year == d._year) {
if (_month > d._month)
return true;
else if (_month == d._month)
return _day - d._day;
}
return false;
}
// ==运算符重载
bool Date::operator==(const Date& d) const {
//if (_year == d._year && _month == d._month && _day == d._day)
// return true;
//else
// return false;
return _year == d._year && _month == d._month && _day == d._day;
}
// >=运算符重载
bool Date::operator >= (const Date& d)const {
//if ((*this) > d || (*this) == d)
// return true;
//else
// return false;
return (*this) > d || (*this) == d;
}
// <运算符重载
bool Date::operator < (const Date& d)const {
return !(*this >= d);
}
// <=运算符重载
bool Date::operator <= (const Date& d)const {
return !(*this > d);
}
// !=运算符重载
bool Date::operator != (const Date& d)const {
return !(*this == d);
}
//赋值运算符重载
Date& Date::operator=(const Date& d) {
if (*this == d) {
return *this;
}
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
// 日期+=天数
Date& Date::operator+=(int day) {
_day += day;
while (_day >= GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
// 日期+天数
Date Date::operator+(int day) {
Date tmp(*this);
tmp += day;
return tmp;
}
// 日期-天数
Date Date::operator-(int day) {
Date tmp(*this);
tmp -= day;
return tmp;
}
// 日期-=天数
Date& Date::operator-=(int day) {
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// 前置++
Date& Date::operator++() {
*this += 1;
return *this;
}
// 后置++
Date Date::operator++(int) {
Date tmp(*this);
++(*this);
return tmp;
}
// 前置--
Date& Date::operator--() {
*this -= 1;
return *this;
}
// 后置--
Date Date::operator--(int) {
Date tmp(*this);
--(*this);
return tmp;
}
// 日期-日期 返回天数
int Date::operator-(const Date& d) {
Date max = *this;
Date min = d;
int flag = 1;
if (max < d)
{
max = d;
min = *this;
flag = -1;
}
int count = 0;
while (max != min)
{
--max;
count++;
}
return flag*count;
}