
个人主页:
wengqidaifeng
✨ 永远在路上,永远向前走
个人专栏:
数据结构
C语言
嵌入式小白启动!
重要OJ算法题详解
蓝桥杯备战
C++从菜鸟到强手
python启航
文章目录
-
- 前言
- 一、类的定义
-
- [1.1 从 struct 到 class](#1.1 从 struct 到 class)
- [1.2 类的基本语法](#1.2 类的基本语法)
- 二、访问限定符
- [三、类的实例化 ------ 类和对象的关系](#三、类的实例化 —— 类和对象的关系)
- [四、this 指针](#四、this 指针)
-
- [4.1 什么是 this 指针](#4.1 什么是 this 指针)
- [4.2 this 指针的特性](#4.2 this 指针的特性)
- [4.3 this 的显式使用](#4.3 this 的显式使用)
- [五、C++ 类的默认成员函数](#五、C++ 类的默认成员函数)
- 六、构造函数
-
- [6.1 为什么需要构造函数](#6.1 为什么需要构造函数)
- [6.2 构造函数的语法](#6.2 构造函数的语法)
- [6.3 默认构造函数](#6.3 默认构造函数)
- [6.4 一个常见陷阱](#6.4 一个常见陷阱)
- [6.5 构造函数与资源管理](#6.5 构造函数与资源管理)
- 七、析构函数
-
- [7.1 为什么需要析构函数](#7.1 为什么需要析构函数)
- [7.2 析构函数的语法](#7.2 析构函数的语法)
- [7.3 用 C++ 版 Stack 简化代码](#7.3 用 C++ 版 Stack 简化代码)
- [7.4 编译器自动生成的析构函数](#7.4 编译器自动生成的析构函数)
- 八、运算符重载入门
- 总结
前言
C++ 与 C 语言最大的区别之一,就是 C++ 是一门面向对象 的编程语言。而面向对象的核心,就是类(Class)和对象(Object)。
在 C 语言中,我们用 struct 来组织数据,相关的函数只能定义在结构体外部,数据和操作是分离的。C++ 的 class 则把数据和操作封装在了一起,让代码更加内聚、更加符合现实世界的思维模型。
本系列分为上、中、下三篇,由浅入深地介绍 C++ 类和对象。本篇(上)聚焦最基础的概念。
一、类的定义
1.1 从 struct 到 class
cpp
// C 风格:数据与操作分离
struct Date_C {
int _year;
int _month;
int _day;
};
void Init(Date_C* d, int year, int month, int day) {
d->_year = year;
d->_month = month;
d->_day = day;
}
// C++ 风格:数据与操作封装在一起
class Date {
public:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
C++ 中 struct 也可以定义成员函数 ,与 class 的唯一区别是:struct 的默认访问权限是 public,class 的默认访问权限是 private。
一张图看懂 C struct 与 C++ class 的区别:
升级
C++:数据与操作封装在一起
class Date {
int y,m,d;
void Init(...){ }
void Print(){ }
};
C 语言:数据与操作分离
struct Date { int y,m,d; };
void Init(Date* d, ...) { }
void Print(Date* d) { }
1.2 类的基本语法
cpp
class ClassName {
// 成员函数(方法)
// 成员变量(属性)
};
注意:类定义末尾必须加分号 ;,这和函数不一样。
二、访问限定符
| 限定符 | 含义 |
|---|---|
public |
类的外部可以访问 |
private |
只能在类的成员函数内部访问 |
protected |
与 private 类似,但在继承中子类可以访问 |
Date 类
可以访问
✅ 允许
❌ 禁止 d._year
private 区域------仅类内可访问
_year / _month / _day 等成员变量
public 区域------外部可访问
Init() / Print() 等成员函数
外部代码: d.Init() ✅
cpp
class Date {
public:
void Init(int year, int month, int day) {
_year = year; // 类内可以访问 private 成员
_month = month;
_day = day;
}
private:
int _year; // 类的"外面"无法直接访问
int _month;
int _day;
};
int main() {
Date d;
d.Init(2024, 7, 10); // OK,public 成员函数可访问
// d._year = 2024; // 错误!private 成员不能从外部直接访问
}
设计原则 :成员变量一般设为
private,对外提供public的成员函数作为接口。这是"封装"的思想。
三、类的实例化 ------ 类和对象的关系
类 是描述某一类事物的"模板"(蓝图),对象是根据这个模板创建出来的具体"实例"。
cpp
class Date {
// ...
private:
int _year; // 这里只是声明,还没有分配空间
int _month;
int _day;
};
int main() {
Date d1; // d1 是一个具体的对象,此时才真正占用内存
Date d2; // d2 是另一个对象,有自己独立的内存空间
cout << sizeof(d1) << endl; // 输出:12(3个int)
cout << sizeof(Date) << endl; // 输出:12(同上,类本身也有大小)
}
重要理解 :类本身不占用存储空间(只是一个模板),只有实例化出来的对象才占用内存。用 sizeof(类名) 得到的是该类对象的大小。
空类的大小
cpp
class A {}; // 空类,没有成员变量
class B {
void Print() {} // 只有成员函数,没有成员变量
};
cout << sizeof(A) << endl; // 输出:1
cout << sizeof(B) << endl; // 输出:1
空类的大小为 1 字节,这是 C++ 标准规定的------为了让每个对象在内存中有唯一的地址。成员函数不占用对象的内存空间,它们存放在代码段中。
class Date ------ 模板/蓝图(不占运行时内存)
实例化
实例化
成员变量声明
int _year;
int _month;
int _day;
成员函数定义
void Init();
void Print();
对象 d1
栈内存
_year=2024
_month=7
_day=10
sizeof=12
对象 d2
栈内存
_year=2024
_month=7
_day=5
sizeof=12
代码段(所有对象共享)
Init() 的二进制代码
Print() 的二进制代码
四、this 指针
4.1 什么是 this 指针
当你写下:
cpp
d1.Init(2024, 3, 31);
d2.Init(2024, 7, 5);
d1 和 d2 调用的是同一个 Init 函数 。编译器怎么知道要初始化的是 d1 还是 d2 的成员变量呢?
答案是 this 指针。编译器在编译成员函数时,会隐式地增加一个参数:
cpp
// 你写的:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
// 编译器处理后的等价形式:
void Init(Date* const this, int year, int month, int day) {
this->_year = year;
this->_month = month;
this->_day = day;
}
调用时也做了转换:
cpp
d1.Init(2024, 3, 31);
// 编译器转换为:
d1.Init(&d1, 2024, 3, 31);
this 指针工作机制一图胜千言:
运行时 编译器 源代码 运行时 编译器 源代码 d1.Init(2024, 3, 31) 隐式转换: d1.Init(&d1, 2024, 3, 31) 生成汇编,this 通过 ecx 寄存器传递 void Init(Date* const this, ...) this->>_year = 2024 → 写入 d1 的内存 this->>_month = 3 this->>_day = 31
4.2 this 指针的特性
- 类型 :
Date* const this------ 指针本身不可修改(不能this = ...),但指向的对象可以修改 - 位置 :this 通过 ecx 寄存器(VS 下)传递,而不是压栈
- 作用域 :只能在非静态成员函数中使用
- 可为空 :在以下情况下 this 可以为
nullptr,只要不访问成员变量就不会崩溃
cpp
class A {
public:
void Print() {
cout << "A::Print()" << endl; // 没有访问成员变量
}
int _a;
};
int main() {
A* p = nullptr;
p->Print(); // OK!没有解引用空指针,只是传递了 nullptr 给 this
// p->_a = 1; // 崩溃!访问空指针的成员变量
}
理解:
p->Print()底层是调用Print(p),函数内部没有使用this访问任何成员变量,所以不会崩溃。
4.3 this 的显式使用
正常情况下你不需要写 this->,编译器会自动补全。但以下场景需要显式写出:
cpp
class Date {
public:
void Init(int year, int month, int day) {
// 如果参数名和成员名冲突,必须用 this 区分
// 此时不带 this 的 year 指的是参数
// (建议成员变量加 _ 前缀来避免冲突)
}
};
五、C++ 类的默认成员函数
如果一个类是"空"的,编译器会自动生成 6 个默认成员函数:
6个默认成员函数
初始化和清理
构造函数
对象创建时自动调用
可重载
析构函数
对象销毁时自动调用
不可重载
拷贝复制
拷贝构造函数
用已有对象初始化新对象
浅拷贝/深拷贝
赋值运算符重载
两个已存在对象间赋值
需考虑自我赋值
地址相关
取地址运算符重载
普通对象取地址
const取地址运算符重载
const对象取地址
- 构造函数 ------ 用于对象初始化
- 析构函数 ------ 用于对象销毁时的清理
- 拷贝构造函数 ------ 用于创建一个已有对象的副本
- 赋值操作符重载 ------ 用于对象间的赋值
- 取地址操作符重载 ------ 返回对象的地址
- const 取地址操作符重载 ------ const 对象的地址
下面我们详细讲解最重要的两个:构造函数和析构函数。
六、构造函数
6.1 为什么需要构造函数
构造函数是对象创建时自动调用的特殊成员函数,用于初始化。
cpp
class Date {
public:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
d1.Init(2024, 7, 10); // 必须先创建对象,再调用 Init------两步操作
// 如果忘记调用 Init,成员变量就是随机值(未初始化)
}
有没有办法在创建对象的同时就完成初始化呢?构造函数就是干这个的。
6.2 构造函数的语法
- 函数名与类名相同
- 没有返回值 (不是
void,是真的没有返回类型) - 对象创建时自动调用
- 可以重载(多个构造函数,参数列表不同)
cpp
class Date {
public:
// 1. 无参构造函数
Date() {
_year = 1;
_month = 1;
_day = 1;
}
// 2. 带参构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
// 3. 全缺省构造函数(推荐写法------一个顶多个)
Date(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; // 调用无参/全缺省构造函数
Date d2(2024, 7, 10); // 调用带参构造函数
Date d3(2024); // 使用缺省值,月日=1
d1.Print(); // 输出:1/1/1
d2.Print(); // 输出:2024/7/10
d3.Print(); // 输出:2024/1/1
}
推荐写法:使用全缺省构造函数,一个函数覆盖多种调用方式。
构造函数生命周期概览:
有
无(编译器生成)
定义对象
Date d1;
分配内存
(栈/堆)
有构造函数?
调用构造函数
初始化成员变量
默认构造函数
内置类型=随机值
类类型=调默认构造
对象可用
...使用对象...
作用域结束
调用析构函数
释放内存
6.3 默认构造函数
默认构造函数指不需要传参就能调用的构造函数,它可以是以下三种之一:
- 编译器自动生成的(如果你一个构造函数都没写)
- 你自己写的无参构造函数
Date() - 你自己写的全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
重要规则 :如果你写了任何一个 构造函数,编译器就不再 自动生成默认构造函数。此时如果你还想用
Date d1;的方式创建对象,你必须自己提供默认构造函数。
cpp
class Date {
public:
Date(int year, int month, int day) { /* ... */ }
private:
int _year, _month, _day;
};
int main() {
// Date d1; // 错误!没有默认构造函数
Date d2(2024, 7, 10); // OK,使用带参构造函数
}
6.4 一个常见陷阱
cpp
Date d1; // OK,调用默认构造函数
Date d2(); // 这不是创建对象!这是声明一个返回 Date 的函数!
// d2.Print(); // 错误!d2 是函数声明,不是对象
Date d2();被编译器解析为函数声明 而不是对象定义,这是 C++ 的"最令人迷惑的解析"(Most Vexing Parse)。要调用默认构造函数,直接Date d2;或Date d2{};(C++11)。
6.5 构造函数与资源管理
构造函数最常见的用途之一:申请资源。
cpp
typedef int STDataType;
class Stack {
public:
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
// ...
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main() {
Stack st1; // 默认容量 4,malloc 在构造函数中自动调用
Stack st2(100); // 容量 100
}
对比 C 语言的方式:
cpp
// C 语言:必须显式调用 Init,两步操作
ST st;
STInit(&st); // 容易忘记调用
// C++:构造函数自动完成,一步到位
Stack st; // 构造函数自动调用,不会忘记
七、析构函数
7.1 为什么需要析构函数
有创建就有销毁。构造函数负责"申请资源",析构函数负责"释放资源"。
7.2 析构函数的语法
- 函数名:
~类名 - 无参数、无返回值
- 对象生命周期结束时自动调用(不能重载,一个类只有一个析构函数)
- 执行顺序:先创建的后销毁,后创建的先销毁(栈的行为)
cpp
class Stack {
public:
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType) * n);
_capacity = n;
_top = 0;
}
~Stack() {
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
7.3 用 C++ 版 Stack 简化代码
看一个实际对比------括号匹配问题,C 版本 vs C++ 版本:
cpp
// C 版本:需要手动调用 Init 和 Destroy
bool isValid_C(const char* s) {
ST st;
STInit(&st);
while (*s) {
if (*s == '(' || *s == '[' || *s == '{') {
STPush(&st, *s);
} else {
if (STEmpty(&st)) {
STDestroy(&st); // 别忘了清理!
return false;
}
char top = STTop(&st);
STPop(&st);
if ((top == '(' && *s != ')')
|| (top == '[' && *s != ']')
|| (top == '{' && *s != '}')) {
STDestroy(&st); // 别忘了清理!
return false;
}
}
++s;
}
bool ret = STEmpty(&st);
STDestroy(&st); // 别忘了清理!
return ret;
}
// C++ 版本:构造函数和析构函数自动管理资源
bool isValid_Cpp(const char* s) {
Stack st; // 构造自动调用
while (*s) {
if (*s == '(' || *s == '[' || *s == '{') {
st.Push(*s);
} else {
if (st.Empty()) {
return false; // 析构自动调用,无需手动清理!
}
char top = st.Top();
st.Pop();
if ((top == '(' && *s != ')')
|| (top == '[' && *s != ']')
|| (top == '{' && *s != '}')) {
return false; // 析构自动调用!
}
}
++s;
}
return st.Empty(); // 析构自动调用!
}
C++ 版本代码更简洁、更安全------无论函数从哪个 return 退出,析构函数都会自动执行,不会发生资源泄漏。这就是 RAII(Resource Acquisition Is Initialization) 的核心思想。
C++ RAII 方式
构造函数自动 Init
使用中...
析构函数自动 Destroy
✅ 任何 return → 自动析构 → 绝不泄漏
C 语言方式
STInit(&st) ------ 手动初始化
使用中...
STDestroy(&st) ------ 手动清理
❌ 提前 return → 忘记 Destroy → 内存泄漏
7.4 编译器自动生成的析构函数
如果类中没有动态分配的资源(如 malloc/new 出来的指针),编译器生成的析构函数就够了。如 Date 类只有 int 成员,编译器生成的析构函数什么都不用做。
八、运算符重载入门
C++ 允许我们给自定义类型定义运算符的行为,让代码更加直观。
cpp
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// 成员函数形式重载 ==
bool operator==(Date d2) {
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year, _month, _day;
};
int main() {
Date x1(2024, 7, 10);
Date x2(2024, 7, 11);
// 以下两种写法等价:
x1.operator==(x2); // 显式调用
x1 == x2; // 编译器转换为 x1.operator==(x2)
}
运算符重载的本质 是一个名为 operatorX 的特殊函数:
| 你写 | 编译器转换 |
|---|---|
x1 == x2 |
x1.operator==(x2) |
x1 + x2 |
x1.operator+(x2) |
x1 = x2 |
x1.operator=(x2) |
运算符重载不会改变运算符的优先级和结合性。它是语法糖,目的是让自定义类型的使用方式和内置类型一样自然。
篇幅所限,运算符重载的详细讲解留到中篇和下篇。
总结
上篇知识体系总览:
C++类和对象 上篇
类的定义
class vs struct
默认访问权限不同
成员函数 + 成员变量
封装思想
访问限定符
public ------ 外部可访问
private ------ 仅类内可访问
protected ------ 继承中用
实例化
类 = 模板,不占内存
对象 = 实例,占用内存
空类大小为 1 字节
this指针
编译器隐式传递
类型: Date* const
通过 ecx 寄存器
构造函数
对象创建时自动调用
可重载
推荐全缺省形式
析构函数
对象销毁时自动调用
RAII 思想
后创建先销毁
运算符重载入门
本质是 operatorX 函数
语法糖
不改变优先级
本篇我们学习了 C++ 类和对象的基础:
- 类的定义 :
class关键字,成员变量 + 成员函数,分号结尾 - 访问限定符 :
public、private、protected,class 默认 private - 实例化:类只是模板,对象才是真正占用内存的实体
- this 指针:编译器隐式传递给每个非静态成员函数,指向调用该函数的对象
- 构造函数:对象创建时自动调用,用于初始化,可以重载,推荐全缺省形式
- 析构函数:对象销毁时自动调用,用于清理资源,体现 RAII 思想
- 运算符重载入门 :让自定义类型支持运算符,本质是
operatorX函数
下一篇(中篇),我们将深入讲解拷贝构造函数、深拷贝与浅拷贝、赋值运算符重载、比较运算符重载、const 成员函数等重要内容。