
🎬 个人主页 :道尔柯南
❄专栏传送门 :《C语言》《C++》《Linux操作系统》
昙花一现,却等待了整个白昼;蝉鸣一夏,却蛰伏了好几个四季。
文章目录
-
- 一、前言
- 二、类的定义
-
- [2.1 什么是类?](#2.1 什么是类?)
- [2.2 使用 struct 定义类](#2.2 使用 struct 定义类)
- 三、访问限定符:实现类的封装
-
- [3.1 为什么需要访问限定符?](#3.1 为什么需要访问限定符?)
- [3.2 三种访问限定符](#3.2 三种访问限定符)
- [3.3 class 与 struct 的默认访问权限](#3.3 class 与 struct 的默认访问权限)
- 四、类域与成员函数的类外定义
-
- [4.1 什么是类域?](#4.1 什么是类域?)
- [4.2 为什么要使用类域?](#4.2 为什么要使用类域?)
- 五、类的实例化
-
- [5.1 类与对象的区别](#5.1 类与对象的区别)
- [5.2 如何创建对象?](#5.2 如何创建对象?)
- [5.3 实例化的本质](#5.3 实例化的本质)
- 六、对象的大小与内存对齐
-
- [6.1 对象中存储了什么?](#6.1 对象中存储了什么?)
- [6.2 内存对齐规则](#6.2 内存对齐规则)
- [6.3 没有成员变量的类为什么也有大小?](#6.3 没有成员变量的类为什么也有大小?)
- [6.4 如何验证对象大小?](#6.4 如何验证对象大小?)
- [七、this 指针:成员函数如何区分不同对象?](#七、this 指针:成员函数如何区分不同对象?)
-
- [7.1 为什么需要 this 指针?](#7.1 为什么需要 this 指针?)
- [7.2 this 指针的工作原理](#7.2 this 指针的工作原理)
- [7.3 this 指针的特点](#7.3 this 指针的特点)
- [八、关于 this 指针的两个思考题](#八、关于 this 指针的两个思考题)
-
- [8.1 空指针调用成员函数,一定会崩溃吗?](#8.1 空指针调用成员函数,一定会崩溃吗?)
- [8.2 如果成员函数访问成员变量呢?](#8.2 如果成员函数访问成员变量呢?)
- [九、C 语言与 C++ 实现栈的区别](#九、C 语言与 C++ 实现栈的区别)
-
- [9.1 C 语言实现栈](#9.1 C 语言实现栈)
- [9.2 C++ 语言实现栈](#9.2 C++ 语言实现栈)
- [9.3 C 与 C++ 实现方式对比](#9.3 C 与 C++ 实现方式对比)
- [十、总结:掌握 C++ 类与对象的核心知识](#十、总结:掌握 C++ 类与对象的核心知识)
-
- [10.1 核心知识点回顾](#10.1 核心知识点回顾)
- [10.2 学习建议](#10.2 学习建议)
- 结尾
一、前言
在学习 C++ 的过程中,我们会逐渐接触到面向对象编程的思想。与 C 语言主要通过结构体和函数组织程序不同,C++ 提供了类(Class)这一重要机制,可以将数据和操作数据的函数组织在一起,从而让程序结构更加清晰,也更便于维护。
例如,我们之前使用 C 语言实现一个栈,需要定义栈的结构体,再单独编写初始化、入栈、出栈和销毁等函数。调用这些函数时,还需要显式传入栈对象的地址。
而在 C++ 中,我们可以把栈的数据和操作封装到一个类中,通过对象直接调用对应的成员函数。
本篇文章将围绕 C++ 类和对象的基础知识展开,帮助大家理解面向对象编程的基本概念,为后续学习构造函数、继承、多态以及 STL 打下基础。
二、类的定义
2.1 什么是类?
类是对一类事物共同特征和行为的抽象描述。
例如,一个日期对象通常包含年、月、日三个属性,同时具备初始化日期、打印日期等行为。我们可以使用一个 Date 类来描述这些共同特征。
在 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;
};
定义一个类时,需要注意以下几点:
class是定义类的关键字。Date是类名。{}内部是类体,包含成员变量和成员函数。- 类定义结束时,右花括号后面的分号不能省略。
类中的变量称为成员变量 ,也叫属性;类中的函数称为成员函数,也叫方法。
为了区分成员变量与函数中的普通变量,实际开发中经常使用 _year、_month 或 m_year 等命名方式。这属于命名习惯,并不是 C++ 强制规定。
2.2 使用 struct 定义类
在 C++ 中,struct 不仅可以像 C 语言一样定义结构体,还可以包含成员函数。
例如:
struct ListNode
{
void Init(int x)
{
next = nullptr;
val = x;
}
ListNode* next;
int val;
};
这里的 ListNode 不仅包含数据成员 next 和 val,还包含初始化函数 Init()。
与 C 语言不同,C++ 中可以直接使用 ListNode 作为类型名,不需要额外通过 typedef 为结构体类型起别名。
虽然 struct 和 class 都可以定义类,但在实际开发中,通常推荐使用 class 表达面向对象设计的意图。
三、访问限定符:实现类的封装
3.1 为什么需要访问限定符?
假设我们定义一个日期类:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
如果允许外部代码随意修改日期对象中的数据,就可能出现不合理的状态。
例如,直接把月份修改为一个不合法的值,就会给后续程序带来问题。
因此,C++ 提供了访问限定符,用来控制类成员可以被哪些代码访问。
3.2 三种访问限定符
C++ 中主要有三种访问限定符:
public:公有成员,可以在类外通过对象直接访问。protected:保护成员,不能在类外直接访问,具体与private的区别主要体现在继承中。private:私有成员,不能在类外直接访问。
示例:
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 d;
d.Init(2026, 10, 10);
// d._year = 2025; // 编译错误
return 0;
}
这里,外部代码可以调用 Init(),但不能直接访问私有成员 _year。
这体现了面向对象编程中的一个重要特性:封装。
封装将数据和操作数据的方法组织在一起,并通过访问权限限制外部对内部数据的直接访问,从而降低程序出现错误的风险。
3.3 class 与 struct 的默认访问权限
两者有一个需要特别注意的区别:
class中的成员默认访问权限是private。struct中的成员默认访问权限是public。
例如:
class A
{
int x; // 默认 private
};
struct B
{
int x; // 默认 public
};
此外,访问权限的作用范围从对应的访问限定符开始,一直到下一个访问限定符出现,或者类定义结束。
实际开发中,一般将需要提供给外部使用的接口放在 public 区域,将内部数据放在 private 区域。
四、类域与成员函数的类外定义
4.1 什么是类域?
每个类都定义了自己的作用域,类中的成员属于该类的类域。
当我们在类外定义成员函数时,需要使用作用域解析运算符 ::,明确指出该函数属于哪个类。
例如:
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("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
在上面的代码中:
void Stack::Init(int n)
表示 Init() 是 Stack 类的成员函数。
如果省略 Stack::,编译器就无法将这个类外定义识别为 Stack 的成员函数定义。
4.2 为什么要使用类域?
当类的成员函数比较简单时,可以直接在类内部定义。
但是,当一个类包含较多函数时,将声明和定义分离,可以让类的结构更加清晰。
例如:
class Date
{
public:
void Init(int year, int month, int day);
void Print();
private:
int _year;
int _month;
int _day;
};
void Date::Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << "/"
<< _month << "/"
<< _day << endl;
}
这种写法将类的接口声明与具体实现分离,方便后续维护。
需要注意,直接定义在类体内的成员函数通常隐式具有 inline 属性。不过,inline 并不意味着编译器一定会将函数展开,而是与函数的定义规则有关。
五、类的实例化
5.1 类与对象的区别
理解类和对象之间的关系,是学习面向对象编程的关键。
可以将类比作建筑设计图,将对象比作按照设计图建造出来的实际房屋。
设计图规定了房屋应该具有哪些结构,但设计图本身并不是一栋可以居住的房子。
同样,类描述了对象应该具备哪些成员,而对象才是实际创建出来、能够存储成员变量数据的实体。
类是抽象描述,对象是具体实例。
5.2 如何创建对象?
以日期类为例:
#include <iostream>
using namespace std;
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;
};
int main()
{
Date d1;
Date d2;
d1.Init(2026, 10, 10);
d2.Init(2026, 12, 25);
d1.Print();
d2.Print();
return 0;
}
运行结果:
2026/10/10
2026/12/25
这里,Date 是类,d1 和 d2 是通过 Date 类实例化得到的两个对象。
两个对象分别拥有自己的成员变量,因此可以保存不同的日期数据。
5.3 实例化的本质
在类定义中,成员变量只是描述对象应该具备哪些数据成员。通过类创建对象时,才会为对象分配相应的存储空间。
一个类可以实例化出多个对象,每个对象都可以保存各自独立的数据。
需要注意的是,这并不意味着类的所有相关内容都必须存放在对象内部。普通成员函数的代码通常存储在程序的代码区域中,而不是为每个对象重复保存一份。
这就引出了下一个问题:一个类对象究竟占用多少内存?
六、对象的大小与内存对齐
6.1 对象中存储了什么?
考虑下面的代码:
class A
{
public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
对象中包含成员变量 _ch 和 _i。
那么,成员函数 Print() 是否也会占用对象内部的空间呢?
通常情况下,普通成员函数的机器代码不会被复制到每一个对象中。多个对象可以调用同一份成员函数代码,因此不需要在每个对象内部重复保存成员函数。
所以,计算普通类对象大小时,重点是成员变量以及相关的内存对齐和填充空间。
6.2 内存对齐规则
C++ 对象的布局需要满足相应的对齐要求。常见内存对齐规则如下:
- 第一个成员变量从对象起始位置开始存储。
- 其他成员变量需要放置在满足其对齐要求的位置。
- 对齐要求通常取成员类型自身的对齐要求与编译器默认对齐参数中的较小值。
- 对象的整体大小需要满足整体对齐要求。
- 对于包含其他结构体或类类型成员的情况,还需要考虑嵌套类型的对齐要求。
以常见的编译环境为例:
class A
{
private:
char _ch;
int _i;
};
假设 char 占用 1 字节,int 占用 4 字节,且采用常见的 4 字节对齐方式,则对象布局可能如下:
偏移量 0:_ch,占用 1 字节
偏移量 1:填充空间,占用 3 字节
偏移量 4:_i,占用 4 字节
对象总大小:8 字节
如果没有填充空间,_i 的起始位置就可能不满足当前平台要求的对齐条件。
需要注意,具体结果取决于编译器、平台以及类型的对齐要求,不能将某一种环境下的结果直接当作所有环境的固定结论。
6.3 没有成员变量的类为什么也有大小?
例如:
class Empty
{
};
int main()
{
Empty e;
cout << sizeof(e) << endl;
return 0;
}
在常见的 C++ 实现中,输出通常是:
1
空类对象通常至少占用 1 字节,以便不同的独立对象能够具有可区分的地址。
因此,即使类中没有成员变量,对象也不一定占用 0 字节。
6.4 如何验证对象大小?
可以通过 sizeof 运算符查看对象大小:
cout << sizeof(A) << endl;
cout << sizeof(Empty) << endl;
学习对象大小时,不仅要记住常见结果,更重要的是理解成员变量、内存对齐和填充空间之间的关系。
七、this 指针:成员函数如何区分不同对象?
7.1 为什么需要 this 指针?
前面我们创建了两个日期对象:
Date d1;
Date d2;
d1.Init(2026, 10, 10);
d2.Init(2026, 12, 25);
两个对象调用的是同一个 Init() 成员函数,但为什么第一次调用修改的是 d1 的数据,第二次调用修改的却是 d2 的数据?
原因就在于 C++ 提供了一个隐含的 this 指针。
this 指针指向当前正在调用成员函数的对象。通过它,成员函数能够访问当前对象中的成员变量。
7.2 this 指针的工作原理
例如:
class Date
{
public:
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
};
这里的:
this->_year = year;
表示将参数 year 的值赋给当前对象的成员变量 _year。
当调用:
d1.Init(2026, 10, 10);
时,可以将其理解为成员函数获得了指向 d1 的隐含对象指针。
当调用:
d2.Init(2026, 12, 25);
时,这个指针则指向 d2。
从概念上看,可以将成员函数的调用理解为:
// 概念性示意,并非可以直接这样调用的实际语法
Init(&d1, 2026, 10, 10);
Init(&d2, 2026, 12, 25);
实际上,C++ 编译器会按照语言规则处理成员函数调用,程序员不需要手动传递 this 指针。
7.3 this 指针的特点
理解 this 指针时,需要掌握以下几个重点:
this指针是成员函数隐含使用的对象指针。- 普通非静态成员函数可以通过
this访问当前对象的成员。 this指针不能在函数的显式形参列表中由程序员自行声明传入。- 在成员函数体内,可以显式使用
this。 this本身不能被重新赋值为另一个地址。
例如:
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
// this = nullptr; // 错误:不能给 this 重新赋值
}
需要特别注意,this 指针并不是存放在对象内部的一个普通成员变量。对于常见的非静态成员函数调用,它是用于标识当前对象的隐含机制。
八、关于 this 指针的两个思考题
课件通过两个代码示例,帮助我们理解 this 指针与对象访问之间的关系。
8.1 空指针调用成员函数,一定会崩溃吗?
考虑下面的代码:
#include <iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
从代码形式来看,p 是一个空指针,但程序却通过它调用了 Print()。
这里需要区分语法形式和实际行为:通过空指针访问非静态成员函数,并不意味着可以安全地调用该函数。
在 C++ 中,如果这样的调用不满足语言规定的对象访问要求,程序行为就是未定义的。不能因为某个编译器在某次运行中输出了结果,就认为这种写法是安全的。
在这个例子中,Print() 没有访问任何对象成员,因此某些编译环境可能仍然输出:
A::Print()
但这不构成程序正确性的保证。
8.2 如果成员函数访问成员变量呢?
再看下面的代码:
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
与上一个示例相比,这里的 Print() 还访问了成员变量 _a。
访问 _a 需要对应的对象存储空间,而空指针并没有指向一个有效的 A 对象。因此,这种调用不能被视为合法、安全的对象访问,程序行为是未定义的。
学习这两个例子,重点不是记住某一种运行结果,而是理解:
- 成员函数代码通常与对象数据分开存储。
- 成员函数需要访问对象数据时,必须有有效的对象。
- 不应该通过空指针调用非静态成员函数来依赖特定编译器的行为。
九、C 语言与 C++ 实现栈的区别
前面介绍了类、对象和 this 指针,接下来通过栈的实现,进一步理解 C++ 面向对象编程的优势。
9.1 C 语言实现栈
在 C 语言中,通常需要先定义一个结构体:
typedef struct Stack
{
int* a;
int top;
int capacity;
} ST;
然后在结构体之外定义操作函数:
void STInit(ST* ps);
void STPush(ST* ps, int x);
void STPop(ST* ps);
int STTop(ST* ps);
void STDestroy(ST* ps);
调用时,需要将栈对象的地址传递给相应函数:
ST s;
STInit(&s);
STPush(&s, 10);
STPush(&s, 20);
printf("%d\n", STTop(&s));
STDestroy(&s);
这种方式能够完成栈的基本功能,但数据结构与操作函数是分开组织的,调用时也需要显式传入对象地址。
9.2 C++ 语言实现栈
在 C++ 中,可以将栈的数据和操作封装到一个类中:
#include <iostream>
#include <cstdlib>
#include <cassert>
using namespace std;
class Stack
{
public:
void Init(int n = 4)
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
void Push(int x)
{
if (_top == _capacity)
{
int newcapacity = _capacity == 0
? 4 : _capacity * 2;
int* tmp = (int*)realloc(
_a, sizeof(int) * newcapacity);
if (tmp == nullptr)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
bool Empty()
{
return _top == 0;
}
int Top()
{
assert(_top > 0);
return _a[_top - 1];
}
void Pop()
{
assert(_top > 0);
--_top;
}
void Destroy()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a = nullptr;
size_t _capacity = 0;
size_t _top = 0;
};
int main()
{
Stack s;
s.Init();
s.Push(10);
s.Push(20);
s.Push(30);
while (!s.Empty())
{
cout << s.Top() << endl;
s.Pop();
}
s.Destroy();
return 0;
}
运行结果:
30
20
10
这里使用 private 保护栈的内部数据,并通过 public 提供入栈、出栈、判空和取栈顶等接口。
调用成员函数时,不需要像 C 语言那样显式传入对象地址:
s.Push(10);
s.Push(20);
编译器会通过成员函数调用机制处理当前对象。
需要说明的是,这个示例主要用于展示类的基本用法。实际工程开发中,还需要进一步考虑初始化失败后的对象状态、资源释放以及拷贝控制等问题。后续学习构造函数、析构函数和资源管理时,可以进一步改进这个栈。
9.3 C 与 C++ 实现方式对比
| 对比维度 | C 语言 | C++ |
|---|---|---|
| 数据组织 | 通常使用结构体 | 使用类组织数据 |
| 函数组织 | 函数通常定义在结构体外 | 成员函数可以定义在类内或类外 |
| 访问控制 | 需要依靠接口约定等方式管理 | 可以使用 public、private 等限定符 |
| 调用方式 | 通常显式传入对象地址 | 通过对象调用成员函数 |
| 对象指针处理 | 常见操作需要显式传递指针 | 非静态成员函数具有隐含的 this 机制 |
| 类型使用 | 结构体类型常结合 typedef 使用 |
可以直接使用类名作为类型名 |
从底层数据结构和栈的基本操作来看,两种实现的核心逻辑并没有发生本质变化。C++ 的优势主要体现在组织代码、管理访问权限以及表达对象行为等方面。
十、总结:掌握 C++ 类与对象的核心知识
通过本篇文章,我们学习了 C++ 面向对象编程的基础内容。
10.1 核心知识点回顾
- 类的定义 :使用
class或struct定义类型,将成员变量与成员函数组织在一起。 - 访问限定符 :使用
public、protected和private控制成员访问权限。 - 类域 :通过
类名::成员函数名指明类外定义的成员函数所属的类。 - 对象实例化:通过类创建对象,对象拥有各自独立的成员变量数据。
- 对象大小:主要考虑成员变量、内存对齐及填充空间等因素。
- this 指针:帮助成员函数识别当前调用对象,并访问该对象的成员。
- 封装思想:将数据与操作组织在一起,通过明确的接口管理外部访问。
- C 与 C++ 的区别:C++ 通过类提供了更加统一的对象组织和访问控制机制。
10.2 学习建议
学习这些内容时,不建议只记忆概念和结论,而应该多动手编写小程序。
可以尝试完成以下练习:
- 编写一个
Date类,实现日期初始化和打印。 - 创建两个对象,观察它们的数据是否相互独立。
- 修改成员变量的访问权限,观察编译结果。
- 调整类中成员变量的顺序,比较
sizeof的结果。 - 在成员函数中分别使用和不使用
this,理解两种写法的关系。 - 对比 C 与 C++ 的栈实现,理解封装带来的变化。
当这些基础知识掌握扎实之后,再继续学习构造函数、析构函数、拷贝控制以及继承和多态,就会更加容易理解。
C++ 面向对象编程的关键,不仅是学会如何定义一个类,更是理解如何通过类组织数据、管理访问权限,并建立清晰、可靠的程序结构。
结尾
uu们,本文的内容到这里就全部结束了,道尔在这里再次感谢您的阅读!
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 道尔柯南 C/C++ & Linux 底层探索者 | 一个正在努力学习的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日目标达成!" |
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!