【C++】类和对象(一)
对面向过程和面向对象的初步认识
C语言是面向过程的。所谓面向过程,关注的是过程,将一个事件逐步分析,分析出解决问题的步骤,再通过调用对应的函数逐步解决问题
例如洗衣服,面向过程就是下面这样的
而C++是基于面向对象的。关注的是对象,将整个事件拆分为不同对象,靠着对象之间的交互解决问题
这次以洗衣机洗衣服为例。可将事件分为不同对象:人、衣服、洗衣机、洗衣粉
人将洗衣粉和衣服放入洗衣机,启动洗衣机,随后洗衣机自己完成洗衣及甩干。
完成整个事件主要是人、衣服、洗衣机、洗衣粉这几个对象之间的交互,至于洗衣机是如何完成工作的,人不需要关心
这一块儿看的模模糊糊也没关系,在以后的学习中我们会逐渐领会面向对象的思想
类的引入
C语言的结构体中仅可以定义变量,而C++的结构体不仅可以定义变量,还可以定义函数
以下是用C语言实现栈,结构体中仅可以定义变量,而栈的各种函数只能定义在结构体之外
// 栈的结构体
struct Stack
{
int* _data;
int _capacity;
int _size;
};
// 初始化栈
void StackInit(struct Stack* ps, int capacity)
{
ps->_data = (int*)malloc(sizeof(int) * capacity);
ps->_capacity = capacity;
ps->_size = 0;
}
用C++就可以将变量与函数定义在同一结构体中
struct Stack
{
void Init(int capacity)
{
_data = (int*)malloc(sizeof(int) * capacity);
_capacity = capacity;
_size = 0;
}
int* _data;
int _capacity;
int _size;
};
类的定义
定义类与定义结构体类似,不过定义类使用的关键字是class
class ClassName
{
// 类体
}; // 注意分号不要丢了
在类中,类体部分的内容称为类的成员, 类中的变量称为类的属性或成员变量;类中的函数称为类的方法或成员函数
简单定义一个日期:
class Date
{
// 初始化
void Init(int year)
{
year = year;
}
// 成员变量
int year;
int month;
int day;
};
在上面的类中,成员函数Init()
的代码是不是不太好看?无法区分形参与成员变量
为了避免以上情况,一般是给成员变量加个前缀或后缀,如下
class Date
{
// 初始化
void Init(int year)
{
_year = year;
}
// 成员变量
int _year;
int _month;
int _day;
};
类的定义方式
类的定义方式有两种
1.声明和定义都放在类体中
class Date
{
// 初始化
void Init(int year)
{
_year = year;
}
// 成员变量
int _year;
int _month;
int _day;
};
当成员函数写在类体中,可能会被编译器当成内联函数展开
2.声明和定义分离
声明放在头文件.h
中,成员函数的实现放在.cpp
文件中
// Date.h
class Date
{
// 初始化
void Init(int year);
// 成员变量
int _year;
int _month;
int _day;
};
// Date.cpp
#include "Date.h"
void Date::Init(int year) // 成员函数名前需加上类名+域访问限定符::
{
_year = year;
}
一般建议用第二种方法
类的访问限定符与封装
访问限定符
类有三个访问限定符:public
(公有)、private
(私有)、protected
(保护),用来修饰类成员
1.被private
或protected
修饰的成员只能在类中被访问,不能在类外被访问
class Date
{
private:
// 被 private 修饰的类成员
// 初始化
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
这时如果访问类的私有成员
int main()
{
Date d1;
d1.Init(2024, 3, 26);
return 0;
}
将会发生报错
2.被public
修饰的成员可以在类外被访问
class Date
{
public:
// 成员函数可以在类外被访问
// 初始化
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
// 成员变量不可以在类外被访问
int _year;
int _month;
int _day;
};
一般我们不期望成员变量可以在类外被访问,如果要修改成员变量,可以通过被public
修饰的成员函数来改变成员变量
3.访问限定符的作用域
从访问限定符出现开始,直到遇到下一个访问限定符,这就是该访问限定符的作用域;如果直到结束都没有出现下一个限定符,那就作用到类域结束
4.如果类中没有写访问限定符,那么默认权限就是private
;而struct
为了兼容C语言,默认权限是public
封装
所谓封装,就是将成员与方法放在类中,再用访问限定符来规定谁能访问,谁不能访问。本质上是管控
封装的好处:
1.类中的数据不会被外部轻易修改,使数据更加安全;
2.将数据和方法放到类中,易于管理,使用也更加便利
类的作用域
与命名空间类似,类也开辟了一个作用域,类的成员都在类的作用域中
在类外定义成员时,需使用域访问限定符来说明成员属于哪个类
class Date
{
public:
void Init(int year, int month, int day);
private:
int _year;
int _month;
int _day;
};
// Date::指明类域
void Date::Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
类的实例化
使用类类型创建对象的过程,称为类的实例化
// 定义一个类
class Date
{
// 类体
};
Date d1; // 类的实例化
类是用来描述对象的,本身就类似一种概念,定义类不占用空间;而通过类类型实例化出来的对象,则占用物理空间,用来存储成员变量
可以理解为:定义出来的类相当于建房子的图纸,而实例化的类对象就是依据图纸建出来的房子
类对象的存储
上面我们说过,类对象会占用物理空间用来存储成员变量,而我并没有说存储成员函数。至于为什么,我们稍后就清楚了
类对象中可能会有很多不同类型的变量,那又是如何存储的呢?——类对象采用的存储方式与结构体的存储方式相同:内存对齐
内存对齐这里就不再赘述了,如果不清楚可以到网上去了解下,或者看我之前写过的文章:http://t.csdnimg.cn/qtVYb
下面我们来验证一下类对象的存储
class A
{
private:
char _a;
int _b;
int _c;
};
int main()
{
A a1;
cout << sizeof(a1) << endl;
return 0;
}
按照内存对齐的规则,a1
的大小应该是 12
结果如我们所料
这时我们再来看成员函数的存储问题,我们在类中写一个函数,再来看看类对象的大小会如何
class A
{
public:
void Print()
{
cout << "Print" << endl;
}
private:
char _a;
int _b;
int _c;
};
int main()
{
A a1;
cout << sizeof(a1) << endl;
return 0;
}
直接看结果
还是12
,这就说明类对象会存储成员变量,而不会存储成员函数。这是为什么?
我们假设由一个类实例化了多个类对象,每个类对象不仅会存储自己的成员变量,还会存储成员函数
根据需要,每个类对象中的成员变量的具体数据可能会不同,但是成员函数一定是相同的。当不同的类对象都会调用成员函数a,每个类对象都存着成员函数a,每个成员函数a都是相同的,这就有点浪费空间了
所以成员函数与成员变量的存储是分离的,成员函数独立存储于类对象之外。成员函数只需存储一份,不同的类对象需要时就调用同一函数,节约了空间
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2023, 3, 28);
d2.Init(2023, 3, 29);
d1.Print();
d2.Print();
return 0;
}
运行结果:
如上代码中,使用日期类实例化了两个对象,d1
和d2
,随后两个对象分别调用初始化函数和打印函数
那么问题来了,成员函数中并没有对不同对象进行区分,那当d1
调用函数时,函数是如何知道调用者是d1
,而不是d2
呢?——C++中通过引入this指针解决该问题
即:C++编译器给每个“非静态的成员函数“增加了一个隐藏
的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”
的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编
译器自动完成。
意思就是,每个对象调用成员函数时,都会将指向自己的指针传给函数,成员函数也会根据这个指针来对对象进行处理。这个指针对外隐藏,指针的传递和解引用工作不需要用户去做,编译器自己就做了
编译器处理后的代码可以这样理解(部分代码):
class Date
{
public:
void Print(Date* this)
{
cout << this->_year << '/' << this->_month << '/' << this->_day << endl;
}
// ...
};
// ...
d1.Print(&d1);
d2.Print(&d2);
// ...
注意:在实际编程中,无论是形参还是实参,都不可以显示写this指针,但是在成员函数中可以使用,如下
class Date
{
public:
void Print() // 不可以显示写this指针
{
// 函数中可以使用
cout << this->_year << '/' << this->_month << '/' << this->_day << endl;
}
// ...
};
// ...
d1.Print(); // 不可以显示写this指针
d2.Print();
// ...
this指针的特性
1.this指针不可修改,this指针的类型是这样的:类类型* const this
,如:Date* const this
2.this指针是成员函数的第一个参数,只不过是隐藏的
3.this指针本质就是一个形参,类对象不会储存this指针
4.不可以在形参和实参中显示写,在函数中可以使用
小练习
以下代码运行结果: A.编译出错 B.运行崩溃 C.正常运行
class A
{
public:
void Print()
{
cout << "Print" << endl;
}
private:
int _a;
};
int main()
{
A* a = nullptr;
a->Print();
return 0;
}
来看结果:
运行成功,下面来作解释
A首先排除,即便有空指针问题,那也不是语法问题,编译肯定是过得去的
B和C,有人可能会有疑问,a->Print()
这不是对空指针解引用了吗,为什么不崩溃?——我们知道,成员函数并没有存储在对象中,即使这样写了解引用,编译器也不会傻傻的去解引用,而是直接去找成员函数了
下面对代码稍作修改,以下代码运行结果: A.编译出错 B.运行崩溃 C.正常运行
class A
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* a = nullptr;
a->Print();
return 0;
}
ok,直接看结果:
程序崩溃了
编译器在这一步a->Print()
不是不会解引用吗?怎么崩溃了?
其实导致崩溃的是这里,打印成员变量时,对this指针进行了解引用
void Print()
{
cout << _a << endl;
}
this指针是什么?是形参;是谁的形参?是指向对象的指针的形参
而指向对象的指针A* a = nullptr
是空指针,对空指针解引用自然就导致崩溃
结束,再见 😄