类是 C++ 的核心特性,一种用户自定义的数据类型,它是一种封装了数据和函数的组合。
类可以被看作是一种模板,可以用来创建具有相同属性和行为的多个对象。
C++面向对象的三大特性为:封装、继承、多态。
C++认为万事万物都皆为对象,对象上有其属性和行为。
例如:
? 人可以作为对象,属性有姓名、年龄、身高、体重…,行为有走、跑、跳、吃饭、唱歌。
? 车也可以作为对象,属性有轮胎、方向盘、车灯…,行为有载人、放音乐、放空调。
? 具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类。
封装是C++面向对象三大特性之一
封装的意义:
class 类名{ 访问权限: 属性 / 行为 };
const double PI = 3.14;
//设计一个圆类,求圆的周长
//圆求周长的公式:2 * PI * 半径
//class 代表设计一个类,类后面紧跟着的就是类名称
class Circle
{
//访问权限 公共权限
public:
//属性
int m_r; //半径
//行为
double calculateZC(){ //获取圆的周长
return 2 * PI * m_r;
}
};
int main()
{
//通过圆类 创建具体的圆(对象)
//实例化对象c1 (通过一个类 创建一个对象的过程)
//c1类比于人类的对象如:小红、小张、小明等
Circle c1;
//给圆对象的属性进行赋值
c1.m_r = 10;
cout << "圆的周长为: " << c1.calculateZC() << endl;
system("pause");
return 0;
}
类中的属性和行为 统一称为成员 成员 = 属性 + 行为
属性即为 成员属性或者成员变量
行为即为 成员函数或者成员方法
设计学生类
一共分为三种
公共权限 public 成员 类内可以访问 类外可以访问
保护权限 protected 成员 类内可以访问 类外不可以访问 儿子可以访问父亲的保护内容
私有权限 private 成员 类内可以访问 类外不可以访问 儿子可以不访问父亲的私有内容
儿子和父亲后续会描述的继承特性
class Person
{
public:
//公共权限
string m_Name; //姓名
protected:
//保护权限
string m_Car; //汽车
private:
//私有权限
int m_Password; //银行卡
public:
void func(){
m_Name = "张三";
m_Car = "拖拉机";
m_Password = 123456;
}
};
Person p1;
p1.m_Name = "李四";
//p1.m_Car = "奔驰"; //保护权限内容,类外无法访问
//p1.m_Password = 444444; //私有权限内容,类外无法访问
在C++中 struct和class唯一的区别就在于默认的访问权限不同。
struct默认权限为公有,class默认权限为私有。
class C1
{
int m_A; //class 默认权限是私有的
};
struct C2
{
int m_A; //struct 默认权限是公有的
};
C1 c1;
//c1.m_A = 100; //在class里默认权限是私有,因此类外不可以访问
C2 c2;
c2.m_A = 100; //在struct默认的权限为公共,因此可以访问
//成员属性设置为私有
//1、可以自己控制读写权限
//2、对于写权限,可以检测数据的有效性
//设计人类
class Person
{
public:
//设置姓名
void setName(string name){
m_Name = name;
}
//获取姓名
string getName(){
return m_Name;
}
//获取年龄 只读/可读可写 如果想修改(年龄范围必须为0~150)
int getAge(){
m_Age = 20;
return m_Age;
}
// //设置年龄
// void setAge(int age){
// if (age < 0 || age > 150){
// cout << "输入有误" << endl;
// m_Age = 0;
// return;
// }
// m_Age = age;
// }
//设置情人 只写
void setLover(string lover) {
m_Lover = lover;
cout << "情人: " << m_Lover << endl;
}
private: //属性设置私有 利于封装 只提供接口 不要改变其值
//姓名 可读可写
string m_Name;
//年龄 只读
int m_Age;
//情人 只写
string m_Lover;
};
int main()
{
Person p;
//p.m_Name = "张三" 私有权限,无法访问 被禁止访问p.m_Name
p.setName("张三");
cout << "姓名: " << p.getName() << endl;
//p.setAge(20);
//只读
cout << "年龄: " << p.getAge() << endl;
//只写
p.setLover("小米");
system("pause");
return 0;
}
设计立方体类(Cube),求出立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等。
设计一个圆形类(Circle),和一个点类(Point),计算点和圆的关系。
分文件编写,共有5个文件,即circle.h、circle.c、point.h、point.c、main.c
点和圆的关系
c++利用构造函数和析构函数解决对象的初始化和清理两个非常重要的安全问题,两个函数会被编译器自动调用。
如果我们不提供构造和析构,编译器提供的构造函数和析构函数是空实现。
创建对象时
为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。1、构造函数
作用:进行初始化操作,
特点:①没有返回值 不用写void ②函数名与类名相同
③构造函数可以有参数,可以发生重载 ④创建对象的时候,构造函数会自动调用,而且只调用一次
2、析构函数
作用:进行清理的操作
特点:①没有返回值 不用写void ②函数名 与类名相同 在名称前加 ~
③析构函数不可以有参数,不可以发生重载 ④对象在销毁前 会自动调用析构函数,而且只调用一次
//对象的初始化和清理
class Person
{
public:
//构造函数
Person(){
cout << "Person的构造函数调用" << endl;
}
//析构函数
~Person(){
cout << "Person的析构函数调用" << endl;
}
};
//构造和析构都是必须有的实现,如果我们自己不提供,编译器会提供一个空实现
void test01()
{
Person p; //这是在栈上的数据,test01执行完毕后,就释放这个对象
//对象销毁前,自动调用析构函数
}
int main()
{
test01();
}
构造函数的分类及调用
按照参数分类 无参构造 有参构造
按照类型分类 普通构造 拷贝构造
即有三种构造函数:①无参(默认)构造函数 ②有参构造函数 ③拷贝构造函数
class Person
{
public:
//无参(默认)构造函数 函数重载
Person(){
cout << "Person的无参构造函数调用" << endl;
}
//有参构造函数
Person(int a){
m_Age = a;
cout << "Person的有参构造函数调用" << endl;
}
//拷贝构造函数
Person( const Person &p){
//将传入的人身上的所以属性,拷贝到当前的对象上
m_Age = p.m_Age;
cout << "Person的拷贝构造函数调用" << endl;
}
~Person(){
cout << "Person的析构函数调用" << endl;
}
int m_Age;
};
//调用
void test01()
{
//1、括号法 常用
Person p1; //默认构造函数的调用
Person p2(10); //有参构造函数
Person p3(p2); //拷贝构造函数
//注意事项1
//调用默认构造函数的时候,不要加()
// 因为下面这行代码,编译器会认为是一个函数的声明,不会认为在创建对象
// Person p1();
cout << "p2的年龄为:" << p2.m_Age << endl;
cout << "p3的年龄为:" << p3.m_Age << endl;
//2、显示法
Person p1;
Person p2 = Person(10); //有参构造
Person p3 = Person(p2); //拷贝构造
//单独把上式右边部分拿出来Person(10); //匿名对象 特点:当前行执行结束后,系统会立即回收掉匿名对象
//即马上输出Person的析构函数调用,之后再输出下一行数据
//cout << "aaaaa" << endl;
//注意事项2
// 不要利用拷贝构造函数 初始化匿名对象:Person(p3); 编译器会认为Person (p3) == Person p3,即实例化对象p3,对象的声明
//3、隐式转换法
Person p4 = 10; //相当于 Person p4 = Person(10); 有参构造
Person p5 = p4; //拷贝构造
}
int main()
{
test01();
}
1、使用一个已经创建完毕的对象来初始化一个新对象
2、值传递的方式给函数参数传值
3、值方式返回局部对象
class Person
{
public:
Person(){
cout << "Person的默认构造函数调用" << endl;
}
Person(int age){
cout << "Person的有参构造函数调用" << endl;
m_Age = age;
}
Person(const Person& p){
cout << "Person的拷贝构造函数调用" << endl;
m_Age = p.m_Age;
}
~Person(){
cout << "Person的析构函数调用" << endl;
}
int m_Age;
};
//1、使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
Person p1(20); //首先创建一个有参的对象p1
Person p2(p1);
cout << "p1的年龄为:" << p1.m_Age << endl;
cout << "p2的年龄为:" << p2.m_Age << endl;
}
//2、值传递的方式给函数参数传值
void doWork(Person p) //值传递的本质会拷贝出一个临时的副本出来
{
}
void test02()
{
Person p;
doWork(p); //以值传递传入参数时 会有 Person p = p 即隐式转换法的对拷贝构造函数调用
//注:传入的p和接收的p并不是一回事
}
//3、值方式返回局部对象
Person doWork2()
{
Person p1;
//cout << (int*)&p1 << endl;
return p1; //因为是以值的方式返回,会拷贝出一个新的副本
//此时会调用拷贝构造函数,即 Person p' = p1 但此编译器已经优化了
}
void test03()
{
Person p = doWork2(); //最终是新的Person p = p'
//cout << (int*)&p << endl;
}
int main()
{
//test01();
//test02();
test03();
}
1、创建一个类,c++编译器会给每个类都添加至少三个函数
默认构造 (空实现)
析构函数 (空实现)
拷贝构造 (值拷贝)
2、如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数
class Person
{
public:
Person(){
cout << "Person的默认构造函数调用" << endl;
}
Person(int age){
cout << "Person的有参构造函数调用" << endl;
m_Age = age;
}
Person(const Person& p){
cout << "Person的拷贝构造函数调用" << endl;
m_Age = p.m_Age;
}
~Person(){
cout << "Person的析构函数调用" << endl;
}
int m_Age;
};
void test01()
{
Person p;
p.m_Age = 18;
Person p1(28); //有参构造
Person p2(p); //拷贝构造
cout << "p2的年龄:" << p2.m_Age << endl;
}
//2、如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
void test02()
{
Person p(28);
Person p2(p);
cout << "p2的年龄:" << p2.m_Age << endl;
}
int main()
{
//test01();
test02();
}
test01()运行结果
test02()运行结果
总结 提供有参则不提供默认 提供拷贝则不提供有参和默认
有多少对象就释放多少个析构函数
浅拷贝:简单的赋值拷贝操作。
深拷贝
:在堆区重新申请空间,进行拷贝操作。
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。
//深拷贝和浅拷贝
class Person
{
public:
Person(){
cout << "Person的默认构造函数调用" << endl;
}
Person(int age, int height){
m_Age = age;
m_Height = new int(height); //m_Height是指针定义的变量来接收
cout << "Person的有参构造函数调用" << endl;
}
//自己实现拷贝构造函数 解决浅拷贝带来的问题
Person(const Person& p){
cout << "Person 拷贝构造函数调用" << endl;
m_Age = p.m_Age;
//m_Height = p.m_Height; 编译器默认实现就是这行代码
//深拷贝操作
m_Height = new int(*p.m_Height); //重新通过解指针赋予其值开辟一个新的地址
}
~Person()
{
//析构代码,将堆区开辟数据做释放操作
if (m_Height != NULL){
delete m_Height;
m_Height = NULL;
}
cout << "Person的析构函数调用" << endl;
}
int m_Age;
int *m_Height; //定义一个int类型的指针来表示身高 准备将身高的数据开辟到堆区
};
void test01()
{
Person p1(18,180);
cout << "p1的年龄为:" << p1.m_Age << "身高为: " << *p1.m_Height << endl;
Person p2(p1);
cout << "p2的年龄为:" << p2.m_Age << "身高为: " << *p2.m_Height << endl;
}
int main()
{
test01();
}
执行Person p2(p1);代码时,编译器会进行浅拷贝操作,此时p1、p2的指针都指向同一块内存,在执行析构代码时,由于先进后出原则,会导致p2的析构函数先执行,此时堆区数据内存会被释放,后面p1又会再执行依次重复释放内存,但是p2已经释放了,因此为非法操作。
解决办法:p2自己开辟一块内存,并让指针指向该内存即可。