C++面向对象编程核心:深入理解类的定义与六大默认成员函数
在C++编程语言中,类是面向对象编程(OOP)的基石,它封装了数据和行为,是构建复杂软件系统的核心单元。与Python、JavaScript等动态语言不同,C++的类机制更接近底层,提供了对内存和性能的精细控制。理解C++类的完整生命周期,特别是编译器为我们自动生成的六大默认成员函数,是编写健壮、高效C++代码的关键。本文将带你从类的定义出发,深入剖析其内部机制,并通过与Go、TypeScript等现代语言的对比,揭示C++类设计的独特之处。
一、类的定义、封装与作用域
C++中使用class关键字来定义一个类,这是封装数据和相关操作的基本单元。一个典型的类定义如下所示:
//Stack类名
class Stack
{
public:
//成员函数
void Init(int n = 4)
{
_array = (int*)malloc(sizeof(int) * n);
if (_array == nullptr)
{
perror("Init():malloc fail");
return;
}
_capacity = n;
_top = 0;
}
private:
//成员变量
int* _array;
size_t _capacity;
size_t _top;
};
这里,Stack是类名,花括号内是类的主体。类主体包含两种主要成员:成员变量(属性)和成员函数(方法)。与Python使用缩进或JavaScript使用class关键字(ES6)不同,C++的类定义更显式,且与C语言的结构体(struct)有深厚渊源。事实上,C++中的struct也被升级为类,可以包含函数,但默认访问权限与class不同。
访问限定符是实现封装的关键。C++提供了三种:
public:公有成员,在类内外均可访问。protected:受保护成员,在类内和派生类中可访问。private:私有成员,仅在类内可访问。

良好的封装实践通常将成员变量设为private或protected,仅将需要对外提供的接口设为public。这有助于维护对象的内部状态一致性,是C++、Java等静态类型语言的常见模式,与TypeScript的private修饰符理念相通。
此外,类本身定义了一个新的作用域。在类外定义成员函数时,必须使用作用域解析运算符::来指明其所属的类域,这影响了编译器的名称查找规则。
class Stack
{
public:
//成员函数
void Init(int n = 4);
private:
//成员变量
int* _array;
size_t _capacity;
size_t _top;
};
//函数的声明和定义分离,需要指定类域
void Stack::Init(int n)
{
_array = (int*)malloc(sizeof(int) * n);
if (_array == nullptr)
{
perror("Init():malloc fail");
return;
}
_capacity = n;
_top = 0;
}
int main()
{
Stack st;
st.Init();
return 0;
}
二、对象的实例化、内存布局与this指针
类只是一个蓝图或类型声明,真正在内存中创建实体对象的过程称为实例化。这个过程在栈或堆上分配内存,用于存储对象的成员变量。
class Date
{
public:
void Init(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
//只是声明,没有开空间
int _year;
int _month;
int _day;
};
int main()
{
//类实例化对象,开空间
Date d1;
d1.Init(2025, 2, 1);
d1.Print();
Date d2;
d2.Init(2025, 2, 11);
d2.Print();
return 0;
}

一个关键问题是:对象中存储了什么? 成员函数并不存储在每一个对象中。函数代码作为指令存放在代码段,所有对象共享同一份函数代码。对象内存中只包含其独立的成员变量数据。这与Python或JavaScript中对象同时包含属性和方法引用的模型有显著差异。
⚙️ 对象大小的计算遵循严格的内存对齐规则,目的是提升CPU访问内存的效率。主要规则包括:
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 结构体总大小为最大对齐数的整数倍。
class A
{
private:
char c;
int _year;
};
class B
{
public:
void Init()
{}
};
class C
{};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
return 0;
}

即使是一个空类,其对象大小也为1字节,这是为了确保每个对象在内存中都有唯一的地址。
那么,当多个对象调用同一个成员函数时,函数如何知道操作哪个对象的数据呢?答案就是this指针。它是一个隐含的、常量指针,指向调用该成员函数的对象本身。编译器在编译阶段,会将成员函数调用转换为带有this指针参数的普通函数调用。
class Date
{
public:
//void Init(Date* const this,int year,int month,int day)
void Init(int year = 1, int month = 1, int day = 1)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout << this->_year << " " << this->_month << " " << this->_day << endl;
}
private:
//只是声明,没有开空间
int _year;
int _month;
int _day;
};
int main()
{
//类实例化对象,开空间
Date d1;
d1.Init(2025, 2, 1);
//d1.Print(&d1);
//this指针接收的是变量的地址
d1.Print();
Date d2;
d2.Init(2025, 2, 11);
d2.Print();
return 0;
}
理解this指针是理解C++成员函数运作机制的核心,它类似于Python中方法的self参数,但由编译器隐式处理。
三、六大默认成员函数详解(上):构造、析构与拷贝
当用户没有显式定义时,编译器会自动生成六个特殊的成员函数,它们管理着对象的生命周期和基本操作。理解它们何时生成、行为如何,是避免内存泄漏和逻辑错误的重中之重。

1. 构造函数 用于在对象创建时初始化其成员。其名称与类名相同,无返回值。
- 可以重载,提供多种初始化方式。
- 如果用户未定义任何构造函数,编译器生成一个默认构造函数(无参),但对内置类型成员不做初始化(值随机)。
- C++11支持在类内给成员变量缺省值,这会被默认构造函数使用。
class Date
{
public:
//无参的构造函数与带参的构造函数构成函数重载
//无参的默认构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//带参的构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//调用默认构造函数
//没有默认构造函数,编译器会报错
Date d1;
//Date d1()//调用无参的构造函数,后面不需要(),否则编
//译器无法区分是函数声明还是实例化对象
//调用带参的构造函数
Date d2(2025, 2, 12);
return 0;
}

2. 析构函数 用于在对象销毁时清理资源(如动态内存)。其名称是在类名前加~。
- 无参数,不可重载。
- 对于管理资源的类(如动态数组、文件句柄),必须显式定义析构函数来释放资源,否则会造成资源泄漏。
- 析构函数的调用顺序与构造顺序相反。
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认生成MyQueue的析构函数会去调用Stack的析构,释放Stack内部的资源
/*~MyQueue()
{}*/
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st;
MyQueue mq;
return 0;
}

3. 拷贝构造函数 用于用一个已存在的对象初始化一个新对象。
- 参数必须是本类对象的常量引用(避免无限递归)。
- 默认的拷贝构造函数进行浅拷贝(按字节复制)。如果类中有指针成员并指向堆内存,浅拷贝会导致两个对象的指针指向同一块内存,引发双重释放等问题。此时必须自定义实现深拷贝。
class Date
{
public:
//带参的构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
//传引用返回
Date& func1(const Date& d)
{
Date tmp(d);
//tmp是一个局部对象,出了函数作用域就会被销毁,不能用引用返回
//相当于野引用
return tmp;
}
int main()
{
Date d1(2025, 2, 12);
Date d2(d1);
Date d3 = func1(d1);
return 0;
}
四、六大默认成员函数详解(下):赋值与取地址
4. 赋值运算符重载 是拷贝构造函数的“兄弟”,但它是在两个都已存在的对象之间进行赋值。
- 函数原型:
类名& operator=(const 类名&)。 - 必须注意自赋值检查(
if(this != &d))。 - 为了支持连续赋值(
a = b = c),通常返回*this的引用。 - 默认的赋值重载也是浅拷贝,在涉及资源管理时需要自定义。
class Date
{
public:
Date(int year = 2025, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//传引用返回可以减少拷贝
Date& operator=(const Date& d)
{
//...
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 2, 6);
Date d2(2025, 2, 12);
//赋值重载
//Date成员都是内置类型成员,编译器默认生成的赋值重载就可以
d1 = d2;
return 0;
}
5. & 6. 取地址及const取地址运算符重载 这两个函数通常不需要自定义,编译器生成的版本(返回对象地址)已足够。只有在极特殊场景下,例如想隐藏对象地址或返回一个假地址时,才需要重载它们。
在讨论取地址重载时,不得不提const成员函数。在成员函数参数列表后加const,修饰的是隐含的this指针,表明该函数不会修改对象的成员变量(mutable修饰的除外)。这提高了代码的健壮性,并允许const对象调用这些函数。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// void Print(const Date* const this) const
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 这⾥⾮const对象也可以调⽤const成员函数是⼀种权限的缩⼩
Date d1(2024, 7, 5);
d1.Print();
const Date d2(2024, 8, 5);
d2.Print();
return 0;
}
[AFFILIATE_SLOT_2]
五、实践指南:何时需要自定义默认成员函数?
理解六大默认成员函数后,最关键的是知道何时需要自己动手实现它们。这里有一个简单的“三分法”原则:
- 无需自定义(使用编译器默认):当你的类仅包含基本数据类型(int, double等)或其他类的对象,且这些类自身管理良好时。例如,一个简单的数据聚合类(POJO)。
- 必须自定义(Rule of Three):如果你的类直接管理动态内存、文件描述符、网络套接字等资源(即拥有原始指针指向堆内存),那么你通常需要自定义析构函数、拷贝构造函数和赋值运算符重载。这是经典的“三法则”。C++11后,通过定义移动语义函数(移动构造、移动赋值),可以优化资源转移,演变为“五法则”或“零法则”(使用智能指针等RAII对象,让编译器管理一切)。
- 选择性自定义:构造函数根据初始化需求定义;取地址重载几乎永远不需要动。
让我们通过一个完整的日期类实现,来串联这些知识:
#pragma once
#include<iostream>
using namespace std;
namespace LC
{
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
int Get_Year_Month_Day(int year, int month)const
{
static int day[13] = { -1,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 day[month] + 1;
}
return day[month];
}
bool CheckDate()const
{
if (_month > 12 || _month <= 0)
{
return false;
}
else if (_day <= 0 || _day > Get_Year_Month_Day(_year, _month))
{
return false;
}
return true;
}
//默认构造函数
//构造函数函数名与类名相同,无返回值
//构造函数一旦显示实现,编译器将不再生成构造函数
//构造函数对于内置类型不做处理,是否初始化,取决于编译器
//对于自定义类型会自动调用构造函数
//缺省参数只能在声明的地方给,函数定义的地方不允许给缺省参数
Date(int year = 2025, int month = 1, int day = 1);
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;
Date operator+(int day)const;
Date& operator+=(int day);
Date operator-(int day)const;
Date& operator-=(int day);
int operator-(const Date& d)const;
Date& operator++();
Date operator++(int);
Date& operator--();
Date operator--(int);
private:
//成员变量的声明
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
}
总结与对比:C++的类机制提供了强大的控制力,但也带来了复杂性。相比之下:
- Python/JavaScript:动态类型,无需提前声明类型;内存管理和拷贝语义更简单(引用计数或垃圾回收),但控制力弱。
- Go:没有类的概念,使用结构体和关联方法;组合优于继承;内存管理相对简单。
- TypeScript:为JavaScript添加静态类型和类语法,更接近传统OOP,但运行时行为仍遵循JavaScript。
掌握C++的类与默认成员函数,不仅是学好C++的必经之路,也能让你更深刻地理解其他语言设计背后的权衡与哲学。从理解封装、内存布局开始,到熟练运用构造、析构、拷贝控制,你便掌握了C++面向对象编程的核心武器库。
class Date
{
public:
Date* operator&()
{
//return nullptr;
return this;
}
const Date* operator&()const
{
//return nullptr;
return this;
}
private:
int _year;
int _month;
int _day;
};
浙公网安备 33010602011771号