当前位置: 首页 > news >正文

C++-第三章:类和对象

目录

第一节:成员的访问权限

        1-1.public

        1-2.private

        1-3.protect

第二节:成员函数声明与定义分离

第三节:封装和this指针

        3-1.封装

        3-2.this指针

        3-3.附加

第四节:类的默认成员函数

        4-1.默认构造函数

                4-1-1.默认初始化函数

        4-1-2.默认拷贝函数

        4-2.默认析构函数

                4-3.默认拷贝赋值运算符函数

下期预告:


第一节:成员的访问权限

        一般情况下,一个类由成员变量和成员函数组成(如果有),为了实现对类中某些重要数据的保护,类中成员变量的访问和修改,成员函数的调用都可以设置public、private、protect三种权限。

        1-1.public

        public为公有权限,它修饰的成员变量允许在任何地方被修改,它修饰的成员函数允许在任何地方被调用:

#include <iostream>
class Test
{
public: // 修饰它下面的成员,直到遇到另一个访问限定符为止int a = 18;int Add(int x,int y){return x + y;}
};
int main()
{Test t;std::cout << t.a << " " << t.Add(1, 2) << std::endl;return 0;
}

 

        

        1-2.private

        private私有权限,它修饰的成员变量只允许在自己的成员函数内部被访问和修改,它修饰的成员函数也只允许被自己的成员函数调用:

        使用private修饰后再在外部访问成员 a 和 Add 就会报错,如果此时要在不改变权限的情况下得到a的值、修改a的值和使用Add函数,可以在类中定义一些public函数来修改、调用它们:

class Test
{
private: // 修饰它下面的成员,直到遇到另一个访问限定符为止int a = 18;int Add(int x,int y){return x + y;}
public:int Get_a() // 得到a的值{return a;}void Mod_a(int x) // 修改a的值{a = x;}int Call_Add(int x,int y) //调用 Add 函数{return Add(x,y);}
};

         因为上述3个public函数都在类的内部,即使a和Add用private修饰了也是能访问到的,此时就可以经由这3个public函数之手操作它的private成员:

int main()
{Test t;std::cout << t.Get_a() << " " << t.Call_Add(1, 2) << std::endl;return 0;
}

  

        1-3.protect

        protect保护权限,它的权限和private一模一样,只在类的继承中有意义,类的继承以后会讲到。

        注意:class和struct都有默认权限,class的默认权限是private,struct的默认权限是public,这是它们唯一的区别。

第二节:成员函数声明与定义分离

        成员函数也支持声明与定义分离,声明放在类中,定义需要指明类域:

class Test
{
public:int a = 18;int Add(int x, int y); // 声明
};
int Test::Add(int x, int y) // 定义,需要指明类域
{return x + y;
}

        定义中的变量会有匹配优先级:局部域>类域>全局域,类域就是实例自己的空间,Test类中有一个变量a,假如在函数中也定义一个同名变量a,根据优先级它就会优先匹配新定义的a:

#include <iostream>
class Test
{
public:int a = 18;int Add(int x, int y); // 声明
};
int Test::Add(int x, int y) // 定义,需要指明类域
{int a = 1; // 局部域的astd::cout << "a=" << a << std::endl;return x + y;
}
int main()
{Test t;t.a = 0; // 初始化类域中的at.Add(1, 2);return 0;
}

 

       如果局部域没有同名变量,它就会使用类域中的变量:

int Test::Add(int x, int y) // 定义,需要指明类域
{//int a = 1; // 局部域的astd::cout << "a=" << a << std::endl;return x + y;
}

 

        如果要使用全局域的变量,可以在变量前加::,表示属于全局域的变量:

int a = 2; // 全局域的a
class Test
{
public:int a = 18;int Add(int x, int y); // 声明
};
int Test::Add(int x, int y) // 定义,需要指明类域
{//int a = 1; // 局部域的astd::cout << "a=" << ::a << std::endl; // a前加::,在全局域中找变量areturn x + y;
}

 

        那么如果局部域中有类域中的同名变量,那么要怎么访问类域中的变量呢,接下来讲了类的封装和this指针问题就迎刃而解了。

        

第三节:封装和this指针

        3-1.封装

        封装是面向对象的三大特性之一,它实际上是对数据的一种管控:

        (1)将数据和函数都放在类中

        (2)用访问限定符对成员变量和函数进行限定

        例如第一节对private成员进行使用和修改的那3个public函数就是一种封装,因为我们只能通过这个类提供的函数来合法的操作里面的数据,就是对数据的一种管控和保护。

        另外,封装还能屏蔽底层细节,就是一个函数可以调用一个或者多个函数。

        例如如果我们需要得到 x*y+z 的值,我们就可以把它分成乘、加两个子任务,就可以写好完成乘的Mul函数和完成加的Add函数,然后设置成private函数由 Mul_Add 函数统一调用即可:

class Test
{
private:int Add(int x, int y){return x + y;}int Mul(int x, int y){return x * y;}
public:int a = 18;int Mul_Add(int x,int y,int z){return Add(Mul(x, y), z);}
};
int main()
{Test t;std::cout << t.Mul_Add(1,2,3);return 0;
}

 

        上述代码我们在外部只调用了Mul_Add函数,让它自己调用Mul和Add函数,这种也叫封装,它的作用是屏蔽底层代码细节,使用某一个功能时更方便;

        且对于复杂的需求,就可以把它分成几个子任务,单独完成后再统一调用,这样的代码不仅可读性强、解耦性好、还易于维护。

        3-2.this指针

        之前我们说过类的函数是统一存放在公共区域的,每个实例调用的都是同一个函数,那么请看如下代码预测结果:

#include <iostream>
class Test
{
public:int a;void Print(){std::cout << a << std::endl;}
};
int main()
{Test t1;Test t2;t1.a = 0;t2.a = 1;t1.Print();t2.Print();return 0;
}

 

        我们发现实例调用函数时,打印的是自己的变量a的值,但是调用的不都是公共区域的函数吗,为什么结果却不一样呢?

        这是因为类的函数有一个隐含的参数——this指针,我们可以显示的把它写出来,即:

void Print(Test* this) // 显示的写出this指针
{std::cout << a << std::endl;
}

        this的类型就是函数所在类的类型的指针,在具体的实例调用函数时,除了本来的参数,还会自动的把自己的地址也传给这个参数。

        上述语句块的a,实际上也被省略了前缀,完整的是:this->a,访问的是具体实例中的变量,所以不同实例调用同一个函数却能打印出自己的变量的值。

t1.Add() 等价于 t1.Add(&t1)

        类的实例与成员函数的关系如下图:

        3-3.附加

        一个类的成员函数因为存放在一个公共区域,所以函数不计算实例和类的大小,而如果一个类或者实例没有成员变量,它的大小也不会是0,在vs2022中是1,因为没有体积的类是无意义的,占用1字节来表示该类是存在的。

        其次,如果一个类有成员变量,它的大小计算规则与C语言的结构体相同。

        对于类来说,成员变量前一般加_ 符号,让它与函数的参数分开,这是一种优良的代码习惯。

        

第四节:类的默认成员函数

        默认成员函数就是定义一个类时会默认生成的函数,你不实现,它就存在于类中。

        但你一旦实现,就不会生成。

        4-1.默认构造函数

        默认构造函数具有以下3个特征:

        1、函数名与类型相同。

        2、无返回值,无返回值指函数连返回类型都不需要写出来,而不是返回void。

        3、类在实例化时自动调用对应的构造函数。

        默认构造函数有默认初始化函数和默认拷贝构造函数,如果不实现任何构造函数,编译器会默认生成一个默认初始化函数+一个默认拷贝函数,只要实现了其中任何一个,两个默认函数都不会生成。

        默认构造函数或者构造函数都不是开辟空间,而是初始化实例中的各种成员变量。

                4-1-1.默认初始化函数

       默认初始化函数的"样子"如下:

class Test
{
public:Test(){// 语句;}
};

        实际上默认成员函数是不能看见的,写出来只是帮助理解,如果像上述代码中这样写出来的话默认构造函数就不会生成了。

        初始化函数的调用时机是固定的,都是在创建一个新对象时调用:

class Test
{
public:int a;void Print(){std::cout << a << std::endl;}
};
int main()
{Test t1; // 调用默认构造Test t2; // 调用默认构造return 0;
}

        看起来好像只是创建了实例,而没有调用任何函数,这是因为默认的初始化函数只作一个功能,且默认初始化函数就是不需要传任何参数:1、对于内置类型成员不作任何处理;2、对于自定义类型成员调用它的默认初始化函数。

        内置类型就是int、char等语言自带的类型,自定义类型就是类和结构体类型。

        实际上,对于一个类一般都会自己实现一个初始化函数,例如一个student类,我们想在创建实例时就确定它的学号、年龄:

#include <string>
class student
{
public:student(const char* name,int age){_name = name;_age = age;}std::string _name;int _age;
};
int main()
{student Eric("Eric", 18); // 调用初始化函数初始化内置类型return 0;
}

        初始化函数还允许重载,以适应不同的初始化需求:

#include <string>
class student
{
public:student(const char* name,int age){_name = name;_age = age;}student(const char* name) // 仅初始化名字{_name = name;}student(int age) // 仅初始化年龄{_age = age;}std::string _name;int _age;
};
int main()
{student Eric("Eric", 18); // 调用初始化函数初始化内置类型student Bob("Bob"); // 调用初始化函数初始化内置类型student Jack(18); // 调用初始化函数初始化内置类型return 0;
}

        4-1-2.默认拷贝函数

        默认拷贝函数也属于默认构造函数,它的功能是可以用一个实例来初始化另一个实例,:

#include <iostream>
#include <string>
class student
{
public:std::string _name;int _age;
};
int main()
{student Bob;Bob._name = "Bob";Bob._age = 20;student Bob_copy(Bob); // 调用拷贝函数初始化内置类型std::cout << Bob_copy._name << " " << Bob_copy._age << std::endl;return 0;
}

 

        默认拷贝构造会对内置类型作值拷贝,对于自定义类型会调用它的拷贝构造函数。

        因为上述代码中Bob_copy是用Bob初始化的,所以它们的内容是一样的。

        拷贝构造函数也可以重载,一般用引用+const接收实例:

        但是当我实现了拷贝构造之后,就不能创建实例了,这是因为拷贝构造函数也属于构造函数,实现了图中构造函数之后默认初始化函数也不会自动生成了,所以需要自己再写一个初始化函数:

#include <iostream>
#include <string>
class student
{
public:student() // 无参的初始化函数,不作任何处理{}student(const student& d) // 拷贝构造{_name = d._name;_age = d._age;}std::string _name;int _age;
};
int main()
{student Bob;Bob._name = "Bob";Bob._age = 20;student Bob_copy = Bob; // 拷贝函数初始化内置类型std::cout << Bob_copy._name << " " << Bob_copy._age << std::endl;return 0;
}

 

        当然自己写的拷贝构造函数也不用拷贝所有的内置类型,可以只拷贝年龄等,可以按具体的需求写拷贝构造函数的语句项,非常灵活。

        拷贝构造除了用()调用,还可以用 = 来调用:

int main()
{student Bob;Bob._name = "Bob";Bob._age = 20;student Bob_copy = Bob; // 用=调用拷贝函数初始化内置类型std::cout << Bob_copy._name << " " << Bob_copy._age << std::endl;return 0;
}

 

        所以在对类进行传值调用时,形参也是实参的一份拷贝,即:形参 = 实参,所以会调用一次拷贝构造:

void func(student x)
{return;
}
int main()
{student Bob;func(Bob);return 0;
}

  

        这次拷贝构造就是在传参:student x = Bob 时调用的。

        总结:当不实现任何构造函数时(初始化函数、拷贝函数),编译器会默认生成一个默认初始化函数+一个默认拷贝函数,只要实现了其中任何一个,编译器两个默认函数都不会生成。

        默认初始化函数是不需要传参(无参数或者全缺省)的函数,对内置类型不作处理,对自定义类型调用它的初始化函数;

        默认拷贝函数是带两个参数(隐含的this指针+一个实例)的函数,对内置类型作值拷贝,对自定义类型调用它的拷贝函数。

        4-2.默认析构函数

        析构函数的作用是在实例的生命周期终结的时候自动调用,它的功能不是销毁类而是清理类中的资源,它具有以下特征:

        1、函数名是类名前加"~"

        2、无参数;无返回值

        3、一个类只能有一个析构函数,未显示声明系统会生成一个,所以析构函数不能重载

        4、实例的生命周期结束时会自动调用

        5、默认析构函数对内置类型不作处理,对自定类型会调用它的析构函数

        析构函数也可以显示的写出来,一个析构函数的"样子"如下:

class Test
{
public:~Test(){// 语句;}
};

        如果实例都在栈上,而栈具有"先进后出"的特性,所以在程序结束时,先定义的实例反而后销毁:

#include <iostream>
#include <string>
class student
{
public:student(const char* name,int age) // 初始化函数{_name = name;_age = age;}student(const student& d) // 拷贝构造,但是不使用引用{std::cout << "调用了一次拷贝构造" << std::endl; // 打印提示信息_name = d._name;_age = d._age;}~student() // 析构函数{std::cout << _name << "被销毁了" << std::endl;}std::string _name;int _age;
};
void func(student x)
{return;
}
int main()
{student Bob("Bob", 20);student Eric("Eric",18);return 0;
}

  

        不同区域的类的销毁顺序如下:

        局部变量->局部静态变量->全局静态变量和全局变量。

        4-3.默认拷贝赋值运算符函数

        拷贝赋值运算符函数与拷贝函数不同,一个在实例定义之后使用,一个在实例定义时使用,讲拷贝赋值运算符之前,我们先看看它是怎么使用的:

int main()
{student Bob("Bob", 20);student Eric("Eric",18);Eric = Bob; // 拷贝赋值运算符return 0;
}

  

        虽然这好像是用了赋值符号=,而没有调用函数,实际上对于自定义类型这就是一种函数调用的方式,这和operator关键字有关,下一章我们就会讲。

        就和内置类型的赋值一样,Bob将自己的数据拷贝了一份给Eric,所以程序结束时有两个"Bob"。

        默认拷贝赋值函数会做的工作如下:

        对内置类型进行只拷贝,对自定义类型调用它的拷贝赋值函数。

        它具有以下特点:

        1、一般一个类只有一个拷贝赋值函数

        2、如果不实现拷贝赋值函数,编译器就是生成一个默认拷贝赋值函数

        关于如何实现一个拷贝赋值函数,这和operator关键字有关,下一章讲详细叙述。

                

下期预告:

        下一期还是默认成员函数的内容:

        1、operator关键字

        2、取地址函数重载

        3、初始化列表

        4、类的隐式类型转换        

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 使用vite+react+ts+Ant Design开发后台管理项目(三)
  • 我的AI工具箱Tauri版-VideoIntroductionClipCut视频介绍混剪
  • ubuntu24.04 怎么调整swap分区的大小,调整为16G
  • TLC/TK Adv学习笔记1 - Py版本+美化
  • PTA L1-062 幸运彩票
  • One-Class Classification: A Survey
  • 猫头虎分享:Python库 Falcon 的简介、安装、用法详解入门教程
  • 网络通信——OSI七层模型和TCP/IP模型
  • 黑马智数Day5
  • contenteditable=“true“可编辑div字数限制
  • JVM —— 类加载器的分类,双亲委派机制
  • Ubuntu中交叉编译armdillo库
  • PostgreSQL主备环境配置
  • SpringBoot 整合 Easy_Trans 实现翻译的具体介绍
  • 人工智能有助于解决 IT/OT 集成安全挑战
  • $translatePartialLoader加载失败及解决方式
  • 【跃迁之路】【444天】程序员高效学习方法论探索系列(实验阶段201-2018.04.25)...
  • 【跃迁之路】【735天】程序员高效学习方法论探索系列(实验阶段492-2019.2.25)...
  • CAP 一致性协议及应用解析
  • create-react-app做的留言板
  • Electron入门介绍
  • Essential Studio for ASP.NET Web Forms 2017 v2,新增自定义树形网格工具栏
  • gulp 教程
  • Java 最常见的 200+ 面试题:面试必备
  • Making An Indicator With Pure CSS
  • SQLServer之创建显式事务
  • V4L2视频输入框架概述
  • Vue--数据传输
  • 百度贴吧爬虫node+vue baidu_tieba_crawler
  • 经典排序算法及其 Java 实现
  • 利用阿里云 OSS 搭建私有 Docker 仓库
  • 猫头鹰的深夜翻译:Java 2D Graphics, 简单的仿射变换
  • 普通函数和构造函数的区别
  • 入手阿里云新服务器的部署NODE
  • 想晋级高级工程师只知道表面是不够的!Git内部原理介绍
  • 项目实战-Api的解决方案
  • 优秀架构师必须掌握的架构思维
  • ​​​​​​​​​​​​​​汽车网络信息安全分析方法论
  • # 睡眠3秒_床上这样睡觉的人,睡眠质量多半不好
  • #define用法
  • (+4)2.2UML建模图
  • (1)SpringCloud 整合Python
  • (11)工业界推荐系统-小红书推荐场景及内部实践【粗排三塔模型】
  • (7)svelte 教程: Props(属性)
  • (android 地图实战开发)3 在地图上显示当前位置和自定义银行位置
  • (MATLAB)第五章-矩阵运算
  • (pojstep1.1.2)2654(直叙式模拟)
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (Redis使用系列) Springboot 实现Redis 同数据源动态切换db 八
  • (层次遍历)104. 二叉树的最大深度
  • (附源码)springboot猪场管理系统 毕业设计 160901
  • (十六)一篇文章学会Java的常用API
  • (转)visual stdio 书签功能介绍
  • (转)大道至简,职场上做人做事做管理
  • .NET 6 在已知拓扑路径的情况下使用 Dijkstra,A*算法搜索最短路径