深入计算机语言之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 ,导致了程序的崩溃

相关文章
|
18天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
28 2
|
24天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
55 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
59 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
71 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
29 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
23 1
|
2月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
17 0
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)