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

相关文章
|
4月前
|
存储 C++
C++语言中指针变量int和取值操作ptr详细说明。
总结起来,在 C++ 中正确理解和运用 int 类型地址及其相关取值、设定等操纵至关重要且基础性强:定义 int 类型 pointer 需加星号;初始化 pointer 需配合 & 取址;读写 pointer 执向之处需配合 * 解引用操纵进行。
483 12
|
8月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
350 12
|
6月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
192 0
|
6月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
294 0
|
9月前
|
存储 算法 安全
企业员工数据泄露防范策略:基于 C++ 语言的布隆过滤器算法剖析[如何防止员工泄密]
企业运营过程中,防范员工泄密是信息安全领域的核心议题。员工泄密可能致使企业核心数据、商业机密等关键资产的流失,进而给企业造成严重损失。为应对这一挑战,借助恰当的数据结构与算法成为强化信息防护的有效路径。本文专注于 C++ 语言中的布隆过滤器算法,深入探究其在防范员工泄密场景中的应用。
225 8
|
9月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
193 16
|
9月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
9月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
9月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
10月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。