太原的网站搭建公司,wordpress 很占内存,广州网站优化建设,济南网站建设团队一、类的概述
1.类的引入
类的封装#xff1a;将数据和方法封装在一起#xff0c;加以权限区分#xff0c;用户只能通过公共方法访问私有数据。
为什么要将数据和方法封装在一起呢#xff0c;而且还要通过公共方法才能访问私有数据#xff1f;
C语言中数据和方法分开可…一、类的概述
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 183.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 -1int 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无参、有参、拷贝构造总结
如果用户提供了有参构造或拷贝构造会屏蔽默认的无参构造但用户提供有参构造或无参构造不会屏蔽默认拷贝构造只有用户提供拷贝构造才会屏蔽默认拷贝构造如果类中有指针成员且指向堆区必须实现析构函数和拷贝构造函数深拷贝构造函数实例化对象自动调用先开辟空间后调用构造函数可以重载析构函数对象结束的时候自动调用先调用析构函数后释放对象自身空间不能重载。