03-类和对象(上)
一、类的概述
1.类的引入
类的封装:将数据和方法封装在一起,加以权限区分,用户只能通过公共方法访问私有数据。
为什么要将数据和方法封装在一起呢,而且还要通过公共方法才能访问私有数据?
- C语言中数据和方法分开可能产生的问题:
// 定义一个狗结构体
struct Dog
{char name[32];
};// 定义一个人结构体
struct Person
{char name[32];
};// 定义一个狗的方法
void dogRun(struct Dog *dog)
{printf("%s正在用四条腿跑\n", dog->name);
}// 定义一个人的方法
void personRun(struct Person *per)
{printf("%s正在用两条腿跑\n", per->name);
}void test01()
{struct Dog laifu = {"来福"};struct Person changwei = {"常威"};dogRun(&laifu);personRun(&changwei);dogRun((struct Dog *)&changwei);
}
- 运行结果
来福正在用四条腿跑
常威正在用两条腿跑
常威正在用四条腿跑
- 可以看到,如果将数据和方法分开的话,由于对函数和数据的使用不规范,可能造成非常尴尬的局面。
2.类的封装
- 定义类的关键字为:class;
- 对类的数据加以权限区分:public 公有数据 、protected 受保护数据 、private 私有数据;
- public 修饰的数据和方法,类外可以直接访问;
- private、protected 修饰的数据和方法,类外不可以访问;
- 没有涉及继承时,private 和 protected 没有任何区别;
- 权限限制只是针对类外访问而言的,类的内部没有权限的区分,因此可以通过类的成员函数访问类中的任意数据;
- 一般建议,数据为私有,方法为公有。
- 代码演示
// 定义一个类
class Data
{
private:int a;
protected:int b;
public:int c;// 定义一个成员函数,用于初始化数据void initData(int a1, int b1, int c1){a = a1;b = b1;c = c1;}// 定义一个成员函数,打印所有变量的值void showData(){printf("a = %d,b = %d, c = %d\n",a , b, c);}
};void test02()
{// 实例化一个对象Data data1;data1.initData(11, 22, 33);// 访问data1中的数据// printf("a = %d\n", data1.a); // 报错,数据私有,不允许访问// printf("b = %d\n", data1.b); // 报错,数据受保护,不允许访问printf("c = %d\n", data1.c); // 访问成功,数据公有// 通过成员函数访问data1.showData();
}
- 运行结果
c = 33
a = 11,b = 22, c = 33
- 说明:
- 定义类的时候和结构体一样,是不占用空间的,实例化对象的时候才会开辟空间;
- 可以看到,我们在类的外部访问私有和受保护数据的时候,访问失败,访问公共数据,访问成功;
- 但是无论公有私有还是公共数据,通过类的成员函数进行读写操作都能成功,这也证明了在类的内部没有权限区分;
- 因此以后,想要访问类私有属性,就需要通过公共方法来访问。
3.如何设计一个类
因为类是封装了数据和方法,因此在设计类前我们要想好需要有哪些数据,然后要对这些数据进行哪些操作。
3.1案例一
- 案例1:定义一个 Person 类,包括人的姓名和年龄数据,包含初始化方法,对每个数据的读写操作,显示所有信息,同时限制年龄为合理范围。
// 定义一个 Person 类
class Person
{
private:char name[32];int age;
public:// 定义函数,初始化对象void initPerson(char *new_name, int new_age){if (new_age >= 0 && new_age <= 120){age = new_age;}else{cout << "请输入有效年龄" << endl;return;}strcpy(name, new_name);}// 定义函数,获取name的值char *getName(void){return name;}// 定义函数,获取age的值int getAge(void){return age;}// 定义函数,修改namevoid setName(char *new_name){strcpy(name, new_name);}// 定义函数,修改agevoid setAge(int new_age){if (new_age >= 0 && new_age <= 120){age = new_age;}else{cout << "请输入有效年龄" << endl;return;}}// 定义函数,打印所有信息void showPerson(){cout << "name = " << name << ", age = " << age << endl;}
};void test03()
{// 创建一个Person对象Person jack;jack.initPerson("jack", 20);cout << "name = " << jack.getName() << endl;cout << "age = " << jack.getAge() << endl;jack.setName("rose");jack.setAge(18);jack.showPerson();
}
- 运行结果
name = jack
age = 20
name = rose, age = 18
3.2案例二
- 案例2:设计立方体类(Cube),立方体的长宽高为 a b c ,求出立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等。
// 定义一个立方体类
class Cube
{
private:int a;int b;int c;
public:// 定义函数,初始化长宽高void initCube(int a1, int b1, int c1);// 定义函数,设置长宽高void setA(int a1);void setB(int b1);void setC(int c1);// 定义函数,获取长宽高int getA();int getB();int getC();// 定义函数,计算立方体的面积int getArea();// 定义函数,计算立方体的体积int getVolume();// 定义成员函数,判断两个立方体是否相等bool cmpCube(Cube &other_cube);
};void Cube::initCube(int a1, int b1, int c1) {a = a1;b = b1;c = c1;
}void Cube::setA(int a1) {a = a1;
}void Cube::setB(int b1) {b = b1;
}void Cube::setC(int c1) {c= c1;
}int Cube::getA() {return a;
}int Cube::getB() {return b;
}int Cube::getC() {return c;
}int Cube::getArea() {int area = (a * b + a * c + b * c) * 2;return area;
}int Cube::getVolume() {int volume = a * b *c;return volume;
}bool Cube::cmpCube(Cube &other_cube) {if ((a == other_cube.getA()) && (b == other_cube.getB()) && (c == other_cube.getC()))return true;return false;
}// 定义全局函数,判断两个立方体是否相等
bool cmpCube1(Cube &cube1, Cube &cube2)
{if ((cube1.getA() == cube2.getA()) && (cube1.getB() == cube2.getB()) && (cube1.getC() == cube2.getC()))return true;return false;
}void test04()
{// 创建一个立方体对象Cube cube1;cube1.initCube(2, 3, 5);cout << "立方体1面积:" << cube1.getArea() << ", 立方体1体积:" << cube1.getVolume() << endl;// 创建另一个立方体对象Cube cube2;cube1.initCube(1, 4, 8);cout << "立方体2面积:" << cube1.getArea() << ", 立方体2体积:" << cube1.getVolume() << endl;// 判断两个立方体是否相等// if (cmpCube1(cube1, cube2)) // 全局函数判断if (cube1.cmpCube(cube2)) // 成员函数判断{cout << "两个立方体完全一样" << endl;}else{cout << "两个立方体不一样" << endl;}
}
- 运行结果
立方体1面积:62, 立方体1体积:30
立方体2面积:88, 立方体2体积:32
两个立方体不一样
- 说明:
- 上面定义类的时候,将类的成员函数在类中声明,类外定义。注意,类外定义的函数得加上作用域的限制,标明这个成员函数是属于哪一个类的;
- 这里在比较两个立方体是否相等的时候,返回值用到了 bool 类型,表示真假值,在C语言中没有用过,c++阶段学习;
- 当使用全局函数比较两个立方体时,需要传两个参数,但使用成员函数比较两个立方体大小的时候,只需要传另外一个立方体就行。
3.3案例三
- 案例3:设计一个圆形类(Circle),和一个点类(Point),计算点和圆的关系。 假如圆心坐标为x0, y0, 半径为 r,点的坐标为 x1, y1,判断点在圆上、圆外还是园内。
// 定义一个点类
class Point
{
private:int x;int y;
public:// 初始化点坐标void initPoint(int x1, int y1){x = x1;y = y1;}// 获取点的横纵坐标int getX(){return x;}int getY(){return y;}
};// 定义一个圆类
class Circle
{
private:int r;Point p;
public:// 初始化圆void initCircle(int r0, int x0, int y0){// 初始化圆半径r = r0;// 初始化圆心p.initPoint(x0, y0);}// 定义函数,判断点和圆的位置(外 上 内 对应 1 0 -1)int getRelation(Point &p1){// 定义变量分别表示半径的平方,横坐标、纵坐标差值的平方int R = r*r;int X = (p1.getX() - p.getX()) * (p1.getX() - p.getX());int Y = (p1.getY() - p.getY()) * (p1.getY() - p.getY());// 判断位置if (R == X + Y)return 0;else if (R < X + Y)return 1;elsereturn -1;}
};void test05()
{// 创建一个点对象Point p;p.initPoint(3, 4);// 创建一个圆对象Circle c;c.initCircle(5, 0, 0);int ret = c.getRelation(p);switch(ret){case 0:cout << "点在圆上" << endl;break;case 1:cout << "点在圆外" << endl;break;case -1:cout << "点在圆内" << endl;break;default:cout << "未得出结果" << endl;break;}
}
- 运行结果
点在圆上
- 说明:上面的案例中用到了对象的嵌套,圆对象里面又嵌套了一个点对象。
二、构造析构
1.构造函数
1.1构造函数的概述
我们前面定义类的时候,都会设置一个给对象初始化的函数,创建对象以后再手动调用,其实 c++ 类里有可以主动调用的初始化函数,叫做构造函数。
构造函数:类实例化对象的时候自动调用。构造函数的本质功能就是初始化对象中的数据成员。
1.2构造函数的定义
构造函数名和类名称相同,没有返回值类型,连 void 都不可以,可以有参数,可以重载,因为创建对象是在类外创建,构造函数在创建对象的时候调用,相当于要在类外调用构造函数,因此构造函数权限为 public。
- 代码演示
// 定义一个类
class Data
{
private:int a;int b;
public:Data(){a = 0;b = 0;cout << "无参的构造函数被调用" << endl;}Data(int a1 ,int b1){a = a1;b = b1;cout << "两个参数的构造函数被调用" << endl;}
};void test06()
{// Data obj; // error: no matching function for call to 'Data::Data()'Data obj;Data obj1(1, 2);
}
- 运行结果
无参的构造函数被调用
两个参数的构造函数被调用
- 说明:
- 我们前面没有学习构造函数的时候,可以直接
Data obj
来实例化对象,而这里,我们定义了两个参数的构造函数以后,没有定义无参的构造函数,再去用Data obj
实例化对象会报错:error: no matching function for call to 'Data::Data()
,可以看到错误提示信息是 Data 类中没有 Data() 函数; - 上面问题的原因是,如果不提供任何构造函数,编译器会为类提供一个默认的无参的构造函数,因此不定义任何构造函数的时候,通过
Data obj
实例化对象,其实是调用了默认的无参构造; - 但定义了构造函数以后,
Data obj
创建对象失败是因为,用户提供任何一个构造函数都会屏蔽默认无参的构造函数; - 因此,我们在定义构造函数的时候,最好定义一个无参构造。
- 我们前面没有学习构造函数的时候,可以直接
1.3构造函数的调用
构造函数的调用形式,本质上也就是我们创建对象的形式,因为创建对象时主动触发构造函数调用。
- 代码演示
// 定义一个类
class Data
{
private:int a;int b;
public:Data(){a = 0;b = 0;cout << "无参的构造函数被调用" << "a = " << a << " b = " << b << endl;}Data(int a1){a = a1;b = 0;cout << "一个参数的构造函数被调用" << "a = " << a << " b = " << b << endl;}Data(int a1 ,int b1){a = a1;b = b1;cout << "两个参数的构造函数被调用" << "a = " << a << " b = " << b << endl;}
};void test06()
{// 隐式调用cout << "--------------------隐式调用--------------------" << endl;Data obj;Data obj1(1);Data obj2(2, 3);// 显示调用cout << "--------------------显式调用--------------------" << endl;Data obj3 = Data();Data obj4 = Data(4);Data obj5 = Data(5, 6);// 为什么有参的隐式调用都加(),无参的不加cout << "--------------------无参加()--------------------" << endl;Data obj6();cout << "--------------------隐式转换--------------------" << endl;Data obj7 = 7;cout << "--------------------匿名对象--------------------" << endl;Data();Data(8);Data(9, 10);
}
- 运行结果
--------------------隐式调用--------------------
无参的构造函数被调用a = 0 b = 0
一个参数的构造函数被调用a = 1 b = 0
两个参数的构造函数被调用a = 2 b = 3
--------------------显式调用--------------------
无参的构造函数被调用a = 0 b = 0
一个参数的构造函数被调用a = 4 b = 0
两个参数的构造函数被调用a = 5 b = 6
--------------------无参加()--------------------
--------------------隐式转换--------------------
一个参数的构造函数被调用a = 7 b = 0
--------------------匿名对象--------------------
无参的构造函数被调用a = 0 b = 0
一个参数的构造函数被调用a = 8 b = 0
两个参数的构造函数被调用a = 9 b = 10
- 说明:
- 上面的演示,构造函数的调用有两种方式:隐式调用、显示调用,推荐使用隐式调用;
- 无参的构造在隐式调用的时候是不加 () 的,因为加了会产生冲突,如:
Data obj6()
,加了 () 以后就不是创建对象了,而是声明了一个名为 obj6 的函数,且函数的返回值是一个 Data 类型的对象; - 当类中有一个参数的构造函数的时候,
Data obj7 = 7
这种写法会触发一个参数构造函数的隐式转换,不推荐这种写法; - 创建对象的时候,可以定义一个变量名来接收创建的对象,当没有定义变量接收的时候,这里创建的对象叫匿名对象,匿名对象没有变量保存,也没有被使用的话,创建完就会释放。
2.析构函数
2.1析构函数的概述
析构函数:当对象生命结束的时候,系统自动调用析构函数,完成对象的清理工作。
构造函数:先为对象开辟空间,然后调用构造函数完成初始化。
析构函数:先调用析构函数,然后释放对象自身的空间。
- 析构函数注意点:
- 如果不提供析构函数,系统会自动提供一个空的析构函数;
- 析构函数并不是清理对象自身的空间(由系统自动释放),而是清理指针成员指向的堆区空间(避免内存泄漏);
- 如果类中有指针成员且指向堆区,必须实现析构函数手动释放堆区空间。
2.2析构函数的定义
析构函数:~类名
称为析构函数名,析构函数没有返回值类型,连 void 都不可以,不能有参数,因此不能被重载。
- 代码演示:还是上面的代码,加一个析构函数,看现象
// 定义一个类
class Data
{
private:int a;int b;
public:// 定义构造函数Data(){a = 0;b = 0;cout << "无参的构造函数被调用" << "a = " << a << " b = " << b << endl;}Data(int a1){a = a1;b = 0;cout << "一个参数的构造函数被调用" << "a = " << a << " b = " << b << endl;}Data(int a1 ,int b1){a = a1;b = b1;cout << "两个参数的构造函数被调用" << "a = " << a << " b = " << b << endl;}// 定义析构函数~Data(){cout << "调用析构函数" << "a = " << a << " b = " << b << endl;}
};void test06()
{// 隐式调用cout << "--------------------隐式调用--------------------" << endl;Data obj;Data obj1(1);Data obj2(2, 3);// 显示调用cout << "--------------------显式调用--------------------" << endl;Data obj3 = Data();Data obj4 = Data(4);Data obj5 = Data(5, 6);// 为什么有参的隐式调用都加(),无参的不加cout << "--------------------无参加()--------------------" << endl;Data obj6();cout << "--------------------隐式转换--------------------" << endl;Data obj7 = 7;cout << "--------------------匿名对象--------------------" << endl;Data();Data(8);Data(9, 10);cout << "--------------------析构函数--------------------" << endl;
}
- 运行结果
--------------------隐式调用--------------------
无参的构造函数被调用a = 0 b = 0
一个参数的构造函数被调用a = 1 b = 0
两个参数的构造函数被调用a = 2 b = 3
--------------------显式调用--------------------
无参的构造函数被调用a = 0 b = 0
一个参数的构造函数被调用a = 4 b = 0
两个参数的构造函数被调用a = 5 b = 6
--------------------无参加()--------------------
--------------------隐式转换--------------------
一个参数的构造函数被调用a = 7 b = 0
--------------------匿名对象--------------------
无参的构造函数被调用a = 0 b = 0
调用析构函数a = 0 b = 0
一个参数的构造函数被调用a = 8 b = 0
调用析构函数a = 8 b = 0
两个参数的构造函数被调用a = 9 b = 10
调用析构函数a = 9 b = 10
--------------------析构函数--------------------
调用析构函数a = 7 b = 0
调用析构函数a = 5 b = 6
调用析构函数a = 4 b = 0
调用析构函数a = 0 b = 0
调用析构函数a = 2 b = 3
调用析构函数a = 1 b = 0
调用析构函数a = 0 b = 0
- 说明:
- 根据上面代码运行的结果可以看出,匿名对象,在定义以后没有变量保存其值,匿名对象也没有被使用,定义完立马就释放了;
- 而非匿名对象,这里的对象是在同一作用域定义的前提下,函数调用(当前复合语句)结束,调用析构函数,且满足先定义的后释放,后定义的先释放。
2.3析构顺序
前面的案例中,同级别的对象,先创建的后释放,当对象在不同作用域定义的时候呢?
- 代码演示
// 定义一个类
class Data
{
private:int a;
public:Data(){a = 0;cout << "无参的构造函数被调用" << "a = " << a << endl;}Data(int a1){a = a1;cout << "有参的构造函数被调用" << "a = " << a << endl;}~Data(){cout << "析构函数被调用" << "a = " << a << endl;}
};// 在全局创建对象
Data obj1(1);
Data obj2(2);
void test07()
{Data obj3(3);{Data obj4(4);Data obj5(5);}Data obj6(6);
}
- 运行结果
有参的构造函数被调用a = 1
有参的构造函数被调用a = 2
有参的构造函数被调用a = 3
有参的构造函数被调用a = 4
有参的构造函数被调用a = 5
析构函数被调用a = 5
析构函数被调用a = 4
有参的构造函数被调用a = 6
析构函数被调用a = 6
析构函数被调用a = 3
析构函数被调用a = 2
析构函数被调用a = 1
- 说明:
- 上面的演示可以看出:对象的创建是按照代码执行的顺序依次执行的;
- 但是析构函数,会先遵循作用域的变量释放顺序,和前面学习的普通变量的释放顺序一样,即当前复合语句结束,变量就释放了。最先结束的符合语句里的对象最先释放,全局的对象最后释放。满足上述对象释放顺序的前提下,还满足同一作用域,先创建的后释放。
3.拷贝构造
3.1拷贝构造函数的定义
- 代码演示
// 定义一个类
class Data
{
private:int a;int b;
public:// 定义构造函数Data(){a = 0;b = 0;cout << "无参的构造函数被调用" << endl;}Data(int a1, int b1){a = a1;b = b1;cout << "有参的构造函数被调用" << endl;}// 定义拷贝构造函数Data(const Data &obj){a = obj.a;b = obj.b;cout << "拷贝构造函数被调用" << endl;}// 显示对象信息的函数void showData(){cout << "a = " << a << " b = " << b << endl;}
};void test08()
{// 先创建一个对象Data obj1(11, 22);// 再创建一个对象,并将旧对象赋值给它Data obj2 = obj1;obj2.showData();
}
- 运行结果
有参的构造函数被调用
拷贝构造函数被调用
a = 11 b = 22
- 说明:
- 如果用户没有提供拷贝构造函数,系统会提供默认拷贝构造函数,默认拷贝构造是浅拷贝,对应成员为普变量的话,浅拷贝足够了。但如果类中有指针成员且指向堆区, 必须实现拷贝构造函数,深拷贝;
- 拷贝构造函数定义和普通构造函数一样,类名作为函数名,只是参数不一样,参数
const Data &obj
中,obj 为形参名,加 & 表示引用传递,因为实参是一个对象,对象占用空间一般很大,引用传参节约空间。同时,参数要加 const 修饰,不允许函数内部修改传入的对象的数据。
3.2拷贝构造函数的调用
拷贝构造函数和构造函数一样,不需要手动调用,而是在特定的情况下自动触发调用。
拷贝构造函数是:创建一个新对象的时候,通过旧对象给新对象初始化,会自动调用拷贝构造函数,会自动将旧对象的引用作为实参传入拷贝构造函数,将旧对象的数据拷贝给新对象。
3.2.1普通对象作为函数的参数
普通对象作为函数的参数的时候,会调用拷贝构造。
- 代码演示
// 定义一个类
class Data {
private:int a;int b;
public:// 定义构造函数Data() {a = 0;b = 0;cout << "无参的构造函数被调用" << endl;}Data(int a1, int b1) {a = a1;b = b1;cout << "有参的构造函数被调用" << endl;}// 定义拷贝构造函数Data(const Data &obj) {a = obj.a;b = obj.b;cout << "拷贝构造函数被调用" << endl;}
};void func(Data obj){}
// void func(Data &obj){}void test09() {// 创建一个对象Data obj1(11, 22);// 普通对象作为函数的参数func(obj1);
}
- 运行结果
有参的构造函数被调用
拷贝构造函数被调用
- 说明:
- 可以看到,上面的代码中,我们并没有像前面一样创建一个新对象,然后将旧对象的值赋给新对象,却发送了拷贝构造。其实这里也发生了旧对象赋值给新对象的动作,只是比较隐晦;
- 前面学习函数的时候知道,函数在定义的时候不占用空间,在调用的时候才占用空间,因为函数调用的时候会为形参开辟空间,然后定义一个形参指定类型的变量,将实参赋值给形参变量,而这里的形参就是一个新创建的 Data 类型的对象,实参是一个旧的对象,那么调用函数传参的过程就是一个旧对象给新对象赋值的过程,因此会调用拷贝构造;
- 为了防止拷贝构造被调用,一般将形参设置为引用传递,引用传递只是给旧对象起了个别名,没有旧对象给新对象赋值的过程,不会调用拷贝构造。
3.2.2普通对象作为函数的返回值
普通对象作为函数的返回值,外部通过一个新对象去接收,按照我们前面的理解,是将一个旧对象返回,赋值给新对象,理论上会发生拷贝构造,但事实并不一定如此,不同的高级编译器会有不同的结果。在 visual studio 里面会发生拷贝构造,但是在 Qt 或者 CLion 里面不会,具体可以在自己习惯使用的编译器里验证。
- 这里演示 CLion 的
// 定义一个类
class Data {
private:int a;int b;
public:// 定义构造函数Data() {a = 0;b = 0;cout << "无参的构造函数被调用" << endl;}Data(int a1, int b1) {a = a1;b = b1;cout << "有参的构造函数被调用" << endl;}// 定义拷贝构造函数Data(const Data &obj) {a = obj.a;b = obj.b;cout << "拷贝构造函数被调用" << endl;}void showData(){cout << "a = " << a << " b = " << b << endl;}// 析构函数~Data(){cout << "析构函数被调用" << endl;}
};Data func()
{Data obj1(11, 22);cout << &obj1 << endl;return obj1;
}void test09() {Data obj2 = func();obj2.showData();cout << &obj2 << endl;
}
- 运行结果
有参的构造函数被调用
0xc718fffd88
a = 11 b = 22
0xc718fffd88
析构函数被调用
- 说明
- 根据上面的运行结果,可以看到,只调用了一次构造函数,没有调用拷贝构造,而且明明创建了两个对象,但是结果却只析构了一次;
- 打印两个对象的内存地址,发现内存地址相同,结合上面的分析,可以知道,这两个变量本质上是指向同一个空间;
- 因此,可以得出结论, func 函数调用完成以后,局部变量 obj1 被删除了,但它指向的空间并没有被释放,而是由 obj2 指向了同一个空间,等到 obj2 释放的时候,空间才会释放。
3.2.3无参、有参、拷贝构造总结
- 如果用户提供了有参构造或拷贝构造会屏蔽默认的无参构造;
- 但用户提供有参构造或无参构造不会屏蔽默认拷贝构造,只有用户提供拷贝构造,才会屏蔽默认拷贝构造;
- 如果类中有指针成员且指向堆区,必须实现析构函数和拷贝构造函数(深拷贝);
- 构造函数实例化对象自动调用,先开辟空间,后调用构造函数,可以重载;
- 析构函数,对象结束的时候自动调用,先调用析构函数,后释放对象自身空间,不能重载。