C++:类和对象初识
前言
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
以我们洗衣服为例:
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
在面向对象编程(OOP)中,"类"和"对象"是最核心的概念。C++作为一门面向对象的语言,通过类和对象实现了数据抽象、封装、继承和多态等特性。理解类和对象的工作机制是掌握C++面向对象编程的关键。
类的引入与定义
引入
在C语言中,我们使用结构体(struct)来组织相关数据。C++在此基础上进行了扩展,允许结构体中不仅包含数据成员,还可以包含函数成员:
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义函数。
//C++兼容C语言,同时C++中struct升级成了 类(具有类的所有特性)
struct _Stack {
//成员方法
void Init(int defaultCapacity = 4) {
base = (int*)malloc(sizeof(int) * defaultCapacity);
if (base == nullptr) {
perror("malloc failed\n");
return;
}
this->size = 0;
this->capacity = defaultCapacity;
}
void Push() {
//......
}
void Destroy() {
free(this->base);
this->base = nullptr;
this->top = this->capacity;
}
//成员变量
int* base;
int top;
int capacity;
int size;
};
C++中结构体的名字,可以当成类名来使用。C++中的结构体具有class的所有功能(包括但不限于权限管理与this指针),只是成员的默认权限不同。
- 结构体中,
所有成员的默认访问权限是public
,结构体外可以直接访问 - C++的类中,
class所有成员的默认访问权限是private
, 不能再类外访问。
但更规范的C++做法是使用class关键字来定义类,它提供了更完善的访问控制机制。
定义
类定义的基本语法:
class className{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
- class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
- 类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
示例:
class Clock {
private: // 访问限定符
int hour;
int minute;
int second;
public:
void setTime(int h, int m, int s);
void showTime() {
cout << hour << ":" << minute << ":" << second << endl;
}
};
类的两种定义方法
1. 声明和定义全部放在类体中
例如该类:
class Clock {
private: // 访问限定符
int hour;
int minute;
int second;
public:
void showTime() {
cout << hour << ":" << minute << ":" << second << endl;
}
};
- 声明和定义全部放在类体中,需注意:成员函数如果在类体中定义,编译器可能会将其当成内联函数处理。(相当于在函数前加上了
inline
关键字,建议编译器使其称为内联函数)
2. 声明和定义分离式
//Stack.h
class Stack {
public:
void Init(int defaultCapacity = 4);
//在类内定义的函数,会直接建议编译器让该函数称为内联函数,
void Push() { //类内定义的函数,不管加不加 inline ,都相当于加上了 inline
//.......此处省略
}
void Pop() {
//.......
}
private:
int* base;
int top;
int capacity;
};
//Stack.cpp
//类的声明和定义分离,需要在函数名前面,加上类的作用域限定
void Stack:: Init(int defaultCapacity) { //缺省参数一般在 函数声明 给出
base = (int*)malloc(sizeof(int) * defaultCapacity);
capacity = defaultCapacity;
top = 0;
}
需要注意的是:
声明定义分离式
,如果函数有默认参数,一般要在函数声明处给出类的默认参数
一般情况下,更期望采用第二种方式, 因为我们并不希望所有的函数都称为内联函数。
类的成员变量命名规则
我们看该类:
class Clock {
private: // 访问限定符
int hour;
int minute;
int second;
public:
void setTime(int hour, int minute, int second){
// 这里的hour到底是成员变量,还是函数形参?
hour = hour;
}
};
我们的疑问如注释中所写,为了避免这样的矛盾,我们通常这样定义类的成员变量。
private: // 访问限定符
int _hour;
int _minute;
int _second;
public:
void setTime(int hour, int minute, int second){
// 这里的hour到底是成员变量,还是函数形参?
_hour = hour;
//这样就解决了分歧,避免了二义性。
}
类的访问限定符及封装
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
访问限定符
C++通过三个访问限定符实现封装:
public
:公有成员,类内外均可访问private
:私有成员,仅类内和友元可访问protected
:保护成员,介于两者之间(继承时使用)
特点:
public
修饰的成员在类外可以直接被访问。protected
和private
修饰的成员在类外不能直接被访问(此处protected和private是类似的)。- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
- 如果后面没有访问限定符,作用域就到}即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C语言)。
- C++需要兼容C语言,所以C++中struct可以当成结构体使用。
- 另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。
- 注意:在继承和模板参数列表位置,struct和class也有区别,后序给大家介绍。
通常建议我们不要采用C++语法中的默认权限,不管是class还是struct,我们都应该手动控制访问权限。
封装
面向对象的三大特性:封装、继承、多态
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装是指将数据和操作数据的方法进行有机结合,并对外部隐藏实现细节。其优势体现在:
- 数据保护:通过私有化成员变量防止意外修改
- 接口统一:通过公有方法提供规范的操作方式
- 实现隔离:修改内部实现不影响外部使用
示例封装:
class BankAccount {
private:
double balance; // 私有数据
public:
// 公开接口
void deposit(double amount) {
if(amount > 0) balance += amount;
}
bool withdraw(double amount) {
if(amount <= balance) {
balance -= amount;
return true;
}
return false;
}
double getBalance() const {
return balance;
}
};
类的作用域与实例化
类的作用域
类定义了一个独立的作用域:
- 成员变量/函数的作用域在整个类体内(整个类内是一个整体)
- 外部访问需通过类名::成员或对象.成员
注意点:
- 如上文所讲,成员函数在类外定义时需要指定类域
void Clock::setTime(int h, int m, int s) {
// 实现代码
}
我们可以对比一下各种作用域各有什么特点。
全局域
局部域
类作用域
命名空间域(使用时需要指定)
局部域和全局域会影响变量的生命周期, 类域和命名空间域不会影响生命周期
类实例化
用类类型创建对象的过程,称为类的实例化
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
int main(){
person._age = 100; // 编译失败:error C2059: 语法错误
return 0;
}
Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄。
做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
实例化方式:
Clock myClock; // 栈上分配
Clock* pClock = new Clock; // 堆上分配
new关键词我们将在后续讲解。
需要注意:
- 类声明不分配内存,实例化时才分配实际空间
- 同一类的不同对象拥有独立的成员变量存储空间
- 成员函数被所有对象共享(代码区存储)
类对象模型
类对象的大小
计算规则:
- 遵循结构体内存对齐原则
- 只计算成员变量大小(包括继承的)
- 空类大小为1字节(占位标识)
验证示例:
class Empty {};
class Data {
int num; // 4字节
double value; // 8字节
char tag; // 1字节
};// 8 + 8 + 1 = 17 → vs下实际输出24(内存对齐)
int main() {
cout << sizeof(Empty) << endl;; // 输出1
cout << sizeof(Data) << endl;; // 输出24
}
存储方式
类对象的存储采用分治策略:
- 成员变量:每个对象独立存储(栈区/堆区)
- 成员函数:所有对象共享代码区的一份拷贝
- 静态成员:存储在全局数据区
如图所示
这个模型在我们计算对象的大小时也有体现
:
- 对象中只存储成员变量。
- 成员函数存放在一个公共区域(成员函数不在对象内存储)
内存布局示例:
对象1: [成员变量区]
对象2: [成员变量区]
...
代码区: [成员函数]
结论:
- 一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐。
- 注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
结构体内存对齐规则
//结构体内存对齐规则
//1. 第一个成员在与结构体偏移量为0的地址处。
//2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
//注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
//VS中默认的对齐数为8
//3. 结构体总大小为:最大对齐数(所有变量类型最大者 与 默认对齐参数 取较小的那个)的整数倍。
//4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
//体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
this指针(重点)
引出
我们先定义一个日期类
class Date{
public:
void Init(int year, int month, int day){
_year = year;
_month = month;
_day = day;
}
void Print(){
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
int main(){
Date d1, d2;
d1.Init(2025,1,11);
d2.Init(2024, 2, 22);
d1.Print();
d2.Print();
return 0;
}
对于上述类,有这样的一个问题:
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
编译器隐式添加this指针参数:
// 编译器视角下的成员函数
//编译器编译后,会对成员函数进行处理,会给成员函数加上参数this, 访问变量时,使用this访问
void Init(Date* const this, int year, int month, int day){
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print(Date* const this){
cout <<this->_year<< "-" <<this->_month << "-"<< this->_day <<endl;
}
int main(){
Date d1, d2;
//编译器视角下的函数调用,是编译器帮助我们传入的当前对象的地址
d1.Init(&d1, 2025, 1, 11);
d2.Init(&d2, 2024, 2, 22);
d1.Print(&d1);
d2.Print(&d2);
return 0;
}
特性
- this指针的类型:
className* const
,const修饰指针本身,该指针不能被修改,也就是不能当左值。即成员函数中,不能给this指针赋值。 - 只能在“成员函数”的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。正因如此,上文计算对象大小的时候并没有计算this指针。
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
- this指针在函数内部是要反复调用的, vs的编译器对this指针传递做了优化,对象地址放在ecx寄存器中,exc中存储this指针的值
- this指针是函数的形参,因此this指针存放在内存的栈区中
深入理解
思考题?(重点中的重点)
有如下类和代码,思考?
class TestThis {
public:
void Print() {
cout << "Print" << endl;
}
void PrintA() {
cout << _A << endl;
}
private:
int _A;
};
void Test_This_1() {
TestThis* p = nullptr;
p->Print();
}
void Test_This_2() {
TestThis* p = nullptr;
p->PrintA();
}
int main(){
Test_This_1();
//Test_This_2();
return 0;
}
思考两个函数调用的结果分别是什么?
1.编译时报错 2. 运行时崩溃 3. 正常运行
我们依次调用来看:
解答情形1
我们可以看到,中断打印Print, 并且提示:进程已退出,代码为0
,0代表正常返回,可以看到,第一种情况的结果是:正常运行。
解答情形2
观察两张图可看到,图一,光标一直在闪动,图二,程序结束时,退出代码为**-1073741819**,光标一直在闪动且退出代码为负数,显然是运行时崩溃。
原因分析
class TestThis {
public:
void Print() {
cout << Print() << endl;
}
void PrintA() {
cout << _A << endl;
}
private:
int _A;
};
先看该类,挺简单的。两个成员函数:
Print()
,打印字符串“Print”
PrintA()
,打印成员变量_A的值
再看两个调用:
void Test_This_1() {
TestThis* p = nullptr;
p->Print();
}
void Test_This_2() {
TestThis* p = nullptr;
p->PrintA();
}
说白了讲,p是TestThis对象的空指针,通过指针p,分别调用
Print()
和PrintA()
函数
class TestThis {
public:
void Print() {
//this指针为空,但函数内没有对this指针解引用
cout << Print() << endl;
}
void PrintA() {
//this指针为空,但函数内访问_A,本质是this->_A
//成员变量在对象内,因此发生了this指针的解引用。
cout << _A << endl;
}
private:
int _A;
};
void Test_This_1() {
TestThis* p = nullptr;//表示this指针为空
p->Print();
}
void Test_This_2() {
TestThis* p = nullptr;//表示this指针为空
p->PrintA();
}
两种情形下,this指针都是空的。
p->Print();
,p调用Print, 不会发生解引用。因为,由上文得,Print的地址并不在对象中,p会作为实参传递给this指针,并没有发生空指针的解引用。p->PrintA();
,p调用PrintA, 不会发生解引用。但PrintA()
函数内,有cout << _A << endl;
,本质上是cout << this->_A << endl;
,变量_A
存储在对象内,因此需要去对象中找,也就发生了对象指针的解引用。此时对象指针为空,那么对空指针解引用,也就发生了运行时崩溃。
总结
- 类与对象关系:类是蓝图,对象是实体
- 访问控制:通过public/private/protected实现封装
- 存储模型:对象独立存储数据,共享函数代码
- this机制:隐式指针实现对象自治
- 设计原则:高内聚低耦合,合理使用访问限定符
理解类和对象的工作机制是掌握C++面向对象编程的基础,后续的继承、多态等特性都是建立在此基础之上的深入扩展。正确使用类和对象可以有效提高代码的可维护性和复用性。
今天的分享到此结束了,各位大佬多多支持。
一键三连,好运连连!