一、类与对象
1.1 概念
类:类是抽象的概念,规定了同一类对象拥有的属性和行为(成员=属性+行为)。
对象:对象是具体的,是按照类的概念创建出来的实体。
1.2 类的定义
【例子】以“手机”为例,来说明类的定义。
手机的属性:
品牌、型号、重量
手机的行为:
播放音乐、运行游戏、通讯
1.3 对象实例化
C++有两种创建对象的方式:
1.3.1 栈内存对象
这种对象创建后,在其生命周期结束后(所在的花括号执行完成后),自动被销毁。
#include <iostream>
using namespace std;
class MobilePhone
{
public:
string brand;
string model;
int weight;
void play_music()
{
cout << "我在遥望,月亮之上~" << endl;
}
void run_game()
{
cout << "Timi" << endl;
}
void communicate()
{
cout << "喂?" << endl;
}
};
int main()
{
// 创建栈内存对象
MobilePhone mp1;
// 调用属性
mp1.brand = "小米";
mp1.model = "12";
mp1.weight = 199;
cout << mp1.brand << " " << mp1.model << " "
<< mp1.weight << endl;
// 调用成员函数
mp1.play_music();
mp1.run_game();
mp1.communicate();
return 0;
} // 主函数执行完成后 mp1自动销毁
1.3.2 堆内存对象
堆内存对象的创建需要使用new关键字,并且使用指针来指向此对象。
堆内存的对象需要程序员手动使用delete关键字来销毁,如果没有delete,那么这个对象所在的花括号执行完成后,会持续占用内存且无法回收,这种现象被称为“内存泄漏”。轻微的内存泄漏并不会对程序运行有明显的影响,随着内存泄漏的累积,逐渐会导致程序卡顿,甚至无法正常运行。
因此对于堆内存对象,通常有new就一定有一个对应的delete。
已经delete的对象,有时候还可以正常使用其部分功能,但是请不要这要做!
栈内存对象不支持delete关键字。
#include <iostream>
using namespace std;
class MobilePhone
{
public:
string brand;
string model;
int weight;
void play_music()
{
cout << "我在遥望,月亮之上~" << endl;
}
void run_game()
{
cout << "Timi" << endl;
}
void communicate()
{
cout << "喂?" << endl;
}
};
int main()
{
// 创建堆内存对象
MobilePhone* mp2 = new MobilePhone;
// 调用属性
mp2->brand = "华为";
mp2->model = "P50";
mp2->weight = 201;
cout << mp2->brand << " " << mp2->model << " "
<< mp2->weight << endl;
// 调用成员函数
mp2->play_music();
mp2->run_game();
mp2->communicate();
// 手动销毁mp2
delete mp2;
return 0;
}
二、封装
封装指的是,先将类的一些属性和其它细节隐藏,根据业务需求重新提供给外部对应功能的调用接口。
外部的调用接口最基础的有两类:
-
读取属性值 getter
-
写入属性值 setter
封装前后的类,类似于测试中的白盒与黑盒的概念。
以之前的手机类为例,来进行封装的演示。
#include <iostream>
using namespace std;
class MobilePhone
{
private: // 私有权限:只能在类内部访问
string brand; // 只读:只添加getter接口
string model; // 只写:只添加setter接口
int weight; // 可读可写:同时添加getter和setter接口
public: // 重新对外部开放对应的功能接口
string get_brand()
{
return brand;
}
void set_model(string m)
{
model = m;
}
int get_weight()
{
return weight;
}
void set_weight(int w)
{
weight = w;
}
};
int main()
{
// 创建栈内存对象
MobilePhone mp1;
// 通过公开接口访问属性
mp1.set_weight(177);
mp1.set_model("山寨");
// 存在一个问题:brand属性默认值是空的
cout << mp1.get_brand() << " " << mp1.get_weight() << endl;
return 0;
}
三、构造函数
3.1 构造函数的基本使用
构造函数用来创建一个类的对象,在创建对象时还可以给对象的属性赋予初始值。
构造函数不用写返回值类型,函数名称必须与类名完全一致。
当程序员没有手动编写构造函数时,编译器会自动添加一个没有参数的,函数体为空的构造函数。一旦程序员手动编写了任一一个构造函数,编译器不再自动添加构造函数。
构造函数也支持函数重载和函数参数默认值。
#include <iostream>
using namespace std;
class MobilePhone
{
private:
string brand;
string model;
int weight;
public:
// 编译器自动添加的构造函数
// MobilePhone(){}
// 手动添加一个构造函数,来给属性赋予初始值
MobilePhone(string b,string m,int w)
{
brand = b;
model = m;
weight = w;
}
// 函数重载
MobilePhone()
{
brand = "山寨";
model = "8848钛金手机";
weight = 666;
}
// 打印当前的属性值
void show()
{
cout << brand << " " << model << " " << weight << endl;
}
};
int main()
{
MobilePhone mp1("苹果","14 Pro",188);
mp1.show();
MobilePhone* mp2 = new MobilePhone("小米","12",199);
mp2->show();
delete mp2;
MobilePhone mp3;
mp3.show();
return 0;
}
3.2 构造初始化列表
是基于构造函数对属性赋予初始值的另一种简便写法,在现有阶段,可以根据实际情况来决定是否采用。
#include <iostream>
using namespace std;
class MobilePhone
{
private:
string brand;
string model;
int weight;
public:
// 构造初始化列表
MobilePhone(string b,string m,int w)
:brand(b),model(m),weight(w){}
void show()
{
cout << brand << " " << model << " " << weight << endl;
}
};
int main()
{
MobilePhone mp1("苹果","14 Pro",188);
mp1.show();
return 0;
}
关于构造初始化列表,需要注意以下几点:
-
构造初始化列表的效率比构造函数函数体中赋值更高。
-
如果成员变量是常成员变量,则不能在构造函数的函数体中赋予初始值,可以通过构造初始化列表赋予初始值。
-
如果影响代码的可读写,可以不使用构造初始化列表。
3.3 拷贝构造函数
如果程序员不手动编写拷贝构造函数,编译器会为每个类增加一个默认的拷贝构造函数。
#include <iostream>
using namespace std;
class MobilePhone
{
private:
// 设置属性的默认值
string brand = "山寨";
string model = "8848";
int weight = 188;
public:
void show()
{
cout << brand << " " << model << " " << weight << endl;
}
};
int main()
{
MobilePhone mp1;
mp1.show();
// 调用默认的拷贝构造函数
MobilePhone mp2(mp1);
mp2.show();
cout << &mp1 << " " << &mp2 << endl; // 0x61fe84 0x61fe78
return 0;
}
上面的例子中,mp1和mp2是两个数据相同的对象,每个对象的数据都是相互独立,只属于当前对象持有。
3.3.1 浅拷贝
当成员变量出现指针类型时,默认的拷贝构造函数是基于赋值操作的,因此在拷贝的过程中,会出现指针的拷贝,会造成多个对象的属性指向同一个内存区域的问题,这样的现象不符合面向对象的设计规范。
#include <iostream>
#include <string.h>
using namespace std;
class Dog
{
private:
char* name; // 故意使用char*
public:
Dog(char* n)
{
name = n;
}
// 编译器自动添加下面的拷贝构造函数
Dog(const Dog& d)
{
name = d.name; // 指针的拷贝!
}
void show_name()
{
cout << name << endl;
}
};
int main()
{
char c[20] = "WangCai";
Dog d1(c);
Dog d2(d1); // 拷贝构造函数
strcpy(c,"XiaoBai");
// 属性值无法对象独自持有
d1.show_name(); // XiaoBai
d2.show_name(); // XiaoBai
return 0;
}
3.3.2 深拷贝
默认的拷贝是浅拷贝,当属性出现指针类型,应该手写一个深拷贝构造函数。
#include <iostream>
#include <string.h>
using namespace std;
class Dog
{
private:
char* name;
public:
Dog(char* n)
{
// 创建堆内存对象
name = new char[20];
// 数据复制
strcpy(name,n);
}
Dog(const Dog& d)
{
// 创建堆内存对象
name = new char[20];
// 数据复制
strcpy(name,d.name);
}
void show_name()
{
cout << name << endl;
}
};
int main()
{
char c[20] = "WangCai";
Dog d1(c);
Dog d2(d1); // 拷贝构造函数
strcpy(c,"XiaoBai");
// 属性值无法对象独自持有
d1.show_name(); // WangCai
d2.show_name(); // WangCai
return 0;
}
在实际开发过程中,解决浅拷贝的问题可以通过一些简单粗暴的方式,例如直接把拷贝构造函数设置为私有权限,这样就屏蔽了外界对拷贝构造函数的调用。
ps.开辟的内存要记得释放
3.4 隐式调用构造函数
之前的例子都是显式调用构造函数,下面的例子来演示隐式调用构造函数。
#include <iostream>
using namespace std;
class Test
{
private:
string name;
public:
Test(string n):name(n)
{
cout << "调用了构造函数!" << endl;
}
string get_name()
{
return name;
}
};
int main()
{
string s1 = "AAA";
string s2 = "BBB";
// 显式调用构造函数
Test t1(s1);
cout << t1.get_name() << endl;
// 隐式调用构造函数
Test t2 = s2;
cout << t2.get_name() << endl;
return 0;
}
尽管隐式调用构造函数比较方便,但是可能会存在一个问题:在参数传递的过程中无意中创建了对象,这种情况可以使用explicit关键字修饰构造函数,来达到屏蔽使用构造函数的隐式调用。
四、析构函数
在3.3.2节中,深拷贝的示例代码虽然解决了指针作为成员变量拷贝对象的问题,但是在构造函数中开辟的内存区域,没有得到合理的释放,造成了内存泄漏的问题。要解决这个问题需要使用析构函数。
先来学习析构函数的基本使用,析构函数是与构造函数完全对立的函数。
构造函数 | 析构函数 |
调用时机:对象创建时 | 调用时机:销毁对象时 |
手动调用 | 自动调用 |
可以有参数,可以重载 | 不能有参数,不能重载 |
函数名是类名 | 函数名是~类名 |
通常用于对象数据的初始化 | 通常用于对象占用资源的释放 |
#include <iostream>
using namespace std;
class Test
{
private:
string name;
public:
Test(string n):name(n){}
~Test()
{
cout << name << "析构函数" << endl;
}
};
int main()
{
cout << "--------start--------" << endl;
// 创建一个栈内存对象
Test t1("A");
// 创建一个堆内存对象
Test* t2 = new Test("B");
delete t2;
cout << "--------finish--------" << endl;
return 0;
}
运行结果如下: