深入计算机语言之C++:类与对象(上)

简介: 深入计算机语言之C++:类与对象(上)

前面我们学习了关于c++语言的一些基础知识,今天我们来正式进入c++语言的学习


一、类的定义

1.1 类的定义

C++是一门面向对象的语言,理解c++,首先要理解类和对象。


C++中的类可以看成c语言中的结构体的升级版,结构体是一种构造类型,可以包含若干个成员变量,成员变量的类型可以不同。而由类声明定义的变量,我们称为对象。


C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化是 struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类。

在类里面定义的成员函数默认为内联函数(inline)


1.2 类的语法规格式

class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后⾯分号不能省 略。

class Stack 
{
public:
 
    void Init(int n = 4) 
    {
        array = (int*)malloc(sizeof(int) * n);
        if (nullptr == array)
        {
            perror("malloc申请空间失败");
            return; 
        }
        capacity = n;
        top = 0; 
    }
 
    int* array;
    size_t capacity;
    size_t top;
};//分号不能省略
 
int main()
{
    Stack st;
    st.Init();
 
    return 0;
}


1.3 成员变量的命名

虽然C++标准并没有规定成员变量的命名规则,但是大家约定俗成地在定义变量时会有一套特定的规则,目的就是解决可能存在的命名冲突的问题,比如说下面这段代码:

class Date
{
  void Init(int year, int month, int day)
  {
    //命名冲突
    year = year;
    month = month;
    day = day;
  }
  int year;
  int month;
  int day;
};


这时为了解决问题这类问题,我们在定义成员变量时会对其进行特定地修饰。比如说 _变量名 ,m_变量名 和变量名_ 等等不同的修饰

class Date
{
  void Init(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  int _year;
  int _month;
  int _day;
};


二、限定访问符

在C++类中有三种访问限定符:**public,private,protected。**他们每一个都有自己独特的作用:


  • public:公有属性,修饰的成员在类内外都可以直接被访问。
  • protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
  • private:私有属性
  • protected:保护属性
  • 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。如果后面没有访问限定符,作用域就到 } 即类结束。


那在C++中struct 和class有什么区别呢?


struct :内部默认是公有权限,结构体外部可以访问其内部成员


class:内部默认是私有权限,类的外部不能直接访问内部成员;可以手动声明为共有权限;


三、类域

类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤ :: 作 ⽤域操作符指明成员属于哪个类域。

#include<iostream>
using namespace std;
 
class Stack
{
public:
 
    void Init(int n = 4);
 
    int* array;
    size_t capacity;
    size_t top;
};
 
void Stack::Init(int n) {
    array = (int*)malloc(sizeof(int) * n);
    if (nullptr == array)
    {
        perror("malloc申请空间失败");
        return;
    }
 
    capacity = n;
    top = 0;
}
 
int main()
{
    Stack st;
    st.Init();
 
    return 0;
}


四、类对象模型

4.1 类对象的实例化

⽤类类型在物理内存中创建对象的过程,称为类实例化出对象。

在类中的成员变量实际是一种声明,相当于一个设计图纸。而我们利用类名定义的对象就是类对象的实例化,相当于通过设计图纸实际创建出来。单独的类是并不占据实际空间的大小。


⼀个类可以实例化出多个对象,实例化出的对象 占⽤实际的物理空间,存储类成员变量。打个比⽅:类实例化出对象就像现实中使⽤建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多少个房间,房间⼤⼩功能等,但是并没有实体的建筑存在,也不能住⼈,⽤设计图修建出房⼦,房⼦才能住⼈。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。


4.2 类对象的存储

我们知道了类对象的创建,那么具体类中的成员变量与成员函数又是如何存储的呢?

类实例化出的每个对象,都有独⽴的数据空间,所以对象中肯定包含 成员变量,那么成员函数是否包含呢?

  1. 每次创建对象时,都开辟一个空间存储类成员变量与成员函数。
  2. 每次创建对象时,都开辟一个空间存储类成员变量与成员函数的地址。
  3. 每次创建对象时,都开辟一个空间存储类成员变量。而成员函数提前单独存储一个区域
  • ⾸先函数被编译后是⼀段指令,对象中没办法存储,这些指令存储在⼀个单独的区域(代码段),而且每次对象的实例化都会开辟函数的空间。而每个函数的功能都是一样的,这就造成了空间的浪费。那么对象中⾮要存储的话,只能是成员函数的指针。
  • 再分析⼀下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各⾃独⽴的成员变量_year/_month/_day存储各⾃的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的,存储在对象中就浪费了,这⾥也就没有必要其。
  • 函数指针是⼀个地址,调⽤函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找。


除此之外我们还可以通过计算类的大小来判断是以哪种形式存在的

4.3 类对象的大小

4.3.1 一般类的计算

类型对象的大小我们可以借助运算符sizeof计算,并且类的大小也遵循结构体内存对齐规则,这部分的内容已经在结构体的部分谈过了,就不再赘述,详情见:深入C语言:探索结构体的奥秘 的第五点

#include<iostream>
using namespace std;
class Date
{
//可以被直接访问
public:
  void Init(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
//不能被直接访问
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d;
  cout << sizeof(d) << endl;
  return 0;
}


根据内存对齐规则,我们知道成员变量的大小就是12,所以证明成员函数是存在于内存的其他位置。所以说存储三正确。一般这个存储成员函数的区域我们称之为公共代码段。


4.3.2 空类的计算

当类中只有成员函数,或者什么都没有时。类的大小又为多少呢?

// 类中仅有成员函数
class A1
{
public:
  void func2() {}
};
// 类中什么都没有---空类
class A2
{
};
int main()
{
  A1 d1;
  A2 d2;
  cout << sizeof(d1) << endl;
  cout << sizeof(d2) << endl;
  return 0;
}


为什么空类的大小为1,而不是0呢?其实并不能难像,因为在我们进行空类的实例化时必须要有空间存储对象的大小。这是编译器就会默认给一个字节大小来标记这个类的对象,实际操作中实用性也很少。


五、this指针

我们先来看一段代码:

#include<iostream>
using namespace std;
class Date
{
public:
  //初始化
  void Init(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year; // 年
  int _month; // 月
  int _day; // 日
};
int main()
{
  Date d1, d2;
  d1.Init(2022, 1, 11);
  d2.Init(2022, 1, 12);
  d1.Print();
  d2.Print();
  return 0;
}



对于上述类,有这样的一个问题:


Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?


那么这⾥就要看到C++给了⼀个隐含的this指针解决这⾥的问题编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个隐藏的当前类类型的指针,叫做this指针,只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。。


void Init(Date* const this, int year, int month, int day)
{
  this->_year = year;
  this->_month = month;
  this->_day = day;
}
d1.Init(&d1,2022, 1, 11);//实际传参

但是注意:我们并不能将隐式传参书写出来,因为这是编译器默认添加的

虽然我们不能在传参的时候书写this指针,但我们在成员函数内部可以使用this指针,使用this指针的时候需要注意一些问题:

  • this指针的类型:类型为const,即成员函数中,不能给this指针赋值。
  • 只能在“成员函数”的内部使用。
  • this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  • this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。


问题:

问题1:

// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class Betty
{
public:
  void Print()
  {
    cout << "Print()" << endl;
  }
private:
  int _a;
};
int main()
{
  Betty* p = nullptr;
  p->Print();
  (*p).Print();
  return 0;
}

为什么程序会正常运行,对空指针解引用不是会发生运行崩溃吗?首先我们得明白成员函数并不存放在类对象中,而是存放在公共代码段。虽然我们表面看上去解引用,但实际上编译器不需要通过解引用去找对应函数,只需要去公共代码区执行对应函数即可。

问题2:

// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class Betty
{
public:
  void Print()
  {
    cout << _a << endl;
  }
private:
  int _a;
};
int main()
{
  Betty* p = nullptr;
  p->Print();
  (*p).Print();
  return 0;
}


这里就引起程序崩溃,因为我们知道访问对应的成员变量,会传递对应对象的地址。cout 需要访问 _a,从而需要对p进行解引用,而这里 p 的地址为 nullptr ,导致了程序的崩溃

相关文章
|
2天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1517 4
|
29天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
5天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
501 19
|
2天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
179 1
|
8天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
21天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
9天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
451 5
|
7天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
314 2
|
23天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
25天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2608 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析