C++核心编程 day03 类
1. 关于类的案例
1.1 立方体类的设计
在本案例中,我们需要设计一个立方体类Cube
,该类有三个成员变量,分别是长、宽、高。类中有成员方法,分别是设置以及获取成员属性的方法,以及计算该立方体的面积与体积,和判断两个立方体是否一样的方法。除此之外,我们还需要定义一个全局的函数去判断两个立方体是否一样。
根据前面最后说的,我们需要将类中的成员属性均要定义为私有,这样可以对属性的读写进行访问控制,同时也可以对属性的合法性进行校验。根据这些我们可以设计出以下的立方体类:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
/*
设计立方体类(Cube),求出立方体的面积( 2*a*b + 2*a*c + 2*b*c )和体积( a * b * c),
分别用全局函数和成员函数判断两个立方体是否相等。
*/
class Cube
{
private:
int _L;
int _W;
int _H;
public:
// 设置长宽高
void setL(int L)
{
_L = L;
}
void setW(int W)
{
_W = W;
}
void setH(int H)
{
_H = H;
}
// 获取长宽高
int getL()
{
return _L;
}
int getW()
{
return _W;
}
int getH()
{
return _H;
}
// 计算面积
int caculateArea()
{
return 2 * _L * _W + 2 * _L * _H + 2 * _W * _H;
}
// 计算体积
int caculateVolume()
{
return _L * _W * _H;
}
// 判断两个立方体是否一样
bool isEqual(Cube &cube)
{
return _L == cube.getL() && _W == cube.getW() && _H == cube.getH();
}
};
// 全局函数:判断两个立方体是否一样
bool isEqualCube(Cube &cube1, Cube &cube2)
{
return cube1.getL() == cube2.getL() && cube1.getW() == cube2.getW() && cube1.getH() == cube2.getH();
}
int main()
{
Cube cube1;
cube1.setL(1);
cube1.setW(1);
cube1.setH(1);
cout << "cube1 面积为: " << cube1.caculateArea() << endl;
cout << "cube1 体积为: " << cube1.caculateVolume() << endl;
Cube cube2;
cube2.setL(1);
cube2.setW(1);
cube2.setH(1);
cout << "cube2 面积为: " << cube2.caculateArea() << endl;
cout << "cube2 体积为: " << cube2.caculateVolume() << endl;
Cube cube3;
cube3.setL(1);
cube3.setW(1);
cube3.setH(2);
cout << "cube3 体积为: " << cube3.caculateVolume() << endl;
cout << "cube3 面积为: " << cube3.caculateArea() << endl;
// 利用成员函数判断是否一样
bool ret = cube1.isEqual(cube2);
if (ret)
{
cout << "cube1 和 cube2 一样" << endl;
}
else
{
cout << "cube1 和 cube2 不一样" << endl;
}
// 利用全局函数判断是否一样
ret = isEqualCube(cube1, cube3);
if (ret)
{
cout << "cube1 和 cube3 一样" << endl;
}
else
{
cout << "cube1 和 cube3 不一样" << endl;
}
system("pause");
return EXIT_SUCCESS;
}
1.2 点和圆类的设计
上面的立方体类Cube
的代码如果我们需要进行复用,则会感觉有点头疼。为了解决这个,我们可以考虑将类分别独立出去,给他单独地写一个头文件和C++的源程序文件,其中头文件中写类的定义,包括了类的成员属性的定义和成员方法的声明,在源程序文件中我们需要写成员方法的实现。
例如下面我们需要做一个点与圆关系的案例。在该案例中我们需要判断点在圆内、圆上还是说在圆外。在这里我们涉及到了两个类,一个是点类Point
,另一个是圆类Circle
,需要分别定义,又因为圆心也是一个点,所以圆心可以为Point
类的一个变量。这两个类的设计如下:
Point.h
#ifndef __POINT_H__
#define __POINT_H__
class Point
{
private:
// 点的坐标
double _x, _y;
public:
// 设置横纵坐标的方法
void setX(double x);
void setY(double y);
// 获取横纵坐标的方法
double getX();
double getY();
};
#endif
Point.cpp
#include "Point.h"
// 设置横坐标
void Point::setX(double x)
{
_x = x;
}
// 设置纵坐标
void Point::setY(double y)
{
_y = y;
}
// 获取横坐标
double Point::getX()
{
return _x;
}
// 获取纵坐标
double Point::getY()
{
return _y;
}
Circle.h
#ifndef __CIRCLE_H__
#define __CIRCLE_H__
#include "Point.h"
class Circle
{
private:
// 圆心与半径
Point _center;
int _r;
public:
// 设置圆心与获取圆心
void setCenter(Point center);
Point getCenter();
// 设置半径与获取半径
void setR(int r);
int getR();
// 判断圆与点的关系
void relationshipOfPoint(Point &point);
};
#endif
Circle.cpp
#include "Circle.h"
#include <iostream>
using namespace std;
// 设置和获取圆心
void Circle::setCenter(Point center)
{
_center = center;
}
Point Circle::getCenter()
{
return _center;
}
// 设置和获取半径
void Circle::setR(int r)
{
_r = r;
}
int Circle::getR()
{
return _r;
}
// 判断圆和点的关系
void Circle::relationshipOfPoint(Point &point)
{
// 两点间的距离平方
int distanceSquare = (_center.getX() - point.getX()) * (_center.getX() - point.getX()) + (_center.getY() - point.getY()) * (_center.getY() - point.getY());
// 半径的平方
int rSquare = _r * _r;
if (distanceSquare > rSquare)
{
cout << "点在圆外" << endl;
}
else if (distanceSquare == rSquare)
{
cout << "点在圆上" << endl;
}
else
{
cout << "点在圆内" << endl;
}
}
接下来我们对上述的类的关系进行测试,同时也写了一个全局函数去判断点与类的关系:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
/*
设计一个圆形类(AdvCircle),和一个点类(Point),计算点和圆的关系。
假如圆心坐标为x0, y0, 半径为r,点的坐标为x1, y1:
*/
#include "Point.h"
#include "Circle.h"
// 判断圆与点的关系
void judgeRelationshipOfCircleAndPoint(Circle &circle, Point &point)
{
// 点和圆心的坐标
int cx = circle.getCenter().getX();
int cy = circle.getCenter().getY();
int px = point.getX();
int py = point.getY();
// 半径的平方和圆心与点的距离平方
int rSquare = circle.getR() * circle.getR();
int distanceSquare = (cx - px) * (cx - px) + (cy - py) * (cy - py);
// 判断关系
if (distanceSquare > rSquare)
{
cout << "点在圆外" << endl;
}
else if (distanceSquare == rSquare)
{
cout << "点在圆上" << endl;
}
else
{
cout << "点在圆内" << endl;
}
}
int main()
{
// 定义一个圆心在(0,0) 半径为5的圆
Point center;
center.setX(0);
center.setY(0);
Circle circle;
circle.setCenter(center);
circle.setR(5);
// 定义三个点,分别在(4, 0), (5, 0), (6, 0)
Point p1, p2, p3;
p1.setX(4);
p1.setY(0);
p2.setX(5);
p2.setY(0);
p3.setX(6);
p3.setY(0);
// 用成员函数判断p1与圆的关系
circle.relationshipOfPoint(p1);
// 用全局函数判断p2与圆的关系
judgeRelationshipOfCircleAndPoint(circle, p2);
// 用成员函数判断p3与圆的关系
circle.relationshipOfPoint(p3);
system("pause");
return EXIT_SUCCESS;
}
2. 构造函数与析构函数
2.1 构造函数与析构函数的定义
在C++的类中,我们要实例化一个对象,也就是创建一个对象,必须要有构造函数。而对象的生命周期结束的时候,需要销毁对象,所以要有析构函数。在C++语言中,构造函数与析构函数的声明都必须是在全局作用域的。构造函数是没有返回值的,不用写void
,构造函数的函数名与类名是一样的,构造函数可以有参数,也可以没有参数,可以发生重载。析构函数函数名也类名是一样的,不同的是函数名前需要添加~
,而且析构函数是没有参数的,不可以发生重载,没有返回值不用写void
。这两个函数都不需要我们手动进行调用,都是由编译器自动调用一次。例如下面就是构造函数与析构函数的示例代码:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person
{
public: // 构造和析构必须声明在全局作用域
// 构造函数
// 没有返回值 不用写void
// 函数名与类名相同
// 可以有参数,可以发生重载
// 构造函数由编译器自动调用一次,无须手动调用
Person()
{
cout << "Person 的构造函数被调用" << endl;
}
// 析构函数
// 没有返回值, 不用写void
// 函数名与类名相同,函数名前加 ~
// 不可以有参数,不可以发生重载
// 析构函数 也是由编译器自动调用一次,无须手动调用
~Person()
{
cout << "Person 的析构函数被调用" << endl;
}
};
int main()
{
Person p;
system("pause");
return EXIT_SUCCESS;
}
2.2 构造函数的分类以及调用
根据参数的不同,构造函数可以分为无参构造函数(默认构造函数)和有参构造函数。按照类型的不同也可以分为普通构造函数与拷贝构造函数。与普通构造函数的参数不同,拷贝构造函数的参数只有一个且是该类的一个实例化对象的引用,如一个Person
类,则拷贝构造函数的声明为Person(const Person &p);
。
使用构造函数来实例化对象,我们一般常有三种方法,以一个具体的类来说一下这些方法。假如有一个类为Person
,其定义如下:
class Person
{
public:
Person()
{
cout << "Person 的默认构造函数调用" << endl;
}
Person(int age)
{
_age = age;
cout << "Person 的有参构造函数调用" << endl;
}
// 拷贝构造函数
Person(const Person &p)
{
_age = p._age;
cout << "Person 的拷贝构造函数调用" << endl;
}
// 析构函数
~Person()
{
cout << "Person 的析构函数调用" << endl;
}
int _age;
};
第一种调用的方法是括号法,也就是在需要实例化的对象后面添加括号即可。如:
Person p1(10);
Person p2(p1);
就分别是调用了Person
类中的有参构造方法和拷贝构造方法。这种方法不能用于无参构造函数的调用,因为当你写成Person p();
的时候,编译器会认为p
是一个函数的声明,该函数的返回值为Person
类型。
第二种方法是显式法,也就是直接调用构造函数实例化一个匿名对象再进行赋值。如:
Person p3 = Person();
Person p4 = Person(20);
Person p5 = Person();
Person(20); // 匿名对象, 当前行执行完后会立即释放
在上面的代码中p3
、p5
调用了无参构造函数,p4
和最后一行代码调用了普通构造函数中的有参构造函数。其中最后一句中只是实例化了对象,但是没有给该对象取一个名字,也就是没有变量名表示它,这个对象是一个匿名对象。匿名对象执行完当前行后就会立即释放掉内存空间。在显示调用之中,我们不能用拷贝构造函数去初始化一个匿名对象,因为编译器会认为是对象的实例化。如果已经有了该对象,编译器会认为是重定义。如你要用上述中的p4
通过拷贝构造函数创建一个匿名对象,此时你写了Person(p4);
的代码,而编译器则会认为是Person p4;
,也就是定义一个对象,此时就会出现错误。关于拷贝构造函数的调用规则,在下一节会详细进行讲解。关于构造函数的分类调用示例代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
// 构造函数的分类
// 按照参数分类: 无参构造函数(默认构造函数) 和 有参构造函数
// 按照类型分类: 普通构造函数 和 拷贝构造函数
class Person
{
public:
Person()
{
cout << "Person 的默认构造函数调用" << endl;
}
Person(int age)
{
_age = age;
cout << "Person 的有参构造函数调用" << endl;
}
// 拷贝构造函数
Person(const Person &p)
{
_age = p._age;
cout << "Person 的拷贝构造函数调用" << endl;
}
// 析构函数
~Person()
{
cout << "Person 的析构函数调用" << endl;
}
int _age;
};
// 构造函数的调用
void test01()
{
Person p;
// 1. 括号法
Person p1(10);
Person p2(p1);
// 不要用括号法调用无参构造函数,编译器会认为该代码是函数的声明
// 2. 显示法
Person p3 = Person();
Person p4 = Person(20);
Person p5 = Person();
Person(20); // 匿名对象, 当前行执行完后会立即释放
// 不要用拷贝函数初始化匿名对象, 编译器会认为是对象的实例化,如果已有该对象,编译器会认为重定义
// 隐式法
Person p6 = 10;
Person p7 = p6;
}
int main()
{
test01();
system("pause");
return EXIT_SUCCESS;
}
接下来我们再来说一说关于构造函数的调用规则。在C++中,编译器会至少添加三个函数,分别是默认构造函数(无参构造函数),析构函数(空实现)和拷贝构造函数(值拷贝)。当我们提供了有参构造函数的时候,编译器不会提供默认的构造函数,但是会提供拷贝构造函数。而当我们提供了拷贝构造函数的时候,则编译器不会提供其他的构造函数。构造函数的调用规则代码如下所示:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
// 编译器会给类至少添加三个函数,分别是默认构造函数(空实现) 析构函数(空实现) 拷贝构造函数(值拷贝)
// 如果我们提供了有参构造函数,则编译器不会提供默认构造函数,但是仍会提供拷贝构造函数
// 如果我们提供了拷贝构造函数,则编译器不会提供其它构造函数
class Person
{
public:
Person()
{
cout << "Person 的无参构造函数调用" << endl;
}
Person(int age)
{
_age = age;
cout << "Person 的有参构造函数调用" << endl;
}
Person(const Person &p)
{
_age = p._age;
cout << "Person 的拷贝构造函数调用" << endl;
}
~Person()
{
cout << "Person 的析构函数调用" << endl;
}
int _age;
};
int main()
{
Person p1; // 提供拷贝构造函数后,要自己提供无参构造函数,否则会报错
p1._age = 20;
Person p2(p1);
cout << "p2 的年龄为: " << p2._age << endl;
system("pause");
return EXIT_SUCCESS;
}
2.3 拷贝构造函数的调用时机
拷贝构造函数的调用时机规则主要有三个,第一个是使用已经初始化的对象来初始化新的对象的时候,此时会触发拷贝构造函数的调用。需要注意的是在拷贝构造函数的参数中我们传递的是const 类名 &
,而我们省略了&
符号,编译器则会出错,因为这样会一直去调用拷贝构造函数,造成无限递归调用下去,就会发生错误。
第二种是值传递的方式给函数参数传值,此时会自动调用拷贝构造函数。最后一种方式是以值得方式返回一个局部对象,此时也会调用拷贝构造函数实例化一个匿名对象,但是编译器有时候会做一些优化。关于拷贝构造函数的调用时机的示例代码如下所示:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person
{
public:
Person()
{
cout << "Person 的无参构造函数调用" << endl;
}
Person(int age)
{
_age = age;
cout << "Person 的有参构造函数调用" << endl;
}
Person(const Person &p)
{
_age = p._age;
cout << "Person 的拷贝构造函数调用" << endl;
}
~Person()
{
cout << "Person 的析构函数调用" << endl;
}
int _age;
};
// 1. 用已经创建好的对象来初始化新的对象
void test01()
{
Person p1(18);
Person p2 = Person(p1);
cout << "p2的年龄:" << p2._age << endl;
}
// 2. 值传递的方式给函数参数传值
void func(Person p)
{
}
void test02()
{
Person p(10);
func(p);
}
// 3. 以值的方式返回局部对象
Person func2()
{
Person p(10);
return p;
}
void test03()
{
Person p = func2();
}
int main()
{
//test01();
//test02();
test03();
system("pause");
return EXIT_SUCCESS;
}
2.3 深拷贝与浅拷贝的问题
C++编译器在创建对象的时候提供的默认拷贝构造函数是值拷贝的,也就是当我们类属性里面如果有开辟在堆区的内存,此时使用拷贝构造函数拷贝出来的该属性只是将堆区的内存地址进行了拷贝,而没有拷贝堆区里面的内容。这种情况会导致对象再最后调用析构函数的时候会重复的释放同一块内存空间。解决的方法是拷贝构造函数进行重写,为堆区的内容新开辟一块内存空间再进行拷贝。关于深浅拷贝的示例代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person
{
public:
Person(char *name, int age)
{
_name = (char *)malloc(strlen(name) + 1);
strcpy(_name, name);
_age = age;
}
Person(const Person &p)
{
_name = (char *)malloc(strlen(p._name) + 1);
strcpy(_name, p._name);
_age = p._age;
}
~Person()
{
if (_name != NULL)
{
cout << "Person 析构调用" << endl;
free(_name);
_name = NULL;
}
}
char *_name;
int _age;
};
int main()
{
Person p1("迪马西亚之力", 56);
cout << "姓名: " << p1._name << " 年龄: " << p1._age << endl;
Person p2("古拉拉黑暗之神", 48);
cout << "姓名: " << p2._name << " 年龄: " << p2._age << endl;
system("pause");
return 0;
}
2.4 构造函数的初始化列表
在前面的构造函数中,我们是直接传参数,在函数体内进行赋值构造。但是C++中还提供了另一种初始化的方法,也就是初始化列表。假设有一个Person
类,类中有属性string _name
、unsigned int _age
,则此时用初始化列表写一个只含姓名的构造函数就是Person(string name):_name(name){}
,如果我们要用姓名和年龄同时写一个构造函数则应该是Person(string name, unsigned int age): _name(name), _age(age) {}
。所以我们可以的出来初始化列表的语法为:构造函数名(形参类型1 形参1, 形参类型2 形参2, ...) : 属性1(形参1), 属性2(形参2) ... {}
。关于初始化列表的使用案例如下所示:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person
{
public:
//Person(int a, int b, int c)
//{
// _a = a;
// _b = b;
// _c = c;
//}
// 构造函数名称后: 属性(值), 属性(值), ...
Person(int a, int b, int c) :_a(a), _b(b), _c(c){}
int _a;
int _b;
int _c;
};
int main()
{
Person p(10, 20, 30);
cout << "p._a = " << p._a << endl;
cout << "p._b = " << p._b << endl;
cout << "p._c = " << p._c << endl;
system("pause");
return 0;
}
2.5 类对象作为类成员
除了普通的变量可以作为类成员之外,一个类的对象也可以作为另一个类的成员。在类里面使用其他类的对象作为成员的时候,构造函数调用首先是调用其他类的构造函数,然后再调用本类的构造函数。而在析构函数的调用正好与此相反,是本类的析构函数先调用,再调用其他类的析构函数。关于类对象作为类成员的示例代码如下所示:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include <string>
class Phone
{
public:
Phone(string phoneName) :_phoneName(phoneName)
{
cout << "Phone 构造函数调用" << endl;
}
~Phone()
{
cout << "Phone 析构函数调用" << endl;
}
string _phoneName;
};
class Game
{
public:
Game(string gameName) :_gameName(gameName)
{
cout << "Game 构造函数调用" << endl;
}
~Game()
{
cout << "Game 析构函数调用" << endl;
}
string _gameName;
};
class Person
{
public:
Person(string name, string phone, string game) :_name(name), _phone(phone), _game(game)
{
cout << "Person 构造函数调用" << endl;
}
~Person()
{
cout << "Person 析构函数调用" << endl;
}
void playGame()
{
cout << _name << "在使用<<" << _phone._phoneName << ">>打" << _game._gameName << endl;
}
string _name;
Phone _phone;
Game _game;
};
int main()
{
Person p1("弗洛伊德", "HUAWEI P50", "王者荣耀");
p1.playGame();
Person p2("迪杰斯特拉", "HUAWEI Meta50", "原神");
p2.playGame();
system("pause");
return 0;
}
2.7 explicit关键字的使用
在有些时候,我们使用隐式转换的方法去实例化一个类会让人读起来产生歧义。如有一个字符串类MyString
,其中有一个构造方法为MyString(int len);
,而用户在实例化对象的时候使用MyString ms = 10;
,此时就会让用户产生一种歧义,究竟是长度为10还是说该字符串就是10。因此我们可以使用explicit
关键字不让用户使用该构造函数的隐式转换调用方法。示例代码如下所示:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class MyString
{
public:
MyString(char *)
{
}
// explicit防止利用隐式类型转换的方式来构造对象
explicit MyString(int len)
{
}
};
int main()
{
MyString str1 = "hello world";
MyString str2 = MyString(10);
//MyString str3 = 100;
system("pause");
return 0;
}
2.8 new和delete的使用
delete
和new
都是两个操作符,而不是关键字或者库函数。而前面C语言中我们学的malloc
和free
则是两个库函数。delete
的作用是用于释放对象的,会自动调用对象的析构函数。而前面学的malloc
则不会调用构造函数,而free
也不会调用析构函数。当然在这里也有一个区别是用malloc
开辟的内存区域返回值是void *
类型,在C++中如果需要赋值的话只有同类型才能相互赋值,所以必须要强制类型转换,比较麻烦。而new
返回创建对象的指针。void *
类型也不能调用析构函数,因此我们不用void *
类型的指针去接收new
出来的对象。
除了创建对象之外,我们也可以用new
来开辟数组,其语法是数据类型 *数组名 = new 数据类型[长度];
,这样就开辟了一个堆区的数组,值得注意的是释放的时候一定要加上[]
,也就是delete [] 数组名
。在栈上也可以开辟数组,使用方法和普通数组的定义一样,也就是数据类型 数组名[长度];
。关于new
和delete
的示例代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person
{
public:
Person()
{
cout << "Person 构造函数调用" << endl;
}
Person(int a)
{
cout << "Person 有参构造函数调用" << endl;
}
~Person()
{
cout << "Person 析构函数调用" << endl;
}
};
// malloc 和 free 区别
// malloc 和 free 属于库函数 new 和 delete 属于运算符
// malloc 不会调用构造函数 new会调用构造函数
// malloc 的返回值为void *类型,在C++下需要强转, new返回创建的对象的指针
void test01()
{
Person *p = new Person;
delete p;
}
// 注意事项 不要用void *去接受new出来的对象,利用void *无法调用解析函数
void test02()
{
void *p = new Person;
delete (Person *)p;
}
// 利用new开辟数组
void test03()
{
//int *intArray = new int[10];
//double *doubleArray = new double[10];
// 堆区开辟数组 一定会调用默认构造函数
Person *personArray = new Person[10];
// 释放数组的时候,需要加[]
delete [] personArray;
}
int main()
{
//test01();
//test02();
test03();
system("pause");
return 0;
}