mthoutai

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

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:私有成员,仅在类内可访问。
在这里插入图片描述

良好的封装实践通常将成员变量设为privateprotected,仅将需要对外提供的接口设为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访问内存的效率。主要规则包括:

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  3. 结构体总大小为最大对齐数的整数倍。
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参数,但由编译器隐式处理。

[AFFILIATE_SLOT_1]

三、六大默认成员函数详解(上):构造、析构与拷贝

当用户没有显式定义时,编译器会自动生成六个特殊的成员函数,它们管理着对象的生命周期和基本操作。理解它们何时生成、行为如何,是避免内存泄漏和逻辑错误的重中之重。

在这里插入图片描述

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]

五、实践指南:何时需要自定义默认成员函数?

理解六大默认成员函数后,最关键的是知道何时需要自己动手实现它们。这里有一个简单的“三分法”原则:

  1. 无需自定义(使用编译器默认):当你的类仅包含基本数据类型(int, double等)或其他类的对象,且这些类自身管理良好时。例如,一个简单的数据聚合类(POJO)。
  2. 必须自定义(Rule of Three):如果你的类直接管理动态内存、文件描述符、网络套接字等资源(即拥有原始指针指向堆内存),那么你通常需要自定义析构函数、拷贝构造函数和赋值运算符重载。这是经典的“三法则”。C++11后,通过定义移动语义函数(移动构造、移动赋值),可以优化资源转移,演变为“五法则”或“零法则”(使用智能指针等RAII对象,让编译器管理一切)。
  3. 选择性自定义:构造函数根据初始化需求定义;取地址重载几乎永远不需要动。

让我们通过一个完整的日期类实现,来串联这些知识:

#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;
};
posted on 2026-04-04 17:44  mthoutai  阅读(15)  评论(0)    收藏  举报